Bash shell 基础

摘抄自廖雪峰

变量

环境变量

env命令或printenv命令,可以显示所有环境变量。

$ env
# 或者
$ printenv

下面是一些常见的环境变量。

  • BASHPID:Bash 进程的进程 ID。
  • BASHOPTS:当前 Shell 的参数,可以用shopt命令修改。
  • DISPLAY:图形环境的显示器名字,通常是:0,表示 X Server 的第一个显示器。
  • EDITOR:默认的文本编辑器。
  • HOME:用户的主目录。
  • HOST:当前主机的名称。
  • IFS:词与词之间的分隔符,默认为空格。
  • LANG:字符集以及语言编码,比如zh_CN.UTF-8
  • PATH:由冒号分开的目录列表,当输入可执行程序名后,会搜索这个目录列表。
  • PS1:Shell 提示符。
  • PS2: 输入多行命令时,次要的 Shell 提示符。
  • PWD:当前工作目录。
  • RANDOM:返回一个0到32767之间的随机数。
  • SHELL:Shell 的名字。
  • SHELLOPTS:启动当前 Shell 的set命令的参数。
  • TERM:终端类型名,即终端仿真器所用的协议。
  • UID:当前用户的 ID 编号。
  • USER:当前用户的用户名。

很多环境变量很少发生变化,而且是只读的,可以视为常量。由于它们的变量名全部都是大写,所以传统上,如果用户要自己定义一个常量,也会使用全部大写的变量名。

查看单个环境变量的值,可以使用printenv命令或echo命令。

$ printenv PATH
# 或者
$ echo $PATH

注意,printenv命令后面的变量名,不用加前缀$

自定义变量

自定义变量是用户在当前 Shell 里面自己定义的变量,仅在当前 Shell 可用。一旦退出当前 Shell,该变量就不存在了。

set命令可以显示所有变量(包括环境变量和自定义变量),以及所有的 Bash 函数。

$ set

创建变量

用户创建变量的时候,变量名必须遵守下面的规则。

  • 字母、数字和下划线字符组成。
  • 第一个字符必须是一个字母或一个下划线,不能是数字。
  • 不允许出现空格和标点符号。

变量声明的语法如下。

variable=value

上面命令中,等号左边是变量名,右边是变量。注意,等号两边不能有空格。(于 python 不同,= 两边有空格)

如果变量的值包含空格,则必须将值放在引号中。

myvar="hello world"

Bash 没有数据类型的概念,所有的变量值都是字符串。

下面是一些自定义变量的例子。

a=z                     # 变量 a 赋值为字符串 z
b="a string"            # 变量值包含空格,就必须放在引号里面
c="a string and $b"     # 变量值可以引用其他变量的值
d="\t\ta string\n"      # 变量值可以使用转义字符
e=$(ls -l foo.txt)      # 变量值可以是命令的执行结果
f=$((5 * 7))            # 变量值可以是数学运算的结果

变量可以重复赋值,后面的赋值会覆盖前面的赋值。

$ foo=1
$ foo=2
$ echo $foo
2

上面例子中,变量foo的第二次赋值会覆盖第一次赋值。

如果同一行定义多个变量,必须使用分号(;)分隔。

$ foo=1;bar=2

上面例子中,同一行定义了foobar两个变量。

读取变量

读取变量的时候,直接在变量名前加上$就可以了。

$ foo=bar
$ echo $foo
bar

每当 Shell 看到以$开头的单词时,就会尝试读取这个变量名对应的值。

如果变量不存在,Bash 不会报错,而会输出空字符。

读取变量的时候,变量名也可以使用花括号{}包围,比如$a也可以写成${a}。这种写法可以用于变量名与其他字符连用的情况。

$ a=foo
$ echo $a_file

$ echo ${a}_file
foo_file

上面代码中,变量名a_file不会有任何输出,因为 Bash 将其整个解释为变量,而这个变量是不存在的。只有用花括号区分$a,Bash 才能正确解读。

事实上,读取变量的语法$foo,可以看作是${foo}的简写形式。

如果变量的值本身也是变量,可以使用${!varname}的语法,读取最终的值。

$ myvar=USER
$ echo ${!myvar}
ruanyf

上面的例子中,变量myvar的值是USER${!myvar}的写法将其展开成最终的值。

如果变量值包含连续空格(或制表符和换行符),最好放在双引号里面读取。

$ a="1 2  3"
$ echo $a
1 2 3
$ echo "$a"
1 2  3

上面示例中,变量a的值包含两个连续空格。如果直接读取,Shell 会将连续空格合并成一个。只有放在双引号里面读取,才能保持原来的格式。

删除变量

unset命令用来删除一个变量。

unset NAME

