Shell Script

2024/03/23

Tags: shell bash linux

最近写的shell脚本比较多,记录一些常用命令, 相当于记录一个索引, 以后用时可以快速回忆起来.

#!/bin/bash

#!/bin/bash被称为shebang line, 指定执行此脚本文件时使用/bin/bash做为shell解释器程序

很多主流操作系统默认的shell解释器也是bash

# echo $SHELL
/bin/bash

set

set命令用来修改shell环境的运行参数, 完整的可定制的官方手册

下面是我常用的几个, 可以合并为如下内容写在脚本开头:

#!/bin/bash
set -uxe
set -o pipefail

set -u

执行脚本时, 如果遇到不存在的变量, Bash默认会忽略, set -u可以让脚本读到不存在变量时报错

set -x

命令执行前会先打印出来, 行首以+表示, 在调试脚本时非常有帮助

set -e

执行脚本时, Bash遇到错误默认会继续执行, set -e使得脚本只要发生错误, 就中止执行

set -o pipefail

set -e有一个例外情况, 就是不适用于管道命令, 比如下面的不会退出

#!/bin/bash
set -e

foo | echo a
echo bar

执行的结果为:

a
set.sh: line 4: foo: command not found
bar

set -o pipefail可以解决这个问题, 只要一个子命令失败, 整个管道命令就失败, 脚本就会终止执行

#!/bin/bash
set -eo pipefail

foo | echo a
echo bar

执行的结果为:

a
set.sh: line 4: foo: command not found

单引号''和双引号""

双引号会将$var解析成本身的值, 单引号不会解析

# var=1
# echo "$var"
1
# echo '$var'
$var

<< here document

一般使用Here Document作为标准输入喂给kubectl apply -f -或者重定向到文件里.

#!/bin/bash

# 标识符或限定符IDENT一般使用EOF表示
COMMAND <<IDENT
this is ...
IDENT

cat <<EOF写入到文件

cat一般用来查看文件内容, cat <<EOF可以用来将多行内容打印到标准输出重定向写入到文件里, 这里限定符使用EOF.

cat <<EOF > doc.md
# this is ...
EOF

kubectl apply -f - <<EOF

使用kubectl直接不创建文件去apply一个yaml

kubectl apply -f - <<EOF
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
  name: macvlan-conf-2
EOF

sed (stream editor)

sed全名stream editor, 会流式的一行一行编辑文件, sed手册

下面的修改都是打印到标准输出, 加上-i参数sed -i 'xxx' filename就可以直接更新到文件了

s命令替换字符串

结合上面的here document一起演示, 这样就不用再单独创建文件了

将镜像taglatest改为v1.1.1

sed 's/latest/v1.1.1/g' - <<EOF
ecr.gobai.top/example:latest
EOF

s表示替换命令, /latest/表示匹配latest, /v1.1.1/表示将匹配到的替换为v1.1.1, /g表示每一行中匹配到的全部替换, 没有g只会替换每一行中的第一个.

只替换行中匹配到的某一个

# 替换第1个
sed 's/latest/v1.1.1/1' - <<EOF
ecr.gobai.top/example:latest latest latest
EOF

# 替换第2个
sed 's/latest/v1.1.1/2' - <<EOF
ecr.gobai.top/example:latest latest latest
EOF

# 替换第2个和之后的
sed 's/latest/v1.1.1/2g' - <<EOF
ecr.gobai.top/example:latest latest latest
EOF

只替换部分行字符串

# 只替换第2行
sed '2s/latest/v1.1.1/g' - <<EOF
ecr.gobai.top/example:latest
ecr.gobai.top/example:latest
ecr.gobai.top/example:latest
EOF

# 只替换2-3行
sed '2,3s/latest/v1.1.1/g' - <<EOF
ecr.gobai.top/example:latest
ecr.gobai.top/example:latest
ecr.gobai.top/example:latest
EOF

更复杂的涉及很多正则的场景我一般直接丢给ChatGPT去写, 知道sed可以完成这些任务就可以了!!!

