Shell 编程

什么是 Shell

  • Shell 是命令解释器,用于解释用户对操作系统的操作

  • shell 有很多, 查看机器支持的 shell

    cat /etc/shells
    

Linux 的启动过程

/BIOS-MBR-BootLoader(grub)-kernel-init-系统初始化-shell

Shell 书写规范

#!/bin/bash
# Sha-Bang
# 第一行的#!/bin/bash,告诉看脚本的人,这个文件需要用哪个shell编译器运行
# 当以 ./xx.sh 的方式运行脚本是,会读取第一行,并以对应的编译器运行

# demo
cd /var
ls
pwd
# 列出当前目录下所有文件大小
du -sh *
# 查看当前文件夹的大小
du -sh
# 查看整文件系统空间使用情况
df -h

执行方式

  • bash ./filename.sh : 产生一个bash的子进程运行

  • ./filename.sh : 使用Sha-Bang 对应的解析器解析,并生成一个子进程运行
  • source [./]filename.sh : 在当前进程运行
  • . filename.sh : source 的简化写法, 在当前进程运行

内建命令和外部命令的区别

  • 内建命令不需要创建子进程, 比如cd, 会切换当前进程目录
  • 内建命令对当前 Shell 生效

管道与重定向

  • 管道与管道符
    • 管道和信号一样,也是进程通信的方式之一
    • 匿名管道(管道符)是 Shell 编程经常用到的通信工具
    • 管道符是“|”,将前一个命令执行的结果传递给后面的命令, 管道符的两边是两个子进程
      • ps | cat
      • echo 123 | ps
  • 子进程与子shell
    • 子进程是 Shell 程序,称作子 Shell ?
    • 内部命令的结果不会传递给子 Shell ?
  • 重定向符号
    • 一个进程默认会打开标准输入、标准输出、错误输出三个文件描述符
    • 输入重定向符号 “ <”

      • read var < /path/to/a/file : 把文件的内容输入到前面的进程
      • wc -l < /etc/passwd : 统计文件行数
    • 输出重定向符号 “>” “»” “2>” “&>”
      • ”>” : echo 123 > test , 输出并覆盖文件内容

      • ”»”: echo 123 » test , 输出内容到文件末尾

      • “2>”: mkdi 2> test , 输出错误结果到文件

      • “&>”: mkdi &> test , 无论输出有没有出错,都输出到文件

      • 组合应用: cat > ~/Desktop/test.sh «EOF 开始文件编辑, 遇到EOF文件结束. 用于在shell内部再生成shell文件

        #!/bin/bash
        cat > ~/Desktop/test.sh <<EOF
        echo "hello bash"
        EOF
        

