Home Shell (3)
Post
Cancel

Shell (3)

[toc]

阮一峰老师写的教程,这里做个精简笔记。

1 首行

脚本第一行通常通过 #! 来指定解释器,即该脚本通过什么程序来执行。

1
#! /usr/bin/env bash

以上命令使用 env 命令(这个命令总是在 /usr/bin 目录),返回 Bash 可执行文件的位置。

env 命令总是指向 /usr/bin/env 文件,或者说,这个二进制文件总是在目录 /usr/binenv NAME 表示返回 NAME 的可执行文件位置。

1
2
## 新建一个不带任何环境变量的 Shell
$ env -i /bin/sh

2 脚本参数

调用脚本时,脚本文件名后可以跟随参数,譬如:

1
$ ./script.sh var1 var2 var3

脚本文件内部,可以使用特殊变量,引用这些参数。

  • $0:脚本文件名,即 script.sh
  • $1~$9:对应脚本的第一个参数到第九个参数。
  • $#:参数的总数。
  • $@:全部的参数,参数之间使用空格分隔。
  • $*:全部的参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。
  • 如果脚本的参数多于9个,那么第10个参数可以用${10}的形式引用,以此类推。
  • 如果多个参数放在双引号里面,视为一个参数。
  • 如果命令是 command -o foo bar,那么 -o$1foo$2bar$3

2.1 shift

shift 命令可以改变脚本参数,每次执行都会移除脚本当前的第一个参数($1),使得后面的参数向前一位,即 $2变成 $1$3 变成 $2$4 变成 $3,以此类推。

通常使用 while 循环结合 shift 进行参数遍历。

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

echo "一共输入了 $# 个参数"

while [ "$1" != "" ]; do
  echo "剩下 $# 个参数"
  echo "参数:$1"
  shift
done

shift 命令可以接受一个整数作为参数,指定所要移除的参数个数,默认为 1。

2.2 getopts

getopts 命令用在脚本内部,可以解析复杂的脚本命令行参数,通常与 while 循环一起使用,取出脚本所有的带有前置连词线(-)的参数。

1
getopts optstring name

它带有两个参数。第一个参数 optstring 是字符串,给出脚本所有的连词线参数。比如,某个脚本可以有三个配置项参数 -l-h-a,其中只有 -a 可以带有参数值,而 -l-h 是开关参数,那么 getopts 的第一个参数写成 lha:,顺序不重要。注意,a 后面有一个冒号,表示该参数带有参数值,getopts 规定带有参数值的配置项参数,后面必须带有一个冒号(:)。getopts 的第二个参数 name 是一个变量名,用来保存当前取到的配置项参数,即 lha

下面是一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
while getopts 'lha:' OPTION; do
  case "$OPTION" in
    l)
      echo "linuxconfig"
      ;;

    h)
      echo "h stands for h"
      ;;

    a)
      avalue="$OPTARG"
      echo "The value provided is $OPTARG"
      ;;
    ?)
      echo "script usage: $(basename $0) [-l] [-h] [-a somevalue]" >&2
      exit 1
      ;;
  esac
done
shift "$(($OPTIND - 1))"

上面例子中,while 循环不断执行 getopts 'lha:' OPTION命令,每次执行就会读取一个连词线参数(以及对应的参数值),然后进入循环体。变量 OPTION 保存的是,当前处理的那一个连词线参数(即 lha)。如果用户输入了没有指定的参数(比如 -x),那么 OPTION 等于 ?。循环体内使用 case 判断,处理这四种不同的情况。

如果某个连词线参数带有参数值,比如 -a foo,那么处理 a 参数的时候,环境变量 $OPTARG 保存的就是参数值。

注意,只要遇到不带连词线的参数,getopts 就会执行失败,从而退出 while 循环。比如,getopts 可以解析command -l foo,但不可以解析 command foo -l。另外,多个连词线参数写在一起的形式,比如 command -lhgetopts 也可以正确处理。

变量 $OPTINDgetopts 开始执行前是 1,然后每次执行就会加 1。等到退出 while 循环,就意味着连词线参数全部处理完毕。这时,$OPTIND - 1 就是已经处理的连词线参数个数,使用 shift 命令将这些参数移除,保证后面的代码可以用 $1$2 等处理命令的主参数。

