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

92 阅读6分钟

Shell基础概念

概念

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

物理终端 => 软件终端 tty => 终端模拟器 => shell

发展

  1. Ken Thompson(来自贝尔实验室)在1971年为UNIX开发了第一个shell,称为v6 shell
  2. Stephen Bourne 在贝尔实验室为V7 UNIX所开发的Bourne shell,即sh
  3. 开源组织GNU为了替代Bourne shell开发的Bourne-Again shell ,即bash
  • 除了替代 v6 shell,sh 还有几个优点,把控制流程,循环,变量引入了脚本,提供了一种更具功能性的语言
  • 主流 Linux 系统使用的 shell,许多都以它为锚点。
  • bash是 sh 的超集,可以直接执行大部分 sh 脚本。 Bash 在兼容 Bourne shell 脚本编程的同时,集成了 Korn shell 和 C shell 的功能,包括命令历史,命令行编辑,目录堆栈(pushd 和 popd),一些实用环境变量,命令自动补全等。

构成

  • shell 不仅提供了与内核和设备交互的方法,还集成了一些今天软件开发中通用的设计模式(比如管道和过滤器), 具备控制流程,循环,变量, 命令查找的机制
  • 既是命令解释器, 也是一门编程语言, 作为命令解释器, 它提供给用户接口,使用丰富的 GNU 工具集, 第三方的或者内置的, 比如 cd、pwd、exec、test、 netstat 等等

语法和命令

变量

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

父子shell

自定义变量

系统环境变量

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

配置文件加载

  • 如果取得 bash 需要完整的登录流程, 我们称之为 login shell, 比如 ssh 远程登录一台主机
  • 不需要登录的bash 我们称为 non-login bash, 比如在原来的 bash 中执行 bash开启子进程、 执行一些外部命令
  • 如果修改了配置文件,不会立即生效,需要我们重启终端或者执行 source 命令(source ~/.bashrc)

运算符和引用

