变量,函数
流程控制if
流程控制while/until
流程控制case
流程控制for

认识shell脚本


什么是shell脚本

一个 shell 脚本就是一个包含一系列命令的文件

编写一个简单的shell脚本

  1. 编辑器(支持高亮)编写脚本

    #!/bin/bash
    # This is our first script.
    echo 'Hello World!'
    

    #!字符序列是一种特殊的结构叫做 shebang。这个 shebang 被用来告诉操作系统将执行此脚本所用的解释器的名字。每个 shell 脚本都应该把这一文本行作为它的第一行

  2. 使脚本可执行
    把如上脚本保存为hello_world

    $ ls -l hello_world
    $ chmod 755 hello_world
    

    对于脚本文件,有两个常见的权限设置;权限为 755 的脚本,则每个人都能执行,和权限为
    700 的脚本,只有文件所有者能够执行。注意为了能够执行脚本,脚本必须是可读的。

  3. 把脚本放到shell可找到的地方

    $ hello_world
    bash: hello_world: command not found
    

    如果没有给出可执行程序的明确路径名,那么系统每次都会搜索一系列的目录,来查找此可执行程序。这个/bin 目录就是其中一个系统会自动搜索的目录。这个目录列表被存储在一个名为 PATH 的环境变量中。这个 PATH 变量包含一个由冒号分隔开的目录列表。

    $ echo $PATH
    /home/monkey/bin:....
    

    大多数的 Linux 发行版会配置
    PATH 变量,让其包含一个位于用户家目录下的 bin 目录,从而允许用户能够执行他们自己的
    程序。

    • 脚本位置
      这个 ~/bin 目录是存放为个人所用脚本的好地方。如果我们编写了一个脚本,系统中的每个用
      户都可以使用它,那么这个脚本的传统位置是 /usr/local/bin。系统管理员使用的脚本经常放
      到 /usr/local/sbin 目录下。大多数情况下,本地支持的软件,不管是脚本还是编译过的程序,
      都应该放到 /usr/local 目录下,而不是在 /bin 或 /usr/bin 目录下。
      添加环境变量:

      export PATH=~/bin:"$PATH"
      
      $ . .bashrc
      

      这个点.命令是 source 命令的同义词,一个 shell 内建命令,用来读取一个指定的 shell
      命令文件,并把它看作是从键盘中输入的一样

编写脚本格式技巧

  1. 长选项
    在命令行中输入选项的时候,短选项更受欢迎,但是当书写脚本的时候,长选项能提供可读性。

  2. 缩进和行继续符

    find playground \
        \( \
            -type f \
            -not -perm 0600 \
            -exec chmod 0600 ‘{}’ ‘;’ \
        \) \
        -or \
        \( \
            -type d \
            -not -perm 0711 \ 
            -exec chmod 0711 ‘{}’ ‘;’ \
        \)
    

    通过使用行继续符(反斜杠-回车符序列)和缩进,这个复杂命令的逻辑性更清楚

  3. vim编辑器常见配置设置
    :syntax on:打开语法高亮
    :set hlsearch:高亮查找结果。比如说我们查找单词“echo”。通过设置这个选项,这个单词的每个实例会高亮显示。
    :set tabstop=4设置一个 tab 字符所占据的列数。默认是 8 列。把这个值设置为 4(一种常见做法),从而让长文本行更容易适应屏幕。
    :set autoindent:打开 “auto indent” 功能。这导致 vim 能对新的文本行缩进与刚输入的文本行相同的列数

    通过把这些命令(没有开头的冒号字符)添加到你的 ~/.vimrc 文件中,这些改动会永久生效

Here document

command << token
text
token

变量


变量名的规则

  1. 变量名可由字母数字字符(字母和数字)和下划线字符组成
  2. 变量名的第一个字符必须是一个字母或一个下划线
  3. 变量名中不允许出现空格和标点符号
  • 给变量和常量赋值
    variable=value
    

    注意在赋值过程中,变量名、等号和变量值之间必须没有空格

    a=z # 无引号,字符串
    b="a string" # 双引号,字符串可包含空格
    g='string' # 单引号,字符串
    c="a string and $b" # 参数展开
    d=$(ls -l foo.txt) # 参考命令替换
    l=`ls`;echo $l;echo $(ls)  # 表⽰执⾏ls后的结果,与``(旧shell)作⽤⼀致,但可以嵌套,后面详细介绍
    e=$((5 * 7)) # 参考算数扩展
    f="\t\ta string\n" # 双引号,支持转义
    a=5 b="a string" #可以在同一行中对多个变量赋值
    

    如果内容中有空格,需要用单引号或者双引号

  • 预定义变量
    $PWD $USER $HOME $PATH
  • 布尔值
    true false

