Back to Blogs
bash
shell
linux

Bash

Soloman
2019-07-10

Bash 脚本编程基础

  • Bash 只有一种数据类型,就是字符串。不管用户输入什么数据,Bash 都视为字符串
  • Bash 变量名区分大小写,HOMEhome是两个不同的变量。
# 查看当前使用的 shell
echo $SHELL

# 查看当前的 Linux 系统安装的所有 Shell
cat /etc/shells

# 查看 bash 版本
bash --version

# shopt 命令可以调整 Bash 的行为
# 打开某个参数
shopt -s [optionname]

# 关闭某个参数
shopt -u [optionname]

# 查询某个参数关闭还是打开
shopt [optionname]

# optionname:
# globstar 参数可以使得**匹配零个或多个子目录。该参数默认是关闭的。

1 扩展

  • Bash 一共提供八种扩展。
波浪线扩展:波浪线~会自动扩展成当前用户的主目录。~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 命令

  • 这两种写法建议放在所有 Bash 脚本的头部。
# 写法一
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 官方文档