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

143 阅读5分钟

0课程准备

  • linux环境

    • 可以采用虚拟机+linux发行版的方式

      • 虚拟机:VMware(网上资源多)
      • linux发行版:centos,Ubuntu...(都可以国内有镜像网站,速度慢可以找热心网友的网盘分享)
    • 也可以使用蓝桥云课

  • vscode安装bash debug插件

  • npm全局安装zx依赖

1为什么要学习shell

  • 服务的编译和部署->编写tce、scm、docker脚本

  • 前端node.js服务的进程管理、问题排查、资源监控等运维操作

2shell基础概念

2.1概念

  • 终端:获取用户输入、展示运算结果的硬件设备->用户交互硬件
  • tty:和终端等价,在linux中是输入/输出环境
  • 终端模拟器:关联虚拟tty的输入输出软件
  • shell:处理来自终端模拟器的输入,解释执行之后输出结果给终端
  • bash:shell的一种具体实现

2.2发展

  • shell顾名思义就是“壳”,操作系统上为了实现用户与内核交互的设计
  • Thompson shell,为V6Unix编写了第一个shell,即/bin/sh
  • Bourne shell
  • 开源组织的GNU开发的Bash shell

2.3构成

  • shell具有两重性质

    • 解释器(linux的核心思想:一切皆为文件)可以通过type <command>尝试一下

      • bash内置命令
      • gnu核心工作集
      • 第三方库
    • 内建命令实际上是 shell 程序的一部分,其中包含的是一些比较简单的 Linux 系统命令,这些命令是写在 bash 源码的 builtins 里面的,由 shell 程序识别并在 shell 程序内部完成运行,通常在 Linux 系统加载运行时 shell 就被加载并驻留在系统内存中。而且解析内部命令 shell 不需要创建子进程,因此其执行速度比外部命令快。比如:history、cd、exit 等等。

      外部命令是 Linux 系统中的实用程序部分,因为实用程序的功能通常都比较强大,所以其包含的程序量也会很大,在系统加载时并不随系统一起被加载到内存中,而是在需要时才将其调入内存。虽然其不包含在 shell 中,但是其命令执行过程是由 shell 程序控制的。外部命令是在 Bash 之外额外安装的,通常放在/bin,/usr/bin,/sbin,/usr/sbin 等等。比如:ls、vi 等。

    • 编程语言

      • 变量

        • 自定义变量
        • 环境变量
        • 系统环境变量
      • 运算

        • 逻辑运算符
        • 算术运算符
      • 语句

        • 判断
        • 分支
        • 循环
      • 函数

3shell语法和命令

3.1变量

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

    • 父进程和子进程

3.2自定义变量

#!/bin/bash                 先写解析器,没有用默认,默认的解析器可以从passwd文件中查看

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

# 将命令复制给变量
_ls=ls

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

# 默认的变量都为字符串类型,故以下的写法是错误的
total=page_size*page_num

# 声明变量为整型,两种方式
let total=page_size*page_num

declare -i total=page_size*page_num

# 导出环境变量
export total

declare -x total
  • 对于上述例子,可以在linux环境下的终端中尝试一下得到以下结果,由于默认为字符串,最初我们是将page_size*page_num这个字符串赋给了total

  • 对于declare变量的参数

    • - 给变量设定类型属性

    • + 取消变量的类型属性

    • -a 将变量声明为数组类型

    • -i 将变量声明为整型

    • -x 将变量声明为环境变量

    • -r 将变量声明为只读变量

    • -p显示指定变量的被声明的类型

  • 数据类型

    • String,只有一种数据类型,但是可以对其进行声明
  • 变量取值

    • $变量名
      ${变量名}  #主要是为了区别,因为变量无需声明就可以使用
      
  • 变量赋值

    • [let] var=value
          b=$a
          
      shiyanlou:~/ $ a=6                                                                                    [19:32:56]
      shiyanlou:~/ $ echo $a                                                                                [19:33:00]
      6
      shiyanlou:~/ $ echo ${b?}                                                                             [19:33:03]
      zsh: b: parameter not set
      shiyanlou:~/ $ echo ${b?"no var"}                                                                     [19:33:13]
      zsh: b: "no var"
      shiyanlou:~/ $ echo ${b:="tt"}                                                                        [19:33:26]
      tt
      shiyanlou:~/ $ echo $b                                                                                [19:33:39]
      tt
      shiyanlou:~/ $ echo ${a:="tt"}                                                                        [19:33:44]
      6
      shiyanlou:~/ $ echo ${c:-"tt"}                                                                        [19:34:00]
      tt
      shiyanlou:~/ $ echo $c                                                                                [19:34:18]
      
      shiyanlou:~/ $ echo ${c:+"tt"}                                                                        [19:34:26]
      
      shiyanlou:~/ $ echo $c                                                                                [19:34:41]
      
      shiyanlou:~/ $ echo ${a:+"tt"}                                                                        [19:34:45]
      tt
      shiyanlou:~/ $ echo $a                                                                                [19:34:56]
      6
      shiyanlou:~/ $ echo ${a:+"temp"}                                                                      [19:34:59]
      temp
      
      '#'从左匹配
      
  • 清空变量

    • readonly var
      

