[toc]
阮一峰老师写的教程,这里做个精简笔记。
1 首行
脚本第一行通常通过 #! 来指定解释器,即该脚本通过什么程序来执行。
1
#! /usr/bin/env bash
以上命令使用 env 命令(这个命令总是在 /usr/bin 目录),返回 Bash 可执行文件的位置。
env 命令总是指向 /usr/bin/env 文件,或者说,这个二进制文件总是在目录 /usr/bin,env 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是$1,foo是$2,bar是$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 是一个变量名,用来保存当前取到的配置项参数,即 l、h 或 a。
下面是一个例子。
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 保存的是,当前处理的那一个连词线参数(即 l、h 或 a)。如果用户输入了没有指定的参数(比如 -x),那么 OPTION 等于 ?。循环体内使用 case 判断,处理这四种不同的情况。
如果某个连词线参数带有参数值,比如 -a foo,那么处理 a 参数的时候,环境变量 $OPTARG 保存的就是参数值。
注意,只要遇到不带连词线的参数,getopts 就会执行失败,从而退出 while 循环。比如,getopts 可以解析command -l foo,但不可以解析 command foo -l。另外,多个连词线参数写在一起的形式,比如 command -lh,getopts 也可以正确处理。
变量 $OPTIND 在 getopts 开始执行前是 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 ]:如果string1和string2相同,则判断为真。[ string1 == string2 ]等同于[ string1 = string2 ]。[ string1 != string2 ]:如果string1和string2不相同,则判断为真。[ 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 结构用于多值判断,可以为每个值指定对应的命令,跟包含多个 elif 的 if 结构等价,但是语义更好。它的语法如下。
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
上面例子中,最后一条匹配语句的模式是 *,这个通配符可以匹配其他字符和没有输入字符的情况,类似 if 的 else 部分。
下面是另一个例子,判断当前操作系统。
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):匹配a或b。[[: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