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
$
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
的。
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 内建命令,用于读取指定的内容到数组内。