这是参加「青训营 x 字节前端训练营」笔记创作活动的第 13 天的文章
pass: 本篇文章是对Shell脚本基础的记录
课程介绍
学习 Shell 的价值
- Linux服务器的基本操作和管理
- 前端Node.js服务的进程管理、问题排查、资源监控等运维操作
- 使用shell编写TCE、SCM、Docker脚本,完成服务编译和部署
课程准备
- 一台安装了linux系统的物理机或者云主机,可运行shell脚本
- 本地的vscode安装Bash Debug插件,并升级bash到4.x以上
- npm全局安装zx依赖
Shell基础概念
Shell基础
概念
物理终端 => 软件终端 tty => 终端模拟器 => shell
- 终端:获取用户输入、展示运算结果的硬件设备
- tty:teletypeWriter的简称,和终端等价,早期指电传打印机,在linux中是输入/输出环境
- 终端模拟器Mac Terminal、iTerm2等,关联虚拟tty的输入输出软件,我们可以在终端模拟器中输入 tty 查看关联到的虚拟 tty,每个终端模拟器关联一个虚拟 tty ,和内核打交道。
- Shell:command interpreter, 处理来自终端模拟器的输入,解释执行之后输出结果给终端
- Bash:shell的一种具体实现, 可以理解成 实例和类 的关系
发展
除了替代 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的关系
自定义变量
系统环境变量
Bash Shell 在启动时总要配置其运行环境, 例如初始化环境变量、设置命令提示符、指定系统命令路径等。
配置文件加载
source ~/.bashrc
- 通过系统用户登录默认运行的shell
- 非登录交互式运行shell
- 执行脚本运行非交互式shell
如果取得 bash 需要完整的登录流程, 我们称之为 login shell, 比如 ssh 远程登录一台主机
不需要登录的bash 我们称为 non-login bash, 比如在原来的 bash 中执行 bash开启子进程、 执行一些外部命令
如果修改了配置文件,不会立即生效,需要我们重启终端或者执行 source 命令
运算符和引用
- 双引号:部分引用,使用这种引用时,$、`(反引号)、(转义符) 这 3 个还是会解析成特殊的意义
- 单引号:完全引用,只原样输出
- 反引号:执行命令
cmd & 实现让命令在后台运行
使用方法一的时候,当我们关闭终端,命令就会停止运行。加上nohup可以在关闭终端后不停止命令
管道
管道与管道符
|,作用是将前一个命令的结果传递给后面的命令
语法:cmd1 | cmd2
要求:管道右侧的命令必须能接受标准输入才行,比如grep命令,ls、mv等不能直接使用,可以使用xargs预处理
注意:管道命令仅仅处理stdout(标准输出),对于stderr(标准错误)会予以忽略,可以使用 set -o pipefail 设置 shell 遇到管道错误退出
如果需要互通,比如第一个命令的返回传递给第二个命令,就需要用到管道了,管道的本质就是将多个程序进行了一个连接,和信号一样,也是进程通信的方式之一
重定向
每个 shell 命令在执行时都会打开三个文件描述符,文件描述符0、1、2,分别对应 stdin、stdout、stderr, 这三个文件描述符默认默认指向 终端输入、终端输出,那么当命令需要获取输入的时候,它会去读取 fd0, 当要输出的时候它会像 fd1、fd2写入, 改变这些描述符指向的行为叫做重定向
- 2>&1 必须写在 > 之后
- << 比较特殊, 表示继续沿用当前的标准输入, 只是当识别到指定的标识符后停止, 将接收到的内容作为 stdin
- 实例: 用户在命令行输入内容,当输入 EOF 的时候停止, 所输入的内容写入 foo.txt
判断命令
根据程序是否正常执行(程序退出的状态)进行判断
- exit:手动退出 shell 的命令
- exit 10 返回 10 给 shell,返回值非 0 为不正常退出
- $? 用于判读昂当前 shell 前一个进程是否正常退出(非 0 为不正常退出)
分支语句
语法1:
if condition; then
程序段
elif condition; then
程序段
esle
程序段
fi
语法2:
case $变量 in:
"第一个变量内容")
程序段
;;
"第一个变量内容")
程序段
;;
*)
程序段
;;
esac
循环
- while循环
while condition ; do 程序段; done - until循环
until condition; do程序段; done【until 当条件成立的时候跳出循环】 - for循环
for var in [words...]; do程序段; done
函数
语法一: funcName(){echo "abc";
语法二: function funcName()echo "abc";
注意:
- shell 自上而下执行,函数必须在使用前定义
- 函数获取变量和shell script类似,1、$2..获取
- 函数内return仅仅表示函数执行状态,不代表函数执行结果
- 返回结果一般使用echo、printf,在外面使用$()、``获取结果
- 如果没有return,函数状态是上一条命令的执行状态,存储在$?中
函数也是命令
为了函数内定义的变量不污染全局, 我们最好使用 local 去定义, 或者在函数退出之前使用 unset 去处理一下
模块化
模块化的原理是在当前shell内执行函数文件,方式
source [函数库的路径]
常用命令
执行过程和原理
执行
- shell 脚本一般以 .sh 结尾,也可以没有,这是一个约定;第一行需要指定用什么命令解释器来执行
#!是内核识别并选择合适的解释器之后,将文本文件再交给解释器执行
#! /bin/bash
#! /usr/bin/env bash
- 启动方式
# 文件名运行
./filename.sh
# 解释器运行
bash ./filename.sh
# source 运行
source ./filename.sh
执行过程
- 字符解析
- 识别换行符、分号 ( ; ) 做行的分割
- 识别命令连接符 ( || && 管道 ) 做命令的分割
- 识别空格、tab符,做命令和参数的分割
- shell 展开,例如 {1..3} 解析为 1 2 3
- 重定向,将stdin、stdout、stderr的文件描述符进行指向变更
- 执行命令
- builtin直接执行。
- 非bui1tin使用$PATH查找,然后启动子进程执行
- 收集状态并返回
这个架构类似一个流水线,在里面进行输入分析和解析
bash 会以一些特殊字符作为分隔符,将文本进行分段解析。最主要是回车还有分号";"。在 bash 脚本中是以回车或者分号作为一行命令结束的标志。这就是第一层级的解析,将大段的命令行进行分段
符号拓展(使用各种方法,比如大括号 {} 、波浪符 ~ 、变量和参数的展开/替换、文件名展开),并最终执行命令(通过 shell 内置命令或外部命令)。
shell 展开
1. 大括号展开 (Brace Expansion) {...}
一般由三部分构成,前缀、一对大括号、后缀,大括号内可以是逗号分割的字符串序列,也可以是序列表达式 {x..y[..icr]}
2. 波浪号展开 (Tilde Expansion) ~
3. 参数展开 (Shell Parameter Expansion)
-
间接参数扩展 ${!parameter}, 其中引用的参数并不是 parameterl 而是 parameter 的实际的值
-
参数长度 ${#parameter}
-
空参数处理
- ${parameter:-word} # 为空替换
- ${parameter:=word} # 为空替换,并将值赋给 $parameter 变量
- ${parameter:?word} # 为空替换
- ${parameter:+word} # 不为空替换
-
参数切片
- ${parameter:offset}
- ${parameter:offset:length}
-
参数部分删除
- ${parameter%word} # 最小限度从后面截取word
- ${parameter%%word} # 最大限度从后面截取word
- ${parameter#word} # 最小限度从前面截取word
- ${parameter##word} # 最大限度从前面截取word
4. 命令替换 (Command Substitution)
在子进程中执行命令,并用得到的结果替换包裹的内容,形式上有两种:$(...) 或 `..`
5. 数学计算 (Arithmetic Expansion) $((..))
使用 $(()) 包裹数学运算表达式,得到结果并替换
6. 文件名展开 (Filename Expansion) *?[..] 外壳文件名模式匹配
当有单词没有被引号包裹,且其中出现了 * 、 ? 、 [ 字符,则 shell 会去按照正则匹配的方式查找文件名进行替换,如果没找到则保持不变。
调试和前端集成
调试
- 普通log, 使用 echo、printf
- 使用set命令
- vscode debug 插件
VSCode配置
插件:
- shellman: 代码提示和自动补全
- shellcheck: 代码语法校验
- shell-format: 代码格式化
- Bash Debug: 支持单步调试
- 安装 vscode 插件
- 编写 launch.json 文件
- 升级 bash 到 4.x 以上版本
前端集成
- node中通过exec、spawn调用shell命令
- exec 启动一个子 shell 进程执行传入的命令,并且将执行结果保存在缓冲区中。默认情况下,缓冲区是有200k的大小限制,执行完毕通过回调函数返回。
- spawn 默认不使用shell,而是直接启动子进程执行命令,且会直接返回一个带有stdout和stderr流的对象,支持写入或者读取流数据,这个在大数据量交互的场景比较适合(当想要子进程返回大量数据给Node时,比如说图像处理,读取二进制数据等,最好使用spawn方法)。
- shell脚本中调用node命令
- 借助zx等库进行javascript、shell script的融合
- 借助shell完成系统操作,文件io、内存、磁盘系统状态查询等
- 借助nodejs完成应用层能力,网络io、计算等
课程总结
shell 的思想和语法和传统的编程语言不太一样, 强调一条语句只干一件事, 所以万物皆命令, 在执行过程中也是逐行、逐个连接符、逐个空格的解析出最小化的命令执行,执行完之后再解析下一句
我们在了解了 shell 的配置加载、执行方式、执行过程、命令解析过程、 必要的语法、常用命令,则可以方便的写出自己的自动化脚本