字符串和数字

  1. 参数展开

    • 基本参数
      • $a展开后,会变成变量 a 所包含的值
      • ${a}简单参数也可能用花括号引起来,由于变量名周围的上下文,其变得不明确的情况下,这会很有帮助
      $ a=hello
      $ echo $a
      hello
      $ echo $a_1
      
      $ echo ${a}_1
      hello_1
      

      $a_1展开后没有任何结果,因为shell试图展开一个名为a_1的变量

    • 位置参数
      如下脚本posit-param例子来解释:
      #!/bin/bash
      # posit-param: script to view command line parameters
      echo "
      Number of arguments: $#
      \$0 = $0
      \$1 = $1
      \$2 = $2
      \$3 = $3
      \$4 = $4
      \$5 = $5
      \$6 = $6
      \$7 = $7
      \$8 = $8
      \$9 = $9
      "
      
      执行结果:
      $ posit-param a b c d
      Number of arguments: 4
      $0 = /home/me/bin/posit-param
      $1 = a
      $2 = b
      $3 = c
      $4 = d
      $5 =
      $6 =
      $7 =
      $8 =
      $9 =
      
      • 位置参数 $0 总会包含命令行中出现的第一个单词,也就是已执行程序的路径名
      • $#是可以得到命令行参数个数的变量
      • 访问第十一个位置参数,我们可以这样做:${11}
    • 字符串双引号""和单引号''
      $ b='hello'
      $ echo '$b world'
      $b world
      $ echo "$b world"
      hello world
      

      双引号支持转义 $开头的变量会自动替换

    • 管理空变量的展开
      ${parameter:-word}:若 parameter 没有设置(例如,不存在)或者为空,展开结果是 word 的值。若 parameter不为空,则展开结果是 parameter 的值。
      $ foo=
      $ echo ${foo:-"substitute value if unset"}
      if unset substitute value
      $ echo $foo
      $ foo=bar
      $ echo ${foo:-"substitute value if unset"}
      bar
      $ echo $foo
      bar
      
      ${parameter:=word}:若 parameter 没有设置或为空,展开结果是 word 的值。另外,word 的值会赋值给parameter。若 parameter 不为空,展开结果是 parameter 的值。

      注意:位置参数或其它的特殊参数不能以这种方式赋值。
      ${parameter:?word}:若 parameter 没有设置或为空,这种展开导致脚本带有错误退出,并且 word 的内容会发送到标准错误。若 parameter 不为空,展开结果是 parameter 的值。
      ${parameter:+word}:若 parameter 没有设置或为空,展开结果为空。若 parameter 不为空,展开结果是 word 的值会替换掉 parameter 的值;然而,parameter 的值不会改变。

  2. 返回变量名的参数展开
    ${!prefix\*}${!prefix@}:这种展开会返回以 prefix 开头的已有变量名。

    $ echo ${!BASH*}
    BASH BASH_ARGC BASH_ARGV BASH_COMMAND BASH_COMPLETION
    BASH_COMPLETION_DIR BASH_LINENO BASH_SOURCE BASH_SUBSHELL
    BASH_VERSINFO BASH_VERSION
    
    • 字符串展开
      有大量的展开形式可用于操作字符串。其中许多展开形式尤其适用于路径名的展开。
      ${#parameter}:展开成由 parameter 所包含的字符串的长度。通常,parameter 是一个字符串;然而,如果parameter 是 @ 或者是 * 的话,则展开结果是位置参数的个数。

      $ foo="This string is long."
      $ echo "'$foo' is ${#foo} characters long."
      'This string is long.' is 20 characters long.
      

      ${parameter:offset}${parameter:offset:length}:用来从 parameter 所包含的字符串中提取一部分字符。提取的字符始于第 offset个字符(从字符串开头算起)直到字符串的末尾,除非指定提取的长度。

      若 offset 的值为负数,则认为 offset 值是从字符串的末尾开始算起,而不是从开头。注意负数前面必须有一个空格,为防止与 ${parameter:-word} 展开形式混淆。
      length,若出现,则必须不能小于零。
      如果 parameter 是 @,展开结果是 length 个位置参数,从第 offset 个位置参数开始。

      ${parameter#pattern}${parameter#pattern}:从 paramter 所包含的字符串中清除开头一部分文本,这些字符要匹配定义的pattern。pattern 是通配符模式,就如那些用在路径名展开中的模式。这两种形式的差异之处是该 # 形式清除最短的匹配结果,而该 # 模式清除最长的匹配结果。

      $ foo=file.txt.zip
      $ echo ${foo#*.}
      txt.zip
      $ echo ${foo#*.}
      zip
      

      ${parameter%pattern}${parameter%%pattern}:和上面的 # 和 # 展开一样,但它们清除的文本从 parameter 所包含字符串的末尾开始,而不是开头。
      ${parameter/pattern/string}${parameter//pattern/string}${parameter/#pattern/string}${parameter/%pattern/string}:这种形式的展开对 parameter 的内容执行查找和替换操作。如果找到了匹配通配符 pattern的文本,则用 string 的内容替换它。

      • 在正常形式下,只有第一个匹配项会被替换掉
      • //形式下,所有的匹配项都会被替换掉
      • /# 要求匹配项出现在字符串的开头
      • /% 要求匹配项出现在字符串的末尾
      • /string 可能会省略掉,这样会导致删除匹配的文本
      $ foo=JPG.JPG
      $ echo ${foo/JPG/jpg}
      jpg.JPG
      $ echo ${foo//JPG/jpg}
      jpg.jpg
      $ echo ${foo/#JPG/jpg}
      jpg.JPG
      $ echo ${foo/%JPG/jpg}
      JPG.jpg
      

      字符串操作展开可以用来替换其它常见命令比方说 sed 和cut。

    • 大小写转换

      • declare 命令的-u和-l选项
        能强制一个变量总是包含所需的格式,无论如何赋值给它
        ul-declare脚本例子:

        declare 命令来创建两个变量,upper 和 lower

        #!/bin/bash
        # ul-declare: demonstrate case conversion via declare
        declare -u upper
        declare -l lower
        if [[ $1 ]]; then
            upper="$1"
            lower="$1"
            echo $upper
            echo $lower
        fi
        
        $ ul-declare aBc
        ABC
        abc
        
      • 参数展开
        大小写转换参数展开:

        格式 结果
        ${parameter,,} 把 parameter 的值全部展开成小写字母。
        ${parameter,} 仅仅把 parameter 的第一个字符展开成小写字母。
        ${parameter^^} 把 parameter 的值全部转换成大写字母。
        ${parameter^} 仅仅把 parameter 的第一个字符转换成大写字母(首字母大写)。
        #!/bin/bash
        # ul-param - demonstrate case conversion via parameter expansion
        if [[ $1 ]]; then
            echo ${1,,}
            echo ${1,}
            echo ${1^^}
            echo ${1^}
        fi
        
        $ ul-param aBc
        abc
        aBc
        ABC
        ABc
        
  3. 算术求值和展开

    • 算数展开

      $((expression))
      
    • 数基
      指定不同的数基

      表示法 描述
      number 默认情况下,没有任何表示法的数字被看做是十进制数(以10为底)。
      0number 在算术表达式中,以零开头的数字被认为是八进制数。
      0xnumber 十六进制表示法
      base#number number 以 base 为底
      $ echo $((0xff))
      255
      $ echo $((2#11111111))
      255
      
    • 一元运算符
      +-表示正数和负数

    • 简单运算
      算术运算符

      运算符 描述
      +
      -
      *
      / 整除
      ** 乘方
      % 取模(余数)

      shell 算术只操作整形,所以除法运算的结果总是整数:

      $ echo $(( 5 / 2 ))
      2
      

      这使得确定除法运算的余数更为重要:

      $ echo $(( 5 % 2 ))
      1
      

      shell只可以处理所有类型的整形算术的, 有几种途径可以做到更高级的数学运算:嵌入的 Perl 或者 AWK 程序,详细可参考linux三剑客

      $ echo $((2/3))
      0
      $ awk 'BEGIN{print 2/3}'
      0.666667
      
    • 赋值运算符

      表示法 描述
      parameter = value 简单赋值。给 parameter 赋值。
      parameter += value 加。等价于 parameter = parameter + value。
      parameter -= value 减。等价于 parameter = parameter – value。
      parameter *= value 乘。等价于 parameter = parameter * value。
      parameter /= value 整除。等价于 parameter = parameter / value。
      parameter %= value 取模。等价于 parameter = parameter % value。
      parameter++ 后缀自增变量。等价于 parameter = parameter + 1 (但,要看下面的讨论)。
      parameter-- 后缀自减变量。等价于 parameter = parameter - 1。
      ++parameter 前缀自增变量。等价于 parameter = parameter + 1。
      --parameter 前缀自减变量。等价于 parameter = parameter - 1。
    • 位运算符

      运算符 描述
      ~ 按位取反。对一个数字所有位取反。
      << 位左移. 把一个数字的所有位向左移动。
      >> 位右移. 把一个数字的所有位向右移动。
      & 位与。对两个数字的所有位执行一个 AND 操作。
      ^ 位异或。对两个数字的所有位执行一个异或操作。

      按位取反运算符之外,其它所有位运算符都有相对应的赋值运算符(例如,<<=)

      $ for ((i=0;i<8;++i)); do echo $((1<<i)); done
      1
      2
      4
      8
      16
      32
      64
      128
      
    • 逻辑运算符

      运算符 描述
      <= 小于或相等
      >= 大于或相等
      < 小于
      > 大于
      == 相等
      != 不相等
      && 逻辑与
      expr1?expr2:expr3 条件(三元)运算符。若表达式 expr1 的计算结果为非零值(算术真),则 执行表达式 expr2,否则执行表达式 expr3。
    • 整数扩展(( ))

      • bash 也提供了 (( )) 复合命令,其有利于操作整数。它支持一套完整的算术计算
      • 复合命令 (( )) 支持各种各样的比较运算符,把结果映射成 shell 正常的退出码
      • (( )) 被用来执行算术真测试。如果算术计算的结果是非零值,则其测试值为真
      $ if ((1)); then echo "true"; else echo "false"; fi
      true
      $ if ((0)); then echo "true"; else echo "false"; fi
      false
      $ a=3;b=4
      $ ((a<b));echo $?
      0
      

数组

  • 什么是数组
    数组是一次能存放多个数据的变量。数组的组织结构就像一张表。
    Bash 中的数组仅限制为单一维度。我们可以把它们看作是只有一列的电子表格。

  • 创建数组

    $ a[1]=foo
    $ echo ${a[1]}
    foo
    

    $ declare -a a

    使用 -a 选项,declare 命令的这个例子创建了数组 a。

    $ days=(Sun Mon Tue Wed Thu Fri Sat)
    $ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu [5]=Fri [6]=Sat)
    

    bash 允许赋值的数组下标包含“间隔”
    days=([1]=Mon [2]=Tue [5]=Fri [6]=Sat)

  • 数组赋值

    • 单个值赋值

      name[subscript]=value
      
      $ a=(a b c d e)
      $ a=
      $ echo ${a[@]}
      b c d e
      $ a=A
      $ echo ${a[@]}
      A c d e
      

      任何没有下标的对数组变量的引用都指向数组元素 0

    • 多个值赋值

      name=(value1 value2 ...)
      
      $ array=(1 2 3 4);echo $array
      1
      $ echo ${array[1]}
      2
      $ array=(`ls`) # 等价于array=($(ls))
      
  • 数组操作

    • 输出整个数组的内容${array[*]} ${array[@]}
      下标 * 和 @ 可以被用来访问数组中的每一个元素。与位置参数一样,@ 表示法在两者之中更有用处,@能展开成分离的词

      $ animals=("a dog" "a cat" "a fish")
      $ for i in ${animals[*]}; do echo $i; done
      a
      dog
      a
      cat
      a
      fish
      $ for i in ${animals[@]}; do echo $i; done
      a
      dog
      a
      cat
      a
      fish
      $ for i in "${animals[*]}"; do echo $i; done
      a dog a cat a fish
      $ for i in "${animals[@]}"; do echo $i; done # 推荐
      a dog
      a cat
      a fish
      
    • 确定数组元素个数#

      $ a=foo;echo ${#a}
      3
      $ b[100]=foo;echo ${#b[@]}
      1
      $ echo ${#b[100]}
      3
      

      尽管我们把字符串赋值给数组元素 100,bash 仅仅报告数组中有一个元素

    • 找到数组使用的下标

      bash 允许赋值的数组下标包含“间隔”,有时候确定哪个元素真正存在是很有用的

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

      $ foo=([2]=a [4]=b [6]=c)
      $ for i in "${foo[@]}"; do echo $i; done
      a
      b
      c
      $ for i in "${!foo[@]}"; do echo $i; done
      2
      4
      6
      
    • 在数组末尾添加元素

      需要在数组末尾附加数据,显然知道数组中元素的个数是没用的,因为通过 * 和 @ 表示法返回的数值不能告诉我们使用的最大数组索引

      += 赋值运算符,能够自动地把值附加到数组末尾

      $ foo=(a b c)
      $ echo ${foo[@]}
      a b c
      $ foo+=(d e f)
      $ echo ${foo[@]}
      a b c d e f
      
    • 数组排序
      array-sort脚本:

      #!/bin/bash
      # array-sort : Sort an array
      a=(f e d c b a)
      echo "Original array: ${a[@]}"
      a_sorted=($(for i in "${a[@]}"; do echo $i; done | sort))
      echo "Sorted array: ${a_sorted[@]}"
      
      $ array-sort
      Original array: f e d c b a
      Sorted array:
      a b c d e f
      
    • 删除数组
      unset命令

      • 删除整个数组

        $ foo=(a b c d e f)
        $ echo ${foo[@]}
        a b c d e f
        $ unset foo
        $ echo ${foo[@]}
        $
        
      • 删除单个数组元素

        $ foo=(a b c d e f)
        $ echo ${foo[@]}
        a b c d e f
        $ unset 'foo[2]'
        $ echo ${foo[@]}
        a b d e f
        
  • 关联数组

    declare -A colors
    colors["red"]="#ff0000"
    colors["green"]="#00ff00"
    colors["blue"]="#0000ff"
    

    关联数组使用字符串而不是整数作为数组索引。
    关联数组必须用带有 -A 选项的declare 命令创建。
    访问关联数组元素的方式几乎与整数索引数组相同:echo ${colors["blue"]}

shell函数


shell 函数是位于其它脚本中的“微脚本”,作为自主程序。
Shell函数有两种语法形式:

function name {
    commands
    return
}
and
name () {
    commands
    return
}

这里的 name 是函数名,commands 是一系列包含在函数中的命令。
在脚本中 shell 函数定义必须出现在函数调用之前。
return可选

局部变量

通过在变量名之前加上单词 local,来定义局部变量。

#!/bin/bash
# local-vars: script to demonstrate local variables
foo=0 # global variable foo
funct_1 () {
    local foo # variable foo local to funct_1
    foo=1
    echo "funct_1: foo = $foo"
}
echo "global: foo = $foo"
funct_1
# 执行结果
global: foo = 0
funct_1: foo = 1

流程控制 if分支


if commands; then
     commands
[elif commands; then
     commands...]
[else
     commands]
fi

退出状态

当命令执行完毕后,命令(包括我们编写的脚本和 shell 函数)会给系统发送一个值,叫做退出状态。这个值是一个 0 到 255 之间的整数,说明命令执行成功或是失败。

  • $?用于检测退出状态
  • 0值总是说明成功,其他所有值说明失败
    $ ls -d /usr/bin
    /usr/bin
    $ echo $?
    0
    $ ls -d /bin/usr
    ls: cannot access /bin/usr: No such file or directory
    $ echo $?
    2
    
  • true 命令总是执行成功,而 false 命令总是执行失败
    $ true
    $ echo $?
    0
    $ false
    $ echo $?
    1
    

测试条件

[ expression ]

expression 是一个表达式,其执行结果是 true 或者是 false。当表达式为真时,这个test 命令返回一个零退出状态,当表达式为假时,test 命令退出状态为 1。

[]与expression的空格必须,否则报错

$ [2 -eq 2];echo $?
-bash: [2: command not found
127
$ [ 2 -eq 2 ];echo $?
0
  • 文件表达式

    表达式 如果为真
    file1 -ef file2 file1 和 file2 拥有相同的索引号(通过硬链接两个文件名指向相同的文件)。
    file1 -nt file2 file1新于 file2。
    file1 -ot file2 file1早于 file2。
    -b file file 存在并且是一个块(设备)文件。
    -c file file 存在并且是一个字符(设备)文件。
    -d file file 存在并且是一个目录。
    -e file file 存在。
    -f file file 存在并且是一个普通文件。
    -g file file 存在并且设置了组 ID。
    -G file file 存在并且由有效组 ID 拥有。
    -k file file 存在并且设置了它的“sticky bit”。
    -L file file 存在并且是一个符号链接。
    -O file file 存在并且由有效用户 ID 拥有。
    -p file file 存在并且是一个命名管道。
    -r file file 存在并且可读(有效用户有可读权限)。
    -s file file 存在且其长度大于零。
    -S file file 存在且是一个网络 socket。
    -t fd fd 是一个定向到终端/从终端定向的文件描述符 。 这可以被用来决定是否重定向了标准输入/输出错误。
    -u file file 存在并且设置了 setuid 位。
    -w file file 存在并且可写(有效用户拥有可写权限)。
    -x file file 存在并且可执行(有效用户有执行/搜索权限)。
    #!/bin/bash
    # test-file: Evaluate the status of a file
    FILE=~/.bashrc
    if [ -e "$FILE" ]; then
        if [ -f "$FILE" ]; then
            echo "$FILE is a regular file."
        fi
        if [ -d "$FILE" ]; then
            echo "$FILE is a directory."
        fi
        if [ -r "$FILE" ]; then
            echo "$FILE is readable."
        fi
        if [ -w "$FILE" ]; then
            echo "$FILE is writable."
        fi
        if [ -x "$FILE" ]; then
            echo "$FILE is executable/searchable."
        fi
    else
        echo "$FILE does not exist"
        exit 1
    fi
    exit
    

    $FILE 的引用,在这里引号并不是必需的。用引号把参数引起来就确保了操作符之后总是跟随着一个字符串,即使字符串为空。
    脚本末尾的 exit 命令。这个 exit 命令接受一个单独的,可选的参数,其成为脚本的退出状态。当不传递参数时,退出状态默认为零。当一个脚本“运行到最后”(到达文件末尾),不管怎样,默认情况下它以退出状态零终止。

    通过带有一个整数参数的 return 命令,shell 函数也可以返回一个退出状态。

  • 字符串表达式

    表达式 如果为真...
    string string 不为 null。
    -n string 字符串 string 的长度大于零。
    -z string 字符串 string 的长度为零。
    string1 = string2 string1 == string2 string1 和 string2 相同. 单或双等号都可以,不过双等号更受欢迎。
    string1 != string2 string1 和 string2 不相同。
    string1 > string2 sting1 排列在 string2 之后。
    string1 < string2 string1 排列在 string2 之前。

    当在if条件表达式中使用><表达式操作符必须用引号引起来(或者是用反斜杠转义),被 shell 解释为重定向操作符,造成潜在地破坏结果。

  • 整数表达式

    表达式 如果为真...
    integer1 -eq integer2 integer1 等于 integer2.
    integer1 -ne integer2 integer1 不等于 integer2.
    integer1 -le integer2 integer1 小于或等于 integer2.
    integer1 -lt integer2 integer1 小于 integer2.
    integer1 -ge integer2 integer1 大于或等于 integer2.
    integer1 -gt integer2 integer1 大于 integer2.

加强版测试条件

[[ expression ]]
  • 增加了一个重要的新的字符串表达式:

    string1 =~ regex
    

    如果 string1 匹配扩展的正则表达式 regex,其返回值为真。这就为执行比如数据验证等任务提供了许多可能性。

  • 另一个功能是 == 操作符支持类型匹配,正如路径名展开所做的那样

    $ FILE=foo.bar
    $ if [[ $FILE == foo.* ]]; then
    > echo "$FILE matches pattern 'foo.*'"
    > fi
    foo.bar matches pattern 'foo.*'
    

结合表达式

把表达式结合起来创建更复杂的计算。通过使用逻辑操作符来结合表达式。

  • 逻辑操作符

    操作符 测试条件[] [[ ]] 和 (( ))
    AND -a &&
    OR -o ||
    NOT ! !
    #!/bin/bash
    MIN_VAL=1
    MAX_VAL=100
    INT=50
    if [ ! \( $INT -ge $MIN_VAL -a $INT -le $MAX_VAL \) ]; then
        echo "$INT is outside $MIN_VAL to $MAX_VAL."
    else
        echo "$INT is in range."
    fi
    

    使用测试条件[],其中所有的表达式和操作符都被 shell 看作是命令参数(不像 [[ ]](( )) ), 对于 bash 有特殊含义的字符,比如说 ,(,和 ),必须引起来或者是转义。

控制操作符&&||

  • command1 && command2:command1执行成功才会执行command2

    $ mkdir temp && cd temp
    

    创建一个名为 temp 的目录,并且若它执行成功后,当前目录会更改为 temp

  • command1 || command2:command1 执行失败后,才会执行 command2

    $ [ -d temp ] || mkdir temp
    

    会测试目录 temp 是否存在,并且只有测试失败之后,才会创建这个目录

读取键盘输入

能够直接接受用户的输入,完成交互

read从标准输入读取数值

用来从标准输入读取单行数据,当使用重定向的时候,读取文件中的一行数据

read [-options] [variable...]
  • variable:是用来存储输入数值的一个或多个变量名
    如果没有提供变量名,shell 变量 REPLY 会包含数据行

    #!/bin/bash
    # read-integer: evaluate the value of an integer.
    echo -n "Please enter an integer -> "
    read int
    if [[ "$int" =~ ^-?[0-9]+$ ]]; then
        if [ $int -eq 0 ]; then
            echo "$int is zero."
        else
            if [ $int -lt 0 ]; then
                echo "$int is negative."
            else
                echo "$int is positive."
            fi
            if [ $((int % 2)) -eq 0 ]; then
                echo "$int is even."
            else
                echo "$int is odd."
            fi
        fi
    else
        echo "Input value is not an integer." >&2
        exit 1
    fi
    
    read-integer
    Please enter an integer -> 5
    5 is positive.
    5 is odd.
    

    echo命令的-n选项会删除输出结果末尾的换行符

  • read 多个变量

    #!/bin/bash
    # read-multiple: read multiple values from keyboard
    echo -n "Enter one or more values > "
    read var1 var2 var3 var4 var5
    echo "var1 = '$var1'"
    echo "var2 = '$var2'"
    echo "var3 = '$var3'"
    echo "var4 = '$var4'"
    echo "var5 = '$var5'"
    
    $ read-multiple
    Enter one or more values > a b c d e
    var1 = 'a'
    var2 = 'b'
    var3 = 'c'
    var4 = 'd'
    var5 = 'e'
    [me@linuxbox ~]$ read-multiple
    Enter one or more values > a
    var1 = 'a'
    var2 = ''
    var3 = ''
    var4 = ''
    var5 = ''
    [me@linuxbox ~]$ read-multiple
    Enter one or more values > a b c d e f g
    var1 = 'a'
    var2 = 'b'
    var3 = 'c'
    var4 = 'd'
    var5 = 'e f g'
    

    read 命令接受到变量值数目少于期望的数字,那么额外的变量值为空,而多余的输入数据则会 被包含到最后一个变量中

  • read命令后没有列出变量名
    一个 shell 变量,REPLY,将会包含 所有的输入

    #!/bin/bash
    # read-single: read multiple values into default variable
    echo -n "Enter one or more values > "
    read
    echo "REPLY = '$REPLY'"
    
    $ read-single
    Enter one or more values > a b c d
    REPLY = 'a b c d'
    
  • options:read选项

    选项 说明
    -a array 把输入赋值到数组 array 中,从索引号零开始。我们 将在第36章中讨论数组问题。
    -d delimiter 用字符串 delimiter 中的第一个字符指示输入结束,而不是一个换行符。
    -e 使用 Readline 来处理输入。这使得与命令行相同的方式编辑输入。
    -n num 读取 num 个输入字符,而不是整行。
    -p prompt 为输入显示提示信息,使用字符串 prompt。
    -r Raw mode. 不把反斜杠字符解释为转义字符。
    -s Silent mode. 不会在屏幕上显示输入的字符。当输入密码和其它确认信息的时候,这会很有帮助。
    -t seconds 超时. 几秒钟后终止输入。read 会返回一个非零退出状态,若输入超时。
    -u fd 使用文件描述符 fd 中的输入,而不是标准输入。

IFS

如上多个由一个或几个空格分离开的单词在输入行中变成独立的个体,并被 read 赋值给单独的变量。
这种行为由 shell 变量__IFS__ (内部字符分隔符)配置。IFS 的默认值包含一个空格,一个 tab,和一个换行符,每一个都会把 字段分割开。
可以调整 IFS 的值来控制输入字段的分离。例如,/etc/passwd 文件包含的数据行 使用冒号作为字段分隔符。通过把 IFS 的值更改为单个冒号,我们可以使用 read 读取 /etc/passwd 中的内容,并成功地把字段分给不同的变量。

详细例子参看参考文档

流程控制 while/until循环


循环while

和 if 一样,while 计算一系列命令的退出状态。只要退出状态为零,它就执行循环内的命令。
语法:

while commands; do commands; done

例子脚本:

#!/bin/bash
# while-count: display a series of numbers
count=1
while [ $count -le 2 ]; do
		echo $count
		count=$((count + 1))
done
echo "Finished."
$ while-count
1
2
Finished.

跳出循环until

  • break:退出当前循环
  • continue:跳过当前的循环,进⼊下⼀次循环
  • until
    • until 命令与 while 非常相似
    • 当遇到一个非零退出状态的时候,while 退出循环,而 until不退出,一个 until 循环会继续执行直到它接受了一个退出状态零
      #!/bin/bash
      # until-count: display a series of numbers
      count=1
      until [ $count -gt 5 ]; do
      		echo $count
      		count=$((count + 1))
      done
      echo "Finished."
      

while 可替代until,两者二选一即可

使用循环读取文件

  • 运用重定向<

    #!/bin/bash
    # while-read: read lines from a file
    while read distro version release; do
        printf "Distro: %s\tVersion: %s\tReleased: %s\n" \
            $distro \
            $version \
            $release
    done < distros.txt
    

    这个 read 命令读取每个文本行之后,将会退出,其退出状态为零,直到到达文件末尾。

  • 运用管道

    #!/bin/bash
    # while-read2: read lines from a file
    sort -k 1,1 -k 2n distros.txt | while read distro version release; do
        printf "Distro: %s\tVersion: %s\tReleased: %s\n" \
            $distro \
            $version \
            $release
    done
    

流程控制 case分支(选学)

语法:

case word in
    [pattern [| pattern]...) commands ;;]...
esac
#!/bin/bash
# case-menu: a menu driven system information program
clear
echo "
Please Select:
1\. Display System Information
2\. Display Disk Space
3\. Display Home Space Utilization
0\. Quit
"
read -p "Enter selection [0-3] > "
case $REPLY in
    1)  echo "Program terminated."
        exit
        ;;
    2)  echo "Hostname: $HOSTNAME"
        uptime
        ;;
    3)  df -h
        ;;
    4)  if [[ $(id -u) -eq 0 ]]; then
            echo "Home Space Utilization (All Users)"
            du -sh /home/*
        else
            echo "Home Space Utilization ($USER)"
            du -sh $HOME
        fi
        ;;
    *)  echo "Invalid entry" >&2
        exit 1
        ;;
esac

模式pattern

模式以一个 “)” 为终止符,如示例:

模式 描述
a) 若单词为 “a”,则匹配
[[:alpha:]]) 若单词是一个字母字符,则匹配
???) 若单词只有3个字符,则匹配
*.txt) 若单词以 “.txt” 字符结尾,则匹配
*) 匹配任意单词。把这个模式做为 case 命令的最后一个模式,是一个很好的做法, 可以捕捉到任意一个与先前模式不匹配的数值;也就是说,捕捉到任何可能的无效值。
#!/bin/bash
read -p "enter word > "
case $REPLY in
    [[:alpha:]])        echo "is a single alphabetic character." ;;
    [ABC][0-9])         echo "is A, B, or C followed by a digit." ;;
    ???)                echo "is three characters long." ;;
    *.txt)              echo "is a word ending in '.txt'" ;;
    *)                  echo "is something else." ;;
esac

还可以使用竖线字符作为分隔符,把多个模式结合起来。这就创建了一个 “或” 条件模式。这对于处理诸如大小写字符很有用处。

执行多个动作

早于版本号4.0的 bash,case 语法只允许执行与一个成功匹配的模式相关联的动作。 匹配成功之后,命令将会终止。
早于4.0的 bash,对于 case 语法绝不能匹配 多个测试条件。现在的 bash 版本,添加 “;;&” 表达式来终止每个行动。

#!/bin/bash
# case4-2: test a character
read -n 1 -p "Type a character > "
echo
case $REPLY in
    [[:upper:]])    echo "'$REPLY' is upper case." ;;&
    [[:lower:]])    echo "'$REPLY' is lower case." ;;&
    [[:alpha:]])    echo "'$REPLY' is alphabetic." ;;&
    [[:digit:]])    echo "'$REPLY' is a digit." ;;&
    [[:graph:]])    echo "'$REPLY' is a visible character." ;;&
    [[:punct:]])    echo "'$REPLY' is a punctuation symbol." ;;&
    [[:space:]])    echo "'$REPLY' is a whitespace character." ;;&
    [[:xdigit:]])   echo "'$REPLY' is a hexadecimal digit." ;;&
esac
$ case4-2
Type a character > a
'a' is lower case.
'a' is alphabetic.
'a' is a visible character.
'a' is a hexadecimal digit.

添加的 “;;&” 的语法允许 case 语句继续执行下一条测试,而不是简单地终止运行。
否则执行结果为

$ case4-1
Type a character > a
'a' is lower case.

流程控制 for循环


传统shell格式

语法:

for variable [in words]; do
    commands
done

例子:

$ for i in A B C D; do echo $i; done
A
B
C
D

words 是一个可选的条目列表,for 命令真正强大的功能是我们可以通过许多有趣的方式创建 words 列表

  • 花括号展开

    $ for i in {A..D}; do echo $i; done
    A
    B
    C
    D
    
  • 路径名展开

    $ for i in distros*.txt; do echo $i; done
    distros-by-date.txt
    distros-dates.txt
    distros-key-names.txt
    distros-key-vernums.txt
    ...
    
  • 命令替换

    #!/bin/bash
    # longest-word : find longest string in a file
    while [[ -n $1 ]]; do
        if [[ -r $1 ]]; then
            max_word=
            max_len=0
            for i in $(strings $1); do
                len=$(echo $i | wc -c)
                if (( len > max_len )); then
                    max_len=$len
                    max_word=$i
                fi
            done
            echo "$1: '$max_word' ($max_len characters)"
        fi
        shift
    done
    

    当在命令行中给出一个或多个文件名的时候, 该程序会使用 strings 程序(其包含在 GNU binutils 包中),为每一个文件产生一个可读的文本格式的 “words” 列表。 然后这个 for 循环依次处理每个单词,判断当前这个单词是否为目前为止找到的最长的一个。当循环结束的时候,显示出最长的单词。

    #!/bin/bash
    # longest-word2 : find longest string in a file
    for i; do
        if [[ -r $i ]]; then
            max_word=
            max_len=0
            for j in $(strings $i); do
                len=$(echo $j | wc -c)
                if (( len > max_len )); then
                    max_len=$len
                    max_word=$j
                fi
            done
            echo "$i: '$max_word' ($max_len characters)"
        fi
    done
    

    省略掉 for 命令的可选项 words 部分,for 命令会默认处理位置参数。
    用 for 循环来代替 while 循环。通过省略 for 命令的 words 列表, 用位置参数替而代之。在循环体内,之前的变量 i 已经改为变量 j。同时 shift 命令也被淘汰掉了

c语言格式

语法:

for (( expression1; expression2; expression3 )); do
    commands
done

expression1,expression2,和 expression3 都是算术表达式

例子:

#!/bin/bash
# simple_counter : demo of C style for command
for (( i=0; i<5; i=i+1 )); do
    echo $i
done

C 语言格式的 for 循环对于需要一个数字序列的情况是很有用处的。

参考