Shell 脚本和编程 | 青训营笔记

118 阅读9分钟

Shell 脚本和编程

学习 shell 的价值:

  1. Linux 服务器的基本操作和管理
  2. 前端 Node.js 服务的进程管理、问题排查、资源监控等运维操作
  3. 使用 Shell 编写 TCE、SCM、Docker 脚本,完成服务编译和部署

课前环境准备

  • 一台安装了 Linux 系统的物理机或者云主机,可运行 shell 脚本
  • 本地的 vscode 安装 Bash Debug 插件,并升级 bash 到 4.x 以上
    • vscode 安装插件就不用介绍了吧
    • Windows 如何能运行 bash 命令
      • 首先,打开控制面板,找到 启用或关闭 Windows 功能 image.png
      • 然后在弹出的对话框中找到 适用于 Linux 的 Windows 子系统,点击确定,等待电脑重启 image.png
      • 重启电脑后,打开 Microsoft store,在搜索框中输入 bash,同样可以增加条件进行筛选 image.png
      • 我这里选择的是 Ubuntu,大家按自己喜好选择,点击获取,下载好后,点击启动 image.png
      • 接着配置用户名和密码即可 image.png
      • 如何查看版本,可以输入命令 bash --version image.png
  • Npm 全局安装 zx 依赖

Shell 基础概念

概念

  • 终端:获取用户输入、展示运算结果的硬件设备
  • tty:teletypeWriter 的简称,和终端等价,早期指电传打印机,在 Linux 中是 输入/输出 环境
  • 终端模拟器:Mac Terminal、Iterm2 等,关联虚拟 tty 的输入输出软件
  • Shell:command interpreter,处理来自终端模拟器的输入,解释执行之后输出结果给终端
  • Bash:shell 的一种具体实现

发展

image.png

构成

image.png

命令和语法

变量

类型作用域声明方式规范
自定义变量当前 shell=字符串、整型、浮点型、日期型
环境变量当前 shell 及其子 shellexport、declare -x
系统环境变量所有 shell启动加载
image.png

自定义变量

#!/bin/bash

# 变量名=变量值(等号左右不能有空格)
page_size=1
page_num=2

# 将命赋值给变量
_ls=ls

# 将命令结果赋值给变量
file_list=$(ls -a)

# 默认字符串,不会进行 + 运算
total=page_size*page_num (error)

# 声明变量为整型
let total=page_size*page_num

declare -i total=page_size*page_num

# 导出环境变量
export total

declare -x total

上述代码块中,shell 中是不能通过算术运算符直接进行算术运算的,因为在 shell 中,如果不指明变量的类型,默认会将变量的值当作字符串

declare [+/-] 选项 变量

选项含义
-给变量设定类型属性
+取消变量的类型属性
-a将变量声明为数组类型
-i将变量声明为整数型
-x将变量声明为环境变量
-r将变量声明为只读变量
-p显示指定变量的被声明的类型

系统环境变量

变量名含义常见操作
$0当前 shell 名称/脚本名称11、2等可以获取到传入参数
$#传入脚本的参数数量if[ $# -gt 1 ]
$*传入脚本的所有参数
$?上条命令执行的状态码if[$? -eq 0]
$PS1命令提示符export PS1="\u@\h \w"
$HOME用户主文件夹cd ~
$PATH全局命令的搜索路径PATH=$PATH:[新增路径]
代码示例:
image.png

配置文件加载

  • shell 的种类
    • login shell:登录主机的时候需要你提供用户名和密码
    • non-login shell:登录主机后打开的终端就是非登录式 shell
      • 交互式:会在终端中等待用户的输入,处理之后将结果返回
      • 非交互式:与交互式相反,例如 sh test.sh 当我们执行一段脚本时,终端并没有等待用户输入指令,而是直接执行

加载流程 image.png 当我们修改了配置文件时,想要当时立马生效需要执行 source ~/.bashrc 命令

运算符和引用

