oynix

于无声处听惊雷,于无色处见繁花

Shell相关汇总

早前写过几篇有关Shell使用的零碎知识点,为便于查看,将其汇总于此。

1. 格式和运行

格式是指开头第一行

1
#! /bin/bash

其中的#!叫shebang,也叫hashbang,后面跟着的这个脚本解释器的绝对路径,我的电脑上用的是bash。运行的时候直接使用sh后面跟上脚本的名字即可,如果给脚本文件加上可执行的权限,用./也可执行,如果不写第一行的话,你会发现也可以执行,如果不光是给自己用,建议还是要写上的。

2. 获取参数

有时一些参数是不固定的,需要每次运行的时候传入。方式有两种,一种是直接在后面写参数的值,如

1
sh command.sh arg1 arg2 arg3

这种方式在读取时,是凭借参数的位置顺序读取的,$0为command.sh,$1为参数一arg1,$2为参数二arg2,依次类推。这种方式方便传入和读取,但是受到位置的限制,有时可能只需要传入参数二,而参数一使用缺省值便可,这时使用-argName的方式会更方便,按需指定并传入,如

1
sh command.sh -a argA -b argB -c argC

读取时,要这样,getopts,即get options,获取所有的可选参数,通过循环读取每个参数的值

1
2
3
4
5
6
7
8
while getopts "a:b:c:" opt
do
case $opt in
a ) argA=$OPTARG ;;
b ) argB=$OPTARG ;;
c ) argC=$OPTARG ;;
esac
done

3. 条件判断

判断是脚本中最常见的命令语句了,基本语法为

1
2
3
4
5
6
7
if [ command ]; then
# do something
elif [ command ]; then
# do something
else
# do something
fi

其中的command有很多,按类型可以分为:判断文件/目录、判断字符串和判断数值大小,举几个常用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 文件/目录
[ -e FILE ] 文件存在
[ -s FILE ] 文件存在且长度不为0
[ -f FILE ] 普通文件存在 -b:二进制文件 -c:字符文件 -r:可读文件 -x:可执行文件
[ -d DIR ] 目录存在
[ FILE1 -nt FILE2 ] newer than,也就是1比2更新 同理,-ot:older than

# 字符串
[ -z STRING ] zero长度为0
[ -n STRING ] not zero长度不为0,-n也可以省略,即等同于 [ STRING ]
[ S1 = S2 ] 相同,同理 !=为不同

# 数值
[ INT1 -eq INT2 ] equals,相等,同理 -ne:not equals
[ INT1 -gt INT2 ] greater than,大于,也可以写成大于号: >,同理 -lt:less than
[ INT1 -ge INT2 ] greater or equals, 即为>=,同理 -le:<= 小于等于

# 多个条件
[ (条件1) -a (条件2) ] all,与,也可以写成 &&
[ () -o () ] or,或,也可以写成 ||
[ ! () ] not,非

4. ssh命令

ssh连接服务器,如果已经把自己的公钥id_rsa.pub添加到了服务器的authorized_keys里,那么可以直接连接,就可以了

1
ssh username@server-ip

如果没有添加,则需要指定服务器的私钥的pem文件作为参数

1
ssh -i identification.pem username@server-ip

原理很简单,一是服务器信任了自己的公钥,二是拿着服务器的私钥的pem,pem的获取方式为

1
2
3
4
# 服务器生成公钥匙私钥对,这会得到一个私钥private,和公钥private.pub
ssh-keygen
# 转换私钥,传入私钥,得到pem格式,凡是拿着这个pem的客户端都可以连接
openssl rsa -in private -outform pem > private.pem

当然,光连上服务器不是目的,这些如果写到shell脚本里会中断执行,而我的目的是在服务器上执行命令,连到服务器只是前提,若执行命令,如下

1
2
3
ssh username@server-ip "[command]"
# e.g。
ssh user@server-ip "cd documents && ls -la"

有时还会需要在本地和远程之间传递文件命令,从本地到远程如下(远程到本地就反过来),注意server-ip后面的冒号,

1
2
3
4
5
scp [options] /local/source/file/path user@server-ip:/path/to/destination
-C 要大写,压缩
-P 要大写,端口号,默认22
-r recursive递归
-p 保留访问修改时间