变量当作命令的参数时,有时希望指定变量只能作为实体参数,不能当作配置项参数,这时可以使用配置项参数终止符 --

3 退出

exit 命令用于终止当前脚本的执行,并向 Shell 返回一个退出值。

退出时,脚本会返回一个退出值。

  • 0 表示正常
  • 1 表示发生错误
  • 2 表示用法不对
  • 126 表示不是可执行脚本
  • 127 表示命令没有发现
  • 如果脚本被信号 N 终止,则退出值为 128 + N。简单来说,只要退出值非0,就认为执行出错。

命令执行结束后,会有一个返回值。0 表示执行成功,非 0(通常是 1)表示执行失败。环境变量 $? 可以读取前一个命令的返回值。

4 source

source 命令用于执行一个脚本,通常用于重新加载一个配置文件。

source 命令最大的特点是在当前 Shell 执行脚本,不像直接执行脚本时,会新建一个子 Shell。所以,source 命令执行脚本时,不需要 export 变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
# test.sh
echo $foo

# 当前 Shell 新建一个变量 foo
$ foo=1

# 打印输出 1
$ source test.sh
1

# 打印输出空字符串
$ bash test.sh

source 命令的另一个用途,是在脚本内部加载外部库。

1
2
3
4
5
#!/bin/bash

source ./lib.sh

function_from_lib

source 有一个简写形式,可以使用一个点(.)来表示。

1
$ . .bashrc

5 read

read 读取用户输入或者文件。

1
2
3
4
5
6
7
8
9
10
11
## 读取用户输入,存入变量 text
#!/bin/bash
echo -n "输入一些文本 > "
read text
echo "你的输入:$text"

## 读取多个用户输入
#!/bin/bash
echo Please, enter your firstname and lastname
read FN LN
echo "Hi! $LN, $FN !"
  • 如果用户的输入项少于 read 命令给出的变量数目,那么额外的变量值为空。
  • 如果用户的输入项多于定义的变量,那么多余的输入项会包含到最后一个变量中。
  • 如果 read 命令之后没有定义变量名,那么环境变量 REPLY 会包含所有的输入。
1
2
3
4
5
#!/bin/bash
# read-single: read multiple values into default variable
echo -n "Enter one or more values > "
read
echo "REPLY = '$REPLY'"

read 命令除了读取键盘输入,可以用来读取文件。

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

filename='/etc/hosts'

while read myline
do
  echo "$myline"
done < $filename

上面的例子通过 read 命令,读取一个文件的内容。done 命令后面的定向符 <,将文件内容导向 read 命令,每次读取一行,存入变量 myline,直到文件读取完毕。

参数如下:

  • -t,超时秒数

    1
    2
    3
    4
    5
    6
    7
    8
    
    #!/bin/bash
        
    echo -n "输入一些文本 > "
    if read -t 3 response; then
      echo "用户已经输入了"
    else
      echo "用户没有输入"
    fi
    
  • -p,提示信息

  • -a,用户输入赋值给数组

  • -n,只读取若干字符作为变量值

  • -e,允许用户输入的时候使用 readline 库提供的快捷方式,例如自动补全

  • -d delimiter,指定 delimiter 的第一个字符作为用户输入的结束而不是换行符

  • -r,raw 模式,不解释转义字符

  • -s,用户输入不显示在屏幕上,用于密码输入

IFS

read 命令读取的值,默认是以空格分隔。可以通过自定义环境变量 IFS(内部字段分隔符,Internal Field Separator 的缩写),修改分隔标志。

IFS 的默认值是空格、Tab 符号、换行符号,通常取第一个(即空格)。

如果把 IFS 定义成冒号(:)或分号(;),就可以分隔以这两个符号分隔的值,这对读取文件很有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# read-ifs: read fields from a file

FILE=/etc/passwd

read -p "Enter a username > " user_name
file_info="$(grep "^$user_name:" $FILE)"

if [ -n "$file_info" ]; then
  IFS=":" read user pw uid gid name home shell <<< "$file_info"
  echo "User = '$user'"
  echo "UID = '$uid'"
  echo "GID = '$gid'"
  echo "Full Name = '$name'"
  echo "Home Dir. = '$home'"
  echo "Shell = '$shell'"
else
  echo "No such user '$user_name'" >&2
  exit 1
fi