image.png

  • 引号运算符需要注意:
    • 双引号,只能部分引用,仅仅  保留作用,例如foo="`\ 保留作用,例如 `foo="{a}123",这里 ${a}` 会将变量 a 的值取出在拼接 123 成一个新的字符串赋值给 foo
  • 圆括号中
    • 双圆括号,可以解决 shell 中不能直接算术运算的问题
  • 命令连接
    • ||:当前面的命令执行不成功时,才会执行后续命令
    • &&:当前面的命令执行成功时,才会执行后续命令
    • ;:当各命令之间毫无关联,但是又想全部执行的时候,可以使用 分号符

管道

  • 定义:管道与管道符|,作用是将前一个命令的结果传递给后面的命令
  • 语法:cmd1 | cmd2
  • 要求:管道右侧的命令必须能接收标准输入才行,比如 grep 命令,ls、mv 等不能直接使用,可以使用 xargs 预处理
  • 注意:管道命令仅仅处理 stdout,对于 stderr 会予以忽略,可以使用 set -o pipefail 设置 shell 遇到管道错误退出
#!/bin/bash

cat platform.access.log | grep ERROR

netstat -an | grep ESTABLISHED | wc -l

find . -maxdepth 1 -name ".sh" | xargs ls -l
  • 命令解读
    • 通过 cat 命令查看 platform.access.log 文件,将返回的结果交给 grep 命令筛选出包含 ERROR 的内容
    • 通过 netstat 命令查看网络的状态,再通过 grep 命令筛选出包含 ESTABLISHED 的结果交给 wc -l 命令统计,最后返回结果
    • find . -maxdepth 1 -name ".sh"
      • 首先找到当前目录的所有 .sh 文件(find . -name ".sh")
      • -maxdepth 1 指定遍历搜索的最大深度为 1
      • ls -l 如果直接管道符接上这条命令,那就会导致前序命令执行的结果 ls 命令并没有接收到,所以需要在 ls 命令前加上 xargs ls -l,用于预处理,而这条命令是打印相关文件的权限以及创建时间等信息

重定向

image.png image.png

  • 重定向符号分类
    • 输出重定向
      • >:覆盖写入文件
      • >>:追加写入文件
      • 2>:错误输出写入文件
      • &>:正确和错误输出统一写入到文件中
    • 输入重定向
      • <
      • <<:代表继续引用当前标准输入,但是当识别到指定符号的时候,就会停止接收,然后将已经接收到的结果传递给命令 测试结果: image.png << 输入重定向测试结果: image.png

