Shell 脚本简单速查
管道与重定向
ls -l > out.txt # 重定向输出(覆盖)
ls -l >> out.txt # 重定向输出(追加)
# 0: 标准输入
# 1: 标准输出
# 2: 标准错误输出
kill -HUP 123 >out.txt 2>err.txt # 标准输出与错误输出分别重定向
# >& 操作符用于结合输出
kill -1 123 >out.txt 2>&1 # 标准输出与错误输出重定向到同一个文件
more < out.txt # 重定向输入
# 管道连接的进程可以同时运行,并随着数据流自动的进行协调。
ps -xo comm | sort | uniq | grep -v sh | more
# here 文档
cat <<!EOF! # << 为标签重定向符,标签是一个特殊序列不能出现在文档中
hello
world
!EOF!
语法
变量
默认情况下,所有变量都被看作字符串,并以字符串来存储。
变量名区分大小写,通过变量名前添加 $
符号进行访问。
hello=World # 注意 = 左右不能有空格
echo $hello
read userinput # read 命令将用户输入赋值给一个变量
echo $userinput
"$hello" # 这里的变量会展开为其值
'$hello' # 不会展开变量
参数/环境变量 | 说明 |
---|---|
$0 |
脚本名称(环境变量) |
$1 $2 |
脚本程序的参数 |
$* |
所有参数,作为一个整体 |
$@ |
所有参数,各参数由 $IFS 的第一个字符分隔开,可使用 for 循环 |
$# |
传递给脚本的参数个数(环境变量) |
$$ |
脚本的进程号(环境变量) |
$? |
上一个命令的退出状态码(环境变量) |
对于第9个参数之后的参数,可以通过 ${pos}
进行访问,比如 ${10}
。
控制结构
test 命令
test
与 [
等价,只是为了可读性,使用 [
时会使用 ]
结尾。
if test -f foo; then echo 'exist'; fi
# 等价于
if [ -f foo ]; then echo 'exist'; fi
# 字符串比较(大于号和小于号需要转义)
str1 = str2 # 两字符串是否相同
str1 != str2 # 两字符串是否不同
str1 < str2 # str1 是否小于 str2
str1 > str2 # str1 是否大于 str2
-n str # 字符串长度是否非0
-z str # 字符串长度是否为0
# 算术比较
exp1 -eq exp2 # 两表达式相等
exp1 -ne exp2 # 两表达式不等
exp1 -gt exp2 # 大于
exp1 -ge exp2 # 大于等于
exp1 -lt exp2 # 小于
exp1 -le exp2 # 小于等于
! exp # 取反
# 文件测试
-d file # file 是否存在且为目录
-e file # file 是否存在(-e 不可移植,通常使用 -f)
-f file # file 是否存在且为普通文件
-s file # file 是否存在且为非空
-r file # file 是否存在且可读
-w file # file 是否存在且可写
-x file # file 是否存在且可执行
-O file # file 是否存在且属当前用户所有
-G file # file 是否存在且默认组与当前用户相同
-g file # file 是否存在且 set-group-id 位置位
-u file # file 是否存在且 set-user-id 位置位
file1 -nt file2 # file1 是否比 file2 新
file1 -ot file2 # file1 是否比 file2 旧
双括号命令(Bash 提供)
双括号命令 (( expression ))
相较于 test 允许使用高级数学表达式。
双括号表达式中的大于号和小于号无需转义。
val++ # 后增
val-- # 后减
++val # 先增
--val # 先减
! # 逻辑取反
~ # 位取反
** # 幂运算
<< # 左移位
>> # 右移位
& # 按位与
| # 按位或
&& # 逻辑与
|| # 逻辑或
双方括号(Bash 提供)
双方括号 [[ expression ]]
相较于 test 提供了针对字符串比较的高级特性(模式匹配)。
==
使用右侧的正则表达式对左侧进行匹配。
if [[ $USER == r* ]]; then
echo "Hello $USER"
fi
# expression 可以为 test 命令采用的标准字符串比较
if [[ $str1 = $str2 ]]; then
echo "$str1 equal $str2"
fi
命令列表 AND OR
命令退出码为 0 表示成功。
&&
与 ||
连接的命令执行时为短路径求值 (short circuit evaluation)。
# AND 列表
statement1 && statement2 && statement3 && ...
# 当 foo 文件存在时才执行 echo 'hello' 命令,即短路与
if [ -f foo ] && echo 'hello'; then
echo 'True'
fi
# OR 列表
statement1 || statement2 || statement3 || ...
# 当 foo 文件不存在时才执行 echo 'hello' 命令,即短路或
if [ -f foo ] || echo 'hello'; then
echo 'True'
fi
语句块
当要在只允许使用单个语句的地方(如 AND 或 OR 列表中)使用多条语句,可以使用 {}
构造一个语句块。
[ -f foo ] && {
echo 'hello'
echo 'world'
}
if 语句
注意: condition 通常采用 test 命令,condition 命令的退出码决定如何执行条件代码。
condition 可以为复合条件测试:[ cond1 ] && [ cond2 ]
和 [ cond1 ] || [ cond2 ]
。
##### 语法结构 ##########
if condition
then
statements
else
statements
fi
########################
# 给变量加上引号,用于防止空变量导致的错误
if [ "$var" = 'hello' ]; then
echo 'hello'
elif [ "$var" = 'world' ]; then
echo 'world'
else
echo 'other'
fi
case 语句
将变量的内容与模式进行匹配,然后根据匹配的模式去执行不同的代码。
##### 语法结构 ##########
case variable in
pattern [ | pattern] ...) statements;;
pattern [ | pattern] ...) statements;;
...
esac
########################
read input
case "$input" in
yes | y | Yes | YES ) echo "Yes";; # 单行写法
[nN]* ) # 多行命令写法
echo "No"
;; # ;; 相当于 break
* ) # 默认条件
echo "Other"
exit 1
;;
esac
for 语句
##### 语法结构 ##########
for variable in values
do
statements
done
########################
for foo in bar fud 43; do
echo $foo
done
for file in $(ls f*.sh); do
echo $file
done
# for 默认使用空格划分列表的值,当在直接在 for 中写列表时需要注意空格和引号
# 对于保存在变量中的列表,一般无需关心对引号转义
for test in I don't know if this'll work "hello world"; do
echo "word:$test"
done
# Output:
# I
# dont know if thisll
# work
# hello world
环境变量 IFS
被称为内部字段分隔符 (internal field separator),其定义了 Bash 用作字段分隔符的一系列字符。 默认情况下 Bash 的字段分隔符为:空格、制表符、换行符。
IFS=$'\n' # 修改字段分隔符
for f in $(ls -l); do
echo $f
done
# for 命令可以使用通配符遍历目录
for file in /path/*; do
echo $file
done
Dollar-sign quote$'
会将$'string'
中的转义字符进行标准替换,但不会对其中的变量进行展开。
Dollar-sign double-quote $"
用于本地化。
C 语言风格 for 命令
for (( variable assignment; condition; iteration process ))
需要注意以下几点:
- 变量赋值可以有空格
- 条件中的变量不以
$
开头 - 迭代过程的算式为使用 expr 命令格式
for (( i = 1; i <= 10; i++ )); do
echo $i
done
# 使用多个变量
for (( a=1, b=10; a <= 10; a++, b-- )); do
echo $a, $b
done
while 语句
当条件为真时反复执行。
##### 语法结构 ##########
while condition
do
statements
done
########################
while [ "$input" != "secret" ]; do
echo "Sorry, try again"
read input
done
# while 命令可以使用多个测试命令,每次迭代时都会执行所有测试命令,
# 并以最后一个测试命令的退出状态码做为判断依据。
# 每个测试命令出现在单独的一行上。
while echo $var
[ $var -ge 0 ]
do
...
done
until 语句
与 while
类似,循环反复执行,直到条件为真。
同样的 until 命令也支持多个测试命令。
##### 语法结构 ##########
until condition
do
statements
done
########################
# 当某个特定用户登录时输出提示
until who | grep "$1" > /dev/null; do
sleep 60
done
echo "$1 has just logged in!"
如果要对循环的输出使用管道或者重定向,可以在done
命令之后添加处理命令。比如done > out.txt
或done | sort
。
函数
函数在调用之前必须先定义。
当一个函数被调用时,位置参数与相关环境变量($*
$@
$#
$1
$2
等)会被替换为函数的参数。当函数执行完毕后,这些参数会恢复之前的值。
函数可以通过 return
命令返回一个数字值,返回字符串的方式可以使用 echo
命令,或将返回值存在一个变量中。
如果函数没有使用 return
指定返回值,则默认返回函数中最后一条命令的退出码。
##### 语法结构 ##########
function_name() {
statements
}
########################
foo() {
local text='Hello' # 声明局部变量,仅在函数的作用域内有效
echo $text
}
foo # 调用函数
res = $(foo) # 捕获函数返回的字符串
foo1() {
echo "param is $1" # 获取参数
if [ "$1" = "true" ]; then
return 0 # 返回 0 表示 true
else
return 1 # 返回非 0 表示 false
fi
}
param="true"
if foo1 "$param"; then # 带参数调用
echo "True"
else
echo "False"
fi
命令
Shell 脚本内使用的命令分为两种
- 外部命令:独立存在于 Shell 之外的命令
- 内部命令:Shell 内置的,不能作为外部程序被调用。不过按照 POSIX 标准,大多数内部命令也同时提供了独立运行的程序。
使用 $(command)
捕获一条命令的执行结果,是命令的输出,而非退出码。
# break 命令
# ---------------------
break number # number 指示跳出的循环层数,不指定默认只跳出一层循环
# continue 命令
# ---------------------
continue number # number 指示继续执行的循环嵌套层数,一般不使用
# : 命令是一个空命令
# ---------------------
# 偶尔被用于简化逻辑,此时相当于 true 的别名
while :
do
...
done
: ${var:=value} # 如果没有 : ,shell 将试图把 $var 当作一条命令来处理
# . 命令
# ---------------------
# 通常,当一个脚本执行一条外部命令或脚本程序时,会创建一个新环境(子shell),
# 命令将在这个新环境中执行,执行完毕后新环境被丢弃,退出码返回给父 shell
# . 命令/source 在执行命令时使用的是当前 shell
. ./script # 在当前 shell 中运行 ./script,使得脚本程序可以改变当前 shell 中的环境设置
# echo 命令
# ---------------------
echo -n "hello" # 输出并去掉自带换行符
echo -e "hello \c" # 启用转义并去掉自带换行符
# eval 命令
# ---------------------
# 对参数进行求值
foo=10; x=foo; y='$'$x; echo $y # 输出 $foo
foo=10; x=foo; eval y='$'$x; echo $y # 输出 10
# exec 命令
# ---------------------
exec wall "Hello" # 将当前 shell 替换为另一个程序
exec 3< ./file # 打开文件操作符 3 以便从 ./file 中读取数据
# exit 命令
# ---------------------
exit n # 使脚本程序以退出码 n 结束运行,0 表示成功,1-125 为可使用代码,其余数字系统保留
[ -f file ] && exit 0 || exit 1
# export 命令
# ---------------------
# 默认在一个 shell 中创建的变量在子 shell 中不可用
# export 将作为其参数的变量导出到子 shell 中
# 更确切的说,被导出的变量构成从该 shell 衍生的任何子进程的环境变量
export hello="world"
# printf 命令
# ---------------------
# X/Open 规范建议使用 printf 代替 echo
# 与 C 语言中 printf 类似,最大的不同为不支持浮点数
printf "format string" param1 param2 ...
printf "%s %d\t%s" "Hello" 10 world
# return 命令
# ---------------------
# 使函数返回,指定的参数被看作函数的返回值,不指定则默认返回最后一条命令的退出码
return n
# set 命令
# ---------------------
# 为 shell 设置参数变量
set $(date) # 将命令输出设置为参数变量
echo "The month is $2"
# 控制 shell 的执行方式
set -x # 开启显示当前执行的命令
# unset 命令
# ---------------------
# 从环境中删除变量或函数,不能删除 shell 本身定义的只读变量(如 IFS)
foo = "Hello"
unset foo # 删除变量 foo
# shift 命令
# ---------------------
# 将所有参数变量左移一个位置,原来 $1 的值被丢弃,$0 保持不变,相应的 $* $@ $# 等也会进行相关变动
shift n # 如果指定数值参数,则表示左移次数,不指定默认为 1
while [ "$1" != "" ]; do
echo "$1" # 依次输出所有位置参数
shift
done
# trap 命令
# ---------------------
# 用于指定在接收到信号后要采取的行动
trap command signal
# X/Open 规定的能被捕获的一些比较重要的信号
# HUP(1) 挂起,通常因终端掉线或用户退出引发
# INT(2) 中断,通常因按下 Ctrl+C 引发
# QUIT(3) 退出,通常因按下 Ctrl+\ 引发
# ABRT(6) 中止,通常因某些严重的执行错误引发
# ALRM(14) 报警,通常用来处理超时
# TERM(15) 终止,通常在系统关机时发送
trap - signal # 重置 signal 的处理方式到默认值
trap signal # 忽略 signal
date > /tmp/tmp_file_$$
trap 'rm -f /tmp/tmp_file_$$' INT # 在要保护的代码钱指定 trap 命令
while [ -f /tmp/tmp_file_$$ ]; do
echo "wait interrupt (CTRL-C)" # Ctrl-C 触发中断,执行相应操作
sleep 1
done
echo "File no longer exists"
expr 命令
将其参数当作一个表达式来求值。最常见的用法就是进行简单数学运算。
在较新的脚本程序中,expr
通常被替换为更有效的 $((...))
语法。
# `` 和 $() 为命令替换
x=`expr $x + 1` # `` 用于给变量取值
x=$(expr $x + 1) # 等价于上边
# expr 常用的求值计算
expr1 & expr2 # 任一个表达式为 0 则为 0,否则为 expr1
expr1 | expr2 # expr1 非 0 则为 expr1,否则为 expr2
expr1 = expr2 # 等于
expr1 > expr2 # 大于
expr1 >= expr2 # 大于等于
expr1 < expr2 # 小于
expr1 <= expr2 # 小于等于
expr1 != expr2 # 不等于
expr1 + expr2 # 加
expr1 - expr2 # 减
expr1 * expr2 # 乘
expr1 / expr2 # 除
expr1 % expr2 # 取余
算术扩展
expr
命令可以处理一些简单的算数命令,但执行起来比较慢,一般建议采用 $((...))
扩展。
$[...]
与 $((...))
相同,前者已经被弃用,尽可能使用后者。
x=0
while [ "$x" -ne 10 ]; do
echo $x
x=$(($x+1)) # 累加
done
参数扩展
for i in 1 2; do
some_process $i_tmp # shell 试图替换变量 $i_tmp 的值
some_process ${i}_tmp # shell 会使用变量 i 的值替换 ${i}
done
# 常见参数扩展方法
${param:-default} # 如果 param 为空,则为 default
${param:=default} # 如果 param 为空,将 param 赋值为 default
${param:?msg} # 如果 param 为空,则显示 msg 并退出脚本
${param:+default} # 如果 param 不为空,则为 default
${#param} # 给出 param 的长度
${param%word} # 从 param 的尾部删除 word 的最短匹配,然后返回剩余部分
${param%%word} # 从 param 的尾部删除 word 的最长匹配,然后返回剩余部分
${param#word} # 从 param 的头部删除 word 的最短匹配,然后返回剩余部分
${param##word} # 从 param 的头部删除 word 的最长匹配,然后返回剩余部分
# 如果需要在 ${} 内使用变量(展开),需要将 $ 换成 !
hello=world
world=ok
echo ${!hello}
# Output: ok
脚本调试
设置 shell 选项的方式可以在调用 shell 时加上命令行选项,或使用 set
命令。
命令行选项 | set 命令 | 说明 |
---|---|---|
sh -n <script> |
set -o noexec set -n |
只检查语法错误,不执行命令 |
sh -v <script> |
set -o verbose set -v |
在执行命令之前回显 |
sh -x <script> |
set -o xtrace set -x |
在执行命令之后回显 |
sh -u <script> |
set -o nounset set -u |
如果使用了未定义的变量,就给出出错消息 |
set -o xtrace
表示启用,set +o xtrace
表示取消设置。
set -- args
将当前命令行参数替换为 args。
getopt
命令格式 getopt optstring parameters
optstring 定义命令行中有效的选项字母,如果选项有参数,则在其后加个冒号,比如 ab:c
。
getopt 命令不擅长处理带空格和引号的参数值,它会将空格作为参数分隔符且无视引号。
getopt ab:cd -a -b test1 -cd test2 test3
# Output: -a -b test1 -c -d -- test2 test3
# -q 选项可以抑制 getopt 输出错误信息
使用示例:
set -- $(getopt -q ab:cd "$@")
while [ -n "$1" ]; do
case "$1" in
-a) echo "Found the -a option" ;;
-b) param="$2"
echo "Found the -b option, with parameter value $param"
shift ;;
-c) echo "Found the -c option" ;;
--) shift
break ;;
*) echo "$1 is not an option";;
esac
shift
done
count=1
for param in "$@"; do
echo "Parameter #$count: $param"
count=$(($count+1))
done
getopts
Bash 内建命令,一次只处理命令行上检测到的一个参数,当所有参数处理完毕后退出,且返回一个大于0的退出状态码。
命令格式 getopts optstring variable
optstring
: 选项字母序列,对于有参数的选项,在其后加个冒号。如需取消错误消息,则在 optstring 之前加个冒号。variable
: 当前参数名。$OPTARG
: 当前参数值。$OPTIND
: 当前参数在参数列表中的位置。
使用示例:
while getopts :ab:cd opt; do
case "$opt" in
a) echo "Found the -a option" ;;
b) echo "Found the -b option, with value $OPTARG" ;;
c) echo "Found the -c option" ;;
d) echo "Found the -d option" ;;
*) echo "Unknown option: $opt" ;;
esac
done
shift $(($OPTIND - 1))
count=1
for param in "$@"; do
echo "Parameter $count: $param"
count=$(($count+1))
done