[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