这个命令不是很有用。因为不存在的 Bash 变量一律等于空字符串,所以即使unset命令删除了变量,还是可以读取这个变量,值为空字符串。

所以,删除一个变量,也可以将这个变量设成空字符串。

$ foo=''
$ foo=

上面两种写法,都是删除了变量foo。由于不存在的值默认为空字符串,所以后一种写法可以在等号右边不写任何值。

输出变量,export 命令

用户创建的变量仅可用于当前 Shell,子 Shell 默认读取不到父 Shell 定义的变量。为了把变量传递给子 Shell,需要使用export命令。这样输出的变量,对于子 Shell 来说就是环境变量。

export命令用来向子 Shell 输出变量。

NAME=foo
export NAME

上面命令输出了变量NAME。变量的赋值和输出也可以在一个步骤中完成。

export NAME=value

上面命令执行后,当前 Shell 及随后新建的子 Shell,都可以读取变量$NAME

子 Shell 如果修改继承的变量,不会影响父 Shell。

# 输出变量 $foo
$ export foo=bar

# 新建子 Shell
$ bash

# 读取 $foo
$ echo $foo
bar

# 修改继承的变量
$ foo=baz

# 退出子 Shell
$ exit

# 读取 $foo
$ echo $foo
bar

上面例子中,子 Shell 修改了继承的变量$foo,对父 Shell 没有影响。

特殊变量

Bash 提供一些特殊变量。这些变量的值由 Shell 提供,用户不能进行赋值。

(1)$?

$?为上一个命令的退出码,用来判断上一个命令是否执行成功。返回值是0,表示上一个命令执行成功;如果不是零,表示上一个命令执行失败。

$ ls doesnotexist
ls: doesnotexist: No such file or directory

$ echo $?
1

上面例子中,ls命令查看一个不存在的文件,导致报错。$?为1,表示上一个命令执行失败。

(2)$$

$$为当前 Shell 的进程 ID。

$ echo $$
10662

这个特殊变量可以用来命名临时文件。

LOGFILE=/tmp/output_log.$$

(3)$_

$_为上一个命令的最后一个参数。

$ grep dictionary /usr/share/dict/words
dictionary

$ echo $_
/usr/share/dict/words

(4)$!

$!为最近一个后台执行的异步命令的进程 ID。

$ firefox &
[1] 11064

$ echo $!
11064

上面例子中,firefox是后台运行的命令,$!返回该命令的进程 ID。

(5)$0

$0为当前 Shell 的名称(在命令行直接执行时)或者脚本名(在脚本中执行时)。

$ echo $0
bash

上面例子中,$0返回当前运行的是 Bash。

(6)$-

$-为当前 Shell 的启动参数。

$ echo $-
himBHs

(7)$@$#

$#表示脚本的参数数量,$@表示脚本的参数值,参见脚本一章。

变量的默认值

Bash 提供四个特殊语法,跟变量的默认值有关,目的是保证变量不为空。

${varname:-word}

上面语法的含义是,如果变量varname存在且不为空,则返回它的值,否则返回word。它的目的是返回一个默认值,比如${count:-0}表示变量count不存在时返回0

${varname:=word}

上面语法的含义是,如果变量varname存在且不为空,则返回它的值,否则将它设为word,并且返回word。它的目的是设置变量的默认值,比如${count:=0}表示变量count不存在时返回0,且将count设为0

${varname:+word}

上面语法的含义是,如果变量名存在且不为空,则返回word,否则返回空值。它的目的是测试变量是否存在,比如${count:+1}表示变量count存在时返回1(表示true),否则返回空值。

${varname:?message}

上面语法的含义是,如果变量varname存在且不为空,则返回它的值,否则打印出varname: message,并中断脚本的执行。如果省略了message,则输出默认的信息“parameter null or not set.”。它的目的是防止变量未定义,比如${count:?"undefined!"}表示变量count未定义时就中断执行,抛出错误,返回给定的报错信息undefined!

上面四种语法如果用在脚本中,变量名的部分可以用数字19,表示脚本的参数。

filename=${1:?"filename missing."}

上面代码出现在脚本中,1表示脚本的第一个参数。如果该参数不存在,就退出脚本并报错。

declare 命令

declare命令可以声明一些特殊类型的变量,为变量设置一些限制,比如声明只读类型的变量和整数类型的变量。

它的语法形式如下。

declare OPTION VARIABLE=value

declare命令的主要参数(OPTION)如下。

  • -a:声明数组变量。
  • -f:输出所有函数定义。
  • -F:输出所有函数名。
  • -i:声明整数变量。
  • -l:声明变量为小写字母。
  • -p:查看变量信息。
  • -r:声明只读变量。
  • -u:声明变量为大写字母。
  • -x:该变量输出为环境变量。

