资料
ABS:http://www.tldp.org/LDP/abs/html
在线 Bash 手册页:https://www.gnu.org/software/bash/manual/bash.html
Bash 手册页 man bash
常见错误:http://mywiki.wooledge.org/BashPitfalls
AWK 手册:https://www.gnu.org/software/gawk/manual/html_node/index.html
问题
- 函数如何传递返回值?写全局变量不好,$()的方式会影响返回状态的捕获
常见的方法还是命令替换捕获输出的方式 -
:(){ :|:& }();:
带有管道的后台到底把哪个进程放到后台了? - 如何提取文本中的正则匹配项?
简单的用grep -o
,复杂的可使用 awk 的match
函数
*POSIX Character Classes 为什么还要加一个方括号?[[:alpha:]]
陷阱
- ssh 远程命令无法返回。
原因:ssh 需要确定没有任何的输入输出之后才能返回。
解决:要执行的命令添加 < /dev/null 和 > /dev/null 或 使用 ssh -f - 子shell变量修改
原因:父进程中的变量在子进程中是可读的,但是当子进程写这个变量时,是不会影响到父进程的(可能是写时在子进程中创建了同名变量)。 - 预防 SIGHUP 信号
解决:使用:
nohup
setsid # 父进程为 init
( command & ) # 父进程为 init
screen # 服务器端保存会话状态 - sort 排序占满磁盘空间
原因:sort 使用的是外排序,会将中间结果保存在文件中,默认存储目录为 /tmp,可以使用 -T 执行目录
解决:-T 指定别的大的磁盘分区,量级更大的任务使用 mapreduce 等工具
一. 基础知识
Bash 环境设置
-
set -u
保证每个变量在使用前都被初始化 -
set -e
脚本中一旦遇到错误就退出 -
set -x
显示详细执行过程,用来 debug 脚本 -
set -o pipefail
管道命令 command1 | command2 | ... | commandn 从左往右执行,即使中间命令出错也继续执行下去。正常情况下整条命令的返回状态是最后一条命令执行的状态。打开该开关后整条命令的返回状态变为最后一个异常退出命令的状态。当所有命令都正常退出时,整条命令的返回状态是0。
进程
bash 中可输入的内容很多,要注意区分关键字(keyword),内置命令(built-in command),外部命令
1. 关键字
- 使用
compgen -k
查看关键字 - 关键字解析先于变量替换,这样可以作特殊处理,如:
string_with_spaces='some spaces here'
# 注意,$string_with_spaces 没有被引用依然工作良好,因为 [[ 是关键字
[[ -n $string_with_spaces ]]
# 这样写就会报错:bash: [: too many arguments
# 原因是 [ 是一个内部命令,先做了变量替换,然后调用命令发现参数个数不对
[ -n $string_with_spaces ]
- 关键字使用不会产生子进程
2. 内置命令
- 使用
type command
验证一个命令是否是内部命令,builtin help
或compgen -b
列出所有内部命令 - 内置命令常驻内存
- 内置命令不会fork子进程(待核实)
- 执行外部命令需要加载磁盘文件到内存,并fork子进程(具体过程待核实)
3. 进程状态捕获
- $$ 当前 shell pid
- $! 最近执行的后台进程pid
- $? 最近退出的前台进程退出状态
- wait $! 获得最近的后台进程退出状态(会阻塞直到这个后台进程退出)
- ps $! 查看后台进程是否退出
4. 名字空间
命令替换会产出子进程,在命令替换中修改父进程的变量是无效的。
元字符
bash 中的标识符、参数等默认都为字符串。字符串可加单引号或双引号或者不加。单引号内字符串不做任何解析,可用来屏蔽元字符,双引号内的做参数替换,命令替换。
由于参数替换和命令替换结果中可能含空白符,建议含二者的字符串总是用双引号扩起,传递时才会作为一个字符串。一个例外情况:
files="a.txt b.txt c.txt"
for f in ${files}; do
cat "${f}"
done
如果给${files}
加上双引号,被被认为是一个字符串,导致循环失败。
变量
1. 数值计算
- 整数计算
(( ))
括号中为 C 语言形式的表达式,要取表达式的值,使用$(( expression ))
- 浮点数计算
bc
awk
等工具
sun@st01-oped-sunrui09 ~/play$ bc <<< "4.5-2.34"
2.16
另外 awk 也可以计算
2. 字符串处理
可参:man bash
Parameter Expansion 节
注:ABS 文档可能有误,字符串Strip和替换说是正则表达式,但是并不是
x=abcd
${#x} # 获取字符串长度
expr index $x "b" # 查找子串,下标从1开始
echo ${x:1} # 获取子串,下标从0开始,从下标为1的位置到最后
echo ${x:0:2} # 获取子串,从下标0,选取长度为2的子串
echo ${x:-nuclear} # 若不存在取默认值
echo ${x#word} # 左端 strip 字符串,最短匹配
echo ${x##word} # 左端 strip 字符串,最长匹配
# strip,查找并替换,查找并删除等
if [[ $1 =~ $regex ]]; then # 正则表达式
...
expr length “this is a test” #
expr substr “this is a test” 3 5
expr index "sarasara" a
3. 数组,关联数组
declare -a array# 显示声明数组(index 只能为0以上的整数)
array=() # 同上
array+=("sun") # 数组追加
declare -A array # 显示声明关联数组(index可以为字符串)
4. 变量作用范围
一般为当前进程空间,子进程、父进程都不可见,相当只对本脚本生效,如果使用 export 导出,或者用 declare -x 声明,则子孙进程可见(不能修改),父进程不可见
一旦一个变量被导出过一次,就变为环境变量,后续的修改都是对环境中该变量的修改,可被所有子孙进程看到,如在 .bashrc 中写了 export JAVA_HOME=/jdk/1.7
,在某个应用脚本中 JAVA_HOME=/jdk/1.8
将直接对所有子孙进程生效。
函数中使用local
定义为作用域仅限函数
5. 变量解析(Parameter Expansion)、复杂变量、eval
- 一般使用 $PARAM 或者 ${PARAM} 的方式解析,
- 使用${!PARAM} 的方式进行间接解析(indirect expansion),例如:
name='sun'
PARAM=name
${!PARAM} # 将会解析为 sun
PARAM 称为复杂变量。上面做了2次变量拓展,bash 环境执行脚本或命令时,首先做各种替换(变量替换,算术替换和命令替换),然后执行替换后的文本。所以一般情况下变量只会被替换一次,( 变量中包含 * 会被再次替换)
需要注意的是,在变量赋值的过程中,bash 环境中的元字符(单双引号等)会被消化掉。具体行为为:
- 将最外层扩住字符串的引号消掉
- 将转义字符(\)消掉,只保留转义后的字符
所以在做变量的多次扩展时要小心保留这些字符。
还可以使用 eval 实现多次拓展。eval [arg...]
的行为:
bash 处理脚本的基本过程为先对整个文本做变量替换,命令替换等,然后执行。所以对于存在 eval 的脚本,bash 也是先对整个文本做变量命令等替换(包括eval 后面的内容),然后执行。eval 的作用是将所有参数再交给 bash 环境做一次处理。
function xargs() {
holder=$1
func=$2
shift 2
while read line; do
eval $holder=$line
eval $func $@
done
}
# 将文件中每个单词按逗号分隔,注意,占位符要用单引号,否则会被提前替换
cat a.txt | xargs word echo -n '$word',
函数
-
[ function ] name() { list }
形式定义,无参数列表,function 关键字可省略 - 函数像普通命令一样被调用,后接各种参数,这些参数在函数内部用
$1,$2,...$n
访问,而$0
始终是脚本文件 - 函数在当前shell环境中执行,不产生新的进程
- 仅作用于函数内部的变量使用 local 关键字声明,而不加 local 的变量作用域是整个进程空间,和子进程空间(只读)
- 函数的返回状态取决于函数内最后执行的命令,默认为0
-
return
是返回调用的函数,exit
则会退出程序,他们都会重置$?
变量,优雅的程序应该单入单出,一般函数中都使用return
,使调用者进行错误处理;只在main
函数中使用exit
,或全都使用return
(?) - 函数返回值捕获,一般使用命令替换的方式
value=$(func arg)
但是这种方法有个缺点:函数的退出状态(return 或 exit 的值)无法通过$?
获得,被赋值覆盖了,命令替换是在子进程中执行的,若函数中存在对当前环境中变量的写操作,将失效。 - 函数可以递归调用,且没有深度限制
:(){ :|:& }();:
fork 炸弹,参http://www.ha97.com/2618.htmlhttps://en.wikipedia.org/wiki/Fork_bomb
重定向
重定向是从右向左解析的:
-
cat <file >file
会清空原文件的内容,因为根据最右边生成一个名为 file的文件(如果已存在就被替换了),然后从一个名为 file 的文件读取内容并写入 file。 -
cat < file >> file
则不会停止。 -
> file 2>&1
这样写是没问题的,把标准错误也写到标准输出里,但是2>&1 >file
这样写就不行,因为先解析发现标准输出写到文件里,然后才发现标准错误写到标准输出里。2>&1
中的&
是标识 后面的1
不是普通名为1
的文件,而是标准输出。使用&> /dev/null
将标准输出和标准错误同时输出到 /dev/null 中。
管道,进程替换(process substitution)
详参 man bash
管道符之前的命令的标准输出作为管道符后命令的标准输入。每一个 |
后面的命令都会作为当前 shell 进程的一个子进程执行。
进程替换命令的输出结果都可以看作是一个临时文件,将这个临时文件的内容作为标准输入传递下去,与管道可以达到相同效果,但是每个管道会产生子进程(外部命令本来就是作为一个子进程执行,但是内部命令在这种情况下也会作为子进程执行),而进程替换不会。进程替换与命令替换$()
是类似的,区别是命令替换的结果被当作一个字符串,进程替换的结果当作一个文件。
ls | grep zink # 相当于 ls > tmp,grep zink tmp
grep zink < <(ls) # 效果同上,但不产生子进程
find . -name 'control' -type f | xargs grep zink
#管道前的 find 命令产生临时文件,xargs 命令的作用是取文件的每一行拼接后面的命令。
sun@st01-oped-sunrui09 ~/play$ ls -l <(ls)
lr-x------ 1 sun sun 64 Aug 25 10:59 /dev/fd/63 -> pipe:[368734728]
mkfifo 命名管道
命名管道当做一个文件使用,但是实际实现是通过内存。命名管道读者会阻塞,直到有进程往其中写,并以EOF结尾,读者才会唤醒并获取所有数据。
一种使命名管道常开的方法:
mkfifo pipe
sleep 10000 > pipe &
cat pipe &
echo "some data" > pipe
echo "more data" > pipe
使用 tee 命令并结合进程替换可以实现分流:
nc -l 5000 | tee >(./process1 | ./collect ) >(./process2 | ./collect ) | ./do_other.sh
执行环境
编写复杂脚本,注意区分父子进程。bash元字符在父子进程间传递解析。
会产生子进程的条件:
- 管道
- 命令替换
命令分组
参考:https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html
命令分组有两种形式:
(list)
{list; }
注意:使用大括号的形式时,括号要与list使用空格分开,并且list末尾要使用分号终止。
here 文件和 here 字符串
- 使用
<<
表示here文件,<<<
表示here字符串
#!/bin/bash
# 直接使用pstree,在每一行前加了行号
while read i; do
echo -e "$((++j))" "\t $i"
done < <( pstree )
# 使用 here-document,直接对多行文本进行逐行处理
while read line; do
echo "this is $line"
done < <(cat <<EOF
xxx
yyy
zzz
EOF)
# 上面使用了进程替换的方式,进程替换 <(cmd args...) 相当于一个匿名文件,然后将文件内容重定向到标准输入流,更直接的:
while read line; do
echo "this is $line"
done <<EOF
xxx
yyy
zzz
EOF
# 使用 here-string
while read line; do
echo "this is $line"
done <<< "xxx
yyy
zzz"
问题
- here 文档和 here 字符串的区别是什么?
几乎没有区别。只是here文档需要使用前后标识。 - here 文档和 here 字符串用在什么场景?
原命令需要从标准输入读取数据,但是由于是在脚本中,here文档(字符串)实现了把脚本中的字符串内容输入到原命令标准输入。
命令替换提供了匿名文件的能力,here文档(字符串)提供了将内容置于脚本内的能力,二者相互结合,可实现在脚本中混合编程,例:
#! /bin/bash
echo "from bash"
python <(cat << EOF
#! /usr/bin/python3
import sys
if __name__ == "__main__":
print("from python " + sys.argv[1])
EOF
) hello
echo "from" | awk -f <(cat <
/from/{
print "from awk"
}
EOF
)
可以用 here 文档和 here 字符串作块注释:
<<!
注释1
!
<<<'
注释2
'
信号
trap command signal 捕获信号,只有信号 9 是无法捕获的
常用 artifact
awk
nc
expect 与进程交互
ss -apn (netstat -apn)
lsof -i:5000
grep -v 排除
wait 等待后台进程
stat 查看文件状态
getopts 处理命令行参数
time
timeout
taskset 设置进程的cpu亲和性
strace 跟踪进程的系统调用
二. 高级主题
2.1 协程
coproc [NAME] command [redirections]
协程异步在子shell中执行,本命令会总是返回成功。注意,如果 command 是简单命令的话,NAME 一定不能提供,此时名称为 COPROC,协程的PID 为 NAME_PID
#!/bin/bash
# create the co-process
coproc myproc {
bash
}
# send a command to it (echo a)
echo 'echo a' >&"${myproc[1]}"
# read a line from its output
read line <&"${myproc[0]}"
# show the line
echo "$line"
wait ${myproc_PID} # 等待协程结束
awk 协程
awk '
{
hive = "hive -S 2>/dev/null"
print ("dfs -du -s " $0 ";") |& hive
hive |& getline line
if(line ~ /^[0-9]+/) print line
}
END {
close(hive)
}
' data_pathes
当使用 |&
时,会在一个子进程中启动任务并打开双向的管道。管道可用任意程序输入,但只能用awk的getline
读取标准输出。
在整个 awk 程序执行期间每当出现该任务的字符串会复用该进程。需要在不用时显式close
。
close
还可以选择只关闭输入或输出:close(hive, "to")
,close(hive, "from")
。
网友评论