服务器的IP地址经常会变,每次通过ssh连接一个不存在于.ssh/known_hosts中的ip时,都会弹出一个警告,问是否要将这个正在连接的主机添加到已知设备中,中断自动化脚本的执行。

在这里,都要连接登陆了,所以肯定是要加的,可以通过指定一个参数,让其自动添加,而不中断自动化流程

1
ssh -o StrictHostKeyChecking=no -i identificaiton.pem username@host

5. 编译shell脚本

有时写完一个shell脚本,需要给别人使用,但是又不想让别人看到里面的代码,可能因为里面有重要数据,也可能没有为什么,就是不想,这时可以把shell脚本编译成可执行文件,这样一来,既可以执行,但又无法查看了。一共有两种方式,一是系统自带的gzexe,二是使用shc命令,

1
gzexe command.sh

这会在目录下生成两个文件,一个是command.sh,另个是command.sh~,前者是压缩之后的,带~的是原文件,打开压缩之后的文件,可以发现里面有不少内容还是可以看懂的,尽管多数都不沾边了。

1
shc -f command.sh

这也会在目录下生成两个文件,一个是command.sh.x,这是可执行文件,另个是command.sh.x.c,这是个c文件,打开可执行文件后,会发现完全看不懂,都是乱码,推测可能先生成了c文件,然后再将c编译成二进制可执行文件。这是shc的github地址

安装shc

按照官方文档介绍,步骤如下

1
2
3
4
5
6
7
8
9
10
11
12
$ git clone git@github.com:neurobin/shc.git --branch release --depth 1

$ cd shc

$ ./configure

$ make

$ sudo make install

$ which shc
/usr/local/bin/shc

6. 脚本里调用其他脚本

当有多个shell文件,其中相互调用的时候,要用source命令

1
2
3
4
#! /bin/bash
echo 'first shell scripts'

source other.sh

虽然写在多个shell文件里,但是因为运行在同一个shell会话中,所以变量是可以共用的,也就是first里声明的变量,在other里可以读取,我一般用这种方式来处理脚本的参数,当一个脚本需要多个参数时,就把这些参数都单独写在一个shell里,然后再最后通过source调用真正的执行文件就好。

7. $相关的变量说明

变量 含义
$0 脚本的名字,即文件名
$n(n ≥ 1) 参数名字
$# 传给脚本的参数个数
$* 传来的所有参数
$@ 传来的所有参数
$? 上个命令的退出值
$$ shell进程ID

其中,$@$*,没有引号包围时,二者完全一样,当有双引号包围时,$@依然无变化,而$*则是把所有参数合并成了一个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sh test.sh p1 p2 p3

# 读取参数代码
for p in []
do
echo $p
done

$@ $* "$@" 输出的都是
p1
p2
p3

"$*" 输出的是 p1 p2 p3

8. 字符串读值

1
2
3
4
5
6
7
8
9
10
11
- ${var}: 取值
- ${var-DEFAULT}: 如果var没有被声明,那么就以DEFAULT作为其值
- ${var:-DEFAULT}: 没被声明或为空,则以DEFAULT作为其值
- ${var=DEFAULT}: 如果var没有被声明,那么就以DEFAULT作为其值
- ${var:=DEFAULT}: 没被声明或为空,则以DEFAULT作为其值
- ${var+OTHER}: 如果var声明了,那么其值就是$OTHER,否则就为null字符串
- ${var:+OTHER}: 同上
- ${var?ERR_MSG}: 如果var没有被声明,则打印ERR_MSG
- ${var:?ERR_MSG}: 同上
- ${!varprefix*}: 匹配之前所有以prefix开头的变量名称
- ${!varprefix@}: 同上

9. 字符串操作