变量

  • 变量的定义

  • 变量的赋值

  • 变量的查看

    • $var , ${var}
  • 数组

    # 定义数组
    IPTS=(10.0.0.1 10.0.0.2 10.0.0.3 )
    # 显示数组的所有元素
    echo ${IPTS[@]}
    # 显示数组元素个数
    echo ${#IPTS[@]}
    # 显示数组的第一个元素
    echo ${IPTS[O]} 
    
  • 变量的作用范围

    • 默认作用范围,当前进程
    • 使用 source subShell.sh 运行的脚本因为是在当前进程运行的,所以可以获得上去上下文的变量
    • export var1=xxx 可以导出变量, 子进程也可以使用var1
    • unset var1 释放变量,将它置为空
  • 系统环境变量

    • env: 获取系统环境变量

    • set: 获取当前环境定义的变量

    • $?: 上一条指令运行正确返回 0, 错误是1

      # 目录 Desktop 存在
      ➜  ~ test -d Desktop 
      ➜  ~ echo $?
      0
      # 目录 Deskto 不存在
      ➜  ~ test -d Deskto 
      ➜  ~ echo $?       
      1
      
      • $$ : 当前进程的pid

         echo $$
         1033
        
      • $0 : 当前进程名, 也是当前运行脚本的文件名

        echo $0
         -zsh
        

        $# : 环境变量的个数

        ${2-_}: 处理空参数的技巧

        ➜  bash_test cat 8.sh 
        #!/bin/bash
              
        # demo
              
        pos1=$1
        # 没有读到参数的时候用‘_’占位
        pos2=${2-_}
              
        echo $pos1
        echo $pos2
        ➜  bash_test sh 8.sh -a -l
        -a
        -l
        ➜  bash_test sh 8.sh -a   
        -a
        _
        
    • 环境变量配置文件

      • /etc/profile , ~/.bash_profile : su - user 的时候才会调用

      • /etc/bashrc , ~/.bashrc : su user 的时候调用

        $ su root
        ~/.bashrc
        /etc/bashrc
              
        $ su - root
        /etc/profile
        ~/.bash_profile
        ~/.bashrc
        /etc/bashrc
        

运算符

  • 赋值运算符: =
    • 使用unset取消为变量的赋值
    • = 除了作为赋值运算符还可以作为测试操作符
  • 算数运算符: + - * / % **
    • 使用expr计算: expr 4 * 5
    • 使用(())计算: (( 4 ** 5 )) ,((4**5))
  • 数字常量:
    • let 常量名=变量值 -> 不使用let定义变量, 用0开头就只是字符串
    • 变量值使用0开头为八进制
    • 变量值使用0x开头为十六进制
  • 双圆括号
    • 双圆括号是let命令的简化 : let a=0x78 等价于 ((a=0x78))
    • (( a= 10 ))
    • (( a++ ))
    • echo $((2**10))

特殊符号大全

  • 引号:

    • 单引号 ‘ : 完全引用, “$var” $不能获取变量值
    • 双引号“: 不完全引用, “$var” $可以获取变量值
    • 反引号 ` : 执行命令
  • 括号

    • ()(())$() 圆括号

      • (): 单独使用圆括号会产生一个子shell (xyz=123) : 子Shell和子线程不是一回事
      • (): 数组初始化 IPS=( ip1 ip2 ip3 )
      • (( )) : 计算括号内的公式, 等价于 let
      • $() : 相当于 ``, 括号内写命令
    • [] [[]]方括号

      • 单独使用方括号是测试(test)或数组元素功能

        var=(1 2 3)
        # 取数组元素
        echo ${var[0]}
        1
        echo ${var[1]}
        2
        echo ${var[2]}
        3
              
        # 测试判断, [] 内需要使用 -le, -ge ... 等比较符号
        [root@3fc93cd751e9 learn_bash]# [ 1 -le 2 ]
        [root@3fc93cd751e9 learn_bash]# echo $?
        0
        [root@3fc93cd751e9 learn_bash]# [ 1 < 2 ]
        bash: 2: No such file or directory
        [root@3fc93cd751e9 learn_bash]# [ 3 -le 2 ]
        [root@3fc93cd751e9 learn_bash]# echo $?
        1
        
      • 两个方括号表示测试表达式

        [root@3fc93cd751e9 learn_bash]# [[ 3 > 1 ]]; echo $?
        0
        [root@3fc93cd751e9 learn_bash]# [[ 3 < 1 ]]; echo $?
        1
        
      • < > 尖括号: 重定向符号

      • { } 花括号

        • 输出范围echo {0..9}

        • 文件复制cp /etc/passwd{,.bak} -> {a,b} 表示一个序列 a 和 b.

          所以前面的句子相当于 cp /etc/passwd /etc/passwd.bak

        [root@3fc93cd751e9 learn_bash]# echo {0..9}
        0 1 2 3 4 5 6 7 8 9
        [root@3fc93cd751e9 learn_bash]# touch name
        [root@3fc93cd751e9 learn_bash]# cp name{,.bak}
        [root@3fc93cd751e9 learn_bash]# ll
        total 4
        -rw-r--r-- 1 root root   0 Jul  8 14:09 name
        -rw-r--r-- 1 root root   0 Jul  8 14:10 name.bak
        [root@3fc93cd751e9 learn_bash]# cp name{.bak,.book}
        [root@3fc93cd751e9 learn_bash]# ll
        total 4
        -rw-r--r-- 1 root root   0 Jul  8 14:09 name
        -rw-r--r-- 1 root root   0 Jul  8 14:10 name.bak
        -rw-r--r-- 1 root root   0 Jul  8 14:10 name.book
        
  • 运算和逻辑符号

    • +-*/%算数运算符

    • ><=比较运算符

    • && || ! 逻辑运算符

      [root@3fc93cd751e9 learn_bash]# (( 5 > 4)); echo $?
      0
      [root@3fc93cd751e9 learn_bash]# (( 5 < 4)); echo $?
      1
      [root@3fc93cd751e9 learn_bash]# (( 5 > 4 && 6 > 5 )); echo $?
      0
      [root@3fc93cd751e9 learn_bash]# (( 5 > 4 && 6 < 5 )); echo $?
      1
      [root@3fc93cd751e9 learn_bash]# (( 5 > 4 || 6 < 5 )); echo $?
      0
      [root@3fc93cd751e9 learn_bash]# (( ! 5 > 4 )); echo $?
      1
      
  • 转义符号

  • 其他符号

    • #注释符

    • ;命令分隔符

      • case语句的分隔符要转义 ;;
    • :空指令

  • .和 source命令相同

    • ~ 家目录
  • ,分隔目录

    # 查看当前目录下所有文件及子目录的内容
    [root@3fc93cd751e9 ~]# ls *
    anaconda-ks.cfg  anaconda-post.log  docker  helloworld.sh  original-ks.cfg
      
    learn_bash:
    name  name.bak	name.book  subShell.sh
    

测试与判断

退出与退出状态

  • 退出程序命令
    • exit : 不带数字使用会返回上一条指令执行返回的错误码,0表示正常退出
    • exit 10 返回10给 Shell,返回值非 0 位不正常退出
    • $? 判断当前 Shell 下前一个进程是否正常退出

测试命令 test

  • test 命令利用程序是否正常退出返回 0 或 1

  • test 可以做以下测试:

    • 文件测试
    • 整数比较测试
    • 字符串测试
    # 查看test的用法
    man test
      
    SYNOPSIS
         test expression
         [ expression ]
    DESCRIPTION
    		 -d file       True if file exists and is a directory.
         -e file       True if file exists (regardless of type).
         -f file       True if file exists and is a regular file.
         string        True if string is not the null string.
         s1 = s2       True if the strings s1 and s2 are identical.
         s1 != s2      True if the strings s1 and s2 are not identical.
         s1 < s2       True if string s1 comes before s2 based on the binary value of their characters.
         s1 > s2       True if string s1 comes after s2 based on the binary value of their characters.
         n1 -eq n2     True if the integers n1 and n2 are algebraically equal.
         n1 -ne n2     True if the integers n1 and n2 are not algebraically equal.
         n1 -gt n2     True if the integer n1 is algebraically greater than the integer n2.
         n1 -ge n2     True if the integer n1 is algebraically greater than or equal to the integer n2.
         n1 -lt n2     True if the integer n1 is algebraically less than the integer n2.
         n1 -le n2     True if the integer n1 is algebraically less than or equal to the integer n2.
      
         These primaries can be combined with the following operators:
         ! expression  True if expression is false.
         expression1 -a expression2
                       True if both expression1 and expression2 are true.
         expression1 -o expression2
                       True if either expression1 or expression2 are true.
         ( expression )
         							 True if expression is true.
    EXIT STATUS
         The test utility exits with one of the following values:
         0       expression evaluated to true.
         1       expression evaluated to false or expression was missing.
         >1      An error occurred
    

    test案例

    [root@3fc93cd751e9 learn_bash]# ls
    exitDemo.sh  name  name.bak  name.book	subShell.sh
    [root@3fc93cd751e9 learn_bash]# test -e exitDemo.sh ; echo $?
    0
    [root@3fc93cd751e9 learn_bash]# test -e exitDe.sh ; echo $?
    1
    [root@3fc93cd751e9 learn_bash]# test -d ../learn_bash ; echo $?
    0
    [root@3fc93cd751e9 learn_bash]# test -f ../learn_bash ; echo $?
    1
    [root@3fc93cd751e9 learn_bash]# [ 3 -gt 2 ]; echo $?
    0
    [root@3fc93cd751e9 learn_bash]# [ 1 -gt 2 ]; echo $?
    1
    [root@3fc93cd751e9 learn_bash]# [ "name" ]; echo $?
    0
    [root@3fc93cd751e9 learn_bash]# [ "name" = 'name'  ]; echo $?
    0
    [root@3fc93cd751e9 learn_bash]# [ "name" = 'name1'  ]; echo $?
    1
    [root@3fc93cd751e9 learn_bash]# [ "name" < 'name1'  ]; echo $?
    bash: name1: No such file or directory
    1
    [root@3fc93cd751e9 learn_bash]# [ "name" -le 'name1'  ]; echo $?
    bash: [: name: integer expression expected
    2
    [root@3fc93cd751e9 learn_bash]# [[ "name" < 'name1'  ]]; echo $?
    0
    [root@3fc93cd751e9 learn_bash]# [[ "namea" < 'name1'  ]]; echo $?
    1
    [root@3fc93cd751e9 learn_bash]# [ 1 -lt 3 -a 4 -le 5  ]; echo $?
    0
    [root@3fc93cd751e9 learn_bash]# [ 1 -lt 3 -a 4 -ge 5  ]; echo $?
    1
    [root@3fc93cd751e9 learn_bash]# [ 1 -lt 3 -o 4 -ge 5  ]; echo $?
    0
    [root@3fc93cd751e9 learn_bash]# [ 1 -lt 3 ]; echo $?
    0
    [root@3fc93cd751e9 learn_bash]# [ ! 1 -lt 3 ]; echo $?
    1
    

使用 if-then语句

  • test 测试语句可以简化为 [ ] 符号

  • if-then 语句的基本用法

    if [ 测试条件成⽴ 或 命令返回0 ] ; then 
    执行相应命令 
    fi 结束
      
    # 或如下写法
    if [ 测试条件成⽴ 或 命令返回0 ]
    then 
    执行相应命令 
    fi 结束
    

使用 if-then-else 语句

  • if-then-else 语句可以在条件不成立时也运行相应的命令

    if [ 测试条件成立 ]
    then 执行相应命令
    else 测试条件不成⽴,执行相应命令 
    fi 结束
      
    # 或如下写法
    if [ 测试条件成立 ]; then 
    执行相应命令
    else 测试条件不成⽴,执行相应命令 
    fi 结束
      
    if [ 测试条件成立1 ]; then 
    执行相应命令
    elif [ 测试条件成立2 ]; then 
    执行相应命令
    else 测试条件不成⽴,执行相应命令 
    fi 结束
    

嵌套 if 的使用

  • if 条件测试中可以再嵌套 if 条件测试
  • 嵌套的结果和复合比较语句 && 结果相同

分支

  • case语句和select语句可以构成分支

    case"$变量” in
    “情况1”)
    	命令... ;;
    “情况2”)
    	命令... ;;
    	* )
    	命令... ;;
    esac
    

    案例

    #!/bin/bash
      
    # case demo
      
    name=$1
      
    case $name in
    	"april"|"APRIL")
    		# april or APRIL
    		echo "hello april" ;;
    	ang*)
    		# ang wildcard
    		echo "hello angie" ;;
    	*)
    		echo "hello nobody" ;;
    esac
    

循环

使用 for 循环遍历命令的执行结果

  • for 循环的语法

    for 参数 in 列表 
    do 执行的命令 
    done 封闭一个循环
    
  • 使用反引号或 $() 方式执行命令,命令的结果当作列表进行处理

    # 获取当前文件夹下面 x.mp3 文件列表,遍历它
    for filename in `ls *.mp3`
    do             
    # 把mp3改成mp4 
    # $(basename $filename .mp3) 取出文件去掉.mp3之后的名字
    mv $filename $(basename $filename .mp3).mp4
    done
    

C 语言风格的 for 命令

for((变量初始化;循环判断条件;变量变化)) 
do
	循环执行的命令
done

for (( i=1 ; i <= 10 ; i++ ))
do 
	echo $i;
done

while 循环

while test测试是否成⽴ 
do
	命令
done


➜  bash_test a=1                    
➜  bash_test while [ $a -lt 10 ]; do
echo $a ; ((a++))
done
1
2
3
4
5
6
7
8
9
➜  bash_test echo $a
10

until 循环

  • until 循环与 while 循环相反,循环测试为假时,执行循环,为真时循环停止

    until test测试是否成⽴ 
    do
    	命令
    done
      
    ➜  bash_test a=1   
    ➜  bash_test until [ $a -ge 10 ];do
    until> echo $a; ((a++))
    until> done
    1
    2
    3
    4
    5
    6
    7
    8
    9
    

循环的使用

* 循环和循环可以嵌套 
* 循环中可以嵌套判断,反过来也可以实现嵌套 
* 循环可以使用 break 和 continue 语句在循环中退出
# break
➜  bash_test for i in {1..9};do 
	if [ $i = 5 ];then 
		break;
	fi
	echo $i
done
1
2
3
4

# continue, skip 5
➜  bash_test for i in {1..9};do
if [ $i = 5 ];then
continue;
fi
echo $i
done
1
2
3
4
6
7
8
9

使用循环处理命令行参数

  • 命令行参数可以使用 $1 $2 … ${10}… $n 进行读取
  • $0 代表脚本名称

  • $* 和 $@ 代表所有位置参数
  • $# 代表位置参数的数量
  • 使用 ${1-_} 方式代替 $1 避免变量为空导致的遗产
#!/bin/bash

# help display help help

for pos in $@;do
if [ "$pos" = "help" ]; then
	echo $pos $pos
else
	echo $pos
fi
done


echo "1:"$1
# 参数为空的时候占位
echo "1_:"${1-_}

# shift 移动参数,每次删掉参数列表的第一个参数
# 配合while [ $# -ge 1 ] 可以便利参数列表
while [ $# -ge 1 ] 
do
	if [ "$1" = "help" ]; then
		echo $1 $1
	fi
	shift 
done

函数

自定义函数

  • 函数用于“包含”重复使用的命令集合

  • 自定义函数

    # function 可以省略
    function fname(){
    命令
    }
      
    ## 函数的执行
    fname
    
  • 函数作用范围的变量

    • local 函数名 : 定义局部变量,避免污染全局变量
  • 函数参数

    • $1 $2 $3 ... $n

自建函数库

  • 使用 source 函数脚本文件“导入”函数 cd

脚本控制

脚本优先级控制

  • 可以使用nice和renice调整脚本优先级

  • 避免出现“不可控的”死循环

    • 死循环导致cpu占用过高
    • 死循环导致死机
    ➜  ~ ulimit -a
    -t: cpu time (seconds)              unlimited
    -f: file size (blocks)              unlimited
    -d: data seg size (kbytes)          unlimited
    -s: stack size (kbytes)             8192
    -c: core file size (blocks)         0
    -v: address space (kbytes)          unlimited
    -l: locked-in-memory size (kbytes)  unlimited
    -u: processes                       2784
    -n: file descriptors                256
    

    fork炸弹: 函数递归调用,cpu满了

      
    func() { func | func& }; func
    # 或
    .() { . | .& }; .
    bash: fork: retry: Resource temporarily unavailable
    bash: fork: retry: No child processes
    bash: fork: retry: No child processes
    bash: fork: retry: Resource temporarily unavailable
    bash: fork: retry: No child processes
    bash: fork: retry: No child processes
    bash: fork: retry: Resource temporarily unavailable
    ...
    

捕获信号

  • 捕获信号脚本的编写

    • kill默认会发送15号信号给应用程序
    • ctrl+c发送2号信号给应用程序
    • 9号信号不可阻塞
    vim 16.sh
      
    #!/bin/bash
    # signal demo
    # 捕获 15, 不会被kill掉
    trap "echo sig 15" 15
    # 捕获 2, 不会被ctrl+c结束进程
    trap "echo sig 2" 2
      
    ## 打印当前进程
    echo $$
      
    while :
    do 
    	:
    done
      
    [root@3fc93cd751e9 learn_bash]# bash 16.sh 
    866
    ^Csig 2
    ^Csig 2
    sig 15
    Killed
      
    ## 只能被kill -9 866 杀掉
    

计划任务

一次性计划任务 at

[root@3fc93cd751e9 learn_bash]# at 09:03
warning: commands will be executed using /bin/sh
at> echo hello > /tmp/hello.txt
at> <EOT>     
job 2 at Sat Jul 10 09:03:00 2021
# 使用ctrl+D结束输入,并提交
[root@3fc93cd751e9 learn_bash]# atq
2	Sat Jul 10 09:03:00 2021 a root

周期性计划任务

  • cron

    • 配置方式: crontab -e
    • 查看现有的计划任务: crontab -l
    • 配置格式:
      • 分钟 小时 日期 月份 星期执行的命令
      • 注意命令的路径问题
    ➜  /tmp crontab -l
    crontab: no crontab for april
    ➜  /tmp crontab -e
    crontab: no crontab for april - using an empty one
    crontab: installing new crontab
    ➜  /tmp crontab -l
    * * * * * date >> /tmp/date.txt 
    ➜  /tmp tail -f date.txt 
    Sat Jul 10 17:19:00 CST 2021
    Sat Jul 10 17:20:00 CST 2021
    

计划任务加锁

  • 如果计算机不能按照预期时间运行
    • anacontab 延时计划任务
    • flock 锁文件
# 排他锁, 使这种方式运行, 如果当前运行的a.sh没有结束
# 就不能在次运行
flock -xn "/tmp/f.lock" -c "/root/a.sh"

Vim 文本编辑器的使用

四种模型:

  • 正常模式(Normal-mode)
    • HJKL, 光标移动, 左下右上, 记忆方式:前两个左下, 后两个上右
    • copy-paste
      • yy,p: yy复制当前行, p ->paste
      • nyy, p: 复制n行
      • y$,p: 复制当前行,插入到某行中间, 上面的复制方式都只能不能插入到行中间
    • cut-paste
      • dd,p: dd剪切当前行, p ->paste
      • d$,p: 剪切当前行,,插入到某行中间, 上面的剪切方式都只能不能插入到行中间
    • u, ctrl+r: u-> undo 撤销操作, ctrl+r -> redo 撤销的反操作
    • x, X: 都是删除光标所在位置的一个字符
    • r, R: r替换光标所在位置的一个字符, R进入替换模式,可以替换多个字符直到esc退出
    • gg, shift+g, ngg: gg 跳到第一行, shift+g 跳到最后一行, ngg 跳到第n行
    • ^ , $ : ^ 跳到行首, $ 跳到行尾
  • 插入模式(Insert-mode)
    • i, o, a
  • 命令模式(Command-mode)
    • :w filename -> 保存文件到某个位置
    • :!cmd -> 在vim里边运行命令, 可以粘贴命令的运行结果
    • 文本替换
      • :s/old/new: 替换当前行第一个old -> new
      • :%s/old/new: 所有行第一个old -> new
      • :%s/old/new/g: 替换全文 old -> new
      • :n,ms/old/new/g: 替换n行到m行中所有 old -> new
    • /str -> 全文搜索str, n下一个, shift+N上一个
    • :set nohlsearch -> 取消搜索高亮
    • :set nonu, set nu -> 取消行号,添加行号
    • vim /etc/vimrc -> 修改vim的启动基本配置
  • 可视模式(Visual-mode)
    • v 字符可视模式
    • V 行可视模式
    • ctrl+v 块可视模式
      • 配合y,d和shift+i命令可以进行块的便利操作
      • y,d 复制和剪切,在所有块模式下都可以用
      • shift+i 在块模式下可以实现多行插入, 编辑完成之后esc按两次