Z.S.K.'s Records

有趣的Shell Snippet

记录下遇到的有趣的shell代码, 可能是一些常用的snippet, 也可能是使用的时候不经意踩到的坑

不定时更新

正确传递数组到函数中

1
2
3
4
5
6
7
function update() {
declare -a apps_version=("${!1}")
echo "${apps_version[@]}"
}

APPS_VERSION=("aaa" "bbb" "ccc")
update APPS_VERSION[@]

要特别注意的是,使用不当就造成只将数组的第一个数组到函数中

上面是正确的使用方法

使用sed修改Yaml文件中指定关键字的下N行

文件如下:

1
2
3
4
5
6
this:
is: is
a: a
test: test
is: is
test: test

现在要修改this所在行的下面2行

1
sed -i "/this:/!b;n;n;c\  a: ${tmp_version}" ${file}
  • !b表示中断sed命令
  • n表示读入下一行
  • c表示将当前行修改为后面的字符串

因为a: a这行需要缩进2个空格,空格需要使用\进行转义

查看进程占用的文件句柄

1
2
3
4
5
6
7
8
9
10
11
12
13
# 用户级
find /proc/*/fd/* -type l -lname 'anon_inode:inotify' -print 2>/dev/null | cut -d/ -f3 |xargs -I '{}' -- ps --no-headers -o '%U' -p '{}' | sort | uniq -c | sort -nr
# 结果: 第一列表示打开的句柄,第二列表示用户
# 39 root
# 36 SENSETIME\zhoushuke
# 2 mfe

# 进程级
find /proc/*/fd/* -type l -lname 'anon_inode:inotify' -print 2>/dev/null | cut -d/ -f3 |xargs -I '{}' -- ps --no-headers -o '%U %p %c' -p '{}' | sort | uniq -c | sort -nr
# 结果: 第一列表示打开的句柄,第二列表示用户,第三列表示用户id,第四列表示进程
# 6 root 1 systemd
# 5 SENSETI+ 11808 sogou-qimpanel
# 5 root 1072 kubelet

使用sed删除多行内容

1
2
3
4
5
6
7
8
9
10
11
12
13
env: prod

sensebee3:
xxx:
yyy: yyy
svc_name: ss_class.ingress_namespace.svc.cluster.local
sensebee: sensebee
svc_port: 8443
node_port: 30123
http: 1234
sensebee2:
http: 1234
svc_name: sensebee.default

对于上面的的yaml文件内容,如果想将sensebee3下的行直到http: 1234之间的内容都删除,但是sensebee3及http: 1234这两行不删除,使用sed如何操作呢?

1
sed -i "/^sensebee3:/I,/^[^[:space:]#]/{//!d;}" file

从上面的格式可以看出,只需要先确定范围,然后删除即可,

/^sensebee3:/用于匹配sensebee3

/^[^[:space:]#]/用于匹配到不是以空格及#号开头的行,自然就匹配到了http: 1234, 这样就选定了这两行之间的内容

{//!d;}其中//表示使用前面的正则表达式, !d表示不删除, 这样就实现了sensebee3及http: 1234这两行不会删除,只删除这两行之间的内容.

在脚本中修改crontab

1
(crontab -l 2>/dev/null; echo '*/2 * * * * bash /usr/local/src/kestrel.openfiles.check > /usr/local/src/logs/kestrel_openfiles.check.$(date "+\%Y\%m\%d-\%H\%M\%S").details 2>&1') | crontab -

善用{}

1
2
3
4
5
ATEST="ISTEST"
# 错误使用,打印为空, bash会把ATEST_exec当成是一个变量,也就是最大的查找_连接的字符
echo $ATEST_exec
# 正确
echo ${ATEST}_exec

使用&& ||

有时为了shell命令能够简短, 经常会连着使用 &&(且) ||(或), 但是如果不多想一次的话,可能就会跟结果相背.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 只有当command1成功(命令返回值为0), 才会执行command2
command1 && command 2

# 只有当command1失败(命令返回值不为0), 才会执行command2
command1 || command2

# command1与command2会顺序执行, 两个命令之间没有关系.
command1; command2

# 例子
# 假如想判断从网上下载一个东西, 如果成功继续执行, 如果失败, 则打印一句话并退出.
# 思考下面三个句子,哪条符合要求

# 这句话相当于(wget -q someURL -O localdir || echo "DOWN FAILED!"); exit 1
# 因此, 不管wget是否成功, exit 1都将被执行.
wget -q someURL -O localdir || echo "DOWN FAILED!"; exit 1

# 当wget成功后, 忽略 || 后面的语句, 只有失败了,才会echo, 由于echo成功, 因此也会执行 exit 1
wget -q someURL -O localdir || (echo "DOWN FAILED!" && exit 1)

# 这句跟上面的效果一样, 唯一的区别在于exit 1必然会执行而不管echo语句有没有成功
wget -q someURL -O localdir || (echo "DOWN FAILED!"; exit 1)

换行符

经常会有读取文件的需要, 用的最多的是使用for循环读取文件, 使用的时候需要特别注意文件的换行符, 默认情况下,换行符为空格, 需要使用IFS指定为换行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 测试文件
cat xx.md
this is a test
for readline
from bash

# 读取文件脚本(错误)
cat readfromfile.sh
#!/bin/bash

for LINE in `cat xx.md`
do
echo $LINE
done

# 输出, 没有按行输出, 第个单词都占据了一行
this
is
a
test
for
readline
from
bash

# 正确的脚本
#!/bin/bash

IFS_old=$IFS # 将原IFS值保存,以便用完后恢复
IFS=$'\n' # 指定回车为分隔符

for LINE in $(cat xx.md)
do
echo ${LINE}
done
IFS=$IFS_old # 恢复原IFS值

