Shell脚本
终端颜色使用
-
颜色及其对应代码列表
字体颜色 颜色代码 背景颜色 颜色代码 重置 0 重置 0 黑色 30 黑色 40 红色 31 红色 41 绿色 32 绿色 42 黄色 33 黄色 43 蓝色 34 蓝色 44 洋红 35 洋红 45 青色 36 青色 46 白色 37 白色 47
代码如下
-e 表示使用转义字符串, -n 不输出额外的换行
echo -ne "\e[1;31m This\e[0m\e[1;32m is\e[0m\e[1;33m colorful\e[0m\e[1;34m words \e[0m"
echo -ne "\e[1;41m\e[1;34mHello\e[0m \e[1;47m\e[1;32mWorld\e[0m"
注意,要设置颜色需要使用
\e[1;开始后跟颜色。重置则使用\e[0m具体看上一个例子
还有很多ASCII终端控制操作,具体可以自行查找。
数值计算
因为 shell 脚本变量默认都为字符串类型,所以需要使用特殊的操作(关键字,括号等)来计算
-
let
no1=4 no2=5 let no1++ # 5 echo $no1 let result=no1+no2 # result = 5 + 5 # result = 10 echo $result let no1++ echo no1++ is $no1 let no1-- echo no1-- is $no1 let no1+=2 echo no1+=2 is $no1 let no1-=2 echo no1-=2 is $no1 let nor=no1**2 echo no1**2 is $nor # 结果为4 -
(())
result3=$(( no1 + 50 )) echo result3="(("no1 + 50"))" is $result3 -
[]
# [] result1=$[ no1 + no2 ] echo result1=no1+no2 is $result1
其他诸如位运算 ~、>>、<<、&、|、^ 和c语言相同,并且也支持三元表达式。在处理更高级的运算时,比如浮点数,shell脚本无法直接计算,但是可以借助 bc 等类似的命令来处理。例如下面的进制转换。
no=100
# obase 表示输出的基数,ibase表示输入的基数
echo "obase=2;$no" | bc
no=11001001
echo "obase=16;ibase=2;$no" | bc
# 运行结果为
# 1100100
# C9
note: 反引号可以获得终端命令运行结果
echo `ls -al | grep waka`
数组
要获取数组所有元素可以使用 * 或者 @ 如下
arr=("a" "b" "c")
echo ${#arr[*]}
此处${arr[*]} 获取了所有数组元素, 同时 # 得到元素个数, 由此可以使用 for 循环遍历元素数组内的元素
for ((i=0; i < ${#arr[*]}; i++))
do
echo ${arr[$i]}
done
注意: 取数组元素个数时使用 # 号需要配合 * 或者 @ 使用,如果只是单纯使用 # 则表示取元素字符串的长度
# 基于上面的arr数组, 有下面的表达式
echo ${#arr}
上面 shell 脚本执行结果为 1, 因为 # 取的是 arr 数组第一个元素的字符串长度(当然 unicode 也是如此)
emojis=(😁 😸 )
echo ${#emojis[*]}
# 结果
# 3
for ((i=0; i < ${#emojis[*]}; i++))
do
echo ${emojis[$i]}
done
# 😁
# 😸
#
数组添加元素
foo=()
foo+=(hello world)
删除元素
foo=(hello world aaa)
# 删除了world
unset foo[1]
# 删除整个数组
unset foo
但是注意,这里虽然删除了world,但是其他元素的下标并不会变,当访问下标为2的元素时任然为aaa,但是访问下标为1的元素时,结果为空。
还有一件事,数组中如果不加下标进行操作,操作对象始终是下标为0的元素 。 比如下面的操作
foo=(hello world aaa)
foo= # 此处实际上将下标为0的元素设置为空
echo ${foo[@]} # 结果为 world aaa
foo=(hello world aaa)
foo=A
echo ${foo[@]} # A world aaa
map(关联数组)
可以查看 declare 的文档,不同的参数的用处,这里使用 -A 创建关联数组
declare -A ass_array
ass_array=([index1]=val1 [index2]=val2)
echo ${ass_array[*]}
# val1 val2
# 获取关联数组索引
echo ${!ass_array[*]} # 也可以使用 ass_array[@]
# index1 index2
函数参数
参数使用 $1, $2 以此类推来获取对应位置的参数, $* 获取所有参数并且所有参数视为一个整体, $@ 获取所有参数但是是分开来的
# file: showArgs.sh
echo "\$@: "
for arg_s in "$@"
do
echo $arg_s
done
echo -ne "\n\$*: "
for arg_w in "$*"
do
echo $arg_w
done
运行
bash showArgs.sh 1 2 3 4
$@:
1
2
3
4
$*: 1 2 3 4
注意: 此处 $@ 和 $* 都需要加上双引号,否则二者效果和加了引号的 $@ 是一样的
使用 shift 来位移参数,比如下面的操作
# file: testShift.sh
#!/bin/bash
len=$#
for ((i=0; i<$len; i++))
do
echo $1
shift
done
# 运行这个文件
./testShift.sh 1 2 3
1
2
3
因为 $# 获取到了输入参数的个数为3,这里循环输出 $1 实际上是将参数位置向前移动,这里还要注意的是 $# 这个的值是动态的,每使用一次 shift 都会让 $# 的值减1。
所以如果想输出全部参数就不要写成下面这样。
for ((i=0; i<$#; i++))
do
echo $1
shift
done
函数返回值
按道理来说函数返回值只能是0-255之间的一个整数,即使用诸如 return 1 之类的操作来返回,但是也可以用别的操作来获取不是数字的值。
使用子shell
具体 子shell 到下面再说,先用起来😄。
hello() {
echo "hello"
}
var=$(hello)
这里子shell会捕获echo的输出,最后返回赋值给var。
使用引用
还可以使用 local 来创建引用变量。
hello() {
local -n h=$1
h="hello"
}
hello var
echo $var # 结果为 hello
local -n 创建了一个指向 $1 的变量。即将一个变量名与另一个变量名进行绑定,这样它们会指向同一个存储位置。最终也实现了获取函数结果的效果。
local 是一个内置命令,即没有对应的二进制文件可以执行,它的参数其实是和另一个内置命令 declare 是相同的,也就是说 declare -n 是声明了一个引用。下面是info里的关于 -n 的解释。
Give each NAME the NAMEREF attribute, making it a name reference to another variable. That other variable is defined by the value of NAME. All references, assignments, and attribute modifications to NAME, except for those using or changing the
-nattribute itself, are performed on the variable referenced by NAME's value. The nameref attribute cannot be applied to array variables.
因为没有创建子shell,所以创建引用相同情况下会比使用子shell更加快速。下面是一个例子。
t1.sh 使用子shell传递函数参数
file: t1.sh
#!/bin/bash
h() {
var=""
for j in `seq 1 100`;
do
var=var$j
done
echo -ne $var
}
hs=()
for i in `seq 1 10000`;
do
v=$(h)
hs+=(v)
done
t2.sh 使用引用变量获取函数结果
file: t2.sh
#!/bin/bash
h() {
local -n var="$1"
for j in `seq 1 100`;
do
var=var$j
done
}
hs=()
for i in `seq 1 10000`;
do
h v
hs+=(v)
done
下面是二者运行需要的时间,作为参考。
time ./t1.sh # ./t1.sh 9.81s user 2.56s system 104% cpu 11.851 total
time ./t2.sh # ./t2.sh 6.35s user 1.68s system 103% cpu 7.781 total
坑
在使用引用的时候存在一个坑,其实这也不算引用特有的坑吧,顺带提一嘴。就是在外部调用函数时,如果内部使用了和外部相同的变量名(这里是指函数内部变量不是全局变量,如果不加 local 或者 declare 默认为全局变量),引用可能会失效。下面是一个具体的例子。
这里正常使用引用获取函数的结果
hello() {
local -n l=$1
local b=""
le=100
l=$le
}
a() {
hello var
echo $var
}
a
# 输出结果
100
这个没有什么问题,但是如果在函数内部有一个和外部引用变量相同的本地变量,那这就不对了。
hello() {
local -n l=$1
local var="hello"
le=100
l=$le
}
a() {
hello var
echo -n "var is: $var"
}
a
# 输出结果
# var is:
这个也是一个比较恶心的地方,假如我外部引用的名字与内部变量相同的名字,那么引用就无法作用到外部变量但是如果var是一个全局变量是可以的。
hello() {
local -n l=$1
var="hello"
l=100
}
a() {
hello var
echo -n "var is: $var"
}
# 输出结果
var is: 100
子 shell和组命令
-
子shell格式: (cmd; cmd; ...) 子shell会在当前shell的复制中执行命令,即子shell会复制当前shell的变量,创建一个新的shell实例,当子shell退出时,复制和操作的环境变量也就消失了。
-
组命令格式: {cmd; cmd; ...} 和子shell不同,它是在当前shell中执行,因此相同情况下会更快。其他大体相同。主要用在管道和重定向上。
# 使用组命令合并输出
{ ls -l; echo "hello"; cat hello; } | awk '{print $0}'
这里需要注意的是,每一个命令都必须结束都必须有分号,并且 { 命令后面需要空白符隔开。
这里着重说说 子shell。 bash的内置命令如果在管道中运行,都会在子shell中运行。 就比如下面这种情况。
echo "hello" | read
echo $REPLY
read 是bash的内置命令,用于读取终端输入,并将输入默认存储在 REPLY 中。(也可以改)但是如果通过管道将字符传送给 read,此时 read 执行在子shell中,子shell中的 REPLY 被修改,当bash等待管道内的命令执行完成,子shell的环境被丢弃,所以此时输出 REPLY 为空。(挺突然的,突然想翻译一部分Bash Info,或者Zsh的文档也行,就当个坑放这吧哈哈哈哈🤗)
进程替换
为了解决这个问题,可以使用进程替换。
产生标准输出的进程: <(list)
吸纳标准输入的进程: >(list)
read < <(echo "hello")
echo $REPLY
流程处理
这里大概只描述几个常用的,并且不打算记录if的判断操作因为懒😁。都能在文档中查到。
if
格式
if test-commands; then
consequent-commands;
[elif more-test-commands; then
more-consequents;]
[else alternate-consequents;]
fi
这里 test-commands 可以是 [[]] 表达式或者 test 命令。
[[]] 表达式
在使用正则匹配字符串的时候,这个表达式还是非常有用的。下面是一个例子。
#!/bin/bash
pattern='[[:alpha:]]*\*'
string="hello*?)world"
if [[ $string =~ $pattern ]]; then
echo "Matched! the matched is $BASH_REMATCH"
else
echo "Oops! not matched!"
fi
这里的正则表示的匹配是在 * 号以及前面有任意多的字母因此这里 hello* 被匹配,并且存储在数组 BASH_REMATCH 中。(注意这里没有加下标操作默认操作的是数组的第一个元素,切记!!!)
对于 [[]] 表达式中使用正则表达式有下面几条规则。
不仅是 =~ 表示要进行正则表达式匹配 == != 也表示对右边的正则进行匹配。(这里还有一些细节选项可以设置,可以参考bash info)。
- 使用引号包裹,并且直接使用的字面值的pattern将被完全匹配字面值,特殊字符将失去意义。
- 匹配的右侧被视为pattern(POSIX extended regular expression pattern)。
- 匹配成功返回0失败返回1,正则表达式错误返回2。
- 如果pattern在变量中,双引号引用的pattern在使用时仍然完全匹配字面。如果为单引号引用的变量,pattern中的特殊字符将拥有其特殊含义。
- 反斜线(backslash)在没有引号的pattern中将移除特殊字符的特殊含义,如果被引号包裹,shell将尽可能保留字面含义。
- 反斜线如果使用在变量中,效果同第4点。
引用官方的一个例子
pattern='\.'
[[ . =~ $pattern ]]
[[ . =~ \. ]]
[[ . =~ "$pattern" ]]
[[ . =~ '\.' ]]
以上前两项会被匹配,后两项则不行。
- 匹配字面值除了引号也可以使用
[] - 匹配到的字符串会被存储在数组变量
BASH_REMATCH中。
下面是原话。
The array variable BASH_REMATCH records which parts of the string matched the pattern. The element of BASH_REMATCH with index 0 contains the portion of the string matching the entire regular expression. Substrings matched by parenthesized subexpressions within the regular expression are saved in the remaining BASH_REMATCH indices. The element of BASH_REMATCH with index n is the portion of the string matching the nth parenthesized subexpression.
意思是,这个数组第一个元素是整个正则表达式的匹配项,后面每一个项都是正则表达式中使用小括号匹配到的项。
下面是一个示例程序。
my_pattern='(.*?\?)\)world'
if [[ $string =~ (.*?\?)\)(world) ]]; then
echo Matched!
echo first value of is $BASH_REMATCH
echo the rest of them are
for rest in ${BASH_REMATCH[@]:1}
do
echo $rest
done
else
echo Oops! Not Matched!
fi
echo length of bash_rematch is ${#BASH_REMATCH[@]}
# 运行结果是这样的。
#
# Matched!
# first value of is hello*?)world
# the rest of them are
# hello*?
# world
# length of bash_rematch is 3
这里需要注意的是使用在打印first value的时候直接使用数组名,因为任何不添加下标的操作都是在对数组的第一个元素进行操作(学过C语言同学可能会有点体会)。
while
和if差不多只是循环罢了
i=10
while [[ $[i - 1] -ge 5 ]];
do
echo i is $i
let i--
done
for
形式诸如:
for var in sequ
do
statement
done
for (( c语言的for循环 ))
do
statement
done
唯一要注意的是在没有换行时记得用分号标记,比如 for (()); do statement; done 。
until
直到某个条件达到就跳出循环
下面是一个例子
i=10
until test $i -lt 5
do
echo "hello"
let i--
done
case
格式
case word in
[ [(] pattern [| pattern]…) command-list ;;]…
esac*
借用info里的一个例子
echo -n "Enter the name of an animal: "
read ANIMAL
echo -n "The $ANIMAL has "
case $ANIMAL in
horse | dog | cat) echo -n "four";;
man | kangaroo ) echo -n "two";;
*) echo -n "an unknown number of";;
esac
echo " legs."
这里的 * 匹配任何字符串,其实类似类C语言switch里面的default。
字符串处理
替换操作
-
${parameter:-word}
当 parameter 为空(不存在或者为空字符串)时用word替换,此处word为变量时需要加上
$符号,或者顺便避免歧义地加上{},也就是 ${} 。
var=5
foo=
echo ${foo:-var} # var
echo ${foo:-$var} # 5
-
${parameter:=word}
这中写法和上面作用基本相同。
-
${parameter:?word}
当 parameter 为空时程序直接退出,word内容输出到标准错误输出。

-
${parameter:+word}
当 parameter 为非空时,表达式使用word的值,但是parameter本身并不被替换。
foo=
echo ${foo:+"hello"} # 啥也不输出
foo=hello
echo ${foo:+"world"} # 输出world
获取变量名称
${!prefix*}
${!prefix@}
以上两个操作结果相同。
# 获取名字以BASH打头的变量
echo ${!BASH*}
字符串操作
- 获取字符串长度
${#parameter}
- 截取字符串
${parameter:offset:length}
这里的offset从0开始,offset或者length为负数表示从末尾开始,比如-1表示最后一个字符。length是可选项,如果不写length,那直接从对应位置到字符串结尾。
注意: 如果使用负数,记得负号 - 和冒号 : 之间需要有空格不然会被误解为前面的 :- 字符串替换。
- 左右匹配并删除
${parameter%pattern}
${parameter%%pattern}
% 表示从右边开始匹配如果匹配到就删除对应的匹配项,一个 % 表示惰性匹配,即如果匹配到了就不再继续匹配,两个表示贪婪匹配,即直到匹配到最多。
path=/home/hello/world
echo ${path%/*} # /home/hello 这里 * 表示任意字符
echo ${path%%/*} # 输出结果为空,因为全部都给匹配了
${parameter#pattern}
${parameter##pattern}
# 表示从左边开始匹配,其余规则和上面一样,就不记录了。
- 匹配替换
${parameter/pattern/string}
${parameter//pattern/string}
${parameter/#pattern/string}
${parameter/%pattern/string}
第一个表示从左匹配并且当第一个匹配到之后用string替换,第二个同理只是匹配多个pattern全部替换,第三个则是表示匹配要在字符串的开头,第四个表示匹配项要在字符串的结尾,也可以省略string来达到删除的目的。
foo=JPG.JPG
echo ${foo/JPG/jpg} # jpg.JPG
echo ${foo//JPG/jpg} # jpg.jpg
echo ${foo/#JPG/jpg} # jpg.JPG
echo ${foo/%JPG/jpg} # JPG.jpg
未完待续的杂项
响应信号
使用内建命令 trap
trap 的格式
trap argument signal [signal...]
下面是一个示例
#!/bin/bash
exit_on_sigint() {
echo "Script interrupted."
exit 0
}
trap exit_on_sigint SIGINT
for i in {1..5}
do
echo Iteration $i of 5
sleep 5
done
这里响应 Ctrl+C 发送的SIGINT打断信号,执行exit_on_sigint函数,最终退出脚本。在退出函数内做一些清理和善后操作还是不错的。
异步执行
wait(built-in)命令
等待子进程结束。下面是一个示例。
#!/bin/bash
PIDARRAY=()
for file in File1.iso File2.iso
do
md5sum $file &
PIDARRAY+=("$!")
done
wait ${PIDARRAY[@]}
$! 保存的是最近一个后台进程的PID。这里将每一个后台进程id放入数组,最后使用 wait 命令进行等待。
wait 命令等待所有子进程结束后终止脚本。
#!/bin/bash
#
for ip in 192.168.0.{1..255};
do
(
ping $ip -c2 &> /dev/null;
if [ $? -eq 0 ]; then
echo $ip is alive
fi
)&
done
wait
Coprocess
一个coprocess是以 coproc shell保留字开始的命令。coprocess异步执行在子shell中,使用 coproc 就如同在命令后加了 & 符号一样。
格式
coproc [NAME] command [redirections]
此处创建一个拥有名字的coprocess,名字为NAME,名字是可选的,如果没有名字默认为COPROC。在官方文档中推荐使用的是下面的语法。
coproc NAME { command; }
注意这里分号不能少。
总结
语言工具还是要多实践吧,shell有些坑不实际写光买些书看是体会不来的。最近也在写一个简单的脚本,找个时间说一说。--来自一个Linux爱好者🤗。
评论 (...)
加载中...