Shell 编程
Shell 介绍
Shell 原意是 “外壳”,跟 kernel(内核)相对应,比喻内核外面的一层,即用户跟内核交互的对话界面。
首先,Shell 是一个程序,提供一个与用户对话的环境。这个环境只有一个命令提示符,让用户从键盘输入命令,所以又称为命令行环境(command line interface,CLI)。Shell 接收到用户输入的命令,将命令送入操作系统执行,并将结果返回给用户。
其次,Shell 是一个命令解释器,解释用户输入的命令。它支持变量、条件判断、循环操作等语法,所以用户可以用 Shell 命令写出各种小程序,又称为脚本(script)。这些脚本都通过 Shell 的解释执行,而不通过编译。
终端模拟器:terminal emulator,一个模拟命令行窗口的程序,让用户在一个窗口中使用命令行环境,并且提供各种附加功能,比如调整颜色、字体大小、行距等。
不同 Linux 发行版(准确地说是不同的桌面环境)带有的终端程序是不一样的,比如 KDE 桌面环境的终端程序是 konsole,Gnome 桌面环境的终端程序是 gnome-terminal,用户也可以安装第三方的终端程序。
主要的 Shell 有 sh、bash、csh、tcsh、ksh、zsh、fish;Bash 是目前最常用的 Shell。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # 查看 Shell
cat /etc/shells # 查看当前的 Linux 系统安装的所有 Shell
echo $SHELL # 当前设备的默认 Shell
ps # 一般来说,ps 命令结果的倒数第二行是当前 Shell
# 切换默认 Shell
chsh -s /bin/zsh
sudo chsh -s /usr/bin/zsh root
bash # 进入 Shell
exit # 退出 Shell,或 Crtl + D
# Shell 命令格式
# command 具体的命令或可执行文件
# arg1 ... argN 传递给命令的参数,可选
command [ arg1 ... [ argN ]]
# 参数的短长形式作用完全一样,前者便于输入,后者便于理解
-v # 短形式
--verbose # 长形式
|
Shell 快捷键
1
2
3
4
5
6
7
8
9
10
11
12
13
| TAB # 命令补全
Ctrl + C # 中止命令
Ctrl + D # 键盘输入结束,可用于退出 Shell 窗口
Crtl + A # 光标移动到命令首
Crtl + E # 光标移动到命令尾
Alt + B / Ctrl + ← # 光标向左移动一个单词
Alt + F / Ctrl + → # 光标向右移动一个单词
Crtl + W # 删除光标左方的单词
Alt + D # 删除光标右方的单词
Crtl + R # 搜索之前输入过的命令
Crtl + G # 退出历史搜索模式
Crtl + ↓ # 跳转至底部
Crtl + L # 将底部内容移至最上方
|
参考资料
Bash 命令报错时,仍会继续执行后面的代码
工具
1
2
3
4
5
6
7
8
9
10
11
12
13
| # Go Module 镜像/代理
export GOPROXY=https://goproxy.cn,direct
# 安装
go install mvdan.cc/sh/v3/cmd/shfmt@latest
shfmt script.sh # 打印格式化后的内容,不修改文件内容
shfmt -w script.sh # 将格式化后的内容写入文件
# 参数
-i n # 指定缩进
-mn # 启用最小化模式,通常删除不必要的空格和换行符
-ln # 指定方言 bash/posix/mksh/bats
|
1
2
3
4
5
| shellcheck [option] script.sh
# 参数
-s # 指定方言 sh, bash, dash, ksh, busybox
-f # 指定输出格式 checkstyle, diff, gcc, json, json1, quiet, tty
|
1
| npm i -g bash-language-server
|
语法
运行脚本
- 脚本第一行以
#!
字符(称为 Shebang)开头,指定解释器;#!/bin/bash
可写为 #!/usr/bin/env bash
1
2
3
4
5
6
| # 方式 1
bash script.sh # 或 sh script.sh
# 方式 2 赋予可执行权限
chmod +x script.sh # Linux 文件颜色变绿;macOS,变红
./script.sh
|
注释
1
2
3
4
5
6
| # 单行注释
: ' 多行注释
comment 1
comment 2
'
|
打印输出
1
2
3
4
5
| echo # 输出一行空行
# 参数
-n # 不自动换行
-e # 转义字符
|
1
2
| # 输出固定位数
printf "%05d\n" 123 # 输出 5 位数,00123
|
变量
- 定义变量:变量名和等号之间不能有空格
- 使用变量:在变量名前面加美元符号
$
;可在变量名外面添加花括号,帮助解释器识别变量边界
- 删除变量:
unset
- 输出变量:
export
1
2
3
4
| var="letter" # 定义变量
echo $var # 使用变量;或 echo ${var}
unset var # 删除变量
export var=value # 输出变量
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| $0 # 当前 Shell 的名称(在命令行直接执行时)或脚本名(在脚本中执行时)
$n # n 为数字,第 n 个参数
$# # 脚本的参数数量
$? # 上一个命令的退出码(成功返回 0,失败返回非零数值)
$_ # 上一个命令的最后一个参数
$* # 脚本的参数值;将所有参数视为一个整体
$@ # 脚本的参数值;所有参数是独立的
$$ # 当前 Shell 的进程 ID
$! # 最近一个后台执行的异步命令的进程 ID
$- # 当前 Shell 的启动参数
# 示例
mkdir directory && cd $_
# bash test.sh {2..4}
#!/bin/bash
echo "script name: $0"
echo "arg length: $#"
echo "arg1: $1"
echo "arg2: $2"
echo "arg3: $3"
for arg in "$*"; do
echo '$* meaning:' "$arg"
done
for arg in "$@"; do
echo '$@ meaning:' "$arg"
done
|
shift
命令可以改变脚本参数,每次执行都会移除脚本当前的第一个参数,使得后面的参数向前一位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| env # 显示所有环境变量;或 printenv
printenv PATH # 查看单个环境变量的值
echo $PATH
export PATH=$PATH:$HOME/bin # 方式 1
export PATH=$HOME/bin:$PATH # 方式 2
# 常见环境变量
HOME # 用户主目录
HOST # 当前主机名称
PATH # 由冒号分开的目录列表,当输入可执行程序名后,会搜索这个目录列表
RANDOM # 生成 0~32767 之间的随机数
[RANDOM%num] # 生成 0~num 之间的随机数
PWD # 当前工作目录
PS1 # 命令提示符
DISPLAY # 图形环境的显示器名字,通常是 :0,表示 X Server 的第一个显示器
IFS # 内部字段分隔符,Internal Field Separator
|
引号
- 单引号不展开任何内容,全部原样输出
- 双引号会展开变量和命令,特殊字符保留(美元符号、反引号和反斜杠,星号会变成普通字符);保存原始命令的输出格式
1
2
3
4
5
| echo $'it\'s' # 单引号中使用单引号,在最前面加 $
echo "it's" # 在双引号之中使用单引号
echo $(cal) # 单行输出
echo "$(cal)" # 原始格式输出
|
字符串操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| # 获取字符串长度
echo ${#str}
echo ${#str[0]}
## 字符串切片
# 语法;offset 从 0 开始;该语法不能直接操作字符串
${str:offset:length}
# 示例
echo ${str:1:4}
echo ${str:1} # 省略 length,表示到字符串结尾
echo ${str: -4} # 从倒数第 4 个字符开始;负号前须有空格
echo ${str: -4:2}
## 字符串大小写转换
# 将字符串转换为小写
echo "Hello World" | tr '[:upper:]' '[:lower:]'
echo "HELLO WORLD" | awk '{ print tolower($0) }'
# 大写
echo "hello world" | tr '[:lower:]' '[:upper:]'
echo "hello world" | awk '{ print toupper($0) }'
# Bash 4.0 及更高版本中有效
${str,,} # 小写
${str,} # 首字母小写
${str^^} # 大写
${str^} # 首字母大写
|
- 大括号
{}
处理字符串:
- 主要利用 Bash 的参数展开(parameter expansion)功能来实现
- 参考:Bash笔记
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| # 基于模式匹配进行字符串剪裁
var="sample.bk.tar.gz"
# 常用于删除字符串前缀
${var#*.} # 删除字符串开头部分,最短匹配;输出 "bk.tar.gz"
${var##*.} # 删除字符串开头部分,最长匹配;输出 "gz"
# 常用于删除字符串后缀
${var%.*} # 删除字符串末尾部分,最短匹配;输出 "sample.bk.tar"
${var%%.*} # 删除字符串末尾部分,最长匹配;输出 "sample"
# 按字符位置截取字符串
${var:N:M} # 从第 N 个位置开始,截取 M 个字符
# 字符串替换
${var/a/b} # 把变量中的第一个 a 替换成 b
${var//a/b} # 把变量中的所有 a 替换成 b
# 生成字符串列表、序列
echo beg{i,a,u}n # 输出 begin began begun
echo {0..5} # 等价于 seq 0 5
echo {00..8..2} # 00 02 04 06 08
#复制文件夹中的多个文件到当前路径;可结合通配符使用
cp /path/{file1,file2,file3,file4} .
|
算术运算
- 简单数学运算:原生 bash 不支持,可通过
expr
命令实现
1
2
3
4
5
| # 表达式和运算符之间要有空格
val=`expr 2 + 2`
# 乘法运算:须加反斜杠
val=`expr 2 \* 3`
|
- 算术扩展:
(( ))
- 只能计算整数;会自动忽略内部的空格;支持常用运算符;指出逻辑运算符;支持赋值运算
++
和 --
这两个运算符有前缀和后缀的区别。作为前缀是先运算后返回值,作为后缀是先返回值后运算
- 在
$((...))
里面使用字符串,Bash 会认为那是一个变量名
1
2
3
4
5
6
7
8
| # 运算符
a=5; b=10; c=$(( a * b )); echo $c
# 逻辑运算符
if (( a < b )); then echo "$a is less than $b"; fi
# 赋值运算
echo $(( a=1 ))
|
1
2
3
4
5
6
7
8
9
10
| echo "5.01-4*2.0" | bc
awk 'BEGIN { print 7.01*5-4.01 }'
# scale 指定保留小数位数
echo "scale=4; 0.05*0.1" | bc
# 除法,只取整数部分
echo "10/3" | bc
echo "scale=2; 10/3" | bc | cut -d "." -f1
|
1
2
3
4
5
6
| # 整型变量自增
a=1; echo $a
# 方式1
let a++; echo $a
# 方式2
let a+=1; echo $a
|
数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| # 元素用空格分割开
array=(value0 value1) # 写法 1
array=( # 写法 2
value0
value1
)
# 添加元素
array+=(value2)
# 删除元素
unset array[1]
# 获取数组元素
echo ${array[*]} # 所有元素;* 或 @
echo ${array} # 第一个元素
echo ${array[0]} # 第一个元素
# 获取数组长度
echo ${#array[*]} # * 或 @
echo ${#array[0]} # 第一个元素的长度
# 提取数组序号
${!array[*]} # * 或 @
# 切片
${array[@]:position:length}
# 数组合并
array1=(xxx); array2=(xxx)
array_merge=(${array1[*]} ${array2[*]})
|
关联数组:使用字符串而不是整数作为数组索引;可等效为字典
1
2
3
4
5
6
7
8
9
10
11
| declare -A sounds # 创建
sounds[dog]="bark" # 添加键值对
sounds[cow]="moo"
echo "${sounds[dog]}" # 根据键访问值
# 遍历
for key in "${!sounds[@]}"; do
echo "$key: ${sounds[$key]}"
done
|
条件控制
if 条件语句
1
2
3
4
5
6
7
8
9
10
| # 语法
# then 可以另起一行,删除分号
if condtion1; then
commands
elif condition2; then
commands
fi
# 写成一行
if condition; then commands; fi
|
if
结构的判断条件写法:[[]]
是扩展条件判断,相比 []
,支持更多的操作符(如正则表达式匹配)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| test expression # 写法一
[ expression ] # 写法二
[[ expression ]] # 写法三
# 字符串条件
[[ -z STR ]] # 空字符串
[[ -n STR ]] # 非空字符串
[[ STR1 == STR2 ]] # 相等
[[ STR1 = STR2 ]] # 相等(同上)
[[ STR1 =~ STR2 ]] # 正则表达式
# 文件条件
[[ -f FILE ]] # 文件
[[ -d FILE ]] # 目录
[[ -e FILE ]] # 文件/目录是否存在
# 整数条件
[[ NUM1 -eq NUM2 ]] # 等于
[[ NUM1 -lt NUM2 ]] # 小于
[[ NUM1 -gt NUM2 ]] # 大于
|
case 分支语句
case
结构用于多值判断,可用到命令行解析中
1
2
3
4
5
6
7
8
9
10
11
| # 语法
# ;; 可以另起一行
# ) 前后面的内容可以在一行
# *):匹配任意输入,通常作为 case 结构的最后一个模式
case expression in
pattern1)
commands ;;
pattern2)
commands ;;
...
esac
|
循环
for 循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| # 语法
# do 可以另起一行,删除分号
for variable in list; do
command
done
# 示例
for i in 1 2 3 4 5; do
echo $i
done
for i in {1..5}; do
echo $i
done
for i in {1..5..2}; do
echo $i
done
for i in $(seq 1 2 5); do
echo $i
done
# 小数序列
for i in $(seq 1 0.2 2); do
echo $i
done
# C 语言风格
for(( i=1; i<=20; i++ )); do
echo $i
done
# 99 乘法表
for i in {1..9}; do
for j in $(seq $i); do
echo -n -e "$j*$i=$[j*i]\t"
done
echo
done
|
while 循环
1
2
3
4
5
6
7
8
9
| # 语法
while condition; do
command
done
# 读取文件内容的每一行
cat file.txt | while read line; do
echo $line
done
|
函数
- Bash 函数体内直接声明的变量,属于全局变量,整个脚本都可以读取
- 函数里面可以用
local
命令声明局部变量
1
2
3
4
5
6
7
8
9
10
11
12
| # 函数定义语法
# 第一种
fn() {
commands
}
# 第二种
function fn() {
commands
}
fn # 函数调用
|
set 命令
set
命令:用于执行脚本时进行调试和错误处理
-
set -u
:执行脚本时,若遇到不存在的变量,Bash 默认忽略它;在脚本头部加上 set -u
,遇到不存在的变量就会报错,并停止执行
-
set -x
:在运行结果之前,先输出执行的那一行命令
-
set -e
:若脚本中有运行失败的命令,Bash 默认会继续执行后面的命令;在脚本头部加上 set -e
,使得脚本只要发生错误,就终止执行;set +e
表示关闭 -e
选项;不适用于管道命令(set -o pipefail
可解决该问题)
-
set -E
:纠正 set -e
导致的函数内的错误不会被 trap
命令捕获的行为
-
set -n
:不运行命令,只检查语法是否正确
-
set -f
:表示不对通配符进行文件名扩展
-
set -o noclobber
:防止使用重定向运算符 >
覆盖已经存在的文件
1
2
3
4
5
6
7
| set # 显示所有的环境变量和 Shell 函数
# 放在一起使用
set -Eeuxo pipefail # 写法一
set -Eeux # 写法二
set -o pipefail
|
重定向
1
2
3
4
5
6
7
| command > file # 标准输出重定向到文件
command >> file # 标准输出追加到文件
command 2> file # 标准错误重定向到文件
command 2>&1 # 标准错误重定向到标准输出
command 2> /dev/null # 标准错误重定向到空
command &> /dev/null # 标准输出和标准错误同时重定向到空
command < file # 将文件内容作为标准输入
|
- Here 文档、字符串:
- Here 文档:一种输入多行字符串的方法;本质是重定向
- Here 文档内部会发生变量替换,同时支持反斜杠转义,但是不支持通配符扩展,双引号和单引号也失去语法作用,变成了普通字符
- Here 字符串:将字符串通过标准输入,传递给命令
1
2
3
4
5
6
7
8
9
| # 语法 token 一般为 EOF
<< token
text
token
# 语法
<<< string
cat <<< 'hi there' # 等同于 echo 'hi there' | cat
|
其他
- 子命令扩展:
$()
和``;将命令的输出作为返回值
1
2
| echo $(date)
echo `date`
|
1
2
3
4
5
6
7
8
9
10
| # 重复输出等号
printf '==%.0s' {1..20}; printf '\n'
# 定义函数
repeat(){
for i in {1..20}; do echo -n "$1"; done
}
repeat '-'; echo
repeat '='; echo
|
1
2
3
4
5
6
7
8
| COMMANDS=("git" "vi")
for COMMAND in $COMMANDS; do
# if [ -x "$(command -v $COMMAND)" ]; then
if ! command -v "$COMMAND" &> /dev/null; then
echo "Please install $COMMAND";
fi
done
|
1
2
3
4
5
6
7
| # 语法
read [-options] [variable...]
# 示例
echo "What is your name?"
read NAME # 输入
echo "Hello, $NAME"
|
- 操作历史
- 退出当前 Shell 的时候,Bash 会将用户在当前 Shell 的操作历史写入
~/.bash_history
文件
Ctrl + R
快捷键,可以搜索操作历史,选择以前执行过的命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| echo $HISTFILE
history # 输出操作历史
history-c # 清除操作历史
## 环境变量
# 设置 history 输出结果格式
# %F 相当于 %Y - %m - %d(年-月-日)
# %T 相当于 %H : %M : %S(时:分:秒)
export HISTTIMEFORMAT='%F %T '
# 设置保存历史操作的数量
export HISTSIZE=10000
# 设置哪些命令不写入操作历史
export HISTIGNORE='pwd:ls:exit'
!n # n 为行号,执行 .bash_history 文件中的第 n 条命令
!-n # n 为数字,执行倒数第 n 条命令
!! # 执行上一条命令,等同于 !-1
! + 搜索词 # 快速执行匹配的命令;只会匹配命令,不会匹配参数
!:p # 输出上一条命令,而不是执行它
!$ # 上一个命令的最后一个参数,等同于 $_
!* # 上一个命令的所有参数
!:n # 匹配上一个命令的指定位置的参数
|
- 配置项参数终止符
--
:-
和 --
开头的参数,会被 Bash 当作配置项解释;--
的作用是告诉 Bash,在它后面的参数开头的 -
和 --
不是配置项,只能当作实体参数解释
1
2
| cat -- -f
cat -- --file
|
1
2
3
4
5
| set -o noclobber # 防止覆盖已存在的文件;拒绝覆写操作并显示一个错误
echo "xxx" >| file # 强制覆盖
set +o noclobber # 临时关闭
|