判断命令

  • shell 中提供了 test、[、[[ 三种判断符号,可用于:
    • 整数测试
    • 字符串测试
    • 文件测试
  • 语法:
    • test condition
    • [ condition ]
    • [[ condition ]]
  • 注意:
    • 中括号前后要有空格符分割
    • [ 和 test 是命令,只能使用自己支持的标志位,<、>、= 只能用来比较字符串
    • 中括号内的变量,最好都是用引号括起来 image.png
    • [[ 更丰富,在整型比较中支持 <、>、=,在字符串比较中支持 =~ 正则
  • test 为例:
    #!/bin/bash
    # 整数测试
    test $n1 -eq $n2 # 判断 n1 == n2 ?
    test $n1 -lt $n2 # 判断 n1 < n2 ?
    test $n1 -gt $n2 # 判断 n1 > n2 ?
    
    # 字符串测试
    test -z $str_a # -z 判断字符串为空
    test -n $str_a # -n 判断字符串非空
    test $str_a = $str_b # 判断两个字符串是否相等
    
    # 文件测试
    test -e /dmt && echo "exist" # 判断这个文件是否存在
    test -f /usr/bin/npm && echo "file exist" # 判断这个文件是否存在并且是不是普通文件
    
    

    分支语句

    • 语法1:
    if condition; then
        程序段
    elif condition; then
        程序段
    esle
        程序段
    fi
    
    • 例子1:
    #!/bin/bash
    
    level=0
    
    if [ -n "$level" ]; then
        if [ "level" == 0 ]; then
            prefix=ERROR
        elif [ "level" == 1 ]; then
            prefix=INFO
        else
            echo "log level not supported"
        fi
    fi
    
    echo "[${prefix}] $message"
    
    
    • 例子2:
    #!/bin/bash
    
    read -p "please inpout (Y/N) : " yn
    
    if [ "$yn" == "y" -o "yn" == "Y" ]; then
        echo "ok continue"
    fi
    
    if [ "yn" == "y" ] || [ "yn" == "Y" ]; then
     echo "ok continue too"
    fi
    
    
    • 语法2:
    case $变量 in:
      "第一个变量内容")
         程序段
         ;;
       "第一个变量内容")
         程序段
         ;;
        *)
        程序段
        ;;
     esac
    
    • 例子:
       #!/bin/bash
       
       name=john
       
       case $name in
           "nick")
               echo "hi nick"
           ;;
           "john")
               echo "my name is john"
           ;;
           *)
               echo "404"
           ;;
       esac
       
       ```
    

循环

  • while 循环
    • 语法:while condition ; do 程序段; done
    • 例子:
      #!/bin/bash
      
      let num=0
      
      while [ $num -lt 10 ];
      do
          echo "current idx: $num"
          ((num++))
      done
      
      ```
    
  • until 循环
    • 语法:until condition ; do 程序段; done
    • 例子:条件成立的时候跳出循环
      #!/bin/bash
      
      let num=0
      
      until [ $num -gt 10 ];
      do
          echo "current idx: $num"
          ((num++))
      done
      
      ```
    
  • for 循环
    • 语法:for var in [words...]; do 程序段; done
    • 例子:
      #!/bin/bash
      
      # 对列表进行循环
      for foo in a b c
      do
          echo $foo
      done
      
      # 数值方式循环
      for ((i=0;i<10;i++))
      do
          echo $i
      done
      
      ```
    

函数

  • 语法一:funcName(){ echo "abc"; }
  • 例子:
#!/bin/bash

