Bash编程

资料

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 helpcompgen -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"

问题

  1. here 文档和 here 字符串的区别是什么?
    几乎没有区别。只是here文档需要使用前后标识。
  2. 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")

2.2 重定向

参考:http://www.tldp.org/LDP/abs/html/io-redirection.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容

  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,380评论 0 5
  • 1. 清空文件内容 $ > file 这一行命令用到了输出重定向操作符>。输出重定向发生时,文件会被打开准备写入。...
    MrHamster阅读 597评论 0 0
  • Bash编程013——环境变量 环境变量可以帮助提升你的Shell体验。很多程序和脚本都通过环境变量来获取系统信息...
    若梦儿阅读 1,002评论 0 7
  • Bash编程002——变量 在任何一门程序设计语言中, 变量都是必不可少的。在shell中变量的涵义跟其他语言中的...
    若梦儿阅读 130评论 0 1
  • 闲赋登高抬望远 满目青山映龙泉 云开见日乘风远 纤巧娇娘东山眠 虎踞龙盘守福地 护村佑民旺收益 草木皆绿意显春 沂...
    笃诚阅读 787评论 1 2