0%

Linux-Shell

阅读更多

1 Basics

1.1 I/O Redirectio

命令(可以加空格地方都加了空格,否则就是不能加空格) 说明
[command] 0< [file] 重定向命令的stdin到文件file,其中0可以省略
[command] 1> [file] 重定向命令的stdout到文件file,其中1可以省略
[command] 1>> [file] 重定向并追加命令的stdout到文件file,其中1可以省略
[command] 2> [file] 重定向命令的stderr到文件file
[command] 2>> [file] 重定向并追加命令的stderr到文件file
&> [file] 重定向stdoutstderr到文件file
&>> [file] 重定向并追加stdoutstderr到文件file
[fd_m]> [file] 重定向文件描述符fd_m到文件filefd_m可以省略,默认为1
[fd_m]>&[fd_n] 重定向文件描述符fd_m到文件描述符fd_nfd_m可以省略,默认为1
[fd_m]>>&[fd_n] 重定向并追加文件描述符fd_m到文件描述符fd_nfd_m可以省略,默认为1
[fd_m]<> [file] 把文件file打开, 并且将文件描述符fd_m分配给它,fd_m可以省略,默认为0
0<&- 关闭stdin,其中0可以省略
[fd_in]<&- 关闭输入文件描述符fd_in
1>&- 关闭stdout,其中1可以省略
[fd_out]>&- 关闭输出文件描述符fd_out
<< tag 将开始标记 tag 和结束标记 tag 之间的内容作为输入

注意:

  • 0stdin的文件描述符
  • 1stdout的文件描述符
  • 2stderr的文件描述符
  • &-:用于关闭文件描述符

1.1.1 Here Document

Here DocumentShell中的一种特殊的重定向方式,用来将输入重定向到一个交互式Shell脚本或程序

它的基本的形式如下

1
2
3
command << delimiter
document
delimiter

例如

1
2
3
4
5
6
wc -l << EOF
欢迎来到
菜鸟教程
www.runoob.com
EOF
3 # 输出结果为 3 行

1.1.2 Here String

A here string is a feature in Unix-like operating systems that allows you to pass a single string as input to a command, without the need for command substitution or creating a separate file. It’s a convenient way to provide input to a command that expects data from standard input.

In Bash and other Unix-like shells, the syntax for a here string is <<<. You use it like this:

1
command <<< "your_string_here"

For example, if you have a command grep 'pattern' that searches for a specific pattern in text, you can use a here string to provide the text directly as input without the need to create a separate file. Here’s how it would look:

1
2
text="This is some example text containing the pattern"
grep 'pattern' <<< "$text"

The here string is particularly useful when you want to avoid the overhead of creating temporary files for passing small amounts of input data to a command.

1.1.3 How to capture stderr while discarding stdout

1
error_msg=$(cmd 2>&1 >/dev/null)
  • 2>&1: redirects stderr (file descriptor 2) to where stdout (file descriptor 1) is currently pointing, which means the terminal for this example.
  • >/dev/null: redirects stdout (file descriptor 1) to /dev/null.

1.2 Pipe

管道命令使用的是|这个界定符号,这个管道命令|仅能处理有前面一个命令传来的正确信息,也就是standard output的信息,对于standard error并没有直接处理的能力

每个管道后面接的第一个数据必须是命令,而且这个命令必须能够接受standard input的数据才行,例如less,more,head,tail等都可以接受standard input的管道命令

管道是作为子进程的方式来运行的。因此在管道中进行一些变量赋值操作,在管道结束后会丢失

1.3 Command Substitution

命令替换(command substitution)可以让括号里单引号里的命令提前于整个命令运行,然后将执行结果插入在命令替换符号处。由于命令替换的结果经常交给外部命令,不应该让结果有换行的行为,所以默认将所有的换行符替换为了空格(实际上所有的空白符都被压缩成了单个空格)

命令替换(command substitution)有如下种形式

  1. `command`
  2. $(command)
  • 同时,上述两种方式也可以被包围在""之中,这样就可以保留命令行执行结果的空白
1
2
3
4
5
echo "what date it is? $(date +%F)"

# 看看下面两个指令的差异
echo "$(ls)"
echo $(ls)

1.4 Process Substitution