1
2
3
4
5
6
7
8
9
10
11
12
- ${#string}: 长度
- ${string:position}: 从position角标开始提取自字符串,角标从0开始
- ${string:position:length}: 同上,同时限定长度
- ${string:0-position:length}: 同上,从右边开始数,从1开始,最右字符是1
- ${string#*substring}: 截取,保留substring右边字符,不包含substring,遇到第一个substring就截取
- ${string##*substring}: 同上,遇到最后一个substring再截取,常用于URL,星号可通配任意个字符
- ${string%substring*}: 从右侧开始截取,保留substring左侧字符,不包含substring,遇到第一个就截取
- ${string%%substring*}: 同上,遇到最后一个再截取
- ${string/substring/replacement}: 用replacement替换第一个substring
- ${string//substring/replacement}: 同上,替换所有
- ${string/#substring/replacement}: 如果string的前缀匹配substring,则用replacement替换substring
- ${string/%substing/replacement}: 同上,换成后缀

10. 脚本调试

  • xtrace
    打印执行的每一条命令

    1
    2
    3
    4
    5
    6
    7
    set -o xtrace # 打开

    set +o xtrace # 关闭

    # 或者这样
    set -x
    set +x
  • e
    出错后立即停止执行。每一条命令在执行成功后多会返回0,返回非0时代表出错。打开e之后,遇到非0返回值后会立即结束,而不会继续执行,这在一条命令需要在上一条成功执行的基础上才能执行时很有用。在某些时候也不会关心执行结果,比如,删除一个文件或文件夹,当删除目标不存在时,返回的就是一个非0结果,这个时候也不影响,所以,按需打开/关闭。

    1
    2
    3
    set -e # 打开

    set +e # 关闭

11. 多进程并发

shell多进程写法很方便,在执行的命令结尾加一个&符号,便可以把当前操作放到后台进程执行,而当前进程可以继续执行其他操作。直接操作,进程数量会变得不可控,比如,对一个目录下每个文件都要执行一个操作,该操作通过&放到后台进程执行,那么,当该目录下有10个文件时,就会启动10个进程,有100个文件时,就会启动100个进程。

而每个进程都是占用系统资源的,有些资源是有一定限制的,用ulimit命令可以查看

1
2
3
4
5
6
7
8
9
10
11
12
$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
file size (blocks, -f) unlimited
max locked memory (kbytes, -l) unlimited
max memory size (kbytes, -m) unlimited
open files (-n) 256
pipe size (512 bytes, -p) 1
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 2784
virtual memory (kbytes, -v) unlimited

可以看到,其中,进程的栈大小限制8192k bytes,即8M,当创建100个进程时,就会占用800M内存。而当内存不够时,系统就会报错提示,

1
Resource temporarily unavailable

所以,需要对进程总数进行控制,下面说下通过管道控制的方式。

管道FIFO First In First Out

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
touch lock_f

trap "rm -f lock_f;exec 9<&-;exec 9>&-;exit 0" 2

thread_num=10

temp_pipe=$$.fifo
mkfifo $temp_pipe
exec 9<>$temp_pipe # 绑定
#rm -rf $temp_pipe

for (( i = 0; i < thread_num; i++ )); do
echo
done >&9

for (( i = 0; i < 20; i++ ));
do
{
echo "$i wait"
read -u9
echo "$i start"
if [[ -f lock_f ]]; then
echo >&9
exit
fi
sleep 2
echo "$i end"
echo >&9
} &
done

wait

rm lock_f

exec 9>&- # 关掉输出
exec 9<&- # 关掉输入

echo "all end"
  • 通过trap拦截编号是2的信号,然后在里面执行一些操作
  • 通过lock file将中断通知给所有进程
  • mkfifo创建管道,然后与系统输入输出绑定,操作系统输入输出等同于操作管道
  • 使用wait,等待所有进程执行完毕

12. 改变输出颜色

这里的改变颜色,指的是改变在shell中输出的颜色,从本质上来讲,输出的其实都是字符,只不过对于一些特殊的字符,shell会帮忙顺手处理一下,然后再显示出来。对于颜色的标定,是使用\033[来标记,shell设定好了一些颜色,比如绿色、红色等,除了颜色,还有样式,比如加粗,具体格式如下

1
echo -e '\033[31m 31m \033[0m'

31m表示的是红色,0m表示不修改,也就是恢复。跟在\033[31m后面的内容都会变成红色,因为只想改变这一条log的颜色,所以在结束后还要把颜色改回来,避免影响下面的输出。其中,-e表示解释转义字符,即\033,否则将原样输出。

31之前的值,我挨着试了一遍,没有什么特别醒目的改变,后面的几个变化较为明显,光说是说不出来,还说上个图吧

总的来说,31、32和33就是标准的红绿黄,可以用来输出error、debug和warn级别的log。剩下的也就35和36看上去还算直观,可以用来输出一些需要特别关注的log。

13. cat命令

cat命令

14. sed命令

sed命令

15. tr命令

tr命令

16. xargs命令

xargs命令

17. vim使用

vim使用

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2022/08/8d305fafd271/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道