学习 shell 的价值
-
Linux 服务器的基本操作和管理
-
前端 Node.js 服务的进程管理、问题排查、资源监控等运维操作
-
使用 shell 编写 TCE、SCM、Docker 脚本,完成服务编译和部署
课程准备
- 一台安装了 linux 系统的物理机或者云主机,可运行 shell 脚本
- 本地的 vscode 安装 Bash Debug 插件,并升级到 bash 到 4.x 以上
- Npm 全局安装 zx 依赖
Shell 基础概念
概念
发展
- 除了替代 v6 shell,sh 还有几个优点,把控制流程、循环、变量引入了脚本,提供了一种更具功能性的语言
- 主流 Linux 系统使用的 shell,许多都以它为锚点
- bash 是 sh 的超集,可以直接执行大部分 sh 脚本
- bash 在兼容 Bourne shell 脚本编程的同时,集成了 Kom shell 和 C shell 的功能,包括命令历史、命令行编辑、目录堆栈(pushd 和 popd),一些实用的环境变量,命令自动补全等
构成
shell 不仅提供了与内核和设备交互的方法,还集成了一些今天软件开发中通用的设计模式(比如管道和过滤器),具备控制流程、循环、变量、命令查找的机制。
shell 既是命令解释器,也是一门编程语言,作为命令解释器,它提供给用户接口,使用丰富的 GNU 工具集,第三方库或内置的,比如:cd、pwd、exec、test、netstat 等等。
语法和命令
变量
类型 | 作用域 | 声明方式 | 规范 |
---|---|---|---|
自定义变量 | 当前shell | = | 字符串、整型、浮点型、日期型 |
环境变量 | 当前 shell 及其子 shell | export、declare -x | |
系统环境变量 | 所有 shell | 启动加载 |
父子 shell
当父进程在执行第三方或核心工具时,会启动一个子进程去执行,执行完后交还给父进程。父进程再执行下一条命令。
在这个过程中,自定义变量只有在父进程中使用到,而环境变量和系统环境变量在父进程和子进程中都可以用到。
不同的脚本执行方式产生的父子shell
source filename
或者. filename
(点 空格 filename)
不创建 子shell,在当前 bash 环境下读取 filename 中的命令,并执行。
关于
source
方法:如果 filename 不在当前路径下,或者 filename 不在执行程序默认的搜索路径下(也就是 PATH 环境变量),bash 可能会提示找不到 filename
bash filename
、sh filename
、./filename
或者/path1/path2/filename
(绝对路径)
创建 子shell,在当前 bash 环境下,创建一个新的子 shell 来读取 filename 中的命令,并执行。子 shell 环境随机关闭,回到父 shell 中。
关于
bash、sh、./
方法:如果 filename 不在当前路径下,或者 filename 不在执行程序默认的搜索路径下(也就是 PATH 环境变量),bash 可能会提示找不到 filename。关于
bash 和 sh
方法:filename 可以不必实现设定可执行权限。因为是将 filename 作为参数传递给bash
或者sh
,是被调用执行。关于
./
方法:filename 必须具备可执行权限,它才可以自己执行。额外:子 shell 继承父 shell 的变量,子 shell 通过 export 来使用父 shell 的变量。
子 shell 从父 shell 继承如下内容:
- 当前工作目录
- 环境变量
- 标准输入、标准输出、标准错误输出
- 所有已打开的文件标识符
- 忽略的信号
子 shell 不能从父 shell 继承的内容:
- 除环境变量和
.bashrc
文件定义的变量之外的 shell 变量- 未被忽略的信号
自定义变量
declare [+/-] 选项 变量
选项 | 含义 |
---|---|
- | 给变量设定类型属性 |
+ | 取消变量的类型属性 |
-a | 将变量声明为数组类型 |
-i | 将变量声明为整数型 |
-x | 将变量声明为环境变量 |
-r | 将变量声明为只读变量 |
-p | 显示指定变量的被声明类型 |
系统环境变量
变量名 | 含义 | 常见操作 |
---|---|---|
$0 | 当前 shell 名称/脚本名称 | $1、$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 shell,比如在原来的 bash 中执行 bash 开启子进程,执行一些外部命令。
需要注意的是,如果修改了配置文件,不会立即生效,需要我们重启终端或者执行 source
命令:
source ~/.bashrc
运算符和引用
类型 | 符号 | 作用 | 用法 |
---|---|---|---|
算术运算符 | + - * / % | & | 常规运算 | |
逻辑运算符 | || && ! | ||
比较运算符 | == != < > | ||
引号 | 双引号 " " | 部分引用,仅仅 $ ` \ 保留作用 | foo= "${a} 123" |
单引号 ' ' | 完全引用,原样输出 | foo= 'foo$a' | |
反引号 ` ` | 执行命令 | foo= `ls -a` | |
圆括号 | (()) | 算术命令 | foo= $((1+2)) |
() | 执行命令 | $(ls -a) | |
命令连接 | || | cmd1 执行完且返回码非0,则继续执行 cmd2 | cmd1 || cmd2 |
&& | cmd1 执行完且返回码为0,则继续执行 cmd2 | cmd1 && cmd2 | |
; | cmd1、cmd2 串行执行 | cmd1 ; cmd2 | |
后台运行 | & | 让命令在后台运行,可与 nohup 一起使用 | cmd & |
nohup cmd &
管道
管道与管道符 |
,作用是将前一个命令的结果传递给后面的命令。管道的本质就是将多个程序进行连接,和信号一样,也是进程通信的方式之一。
语法:cmd1 | cmd2
要求:管道右侧的命令必须能接受标准输入,比如 grep
命令。ls
、mv
等不能直接使用,要使用 xarg
预处理。
注意:管道命令不仅仅处理 stdout(标准输出),对于 stderr(标准错误输出)会予以忽略,可以使用 set -o pipefail
设置 shell 遇到管道错误退出。
#!/bin/bash
cat plantform.access.log | grep ERROR
netstat -an | grep ESTABLISHED | wc -l
find . -maxdepth l -name "*.sh" | xargs ls -l
例:
重定向
每个 shell 命令在执行时都会打开三个文件描述符,文件描述符 0、1、2,分别对应 stdin、stdout、stderr,这三个文件描述符默认指向终端输入、终端输出,那么当命令需要获取输入的时候,它会去取 fd0,当要输出的时候它会向 fd1、fd2 写入,改变这些描述符指向的行为叫做 重定向。
输出重定向符号
command > file
:覆盖写入文件
command >> file
:追加写入文件
command 2> file
:错误输出写入文件
n >& m
:将输出文件 m 和 n 合并
command &> file
:正确和错误输出统一写入到文件中
输入重定向符号
command < file
:将输入重定向到 file
command << delimiter
:将输入重定向到一个交互式 shell 脚本或程序
注意:2>&1
必须写在 >
之后
<<
比较特殊,表示继续沿用当前的标准输入,只是当识别到指定的标识符后停止,将接收到的内容作为 stdin
实例:用户在命令行输入内容,当输入 EOF 时停止,所输入的内容写入 foo.txt
判断命令
shell 中提供了 test、[、[[
三种判断符号,可以用于:
- 整数测试
- 字符串测试
- 文件测试
语法:
test condition
- [ condition ]
- [[ condition ]]
注意:
- 中括号前后要有空格符;
[
和test
是命令,只能使用自己支持的标志位,<、>、=
只能用来比较字符串- 中括号内的变量,最好都是用引号括起来
[[
更丰富,在整型比较中支持<、>、=
,在字符串比较中支持=~
正则
分支语句
语法 1:
if condition; then
程序段
elif condition; then
程序段
else
程序段
fi
语法 2:
case $变量 in:
"第一个变量内容")
程序段
;;
"第一个变量内容")
程序段
;;
*)
程序段
;;
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";}
语法二:
function 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
注意:
- shell 自上而下执行,函数必须在使用前定义
- 函数获取变量和 shell script 类似,
$0
代表函数名,后续参数通过$1、$2 ...
获取 - 函数内
return
仅仅表示函数执行状态,不代表函数执行结果 - 返回结果一般使用
echo、printf
,在外面使用$()、``
获取结果 - 如果没有
return
,函数状态时上一条命令的执行状态,存储在$?
中
#!bin/bash
function test() {
local word="hello world"
echo $word
return 10
unset word
}
content=`test`
echo "状态码: $?"
echo "执行结果: $content"
函数也是命令
exit
:手动退出 shell、命令,exit 10
返回 10 给 shell,返回值非 0 为不正常退出
$?
用于判断当前 shell 前一个命令是否正常退出(非 0 为不正常退出)
为了函数内定义的变量不污染全局,最好使用 local
定义,或者在函数退出之前使用 unset
去处理
模块化
模块化的原理是在当前 shell 内执行函数文件,方式:
source [函数库的路径]
常用命令
命令 | 使用 |
---|---|
grep | 查找错误日志:grep -n "ERROR" -A3 -B3 cloudfun.log 统计次数: grep -n "ERROR" -c cloudfun.log |
sort | 指定分隔符后以第三列进行排序: sort -t " " -k 3 |
wc | 统计出现的行数、单词数、字符数: wc -lwn |
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
#! 是内核识别并选择合适的解释器后,将文本文件再交给解释器执行
执行过程
这个架构类似一个流水线,在里面加入输入分析和解析,bash 会以一些 特殊字符作为分隔符,将文本进行分段解析。
最主要的是 回车 还有 分号。在 bash 脚本中是以回车或者分号作为一行命令结束的标志。这就是第一层的解析,将大段的命令进行分段。
符号拓展(使用各种方法,比如 {}、~
、变量和参数的 展开/替换、文件名展开),并最终执行命令(通过 shell 内置命令或外部命令)。
- 字符解析
- 识别换行符、分号,做行的分割
- 识别命令连接符(
||、&&、管道
) 做命令的分割 - 识别空格、tab符,做命令和参数的分割
- shell 展开,例如
{1..3}
解析为1 2 3
- 重定向,将
stdin、stdout、stderr
的文件描述符进行指向变更 - 执行命令
builtin
直接执行- 非
builtin
使用$PATH
查找,然后启动子进程执行
- 收集状态并返回
shell 展开
- 大括号展开 (Brace Expansion)
{...}
- 波浪号展开 (Tilde Expansion)
~
- 参数展开 (Shell Parameter Expansion)
- 命令替换 (Command Substitution)
- 数学计算 (Arithmetic Expansion)
$((..))
- 文件名展开 (Filename Expansion)
* ? [..]
外壳文件名模式匹配
大括号展开 (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) ~
# 当前用户主目录
~ => $HOME
!/foo => $HOME/foo
# 指定用户的主目录
~fred/foo => 用户fred的 $HOME/foo
# 当前工作目录
~+/foo => $PWD/foo
# 上一个工作目录
~-/foo => ${$OLDPWD- '~-'}/foo
参数展开 (Shell Parameter Expansion)
-
间接参数展开
${!parameter}
,其中引用的参数 并不是 parameter 而是 parameter 实际的值 -
参数长度
${#parameter}
-
空参数处理
${patameter:-word}
# 为空替换${patameter:=word}
# 为空替换,并将值赋给$parameter
变量${patameter:?word}
# 为空报错${patameter:+word}
# 不为空替换 -
参数切片
${patameter:offset}
${patameter:offset:length}
-
参数部分删除
${patameter%word}
# 最小限度从后面截取 word${patameter%%word}
# 最大限度从后面截取 word${patameter#word}
# 最小限度从前面截取 word${patameter##word}
# 最大限度从前面截取 word
命令替换 (Command Substitution)
在子进程中执行命令,并用得到的结果替换包裹的内容,形式上有两种:$(...)
或 `...`
#! /bin/bash
echo $(whoimi)
foo() {
echo "asdasd"
}
a=`foo`
数学计算 (Arithmetic Expansion) $((..))
使用 $(())
包裹数学运算表达式,得到结果并替换
#! /bin/bash
echo $((1+2)) # 3
文件名展开 (Filename Expansion) * ? [..]
外壳文件名模式匹配
当有单词没有被引号包裹,且其中出现了 *、?、[
字符,则 shell 会去按照正则匹配的方式查找文件名进行替换,如果没找到则保持不变。
#! /bin/bash
$ echo D*
# 输出当前目录下所有以 D 字母开头的目录、文件
调试和前端集成
调试
-
普通 log,使用
echo、printf
-
使用
set
命令
set 配置 | 作用 | 补充 |
---|---|---|
-u | 遇到不存在的变量就会报错,并停止执行。 | -o nounset |
-x | 运行结果之前,先输出执行的那一行命令。 | -o xtrace |
-e | 只要发生错误,就终止执行 | -o errexit |
-o pipefail | 管道符连接的,只要一个子命令失败, 整个管道命令就失败,脚本就会终止执行 |
- vscode debug 插件
VSCode 配置
- shellmen:代码提示和自动补全
- shellcheck:代码语法校验
- shell-format:代码格式化
- Bash Debug:支持单步调试
- 安装 vscode 插件
- 编写 launch.json 文件
- 升级 bash 到 4.x 以上版本
前端集成
-
node 中通过
exec、spawn
调用 shell 命令exec
启动一个子 shell 进程执行传入的命令,并且将执行结果保存在缓冲区中(有大小限制),执行完毕后通过回调函数返回。spawn
默认不使用 shell,而是直接启动子进程执行命令,且会直接返回一个流对象,支持写入或者读取流数据,在大数据量交互的场景较适合。 -
shell 脚本中调用
node
命令 -
借助 zx 等库进行 javascript、shell script 的融合
- 借助 shell 完成系统操作,文件io、内存、磁盘系统状态查询等
- 借助 nodejs 完成应用层能力,网络io、计算等