declare命令如果用在函数中,声明的变量只在函数内部有效,等同于local命令。

不带任何参数时,declare命令输出当前环境的所有变量,包括函数在内,等同于不带有任何参数的set命令。

$ declare

(1)-i参数

-i参数声明整数变量以后,可以直接进行数学运算。

$ declare -i val1=12 val2=5
$ declare -i result
$ result=val1*val2
$ echo $result
60

上面例子中,如果变量result不声明为整数,val1*val2会被当作字面量,不会进行整数运算。另外,val1val2其实不需要声明为整数,因为只要result声明为整数,它的赋值就会自动解释为整数运算。

注意,一个变量声明为整数以后,依然可以被改写为字符串。

$ declare -i var=12
$ var=foo
$ echo $var
0

上面例子中,变量var声明为整数,覆盖以后,Bash 不会报错,但会赋以不确定的值,上面的例子中可能输出0,也可能输出的是3。

(2)-x参数

-x参数等同于export命令,可以输出一个变量为子 Shell 的环境变量。

$ declare -x foo
# 等同于
$ export foo

(3)-r参数

-r参数可以声明只读变量,无法改变变量值,也不能unset变量。

$ declare -r bar=1

$ bar=2
bash: bar:只读变量
$ echo $?
1

$ unset bar
bash: bar:只读变量
$ echo $?
1

上面例子中,后两个赋值语句都会报错,命令执行失败。

(4)-u参数

-u参数声明变量为大写字母,可以自动把变量值转成大写字母。

$ declare -u foo
$ foo=upper
$ echo $foo
UPPER

(5)-l参数

-l参数声明变量为小写字母,可以自动把变量值转成小写字母。

$ declare -l bar
$ bar=LOWER
$ echo $bar
lower

(6)-p参数

-p参数输出变量信息。

$ foo=hello
$ declare -p foo
declare -- foo="hello"
$ declare -p bar
bar:未找到

上面例子中,declare -p可以输出已定义变量的值,对于未定义的变量,会提示找不到。

如果不提供变量名,declare -p输出所有变量的信息。

$ declare -p

(7)-f参数

-f参数输出当前环境的所有函数,包括它的定义。

$ declare -f

(8)-F参数

-F参数输出当前环境的所有函数名,不包含函数定义。

$ declare -F

readonly 命令

readonly命令等同于declare -r,用来声明只读变量,不能改变变量值,也不能unset变量。

$ readonly foo=1
$ foo=2
bash: foo:只读变量
$ echo $?
1

上面例子中,更改只读变量foo会报错,命令执行失败。

readonly命令有三个参数。

  • -f:声明的变量为函数名。
  • -p:打印出所有的只读变量。
  • -a:声明的变量为数组。

Bash 启动环境

Session

用户每次使用 Shell,都会开启一个与 Shell 的 Session(对话)。

Session 有两种类型:登录 Session 和非登录 Session,也可以叫做 login shell 和 non-login shell。

登录 Session

登录 Session 是用户登录系统以后,系统为用户开启的原始 Session,通常需要用户输入用户名和密码进行登录。

登录 Session 一般进行整个系统环境的初始化,启动的初始化脚本依次如下。

  • /etc/profile:所有用户的全局配置脚本。
  • /etc/profile.d目录里面所有.sh文件
  • ~/.bash_profile:用户的个人配置脚本。如果该脚本存在,则执行完就不再往下执行。
  • ~/.bash_login:如果~/.bash_profile没找到,则尝试执行这个脚本(C shell 的初始化脚本)。如果该脚本存在,则执行完就不再往下执行。
  • ~/.profile:如果~/.bash_profile~/.bash_login都没找到,则尝试读取这个脚本(Bourne shell 和 Korn shell 的初始化脚本)。

Linux 发行版更新的时候,会更新/etc里面的文件,比如/etc/profile,因此不要直接修改这个文件。如果想修改所有用户的登陆环境,就在/etc/profile.d目录里面新建.sh脚本。

如果想修改你个人的登录环境,一般是写在~/.bash_profile里面。下面是一个典型的.bash_profile文件。

# .bash_profile
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin
PATH=$PATH:$HOME/bin

SHELL=/bin/bash
MANPATH=/usr/man:/usr/X11/man
EDITOR=/usr/bin/vi
PS1='\h:\w\$ '
PS2='> '

if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi

export PATH
export EDITOR

可以看到,这个脚本定义了一些最基本的环境变量,然后执行了~/.bashrc

bash命令的--login参数,会强制执行登录 Session 会执行的脚本。

$ bash --login

bash命令的--noprofile参数,会跳过上面这些 Profile 脚本。

$ bash --noprofile

非登录 Session

