返回首页

Shell脚本

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 -n attribute 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和组命令

  1. 子shell格式: (cmd; cmd; ...) 子shell会在当前shell的复制中执行命令,即子shell会复制当前shell的变量,创建一个新的shell实例,当子shell退出时,复制和操作的环境变量也就消失了。

  2. 组命令格式: {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)。

  1. 使用引号包裹,并且直接使用的字面值的pattern将被完全匹配字面值,特殊字符将失去意义。
  2. 匹配的右侧被视为pattern(POSIX extended regular expression pattern)。
  3. 匹配成功返回0失败返回1,正则表达式错误返回2。
  4. 如果pattern在变量中,双引号引用的pattern在使用时仍然完全匹配字面。如果为单引号引用的变量,pattern中的特殊字符将拥有其特殊含义。
  5. 反斜线(backslash)在没有引号的pattern中将移除特殊字符的特殊含义,如果被引号包裹,shell将尽可能保留字面含义。
  6. 反斜线如果使用在变量中,效果同第4点。

引用官方的一个例子

pattern='\.'

[[ . =~ $pattern ]]
[[ . =~ \. ]]

[[ . =~ "$pattern" ]]
[[ . =~ '\.' ]]

以上前两项会被匹配,后两项则不行。

  1. 匹配字面值除了引号也可以使用 []
  2. 匹配到的字符串会被存储在数组变量 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

字符串处理

替换操作

  1. ${parameter:-word}

    parameter 为空(不存在或者为空字符串)时用word替换,此处word为变量时需要加上 $ 符号,或者顺便避免歧义地加上{},也就是 ${}

var=5
foo=
echo ${foo:-var} # var
echo ${foo:-$var} # 5
  1. ${parameter:=word}

    这中写法和上面作用基本相同。

  2. ${parameter:?word}

    parameter 为空时程序直接退出,word内容输出到标准错误输出。

  3. ${parameter:+word}

    parameter 为非空时,表达式使用word的值,但是parameter本身并不被替换。

foo=
echo ${foo:+"hello"} # 啥也不输出
foo=hello
echo ${foo:+"world"} # 输出world

获取变量名称

${!prefix*}

${!prefix@}

以上两个操作结果相同。

# 获取名字以BASH打头的变量
echo ${!BASH*}

字符串操作

  1. 获取字符串长度

${#parameter}

  1. 截取字符串

${parameter:offset:length}

这里的offset从0开始,offset或者length为负数表示从末尾开始,比如-1表示最后一个字符。length是可选项,如果不写length,那直接从对应位置到字符串结尾。

注意: 如果使用负数,记得负号 - 和冒号 : 之间需要有空格不然会被误解为前面的 :- 字符串替换。

  1. 左右匹配并删除

${parameter%pattern}

${parameter%%pattern}

% 表示从右边开始匹配如果匹配到就删除对应的匹配项,一个 % 表示惰性匹配,即如果匹配到了就不再继续匹配,两个表示贪婪匹配,即直到匹配到最多。

path=/home/hello/world
echo ${path%/*} # /home/hello 这里 * 表示任意字符
echo ${path%%/*} # 输出结果为空,因为全部都给匹配了

${parameter#pattern}

${parameter##pattern}

# 表示从左边开始匹配,其余规则和上面一样,就不记录了。

  1. 匹配替换

${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爱好者🤗。

评论 (...)

加载中...