printName() {
    
    if [ $# -lt 2 ]; then
     echo "illegal parameter"
     exit 1
    fi
    
    echo "firstname is :$1"
    echo "lastname is: $2"
    
}

printName jacky chen

  • 语法二:function funcName(){ echo "abc"; }
  • 例子:
#!/bin/bash

function test() {
    # local 声明的变量限定在当前的作用域生效,避免污染外部环境
    local word="hello world"
    echo $word
    return 10
    
    # 如果没有使用 local 声明变量,可以使用 unset 撤销一下
    unset word
}

content=`test`

echo "状态码:$?"
echo "执行结果:$content"

  • 注意:
    • shell 自上而下执行,函数必须在使用前定义
    • 函数获取变量和 shell script 类似,0代表函数名,后续参数通过0 代表函数名,后续参数通过 1、$2... 获取
    • 函数内 return 仅仅表示函数执行状态,不代表函数执行结果
    • 返回结果一般使用 echo、printf,在外面使用 $()、`` 获取结果
    • 如果没有 return,函数状态是上一条命令的执行状态,存储在 $? 中

模块化

  • 定义:模块化的原理是在当前 shell 内执行函数文件
  • 语法:source [函数库的路径]
  • 例子:
    #!/bin/bash
    
    # add函数
    # @return platForm
    function add() {
        declare -i res=$1+$2
        echo $res
    }
    
    
    #!/bin/bash
    
    source './math.sh'
    
    total=$(add 1 2)
    
    echo $total
    
    

常用命令

命令使用
grep查找错误日志:grep -n "ERROR" -A3 -B3 cloudfun.log
统计次数:grep -n "ERROR" -c cloudfun.log
sort指定分隔符后以第三列进行排序:sort -t " " -k 3
wc统计出现的行数、单词数、字符数:wc -lwm
head查看前十行:head -n 10 cloudfun.log
tail等待追加内容:tail -f -n 10 cloudfun.log
cut对数据行的内容进行处理:cut -d " " -f 3
find文件和目录查找
xargs参数处理
which查找命令路径

执行过程和原理

执行

  • shell 脚本一般以 .sh 结尾,也可以没有,这是一个约定,第一行需要指定使用什么命令解释器来执行
#! /bin/bash
#! /usr/bin/env bash
  • 启动方式
    • 文件名运行:./filename.sh(子进程执行)
    • 解释器运行:bash ./filename.sh(子进程执行)
    • source 运行:source ./filename.sh(当前进程执行)

执行过程

image.png

  • 字符解析
    • 识别换行符、分号(;) 做行的分割
    • 识别命令连接符(|| && 管道) 做命令的分割
    • 识别空格、tab符,做命令和参数的分割
  • shell 展开,例如 {1...3} 解析为 1 2 3
  • 重定向,将 stdin、stdout、stderr 的文件描述符进行指向变更
  • 执行命令
    • builtin 直接执行
    • 非 builtin 使用 $PATH 查找,然后启动子进程执行
  • 收集状态并返回

shell 展开

大括号展开(Brace Expansion) {...}

  • 定义:一般由三部分构成,前缀、一对大括号、后缀,大括号内可以是都好分割的字符串序列,也可以是序列表达式 {x...y[...incr]}
# 字符串序列
a{b,c,d}e => abe ace ade

# 表达式序列,(数组可以使用 incr 调整增量,字母不行)
{1...5} => 1 2 3 4 5
{1...5...2} => 1 3 5
{a...e} => a  b c d e

波浪号展开(Tilde Expansion) ~

  • 当前用户主目录:~/foo => $HOME/foo
  • 指定用户的主目录:~fred/foo => 用户 fred 的 $HOME/foo
  • 当前工作目录:~+/foo => $PWD/foo
  • 上一个工作目录:~-/foo => ${$OLDPWD-'~-'}/foo

参数展开(Shell Parameter Expansion)

  • 间接参数扩展 ${!parameter},其中引用的参数并不是 parameter 而是 parameter 的实际的值
    • 例子:
    parameter="var"
    var="hello"
    echo ${!parameter} => 实际引用的是 parameter 的值,${var}
    
    # 输出 hello
    
  • 参数长度 ${#parameter}
    • 例子:
    par=cd
    echo ${#par} # 就是展开参数的长度
    # 输出 2
    
  • 空参数处理
    • ${parameter: -word} 为空替换
    • ${parameter: =word} 为空替换,并将值赋给 $parameter 变量
    • ${parameter: ?word} 为空报错
    • ${parameter: +word} 不为空替换
    • 例子:
    a=1
    
    # 为空替换
    echo ${a:-word} # 因为 a 不为空,所以不替换,打印原有值 1
    echo ${b:-word} # 因为 b 未定义且没有值,所以进行替换,打印 word
    
    # 为空替换且赋值
    echo ${par:=word} # 因为 par 未定义且为空,所以将替换内容赋值给 par,打印 word
    echo ${par:-hello} # 因为上行代码,已经给 par 赋值,所以不进行替换,打印 word
    
    # 不为空替换
    echo ${par:+foo} # 因为 par 有值所以进行替换,打印 foo
    
    
  • 参数切片
    • ${parameter: offset}
    • ${parameter: offset: length}
    • 例子:
    awk  '{print substr(,1,8)}' 打印所有列的1-8字节
    awk '{print substr($1,1,8)}'  打印1列的1-8字节
    awk '{print substr($1,7)}'  打印1列的7-最后所有字节
    
  • 参数部分删除
    • ${parameter%word} # 最小限度从后面截取 word
    • ${parameter%%word} # 最大限度从后面截取 word
    • ${parameter#word} # 最小限度从前面截取 word
    • ${parameter##word} # 最大限度从前面截取 word
    • 例子:
    #! /bin/bash
    
    str=abcdefg
    
    sp1=${str##*d} # 从左向右去掉最后一个 d 前面包括 d 的字符串
    sp2=${str%%d*} # 从右向左去掉最后一个 d 后面包括 d 的字符串
    
    echo $sp1 # 打印 efg
    echo $sp2 # 打印 abc
    

命令替换(Command Substitution)

  • 定义:在子进程中执行命令,并将得到的结果替换包裹的内容
  • 语法:$(...)...
  • 例子:
#! /bin/bash

echo $(whoimi)

foo() {
    echo "asdasd"
}

a=`foo`

数学计算(Arithmentic Expansion) $((...))

  • 定义:使用 $(()) 包裹数学运算表达式,得到结果并替换
  • 例子:
#! /bin/bash

echo $((1+2)) # 3

文件名展开(Filename Expansion) * ?[...] 外壳文件名模式匹配

  • 定义:当有单词没有被引号包裹,且其中出现了‘*’,‘?’,and ‘[’ 字符,则 shell 会去按照正则匹配的方式查找文件名进行替换,如果没找到则保持不变
  • 例子:
#! /bin/bash

$ echo D*

# 输出当前目录下所有以 D 字母开头的目录、文件

调试和前端集成

调试

  • 普通log,使用 echo,printf
#! /bin/bash

a=1
d=(1 2 3 4 5)

echo $a # 1
echo ${d[3]} # 相当于数组下标的使用 4
echo ${d[@]} # 全部输出 1 2 3 4 5
  • 使用 set 命令 | set 配置 | 作用 | 补充 | | --- | --- | --- | | -u | 遇到不存在的变量就会报错,并停止执行 | -o nounset | | -x | 运行结果之前,先输出执行的哪一行命令 | -o xtrace | | -e | 只要发生错误,就会终止执行 | -o errexit | | -o pipefail | 管道符链接的,只要一个子命令失败,整个管道命令就失败,脚本就会终止执行 | |
# 一般在 shell 的开头进行 set 的配置
#! /bin/bash
set -uxe -o pipefail

echo "hello world"
  • vscode debug 插件
    • shellman:代码提示和自动补全
    • shellcheck:代码语法校验
    • shell-format:代码格式化
    • Baash Debug:支持单步调试
      • 安装 vscode 插件
      • 编写 launch.json 文件
      • 升级 bash 到 4.x 以上版本
      • 配置
      # 安装最新版本 bash
      brew install bash
      
      # 查看安装路径
      which -a bash
      
      # 将新版本 bash 路径加入 PATH
      PATH="/usr/local/bin/bash:$PATH"
      
      # 配置 vscode launch.json 启动文件
      {
          "version": "0.2.0",
          "configurations": [
              {
                  "type": "bashdb",
                  "request": "launch",
                  "name": "Bash-Debug (simplest configuration)",
                  "cwd": "${workspaceFolder}",
                  "program": "debug.sh"
              }
          ]
      }
      
      

前端集成

  • node 中通过 exec、spawn 调用 shell 命令
    • exec 和 spawn 的区别
      • exec:启动一个子 shell 去执行传入的命令并且将命令执行的结果存在缓存区,执行完毕之后,将缓存区的结果返回到函数当中,但是缓存区的大小是有限制的,默认是 200 KB,超过这个大小就会报错
      • spawn
        • 不会启动一个子 shell
        • 返回一个流对象,我们可以直接对这个流对象进行写入和读取的操作,没有大小限制,所以它比较适合大数据量的操作
    • 例子:
    const { exec } = require('child_process');
    
    exec('ls', ['-l'], (err, stdout, strerr) => {
        if (err) {
            console.error(err);
        }
        stdout && console.log(stdout)
    })
    
  • shell 脚本中调用 node 命令
    • 例子:
    #! /bin/bash
    set -e
    node ./exec.js
    echo 'success'
    
  • 借助 zx 等库进行 JavaScript、shell script 的融合
    • 借助 shell 完成系统操作,文件 io、内存、磁盘系统状态查询等
    • 借助 nodejs 完成应用层能力,网络io、计算等
    • 例子:
    #! /bin/bin/env zx
    let files = await $`ls -a | grep node`
    console.log(chalk.blue(`files : $(files).`))
    await sleep(1000)
    await fetch('https://google.com')
    let answer = await question('do you want to create new dir ? ');
    if (answer === 'y') {
        await $`mkdir temp`
    }
    

image.png