# 输出
this is a test
for readline
from bash

# 当然以下两种方式也可以达到逐行输出效果, 大文件请考虑效率
# second
cat xx.md | while read line
do
echo "${line}"
done
# third
while read line
do
echo "${line}"
done < xx.md

变量默认值

有时候定义变量的时候, 经常需要默认值, shell中也有一些比较有趣的表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
${value:=word}  
#如果value存在且非null 则返回value 否则把word赋值给value并返回word(=value), 适合场景:如果value没有初始化 可给它赋初值
echo ${value:=word}--->文件的第一行
echo ${value1:=word}--->word 且会把word赋值给value

${value:+word}
# 如果value存在且非null 则返回word 如果value没有设置或为空 则返回value
echo ${value:+word}--->word
echo ${value1:-word}---> 返回空 因为value1本身就是空

${value:?word}
# 如果valued存在且非Null, 那就什么也不做。否则,value:word会被发送到标准错误输出,并且程序会退出;如果没有指定word 则输出 value: parameter null or not set
echo ${value:?nomessage}---->输出第一行
echo ${value:?nomessage}---->value:nomessage
echo ${value:?}----->value: parameter null or not set

参数解析getops

在写脚本的时候,经常需要对参数进行解析,shell毕竟是个脚本语言, 不可能像python等高级语言一样有很完善的参数解析库, getops是bash自带的一个用于参数解析的工具, 但是它只是用于参数解析, 不能对参数进行更高级的操作,比如参数间依赖, 参数判断等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
function usage() {
echo "${0} -m [master/slave] -t [my/db] -s [slave-ip]"
echo ''
# shellcheck disable=SC2016
echo ' -m: Specify install type: master or slave'
# shellcheck disable=SC2016
echo ' -s: If type == master, then must specify opq slave ip'
# shellcheck disable=SC2016
echo ' -t: Specify instll opq type: my or db, my is short for mingyuan'
echo 'example:'
cat << EOF
#install opq-db
# master
# ./auto_install_opq_from_oss.sh -m master -s 127.0.0.1 -t db
# slave
# ./auto_install_opq_from_oss.sh -m slave -t db
# install opq-mingyuan
# master
# ./install_opq.sh -m master -s 127.0.0.1 -t my
# slave
# ./install_opq.sh -m slave -t my
EOF
}

while getopts "m:s:t:h" opt # ":m:s:t:h" 如果首位的出现: 表示不打印错误信息
# 上面这句表示: -m -s -t 需要参数 -h 不需要传参
do
case ${opt} in
m)
MODE=${OPTARG}
;;
s)
SLAVEIP=${OPTARG}
;;
t)
STYPE=${OPTARG}
;;
h)
usage
exit 0
;;
\?)
echo "Invalid option: -$OPTARG" >&2
usage
exit 1
;;
esac
done

# 这里要说明一下 "m:s:t:h"的含义
# 如果首位(m前)出现 : 表示「不打印错误信息」,也就是说如果需要带参数但没有带时不会打印错误
# 紧邻字母后面的 : 表示该选项接收一个参数, 如果没有带参数的话,则会提示错误

多if时不如使用case

在需要使用法if进行业务判断时, 不防使用case,相对于层层if, 代码会更加清晰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if [[ "$lmode" == "master" ]]; then
case $lstype in
my)
OPQ_UNTAR_DIR_NAME='multiindex_opq_master_mingyuan'
;;
db)
OPQ_UNTAR_DIR_NAME='multiindex_opq_master'
;;
esac
PROGRAM='opq_master'
else
case $lstype in
my)
OPQ_UNTAR_DIR_NAME='multiindex_opq_slave_mingyuan'
;;
db)
OPQ_UNTAR_DIR_NAME='multiindex_opq_slave'
;;
esac
PROGRAM='opq_slave'
fi

加载key-value类的配置文件

1
2
3
4
5
6
7
8
9
cat xx
key1=value1
key2=value2

cat xx.sh
. xx # 这里直接使用. xx即可把xx文件里的key-value引入,后续可直接使用k.
echo $key1, $key2

#注意, 配置文件只能是k=v的形式(多个k=v可以在一行, 使用空格隔开), 其它形式会报错

一行代码: 字符串是否包含子串

1
2
# 判断CONSUL_SERVER中是否包含逗号 如果包含EXPECT_LEN=3, 不包含EXPECT_LEN=1
[ -z "${CONSUL_SERVER##*,*}" ] && EXPECT_LEN=3 || EXPECT_LEN=1

for循环打印带空格字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 需求, 提供个数组,循环数组中的元素, 如果元素存在于某个文件中,则不追加,如果不存在,则追加
K8S_CLUSTER=("172.1.52.250 k8s-master-250" "172.1.52.50 k8s-master-50")
# TARGET=/etc/hosts
# 错误的写法
for x in ${K8S_CLUSTER[@]}; do grep "$x" /etc/hosts > /dev/null || echo "$x" >> /etc/hosts;done
# 会发现cat /etc/hosts输出以下格式, 正是因为元素中存在空格, 而echo的时候会以空格进行换行,不符合预期
172.1.52.250
k8s-master-250
172.1.52.50
k8s-master-50

# 正确的写法
for x in "${K8S_CLUSTER[@]}"; do grep "$x" /etc/hosts > /dev/null || echo "$x" >> /etc/hosts;done
# 将数组用引号做为一个整体进行for循环就没问题, cat /etc/hosts
172.1.52.250 k8s-master-250
172.1.52.50 k8s-master-50

未完待续

参考文章:

转载请注明原作者: 周淑科(https://izsk.me)

 wechat
Scan Me To Read on Phone
I know you won't do this,but what if you did?