本文最后更新于 2024-01-26,文章内容可能已经过时。

Shell脚本总结

一、简介

#!/bin/sh

# 脚本中的第一行的“#!”为Shebang字符串。通常出现在类Unix系统的脚本中第一行,作为前两个字符。在直接调用脚本时,系统的程序载入器会分析 Shebang 后的内容,将这些内容作为解释器指令,并调用该指令,将载有 Shebang 的文件路径作为该解释器的参数,执行脚本,从而使得脚本文件的调用方式与普通的可执行文件类似。例如,以指令#!/bin/sh开头的文件,在执行时会实际调用 /bin/sh 程序(通常是 Bourne shell 或兼容的 shell,例如 bash、dash 等)来执行。
# 如果脚本文件中没有#!这一行,那么执行时会默认采用当前Shell去解释这个脚本(即:SHELL环境变量)。
# 如果#!之后的解释程序是一个可执行文件,那么执行这个脚本时,它就会把文件名及其参数一起作为参数传给那个解释程序去执行。
# 如果#!指定的解释程序没有可执行权限,则会报错 bad interpreter: Permission denied。如果#!指定的解释程序不是一个可执行文件,那么指定的解释程序会被忽略,# 转而交给当前的SHELL去执行这个脚本。
# 如果#!指定的解释程序不存在,那么会报错 bad interpreter: No such file or directory。注意:#!之后的解释程序,需要写其绝对路径(如:#!/bin/bash),它是不会自动到环境变量PATH中寻找解释器的。要用绝对路径是因为它会调用系统调用execve,这可以用strace工具来查看。
# 脚本文件必须拥有可执行权限。可通过chmod +x [filename] 来添加可执行权限。
# 当然,如果你使用类似于 bash test.sh ,python train.py这样的命令来执行脚本,那么#!这一行将会被忽略掉,解释器当然是用命令行中显式指定的解释器。

二、变量

1、Shell内置变量