进程替换(Process Substitution)的作用有点类似管道,但在实现方式上有所区别,管道是作为子进程的方式来运行的,而进程替换会在/dev/fd/下面产生类似/dev/fd/63,/dev/fd/62这类临时文件,用来传递数据。用法如下:

  1. <(command):用来产生标准输出,借助输入重定向,它的结果可以作为另一个命令的输入
    • cat <(ls)
  2. >(command):用来接收标准输入,借助输出重定向,它可以接收另一个命令的输出结果
    • ls > >(cat)
  • 注意<>(之间不能有空格
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 将 <(ls) 当做临时文件,文件内容是ls的结果,cat这个临时文件
cat <(ls)
# 将 >(cat) 当做临时文件,ls的结果重定向到这个文件,最后这个文件被cat
ls > >(cat)

# 将 <(date) 作为参数传递给cat,会显式日期
cat <(date)
# 将 <(date) 作为参数传递给echo,会显式fd
echo <(date)

# 将多个 <(date) 作为参数传递给cat,会显式日期
cat <(date) <(date) <(date)
# 将多个 <(date) 作为参数传递给echo,会显式fd
echo <(date) <(date) <(date)

# 将 <(date) 重定向到cat的标准输入,会显示日期
cat < <(date)
# 将 <(date) 重定向到echo的标准输入,由于echo不从标准输入读取内容,因此输出空白
echo < <(date)

典型示例,统计文件个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 正确方式
count=0
while read line
do
((count++))
done < <(ls)
echo ${count}

# 错误方式
count=0
ls | while read line
do
# 由于管道是在子进程中执行的,变量的更新在子进程结束后就丢失了
((count++))
echo "count=${count}"
done
echo ${count} # 永远是0

进程替换和管道的差异,Process substitution and pipe

  1. Multiple process inputs:进程替换可以同时处理多路输入,而管道不行。例如:

    1
    diff <(cd /foo/bar/; ls) <(cd /foo/baz; ls)
  2. Preserving stdin:进程替换可以保留标准输入,而管道不行。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    cat > script.sh << 'EOF'
    #/bin/bash
    read LINE
    echo "You said ${LINE}!"
    EOF

    # wrong way
    cat script.sh | bash

    # right way
    bash <(cat script.sh)
  3. Outbound process substitution:进程替换可以将标准输出和标准异常导向不同的程序,而管道不行。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    cat > main.cp << 'EOF'
    #include <iostream>

    int main() {
    std::cout << "This is normal output" << std::endl;
    std::cerr << "This is error output" << std::endl;
    return 0;
    }
    EOF

    gcc -o main -lstdc++ -std=gnu++11 main.cpp

    ./main 1> >(sed -n '/output/s/^.*$/Find stdout/p') 2> >(sed -n '/output/s/^.*$/Find stderr/p')
  4. 管道中对于变量的修改,流程的控制比如continuebreakreturn对于外部都是不生效的,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    function return_inside_pipe() {
    echo "return_inside_pipe"
    content="a\nb\nc"
    for((i=0;i<3;i++))
    do
    echo -e "${content}" | while IFS= read -r item
    do
    echo "item=${item}"
    if [ "${item}" == "b" ]; then
    return
    fi
    done
    done
    }

    function return_inside_substitution() {
    echo "return_inside_substitution"
    content="a\nb\nc"
    for((i=0;i<3;i++))
    do
    while IFS= read -r item
    do
    echo "item=${item}"
    if [ "${item}" == "b" ]; then
    return
    fi
    done < <(echo -e "${content}")
    done
    }

    return_inside_pipe
    return_inside_substitution

1.5 Env

1.5.1 Pass to specific program

1
2
FOO=bar env | grep FOO
env | grep FOO

1.5.2 Pass to all programs of the pipe

1
FOO=bar bash -c 'somecommand someargs | somecommand2'

1.5.3 Pass to all programs of the session

1
2
3
4
export FOO=bar
env | grep FOO
unset FOO
env | grep FOO

1.5.4 Frequently-used env variables

  • SHELL:当前使用的shell
  • TERM
  • LANG:语言
  • Locale Category
    • LC_ALL: Overrides all other individual locale-related variables and sets a specific locale for all aspects of the system.
    • LC_COLLATE: Specifies the collation order used for sorting and comparison of strings.
    • LC_CTYPE: Defines the character classification and case conversion rules.
    • LC_MESSAGES: Sets the language used for system messages and the output of certain programs.
    • LC_MONETARY: Determines the formatting of monetary values (currency symbols, decimal separators, etc.).
    • LC_NUMERIC: Defines the formatting of numeric values (decimal point, thousands separator, etc.).
    • LC_TIME: Specifies the format used for date and time representation.

2 Special Symbols

shell中的特殊符号包括如下几种

  1. #:注释符号(Hashmark[Comments])
    • 在shell文件的行首,作为shebang标记,#!/bin/bash
    • 其他地方作为注释使用,在一行中,#后面的内容并不会被执行
    • 但是用单/双引号包围时,#作为#号字符本身,不具有注释作用
  2. ;:分号,作为多语句的分隔符(Command separator [semicolon])
    • 多个语句要放在同一行的时候,可以使用分号分隔
  3. ;;:连续分号(Terminator [double semicolon])
    • 在使用case选项的时候,作为每个选项的终结符
  4. .:点号(dot command [period])
    • 相当于bash内建命令source
    • 作为文件名的一部分,在文件名的开头
    • 作为目录名,一个点代表当前目录,两个点号代表上层目录(当前目录的父目录)
    • 正则表达式中,点号表示任意一个字符
  5. ":双引号(partial quoting [double quote])
    • 双引号包围的内容可以允许变量扩展,允许转义字符的存在。如果字符串内出现双引号本身,需要转义。因此不一定双引号是成对的
  6. ':单引号(full quoting [single quote])
    • 单引号内的禁止变量扩展,不允许转义字符的存在,所有字符均作为字符本身处理(除单引号本身之外)。单引号必须成对出现
  7. ,:逗号(comma operator [comma])
    • 用在连接一连串的数学表达式中,这串数学表达式均被求值,但只有最后一个求值结果被返回。例如echo $[1+2,3+4,5+6]返回11
    • 用于参数替代中,表示首字母小写,如果是两个逗号,则表示全部小写。这个特性在bash version 4的时候被添加的(Mac OS不支持)
      • a="ATest";echo ${a,} 输出aTest
      • a="ATest";echo ${a,,} 输出atest
  8. \:反斜线,反斜杆(escape [backslash])
    • 放在特殊符号之前,转义特殊符号的作用,仅表示特殊符号本身,这在字符串中常用
    • 放在一行指令的最末端,表示紧接着的回车无效(其实也就是转义了Enter),后继新行的输入仍然作为当前指令的一部分
  9. /:斜线,斜杆(Filename path separator [forward slash])
    • 作为路径的分隔符,路径中仅有一个斜杆表示根目录,以斜杆开头的路径表示从根目录开始的路径
    • 在作为运算符的时候,表示除法符号
  10. `:反引号,后引号(Command substitution[backquotes])
    • 命令替换。这个引号包围的为命令,可以执行包围的命令,并将执行的结果赋值给变量
  11. ::冒号(null command [colon])
    • 空命令,这个命令什么都不做,但是有返回值,返回值为0
    • 可做while死循环的条件
    • 在if分支中作为占位符(即某一分支什么都不做的时候)
    • 放在必须要有两元操作的地方作为分隔符
    • 可以作为域分隔符,比如环境变量$PATH中,或者passwd中,都有冒号的作为域分隔符的存在
  12. !:感叹号,取反一个测试结果或退出状态(reverse (or negate) [bang],[exclamation mark])
    • 表示反逻辑,例如!=表示不等于
  13. *:星号(wildcard/arithmetic operator[asterisk])
    • 作为匹配文件名扩展的一个通配符,能自动匹配给定目录下的每一个文件
    • 正则表达式中可以作为字符限定符,表示其前面的匹配规则匹配任意次
    • 算术运算中表示乘法
  14. **:双星号(double asterisk)
    • 算术运算中表示求幂运算
  15. ?:问号(test operator/wildcard[Question mark])
    • 表示条件测试
    • 条件语句(三元运算符)
    • 参数替换表达式中用来测试一个变量是否设置了值,例如echo ${a?},若a已经设定过值,则与echo ${a}相同,否则会出现异常信息parameter null or not set
    • 作为通配符,用于匹配文件名扩展特性中,用于匹配单个字符
    • 正则表达式中,表示匹配其前面规则0次或者1次
  16. $:美元符号(Variable substitution[Dollar sign])
    • 作为变量的前导符,用作变量替换,即引用一个变量的内容
    • 在正则表达式中被定义为行末
    • 特殊变量
      • $*:返回调用脚本文件的所有参数,且将所有参数作为一个整体(字符串)返回

      • $@:返回调用脚本文件的所有参数,且返回的是一个列表,每个参数作为一项。如果原参数是一个包含空格的字符串,那么最好用"$@",这样会保留这个参数中的空格,而不是拆成多个参数

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        cat > test.sh << 'EOF'
        #!/bin/bash

        echo "Using \"\$*\":"
        for a in "$*"; do
        echo $a;
        done

        echo -e "\nUsing \$*:"
        for a in $*; do
        echo $a;
        done

        echo -e "\nUsing \"\$@\":"
        for a in "$@"; do
        echo $a;
        done

        echo -e "\nUsing \$@:"
        for a in $@; do
        echo $a;
        done
        EOF

        bash test.sh one two "three four"
      • $#:表示传递给脚本的参数数量

      • $?:此变量值在使用的时候,返回的是最后一个命令、函数、或脚本的退出状态码值,如果没有错误则是0,如果为非0,则表示在此之前的最后一次执行有错误

      • $$:进程ID变量,这个变量保存了运行当前脚本的进程ID值

  17. ():圆括号(parentheses)
    • 命令组(Command group)。由一组圆括号括起来的命令是命令组,命令组中的命令是在子shell(subshell)中执行。因为是在子shell内运行,因此在括号外面是没有办法获取括号内变量的值,但反过来,命令组内是可以获取到外面的值,这点有点像局部变量和全局变量的关系,在实作中,如果碰到要cd到子目录操作,并在操作完成后要返回到当前目录的时候,可以考虑使用subshell来处理
    • 用于数组初始化
  18. {}:代码块(curly brackets)
    • 这个是匿名函数,但是又与函数不同,在代码块里面的变量在代码块后面仍能访问
  19. []:中括号(brackets)
    • [是bash的内部命令(注意与[[的区别)
    • 作为test用途,不支持正则
    • 在数组的上下文中,表示数组元素的索引,方括号内填上数组元素的位置就能获得对应位置的内容,例如${ary[1]}
    • 在正表达式中,方括号表示该位置可以匹配的字符集范围
  20. [[]]:双中括号(double brackets)
    • [[是 bash 程序语言的关键字(注意与[的区别)
    • 作为test用途。[[]][]支持更多的运算符,比如:&&,||,<,>操作符。同时,支持正则,例如[[ hello = hell? ]]
    • bash把双中括号中的表达式看作一个单独的元素,并返回一个退出状态码
  21. $[...]:表示整数扩展(integer expansion)
  22. (()):双括号(double parentheses)
  23. > >& >> < &< << <>:重定向(redirection)
  24. |:管道
  25. (command)> <(command):进程替换(Process Substitution)
  26. && ||:逻辑操作符
    • 在测试结构中,可以用这两个操作符来进行连接两个逻辑值
  27. &:与号(Run job in background[ampersand])
    • 如果命令后面跟上一个&符号,这个命令将会在后台运行
  28. -:命令中的单个-表示标准输入或输出。具体代表标准输入还是标准输出取决于程序本身
    • cat -:此时,-代表标准输入
    • echo "This is Test" | socat - /tmp/hello.html:此时,-代表标准输出
    • socat - /tmp/hello.html:此时,-代表标准输入

3 Data Type

3.1 Numerical Calculation

3.1.1 $[]

整数扩展,会返回执行后的结果。如果有,分隔,那么只返回最后一个表达式执行的结果

1
2
3
4
5
6
7
8
9
10
echo $[ 1 + 3 ]

a=10;b=5
echo $[ $a + $b ]
echo $[ $a - $b ]
echo $[ $a * $b ] #此时不用对*转义
echo $[ $a / $b ]
echo $[ $a % $b ]

echo $[ 1 + 3, 5 + 6 ]

3.1.2 $(())

$(())有如下两个功能

  1. 数值计算
  2. 进制转换
  • $(())中的变量名称,可于其前面加$符号来替换,也可以不用
1
2
3
4
5
6
7
8
9
10
# 数值计算
a=5;b=7;c=2
echo $((a+b*c))
echo $(($a+$b*$c))

# 进制转换
a=06;b=09;c=02;
echo $(( 10#$a + 10#$b + 10#$c ))
echo $((16#ff))
echo $((8#77))

3.1.3 (())

整数扩展,只计算,不返回值。通常用于重定义变量值,只有赋值语句才能起到重定义变量的作用

1
2
3
4
5
6
7
8
9
a=1
((a+10))
echo $a # 输出1

((a+=10))
echo $a # 输出11

((a++))
echo $a # 输出12

3.1.4 expr

expr是一个用于数值计算的命令,运算符号两边必须加空格,不加空格会原样输出,不会计算

1
2
3
4
5
6
7
8
expr 1 + 3

a=10;b=5
expr $a + $b
expr $a - $b
expr $a \* $b #因为乘号*在shell中有特殊的含义,所以要转义
expr $a / $b #除法取商
expr $a % $b #除法取模

3.1.5 bc

浮点数运算:

1
echo "scale=2; 1/3" | bc

浮点数比较:

1
2
3
4
5
6
7
if [ $(echo "1.2 < 1.212" | bc) -eq 1 ]; then
echo "1.2 < 1.212, ok"
fi

if [ $(echo "1.22 >= 1.21" | bc) -eq 1 ]; then
echo "1.22 >= 1.21, ok"
fi

3.2 String

3.2.1 Concat

1
2
3
your_name="qinjx"
greeting="hello, "${your_name}" \!"
echo ${greeting}

3.2.2 Length

1
2
text="abcdefg"
echo "字符串长度为 ${#text}"

3.2.3 Substring Removal

下面以字符串http://www.aaa.com/123.htm为例,介绍几种不同的截取方式

#:删除左边字符,保留右边字符

  • 其中var是变量名,#是运算符,*是通配符,表示从左边开始删除第一个//号及左边的所有字符。即删除http://,结果是www.aaa.com/123.htm
1
2
var='http://www.aaa.com/123.htm'
echo ${var#*//}

##:删除左边字符,保留右边字符

  • 其中var是变量名,##是运算符,*是通配符,表示从左边开始删除最后(最右边)一个/号及左边的所有字符。即删除http://www.aaa.com/,结果是123.htm
1
2
var='http://www.aaa.com/123.htm'
echo ${var##*/}

%:删除右边字符,保留左边字符

  • 其中var是变量名,%是运算符,*是通配符,表示从从右边开始,删除第一个/号及右边的字符。即删除/123.htm,结果是http://www.aaa.com
1
2
var='http://www.aaa.com/123.htm'
echo ${var%/*}

%%:删除右边字符,保留左边字符

  • 其中var是变量名,%%是运算符,*是通配符,表示从右边开始,删除最后(最左边)一个/号及右边的字符。即删除//www.aaa.com/123.htm,结果是http:
1
2
var='http://www.aaa.com/123.htm'
echo ${var%%/*}

从左边第几个字符开始,从左往右截取若干个字符

  • 其中var是变量名,0表示从左边第1个字符开始,5表示截取5个字符,结果是http:
1
2
var='http://www.aaa.com/123.htm'
echo ${var:0:5}

从左边第几个字符开始,从左往右截取,一直到结束

  • 其中var是变量名,7表示从左边第8个字符开始,结果是www.aaa.com/123.htm
1
2
var='http://www.aaa.com/123.htm'
echo ${var:7}

从右边第几个字符开始,从左往右截取若干个字符

  • 其中var是变量名,0-7表示从右边第7字符开始,3表示截取3个字符,结果是123
1
2
var='http://www.aaa.com/123.htm'
echo ${var:0-7:3}

从右边第几个字符开始,从左往右截取,一直到结束

  • 其中var是变量名,0-7表示从右边第7字符开始,结果是123.htm
1
2
var='http://www.aaa.com/123.htm'
echo ${var:0-7}

3.2.4 Substring Replacement

  • ${variable/pattern/string}: Replaces the first match of pattern with string.
  • ${variable//pattern/string}: Replaces all matches of pattern with string.
  • ${variable/#pattern/string}: If pattern matches the beginning of $variable, it’s replaced with string.
  • ${variable/%pattern/string}: If pattern matches the end of $variable, it’s replaced with string.
1
2
3
4
5
6
7
8
9
ips=( "1.1.1.1" "2.2.2.2" "3.3.3.3" )

echo ${ips[@]//./-}

# % indicates that the replacement should happen at the end of each array element.
echo ${ips[@]/%/:4500}

# # indicates that the replacement should happen at the start of each array element.
echo ${ips[@]/#/address:}

3.2.5 Conditional Assignment

变量为空时,返回默认值

1
2
3
4
echo ${FOO:-val2} # 输出val2
test -z "${FOO}"; echo $? # 输出0
FOO=val1
echo ${FOO:-val2} # 输出val1

变量为空时,将变量设置为默认值,然后返回变量的值

1
2
3
4
echo ${FOO:=val2} # 输出val2
test -z "${FOO}"; echo $? # 输出1
FOO=val1
echo ${FOO:=val2} # 输出val1

变量不为空时,返回默认值

1
2
3
4
FOO=val1
echo ${FOO:+val2} # 输出val2
FOO=
echo ${FOO:+val2} # 输出空白

当变量为空时,输出错误信息并退出

1
echo ${FOO:?error} # 输出error

3.2.6 Read by Line

方式1

1
2
3
4
5
6
7
8
9
10
11
12
13
# 文件名不包含空格的话,可以不要引号
# 文件名若包含空格,且不用引号,则会报错:ambiguous redirect
while read line
do
echo $line
done < "test.txt"

# 或者利用 进程替换

while read line
do
echo $line
done < <(cmd)

方式2

  • 注意,在这种方式下,在while循环内的变量的赋值会丢失,因为管道相当于开启另一个进程
1
2
3
4
STDOUT | while read line
do
echo $line
done

3.2.7 Contains

方式1:利用运算符=~

1
2
3
4
5
6
7
str1="abcd"
str2="bc"
if [[ "${str1}" =~ "${str2}" ]]; then
echo "contains"
else
echo "not contains"
fi

方式2:利用通配符

1
2
3
4
5
6
7
str1="abcd"
str2="bc"
if [[ "${str1}" = *"${str2}"* ]]; then
echo "contains"
else
echo "not contains"
fi

方式3:利用grep

1
2
3
4
5
6
7
8
str1="abcd"
str2="bc"
result=$(echo ${str1} | grep ${str2})
if [ -n "${result}" ]; then
echo "contains"
else
echo "not contains"
fi

3.2.8 trim

3.2.8.1 Method 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function trim() {
local trimmed="$1"

# Strip leading spaces.
while [[ "${trimmed}" = " "* ]]; do
trimmed="${trimmed## }"
done
# Strip trailing spaces.
while [[ "${trimmed}" = *" " ]]; do
trimmed="${trimmed%% }"
done

echo -n "$trimmed"
}

test4="$(trim " two leading")"
test5="$(trim "two trailing ")"
test6="$(trim " two leading and two trailing ")"
test1="$(trim " one leading")"
test2="$(trim "one trailing ")"
test3="$(trim " one leading and one trailing ")"
echo "'$test1', '$test2', '$test3', '$test4', '$test5', '$test6'"

3.2.8.2 Method 2

1
2
3
4
5
6
7
8
9
10
11
12
13
function trim() {
local trimmed="$1"

echo $(echo -n ${trimmed} | xargs)
}

test4="$(trim " two leading")"
test5="$(trim "two trailing ")"
test6="$(trim " two leading and two trailing ")"
test1="$(trim " one leading")"
test2="$(trim "one trailing ")"
test3="$(trim " one leading and one trailing ")"
echo "'$test1', '$test2', '$test3', '$test4', '$test5', '$test6'"

3.2.9 Case Conversion

1
2
echo 'hello' | tr 'a-z' 'A-Z'
echo 'HELLO' | tr 'A-Z' 'a-z'

3.2.10 Extract

1
2
3
4
var='[hello]'
var=${var#\[}
var=${var%\]}
echo ${var}

3.2.11 String with special character

1
2
3
4
5
var="SELECT * FROM t0"
# 会有额外的内容输出
echo ${var}
# 加上双引号即可解决
echo "${var}"

3.2.12 Array to String

1
2
items=( "hello world" "how have you been" )
printf '%s\n' ${items[@]} | tr '\n' ',' | sed 's/,$//g' | paste -sd ',' -

3.2.13 Multi-line Content

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
item="something"
content=$(cat << EOF
This is line 1
This is line 2
This is line 3
This is line 4, item=${item}
EOF
)

echo "${content}"

content=$(cat << 'EOF'
This is line 1
This is line 2
This is line 3
This is line 4, item=${item}
EOF
)

echo "${content}"

3.3 Array

Shell 数组用括号来表示,元素用空格符号分割开,语法格式如下:

1
2
3
4
5
6
7
8
array_name=()
array_name=(value1 ... valuen)

# 或者直接这样定义,注意,不需要${}
array_name[1]=value1
array_name[2]=value2
...
array_name[n]=valuen

3.3.1 Operations

3.3.1.1 Get All Elements

1
2
${array[@]}
${array[*]}

3.3.1.2 Get Length

1
2
${#array[@]}
${#array[*]}

3.3.1.3 Get All Indexes

1
2
${!array[@]}
${!array[*]}

3.3.1.4 Combine Arrays

1
2
3
4
array1=(1 2 3)
array2=(4 5 6)
array3=( ${array1[@]} ${array2[@]} )
echo "array3='${array3[@]}'"

3.3.1.5 Append Elements

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 方法1
array1=()
array1+=( 1 )
array1+=( 2 )
array1+=( 3 )
echo "array1='${array1[@]}'"

# 方法2
array2=()
array2=( ${array2[@]} 1 )
array2=( ${array2[@]} 2 )
array2=( ${array2[@]} 3 )
echo "array2='${array2[@]}'"

# 方法3,针对bash,数组下标从0开始
array3=()
array3[${#array3[@]}]=1
array3[${#array3[@]}]=2
array3[${#array3[@]}]=3
echo "array3='${array3[@]}'"

# 方法4,针对zsh,数组下标从1开始(bash也适用)
array4=()
array4[${#array4[@]}+1]=1
array4[${#array4[@]}+1]=2
array4[${#array4[@]}+1]=3
echo "array4='${array4[@]}'"

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
text='my name is liuye'
array=( $(echo ${text}) )

echo "数组元素为:${array[@]}"
echo "数组元素为:${array[*]}"

echo "数组长度为:${#array[@]}"
echo "数组长度为:${#array[*]}"

echo "数组下标为:${!array[@]}"
echo "数组下标为:${!array[*]}"

for element in ${array[@]}
do
echo $element
done

3.3.2 Start Index

  1. sh/bash:数组下标从0开始
  2. zsh:数组下标从1开始

要始终获得一致的行为,请使用:${array[@]:offset:length}

  • offset:起始索引,sh/bash/zsh都是从0开始
  • length:取多少个元素。若length = 1,那么只取某个元素;若length > 1,那么取的是一个子集
    • 不写length表示直到最后一个元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
array=(1 2 3 4 5 6 7 8 9 10)
# output 1
echo ${array[@]:0:1}

# output 1 2
echo ${array[@]:0:2}

# output 1 2 3
echo ${array[@]:0:3}

# output 4 5 6
echo ${array[@]:3:3}

# output 4 5 6 7 8 9 10
echo ${array[@]:3}

3.3.3 Collection Calculation

假设F1F2是两个数组

F1和F2的交集

1
2
3
F1=( 1 2 3 )
F2=( 2 3 4 )
echo ${F1[@]} ${F2[@]} | tr ' ' '\n' | sort | uniq -d

F1和F2的并集

1
2
3
F1=( 1 2 3 )
F2=( 2 3 4 )
echo ${F1[@]} ${F2[@]} | tr ' ' '\n' | sort | uniq

F1和F2的并集-F1和F2的交集,即差集

1
2
3
F1=( 1 2 3 )
F2=( 2 3 4 )
echo ${F1[@]} ${F2[@]} | tr ' ' '\n' | sort | uniq -u

3.3.4 Iterate Array

1
2
3
4
5
array=(1 2 3 4 5)
for v in ${array[@]}
do
echo ${v}
done
1
2
3
4
5
6
array=(1 2 3 4 5)
for ((i=0;i<${#array[@]};i++))
do
v=${array[@]:i:1}
echo ${v}
done

3.3.4.1 Array Delimiter

如果数组中的内容包含空白,foreach默认会以空格作为分隔符,这就有可能破坏数组元素原有的结构,例如

1
2
3
4
5
6
7
8
9
10
11
12
array=("Hello Foo" "I'm Bar!")
for v in ${array[@]}
do
echo ${v}
done

# this one works fine
for ((i=0;i<${#array[@]};i++))
do
v=${array[@]:i:1}
echo ${v}
done

我们可以通过修改IFS来修改foreach的默认行为

1
2
3
4
5
6
7
8
array=("Hello Foo!" "I'm Bar!")
ifs_bak="${IFS}"
IFS=$'\n'
for v in ${array[@]}
do
echo ${v}
done
IFS="${ifs_bak}"

3.3.5 Echo the array with a specified delimiter

1
2
3
4
5
6
7
# echo not respect IFS
array=(1 2 3 4 5 6)
IFS=/ echo ${array[@]}

# works fine
IFS=/ output=${array[@]}
echo ${output}

3.4 Map

Shell map用括号来表示,元素用空格符号分割开,语法格式如下:

1
2
3
4
5
6
7
8
declare -A map_name=([key1]=value1 ... [keyn]=valuen)

# 或者直接这样定义,注意,不需要${}
declare -A map_name
map_name[key1]=value1
map_name[key2]=value2
...
map_name[keyn]=valuen

属性

  • @*可以获取map中的所有value
1
2
${map[@]}
${map[*]}
  • 获取map长度的方法与获取字符串长度的方法相同,即利用#
1
2
${#map[@]}
${#map[*]}
  • !可以获取map中关键字集合
1
2
${!map[@]}
${!map[*]}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
declare -A map
map['a']=1
map['b']=2
map['c']=3

echo ${map[@]}
echo ${map[*]}

echo ${#map[@]}
echo ${#map[*]}

echo ${!map[@]}
echo ${!map[*]}

for key in ${!map[*]}
do
echo "value=${map[${key}]}"
done

4 Condition

4.1 test

4.1.1 Filetype

关于某个文件名的文件类型判断,如test -e filename

  • -e:该名字对应的文件或目录是否存在
  • -f:该名字对应的文件是否存在
  • -d:该名字对应的目录是否存在
  • -s:该名字对应的文件或目录是否存在且不为空
    • 如果名字对应的是一个已存在的文件,那判断文件内容是否为空,非空返回真
    • 如果名字对应的是一个已存在的目录,那返回真
    • 如果名字不存在,返回假
  • -b:该文件名是否存在且为一个block device设备
  • -c:该文件名是否存在且为一个character device设备
  • -S:该文件名是否存在且为一个Socket文件
  • -p:该文件名是否存在且为以FIFO(pipe)文件
  • -L:该文件名是否存在且为一个链接文件

4.1.2 Permission

关于文件的权限检测,如test -r filename

  • -r:检测该文件名是否存在且具有"可读"的权限
  • -w:检测该文件名是否存在且具有"可写"的权限
  • -x:检测该文件名是否存在且具有"可执行"的权限
  • -u:检测该文件名是否存在且具有"SUID"的属性
  • -g:检测该文件名是否存在且具有"GUID"的属性
  • -k:检测该文件名是否存在且具有"Sticky bit(SBIT)"的属性
  • -s:检测该文件名是否存在且为"非空白文件"

两个文件之间的比较,如test file1 -nt file2

  • -nt:(newer than) 判断file1是否比file2新
  • -ot:(older than) 判断file1是否比file2旧
  • -ef:判断file1与file2是否为同一文件,可用在判断hard link的判断上,主要意义在于判断两个文件是否均指向同一个inode

4.1.3 Number

数值比较,如test n1 -eq n2

  • -eq:两数值相等(equal)
  • -ne:两数值不等(not equal)
  • -gt:n1大于n1(greater than)
  • -lt:n1小于n2(less than)
  • -ge:n1大于等于n2(greater than or equal)
  • -le:n1小于n2(less than or equal)

4.1.4 String

字符串比较,例如test n1 = n2

  • -z:判定字符串是否为空,空返回true
    • []中,变量需要加引号,例如[ -z "${a}" ]
    • [[]]中,变量不需要加引号,例如[[ -z ${a} ]]
  • -n:判断字符串是否为空,非空返回true
    • []中,变量需要加引号,例如[ -n "${a}" ]
    • [[]]中,变量不需要加引号,例如[[ -n ${a} ]]
  • =:判断str1是否等于str2
  • !=:判断str1是否等于str2
  • 字符串变量的引用方式:"${var_name}",即必须加上双引号""

4.1.5 Logical

逻辑与/或/非

  • -a:两个条件同时成立,返回true,如test -r file1 -a -x file2
  • -o:任一条件成立,返回true,如test -r file1 -o -x file2
  • !:反向状态

4.2 []

判断符号[]与判断式test用法基本一致,有以下几条注意事项

  1. 如果要在bash的语法当中使用括号作为shell的判断式时,必须要注意在中括号的两端需要有空格符来分隔
  2. 逻辑与逻辑或,用的是&&||,且需要将条件分开写,如下
    • [ condition1 ] && [ condition2 ]
    • [ condition1 ] || [ condition2 ]
  3. 不可以使用通配符
  4. 仅支持=作为相等比较的运算符,不支持==(取决于shell的实现,bash就支持这两种,但是zsh对语法更严格,仅支持=

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 正确写法
if [ 2 -gt 1 ] && [ 3 -gt 2 ]
then
echo "yes"
else
echo "no"
fi

# 错误写法
if [ 2 -gt 1 && 3 -gt 2 ]
then
echo "yes"
else
echo "no"
fi

# 正确写法
if [ 2 -gt 1 -a 3 -gt 2 ]
then
echo "yes"
else
echo "no"
fi

# 错误写法
if [ 2 -gt 1 ] -a [ 3 -gt 2 ]
then
echo "yes"
else
echo "no"
fi

4.3 [[]]

判断符号[[]]与判断式test用法基本一致,有以下几条注意事项

  1. 如果要在bash的语法当中使用括号作为shell的判断式时,必须要注意在中括号的两端需要有空格符来分隔
  2. 逻辑与逻辑或,用的是&&||,用一个[]或者用两个[[]]都可以,但使用[[]]时不能用-a-o
  3. 可以使用通配符
  4. 支持===作为相等比较的运算符

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 正确写法
if [[ 2 -gt 1 ]] && [[ 3 -gt 2 ]]
then
echo "yes"
else
echo "no"
fi

# 正确写法
if [[ 2 -gt 1 && 3 -gt 2 ]]
then
echo "yes"
else
echo "no"
fi

# 错误写法
if [[ 2 -gt 1 -a 3 -gt 2 ]]
then
echo "yes"
else
echo "no"
fi

# 错误写法
if [[ 2 -gt 1 ]] -a [[ 3 -gt 2 ]]
then
echo "yes"
else
echo "no"
fi

4.4 Command

我们可以用如下方式来判断命令执行是否成功:

1
2
3
if <cmd> [<options>]; then
...
fi

例如:

1
2
3
4
5
6
7
8
if ls ~ > /dev/null; then
echo "success"
fi

output="warning: xxxxx"
if grep -q 'error' <<< ${output} || grep -q 'warning' <<< ${output}; then
echo "error occurs"
fi

5 Control Flow

5.1 if

1
2
3
4
5
6
7
if condition
then
command1
command2
...
commandN
fi

5.2 if else

1
2
3
4
5
6
7
8
9
if condition
then
command1
command2
...
commandN
else
command
fi

5.3 if else-if else

1
2
3
4
5
6
7
8
9
if condition1
then
command1
elif condition2
then
command2
else
commandN
fi

5.4 for

1
2
3
4
5
6
7
for var in item1 item2 ... itemN
do
command1
command2
...
commandN
done
1
2
3
4
5
6
7
for ((i=1;i<=10;i++))
do
command1
command2
...
commandN
done

5.5 while

1
2
3
4
while condition
do
command
done

5.6 until

1
2
3
4
until condition
do
command
done

5.7 case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
casein
模式1)
command1
command2
...
commandN
;;
模式2)
command1
command2
...
commandN
;;
esac

5.7.1 Determine if numerical number

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function isNumber() {
local content=$1
case "${content}" in
[0-9])
echo "'${content}' is number"
;;
[1-9][0-9]*)
echo "'${content}' is number"
;;
*)
echo "'${content}' is not number"
;;
esac
}

for ((i=0; i<100; i++))
do
isNumber ${i}
done

6 Function

6.1 Pass Parameter

传递数组

  1. ""将数组转化成字符串
1
2
3
4
5
6
7
8
9
10
function func() {
# 这里将传入的字符串解析成数组
param=( $(echo $1) )
echo "param=${param[*]}"
}

array=('a', 'b', 'c', 'd', 'e')

# 这里用""将数组转为字符串
func "${array[*]}"

6.2 Standard Output

返回数组

1
2
3
4
5
6
7
8
9
function func() {
# 这里将传入的字符串解析成数组
array=('a' 'b' 'c' 'd' 'e')
echo "${array[*]}"
}

# 这里用""将数组转为字符串
array=( $(func) )
echo ${array[*]}

6.3 Return Value

1
2
3
4
5
6
7
8
9
function func() {
# 这里必须返回非负整数
return 66
}

func

# 获取函数返回值
echo $?

7 builtin

bash shell的命令分为两类:外部命令和内部命令。外部命令是通过系统调用或独立的程序实现的,如sedawk等等。内部命令是由特殊的文件格式(.def)所实现,如cdhistoryexec等等

通过man builtins查看说明文档

7.1 shift

shift用于移动参数的位置

格式:

  • shift [n]:n是数字,默认是1
1
2
3
4
5
6
7
eval set -- a b c d

echo $1 # 输出a
shift
echo $1 # 输出b
shift 2
echo $1 # 输出d

7.2 eval

通过连接参数构造命令,如果包含间接引用,也会保持原有语义,下面以一个例子来说明

1
2
3
4
5
foo=10 x=foo
y='$'$x
echo $y # 输出$foo
eval y='$'$x
echo $y # 输出10

7.3 set

格式:

  • set [option]

参数说明:

  • -e:当任意一个命令的返回值为非0时,立即退出
  • -x:将每个命令及其详细参数输出到标准输出中
  • -o pipefail:针对管道命令,取从右往左第一个非零返回值作为整个管道命令的返回值
  • -o vi/emacsvi模式或emacs模式,默认是emacs模式
  • -H:Enable ! style history substitution

示例:

  • set -e
  • set -x
  • set -o pipefail
  • eval set -- "some new params":设置当前shell的参数

7.4 source

执行shell脚本有2种方式:

  1. ./script.sh或者sh script.sh
    • 当前shell是父进程,生成一个子shell进程,在子shell中执行脚本。脚本执行完毕,退出子shell,回到当前shell
  2. source script.sh或者. script.sh
    • 在当前上下文中执行脚本,不会生成新的进程。脚本执行完毕,回到当前shell

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cat > test.sh << 'EOF'
#!/bin/bash

cd /
EOF

dir=$(pwd)
echo "current dir: '$(pwd)'"

# 当前的工作目录没有切换
cd ${dir} && bash test.sh && echo "current dir: '$(pwd)' after execute script"

# 当前的工作目录切换到了 /
cd ${dir} && source test.sh && echo "current dir: '$(pwd)' after execute script using source"

cd ${dir}

7.5 exec

exec用于进程替换(类似系统调用exec),或者标准输入输出的重定向

示例:

  • exec 1>my.log 2>&1:将标准输出、以及标准异常重定向到my.log文件中,对后续的所有命令都生效

7.5.1 Work with pipe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/bin/bash

# 总数
count=20

# 并行度
paracount=5

# $$表示当前执行文件的PID
tempfifo=$$.fifo

# 捕获信号 2 (ctrl C),并执行相应的动作
trap "exec 1000>&-; exec 1000<&-; exit 0" 2

# 创建fifo
mkfifo ${tempfifo}

# 将文件描述符 1000 分配给 tempfifo
exec 1000<> ${tempfifo}
rm -rf ${tempfifo}

for ((i=1; i<=${paracount}; i++))
do
# 向文件描述符中输入空行
echo >&1000
done

for((i=1; i<=${count}; i++))
do
# 从文件描述符中读取一行
read -u 1000
{
echo "$i, sleep $(($i%5))s"
sleep $(($i%5))
echo >&1000
} &
done

# 等待所有后台任务结束
wait

echo "done"

文件描述符用变量替代的版本1:

  • exec中用{}而不是${}来引用变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#!/bin/bash

# 总数
count=20

# 并行度
paracount=5

# 文件描述符(随意定)
fd_fifo=1000

# $$表示当前执行文件的PID
tempfifo=$$.fifo

# 捕获信号 2 (ctrl C),并执行相应的动作
trap "exec {fd_fifo}>&-; exec {fd_fifo}<&-; exit 0" 2

# 创建fifo
mkfifo ${tempfifo}

# 将文件描述符 fd_fifo 分配给 tempfifo
exec {fd_fifo}<> ${tempfifo}
rm -rf ${tempfifo}

for ((i=1; i<=${paracount}; i++))
do
# 向文件描述符中输入空行
echo >&${fd_fifo}
done

for((i=1; i<=${count}; i++))
do
# 从文件描述符中读取一行
read -u ${fd_fifo}
{
echo "$i, sleep $(($i%5))s"
sleep $(($i%5))
echo >&${fd_fifo}
} &
done

# 等待所有后台任务结束
wait

echo "done"

文件描述符用变量替代的版本2:

  • 使用eval
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#!/bin/bash

# 总数
count=20

# 并行度
paracount=5

# 文件描述符(随意定)
fd_fifo=1000

# $$表示当前执行文件的PID
tempfifo=$$.fifo

# 捕获信号 2 (ctrl C),并执行相应的动作
trap "eval 'exec ${fd_fifo}>&-; exec ${fd_fifo}<&-; exit 0'" 2

# 创建fifo
mkfifo ${tempfifo}

# 将文件描述符 fd_fifo 分配给 tempfifo
eval "exec ${fd_fifo}<> ${tempfifo}"
rm -rf ${tempfifo}

for ((i=1; i<=${paracount}; i++))
do
# 向文件描述符中输入空行
echo >&${fd_fifo}
done

for((i=1; i<=${count}; i++))
do
# 从文件描述符中读取一行
read -u ${fd_fifo}
{
echo "$i, sleep $(($i%5))s"
sleep $(($i%5))
echo >&${fd_fifo}
} &
done

# 等待所有后台任务结束
wait

echo "done"

7.6 shopt

用于启用/禁用shell扩展功能

示例:

  • shopt -s extglob:启用extglob
  • shopt -u extglob:禁用extglob

7.7 read

格式:

  • read [-ers] [-a name] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...]

参数说明:

-a:后跟一个变量,该变量会被认为是个数组,然后给其赋值,默认是以空格为分割符
-d:后面跟一个标志符,其实只有其后的第一个字符有用,作为结束的标志
-p:后面跟提示信息,即在输入前打印提示信息
-e:在输入的时候可以使用命令补全功能
-n:后跟一个数字,定义输入文本的长度,很实用
-r:屏蔽\,如果没有该选项,则\作为一个转义字符,有的话\就是个正常的字符了
-s:安静模式,在输入字符时不再屏幕上显示,例如login时输入密码
-t:后面跟秒数,定义输入字符的等待时间
-u:后面跟fd,从文件描述符中读入,该文件描述符可以是exec新开启的

简单读取

1
2
3
4
5
6
7
8
#!/bin/bash

#这里默认会换行
echo "输入网站名: "
#读取从键盘的输入
read website
echo "你输入的网站名是 $website"
exit 0 #退出

-p参数,允许在read命令行中直接指定一个提示

1
2
3
4
5
#!/bin/bash

read -p "输入网站名:" website
echo "你输入的网站名是 $website"
exit 0

-t参数指定read命令等待输入的秒数,当计时满时,read命令返回一个非零退出状态

1
2
3
4
5
6
7
8
9
#!/bin/bash

if read -t 5 -p "输入网站名:" website
then
echo "你输入的网站名是 $website"
else
echo "\n抱歉,你输入超时了。"
fi
exit 0

-n参数设置read命令计数输入的字符。当输入的字符数目达到预定数目时,自动退出,并将输入的数据赋值给变量

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

read -n1 -p "Do you want to continue [Y/N]?" answer
case $answer in
Y|y)
echo "fine ,continue";;
N|n)
echo "ok,good bye";;
*)
echo "error choice";;
esac
exit 0

-s选项能够使read命令中输入的数据不显示在命令终端上(实际上,数据是显示的,只是read命令将文本颜色设置成与背景相同的颜色)。输入密码常用这个选项

1
2
3
4
5
#!/bin/bash

read -s -p "请输入您的密码:" pass
echo "\n您输入的密码是 $pass"
exit 0

按行读取文件

1
2
3
4
5
6
7
8
9
10
#!/bin/bash

count=1 # 赋值语句,不加空格
cat test.txt | while read line # cat 命令的输出作为read命令的输入,read读到>的值放在line中
do
echo "Line $count:$line"
count=$[ $count + 1 ] # 注意中括号中的空格。
done
echo "finish"
exit 0

7.8 getopts

格式:getopts [option[:]] VARIABLE

  • option:选项,为单个字母
  • ::如果某个选项(option)后面出现了冒号(:),则表示这个选项后面可以接参数
  • VARIABLE:表示将某个选项保存在变量VARIABLE

getopts是linux系统中的一个内置变量,一般用在循环中。每当执行循环时,getopts都会检查下一个命令选项,如果这些选项出现
在option中,则表示是合法选项,否则不是合法选项。并将这些合法选项保存在VARIABLE这个变量中

getopts还包含两个内置变量,及OPTARGOPTIND

  1. OPTARG:选项后面的参数
  2. OPTIND:下一个选项的索引(该索引是相对于$*的索引,因此如果选项有参数的话,索引是非连续的)

示例:getopts ":a:bc:" opt(参数部分:-a 11 -b -c 5

  • 第一个冒号表示忽略错误
  • 字符后面的冒号表示该选项必须有自己的参数
  • $OPTARG存储相应选项的参数,如例中的115两个参数
  • $OPTIND总是存储原始$*中下一个要处理的选项的索引(注意不是参数,而是选项),此处指的是a,b,c这三个选项(而不是那些数字,当然数字也是会占有位置的)的索引
    • OPTIND初值为1,遇到x(选项不带参数),则OPTIND += 1;遇到x:(选项带参数),则OPTARG=argv[OPTIND+1],OPTIND += 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/sh

echo "args: ($@)"
while getopts ":a:bc:" opt
do
case $opt in
a)
echo "option '-a', OPTARG: '${OPTARG}', OPTIND: '${OPTIND}'"
;;
b)
echo "option '-b', OPTIND: '${OPTIND}'"
;;
c)
echo "option '-c', OPTIND: '${OPTIND}'"
;;
:)
echo "option '$OPTARG' must have argument"
exit 1
;;
?)
echo "error"
exit 1
;;
esac
done
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# case0
./testGetopts.sh -f
args: (-f)
error

# case1
./testGetopts.sh -a
args: (-a)
option 'a' must have argument

# case2
./testGetopts.sh -a 5
args: (-a 5)
option '-a', OPTARG: '5', OPTIND: '3'

# case3
./testGetopts.sh -a 5 -b
args: (-a 5 -b)
option '-a', OPTARG: '5', OPTIND: '3'
option '-b', OPTIND: '4'

# case4
./testGetopts.sh -a 5 -b -c 5
args: (-a 5 -b -c 5)
option '-a', OPTARG: '5', OPTIND: '3'
option '-b', OPTIND: '4'
option '-c', OPTIND: '6'

# case5
./testGetopts.sh -a 5 -b -c 5 -b
args: (-a 5 -b -c 5 -b)
option '-a', OPTARG: '5', OPTIND: '3'
option '-b', OPTIND: '4'
option '-c', OPTIND: '6'
option '-b', OPTIND: '7'

注意:如果getopts置于函数内部时,getopts解析的是函数的所有入参,可以通过$@将脚本的所有参数传递给函数

7.9 getopt

格式:getopt [options] -- parameters

参数说明:

  • -o, --options <选项字符串>:要识别的短选项
    • 单个字符表示选项。例如-o "abc"abc表示3个选项
    • 第一个冒号:表示忽略错误
    • 单个字符后接一个冒号:,表示该选项后必须跟一个参数,参数紧跟在选项后或者以空格隔开。例如-o a:bc,选项a必须要有参数,选项bc无需参数
    • 单个字符后接两个冒号::,表示该选项后必须跟一个参数,且参数必须紧跟在选项后不能以空格隔开。例如-o a::bc
  • -a, --alternative:允许长选项以-开始,否则默认长选项要求以--开头
  • -l, --longoptions <长选项>:要识别的长选项
    • 只有-l选项,无-o选项时,-l选项无效(如果没有短选项,可以加上-o ''或者-o ':'
    • 以逗号,分隔的长字符串表示选项。例如-l "along,blong"alongblong表示2个选项
    • 字符串后接一个冒号:,表示该选项后必须跟一个参数,参数和选项必须用空格隔开(仅有这一条冒号规则)
  • -n, --name <程序名>:将错误报告给的程序名

输出:getopt会将参数项进行重组和排序,会分成两组,以--符号分隔

  1. --之前是合法的选项参数集合
  2. --之后是多余的参数集合,注意,这里不包含非法选项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
getopt -o 'a' -- -a
#-------------------------↓↓↓↓↓↓-------------------------
-a --
#-------------------------↑↑↑↑↑↑-------------------------

getopt -o 'a' -- -a 1
#-------------------------↓↓↓↓↓↓-------------------------
-a -- '1'
#-------------------------↑↑↑↑↑↑-------------------------

getopt -o 'a' -- -b
#-------------------------↓↓↓↓↓↓-------------------------
getopt: invalid option -- 'b'
--
#-------------------------↑↑↑↑↑↑-------------------------

# 加上第一个:之后,可以忽略错误的选项
getopt -o ':a' -- -b
#-------------------------↓↓↓↓↓↓-------------------------
--
#-------------------------↑↑↑↑↑↑-------------------------

# 选项和参数紧贴
getopt -o ':a:' -- -a1
#-------------------------↓↓↓↓↓↓-------------------------
-a '1' --
#-------------------------↑↑↑↑↑↑-------------------------

# 选项和参数以空格分开
getopt -o ':a:' -- -a 1
#-------------------------↓↓↓↓↓↓-------------------------
-a '1' --
#-------------------------↑↑↑↑↑↑-------------------------

# 选项和参数紧贴
getopt -o ':a::' -- -a1
#-------------------------↓↓↓↓↓↓-------------------------
-a '1' --
#-------------------------↑↑↑↑↑↑-------------------------

# 选项和'参数'分开,其实'1'并没有被识别为参数
getopt -o ':a::' -- -a 1
#-------------------------↓↓↓↓↓↓-------------------------
-a '' -- '1'
#-------------------------↑↑↑↑↑↑-------------------------
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 当没有-o选项时,-l无效
getopt -l 'along' -- --along
#-------------------------↓↓↓↓↓↓-------------------------
--
#-------------------------↑↑↑↑↑↑-------------------------

# 正常case
getopt -o '' -l 'along' -- --along
#-------------------------↓↓↓↓↓↓-------------------------
--along --
#-------------------------↑↑↑↑↑↑-------------------------

# 没有指定 -a 选项,且选项以 - 开头时
getopt -o '' -l 'along' -- -along
#-------------------------↓↓↓↓↓↓-------------------------
getopt: invalid option -- 'a'
getopt: invalid option -- 'l'
getopt: invalid option -- 'o'
getopt: invalid option -- 'n'
getopt: invalid option -- 'g'
--
#-------------------------↑↑↑↑↑↑-------------------------

# 传入错误参数,且未忽略错误
getopt -o '' -l 'along' -- --blong
#-------------------------↓↓↓↓↓↓-------------------------
getopt: unrecognized option '--blong'
--
#-------------------------↑↑↑↑↑↑-------------------------

# 没有指定 -a 选项,且选项以 - 开头,忽略错误
getopt -o ':' -l 'along' -- -along
#-------------------------↓↓↓↓↓↓-------------------------
--
#-------------------------↑↑↑↑↑↑-------------------------

# 传入错误参数,忽略错误
getopt -o ':' -l 'along' -- --blong
#-------------------------↓↓↓↓↓↓-------------------------
--
#-------------------------↑↑↑↑↑↑-------------------------

# 必须指定参数,且指定参数,以空格分隔
getopt -o '' -al 'along:' -- -along arg1
#-------------------------↓↓↓↓↓↓-------------------------
--along 'arg1' --
#-------------------------↑↑↑↑↑↑-------------------------

# 必须指定参数,且未指定参数,未忽略错误
getopt -o '' -al 'along:' -- -along
#-------------------------↓↓↓↓↓↓-------------------------
getopt: option '--along' requires an argument
--
#-------------------------↑↑↑↑↑↑-------------------------

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
cat > test.sh << 'EOF'
#!/bin/bash

TEMP=`getopt -o ab:c:: --long a-long,b-long:,c-long:: \
-n 'example.bash' -- "$@"`

if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi

# 重新设置参数
eval set -- "$TEMP"

while true ; do
case "$1" in
-a|--a-long) echo "Option a" ; shift ;;
-b|--b-long) echo "Option b, argument \`$2'" ; shift 2 ;;
-c|--c-long)
# c has an optional argument. As we are in quoted mode,
# an empty parameter will be generated if its optional
# argument is not found.
case "$2" in
"") echo "Option c, no argument"; shift 2 ;;
*) echo "Option c, argument \`$2'" ; shift 2 ;;
esac ;;
--) shift ; break ;;
*) echo "Internal error!" ; exit 1 ;;
esac
done
echo "Remaining arguments:"
for arg do
echo '--> '"\`$arg'" ;
done
EOF

bash test.sh -a -b arg arg1 -c
#-------------------------↓↓↓↓↓↓-------------------------
Option a
Option b, argument `arg'
Option c, no argument
Remaining arguments:
--> `arg1'
#-------------------------↑↑↑↑↑↑-------------------------

7.10 printf

主要用于格式转换,以及格式化输出,当入参是数组的时候,会对数组的每个元素应用指定的格式化模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
printf %x 255
#-------------------------↓↓↓↓↓↓-------------------------
ff
#-------------------------↑↑↑↑↑↑-------------------------

items=(1 2 3 4 5)
printf '%d\n' ${items[@]}
#-------------------------↓↓↓↓↓↓-------------------------
1
2
3
4
5
#-------------------------↑↑↑↑↑↑-------------------------

items=("hello" "world")
printf '%s\n' ${items[@]}
#-------------------------↓↓↓↓↓↓-------------------------
hello
world
#-------------------------↑↑↑↑↑↑-------------------------

7.11 declare

declare用于定义变量、增减属性、查看变量信息。若在函数内部使用declare,那么默认是local

格式:declare [-/+ aAfFgilrtux] [-p] [name[=value] ...]

参数说明:

  • -/+-增加属性,+删除属性
    • a:数组
    • A:map
    • f:函数
    • i:整数
  • -p:查看变量信息,包括类型以及值

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看函数定义
declare -f <function>
# 查看普通变量
declare -p <variable>

# 定义数组
declare -a <array>

# 定义map
declare -A <map>

# 定义整数
declare -i <integer>

7.12 local

用于在函数内定义局部变量,其作用域就是函数本身

格式:local [option] [name[=value] ...],其中option部分参考declare即可

示例:

1
2
3
4
5
6
7
8
function test() {
local -a arr
arr=( 1 2 3 )
echo "inside function: '${arr[@]}'"
}

test
echo "outside function: '${arr[@]}'"

7.13 typeset

功能属于declare的子集,不推荐使用

7.14 alias

alias用于设置别名

示例:

  • alias lh='ls -alt'
  • alias xxx="${BASE_DIR}/xxx.sh"${BASE_DIR}的解析发生在配置时,在zsh等shell中,xxx会高亮
  • alias xxx='${BASE_DIR}/xxx.sh'${BASE_DIR}的解析发生在执行时,在zsh等shell中,xxx不会高亮

How to disable alias: \cp will use the original cp command, rather than the alias if there exists.

7.15 export

示例:

  • export -p:打印所有导出的符号
  • export <name>:导出变量
  • export -f <func_name>:导出函数

7.16 ulimit

设置或查看各类限制

示例:

  • ulimit -a
  • ulimit -v 1000000:将进程的最大内存设置为1000000字节,仅在当前shell以及shell的子进程中生效

7.17 pushd & popd

pushdpopd是用于操作目录栈的命令,在切换目录时非常有用。它们可以让你在不同目录之间快速切换,并在需要时返回到先前的目录

示例:

1
2
3
4
5
6
7
8
9
10
set -x
pwd
pushd /usr
dirs
pushd /var
dirs
popd
dirs
popd
dirs

8 Tips

8.1 Option Separator

选项分隔符为--,它有什么用呢?

举个简单的例子,如何创建一个名为-f的目录?mkdir -f肯定是不行的,因为-f会被当做mkdir命令的选项,此时我们就需要选项分隔符来终止mkdir对于后续字符串的解析,即mkdir -- -f

8.2 Trap Signal

trap常用来做一些清理工作,比如你在脚本中将一些进程放到后台执行,但如果脚本异常终止(比如用ctrl+c),那么这些后台进程可能得不到及时处理,这个时候就可以用trap来捕获信号,从而执行清理动作

格式

  • trap "commands" signal-list

注意

  • 如果commands中包含变量,那么该变量在执行trap语句时就已解析,而非到真正捕获信号的时候才解析

示例1

1
2
3
4
5
6
7
8
9
# do other things

ping www.baidu.com &

ping_pid=$!
trap "kill -9 ${ping_pid}; exit 0" SIGINT SIGTERM EXIT

sleep 2
# do other things

示例2(错误),该示例与示例1的差别就是用sudo执行ping命令

  • 这样是没法杀死ping这个后台进程的,因为ping_pid变量获取到的并不是pingpid,而是sudopid
1
2
3
4
5
6
7
8
9
# do other things

sudo ping www.baidu.com &

ping_pid=$!
trap "kill -9 ${ping_pid}; exit 0" SIGINT SIGTERM EXIT

sleep 2
# do other things

实例3(对实例2进行调整)

1
2
3
4
5
6
7
8
9
# do other things

sudo ping www.baidu.com &

ping_pid=$!
trap "pkill -9 ping; exit 0" SIGINT SIGTERM EXIT

sleep 2
# do other things

8.3 Color

8.3.1 ANSI escape codes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
NOCOLOR='\033[0m'

BLACK='\033[0;30m'
RED='\033[0;31m'
GREEN='\033[0;32m'
ORANGE='\033[0;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
LIGHTGRAY='\033[0;37m'
DARKGRAY='\033[1;30m'
LIGHTRED='\033[1;31m'
LIGHTGREEN='\033[1;32m'
YELLOW='\033[1;33m'
LIGHTBLUE='\033[1;34m'
LIGHTPURPLE='\033[1;35m'
LIGHTCYAN='\033[1;36m'
WHITE='\033[1;37m'

echo -e ${BLACK} 黑色 ${NOCOLOR}
echo -e ${RED} 红色 ${NOCOLOR}
echo -e ${GREEN} 绿色 ${NOCOLOR}
echo -e ${ORANGE} 橘黄色 ${NOCOLOR}
echo -e ${BLUE} 蓝色 ${NOCOLOR}
echo -e ${PURPLE} 紫色 ${NOCOLOR}
echo -e ${CYAN} 青色 ${NOCOLOR}
echo -e ${LIGHTGRAY} 浅灰色 ${NOCOLOR}
echo -e ${DARKGRAY} 深灰色 ${NOCOLOR}
echo -e ${LIGHTRED} 浅红色 ${NOCOLOR}
echo -e ${LIGHTGREEN} 浅绿色 ${NOCOLOR}
echo -e ${YELLOW} 黄色 ${NOCOLOR}
echo -e ${LIGHTBLUE} 浅蓝色 ${NOCOLOR}
echo -e ${LIGHTPURPLE} 浅紫色 ${NOCOLOR}
echo -e ${LIGHTCYAN} 浅青色 ${NOCOLOR}
echo -e ${WHITE} 白色 ${NOCOLOR}

8.3.2 tput

设置颜色:

1
2
tput setab [1-7] # Set the background colour using ANSI escape
tput setaf [1-7] # Set the foreground colour using ANSI escape

其中颜色编号表如下:

1
2
3
4
5
6
7
8
9
10
Num  Colour    #define         R G B

0 black COLOR_BLACK 0,0,0
1 red COLOR_RED 1,0,0
2 green COLOR_GREEN 0,1,0
3 yellow COLOR_YELLOW 1,1,0
4 blue COLOR_BLUE 0,0,1
5 magenta COLOR_MAGENTA 1,0,1
6 cyan COLOR_CYAN 0,1,1
7 white COLOR_WHITE 1,1,1

样式设置:

1
2
3
4
5
6
7
tput bold    # Select bold mode
tput dim # Select dim (half-bright) mode
tput smul # Enable underline mode
tput rmul # Disable underline mode
tput rev # Turn on reverse video mode
tput smso # Enter standout (bold) mode
tput rmso # Exit standout mode

其他:

1
2
tput sgr0    # Reset text format to the terminal's default
tput bel # Play a bell

示例:

1
tput setaf 1; tput setab 2; tput bold; echo "this is text"

9 Reference