Shell 脚本编程

1. 认识 Shell Scripts

  1. 学习 Shell 的疑惑
    • 如何启动命令行以及接下来做什么?
    • 如何使用 shell脚本来自动处理系统管理任务,包括从检测系统统计数据和数据文件到为你的老板生成报表?
  2. Shell 简介
    • Shell 是一个用 C 语言编写的程序,Shell 既是一种命令语言,又是一种程序设计语言。
    • Shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
    • Shell 类型
      • /bin/bash
      • /bin/tcsh
      • /bin/dash
      • /bin/csh
      • /bin/sh
      • /bin/zsh
    • 执行 Shell 脚本

      // 新增可执行权限
      chmod +x test.sh
      
      // 作为可执行程序
      ./test.sh
      
      // 作为解释器参数
      /bin/sh test.sh
      
  3. 常见 Shell 操作终端
    • Linux 控制终端
    • Terminal
      • GNOME Terminal
      • Konsole Terminal
    • Terminus
    • Xterm
    • XShell
  4. 理解 Shell 的父子关系

    $ ps -f
    
    $ bash
    $ ps -f
    输入命令之后,一个子 shell 就出现了。第二个 ps -f 是在子 shell 中执行的。可以从显示结果中看到两个 bash shell 程序在运行。
    
    $ bash
    $ bash
    $ bash
    $ ps --forest
    在上面例子中,bash 命令被输入了三次。实际上创建了三个子 shell。ps --forest 命令展示了这些子 shell 间的嵌套结构。可以使用 exit 命令退出子 shell
    $ exit
    

    进程列表

    $ pwd;ls;cd /etc;pwd
    在命令之间加入“;,指定要依次执行的一系列命令
    $ (pwd;ls;cd /etc;pwd)
    使用括号包含命令,成为进程列表
    
    查看是否生成了子 shell,使用:
    $ echo $BASH_SUBSHELL
    

    子shell用法

    // 在后台睡眠10s
    $ sleep 10&
    // 查看后台进程
    $ ps -f
    or
    $ jobs -l
    
    //将进程列表置入后台
    $ (sleep 2;echo $BASH_SUBSHELL;sleep 2)&
    // 创建备份
    $ (tar -cf Rich.rar /home/rich;tar -cf My.tar /home/christine)&
    
    //协程:在后台生成一个子shell,同时在这个子shell中执行命令。
    // 进行协程处理,使用 coproc 命令
    $ coproc sleep 10
    $ coproc My_Job{sleep 10;
    
  5. 理解 shell 的内建命令
    外部命令

    也被称为文件系统命令,是存在于bash shell之外的程序。ps 就是一个外部命令,可以使用 which 和 type 命令找到

    $ which ps
    $ type -a ps
    

    当外部命令执行时,会创建一个子进程,这种操作叫做衍生(forking)。

    内建命令

    内建命令和外部命令的区别在于前者不需要使用子进程来执行。它们已经和 shell 编译成一体,作为 shell 工具的组成部分存在。可以利用 type 命令来了解某个命令是否是内建的。

    $ type cd
    cd is a shell builtin
    

    要注意,有些命令有多种实现。既有内建命令也有外部命令。

    $ type -a echo
    echo is a shell builtin
    echo is /bin/echo
    $ type -a pwd
    pwd is a shell builtin
    pwd is /bin/pwd 
    

2. Shell 基础

2.1. Hello World

#!/bin/bash
################
# Hello World  #
################     

# This script displays the date and who's #脚本用途说明及作者等信息描述 
echo "This's is a shell script." #显示消息
echo -n "The time and date are: " #n表示在一行显示
echo "Hello,World!" # print "Hello,World!"
date # print date.

echo "User info for userid: $USER" #环境变量,用set命令可以查看一份完整的当前环境变量列表。
echo UID: $UID
echo HOME: $HOME
echo "The cost of the item is \$15." #美元需要使用\转义

days= 10 #用户自定义变量
echo $days

# 有两种方法可以将命令赋给变量
test= `date` #用一对反引号把整个命令围起来
test= $(date) #使用$()格式
today= $(date+%y%m%d) #today变量被赋予格式化后的date命令的输出。

2.2. 变量

############
# 定义变量 #
############
your_name="qinjx"
echo $your_name
echo ${your_name}
# 变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界,例如下面的情况:
for skill in Ada Coffe Action Java; do
    echo "I am good at ${skill}Script"
done

# 只读变量
myUrl="https://www.google.com"
readonly myUrl
myUrl="https://www.runoob.com"
:<<EOF
运行脚本,结果如下:
/bin/sh: NAME: This variable is read only.
EOF

############
# 删除变量 #
############
unset variable_name

2.3. 字符串

################
# Shell 字符串 #
################
str='this is a string'
your_name='zrg'
str2="Hello, I know you are \"$your_name\"! \n"
echo -e $str
:<<EOF
输出结果为:
Hello, I know you are "runoob"! 
EOF
# 拼接字符串
# 使用双引号拼接
greeting="hello, "$your_name" !"
greeting_1="hello, ${your_name} !"
# 使用单引号拼接
greeting_2='hello, '$your_name' !'
greeting_3='hello, ${your_name} !'
# 获取字符串长度
string="abcd"
echo ${#string} #输出 4
# 提取子字符串
string="runoob is a great site"
echo ${string:1:4} # 输出 unoo
# 查找子字符串
# 查找字符 i 或 o 的位置(哪个字母先出现就计算哪个):
string="runoob is a great site"
echo `expr index "$string" io`  # 输出 4

2.4. 数组

# 定义
array_name=(value0 value1 value2 value3)
# 读取数组
value=${array_name[n]}
# 使用 @ 符号可以获取数组中的所有元素
echo ${array_name[@]}
# 获取数组的长度
# 取得数组元素的个数
length=${#array_name[@]}
# 或者
length=${#array_name[*]}
# 取得数组单个元素的长度
lengthn=${#array_name[n]}

2.5. 注释

  1. 单行注释:以 # 开头的行就是注释
  2. 多行注释:

    :<<EOF
    注释内容...
    EOF
    
    # 或者是
    :<<'
    注释内容...
    '
    
    :<<!
    注释内容...
    !
    

2.6. 环境变量(Environment Parameter)

  1. 概念:环境变量(environment variable),用来存储有关 shell 会话和工作环境的信息。
  2. 全局环境变量和局部环境变量:

    // 查看全局变量
    $ env
    or
    $ printenv
    
    // 查看某个全局环境变量
    $ env HOME
    or
    $ echo $HOME
    
    // set 命令会显示为某个特定进程设置的所有环境变量,包括全局变量、局部变量以及用户自定义变量。
    $ set
    
  3. 设置用户自定义变量

    $ my_variable=Hello
    

    注意:所有环境变量名均使用大写字母,这是 bash shell 的标准惯例。自己创建的局部变量或是 shell 脚本,请使用小写字母。变量名区分大小写。

    $ my_variable="Hello World"
    
    // 设置全局变量
    $ export my_variable="I am Global now"
    
    // 删除环境变量
    $ unset my_variable
    

    注意:如果要用到变量,使用\(;如果要操作变量,不使用\)。

  4. PATH、PS1 环境变量

    // 全局环境变量
    $ PATH=$PATH:/opt/test/scripts
    
    // 自定义用户命令行的字符显示
    

    PS1 默认提示符变量,如动态显示当前目录:

    $ export PS1="[\u@\h \w]"
    
    Table 1: PS1 变量可使用的参数值
    \d 代表日期,格式为weekday month date,例如:"Mon Aug 1"
    \H 完整的主机名称。例如:我的机器名称为:fc4.linux,则这个名称就是fc4.linux
    \h 仅取主机的第一个名字,如上例,则为fc4,.linux则被省略
    \t 显示时间为24小时格式,如:HH:MM:SS
    \T 显示时间为12小时格式
    \A 显示时间为24小时格式:HH:MM
    \u 当前用户的账号名称
    \v BASH的版本信息
    \w 完整的工作目录名称。家目录会以 ~代替
    \W 利用basename取得工作目录名称,所以只会列出最后一个目录
    \# 下达的第几个命令
    \$ 提示字符,如果是root时,提示符为:# ,普通用户则为:$
    \[ 字符"["
    \] 字符"]"
    \! 命令行动态统计历史命令次数

    PS2 是副提示符变量,默认值是''> ''。PS2一般使用于命令行里较长命令的换行提示信息。可自定义设置如下:

    $ export PS2="PS2 => "
    

    另外,还有 PS3 和 PS4,因为这两个环境变量可能用得不多,所以在这就不介绍了,感兴趣的小伙伴可自行研究。

  5. 定位系统环境变量
    • 登录时作为默认登录 shell
      登录 shell 会从5个不同的启动文件里读取命令,其中 /etc/profile 是默认的 bash shell 主启动文件。

      $HOME/.bash_profile
      $HOME/.bashrc
      $HOME/.bash_login
      $HOME/.profile
      
    • 作为非登录 shell 的交互式 shell
      作为非登录 shell 的交互式启动的,它不会访问 /etc/profile 文件,只会检查 HOME 目录中的 .bashrc 文件。
      .bashrc 文件有两个作用:一是查看/etc目录下通用的 bashrc 文件;二是为用户提供一个定制自己的命名别名和私有脚本函数的地方。
    • 作为运行脚本的非交互式shell
      系统执行 shell 脚本时使用,不同的地方在于它没有命令提示符。bash shell 提供了 BASH_ENV 环境变量,当 shell 启动一个非交互式 shell 进程时,它会检查这个环境变量来查看要执行的启动文件。
      在大多数发行版中,存储个人用户永久性 bash shell 变量的地方是 $HOME/.bashrc 文件。但如果设置了 BASH_ENV 变量,那么记住,除非它指向的是 $HOME/.bashrc,否则应该将非交互式 shell 的用户变量放在别的地方。
  6. 数组变量

    // 环境变量作为数组使用
    $ mytest=(one two three four five)
    $ echo ${mytest[2]}
    three
    $ echo ${mytest[*]}
    one two three four five
    
    //改变某个索引的值
    $ mytest[2] = seven
    
    //删除某个索引的值和删除整个数组
    $ unset mytest[2]
    $ unset mytest
    
  7. 环境变量配置文件
    • /etc/profile
    • /etc/profile.d/*.sh
    • ~/.bash_profile
    • ~/.bashrc
    • /etc/bashrc
    • ~/.bash_logout
    • ~/.bash_history
    • 本地终端欢迎信息
    • 登录后的欢迎信息

2.7. 重定向(Redirect)输入和输出

Table 2: 重定向字符
0 标准输入
1 标准输出
2 标准错误输出
> 默认为标准输出重定向,与 >1 相同
2>&1 把标准输出重定向到标准输出
&>file 把标准输出和标准错误输出都重定向到 file 中
/dev/null 是一个特殊文件,所有重定向到它的东西都丢弃掉
  1. 输出重定向

    // 标准输出重定向
    $ date > test
    $ date >> test
    
    // 标准错误输出重定向
    $ date 2>test
    $ date 2>>test
    
    // 正确输出和错误输出同时保存
    $ date > test 2>&1
    $ date >> test 2>&1
    $ date &>test
    $ date &>>test
    $ date >>test1 2>>test2
    
  2. 输入重定向

    // 输入重定向
    $ wc < test
    //wc 命令,默认情况下,会输出3个值:
    
    • 文本的行数
    • 文本的词数
    • 文本的字节数

      // 内联输入重定向(inline input redirection)
      $ wc << EOF
      

      shell 会用PS2环境变量中定义的次提示符来提示输入数据

2.8. 通配符(Wildcard Character)

  1. shell通配符(wildcard)

    Table 3: shell 常见通配符
    字符 含义 实例
    匹配 0 或多个字符 a*b a与b之间可以有任意长度的任意字符, 也可以一个也没有, 如aabcb, axyzb, a012b, ab。
    ? 匹配任意一个字符 a?b a与b之间必须也只能有一个字符, 可以是任意字符, 如aab, abb, acb, a0b。
    [list] 匹配 list 中的任意单一字符 a[xyz]b a与b之间必须也只能有一个字符, 但只能是 x 或 y 或 z, 如: axb, ayb, azb。
    [!list] 匹配 除list 中的任意单一字符 a[!0-9]b a与b之间必须也只能有一个字符, 但不能是阿拉伯数字, 如axb, aab, a-b。
    [c1-c2] 匹配 c1-c2 中的任意单一字符 如:[0-9] [a-z] a[0-9]b 0与9之间必须也只能有一个字符 如a0b, a1b… a9b。
    {string1,string2,…} 匹配 sring1 或 string2 (或更多)其一字符串 a{abc,xyz,123}b a与b之间只能是abc或xyz或123这三个字符串之一。
  2. shell 特殊字符 shell 除了有通配符之外,由shell 负责预先先解析后,将处理结果传给命令行之外,shell还有一系列自己的其他特殊字符。

    Table 4: shell 特殊字符
    字符 说明
    IFS 由 <space> 或 <tab> 或 <enter> 三者之一组成(我们常用 space )。
    CR 由 <enter> 产生。
    = 设定变量。
    $ 作变量或运算替换(请不要与 shell prompt 搞混了)。
    > 重导向 stdout。 *
    < 重导向 stdin。 *
      命令管线。 *
    & 重导向 file descriptor ,或将命令置于背境执行。 *
    ( ) 将其内的命令置于 nested subshell 执行,或用于运算或命令替换。 *
    { } 将其内的命令置于 non-named function 中执行,或用在变量替换的界定范围。
    ; 在前一个命令结束时,而忽略其返回值,继续执行下一个命令。 *
    && 在前一个命令结束时,若返回值为 true,继续执行下一个命令。 *
    两个竖线 在前一个命令结束时,若返回值为 false,继续执行下一个命令。 *
    ¡ 执行 history 列表中的命令。*
  3. shell 转义符

    Table 5: shell 转义符号
    字符 说明
    ‘’(单引号) 又叫硬转义,其内部所有的shell 元字符、通配符都会被关掉。注意,硬转义中不允许出现’(单引号)。
    “”(双引号) 又叫软转义,其内部只允许出现特定的shell 元字符:$用于参数代换 `用于命令代替
    \(反斜杠) 又叫转义,去除其后紧跟的元字符或通配符的特殊意义。
  4. shell 解析脚本过程

    shell-analysis-process.png

2.9. 位置参数(Positional Parameter)

  1. 特殊变量

    Table 6: 位置参数
    位置参数变量 说明
    $n n为自然数。0代表命令本身,0代表命令本身,1到9代表第1到第9个参数(参数的值是执行该命令时,从9代表第1到第9个参数(参数的值是执行该命令时,从1开始依次输入的),十以上的参数要用大括号包含,如${10}。
    $* 这个变量代表命令行中所有的参数(不包括$0),它把所有的参数当做一个整体对待。对其进行for循环遍历时,只会循环一次。
    $@ 这个变量也代表命令行中所有的参数(不包括$0),它把所有的参数当做独立的个体对待。对其进行for循环遍历时,可循环多次。
    $# 这个变量代表命令行中所有参数的个数(不包括$0)。
    $$ 脚本运行的当前进程ID号
    $! 后台运行的最后一个进程的ID号
    $- 显示Shell使用的当前选项,与set命令功能相同。
    $? 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。

    $ 与 $@ 区别:*

    • 相同点:都是引用所有参数。
    • 不同点:只有在双引号中体现出来。假设在脚本运行时写了三个参数 1、2、3,,则 " * " 等价于 "1 2 3"(传递了一个参数),而 "@" 等价于 "1" "2" "3"(传递了三个参数)。
  2. 读取参数

    #!/bin/bash
    # using one command line parameter
    echo "执行的文件名:$0";
    echo "第一个参数为:$1";
    echo "第二个参数为:$2";
    
    factorial=1
    for ((number=1; number<=$1; number++))
    do
        factorial=$[$factorial * $number]
    done
    echo "The factorial of $1 is $factorial"
    

2.10. 运算符(Operational Character)

  1. 方法1:declare

    $ declare -i c=$a+$b
    $ echo $c
    
  2. 方法2:expr 或 let 运算工具

    $ c=$(expr $a +$b)
    $ echo c
    
  3. 方法3:$((表达式)) 或 $[表达式]

    $ var1=$((1+5))
    $ var2=$[$var1*2]
    // 使用 $ 和 [] 将数学表达式围起来
    

    注意:bash shell数学运算符支持整数运算。z shell(zsh)提供了完整的浮点数算术操作。

  4. 浮点运算解决方案

    使用内建的bash计算器:bc
    $ bc
    3.44 / 5
    0
    scale = 4 // 浮点运算由scale控制,默认值为0
    

    注意:-q 选项可以不显示冗长的欢迎信息

    $ bc -q
    
    #!/bin/bash
    var1 = $(echo "scale=4;3.44 / 5" | bc)
    echo The answer is $var1
    
    #!/bin/bash
    var1= 10.46
    var2= 43.67
    var3= 33.2
    var4= 71
    var5= $(bc << EOF
    scale= 4
    a1= ($var1*$var2)
    a2= ($var3*var4)
    a1+b1
    EOF
        )
    echo "The final answer for this mess is $var5"
    
  5. 运算符
    • 算术运算符

      Table 7: 算术运算符
      运算符 说明 举例
      + 加法 `expr $a + $b` 结果为 30。
      - 减法 `expr $a - $b` 结果为 -10。
      乘法 `expr $a \* $b` 结果为 200。
      / 除法 `expr $b / $a` 结果为 2。
      % 取余 `expr $b % $a` 结果为 0。
      = 赋值 a=$b 将把变量 b 的值赋给 a。
      == 相等。用于比较两个数字,相同则返回 true。 [ $a == $b ] 返回 false。
      != 不相等。用于比较两个数字,不相同则返回 true。 [ $a != $b ] 返回 true。

      注意:条件表达式要放在方括号之间,并且要有空格,例如: [$a==$b] 是错误的,必须写成 [ $a == $b ]。

    • 关系运算符

      Table 8: 关系运算符
      运算符 说明 举例
      -eq 检测两个数是否相等,相等返回 true。 [ $a -eq $b ] 返回 false。
      -ne 检测两个数是否不相等,不相等返回 true。 [ $a -ne $b ] 返回 true。
      -gt 检测左边的数是否大于右边的,如果是,则返回 true。 [ $a -gt $b ] 返回 false。
      -lt 检测左边的数是否小于右边的,如果是,则返回 true。 [ $a -lt $b ] 返回 true。
      -ge 检测左边的数是否大于等于右边的,如果是,则返回 true。 [ $a -ge $b ] 返回 false。
      -le 检测左边的数是否小于等于右边的,如果是,则返回 true。 [ $a -le $b ] 返回 true。
    • 布尔运算符

      Table 9: 布尔运算符
      运算符 说明 举例
      ¡ 非运算,表达式为 true 则返回 false,否则返回 true。 [ ! false ] 返回 true。
      -o 或运算,有一个表达式为 true 则返回 true。 [ $a -lt 20 -o $b -gt 100 ] 返回 true。
      -a 与运算,两个表达式都为 true 才返回 true。 [ $a -lt 20 -a $b -gt 100 ] 返回 false。
    • 逻辑运算符

      Table 10: 逻辑运算符
      运算符 说明 举例
      && 逻辑的 AND [ $a -lt 100 && $b -gt 100 ] 返回 false
      || 逻辑的 OR [ $a -lt 100 || $b -gt 100 ] 返回 true

      注意:“|”,可通过 M-x org-entities-help <RET> 查看,Other > Misc

    • 字符串运算符

      Table 11: 字符串运算符
      运算符 说明 举例
      = 检测两个字符串是否相等,相等返回 true。 [ $a = $b ] 返回 false。
      != 检测两个字符串是否相等,不相等返回 true。 [ $a != $b ] 返回 true。
      -z 检测字符串长度是否为0,为0返回 true。 [ -z $a ] 返回 false。
      -n 检测字符串长度是否为0,不为0返回 true。 [ -n "$a" ] 返回 true。
      $ 检测字符串是否为空,不为空返回 true。 [ $a ] 返回 true。
    • 文件测试运算符

      Table 12: 文件测试运算符
      操作符 说明 举例
      -b file 检测文件是否是块设备文件,如果是,则返回 true。 [ -b $file ] 返回 false。
      -c file 检测文件是否是字符设备文件,如果是,则返回 true。 [ -c $file ] 返回 false。
      -d file 检测文件是否是目录,如果是,则返回 true。 [ -d $file ] 返回 false。
      -f file 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 [ -f $file ] 返回 true。
      -g file 检测文件是否设置了 SGID 位,如果是,则返回 true。 [ -g $file ] 返回 false。
      -k file 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。 [ -k $file ] 返回 false。
      -p file 检测文件是否是有名管道,如果是,则返回 true。 [ -p $file ] 返回 false。
      -u file 检测文件是否设置了 SUID 位,如果是,则返回 true。 [ -u $file ] 返回 false。
      -r file 检测文件是否可读,如果是,则返回 true。 [ -r $file ] 返回 true。
      -w file 检测文件是否可写,如果是,则返回 true。 [ -w $file ] 返回 true。
      -x file 检测文件是否可执行,如果是,则返回 true。 [ -x $file ] 返回 true。
      -s file 检测文件是否为空(文件大小是否大于0),不为空返回 true。 [ -s $file ] 返回 true。
      -e file 检测文件(包括目录)是否存在,如果是,则返回 true。 [ -e $file ] 返回 true。
      -S 判断某文件是否 socket。  
      -L 检测文件是否存在并且是一个符号链接。  

2.11. 变量测试

变量测试主要在 Shell 中使用,其它绝大多数语言是没有这个概念的,通用度不高。而且变量测试比较复杂,在实际写脚本的过程中完全可以用其它方式来取代变量测试。

Table 13: 变量测试表
变量置换方式 y 没有设置 y 为空 y 设置值
x=${y-变量} x=newValue x为空 x=$y
x=${y:-变量} x=newValue x=newValue x=$y
x=${y+变量} x为空 x=newValue x=newValue
x=${y:-变量} x为空 x为空 x=newValue
x=${y=变量} x=newValue x为空 x=$y
  y=newValue y值不变 y值不变
x=${y:=变量} x=newValue x=newValue x=$y
  y=newValue y=newValue y值不变
x=${y?变量} newValue 输出到标准错误输出 x为空 x=$y
x=${y:?变量} newValue 输出到标准错误输出 newValue 输出到标准错误输出 x=$y
x=${y-4}
// 表示如果y不存在,那么x=4;如果y为空值,那么x为空值;如果y有值,那么x被赋y的值。

2.12. 退出

  1. 退出状态码

    Table 14: 退出状态码
    状态码 描述
    0 命令成功结束
    1 一般性未知错误
    2 不适合的shell命令
    126 命令不可执行
    127 没找到命令
    128 无效的退出参数
    128+x 与Linux信号x相关的严重错误
    130 通过Ctrl+C终止的命令
    255 正常范围之外的退出状态码
    $ echo $?
    0
    
  2. exit

    echo 'Hello, World'
    exit 5 
    
    $ ./test 
    Hello, World
    $ echo $?
    5
    

    #+end_src

2.13. 管道(Pipe)命令

选取命令: cut,grep
排序命令: sort,wc,uniq
双向重定向:tee
划分命令: tr,col,join,paste,expand
参数代换: split,xargs

2.14. 正则表达式(Regular Expression)

Table 15: 正则表达式的基本组成部分
正则表达式 描述 示例
\ 转义符,将特殊字符进行转义,忽略其特殊意义 a\.b匹配a.b,但不能匹配ajb,.被转义为特殊意义
^ 匹配行首,awk中,^则是匹配字符串的开始 ^tux匹配以tux开头的行
$ 匹配行尾,awk中,$则是匹配字符串的结尾 tux$匹配以tux结尾的行
. 匹配除换行符\n之外的任意单个字符,awk则中可以 ab.匹配abc或bad,不可匹配abcd或abde,只能匹配单字符
[] 匹配包含在[字符]之中的任意一个字符 coo[kl]可以匹配cook或cool
[^] 匹配[^字符]之外的任意一个字符 123[^45]不可以匹配1234或1235,1236、1237都可以
[-] 匹配[]中指定范围内的任意一个字符,要写成递增 [0-9]可以匹配1、2或3等其中任意一个数字
? 匹配之前的项1次或者0次 colou?r可以匹配color或者colour,不能匹配colouur不支持
+ 匹配之前的项1次或者多次 sa-6+匹配sa-6、sa-666,不能匹配sa-不支持
匹配之前的项0次或者多次 co*l匹配cl、col、cool、coool等
() 匹配表达式,创建一个用于匹配的子串 ma(tri)?匹配max或maxtrix不支持()()()
{n} 匹配之前的项n次,n是可以为0的正整数 [0-9]{3}匹配任意一个三位数,可以扩展为[0-9][0-9][0-9]不支持
{n,} 之前的项至少需要匹配n次 [0-9]{2,}匹配任意一个两位数或更多位数不支持
{n,m} 指定之前的项至少匹配n次,最多匹配m次,n<=m [0-9]{2,5}匹配从两位数到五位数之间的任意一个数字不支持
| 交替匹配 | 两边的任意一项ab(c d)匹配abc或abd不支持

参考:https://man.linuxde.net/docs/shell_regex.html

2.15. 流程控制

2.15.1. if-then

#!/bin/bash
testuser = zrg
#
if grep $testuser /etc/passwd
then
    echo "The bash files for user $testuser are:"
    ls -a /home/$testuser/.b*
    echo
elif ls -d /home/$testuser
then
    echo "The user $testuser has a directory"
else
    echo "The user $testuser does not exist on this system."
    echo
fi

#test命令提供了在if-then语句中测试不同条件的途径。
#test命令可以判断三类条件:数值比较;字符串比较;文件比较

if-then 的高级特性

#!/bin/bash
# (( expression )) expression 可以是任意的数学赋值或比较表达式。
var1=10
if(( $var1 ** 2 > 90))
then
    (( $var2 = $var1 ** 2))
    echo "The square of $var1 is $var2."
fi
#!/bin/bash
# [[ expression ]]
if[[ $USER == r* ]]
then
    echo "Hello $USER"
else
    echo "Sorry, I do not know you."
fi

复合条件

格式:

[condition1] && [condition2] [condition1] || [condition2]

#!/bin/bash
# testing compound comparisons
#
if [-d $HOME] && [-w $HOME/testing]
then
    echo "The file exists and you can write to it."
else
    echo "I cannot write to the file."
fi

2.15.2. test

格式:

if test condition then commands fi

如果 test 命令中列出的条件成立,退出并返回退出状态码0;如果条件不成立,退出并返回非零的退出状态码。
#!/bin/bash
$var = 10
if [$var -eq 5]
then 
    echo "The value $var are equal."
else
    echo "The value $var are different."
fi
$var1 = baduser
if [$USER != $var1]
then
    echo "This is not $var1"
else
    echo "Welcome $var"
fi
$var2 = baseall
$var3 = hockey
if [$var2 \> $var3] #>符号需要转义,防止解释成输出重定向
then
    echo "$var2 is greater than $var3"
else
    echo "$var2 is less than $var3"
fi
特别说明:
1.test命令和测试表达式使用标准的数学比较符号来表示字符串比较,而用文本代码来表示数值比较。
2.比较测试时,大写字母被认为是小于小写字母,但sort命令恰好相反。
#!/bin/bash
var1 = testing
var2 =''
if [-n $var1]
then
    echo "The string '$var1' is not empty."
else
    echo "The string '$var1' is empty."
fi
if [-z $var2]
then
    echo "The string '$var2' is empty."
else
    echo "The string '$var2' is not empty."
fi
#!/bin/bash
jump_directory=/home/arthur
if [-d $jump_directory]
then
    echo "The $jump_directory directory exists."
else
    echo "The $jump_directory directory does not exists."
fi
比较 描述
-d file 检查file是否存在并是一个目录
-e file 检查file是否存在
-f file 检查file是否存在并是一个文件
-r file 检查file是否存在并可读
-s file 检查file是否存在并非空
-w file 检查file是否存在并可写
-x file 检查file是否存在并可执行
-O file 检查file是否存在并属当前用户所有
-G file 检查file是否存在并且默认组与当前用户相同
file1 -nt file2 检查file是否比file2
file1 -ot file2 检查file是否比file2旧

2.15.3. case

格式:

case $变量名 in 模式1) 命令序列1;; 模式2) 命令序列2;; *) 默认执行的命令序列;; esac

#!/bin/bash
case $action in
    start | begin)
        echo "start something"
        echo "begin something";;
    stop | end)
        echo "stop something"
        echo "end something";;
    *)
        echo "Ignorant.";;
esac

2.15.4. for

格式:

for var in list do commands done

#!/bin/bash
#
# basic for command
for country in China America India Japen
do
    echo "The next state is $country"
done

# another example of how not to use the for command
# 1.使用转义字符(反斜线)
# 2.使用双引号
for test in I don\'t know if "this'll" work
do
    echo "word:$test"
done

# using a variable to hold the list
list="China America India Japen"
list=$list" Connecticut"
for country in $list
do
    echo "Welcome to $country"
done

# reading values from a file
file="states"
# 修改IFS环境变量的值,使其只能识别换行符
IFS=$'\n'
for state in $(cat $file)
do
    echo "Visit beautiful $state"
done

# iterate through all the files in a directory
for file in $HOME/* /etc/nginx/*
do
    if [-d "$file"]
    then
        echo "$file is a directory."
    elif [-f "$file"]
    then
        echo "$file is a file."
    fi
done

# C-style for loop
#
for (( i=1; i <= 10; i++))
do
    echo "The next number is $i"
done
# multiple variable
for (( a=1; b=10;a <= 10; a++, b++))
do
    echo "$a - $b"
done

处理循环的输出

可以对循环的输出使用管道或进行重定向,通过在 done 命令之后添加一个处理命令来实现:
for file in /home/zrg/*
do
    ...
done > output.txt

2.15.5. while

格式:

while test command do other commands done

# while command test
var1=10
while [ $var1 -gt 0 ]
do
    echo $var1
    var1=$[ $var1 - 1 ]
done

2.15.6. until

until 命令和 while 命令完全相反。
格式:

until test command do other commands done

1: #!/bin/bash
2: # using the until command
3: var1=100
4: until [ $var1 -eq 0 ]
5: do
6:     echo $var1
7:     var1=$[ $var1 -25 ]
8: done
  • 循环处理文件数据-处理

     1: #!/bin/bash
     2: # changing the IFS value
     3: IFS.OLD=$IFS
     4: IFS=$'\n'
     5: for entry in $(cat /etc/passwd)
     6: do
     7:     echo "Values in $entry -"
     8:     IFS=:
     9:     for value in $entry
    10:     do
    11:         echo "$value"
    12:     done
    13: done
    14: # 该脚本使用了两个不同的 IFS 的值来解析数据,第一个 IFS 值解析出 /etc/passwd 文件中的单独的行,内部 for 循环接着将 IFS 的值修改为冒号,允许你从 /etc/passwd 的行中解析出单独的值。
    

2.15.7. break

 1: #!/bin/bash
 2: # --------------------------------
 3: # 跳出单个循环
 4: # 1.breaking out of a for loop
 5: for var1 in 1 2 3 4 5
 6: do
 7:     if [ $var1 -eq 5]
 8:     then
 9:         break
10:     fi
11:     echo "Iteration number: $var1"
12: done
13: echo "The for loop is completed"
14: # 2.breaking out of a while loop
15: var1=1
16: while [ $var1 -lt 10 ]
17: do
18:     if [ $var1 -eq 5]
19:     then
20:         break
21:     fi
22:     echo "Iteration number: $var1"
23: done
24: echo "The while loop is completed"
25: # --------------------------------
26: # 跳出内部循环
27: # 3.breaking out of an inner loop
28: for(( a = 1; a<4; a++))
29: do
30:     echo "Outer loop: $a"
31:     for((b = 1; b<100; b++))
32:     do
33:         if [ $var1 -eq 5]
34:         then
35:             break
36:         fi
37:         echo "Inner loop: $b"
38:     done
39: done
40: # ---------------------------------
41: # 跳出外部循环
42: # 4.breaking out of an outer loop
43: for(( a = 1; a<4; a++))
44: do
45:     echo "Outer loop: $a"
46:     for((b = 1; b<100; b++))
47:     do
48:         if [ $var1 -eq 5]
49:         then
50:             break 2
51:         fi
52:         echo "Inner loop: $b"
53:     done
54: done

2.15.8. continue

 1: # 1.using the continue command
 2: for((var1 = 1; var1<15; var1++))
 3: do
 4:     if [$var1 -gt 5] && [$var1 -lt 10]
 5:     then
 6:         continue
 7:     fi
 8:     echo "Iteration number: $var1"
 9: done
10: # 2.improperly using the continue command in a while loop
11: var1=1
12: while echo "while iteration: $var1"
13:       [ $var1 -lt 15 ]
14: do
15:     if [ $var1 -gt 5] && [$var1 -lt 10]
16:     then
17:         continue
18:     fi
19:     echo "Inside iteration number: $var1"
20:     var1 = $[$var1 +1]
21: done
22: # 3.continuing an outer loop
23: for(( a = 1; a<5; a++))
24: do
25:     echo "Interation : $a"
26:     for((b = 1; b<3; b++))
27:     do
28:         if [ $b -gt 2] && [$a -lt 4]
29:         then
30:             continue 2
31:         fi
32:         var3=$[$a+$b]
33:         echo "The result of $a * $b is $var3"
34:     done
35: done

2.16. 处理用户输入和数据呈现

2.16.1. 命令行参数

2.16.2. 数据呈现

2.17. 控制脚本

3. Shell 高级

3.1. 函数

3.2. 图形化桌面的脚本编程

3.2.1. 创建文本菜单

3.2.2. 制作窗口

3.3. 其它 Shell

4. 实用的脚本收集

4.1. 查找可执行文件

#!/bin/bash
# finding files in the PATH
IF=:
for folder in $PATH
do
echo "$folder:"
for file in $folder/*
do
if [-x $file]
then
echo "$file"
fi
done
done
#!/bin/bash
# process new user accounts
input = "users.csv"
while IFS=',' read -r userid name
do
echo "adding $userid"
useradd -c "$name" -m $userid
done < "$input"

4.2. 编写简单的脚本实用工具

4.2.1. 归档

4.2.2. 管理用户账户

4.2.3. 检测磁盘空间

4.3. 创建与数据库、Web及E-Mail相关的脚本

4.4. 发送消息

4.5. 获取格言

4.6. 编造借口

4.7. 在当前目录及指定子目录深度下创建.gitignore文件

#!/bin/sh
for dir in `find ./ -mindepth 2 -maxdepth 4 -type d`
do
    echo $dir
    `touch $dir/.gitignore`
    echo "*">$dir/.gitignore
done

4.8. 解决 dpkg: warning: files list file for package 'x' missing

for package in $(sudo apt install catdoc 2&1 |grep "warning: files list file for package'" |grep -Po "[^'\n ]+'" |grep -Po "[^']+");
do
    sudo apt install --reinstall "$package"
done

4.9. 删除大文件的前n行

tail -n +10 old_file>new_file
mv new_file old_file

4.10. 打包文件并上传阿里云及下载

#!/bin/bash
version="$1"
serviceName="$2"

if [ ! -n "${version}" ] || [ ! -n "${serviceName}" ]; then
    echo "参数错误:接收2个参数,参数1:文件名称,如1.1.5,参数2:服务名称,如cpms"
    exit
fi

filename=''
if [ "${serviceName}" == 'cpms' ]; then
    filename='pms'
fi

# Check pom.xml and target directory exist.
pomFilePath="./pom.xml"
targetPath="./target"
if [ ! -f "${pomFilePath}" ] || [ ! -d "${targetPath}" ]; then
    echo "请将执行文件移动或复制到到服务代码根目录下运行!!!"
    exit 
fi

# Check pom.xml file "<version>${filename}</version>"
pomFileVersion=$(cat ${pomFilePath} | grep version | grep ${version})
if [ -z ${pomFileVersion} ]; then
    echo "pom.xml 版本与所传入版本(version)不一致"
    exit
fi

filename="${filename}-${version}.jar"
sourcePath="./target/${filename}"
dest="out-road-park-version/${serviceName}/${filename}"

# Check file exist.
echo "${sourcePath}"
if [ ! -f ${sourcePath} ]; then
    #
    ## 1. Build and package .jar file
    mvn clean install -DskipTests
fi

## 2. Upload to Aliyun OSS.
Host="oss-cn-hangzhou.aliyuncs.com"
Bucket="park-version"
Id="Access ID"
Key="Secret Key"

ossHost=$Bucket.$Host

resource="/${Bucket}/${dest}"
contentType=$(file -ib ${sourcePath} | awk -F ";" '{print $1}')
dateValue=$(TZ=GMT env LANG=en_US.UTF-8 date +'%a, %d %b %Y %H:%M:%S GMT')
stringToSign="PUT\n\n${contentType}\n${dateValue}\n${resource}"
signature=$(echo -en $stringToSign | openssl sha1 -hmac ${Key} -binary | base64)

url=http://${ossHost}/${dest}
echo "upload ${sourcePath} to ${url}"

curl -i -q -X PUT -T "${sourcePath}" \
     -H "Host: ${ossHost}" \
     -H "Date: ${dateValue}" \
     -H "Content-Type: ${contentType}" \
     -H "Authorization: OSS ${Id}:${signature}" \
     ${url}


#!/bin/bash

host="oss-cn-shanghai.aliyuncs.com"
bucket="bucket名"
Id="AccessKey ID"
Key="Access Key Secret"

osshost=$bucket.$host

source="objecetename"
dest="localfilename"

resource="/${bucket}/${source}"
contentType=""
dateValue="`TZ=GMT env LANG=en_US.UTF-8 date +'%a, %d %b %Y %H:%M:%S GMT'`"
stringToSign="GET\n\n${contentType}\n${dateValue}\n${resource}"
signature=`echo -en $stringToSign | openssl sha1 -hmac ${Key} -binary | base64`

url=http://${osshost}/${source}
echo "download ${url} to ${dest}"

curl --create-dirs \
    -H "Host: ${osshost}" \
    -H "Date: ${dateValue}" \
    -H "Content-Type: ${contentType}" \
    -H "Authorization: OSS ${Id}:${signature}" \
    ${url} -o ${dest}

4.11. 读取Excel文件

原理:将excel转换为csv格式,再读取
  1. 安装 "libreoffice-common" 和 "unoconv"

    sudo apt-get update
    sudo apt-get install libreoffice-common unoconv
    
  2. 将 Excel 文件转换为 CSV 格式:

    unoconv -f csv input.xlsx
    
  3. 读取 CSV 文件

    #!/bin/bash
    
    #unoconv -f csv ~/Downloads/cpms-version.xlsx
    
    while IFS=',' read -r col1 col2 col3
    do
        # 在这里处理读取的数据
        echo "Column 1: $col1"
        echo "Column 2: $col2"
        echo "Column 3: $col3"
    done < cpms-version.csv
    

4.12. 根据参数执行指定 PHP 脚本(消息队列rabbitmq)

  1. 消息队列:启用消费者

    #!/bin/bash
    route_category="$1" # 路由组名称
    number="$2"         # 消费者数量
    expect_number="$2"         # 预计启用消费者数量
    
    routes=(user store system)
    
    # Check routes exists
    if [ ! -n "$route_category" ] || [ ! -n "$number" ]; then
        echo "错误:接收两个参数,参数1:路由组名称,参数2:运行消费者数量"
        exit
    fi
    
    # Check correctness of route category
    if [[ ! "${routes[@]}" =~ "$route_category" ]]; then
        echo "错误:非法的路由组名称"
        exit
    fi
    
    # If number less than 1, then let $number equal 1
    if [ $number -lt 1 ]; then
        echo "警告:第二个参数值不能小于1"
        number=1
    fi
    
    # Start execute cumstomer command
    echo "即将启用消费者队列......"
    actual_number=0 # 实际启用消费者数量
    while (($number > 0)); do
        php /www/test/think rabbit_receive "$route_category" &
        let actual_number=actual_number+1
        echo "第 $actual_number 个 $route_category 消费者队列已启用"
        let number=number-1
    done
    
    # Get queue
    total_number=`ps -ef |grep rabbit_receive\ $route_category | wc -l`
    let total_number=total_number-1
    
    echo "----------------------------------"
    echo "完成 $route_category 消费者队列启用"
    echo "预计启用数量:$expect_number"
    echo "实际启用数量:$actual_number"
    echo "当前 $route_category 消息队列总计启用数量:$total_number"
    echo "----------------------------------"
    
    
  2. 消息队列:Kill 启用消费者

    #!/bin/bash
    route_category="$1" # 路由组名称
    kill_number="$2"    # 要杀死的消费者队列数量(可选),不传表示杀死全部
    
    routes=(user store system)
    
    # Check routes exists
    if [ ! -n "$route_category" ]; then
        echo "错误:接收两个参数,参数1:路由组名称,参数2(可选):要杀死的消费者队列数量"
        exit
    fi
    
    # Check correctness of route category
    if [[ ! "${routes[@]}" =~ "$route_category" ]]; then
        echo "错误:非法的路由组名称"
        exit
    fi
    
    # ps -efw 查看所有进程的命令
    # grep -w rabbit_receive\ $route_category 强制 PATTERN 仅完全匹配字词
    # grep -v grep 在列出的进程中去除含有关键字“grep”的进程
    # cut -c 9-15 截取输入行的第9个字符到第15个字符,而这正好是进程号PID
    # head -n $kill_number 指定列出要kill的PID
    # xargs kill -9 xargs命令是用来把前面命令的输出结果(PID)作为“kill -9”命令的参数,并执行该命令
    echo "----------------------------------"
    if [ -n "$kill_number" ] && [ $kill_number -gt 0 ]; then
        ps -efw | grep -w rabbit_receive\ $route_category | grep -v grep | cut -c 9-15 | head -n $kill_number | xargs kill -9
        echo "已 Kill $kill_number 个消费者队列"
        last_number=$(ps -efw | grep -w rabbit_receive\ $route_category | grep -v grep | cut -c 9-15 | wc -l)
        echo "剩余 $last_number 个 $route_category 消费者队列"
    else
        ps -efw | grep -w rabbit_receive\ $route_category | grep -v grep | cut -c 9-15 | xargs kill -9
        all_kill_number=$(ps -efw | grep -w rabbit_receive\ $route_category | grep -v grep | cut -c 9-15 | wc -l)
        echo "已Kill $all_kill_number 个 $route_category 消费者队列,所有 $route_category 消费队列全部Kill完成"
    fi
    echo "----------------------------------"
    
    

5. 参考资料