类型符号作用用法
算数运算符+ - * / %&常规运算
逻辑运算符&& !
比较运算符== != < >
引号双引号 ""部分引用,仅仅$ ` \保留作用foo ="${a}123"
单引号 ''完全引用,原样输出foo='foo$a'
反引号 ``执行命令foo=ls -a
圆括号(())算数运算foo=$((1+2))
()执行命令$(ls-a)
命令连接
&&cmd1 执行完且返回码为0,则继续执行cmd2cmd1 && cmd2
;cmd1、cmd2串行执行cmd1 ; cmd2
后台运行&cmd & 实现让命令在后台运行。但是当我们关闭终端,命令就会停止运行。加上nohup可以在关闭终端后不停止命令cmd &

管道

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

语法:cmd1 | cmd2

要求:管道右侧的命令必须能接受标准输入才行,比如grep命令,ls、mv等不能直接使用,可以使用xargs预处理

注意:管道命令仅仅处理stdout,对于stderr会予以忽略,可以使用set -o pipefail 设置shell遇到管道错误退出

重定向

每个 shell 命令在执行时都会打开三个文件描述符, 文件描述符0、1、2, 分别对应 stdin、stdout、stderr, 这三个文件描述符默认默认指向 终端输入、终端输出,那么当命令需要获取输入的时候,它会去读取 fd0, 当要输出的时候它会像 fd1、fd2写入, 改变这些描述符指向的行为叫做重定向

<< 比较特殊, 表示继续沿用当前的标准输入, 只是当识别到指定的标识符后停止, 将接收到的内容作为 stdin

  • 输出重定向符号

    • :覆盖写入文件

    • :追加写入文件

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

    • <
    • <<

判断命令

shell中提供了test、[、[[ 三种判断符号,可用于:

  • 整数测试
  • 字符串测试
  • 文件测试

语法:

  • test condition
  • [ condition ]
  • [[ condition ]]

注意:

  • 中括号前后要有空格符

  • [ 和test是命令,只能使用自己支持的标志位,<、>、= 只能用来比较字符串

  • 中括号内的变量,最好都是用引号括起来

  • [[ 更丰富,在整型比较中支持<、>、=,在字符串比较中支持=~正则

根据程序是否正常执行(程序退出的状态)进行判断

exit:手动退出 shell 的命令 exit 10 返回 10 给 shell,返回值非 0 为不正常退出 $? 用于判读当前 shell 前一个进程是否正常退出(非 0 为不正常退出)

分支语句

  • 语法1:
if condition ; then
  程序段
elif condition ; then
  程序段
else
  程序段
fi
![](https://secure2.wostatic.cn/static/gXz8NQ2XxqPknPnY8JNp3H/image.png?auth_key=1691551220-rRrvpKoyzRPCq5yLfnXHob-0-e0c69d52bd1b5d1512734be0c2dd8628)

![](https://secure2.wostatic.cn/static/tVyqBnCTbYZUGGRxrH9dom/image.png?auth_key=1691551220-k5LL5jFX1uA7dXvTTFftd7-0-3c7f16d11878885d397dab812249cb47)
  • 语法2:
case $变量 in:
  "第一个变量内容")
    程序段
  ;;
  "第一个变量内容")
    程序段
  ;;
  *)
    程序段
  ;;
esac
      
![](https://secure2.wostatic.cn/static/gjRdyCDAfr7cpk3EzvwRZm/image.png?auth_key=1691551220-jv1m2WuahJxkCLkitideZ6-0-552aeefaf870640142bcf450d3c26738)

循环

  • while循环

    while condition ; do 程序段; done

  • until循环

    until condition ; do 程序段; done

  • for循环

    for var in [words...]; do 程序段; done

函数

  • 语法1:funcName(){ echo "abc"; }
  • 语法2:function funcName(){echo "abc"; }

注意:

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

模块化

模块化的原理是在当前shell内执行函数文件,方式:source [函数库的路径]

常用命令

命令使用
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 cloundfun.log
tail等待追加内容:tail -f -n 10 cloudfun.log
cut对数据行的内容进行处理 cut -d " " -f 3
find文件和目录查找
xargs参数处理
which查找命令路径

执行过程和原理

执行

  1. shell脚本一般以.sh结尾,也可以没有,这是一个约定;第一行需要指定用什么命令解释器来执行
#! /bin/bash

#! /usr/bin/env/bash
  1. 启动方式
#文件名运行
./filename.sh

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

#source运行
source ./filename.sh

执行过程

这个架构类似一个流水线,在里面进行输入分析和解析

  1. 字符解析

    • 识别换行符、分号(;)做行的分割
    • 识别命令连接符(|| && 管道)做命令的分割
    • 识别空格、tab符,做命令和参数的分割
  2. shell展开,例如{1..3}解析为1 2 3

  3. 重定向,将stdin、stdout、stderr的文件描述符进行指向变更

  4. 执行命令

    • builtin直接执行
    • 非builtin使用$PATH查找,然后启动子进程执行
  5. 收集状态并返回

bash 会以一些特殊字符作为分隔符,将文本进行分段解析。最主要是回车还有分号";"。在 bash 脚本中是以回车或者分号作为一行命令结束的标志。这就是第一层级的解析,将大段的命令行进行分段

符号拓展(使用各种方法,比如大括号 {} 、波浪符 ~ 、变量和参数的展开/替换、文件名展开),并最终执行命令(通过 shell 内置命令或外部命令)。

shell展开

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

    一般由三部分构成,前缀、一堆大括号、后缀,大括号内可以是逗号分割的字符串序列,也可以是序列表达式{x..y[..incr]}

  2. 波浪号展开(Tilde Expansion)~

  3. 参数展开(Shell Parameter Expansion)

    1. 间接参数扩展${!parameter},其中引用的参数并不是parameter而是parameter的实际的值

    2. 参数长度${#parameter}

    3. 空参数处理

      • ${parameter:-word} # 为空替换
      • {parameter:=word} # 为空替换,并将值赋给parameter变量
      • ${parameter:?word} # 为空报错
      • ${parameter:+word} # 不为空替换

    4. 参数切片

      • ${parameter:offset}
      • ${parameter:offset:length}
    5. 参数部分删除

      • ${parameter%word}# 最小限度从后面截取word
      • ${parameter%%word} #最大限度从后面截取word
      • ${parameter#word}# 最小限度从前面截取word
      • ${parameter##word} #最大限度从前面截取word

  4. 命令替换(Command Substitution)

    在子进程中执行命令,并用得到的结果 替换包裹的内容,形式上有两种:$(...)或...

  5. 数学计算(Arithmetic Expansion)$((..))

    使用$(())包裹数学运算表达式,得到结果并替换

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

    当有单词没有被引导包裹,且其中出现了 '*' , '?' , and '[' 字符,则shell会去按照正则匹配的方式查找文件名进行替换,如果没找到则保持不变

调试和前端集成

调试

  1. 普通log,使用echo、printf

  2. 使用set命令

set配置作用补充
-u遇到不存在的变量就会报错,并停止执行-o nounset
-x运行结果之前,先输出执行的那一行命令-o xtrace
-e只要发生错误,就终止运行-o errexit
-o pipefail管道符连链接的,只要一个子命令失败,整个管道命令就失败,脚本就会终止执行
  1. vscode debug插件

VSCode配置

  1. shellman:代码提示和自动补全

  2. shellcheck:代码语法校验

  3. shell-format:代码格式化

  4. Bash Debug:支持单步调试

    1. 安装vscode插件
    2. 编写launch.json文件
    3. 升级bash到4.x以上版本

前端集成

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

    • exec 启动一个子 shell 进程执行传入的命令, 并且将执行结果保存在缓冲区中, 并且缓冲区是有大小限制的, 执行完毕通过回调函数返回
    • spawn默认不使用shell, 而是直接启动子进程执行命令, 且会直接返回一个流对象,支持写入或者读取流数据, 这个在大数据量交互的场景比较适合
  2. shell脚本中调用node命令

  3. 借助zx等库进行JavaScript、shell script的融合

    1. 借助shell完成系统操作,文件io、内存、磁盘系统状态查询等
    2. 借助nodejs完成应用层能力、网络IO、计算等

小结