非登录 Session 是用户进入系统以后,手动新建的 Session,这时不会进行环境初始化。比如,在命令行执行bash命令,就会新建一个非登录 Session。

非登录 Session 的初始化脚本依次如下。

  • /etc/bash.bashrc:对全体用户有效。
  • ~/.bashrc:仅对当前用户有效。

对用户来说,~/.bashrc通常是最重要的脚本。非登录 Session 默认会执行它,而登录 Session 一般也会通过调用执行它。每次新建一个 Bash 窗口,就相当于新建一个非登录 Session,所以~/.bashrc每次都会执行。注意,执行脚本相当于新建一个非互动的 Bash 环境,但是这种情况不会调用~/.bashrc

bash命令的--norc参数,可以禁止在非登录 Session 执行~/.bashrc脚本。

$ bash --norc

bash命令的--rcfile参数,指定另一个脚本代替.bashrc

$ bash --rcfile testrc

.bash_logout

~/.bash_logout脚本在每次退出 Session 时执行,通常用来做一些清理工作和记录工作,比如删除临时文件,记录用户在本次 Session 花费的时间。

如果没有退出时要执行的命令,这个文件也可以不存在。

启动选项

为了方便 Debug,有时在启动 Bash 的时候,可以加上启动参数。

  • -n:不运行脚本,只检查是否有语法错误。
  • -v:输出每一行语句运行结果前,会先输出该行语句。
  • -x:每一个命令处理之前,先输出该命令,再执行该命令。
$ bash -n scriptname
$ bash -v scriptname
$ bash -x scriptname

键盘绑定

Bash 允许用户定义自己的快捷键。全局的键盘绑定文件默认为/etc/inputrc,你可以在主目录创建自己的键盘绑定文件.inputrc文件。如果定义了这个文件,需要在其中加入下面这行,保证全局绑定不会被遗漏。

$include /etc/inputrc

.inputrc文件里面的快捷键,可以像这样定义,"\C-t":"pwd\n"表示将Ctrl + t绑定为运行pwd命令。

模式扩展

Shell 接收到用户输入的命令以后,会根据空格将用户的输入,拆分成一个个词元(token)。然后,Shell 会扩展词元里面的特殊字符,扩展完成后才会调用相应的命令。

这种特殊字符的扩展,称为模式扩展(globbing)。其中有些用到通配符,又称为通配符扩展(wildcard expansion)。Bash 一共提供八种扩展。

  • 波浪线扩展
  • ? 字符扩展
  • * 字符扩展
  • 方括号扩展
  • 大括号扩展
  • 变量扩展
  • 子命令扩展
  • 算术扩展

本章介绍这八种扩展。

Bash 是先进行扩展,再执行命令。因此,扩展的结果是由 Bash 负责的,与所要执行的命令无关。命令本身并不存在参数扩展,收到什么参数就原样执行。这一点务必需要记住。

模块扩展的英文单词是globbing,这个词来自于早期的 Unix 系统有一个/etc/glob文件,保存扩展的模板。后来 Bash 内置了这个功能,但是这个名字就保留了下来。

模式扩展与正则表达式的关系是,模式扩展早于正则表达式出现,可以看作是原始的正则表达式。它的功能没有正则那么强大灵活,但是优点是简单和方便。

Bash 允许用户关闭扩展。

$ set -o noglob
# 或者
$ set -f

下面的命令可以重新打开扩展。

$ set +o noglob
# 或者
$ set +f

波浪线扩展

波浪线~会自动扩展成当前用户的主目录。

$ echo ~
/home/me

~/dir表示扩展成主目录的某个子目录,dir是主目录里面的一个子目录名。

# 进入 /home/me/foo 目录
$ cd ~/foo

~user表示扩展成用户user的主目录。

$ echo ~foo
/home/foo

$ echo ~root
/root

上面例子中,Bash 会根据波浪号后面的用户名,返回该用户的主目录。

如果~useruser是不存在的用户名,则波浪号扩展不起作用。

$ echo ~nonExistedUser
~nonExistedUser

? 字符扩展

?字符代表文件路径里面的任意单个字符,不包括空字符。比如,Data???匹配所有Data后面跟着三个字符的文件名。

# 存在文件 a.txt 和 b.txt
$ ls ?.txt
a.txt b.txt

* 字符扩展

*字符代表文件路径里面的任意数量的任意字符,包括零个字符

# 存在文件 a.txt、b.txt 和 ab.txt
$ ls *.txt
a.txt b.txt ab.txt

上面例子中,*.txt代表后缀名为.txt的所有文件。

方括号扩展

方括号扩展的形式是[...],只有文件确实存在的前提下才会扩展。如果文件不存在,就会原样输出。括号之中的任意一个字符。比如,[aeiou]可以匹配五个元音字母中的任意一个。

