Bash编程

2020-04-04  本文已影响0人  nuclear_sun

资料

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

问题

陷阱

一. 基础知识


Bash 环境设置

进程

bash 中可输入的内容很多,要注意区分关键字(keyword)内置命令(built-in command)外部命令

1. 关键字
string_with_spaces='some spaces here'
# 注意,$string_with_spaces 没有被引用依然工作良好,因为 [[ 是关键字
[[ -n $string_with_spaces ]]
# 这样写就会报错:bash: [: too many arguments 
# 原因是 [ 是一个内部命令,先做了变量替换,然后调用命令发现参数个数不对
[ -n $string_with_spaces ]
2. 内置命令
3. 进程状态捕获
4. 名字空间

命令替换会产出子进程,在命令替换中修改父进程的变量是无效的。

元字符

bash 中的标识符、参数等默认都为字符串。字符串可加单引号或双引号或者不加。单引号内字符串不做任何解析,可用来屏蔽元字符,双引号内的做参数替换,命令替换。
由于参数替换和命令替换结果中可能含空白符,建议含二者的字符串总是用双引号扩起,传递时才会作为一个字符串。一个例外情况:

files="a.txt b.txt c.txt"
for f in ${files}; do
    cat "${f}"
done

如果给${files}加上双引号,被被认为是一个字符串,导致循环失败。

变量

1. 数值计算
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
name='sun'
PARAM=name
${!PARAM}  # 将会解析为 sun

PARAM 称为复杂变量。上面做了2次变量拓展,bash 环境执行脚本或命令时,首先做各种替换(变量替换,算术替换和命令替换),然后执行替换后的文本。所以一般情况下变量只会被替换一次,( 变量中包含 * 会被再次替换)
需要注意的是,在变量赋值的过程中,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',

函数

重定向

重定向是从右向左解析的:

管道,进程替换(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 字符串

#!/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

上一篇 下一篇

猜你喜欢

热点阅读