Shell 编程 | 青训营笔记

180 阅读6分钟

Shell 编程

在使用 Linux 过程中, 必不可少接触到的就是 Shell。 作为没有任何图形化界面的 Terminal 来说,有alias 和 shell script 是提升终端和 Linux 必不可少的操作之一。因为终端在一定情况下也是有些繁琐,所以类似于“快捷方式”的 shell 脚本是必不可少的。

例如,在KDE 和 Gnome 上的桌面快捷方式,其实就是一个shell script 和 一个 icon 组成的哦。

下面我们就这个课程的笔记整理来简单理解和学习下 Shell 吧!


Shell 的价值:

  1. Linux 服务器的基本操作和管理

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

  3. 使用 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-constitution.png

Shell 不仅提供了与内核和设备交互的方法,还集成了一些今天开发中通用的设计模式(例如 pipe 和 filter),具备控制流程、循环、变量、命令查找的机制、

既是命令解释器,也是变成语言。作为命令解释器,提供给用户接口,使用丰富的GNU工具集,或第三方或内置。例如cd pwd exec test netstat etc.

02 Shell 语法和命令

变量:

类型作用域声明方式规范
自定义变量当前 Shell=String、integer、Float、Date
环境变量当前以及子 shellexport、declare、-x
系统环境变量所有 Shell启动加载

父子Shell:

parent-shell.png

自定义变量

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-config-load.png

  • 通过系统用户登录默认运行的shell

  • 非登录交互式运行shell

  • 执行脚本运行非交互式shell

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

Shell Operator & quotes

shell-operator.png

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-redirection.png

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

  • 2>&1 必须写在 > 之后

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

    实例: 用户在命令行输入内容,当输入 EOF 的时候停止, 所输入的内容写入 foo.txt

判断命令

shell-judgement-cmd.png 根据程序是否正常执行(程序退出的状态)进行判断
exit:手动退出 shell 的命令
exit 10 返回 10 给 shell,返回值非 0 为不正常退出
$? 用于判读昂当前 shell 前一个进程是否正常退出(非 0 为不正常退出)

exmaple:

judgement-exam1.png

name="hello world"

[ $name=="hello" ]

# [ : too many arguments
# Exited with error status 2

分支语句:

shell-if-statements.png

循环:

shell-loop.png

函数:

shell-func.png

模块化:

shell-modules.png

常用命令:

shell-normal-cmd.png

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) ${}

  1. 间接参数扩展 ${!parameter}, 其中引用的参数并不是 Parameter 而是 Parameter 的实际的值.
   parameter="var"
   var="hello"
   echo $(!parameter)      # 第一层展开 变成 ${var}
   # 输出 hello
  1. 参数长度
  • ${#parameter}

example:

par=cd
echo $(#par)
# 输出 2
  1. 空参数处理 变量为空时 做特殊操作
  • ${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
  1. 参数切片
  • ${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 调试和前端集成

调试

  1. 普通log, 使用 echo、printf

  2. 使用 set 命令

  3. 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 配置

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

  2. shellcheck: 代码语法校验

  3. shell-format: 代码格式化

  4. Bash debug: 支持单步调试

前端集成

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

  2. shell 脚本中调用 node 命令

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

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

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

exec启用一个子 shell 进程执行传入的命令,并且将执行结果保存在缓冲区中,并且缓冲区是有大小限制的,执行完毕通过回调函数返回。

spawn 默认不使用 shell,而是直接启动子进程执行命令,且会直接返回一个 流对象,支持写入或者读取流数据,这个在大数据量交互的场景比较适合。


总结

shell 的思想和语法和传统的编程语言不太一样, 强调一条语句只干一件事, 所以万物皆命令, 在执行过程中也是逐行、逐个连接符、逐个空格的解析出最小化的命令执行,执行完之后再解析下一句。 我们在了解了 shell 的配置加载、执行方式、执行过程、命令解析过程、 必要的语法、常用命令,则可以方便的写出自己的自动化脚本。

shell-roadmap.png