# 存在文件 a.txt 和 b.txt
$ ls [ab].txt
a.txt b.txt

# 只存在文件 a.txt
$ ls [ab].txt
a.txt

上面例子中,[ab]可以匹配ab,前提是确实存在相应的文件。

[start-end] 扩展

方括号扩展有一个简写形式[start-end],表示匹配一个连续的范围。比如,[a-c]等同于[abc][0-9]匹配[0123456789]

# 存在文件 a.txt、b.txt 和 c.txt
$ ls [a-c].txt
a.txt
b.txt
c.txt

# 存在文件 report1.txt、report2.txt 和 report3.txt
$ ls report[0-9].txt
report1.txt
report2.txt
report3.txt
...

下面是一些常用简写的例子。

  • [a-z]:所有小写字母。
  • [a-zA-Z]:所有小写字母与大写字母。
  • [a-zA-Z0-9]:所有小写字母、大写字母与数字。
  • [abc]*:所有以abc字符之一开头的文件名。
  • program.[co]:文件program.c与文件program.o
  • BACKUP.[0-9][0-9][0-9]:所有以BACKUP.开头,后面是三个数字的文件名。

这种简写形式有一个否定形式[!start-end],表示匹配不属于这个范围的字符。比如,[!a-zA-Z]表示匹配非英文字母的字符。

$ ls report[!1–3].txt
report4.txt report5.txt

上面代码中,[!1-3]表示排除1、2和3。

大括号扩展

大括号扩展{...}表示分别扩展成大括号里面的所有值,各个值之间使用逗号分隔。比如,{1,2,3}扩展成1 2 3

$ echo {1,2,3}
1 2 3

$ echo d{a,e,i,u,o}g
dag deg dig dug dog

$ echo Front-{A,B,C}-Back
Front-A-Back Front-B-Back Front-C-Back

注意,大括号扩展不是文件名扩展。它会扩展成所有给定的值,而不管是否有对应的文件存在。

$ ls {a,b,c}.txt
ls: 无法访问'a.txt': 没有那个文件或目录
ls: 无法访问'b.txt': 没有那个文件或目录
ls: 无法访问'c.txt': 没有那个文件或目录

上面例子中,即使不存在对应的文件,{a,b,c}依然扩展成三个文件名,导致ls命令报了三个错误。

另一个需要注意的地方是,大括号内部的逗号前后不能有空格。否则,大括号扩展会失效。

$ echo {1 , 2}
{1 , 2}

上面例子中,逗号前后有空格,Bash 就会认为这不是大括号扩展,而是三个独立的参数。

逗号前面可以没有值,表示扩展的第一项为空。

$ cp a.log{,.bak}

# 等同于
# cp a.log a.log.bak

{start..end} 扩展

大括号扩展有一个简写形式{start..end},表示扩展成一个连续序列。比如,{a..z}可以扩展成26个小写英文字母。

$ echo {a..c}
a b c

$ echo d{a..d}g
dag dbg dcg ddg

$ echo {1..4}
1 2 3 4

这个写法的另一个常见用途,是直接用于for循环。

for i in {1..4}
do
  echo $i
done

上面例子会循环4次。

这种简写形式还可以使用第二个双点号(start..end..step),用来指定扩展的步长。

$ echo {0..8..2}
0 2 4 6 8

上面代码将0扩展到8,每次递增的长度为2,所以一共输出5个数字。

多个简写形式连用,会有循环处理的效果。

$ echo {a..c}{1..3}
a1 a2 a3 b1 b2 b3 c1 c2 c3

变量扩展

Bash 将美元符号$开头的词元视为变量,将其扩展成变量值。

$ echo $SHELL
/bin/bash

变量名除了放在美元符号后面,也可以放在${}里面。

$ echo ${SHELL}
/bin/bash

${!string*}${!string@}返回所有匹配给定字符串string的变量名。

$ echo ${!S*}
SECONDS SHELL SHELLOPTS SHLVL SSH_AGENT_PID SSH_AUTH_SOCK

上面例子中,${!S*}扩展成所有以S开头的变量名。

子命令扩展

$(...)可以扩展成另一个命令的运行结果,该命令的所有输出都会作为返回值。

$ echo $(date)
Tue Jan 28 00:01:13 CST 2020

上面例子中,$(date)返回date命令的运行结果。

还有另一种较老的语法,子命令放在反引号之中,也可以扩展成命令的运行结果。

$ echo `date`
Tue Jan 28 00:01:13 CST 2020

$(...)可以嵌套,比如$(ls $(pwd))

算术扩展

$((...))可以扩展成整数运算的结果。

$ echo $((2 + 2))
4

字符类

