Shell 编程
在使用 Linux 过程中, 必不可少接触到的就是 Shell。 作为没有任何图形化界面的 Terminal 来说,有alias 和 shell script 是提升终端和 Linux 必不可少的操作之一。因为终端在一定情况下也是有些繁琐,所以类似于“快捷方式”的 shell 脚本是必不可少的。
例如,在KDE 和 Gnome 上的桌面快捷方式,其实就是一个shell script 和 一个 icon 组成的哦。
下面我们就这个课程的笔记整理来简单理解和学习下 Shell 吧!
Shell 的价值:
-
Linux 服务器的基本操作和管理
-
前端 Node 服务的进程管理、问题排查、资源监控等运维操作
-
使用 Shell 编写 TCE、SCM、Docker 脚本,完成服务编译和部署。
01 Shell 基础概念
概念:
Physics Terminal => teletype writer => Terminal Emulator => shell
如果有使用过 Arch 或者 Minimal install 的同学应该很熟悉 tty 啦,简单描述就是无 DM 的操作系统。
tty或者说终端最开始指的是获取用户输入并输出的物理设备, 比如电传打字机
在 linux 中是接收用户输入、输出结果的终端仿真软件, 比如我们用的 mac terminal、 iterm2 等, 更强输入辅助功能、画面绘制输出的模拟终端器;
而 tty 变成一个虚拟概念, 是linux的一个程序,每个终端模拟器关联一个虚拟 tty ,和内核打交道。 我们可以在 终端模拟器中输入 tty 查看关联到的虚拟 tty
bash是 shell的一种具体实现, 可以理解成 实例和类的关系
发展:
Bell 实验室 在1971年 为Unix 开发第一个 V6 Shell
=> 为 V7 UNIX 开发的 sh
=> GNU 开发的 Bourne-Again Shell, bash
构成:
Shell 不仅提供了与内核和设备交互的方法,还集成了一些今天开发中通用的设计模式(例如 pipe 和 filter),具备控制流程、循环、变量、命令查找的机制、
既是命令解释器,也是变成语言。作为命令解释器,提供给用户接口,使用丰富的GNU工具集,或第三方或内置。例如cd pwd exec test netstat etc.
02 Shell 语法和命令
变量:
| 类型 | 作用域 | 声明方式 | 规范 |
|---|---|---|---|
| 自定义变量 | 当前 Shell | = | String、integer、Float、Date |
| 环境变量 | 当前以及子 shell | export、declare、-x | |
| 系统环境变量 | 所有 Shell | 启动加载 |
父子Shell:
自定义变量
tips:等号左右不能有空格
# 变量名=变量值(等号左右不能有空格)
page_size=1
page_num=2
# 将命令赋值给变量
_ls=ls
# 将命令结果赋值给变量
file_list=$(ls -a)
# 默认字符串,不会进行 + 运算
ptotal=page_size*page_num # error!!!
# 声明变量为整型
let total=page_size*page_num
declare -i total=page_size*page_info
# 导出环境变量
expor total
declare -x total
declare tags:
declare [+/-] tags param
| 选项 | 含义 |
|---|---|
| - | 给变量设定类型属性 |
| + | 取消变量的类型属性 |
| -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:[New path] |
配置文件加载:
-
通过系统用户登录默认运行的shell
-
非登录交互式运行shell
-
执行脚本运行非交互式shell
如果取得 bash 需要完整的登录流程, 我们称之为 login shell, 比如 ssh 远程登录一台主机
不需要登录的bash 我们称为 non-login bash, 比如在原来的 bash 中执行 bash开启子进程、 执行一些外部命令
如果修改了配置文件,不会立即生效,需要我们重启终端或者执行 source 命令
Shell Operator & quotes
partial ref:
-
双引号:部分引用,使用这种引用时,$、`(反引号)、(转义符) 这 3 个还是会解析成特殊的意义
-
单引号:完全引用,只原样输出
-
反引号:执行命令
-
cmd & 实现让命令在后台运行
-
使用方法一的时候,当我们关闭终端,命令就会停止运行。加上nohup可以在关闭终端后不停止命令
Pipe:
管道与管道符|,作用是将前一个命令的结果传递给后面的命令
-
语法:
cmd1 | cmd2 -
要求:管道右侧的命令必须能接受标准输入才行,例如
grep.ls mv等不能直接使用,可以用xargs预处理。 -
注意:管道命令仅仅处理 stdout,对于 stderr 会予以忽略,可以使用
set -o pipefail设置shell 遇到管道错误退出
cat platform.access.log | grep ERROR
netstat -an | grep ESTABLISHED | wc -l
find . -maxdepth 1-name "*.sh" | xargs ls -l
Pipe 的本质就是将多个程序进行了一个连接,和信号一样,也是进程通信的方式之一。
重定向
-
每个 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 为不正常退出)
exmaple:
name="hello world"
[ $name=="hello" ]
# [ : too many arguments
# Exited with error status 2
分支语句:
循环:
函数:
模块化:
常用命令:
03 Shell 展开
大括号展开(Brace Expansion): {...}
一般由三部分组成,前缀,一对大括号,后缀, 大括号内可以是逗号分割的字符串序列,也可以是序列表达式{x..y[.. incr]}
# String Sequence
a{b,c,d}e => abe ace ade
# Expression Sequence
{1..5} => 1 2 3 4 5
{1..5..2} => 1 3 5
{a..e} => a b c d e
# current user Directory
~ => $HOME
# 指定用户的主目录
~<user>/foo => 用户 <user> 的 $HOME/foo
# 当前工作目录
~+/foo => $PWD/foo
# 上一个工作目录
~-/foo => ${$OLDPWD- '~-'}/foo
参数展开(Shell Parameter Expansion) ${}
- 间接参数扩展
${!parameter}, 其中引用的参数并不是 Parameter 而是 Parameter 的实际的值.
parameter="var"
var="hello"
echo $(!parameter) # 第一层展开 变成 ${var}
# 输出 hello
- 参数长度
${#parameter}
example:
par=cd
echo $(#par)
# 输出 2
- 空参数处理 变量为空时 做特殊操作
${parameter:-word}# 为空替换${parameter:=word}# 为空替换,并将值赋给 $parameter 变量${parameter:?word}# 为空报错${parameter:+word}# 不为空替换
example:
a=1
echo ${a:-word} #1 # a 不为空,不替换
echo ${b:-word} #word # b 为空,替换
echo ${par:=word} # word
echo ${par:-hello} # word
echo ${par:+foo} #foo
- 参数切片
${parameter:offset}${parameter:offset:length}
5.参数部分删除
${parameter%word}# 最小限度从后面截取word${parameter%%word}# 最大限度从后面截取word${parameter#word}# 最小限度从前面截取word${parameter##word}# 最大限度从前面截取word
example:
str=abcdefg
sp1=${str##*d}
sp2=${str%%d*}
echo $sp1 # 输出 efg
cho $sp2 # 输出 abc
命令替换 (Command Substitution)
在子进程中执行命令,并用得到结果替换包裹的内容有两种
-
$(...) -
``...
echo $(whoami)
foo(){
echo "asdasd"
}
a=`foo`
数学计算(Arithmetic Expansion) $((...))
使用 $((...)) 包裹数学运算表达式,得到结果并替换
echo $((1+2)) # 3
文件名展开(Filename Expansion)
* ? [..] 外壳文件名模式匹配
当有单词没有被引号包裹,且其中出现了 ‘*’ , '?', and '[' 字符,则 shell 会去按照正则匹配的方式查找文件名进行替换,如果没找到则保持不变。
$ echo D*
# 输出当前目录下所有以 D 字幕开头的目录、文件
04 调试和前端集成
调试
-
普通log, 使用 echo、printf
-
使用 set 命令
-
vscode debug 插件
example for normal log:
a=1
d={1 2 3 4 5}
echo $a # 1
echo ${d[3]} # 4
echo ${d[@]} # 1 2 3 4 5
example for set:
set -uxe -o pipefail
echo "hello world"
| Set 配置 | 作用 | 补充? |
|---|---|---|
| -u | 遇到不存在的变量就会报错,并停止执行。 | -o nounset |
| -x | 运行结果之前,先输出执行的那一行命令。 | -o xtrace |
| -e | 只要发生错误,就终止执行 | -o errexit |
| -o pipefail | 管道符链接的,只要一个子命令失败,整个管道命令就失败, 脚本就会终止执行。 |
VSCode 配置
-
shellman: 代码提示和自动补全
-
shellcheck: 代码语法校验
-
shell-format: 代码格式化
-
Bash debug: 支持单步调试
前端集成
-
node 中通过 exec、spawn 调用 shell 命令
-
shell 脚本中调用 node 命令
-
借助 zx 等库进行 JavaScript、 shell script的融合
-
借助shell 完成系统操作,文件IO、内存、磁盘系统状态查询等
-
借助 nodejs 完成应用层能力,网络io、计算等
exec启用一个子 shell 进程执行传入的命令,并且将执行结果保存在缓冲区中,并且缓冲区是有大小限制的,执行完毕通过回调函数返回。
spawn 默认不使用 shell,而是直接启动子进程执行命令,且会直接返回一个 流对象,支持写入或者读取流数据,这个在大数据量交互的场景比较适合。
总结
shell 的思想和语法和传统的编程语言不太一样, 强调一条语句只干一件事, 所以万物皆命令, 在执行过程中也是逐行、逐个连接符、逐个空格的解析出最小化的命令执行,执行完之后再解析下一句。 我们在了解了 shell 的配置加载、执行方式、执行过程、命令解析过程、 必要的语法、常用命令,则可以方便的写出自己的自动化脚本。