bash shell 这种古老而又古怪的语言,虽然在处理日常的工作时能很大提升效率,但是其一些语法是真的很难像其他语言一样信手拈来的就使用。所以这篇文章记录的是我日常工作中经常容易忘记的部分。
目录和文件
遍历目录和对目录中的文件进行操作时很常见的。很多情况下我们深度遍历一个目录可能会用两个for
循环,但是实际上我们可以有几种不同的方式来完成这些任务。
find
find . // 查找当前目录下所有的文件
find . -name "*.txt" -type f //find all txt file
find . -type f -name "*.txt" -print0 | xargs -0 fgrep text
for
正常的思路是使用for
循环去遍历,使用for
循环也是有一定的技巧的,正常的情况是使用一个递归的方式遍历目录,但是在bash4
的版本下,可以开启globstar
或者dotglob
这两个选项,区别在于是否要匹配隐藏的文件。
shopt -s globstar || exit
for f in **
do
if [ "$f" =~ \.txt$ ]; then
echo "$f"
fi
done
这里面用到了两个技巧,一个是如果不支持globstar
就退出脚本执行,另一个是if
的正则功能支持。话说回来,如果系统不支持这样的选项,那我们只能回到一个原始的方式了。
function walk_tree()
{
for f in "$1"; do
if [ -d "$f"]; then
walk_tree "$f"
else
fullpath=`readlink -f "$f"`
if [ "$fullpath" =~ \.txt$ ]; then
echo "$fullpath"
fi
done
}
有两个可以注意的点是:readlink -f
可以输出文件的完整路径。同时$f
本身会带有相对的路径,所以不需要再传路径。实际上如果想要一行就解决问题find
是首选的。其次不需要使用递归也是一个非常棒的方式。find
配合上exec
和xargs
同样可以执行一些简单的命令。
重定向
重定向在简单的使用上没有什么问题,但是涉及到文件描述符的复制时,往往会一脸懵。毕竟我们大多数时间使用的是>
,>>
这两个。下面是简单的一些记录很处理。
描述符复制
n>&m
将描述符 n 指向 m 所指向的位置。
我们一般使用exec
来执行描述符之间的复制。
$ exec 3 < file
$ exec 4>&3
$ read -u 4 line
$ echo "$line"
上面的将文件 file 打开并用描述符 3 表示。接着用描述符 4 来复制描述符 3。接着从描述符 4 中读取内容,这个内容就是 file 中的内容了。这就表示此时描述符 3 和描述符 4 指向了同一个位置。
重定向顺序
2>&1 >foo
描述符 2 和 描述符 1 指向不同的位置。
>foo 2>&1
描述符 2 和 描述符 1 指向相同的位置。
关闭描述符
- n<&- 关闭一个输入的文件描述符
- n>&- 关闭一个输出的文件描述符
read 命令中使用重定向
常规使用 read 读取文件中所有的内容时,我们会使用一个简单的重定向,简单的示例代码如下:
while read -r line; do
echo "$line"
done < file
然而我们此时我们想在循环体内容再一次使用 read 读取标准输入时,就会出错了。这时使用指定描述符的方式可以解决这个问题。
exec 3 < file
while read -u 3 line; do
echo "$line"
read -p "continue to read?" -n 1
done
这样就能够很好区分开不同的描述符,不至于使得后面的使用会出错。
Here document 和 Here strings
实际上这两者都是基于重定向的,不过在某些时候还是很有用的。比如 here document 在输出 usage 的时候就有很好的使用,但是这里面的使用还是有一定的技巧的。
Here document
command <<[-]word
...
...
word
here document 的使用中有两个技巧,从上面的语法描述上能够看到那个可选的-
。使用<<-
会将文本中的前导 tab 都删除,这就意味着文本不是原样输出。
另一个技巧时在 word 上。一般我们会取一个关键字用来标记输入的结束,但是这个 word 如果加上了单引号,比如'EOF'
这个会抑制后面中的变量的展开。简单的示例如下:
$ cat << 'EOF'
> This is my name $name
> EOF
This is my name $name
同时 Here document 也是可以在管道中使用的,简单的示例如下:
$ cat << 'EOF' | sed 's/a/b/g'
> abc
> nab
> EOF
bbc
nbb
还有一个比较常见的需求是在脚本中将 cat 的内容输入到文件中,我们可以使用重定向。下面的示例是在脚本中使用:
cat << EOF > filename
aaaa
bbb
ccc
EOF
Here strings
大部分情况下使用 here string 主要解决在出了管道之后,还能继续使用变量。我们知道管道等是在 subshell 中使用的。所以有些变量在出了这个作用域之后就不存在了。
$ echo "Hello World" | read first second
$ echo "$first" "$second"
# nothing
此时使用 here strings 则就很合适了。
$ read first second <<< "hello world"
$ echo "$first" "$second"
hello world
数学计算
数学计算在 shell 中有几种书写方式,但是太多的方式总会让人不知所措,所以还是只精通一种最好用的。实际上现在我们使用的最多的是(( ))
, 有时我们还会使用$(( ))
这个是 POSIX 的一个形式。
bash 中有一个语法,用来转换进制的。即:<base>#number
我们可以直接在$(( ))
中使用。其次在数学计算符中可以不用使用$
符来引用变量。
$ ((a=1, a+=2))
$ echo $a
3
$ printf '%d\n' $((1+3))
4
在$(( ))
中也是支持变量操作的。比如:
$ ((a=16#abc, b=16#${a:0:2})); printf '%s, %s\n' $a $b
2748, 39
注意并不是所有的操作都是合法的,也有一些情况需要我们注意到。比如下面的例子中:
$ x=1
$ echo $(($x[0])) # 将会被扩展为 $((1[0]))
bash: 1[0]: syntax error: invalid arithmetic operator (error token is "[0]")
$ printf '%d\n' $((${x[0]}))
1
$ printf '%d\n' $(("$x" == 1)) # 解析为 $(("1"))
1
此外,我们也可以用变量扩展作为布尔值的判断。比如:
if ((1 == 2)); then
echo "true"
else
echo "false"
fi
# false
echo 输出
echo 在使用-e
的时候可以支持转义字符的输出,但是如果不想使用这个标记的时候,可以通过
#39;string'
的方式来进行。比如:$ echo "This is a line"$'\n'
This is a line
$
-e
问题
在日常编写脚本的过程中,我们习惯于使用 echo 来完成内容的输出。大部分情况下 echo 是可以正常工作的。但是有时 echo 会因为一些奇怪的问题出现一些不可预期的结果。
$ a=-e
$ echo $a
$
此时便什么都不会输出,即使我们对变量加上"$a"
也不会输出任何内容,同样的, 如果变量的内容是-n/-E
也会有同样的问题。这些内容是 echo 的相关选项。同样的问题在 zipecho 命令会有,因为这个命令中使用了 echo 和 sed 的组合。
因此比较推荐的是使用 printf 作为首选的打印输出。如果实在想用 echo ,上面的问题也是有一个规避方式的,即:
$ a=-e
$ echo "$a "
-e
在变量后增加一个空格,此时则会被解释为字符串,而不是一个选项。
trap
trap 一般用于脚本退出时的一些状态清理工作,在捕捉到一定的信号后作出对应的作用。比较常用的是监听 EXIT 的事件。一个简单的例子:
trap '[ $? -eq 0 ] || dosomething' EXIT
split
split 在 shell 中并不是原生支持,需要自己实现一个函数,一般使用 read 来实现。
split() { # Usage: split "string" "delimiter" IFS=$'\n' read -d "" -ra arr <<< "${1//$2/
#39;\n'}" printf '%s\n' "${arr[@]}" }
这种方式在正常场景下并不会有问题,但是当我们设置了set -e
后,上述实现就会提前退出。简单解释一下:
$ read -d '' <<< 'Hello World'
$ echo $?
1
这个方式在 while 循环中是非常友好的方式,但是由于设置set -e
, 存在返回值不为0 的时候,程序会自动退出。这也就意味着 read 之后就直接 exit 了,所以我们需要简单的规避一下:
split() { # Usage: split "string" "delimiter" IFS=$'\n' read -d "" -ra arr <<< "${1//$2/
#39;\n'}" || true printf '%s\n' "${arr[@]}" }
通过||
将返回值重新变成 0 且逻辑上也是符合需求的。
tee redirection
在 shell 中实现 tee 对 stdout 和 stderr 的重定向,可以使用如下的方式:
command > >(tee -a stdout.log) 2> >(tee -a stderr.log >&2)
其中,>(...) (process substitution)
创建一个 FIFO,同时将 command 的输出重定向到这个 FIFO 中。
find
我们通常需要将 find 的结果放到一个 array 中。如果 bash 版本的比较低的时候,我们可以用通过如下的方式进行:
array=()
while IFS= read -r -d $'\0'; do
array+=("$REPLY")
done < <(find * -type d -print0)
for item in "${array[@]}"; do
echo "$item"
done
# output
# dirl
# dir2
上面是查找当前目录下的所有的目录类型。 注意这边 find 用的是*
。这会去掉结果前面的./
这个前缀。如果我们的 bash 版本在 4.4 以上,我们可以通过下面的一行命令完成。
$ mapfile -d $'\0' array < <(find * -type d -print0)
# or
$ readarray -d '' array2 < <(find * -type d -print0)
set
-x
在日常脚本中我们需要调试时,可以在脚本的开头或者是需要调试的函数附近加上set -x
。这样在此之后的脚本都会以调试模式输出,在不想调试的代码前加上set +x
以关闭调试功能。
在现代的 bash 脚本,支持在脚本内可以直接将调试的输出重定向到指定的文件内,方式如下:
#!/bin/bash
exec 19>logfile
BASH_XTRACEFD=19
set -x
command1
command2
...
其中BASH_XTRACEFD
是用来指定文件描述符给set -x
的。
-e
Abort script at first error, when a command exits with non-zero status (except in until or while loops, if-tests, list constructs)
-e Exit immediately if a pipeline (which may consist of a single simple command), a list, or a compound command (see SHELL GRAMMAR above), exits with a non-zero status.
The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test following the if or elif reserved words, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return value is being inverted with !.
If a compound command other than a subshell returns a non-zero status because a command failed while -e was being ignored, the shell does not exit.
A trap on ERR, if set, is executed before the shell exits. This option applies to the shell environment and each subshell environment separately (see COMMAND EXECUTION ENVIRONMENT above), and may cause subshells to exit before executing all the commands in the subshell. If a compound command or shell function executes in a context where -e is being ignored, none of the commands executed within the compound command or function body will be affected by the -e setting, even if -e is set and a command returns a failure status. If a compound command or shell function sets -e while executing in a context where -e is ignored, that setting will not have any effect until the compound command or the command containing the function call completes.
其中需要注意的是:The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test following the if or elif reserved words, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return value is being inverted with !.这种情况下会失效。
比如:
set -e
false && true; echo "will print"
true && false; echo "not print"
true && false || true; echo "will print"
这是因为使用了&&
之后,shell 会认为&&
左侧部分是被 test 过了,再加上短路原则,右侧部分不会执行到,所以set -e
便没有被触发。 实际上&&
是一个list constructs。这种情况下就不会触发set -e
了。
check file
Linux 系统上,一切皆文件,但是文件也有不同的类型,当我们需要在脚本中判断某个文件是否存在时,可以通过内置的 flag 来判断:
- -f file(not directory or device file)
- -d directory
- -c character device
- -b block device
- -p named pipe
- -S socket
- -e file exists
- -r readable
- -w writable
- -x executable
- -s file is not zero size
- -O you are owner of file
- -G group-id of file same as yours
- -N file modified since it was last read
if [ -f /etc/passwd ]; then
echo "File exists"
fi
Parameters Substitution
+ ${var} Value of var (same as $var)
+ ${var-$DEFAULT} If var not set, evaluate expression as $DEFAULT *
+ ${var:-$DEFAULT} If var not set or is empty, evaluate expression as $DEFAULT *
+ ${var=$DEFAULT} If var not set, evaluate expression as $DEFAULT *
+ ${var:=$DEFAULT} If var not set or is empty, evaluate expression as $DEFAULT *
+ ${var+$OTHER} If var set, evaluate expression as $OTHER, otherwise as null string
+ ${var:+$OTHER} If var set, evaluate expression as $OTHER, otherwise as null string
+ ${var?$ERR_MSG} If var not set, print $ERR_MSG and abort script with an exit status of 1.*
+ ${var:?$ERR_MSG} If var not set, print $ERR_MSG and abort script with an exit status of 1.*
+ ${!varprefix*} Matches all previously declared variables beginning with varprefix
+ ${!varprefix@} Matches all previously declared variables beginning with varprefix
array as parameter
在 bash 中将数组作为函数参数传递时,方式与其他的语言略有不同。具体操作如下:
copyfiles() {
dst=$1
shift
srcs=($@)
for src in "$srcs{@}";do
.......
done
}
copyfiles "$dst" "${srcs[@]}"
readarray
在 bash 4+ 版本里面引入了 readarray 内建命令,用于读取指定的内容到数组内。
debug shell script
$ (exec 111> log ;
export SHELLOPTS BASH_XTRACEFD=111 PS4='($BASH_SOURCE:$LINENO:$FUNCNAME): ' ;
set -x ; ./cih.sh)