内置变量 描述
$? 上一条命令执行状态 0 代表执行成功,1代表执行失败
$0~$9 位置参数1-9
${10} 位置参数10
$# 位置参数个数
$$ 脚本进程的PID
$- 传递到脚本中的标识
$! 运行在后台的最后一个作业的进程ID(PID)
$_ 之前命令的最后一个参数
$@ 传递给脚本或函数的所有参数。被双引号(" ")包含时,与 $* 稍有不同
$* 传递给脚本或函数的所有参数
$0 脚本的文件名
${@: -1} 传递给脚本或函数的最后一个参数
${@:1:$#-1} 传递给脚本或函数除最后一个参数以外的所有参数

$* 和 $@ 的区别

$*$@ 都表示传递给函数或脚本的所有参数,不被双引号(" ")包含时,都以"$1" "$2" … "$n" 的形式输出所有参数。但是当它们被双引号(" ")包含时,"$*" 会将所有的参数作为一个整体,以"$1 $2 …\$n"的形式输出所有参数;"$@" 会将各个参数分开,以”\$1” "\$2" … "$n" 的形式输出所有参数。

2、变量的定义、赋值

①将命令输出赋值变量

var=`shell命令`  # `是反引号
var=$(shell命令) 
var='
line 1
line 2
line 3
'

②读取标准输入输出赋值给变量

read -p "请输入一个字符: " key
echo $key

3、变量的引用

①基础引用

$var
${var}
${var:defaultvalue}

②变量的引用默认值

image-20231019140629141

③用变量值作为新变量名

$ name=test
$ test_p=123
$ echo `eval echo '$'"$name""_p"`
123

或者

$ var="world"
$ declare "hello_$var=value"
$ echo $hello_world
value

或者( bash 4.3+)

$ hello_world="value"
$ var="world"
$ declare -n ref=hello_$var
$ echo $ref
value

或者

$ hello_world="value"
$ var="world"
$ ref="hello_$var"
$ echo ${!ref}
value

参考:https://github.com/dylanaraps/pure-bash-bible#variables

④多行与单行变量值的引用

在 bash/sh中,如果变量的值是一个命令的输出,而输出是多行,那么不同的引用方式,得到的数据是不同的

$ var='line 1
line 2
line 3
'

$ echo $var 
line 1 line 2 line 3

$ echo "$var"
line 1
line 2
line 3

# 总共是有四行的输出,最后一个是空行

4、变量的数值运算

①加减乘除

#样本数据
a=120
b=110

((c=$a+$b))    #结果:230
((d=$a-$b))    #结果:10
((e=$a*$b))    #结果:13200
((f=$a/$b))    #结果:1

c=$((a+b))     #结果:220
d=$((a-b))     #结果:20
e=$((a*b))     #结果:12000
f=$((a/b))     #结果:1

c=`expr a+b`        #结果:220
d=`expr $a - $b`    #结果:20
e=`expr $a \* $b`   #结果:12000
f=`expr $a / $b`    #结果:1

②自增

a=1

#第一种整型变量自增方式
a=$(($a+1))
echo $a

#第二种整型变量自增方式
a=$[$a+1]
echo $a

#第三种整型变量自增方式
a=`expr $a + 1`
echo $a

#第四种整型变量自增方式
let a++
echo $a

#第五种整型变量自增方式
let a+=1
echo $a

#第六种整型变量自增方式
((a++))
echo $a

5、数值变量的判断

-gt    大于,如[ $a -gt $b ]
-lt    小于,如[ $a -lt $b ]
-eq    等于,如[ $a -eq $b ]
-ne    不等于,如[ $a -ne $b ]
-ge    大于等于,如[ $a -ge $b ]
le    小于等于 ,如 [ $a -le $b ]
<     小于(需要双括号),如:(($a < $b))
<=    小于等于(需要双括号),如:(($a <= $b))
>     大于(需要双括号),如:(($a > $b))
>=    大于等于(需要双括号),如:(($a >= $b))

6、变量的处理

①变量输出多行变一行并追加字符

$ echo $a

1

2

3

$ echo $a | tr '\n' ',’

1,2,3,

②位数截取

a=1110418197875

# 截去后三位,要求只取"1110418197875"

# 方式1: 数值运算
b=$((a/1000))

# 方式2:字符截取(将数值变量当成字符串来处理)
c=${a:0:-3}

三、文件目录的判断

[ -a FILE ] 如果 FILE 存在则为真。
[ -b FILE ] 如果 FILE 存在且是一个块文件则返回为真。
[ -c FILE ] 如果 FILE 存在且是一个字符文件则返回为真。
[ -d FILE ] 如果 FILE 存在且是一个目录则返回为真。
[ -e FILE ] 如果 指定的文件或目录存在时返回为真。
[ -f FILE ] 如果 FILE 存在且是一个普通文件则返回为真。
[ -g FILE ] 如果 FILE 存在且设置了SGID则返回为真。
[ -h FILE ] 如果 FILE 存在且是一个符号符号链接文件则返回为真。(该选项在一些老系统上无效)
[ -k FILE ] 如果 FILE 存在且已经设置了冒险位则返回为真。
[ -p FILE ] 如果 FILE 存并且是命令管道时返回为真。
[ -r FILE ] 如果 FILE 存在且是可读的则返回为真。
[ -s FILE ] 如果 FILE 存在且大小非0时为真则返回为真。
[ -u FILE ] 如果 FILE 存在且设置了SUID位时返回为真。
[ -w FILE ] 如果 FILE 存在且是可写的则返回为真。(一个目录为了它的内容被访问必然是可执行的)
[ -x FILE ] 如果 FILE 存在且是可执行的则返回为真。
[ -O FILE ] 如果 FILE 存在且属有效用户ID则返回为真。
[ -G FILE ] 如果 FILE 存在且默认组为当前组则返回为真。(只检查系统默认组)
[ -L FILE ] 如果 FILE 存在且是一个符号连接则返回为真。
[ -N FILE ] 如果 FILE 存在 and has been mod如果ied since it was last read则返回为真。
[ -S FILE ] 如果 FILE 存在且是一个套接字则返回为真。
[ FILE1 -nt FILE2 ] 如果 FILE1 比 FILE2 新, 或者 FILE1 存在但是 FILE2 不存在则返回为真。
[ FILE1 -ot FILE2 ] 如果 FILE1 比 FILE2 老, 或者 FILE2 存在但是 FILE1 不存在则返回为真。
[ FILE1 -ef FILE2 ] 如果 FILE1 和 FILE2 指向相同的设备和节点号则返回为真。

四、字符串的处理

1、截取

表达式 含义
${#string} $string的字符个数
${string:position} $string中, 从位置$position开始提取子串
${string:position:length} $string中, 从位置positionlength的子串
${string#substring} 从 变量$string的开头, 删除最短匹配$substring的子串
${string##substring} 从 变量$string的开头, 删除最长匹配$substring的子串
${string%substring} 从 变量$string的结尾, 删除最短匹配$substring的子串
${string%%substring} 从 变量$string的结尾, 删除最长匹配$substring的子串
${string/substring/replacement} 使用$replacement, 来代替第一个匹配的$substring
${string//substring/replacement} 使用$replacement, 代替所有匹配的$substring
${string/#substring/replacement} 如果$string的前缀匹配substring,replacement来代替匹配到的$substring
${string/%substring/replacement} 如果$string的后缀匹配substring,replacement来代替匹配到的$substring
expr match "$string" '$substring' 匹配$string开头的$substring* 的长度
expr "$string" : '$substring' 匹 配$string开头的$substring* 的长度
expr index "$string" $substring $string中匹配到的$substring的第一个字符出现的位置
expr substr $string $position $length $string中 从位置positionlength的子串
expr match "$string" '\($substring\)' $string的 开头位置提取$substring*
expr "$string" : '\($substring\)' $string的 开头位置提取$substring*
expr match "$string" '.*\($substring\)' $string的 结尾提取$substring*
expr "$string" : '.*\($substring\)' $string的 结尾提取$substring*

①#号从左边开始,删除第一次匹配到条件的左边字符,保留右边字符

# 样本: a="docker.io/openshift/origin-metrics-cassandra:v3.9"
b=${a#*/};echo $b
# 结果:openshift/origin-metrics-cassandra:v3.9

②##号从左边开始,删除最后一次匹配到条件的左边字符,保留右边字符

# 样本: a="docker.io/openshift/origin-metrics-cassandra:v3.9"
b=${a##*/};echo $b
# 结果:origin-metrics-cassandra:v3.9

③%号从右边开始,删除第一次匹配到条件的右边内容,保留左边字符(不保留匹配条件)

# 样本: a="docker.io/openshift/origin-metrics-cassandra:v3.9"   
b=${a%/*};echo $b
# 结果:docker.io/openshift

④ %%号从右边开始,删除最后一次匹配到条件的右边内容,保留左边字符(不保留匹配条件)

# 样本: a="docker.io/openshift/origin-metrics-cassandra:v3.9"   
b=${a%%/*};echo $b
# 结果:docker.io

⑤从左边第几个字符开始,及字符的个数

# 样本: a="docker.io/openshift/origin-metrics-cassandra:v3.9"   
b=${a:0:5};echo $b
# 结果:docke

⑥从左边第几个字符开始,一直到结束

# 样本: a="docker.io/openshift/origin-metrics-cassandra:v3.9"   
b=${a:7};echo $b
# 结果:io/openshift/origin-metrics-cassandra:v3.9

⑦从右边第几个字符开始,向右截取 length 个字符。

# 样本: a="docker.io/openshift/origin-metrics-cassandra:v3.9"   
b=${a:0-8:5};echo $b
# 结果:dra:v

⑧从右边第几个字符开始,一直到结束

# 样本: a="docker.io/openshift/origin-metrics-cassandra:v3.9"   
b=${a:0-8};echo $b
# 结果:dra:v3.9

⑨截取字符串中的ip

# 样本: a="当前 IP:123.456.789.172  来自于:中国 上海 上海  联通"
b=${a//[!0-9.]/};echo $b

或者
echo $a | grep -o -E "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]*"
# 结果:123.456.789.172

⑩提取字符串中的数字

echo "test-v1.1.0" | tr -cd '[0-9.]'    # 输出1.1.0
aa="test-v1.1.0" | echo ${aa//[!0-9.]/} # 输出1.1.0

2、包含判断

样本数据

a="test"
b="curiouser"
c="test hahah devops"

①通过grep来判断

if `echo $c |grep -q $a` ;then
    echo "$c" " ----包含--- " "$a"
else
    echo "$c" " ----不包含--- " "$a"
fi

②字符串运算符

if [[ $c =~ $a ]] ;then
    echo "$c" " ----包含--- " "$a"
else
    echo "$c" " ----不包含--- " "$a"
fi

③用通配符*号

用通配符*号代替str1中非str2的部分,如果结果相等说明包含,反之不包含

if [[ $c == *$a* ]] ;then
    echo "$c" " ----包含--- " "$a"
else
    echo "$c" " ----不包含--- " "$a"
fi

④利用替换

if [[ ${c/$a//} == $c ]] ;then
    echo "$c" " ----不包含--- " "$a"
else
    echo "$c" " ----包含--- " "$a"
fi

五、语句控制

1、IF 判断

if [ command ]; then
    符合该条件执行的语句
fi
if [ command ]; then
    command执行返回状态为0要执行的语句
else
    command执行返回状态为1要执行的语句
fi
if [ command1 ]; then
    command1执行返回状态为0要执行的语句
elif [ command2 ]; then
    command2执行返回状态为0要执行的语句
else
    command1和command2执行返回状态都为1要执行的语句
fi

PS: [ command ],command前后要有空格

2、for循环

①数字性循环

#!/bin/bash
for((i=1;i<=10;i++));
do
    echo $(expr $i \* 3 + 1);
done
#!/bin/bash
for i in $(seq 1 10)
do
    echo $(expr $i \* 3 + 1);
done
#!/bin/bash
for i in {1..10}
do
    echo $(expr $i \* 3 + 1);
done
#!/bin/bash
awk 'BEGIN{for(i=1; i<=10; i++) print i}'

②字符性循环

#!/bin/bash
for i in `ls`;
do
    echo $i is file name\! ;
done
#!/bin/bash
for i in $* ;
do
    echo $i is input chart\! ;
done
#!/bin/bash
for i in f1 f2 f3 ;
do
    echo $i is appoint ;
done
#!/bin/bash
list="rootfs usr data data2"

for i in $list;
do
    echo $i is appoint ;
done

③路径查找

#!/bin/bash
for file in /proc/*;
do
    echo $file is file path \! ;
done
#!/bin/bash
for file in $(ls *.sh)
do
    echo $file is file path \! ;
done

3、While循环

while condition ; do
    statements ...
done 

Note: 和if一样,condition可以有一系列的statements组成,值是最后的statment的exit status

4、Util循环

until [condition-is-true] ; do 
  statements ... 
done 

Note: 执行statements,直至command正确运行。在循环的顶部判断条件,并且如果条件一直为false那就一直循环下去

六、自定义函数

格式

[ function ] 函数名 [()]
{
    action;
    [return int;]
}

# 1.函数在被调用前先声明好
# 2.function关键字可有无

调用

$ 函数名()
$ 函数名(参数1,参数2)
$ 函数名 参数1 参数2
$ (函数名 参数1 参数2)

七、参考