Bash 脚本编程基础
- Bash 只有一种数据类型,就是字符串。不管用户输入什么数据,Bash 都视为字符串
- Bash 变量名区分大小写,
HOME和home是两个不同的变量。
# 查看当前使用的 shell
echo $SHELL
# 查看当前的 Linux 系统安装的所有 Shell
cat /etc/shells
# 查看 bash 版本
bash --version
# shopt 命令可以调整 Bash 的行为
# 打开某个参数
shopt -s [optionname]
# 关闭某个参数
shopt -u [optionname]
# 查询某个参数关闭还是打开
shopt [optionname]
# optionname:
# globstar 参数可以使得**匹配零个或多个子目录。该参数默认是关闭的。
1 扩展
波浪线扩展:波浪线~会自动扩展成当前用户的主目录。~user表示扩展成用户user的主目录。~+会扩展成当前所在的目录,等同于pwd命令。
? 字符扩展:?字符代表文件路径里面的任意单个字符,不包括空字符。? 字符扩展属于文件名扩展,只有文件确实存在的前提下,才会发生扩展。如果文件不存在,扩展就不会发生。
ls ??.py
* 字符扩展:*字符代表文件路径里面的任意数量的任意字符,包括零个字符。* 字符扩展属于文件名扩展,只有文件确实存在的前提下才会扩展。如果文件不存在,就会原样输出。
方括号扩展:匹配方括号之中的任意一个字符。方括号扩展属于文件名匹配,即扩展后的结果必须符合现有的文件路径。如果不存在匹配,就会保持原样,不进行扩展。方括号扩展有一个简写形式[start-end],表示匹配一个连续的范围。
大括号扩展:大括号扩展{...}表示分别扩展成大括号里面的所有值,各个值之间使用逗号分隔,大括号扩展有一个简写形式{start..end},表示扩展成一个连续序列。
变量扩展:Bash 将美元符号$开头的词元视为变量,将其扩展成变量值,变量名除了放在美元符号后面,也可以放在${}里面。${!string*}或${!string@}返回所有匹配给定字符串string的变量名。
子命令扩展:$(...)可以扩展成另一个命令的运行结果,该命令的所有输出都会作为返回值。例如 $(date)返回date命令的运行结果。
算术扩展:$((...))可以扩展成整数运算的结果
[[:class:]]表示一个字符类,扩展成某一类特定字符之中的一个。字符类也属于文件名扩展,如果没有匹配的文件名,字符类就会原样输出。
[[:alnum:]]:匹配任意英文字母与数字
[[:alpha:]]:匹配任意英文字母
[[:blank:]]:空格和 Tab 键。
[[:cntrl:]]:ASCII 码 0-31 的不可打印字符。
[[:digit:]]:匹配任意数字 0-9。
[[:graph:]]:A-Z、a-z、0-9 和标点符号。
[[:lower:]]:匹配任意小写字母 a-z。
[[:print:]]:ASCII 码 32-127 的可打印字符。
[[:punct:]]:标点符号(除了 A-Z、a-z、0-9 的可打印字符)。
[[:space:]]:空格、Tab、LF(10)、VT(11)、FF(12)、CR(13)。
[[:upper:]]:匹配任意大写字母 A-Z。
[[:xdigit:]]:16进制字符(A-F、a-f、0-9)。
字符类的第一个方括号后面,可以加上感叹号!,表示否定。比如,[![:digit:]]匹配所有非数字。
量词语法用来控制模式匹配的次数。它只有在 Bash 的extglob参数打开的情况下才能使用
shopt extglob
shopt -s extglob
?(pattern-list):匹配零个或一个模式。
*(pattern-list):匹配零个或多个模式。
+(pattern-list):匹配一个或多个模式。
@(pattern-list):只匹配一个模式。
!(pattern-list):匹配给定模式以外的任何内容。
2 转义字符
# 使用 -e 输出转义字符\t:a b
echo -e "a\tb"
# 三个特殊字符除外:美元符号($)、反引号(`)和反斜杠(\)。这三个字符在双引号之中,依然有特殊含义,会被 Bash 自动扩展。
# 美元符号用来引用变量,反引号则是执行子命令,反斜杠用来转义
# 双引号还有一个作用,就是保存原始命令的输出格式。
echo "$(cal)"
# Here 文档(here document)是一种输入多行字符串的方法
<< name
multi-line text
name
# Here 文档还有一个变体,叫做 Here 字符串(Here string),它的作用是将字符串通过标准输入,传递给命令。
<<< string
3 环境变量
- env命令或printenv命令,可以显示所有环境变量。
env
printenv
# 查看单个环境变量的值,可以使用printenv命令或echo命令。printenv命令后面的变量名,不用加前缀$
printenv PATH
echo $PATH
# $$为当前 Shell 的进程 ID。
echo $$
4 算术运算
- ((...))语法可以进行整数的算术运算。
((...))会自动忽略内部的空格,这个语法不返回值。如果要读取算术运算的结果,需要在((...))前面加上美元符号$((...)),使其变成算术表达式,返回算术运算的值。((...))语法支持的算术运算符如下:
+:加法
-:减法
*:乘法
/:除法(整除),除法运算符的返回结果总是整数
%:余数
**:指数
++:自增运算(前缀或后缀)
--:自减运算(前缀或后缀)
- 前缀是先运算后返回值,作为后缀是先返回值后运算。
- 这个语法只能计算整数,否则会报错。
5 位运算
$((...))支持以下的二进制位运算符:
<<:位左移运算,把一个数字的所有位向左移动指定的位。
>>:位右移运算,把一个数字的所有位向右移动指定的位。
&:位的“与”运算,对两个数字的所有位执行一个AND操作。
|:位的“或”运算,对两个数字的所有位执行一个OR操作。
~:位的“否”运算,对一个数字的所有位取反。
^:位的异或运算(exclusive or),对两个数字的所有位执行一个异或操作。
6 逻辑运算
$((...))支持以下的逻辑运算符。
<:小于
>:大于
<=:小于或相等
>=:大于或相等
==:相等
!=:不相等
&&:逻辑与
||:逻辑或
!:逻辑否
expr1?expr2:expr3:三元条件运算符。若表达式expr1的计算结果为非零值(算术真),则执行表达式expr2,否则执行表达式expr3。
- 如果逻辑表达式为真,返回
1,否则返回0。
7 行操作
7.1 常用快捷键
Ctrl + a:移到行首。
Ctrl + e:移到行尾。
Ctrl + l 快捷键可以清除屏幕,即将当前行移到屏幕的第一行,与clear命令作用相同。
Ctrl + k:剪切光标位置到行尾的文本。
Ctrl + u:剪切光标位置到行首的文本。
Ctrl + y:在光标位置粘贴文本。
Tab:完成自动补全。
7.2 操作历史
- Bash 会保留用户的操作历史,即用户输入的每一条命令都会记录。有了操作历史以后,就可以使用方向键的
↑和↓,快速浏览上一条和下一条命令。
- 退出当前 Shell 的时候,Bash 会将用户在当前 Shell 的操作历史写入
~/.bash_history文件,该文件默认储存500个操作。history命令会输出这个文件的全部内容。用户可以看到最近执行过的所有命令,每条命令之前都有行号。越近的命令,排在越后面。
- 输入命令时,按下
Ctrl + r快捷键,就可以搜索操作历史,选择以前执行过的命令。这时键入命令的开头部分,Shell 就会自动在历史文件中,查询并显示最近一条匹配的结果,这时按下回车键,就会执行那条命令。
- 如果想搜索某个以前执行的命令,可以配合
grep命令搜索操作历史。操作历史的每一条记录都有编号。知道了命令的编号以后,可以用感叹号 + 编号执行该命令。
- 如果希望确定是什么命令,然后再执行,可以打开
histverify选项。这样的话,使用!快捷键所产生的命令,会先打印出来,等到用户按下回车键后再执行。
history | grep /usr/bin
history | grep python3
436 2019-09-07 17:47:04 python3 main.py --l
437 2019-09-07 17:47:25 python3 main.py --r
438 2019-09-07 17:47:29 python3 test.py --l
441 2019-09-07 17:54:11 python3 test.py --r
449 2019-09-07 17:55:23 python3 spider.py --r
451 2019-09-07 17:55:55 python3 spider.py --k
455 2019-09-07 17:57:30 python3 index.py --k
456 2019-09-07 17:57:34 python3 index.py --r
462 2019-09-07 18:38:56 python3 index.py --r
# 执行历史记录中编号为449的命令,即 python3 spider.py --r
!449
shopt -s histverify
8 目录堆栈
# Bash 可以记忆用户进入过的目录。默认情况下,只记忆前一次所在的目录,cd - 命令可以返回前一次的目录。
cd -
# 如果希望记忆多重目录,可以使用pushd命令和popd命令。它们用来操作目录堆栈。
pushd /home/me/etc
popd
# dirs 命令可以显示目录堆栈的内容,一般用来查看pushd和popd操作后的结果。它有以下参数:
-c:清空目录栈。
-l:用户主目录不显示波浪号前缀,而打印完整的目录。
-p:每行一个条目打印目录栈,默认是打印在一行。
-v:每行一个条目,每个条目之前显示位置编号(从0开始)。
+N:N为整数,表示显示堆顶算起的第 N 个目录,从零开始。
-N:N为整数,表示显示堆底算起的第 N 个目录,从零开始。
dirs -l
9 Shebang 行
- 脚本的第一行通常是指定解释器,即这个脚本必须通过什么解释器执行。这一行以#!字符开头,这个字符称为 Shebang
#!/bin/sh
# 或者
#!/bin/bash
# 或者
#!/usr/bin/env bash
10 env 命令
env命令总是指向/usr/bin/env文件,#!/usr/bin/env NAME这个语法的意思是,让 Shell 查找$PATH环境变量里面第一个匹配的NAME。
# Node.js 脚本的 Shebang 行,可以写成下面这样。
#!/usr/bin/env node
# Python 脚本的 Shebang 行,可以写成下面这样。
#!/usr/bin/env python3
11 条件与循环
# 只有符合给定条件时,才会执行指定的命令。
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi
# if 结构的判断条件,一般使用test命令,有三种形式。
# 写法一
test expression
# 写法二
[ expression ]
# 写法三
[[ expression ]]
# case 结构用于多值判断,可以为每个值指定对应的命令,跟包含多个elif的if结构等价
case expression in
pattern )
commands ;;
pattern )
commands ;;
...
esac
# 只要满足条件condition,就会执行命令commands,只有不满足条件,才会退出循环。
while condition; do
commands
done
while condition
do
commands
done
# until 循环与while循环恰好相反,只要不符合判断条件(判断条件失败),就不断循环执行指定的语句。一旦符合判断条件,就退出循环。
until condition; do
commands
done
until condition
do
commands
done
# for...in 循环用于遍历列表的每一项。
for variable in list; do
commands
done
for variable in list
do
commands
done
# expression1用来初始化循环条件,expression2用来决定循环结束的条件,expression3在每次循环迭代的末尾执行,用于更新值。
for (( expression1; expression2; expression3 )); do
commands
done
# 它等同于下面的while循环。
(( expression1 ))
while (( expression2 )); do
commands
(( expression3 ))
done
# select结构主要用来生成简单的菜单。它的语法与for...in循环基本一致。
# 1.select生成一个菜单,内容是列表list的每一项,并且每一项前面还有一个数字编号。
# 2.Bash 提示用户选择一项,输入它的编号。
# 3.用户输入以后,Bash 会将该项的内容存在变量name,该项的编号存入环境变量REPLY。如果用户没有输入,就按回车键,Bash 会重新输出菜单,让用户选择。
# 4.执行命令体commands。
# 5.执行结束后,回到第一步,重复这个过程。
select name
[in list]
do
commands
done
12 函数
- 函数总是在当前 Shell 执行,这是跟脚本的一个重大区别,Bash 会新建一个子 Shell 执行脚本。当别名、函数与脚本同名时,执行优先级为:别名>函数>脚本
# 函数定义:fn 是自定义的函数名,函数代码就写在大括号之中
# 第一种
fn() {
# codes
}
# 第二种
function fn() {
# codes
}
# 删除一个函数,可以使用unset命令。
unset -f functionName
# 查看当前 Shell 已经定义的所有函数,可以使用declare命令。包含函数体。
declare -f functionName
# declare -F可以输出所有已经定义的函数名,不含函数体。
declare -F
12.1 参数变量
- 函数体内可以使用参数变量,获取函数参数。函数的参数变量,与脚本参数变量是一致的。
$1~$9:函数的第一个到第9个的参数。
$0:函数所在的脚本名。
$#:函数的参数总数。
$@:函数的全部参数,参数之间使用空格分隔。
$*:函数的全部参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。
- 如果函数的参数多于9个,那么第10个参数可以用
${10}的形式引用,以此类推。
- Bash 函数体内直接声明的变量,属于全局变量,整个脚本都可以读取。这一点需要特别小心。
- 函数体内不仅可以声明全局变量,还可以修改全局变量。
- 函数里面可以用
local命令声明局部变量。
13 命令
13.1 set 命令
# 写法一
set -Eeuxo pipefail
# 写法二
set -Eeux
set -o pipefail
13.2 shopt 命令
shopt命令用来调整 Shell 的参数,跟set命令的作用很类似。
# 直接输入shopt可以查看所有参数,以及它们各自打开和关闭的状态。
shopt
# shopt命令后面跟着参数名,可以查询该参数是否打开。
shopt globstar
# -s用来打开某个参数。
shopt -s optionNameHere
# -u用来关闭某个参数。
$ shopt -u optionNameHere
# -q的作用也是查询某个参数是否打开,但不是直接输出查询结果,而是通过命令的执行状态($?)表示查询结果。
# 如果状态为0,表示该参数打开;如果为1,表示该参数关闭。这个用法主要用于脚本,供if条件结构使用。
# 下面例子是如果打开了这个参数,就执行if结构内部的语句。
if !(shopt -q globstar); then
...
fi
13.3 mktemp 命令
mktemp命令就是为安全创建临时文件而设计的。虽然在创建临时文件之前,它不会检查临时文件是否存在,但是它支持唯一文件名和清除机制,因此可以减轻安全攻击的风险。
# 直接运行mktemp命令,就能生成一个临时文件。
mktemp
# 为了确保临时文件创建成功,mktemp命令后面最好使用 OR 运算符(||),保证创建失败时退出脚本。
TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"
# -d 参数可以创建一个临时目录。
mktemp -d
# -p 参数可以指定临时文件所在的目录。默认是使用$TMPDIR环境变量指定的目录,如果这个变量没设置,那么使用/tmp目录。
mktemp -p /home/soloman123/
# -t 参数可以指定临时文件的文件名模板,模板的末尾必须至少包含三个连续的X字符,表示随机字符。
# 建议至少使用六个X。默认的文件名模板是tmp.后接十个随机字符。
mktemp -t mytemp.XXXXXXX
13.4 trap 命令
trap命令用来在 Bash 脚本中响应系统信号。“动作”是一个 Bash 命令,“信号”常用的有以下几个。
- HUP:编号1,脚本与所在的终端脱离联系。
- INT:编号2,用户按下 Ctrl + C,意图让脚本终止运行。
- QUIT:编号3,用户按下 Ctrl + 斜杠,意图退出脚本。
- KILL:编号9,该信号用于杀死进程。
- TERM:编号15,这是
kill命令发出的默认信号。
- EXIT:编号0,这不是系统信号,而是 Bash 脚本特有的信号,不管什么情况,只要退出脚本就会产生。
- 注意,
trap命令必须放在脚本的开头。否则,它上方的任何命令导致脚本退出,都不会被它捕获。
# trap [动作] [信号1] [信号2] ...
# 脚本遇到EXIT信号时,就会执行rm -f "$TMPFILE"。
trap 'rm -f "$TMPFILE"' EXIT
# trap命令的-l参数,可以列出所有的系统信号。
trap -l
# 如果trap需要触发多条命令,可以封装一个 Bash 函数。
function egress {
command1
command2
command3
}
trap egress EXIT
14 参考文档
Bash 官方文档