3.3系统环境变量

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

3.4位置变量

3.5配置文件加载

  • login shell

  • non-login shell

    • 交互式的
    • 非交互式的
  • 加载过程

    • (login shell)->(/etc/profile)->(first of:~/bash_profile ~/bash_login /.profile)->(./bashrc)->(/etc/bashrc)

3.6运算符和引用

3.7管道

  • 管道与管道符| ,作用是将前一个命令的结果传递给后面的命令

    • 语法: cmd | cmd2
    • 注意:管道右侧的命令必须能接受标准输入才行,ls、mv等不能直接使用,可以使用xargs预处理
  • xargs ls -l
    • stderr会忽略,可以在脚本当中加上set -o pipefail进行设置,管道在错误的时候退出

3.8重定向

  • 输出重定向符号
>:覆写写入文件
>>:追加写入文件
2>:错误输出写入文件
&>:正确和错误输出统一写入到文件当中
  • 输入重定向符号
<
<<

3.9判断命令

  • shell中提供了test`[\[[`三种判断符号,可用于整数测试、字符串测试、文件测试

  • 语法

    • test condition

    • [ condition ]

      • 前后要有空格符
      • 中括号内的变量最好都用括号括起来
      • test和[是命令,只能使用自己支持的标志位,<>=只能用来比较字符串
    • [[ condition ]]

      • 在整型比较中支持<>=,在字符串比较中支持=~正则
  • 一些例子

    • #!/bin/bash
      
      #整数测试
      test $nl -eq $n2  #等于
      test $nl -lt $n2  #小于
      test $nl -gt $n2  #大于
      
      #字符串
      test -z $str_a  #为空
      test -n $str_a  #非空
      test $str_a = $str_b  #是否相等
      
      #文件测试
      test -e /dmt && echo "exist"  #文件是否存在
      test -f /usr/bin/npm && echo "file exist" #文件是否普通且存在
      

3.10分支语句

  • 语法1

    • if condition;then
          程序段
      elif condition;then
          程序段
      else
          程序段
      fi
      
      #例子
      
      #!/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"
      
  • 语法二

    • case $变量 in"第一个变量内容")
              程序段
          ;;
          "第二个变量内容")
              程序段
          ;;
          *)#默认分支表达式
          程序段
          ;;
      esac
      

3.11循环语句

  • 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
      

3.12函数

语法1

funcName(){echo"abc";}

语法二
function funcName(){echo"abc";}

函数调用
funcName var1 var2
  • 注意

    • 函数要在使用前定义

    • 函数获取变量和shell script类似,0代表函数名,后续参数通过0代表函数名,后续参数通过1$2...获取

    • 函数内return仅仅表示函数执行状态,不代表函数执行结果

    • 返回结果一般使用echo、printf,在外面使用$() 、" 获取结果//去打印ji

      • #!/bin/sh
        
        function test(){
            local word="hello world" # local,理解为局部变量
            echo $word
            return 10
            
            unset word
        }
        
        content='test'
        
        echo "状态码:$?"  #$?上条命令执行的状态码
        
        echo "执行结果:$content"
        
    • 如果没有return,函数状态是上一条命令的执行状态,存储在$?当中

3.13模块化

  • 在当前shell内执行函数文件

source [函数库的路径]

3.14常用命令

  • 例子
#筛选日志当中的错误信息
cat cloudfun.log | grep -n "ERROR"  #grep本质是去匹配字符串 -n:显示匹配行及 行号


#筛选日志当中的错误信息并按照时间顺序排序
cat cloudfun.log | grep -n "ERROR" | sort -t " " -k 3 #将信息依照空格分割后,以第三个列的值进行排序,需要注意的是,对于行的判定,一定是具有换行符才会被称作一行,和我们显示所看到的没有太大的关联

#查看错误日志信息的上下文
grep -n "ERROR" cloudfun.log -A3 -B3 #-A:After -B:Before