上面例子中,IFS 设为冒号,然后用来分解 /etc/passwd 文件的一行。IFS 的赋值命令和 read 命令写在一行,这样的话,IFS 的改变仅对后面的命令生效,该命令执行后IFS会自动恢复原来的值。如果不写在一行,就要采用下面的写法。

1
2
3
4
OLD_IFS="$IFS"
IFS=":"
read user pw uid gid name home shell <<< "$file_info"
IFS="$OLD_IFS"

另外,上面例子中,<<< 是 Here 字符串,用于将变量值转为标准输入,因为 read 命令只能解析标准输入。

6 条件判断

if 条件判断的基本结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if commands; then
  commands
[elif commands; then
  commands...]
[else
  commands]
fi

## 或者删除分隔符 ; 把 then 放在下一行
if commands
then
  commands
[elif commands
then
  commands...]
[else
  commands]
fi

## 或者全部放在一行
if commands; then commands; fi

if 后面是主要的判断条件(true false),或者是命令(命令执行成功,即返回值是 0 表示判断条件成立)。

if 后面可以跟任意数量的命令。这时所有命令都会执行,但是判断条件是否成立只取决于最后一个命令,即使前面所有命令都失败,只要最后一个命令返回 0,就会执行 then 的部分。

if 的判断条件使用 test 命令,有三种形式:

1
2
3
4
5
6
7
8
# 写法一
test expression

# 写法二
[ expression ]

# 写法三,支持正则判断
[[ expression ]]

6.1 判断表达式

文件判断

  • [ -a file ]:如果 file 存在,则为true
  • [ -b file ]:如果 file 存在并且是一个块(设备)文件,则为true
  • [ -c file ]:如果 file 存在并且是一个字符(设备)文件,则为true
  • [ -d file ]:如果 file 存在并且是一个目录,则为true
  • [ -e file ]:如果 file 存在,则为true
  • [ -f file ]:如果 file 存在并且是一个普通文件,则为true
  • [ -g file ]:如果 file 存在并且设置了组 ID,则为true
  • [ -G file ]:如果 file 存在并且属于有效的组 ID,则为true
  • [ -h file ]:如果 file 存在并且是符号链接,则为true
  • [ -k file ]:如果 file 存在并且设置了它的“sticky bit”,则为true
  • [ -L file ]:如果 file 存在并且是一个符号链接,则为true
  • [ -N file ]:如果 file 存在并且自上次读取后已被修改,则为true
  • [ -O file ]:如果 file 存在并且属于有效的用户 ID,则为true
  • [ -p file ]:如果 file 存在并且是一个命名管道,则为true
  • [ -r file ]:如果 file 存在并且可读(当前用户有可读权限),则为true
  • [ -s file ]:如果 file 存在且其长度大于零,则为true
  • [ -S file ]:如果 file 存在且是一个网络 socket,则为true
  • [ -t fd ]:如果 fd 是一个文件描述符,并且重定向到终端,则为true。 这可以用来判断是否重定向了标准输入/输出/错误。
  • [ -u file ]:如果 file 存在并且设置了 setuid 位,则为true
  • [ -w file ]:如果 file 存在并且可写(当前用户拥有可写权限),则为true
  • [ -x file ]:如果 file 存在并且可执行(有效用户有执行/搜索权限),则为true
  • [ file1 -nt file2 ]:如果 FILE1 比 FILE2 的更新时间最近,或者 FILE1 存在而 FILE2 不存在,则为true
  • [ file1 -ot file2 ]:如果 FILE1 比 FILE2 的更新时间更旧,或者 FILE2 存在而 FILE1 不存在,则为true
  • [ FILE1 -ef FILE2 ]:如果 FILE1 和 FILE2 引用相同的设备和 inode 编号,则为true

字符串判断

  • [ string ]:如果string不为空(长度大于0),则判断为真。
  • [ -n string ]:如果字符串string的长度大于零,则判断为真。
  • [ -z string ]:如果字符串string的长度为零,则判断为真。
  • [ string1 = string2 ]:如果string1string2相同,则判断为真。
  • [ string1 == string2 ] 等同于[ string1 = string2 ]
  • [ string1 != string2 ]:如果string1string2不相同,则判断为真。
  • [ string1 '>' string2 ]:如果按照字典顺序string1排列在string2之后,则判断为真。
  • [ string1 '<' string2 ]:如果按照字典顺序string1排列在string2之前,则判断为真。