[[: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)。

请看下面的例子。

$ echo [[:upper:]]*
A.txt

上面命令输出所有大写字母开头的文件名。

字符类的第一个方括号后面,可以加上感叹号!,表示否定。比如,[![:digit:]]匹配所有非数字。

$ echo [![:digit:]]*

上面命令输出所有不以数字开头的文件名。

字符类也属于文件名扩展,如果没有匹配的文件名,字符类就会原样输出。

# 不存在以大写字母开头的文件
$ echo [[:upper:]]*
[[:upper:]]*

上面例子中,由于没有可匹配的文件,字符类就原样输出了。

量词语法

量词语法用来控制模式匹配的次数。它只有在 Bash 的extglob参数打开的情况下才能使用,不过一般是默认打开的。下面的命令可以查询。

$ shopt extglob
extglob         on

如果extglob参数是关闭的,可以用下面的命令打开。

$ shopt -s extglob

量词语法有下面几个。

  • ?(pattern-list):模式匹配零次或一次。
  • *(pattern-list):模式匹配零次或多次。
  • +(pattern-list):模式匹配一次或多次。
  • @(pattern-list):只匹配一次模式。
  • !(pattern-list):匹配给定模式以外的任何内容。
$ ls abc?(.)txt
abctxt abc.txt

上面例子中,?(.)匹配零个或一个点。

$ ls abc?(def)
abc abcdef

上面例子中,?(def)匹配零个或一个def

$ ls abc@(.txt|.php)
abc.php abc.txt

上面例子中,@(.txt|.php)匹配文件有且只有一个.txt.php后缀名。

$ ls abc+(.txt)
abc.txt abc.txt.txt

上面例子中,+(.txt)匹配文件有一个或多个.txt后缀名。

$ ls a!(b).txt
a.txt abb.txt ac.txt

上面例子中,!(b)表示匹配单个字母b以外的任意内容,所以除了ab.txt以外,其他文件名都能匹配。

量词语法也属于文件名扩展,如果不存在可匹配的文件,就会原样输出。

# 没有 abc 开头的文件名
$ ls abc?(def)
ls: 无法访问'abc?(def)': 没有那个文件或目录

上面例子中,由于没有可匹配的文件,abc?(def)就原样输出,导致ls命令报错。

引号和 Here 文档

单引号

Bash 允许字符串放在单引号或双引号之中,加以引用。

单引号用于保留字符的字面含义,各种特殊字符在单引号里面,都会变为普通字符,比如星号(*)、美元符号($)、反斜杠(\)等。

$ echo '*'
*

$ echo '$USER'
$USER

$ echo '$((2+2))'
$((2+2))

$ echo '$(echo foo)'
$(echo foo)

上面命令中,单引号使得 Bash 扩展、变量引用、算术运算和子命令,都失效了。如果不使用单引号,它们都会被 Bash 自动扩展。

双引号

双引号比单引号宽松,大部分特殊字符在双引号里面,都会失去特殊含义,变成普通字符。

$ echo "*"
*

双引号还有一个作用,就是保存原始命令的输出格式。

# 单行输出
$ echo $(cal)
一月 2020 日 一 二 三 四 五 六 1 2 3 ... 31

# 原始格式输出
$ echo "$(cal)"
      一月 2020
日 一 二 三 四 五 六
          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

上面例子中,如果$(cal)不放在双引号之中,echo就会将所有结果以单行输出,丢弃了所有原始的格式。

Here 文档

Here 文档(here document)是一种输入多行字符串的方法,格式如下。

<< token
text
token

它的格式分成开始标记(<< token)和结束标记(token)。开始标记是两个小于号 + Here 文档的名称,名称可以随意取,后面必须是一个换行符;结束标记是单独一行顶格写的 Here 文档名称,如果不是顶格,结束标记不起作用。两者之间就是多行字符串的内容。

Examples of cat <<EOF syntax usage in Bash:

1. Assign multi-line string to a shell variable

$ sql=$(cat <<EOF
SELECT foo, bar FROM db
WHERE foo='baz'
EOF
)

The $sql variable now holds the new-line characters too. You can verify with echo -e "$sql".

2. Pass multi-line string to a file in Bash

$ cat <<EOF > print.sh
#!/bin/bash
echo \$PWD
echo $PWD
EOF

The print.sh file now contains:

#!/bin/bash
echo $PWD
echo /home/user

3. Pass multi-line string to a pipe in Bash

$ cat <<EOF | grep 'b' | tee b.txt
foo
bar
baz
EOF

The b.txt file contains bar and baz lines. The same output is printed to stdout.

Here 字符串

Here 文档还有一个变体,叫做 Here 字符串(Here string),使用三个小于号(<<<)表示。

<<< string

它的作用是将字符串通过标准输入,传递给命令。

有些命令直接接受给定的参数,与通过标准输入接受参数,结果是不一样的。所以才有了这个语法,使得将字符串通过标准输入传递给命令更方便,比如cat命令只接受标准输入传入的字符串。

$ cat <<< 'hi there'
# 等同于
$ echo 'hi there' | cat

上面的第一种语法使用了 Here 字符串,要比第二种语法看上去语义更好,也更简洁。

$ md5sum <<< 'ddd'
# 等同于
$ echo 'ddd' | md5sum

上面例子中,md5sum命令只能接受标准输入作为参数,不能直接将字符串放在命令后面,会被当作文件名,即md5sum ddd里面的ddd会被解释成文件名。这时就可以用 Here 字符串,将字符串传给md5sum命令。

字符串操作

字符串的长度

获取字符串长度的语法如下。

${#varname}

下面是一个例子。

$ myPath=/home/cam/book/long.file.name
$ echo ${#myPath}
29

大括号{}是必需的,否则 Bash 会将$#理解成脚本的参数个数,将变量名理解成文本。

$ echo $#myvar
0myvar

上面例子中,Bash 将$#myvar分开解释了。

子字符串

字符串提取子串的语法如下。

${varname:offset:length}

上面语法的含义是返回变量$varname的子字符串,从位置offset开始(从0开始计算),长度为length

$ count=frogfootman
$ echo ${count:4:4}
foot

上面例子返回字符串frogfootman从4号位置开始的长度为4的子字符串foot

这种语法不能直接操作字符串,只能通过变量来读取字符串,并且不会改变原始字符串。

# 报错
$ echo ${"hello":2:3}

上面例子中,"hello"不是变量名,导致 Bash 报错。

如果省略length,则从位置offset开始,一直返回到字符串的结尾。

$ count=frogfootman
$ echo ${count:4}
footman

上面例子是返回变量count从4号位置一直到结尾的子字符串。

如果offset为负值,表示从字符串的末尾开始算起。注意,负数前面必须有一个空格, 以防止与${variable:-word}的变量的设置默认值语法混淆。这时还可以指定lengthlength可以是正值,也可以是负值(负值不能超过offset的长度)。

$ foo="This string is long."
$ echo ${foo: -5}
long.
$ echo ${foo: -5:2}
lo
$ echo ${foo: -5:-2}
lon

上面例子中,offset-5,表示从倒数第5个字符开始截取,所以返回long.。如果指定长度length2,则返回lo;如果length-2,表示要排除从字符串末尾开始的2个字符,所以返回lon

搜索和替换

Bash 提供字符串搜索和替换的多种方法。

(1)字符串头部的模式匹配。

以下两种语法可以检查字符串开头,是否匹配给定的模式。如果匹配成功,就删除匹配的部分,返回剩下的部分。原始变量不会发生变化。

# 如果 pattern 匹配变量 variable 的开头,
# 删除最短匹配(非贪婪匹配)的部分,返回剩余部分
${variable#pattern}

# 如果 pattern 匹配变量 variable 的开头,
# 删除最长匹配(贪婪匹配)的部分,返回剩余部分
${variable##pattern}

上面两种语法会删除变量字符串开头的匹配部分(将其替换为空),返回剩下的部分。区别是一个是最短匹配(又称非贪婪匹配),另一个是最长匹配(又称贪婪匹配)。

匹配模式pattern可以使用*?[]等通配符。

$ myPath=/home/cam/book/long.file.name

$ echo ${myPath#/*/}
cam/book/long.file.name

$ echo ${myPath##/*/}
long.file.name

上面例子中,匹配的模式是/*/,其中*可以匹配任意数量的字符,所以最短匹配是/home/,最长匹配是/home/cam/book/

下面写法可以删除文件路径的目录部分,只留下文件名。

$ path=/home/cam/book/long.file.name

$ echo ${path##*/}
long.file.name

上面例子中,模式*/匹配目录部分,所以只返回文件名。

下面再看一个例子。

$ phone="555-456-1414"
$ echo ${phone#*-}
456-1414
$ echo ${phone##*-}
1414

如果匹配不成功,则返回原始字符串。

$ phone="555-456-1414"
$ echo ${phone#444}
555-456-1414

上面例子中,原始字符串里面无法匹配模式444,所以原样返回。

如果要将头部匹配的部分,替换成其他内容,采用下面的写法。

# 模式必须出现在字符串的开头
${variable/#pattern/string}

# 示例
$ foo=JPG.JPG
$ echo ${foo/#JPG/jpg}
jpg.JPG

上面例子中,被替换的JPG必须出现在字符串头部,所以返回jpg.JPG

(2)字符串尾部的模式匹配。

以下两种语法可以检查字符串结尾,是否匹配给定的模式。如果匹配成功,就删除匹配的部分,返回剩下的部分。原始变量不会发生变化。

# 如果 pattern 匹配变量 variable 的结尾,
# 删除最短匹配(非贪婪匹配)的部分,返回剩余部分
${variable%pattern}

# 如果 pattern 匹配变量 variable 的结尾,
# 删除最长匹配(贪婪匹配)的部分,返回剩余部分
${variable%%pattern}

上面两种语法会删除变量字符串结尾的匹配部分(将其替换为空),返回剩下的部分。区别是一个是最短匹配(又称非贪婪匹配),另一个是最长匹配(又称贪婪匹配)。

$ path=/home/cam/book/long.file.name

$ echo ${path%.*}
/home/cam/book/long.file

$ echo ${path%%.*}
/home/cam/book/long

上面例子中,匹配模式是.*,其中*可以匹配任意数量的字符,所以最短匹配是.name,最长匹配是.file.name

下面写法可以删除路径的文件名部分,只留下目录部分。

$ path=/home/cam/book/long.file.name

$ echo ${path%/*}
/home/cam/book

上面例子中,模式/*匹配文件名部分,所以只返回目录部分。

下面的写法可以替换文件的后缀名。

$ file=foo.png
$ echo ${file%.png}.jpg
foo.jpg

上面的例子将文件的后缀名,从.png改成了.jpg

下面再看一个例子。

$ phone="555-456-1414"
$ echo ${phone%-*}
555-456
$ echo ${phone%%-*}
555

如果匹配不成功,则返回原始字符串。

如果要将尾部匹配的部分,替换成其他内容,采用下面的写法。

# 模式必须出现在字符串的结尾
${variable/%pattern/string}

# 示例
$ foo=JPG.JPG
$ echo ${foo/%JPG/jpg}
JPG.jpg

上面例子中,被替换的JPG必须出现在字符串尾部,所以返回JPG.jpg

(3)任意位置的模式匹配。

以下两种语法可以检查字符串内部,是否匹配给定的模式。如果匹配成功,就删除匹配的部分,换成其他的字符串返回。原始变量不会发生变化。

# 如果 pattern 匹配变量 variable 的一部分,
# 最长匹配(贪婪匹配)的那部分被 string 替换,但仅替换第一个匹配
${variable/pattern/string}

# 如果 pattern 匹配变量 variable 的一部分,
# 最长匹配(贪婪匹配)的那部分被 string 替换,所有匹配都替换
${variable//pattern/string}

上面两种语法都是最长匹配(贪婪匹配)下的替换,区别是前一个语法仅仅替换第一个匹配,后一个语法替换所有匹配。

$ path=/home/cam/foo/foo.name

$ echo ${path/foo/bar}
/home/cam/bar/foo.name

$ echo ${path//foo/bar}
/home/cam/bar/bar.name

上面例子中,前一个命令只替换了第一个foo,后一个命令将两个foo都替换了。

下面的例子将分隔符从:换成换行符。

$ echo -e ${PATH//:/'\n'}
/usr/local/bin
/usr/bin
/bin
...

上面例子中,echo命令的-e参数,表示将替换后的字符串的\n字符,解释为换行符。

模式部分可以使用通配符。

$ phone="555-456-1414"
$ echo ${phone/5?4/-}
55-56-1414

上面的例子将5-4替换成-

如果省略了string部分,那么就相当于匹配的部分替换成空字符串,即删除匹配的部分。

$ path=/home/cam/foo/foo.name

$ echo ${path/.*/}
/home/cam/foo/foo

上面例子中,第二个斜杠后面的string部分省略了,所以模式.*匹配的部分.name被删除后返回。

前面提到过,这个语法还有两种扩展形式。

# 模式必须出现在字符串的开头
${variable/#pattern/string}

# 模式必须出现在字符串的结尾
${variable/%pattern/string}

改变大小写

下面的语法可以改变变量的大小写。

# 转为大写
${varname^^}

# 转为小写
${varname,,}

下面是一个例子。

$ foo=heLLo
$ echo ${foo^^}
HELLO
$ echo ${foo,,}
hello

其它转换大小写的方法

POSIX standard

tr

$ echo "$a" | tr '[:upper:]' '[:lower:]'
hi all

AWK

$ echo "$a" | awk '{print tolower($0)}'
hi all

Non-POSIX

You may run into portability issues with the following examples:

Bash 4.0

$ echo "${a,,}"
hi all

sed

$ echo "$a" | sed -e 's/\(.*\)/\L\1/'
hi all
# this also works:
$ sed -e 's/\(.*\)/\L\1/' <<< "$a"
hi all
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容