#tail/head
tail -n 10 filename
# -f参数:循环读取

4执行原理

4.1执行

  • shell脚本一般以.sh结尾,也可以没有,脚本中第一行指定解释器
#!/bin/bash
#!/usr/bin/env bash
  • 启动方式
# 文件名运行,有执行权限
./filename.sh

# 解释器运行
bash ./filename.sh

# source运行
source ./filename.sh

4.2执行过程(shell是一门解释型语言)

  1. 字符解析

    1. 识别换行符、分号:做行的分割
    2. 识别命令连接符(|| && 管道):做命令的分割
    3. 识别空格、tab符,做命令和参数的分割
  2. shell展开

    1. 大括号展开:{1..3} 解析为1 2 3

      1. 一般由三部分构成,前缀、一对大括号、后缀,大括号内可以是逗号分割的字符串序列,也可以是序列表达式{x..y[..incr]}
      2. # 字符串序列
        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
        
    2. 波浪号展开(~

      1. cd ~其实就是进入当前用户主目录
      2. # 当前用户主目录
        ~ => $HOME
        
        ~/foo => $HOME/foo
        
        # 指定用户的主目录
        ~fred/foo => 用户fred的 $HOME/foo
        
        # 当前工作目录
        ~+foo => $PWD/foo
        
        # 上一个工作目录
        ~-/foo => ${$OLDPWD-'~-'}/foo
        
    3. 参数展开

      1. 间接参数扩展${!parameter},其中引用的参数并不是parameter而是parameter的实际值
      2.     parameter="var"
            var="hello"
            echo ${!parameter}
            
            #输出为 hello
        
      3. 参数长度${#parameter}
      4. par=cd
        echo ${#par}
        #output 2
        #length
        
      5. 空参数处理
      6. a=1
        
        ${parameter:-word}#为空替换
        echo ${a:-word}# 1
        echo ${b:-word}# word
        
        ${parameter:=word}#为空替换并赋值,前面的替换并没有修改值,有点类似于选择
        echo ${par:=word}# word
        echo ${par:-hello}# word
        
        ${par:+word}# foo
        
      7. 参数切片
      8. ${parameter:offset}
        
        ${parameter:length}
        
      9. 参数部分删除(掐头去尾的操作)
      10. str=abcdefg
        
        ${parameter%word}# 最小限度从后面截取word
        
        ${parameter%%word}# 最大限度从后面截取word
        
        ${parameter#word}# 最小限度从前面截取word
        
        ${parameter##word}# 最大限度从后面截取word
        
    4. 命令替换

      1. 在子进程中执行命令,并用得到的结果替换包裹的内容,形式上有两种$(...)`... ```` `
      2. #!/bin/bash
        
        echo $(whoimi)
        
        foo(){
            echo "asdasd"
        }
        
        a='foo'
        
    5. 数学计算($((..))

      1. #!/bin/bash
        
        echo $((1+2)) # 3
        
    6. 文件展开(*?[..]外壳文件名模式匹配)

      1. 当有单词没有被引号包裹,且出现了 * ? [字符,会按照正则匹配的方式查找文件名进行替换
      2. $ echo D*
        
        # 输出当前目录下所有以D字母开头的目录、文件
        
  3. 重定向,将stdinstdoutstderr的文件描述符进行指向变更

  4. 执行命令

    1. builtin直接执行内置命令
    2. 外部命令使用$PATH查找,然后启动子进程执行
  5. 手机状态并返回

5.调试和前端集成

5.1调试

  • 普通log,使用echo、printf

  • 使用set命令

    • 一般在最开头的时候进行相关的设置
    • #!/bin/bash
      
      set -uxe -o pipefail
      
      echo "hello"
      
  • Vscode debug插件及配置

    • shellman:代码提示和自动补全

    • shellcheck:代码语法校验shell-format:代码格式化

    • Bash debug:支持单步调试

      • 安装vscode插件

      • 编写launch.json文件

      • 升级bash到4.x以上版本

5.2前端集成

  • node中通过exec、spawn调用shell命令

    • exec执行一个子shell运行传入的命令,先放在缓冲区,200kb
    • spawn不会有子进程,拥有流对象,适合
  • shell脚本中调用node命令

    • #!/bin/bash
      
      set -e
      
      node ./exec.js
      
      echo 'success'
      
  • 借助zx等库进行js、shell script的融合

    • 借助shell完成系统操作,文件io、内存、磁盘系统状态查询

    • 借助nodejs完成应用层能力,网络io、计算等

#!/usr/bin/env zx

语法$'command'