一些sed中常用的正则的语法

大部分都是通用的, 在其他需要正则匹配的地方也可以使用, 如在grep -egrep -E中也可以使用

https://www.gnu.org/software/sed/manual/html_node/Regular-Expressions.html

字符含义
单个普通字符匹配自身
*匹配*前面正则表达式的零个或多个匹配项
\+类似*, 但是匹配至少一个
\?类似*, 但是匹配零个或一个
\{i\}类似*, 但是匹配i
\{i,\}类似*, 匹配至少i
\(regexp\)将内部的regexp作为一个整体分组
.匹配任意单个字符, 包括换行
^匹配开始处的null字符串
$匹配结尾处的null字符串
[list]匹配方括号中的字符列表中的任意一个
[^list]匹配方括号中的字符列表中的任意一个
regexp1|regexp2匹配regexp1regexp2
regexp1regexp2匹配regexp1regexp2的连接结果
\digit匹配正则表达式中第digit个圆括号\(...\)子表达式
\n匹配换行符
\char匹配char字符, char可以是$ * . [ \ 或者 ^

一些注意点:

  1. 虽然.可以匹配换行符, 但是有的命令是一行一行处理, 所以正则匹配不到换行, 如sed命令

圆括号匹配

圆括号括起来的正则表达式所匹配的字符串可以当成变量来使用, 通过\1\2来引用

VERSION后面的版本替换

V="1.1.1"
sed "s/\(^VERSION:\s*\)[0-9.]\+/\1$V/" - <<EOF
VERSION: 0.0.1
EOF

圆括号()需要转义\(\), 并且因为有变量$V, 单引号需要改为双引号, ^代表行的开始, VERSION:匹配文本字符串, \s*匹配0或多个空白字符, [0-9.]\+匹配一个或多个数字或., \(^VERSION:\s*\)匹配到了版本号前面的内容作为变量1, \1$V代表将匹配到的所有内容替换为版本好前面的内容+新的版本号$V

echo命令

echo -n

-n     do not output the trailing newline

在结尾处不自动添加换行符

echo -eecho -E

-e     enable interpretation of backslash escapes # 激活转义字符
-E     disable interpretation of backslash escapes (default)

grep命令

grep应该是特别常见的命令了, grep支持从标准输入stdin作为参数, 如echo 'abc' | grep abc, 也可以从命令行输入参数, 如grep abc demo.md

grep -E

正则匹配

# str=$(echo '1. aaa\n2. bbb')
# echo $str | grep -E 'a|b'
1. aaa
2. bbb

grep -i

忽略大小写

# str=$(echo '1. aaa\n2. bbb')
# echo $str | grep -i -E 'A|B'
1. aaa
2. bbb

grep -v

-v, --invert-match # 反向匹配, 选择不匹配的行
    Invert the sense of matching, to select non-matching lines.

获取不包含a的行

# str=$(echo '1. aaa\n2. bbb')
# echo $str | grep -v a    
2. bbb

grep -n

-n, --line-number # 打印匹配到的行在输入中的行号
    Prefix each line of output with the 1-based line number within its input file.

打印匹配到的行的行号

# echo $str | grep -n -E 'a|b'
1:1. aaa
2:2. bbb

grep -r

-r, --recursive # 递归读取目录下的文件
    Read all files under each directory, recursively, following symbolic links only if they are on the command line.  Note that if no file operand is
    given, B<grep> searches the working directory.  This is equivalent to the -d recurse option.

在当前目录递归遍历匹配字符串并打印行号

grep -rn "string" .

xargs命令

TODO

awk命令

awk也是依次处理文件的每一行, 适合处理每一行格式相同的数据

基本用法

# 格式
awk 动作 文件名

# 示例
awk '{print $0}' - <<EOF
abc 123 666
bcd 234 777
EOF

大括号{}内部是处理当前行的动作, $0代表当前行, 最终效果就是原样打印所有行. awk会根据空格制表符将每一行分成若干字段, 通过$1 $2 $3代表每一个字段, 也可以通过awk -F ':' '{print $1}' xxx手动指定每一列之间的分隔符为:

ldd命令

ldd可以查看一个可执行文件依赖哪些动态链接库

离线有动态链接库的程序

查看jq命令依赖哪些动态链接库,

# ldd $(which jq)
        linux-vdso.so.1 (0x00007fff12b9f000)
        libjq.so.1 => /lib/x86_64-linux-gnu/libjq.so.1 (0x00007fc42d080000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc42ce00000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fc42cd19000)
        libonig.so.5 => /lib/x86_64-linux-gnu/libonig.so.5 (0x00007fc42cc86000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fc42d0e9000)

如果要离线一个脚本, 只需要将=>后面的文件复制一份打包即可, 这时awk命令就能派上用场了

APP_NAME="jq"
mkdir ${APP_NAME}_archive && cd ${APP_NAME}_archive
mkdir libs
ldd $(which ${APP_NAME}) | awk '{print $3}' | xargs -i cp -L {} libs

这样只需要再把可执行文件也离线, 就可以离线安装运行了

不过还有最后一步, 这些lib文件不适合直接都放入/lib/目录下?, 因为有可能这些lib目录下有和当前程序冲突的版本, 所以直接把程序依赖的lib放在一个目录下然后启动时设置LD_LIBRARY_PATH目录让程序去找正确版本的lib库是更稳妥的.

cp $(which ${APP_NAME}) .
cat <<EOF > app_${APP_NAME}.sh
#!/bin/bash
INSTALL_DIR="/opt/app_archives"
APP_NAME="${APP_NAME}"
export LD_LIBRARY_PATH="\${INSTALL_DIR}/\${APP_NAME}_archive/libs"
\${INSTALL_DIR}/\${APP_NAME}_archive/\${APP_NAME}  "\$@"
EOF
chmod +x app_${APP_NAME}.sh

最终的文件如下

# tree .                     
.
├── app_jq.sh
├── jq
└── libs
    ├── libc.so.6
    ├── libjq.so.1
    ├── libm.so.6
    └── libonig.so.5

1 directory, 6 files

安装时, 只需要将${APP_NAME}_archive目录放在/opt/app_archives目录下, 然后创建一个如下的软链即可

ln -nsf /opt/app_archives/\${APP_NAME}_archive/app_\${APP_NAME}.sh /usr/bin/\${APP_NAME}

mc(minio client)

设置alias

mc alias set {NAME} http://minio.lan:9000 {USER} {PASSWORD}

设置匿名用户对某bucket权限

权限有download, uploadpublic(download+upload)

设置匿名用户可以下载某个bucket下的文件

# mc anonymous set download minio/app
Access permission for `minio/app` is set to `download`

查看匿名用户对某bucket的权限

# mc anonymous get minio/app
Access permission for `minio/app` is `download`

jq命令

jq用来处理json数据还是超级强大的

select过滤数组

下面的命令用到了jq中的管道操作|和过滤操作select

# echo '[{"name":"compute","image":"c-image"},{"name":"log"}]' | jq '.[] | select(.name == "compute") | .image'
"c-image"

有时不想要字符串两边的双引号, 这时就需要用到上面用到的sed命令了, 思路是正则匹配整个字符串, 然后双引号中间的使用圆括号匹配作为变量\1, 最后将整个字符串替换为\1即可

# str=$(echo '[{"name":"compute","image":"c-image"},{"name":"log"}]' | jq '.[] | select(.name == "compute") | .image')
# echo $str | sed 's/^"\(.*\)"$/\1/'
c-image

tar命令

压缩文件

tar -czvf name.tar.gz name

使用pigz加速压缩

如果没安装pigz需要先安装apt install pigz -y

tar -I pigz -czvf name.tar.gz name

解压文件

tar -xzvf name.tar.gz

docker命令

导出镜像到压缩包

docker save -o all.tar image-a:1.0.0 image-b:1.0.0 image-c:1.0.0

从压缩包导入镜像

docker load < all.tar

TODO

参考