注意,test 命令内部的 ><,必须用引号引起来(或者是用反斜杠转义)。否则,它们会被 shell 解释为重定向操作符。

整数判断

  • [ integer1 -eq integer2 ]:如果integer1等于integer2,则为true
  • [ integer1 -ne integer2 ]:如果integer1不等于integer2,则为true
  • [ integer1 -le integer2 ]:如果integer1小于或等于integer2,则为true
  • [ integer1 -lt integer2 ]:如果integer1小于integer2,则为true
  • [ integer1 -ge integer2 ]:如果integer1大于或等于integer2,则为true
  • [ integer1 -gt integer2 ]:如果integer1大于integer2,则为true

正则判断

  • [[ string1 =~ regex ]]:regex 是正则表达式,=~ 进行正则比较

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    #!/bin/bash
        
    INT=-5
        
    if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
      echo "INT is an integer."
      exit 0
    else
      echo "INT is not an integer." >&2
      exit 1
    fi
    

逻辑运算

  • AND 运算:符号&&,也可使用参数-a
  • OR 运算:符号||,也可使用参数-o
  • NOT 运算:符号!

使用否定操作符 ! 时,最好用圆括号确定转义的范围。

1
2
3
4
5
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

上面例子中,test 命令内部使用的圆括号,必须使用引号或者转义,否则会被 Bash 解释。

算术判断

((...)) 可以进行算术运算的判断。

注意,算术判断不需要使用 test 命令,而是直接使用 ((...)) 结构。这个结构的返回值,决定了判断的真伪。

如果算术计算的结果是非零值,则表示判断成立。这一点跟命令的返回值正好相反,需要小心。

7 case

case 结构用于多值判断,可以为每个值指定对应的命令,跟包含多个 elifif 结构等价,但是语义更好。它的语法如下。

1
2
3
4
5
6
7
case expression in
  pattern )
    commands ;;
  pattern )
    commands ;;
  ...
esac

上面代码中,expression 是一个表达式,pattern 是表达式的值或者一个模式,可以有多条,用来匹配多个值,每条以两个分号(;)结尾。

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

echo -n "输入一个1到3之间的数字(包含两端)> "
read character
case $character in
  1 ) echo 1
    ;;
  2 ) echo 2
    ;;
  3 ) echo 3
    ;;
  * ) echo 输入不符合要求
esac

上面例子中,最后一条匹配语句的模式是 *,这个通配符可以匹配其他字符和没有输入字符的情况,类似 ifelse 部分。

下面是另一个例子,判断当前操作系统。

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

OS=$(uname -s)

case "$OS" in
  FreeBSD) echo "This is FreeBSD" ;;
  Darwin) echo "This is Mac OSX" ;;
  AIX) echo "This is AIX" ;;
  Minix) echo "This is Minix" ;;
  Linux) echo "This is Linux" ;;
  *) echo "Failed to identify this OS" ;;
esac

case 的匹配模式可以使用各种通配符,下面是一些例子。

  • a):匹配a
  • a|b):匹配ab
  • [[:alpha:]]):匹配单个字母。
  • ???):匹配3个字符的单词。
  • *.txt):匹配.txt结尾。
  • *):匹配任意输入,通过作为case结构的最后一个模式。
1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

echo -n "输入一个字母或数字 > "
read character
case $character in
  [[:lower:]] | [[:upper:]] ) echo "输入了字母 $character"
                              ;;
  [0-9] )                     echo "输入了数字 $character"
                              ;;
  * )                         echo "输入不符合要求"
esac

上面例子中,使用通配符 [[:lower:]] | [[:upper:]] 匹配字母,[0-9] 匹配数字。

Bash 4.0之前,case 结构只能匹配一个条件,然后就会退出 case 结构。Bash 4.0之后,允许匹配多个条件,这时可以用 ;;& 终止每个条件块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# test.sh

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

执行上面的脚本,会得到下面的结果。

1
2
3
4
5
6
$ test.sh
Type a character > a
'a' is lower case.
'a' is alphabetic.
'a' is a visible character.
'a' is a hexadecimal digit.

可以看到条件语句结尾添加了 ;;& 以后,在匹配一个条件之后,并没有退出 case 结构,而是继续判断下一个条件。

License:署名-相同方式共享 3.0

This post is licensed under CC BY 4.0 by the author.