注:作者原文为繁体行文,部分术语与简体有出入。
本文在原 HAWK.Li 整理的基础上修订了部分口语化描述,增订了 “补充问题” 部分,如有内容异常,请看原文。
Shell 十三问
網中人 发表于 2003-12-09 更新于 2008-10-30
目录
- 为何叫做 shell?
- shell prompt(PS1) 与 Carriage Return(CR) 的关系?
- 别人 echo、你也 echo,是问 echo 知多少?
- " "(双引号)与’ '(单引号)差在哪?
- var=value?export 前后差在哪?
- exec 跟 source 差在哪?
- ( ) 与 { } 差在哪?
- $(( )) 与 $( ) 还有 ${ } 差在哪?
- $@ 与 $* 差在哪?
- && 与 || 差在哪?
- > 与 < 差在哪?
- 你要 if 还是 case 呢?
- for what? while 与 until 差在哪?
1. 为何叫做 shell?
在介绍 shell 是什么之前,先来审视用户与计算机系统的关系。计算机的运行离不开硬件,但用户无法直接驱动硬件,硬件的驱动需通过“操作系统(Operating System)”来管控。实际上,我们常说的 Linux 严格来说只是操作系统的核心(kernel)。
从用户角度而言,不能直接操作 kernel,而是要通过 kernel 的“外壳”程序,即 shell,来与 kernel 沟通,这便是 kernel 与 shell 的命名由来。从技术层面讲,shell 是用户与系统的交互界面(interface),主要让用户通过命令行(command line)使用系统完成工作,其简单定义为命令解释器(Command Interpreter):
- 将用户命令翻译给核心处理;
- 同时,将核心处理结果翻译给用户。
每次完成系统登录(log in),就会取得一个互动模式的 shell,即 login shell 或 primary shell。在 shell 中下达的命令,都是 shell 产生的子进程,此现象称为 fork。若执行脚本(shell script),脚本中的命令则由非互动模式的子 shell(sub shell)执行,即 primary shell 产生 sub shell 进程,sub shell 再产生脚本中所有命令的进程。
需要注意的是,kernel 与 shell 是不同的两套软件,且均可被替换:
- 不同的操作系统使用不同的 kernel;
- 同一 kernel 之上,也可使用不同的 shell。
在 Linux 的默认系统中,通常能在 /etc/shells 文件里找到多种 shell。常见的 shell 主要分为两大主流:
- sh:
- Bourne shell (sh)
- Bourne again shell (bash)
- csh:
- C shell (csh)
- tc shell (tcsh)
- Korn shell (ksh)
大部分 Linux 系统的默认 shell 是 bash,原因大致有两点:
- 自由软件;
- 功能强大。
bash 是 GNU Project 最成功的产品之一,自推出以来深受 Unix 用户喜爱,逐渐成为不少组织的系统标准。
2. shell prompt(PS1) 与 Carriage Return(CR) 的关系?
成功登录进文字界面后,通常会看到一个闪烁的方块或底线(视不同版本而定),这就是游标(cursor)。游标用于指示接下来从键盘输入按键的插入位置,每输入一个键,游标便向右移动一格,若输入过多则自动换行。
在刚登录还未输入任何按键时,游标所在位置同一行的左边部分,就是提示符号(prompt)。在 Linux 上,一般只需留意最接近游标的可见提示符号,通常是 $(给一般使用者账号使用)或 #(给 root 管理员账号使用)。
shell prompt 的含义是告诉用户可以输入命令行了。用户只有在看到 shell prompt 后才能输入命令行,而 cursor 指示键盘在命令行输入的位置,用户每输入一个键,cursor 就往后移动一格,直到输入 CR(Carriage Return,由 Enter 键产生)字符为止。CR 的作用是告知 shell 可以执行用户输入的命令行了。严格来说,命令行就是在 shell prompt 与 CR 字符之间输入的文字。
从技术细节看,shell 会依据 IFS(Internal Field Separator)将 command line 输入的文字拆解为“字段”(word),然后先处理特殊字符(meta),最后重组整行 command line。IFS 是 shell 预设的字段分隔符,可由空格键(White Space)、表格键(Tab)、回车键(Enter)组成。
系统可接受的命令名称(command - name)可通过以下途径获得:
- 明确路径所指定的外部命令;
- 命令别名(alias);
- 自定功能(function);
- shell 内建命令(built - in);
- $PATH 之下的外部命令。
每个命令行都必须包含命令名称。
3. 别人 echo、你也 echo,是问 echo 知多少?
承接上一章的 command line,这里用 echo 命令进一步说明。复习一下,标准的 command line 包含三个部分:command_name、option、argument。
echo 是一个简单直接的 Linux 命令,它将 argument 送出至标准输出(STDOUT),通常在监视器(monitor)上显示。
例如:
$ echo
$
运行 echo 命令后只有一个空白行,然后回到 shell prompt,这是因为 echo 在默认情况下,显示完 argument 后会送出一个换行符号(new - line charactor)。
若要取消换行符号,可使用 echo 的 -n option:
$ echo -n
$
此时由于没有 argument,结果只剩一个换行符号。
再看下面的例子:
$ echo first line first line
$ echo -n first line first line $
在这些命令中,可以看到 argument 的部分显示在屏幕上,换行符号则根据 -n option 的有无而不同。
echo 除了 -n 选项外,常用选项还有:
- -e:启用反斜杠控制字符的转换(参考下表);
- -E:关闭反斜杠控制字符的转换(预设如此)。
echo 命令支持的反斜杠控制字符如下表:
控制字符 | 含义 |
---|---|
\a | ALERT / BELL(从系统喇叭送出铃声) |
\b | BACKSPACE,向左删除键 |
\c | 取消行末之换行符号 |
\E | ESCAPE,跳脱键 |
\f | FORMFEED,换页字符 |
\n | NEWLINE,换行字符 |
\r | RETURN,回车键 |
\t | TAB,表格跳位键 |
\v | VERTICAL TAB,垂直表格跳位键 |
\o | ASCII 八进制编码(以 0 开首) |
\x | ASCII 十六进制编码(以 x 开首) |
|反斜杠本身 |
(表格数据来自 O’Reilly 出版社之 Learning the Bash Shell, 2nd Ed.)
例如:
$ echo -e "a\tb\tc\nd\te\tf"
abc
def
通过这个例子可以了解 echo 的选项及控制字符的使用。
在日后的 shell 操作及 shell script 设计中,echo 命令是常用命令之一。例如,可用于检查变量值:
$ A=B
$ echo $A
B
$ echo $?
0
4. " "(双引号)与’ '(单引号)差在哪?
回到 command line,输入的文字在 shell 中有类别之分。简单来说,分为 literal(普通纯文本,对 shell 无特殊功能)和 meta(对 shell 有特定功能的特殊保留字符)。
常见的 meta 字符有 IFS(由 或 或 组成,用于拆解 command line 的词)、CR(由 产生,用于结束 command line)、=(设定变量)、$(作变量或运算替换)、>(重导向 stdout)、<(重导向 stdin)、|(命令管线)、&(重导向 file descriptor 或后台执行命令)、( )(用于嵌套子 shell 执行命令、运算或命令替换)、{ }(用于非命名函数执行命令或变量替换界定范围)、;(在前一命令结束后忽略返回值继续执行下一命令)、&&(前一命令返回值为真时继续执行下一命令)、||(前一命令返回值为假时继续执行下一命令)、!(执行 history 列表中的命令)等。
若要关闭这些保留字符的功能,需要进行 quoting 处理。在 bash 中,常用的 quoting 方法有三种:
- hard quote(’ ',单引号):单引号内的所有 meta 均被关闭;
- soft quote(" ",双引号):双引号内大部分 meta 会被关闭,但某些如 $ 会保留其功能;
- escape(\,反斜杠):只有紧接在反斜杠后的单个 meta 才被关闭。
例如:
$ A=B C # 这里空格未被关掉,作为 IFS 处理,会导致错误
$ C: command not found.
$ A="B C" # 空格被双引号关掉,仅作为普通空格处理
$ echo $A
B C
再看下面的例子:
$ A='B
> C
> '
$ echo $A
B C
在这个例子中,由于 被置于 hard quote 当中,不再作为 CR 字符处理,只是一个断行符号,直到输入的 不在 hard quote 里面,command line 碰到 CR 字符才结束。
对于变量替换,如:
$ A=B\ C
$ echo "$A"
B C
$ echo '$A'
$A
在第一个 echo 命令中,$ 被置于 soft quote 中,不被关闭,会进行变量替换;在第二个 echo 命令中,$ 被置于 hard quote 中,则被关闭,不会进行变量替换。
练习与思考:
$ A=B\ C
$ echo '"$A"' "$A"
"B C" B C
$ echo "'$A'"
'$A'
这里的结果不同是因为单引号和双引号在 quoting 中的作用不同,单引号会将所有内容按字面量处理,双引号内除了特定的 meta 字符外也按字面量处理,但会对某些 meta 字符(如 $)进行特殊处理。
在处理命令参数时,如 awk
命令,若未正确引用,可能导致语法错误。例如:
$ awk {print $0} 1.txt # 这里 { } 在 shell 中未被关闭,会被视为命令块,导致错误
$ awk '{print $0}' 1.txt # 使用硬引用关闭相关 meta 字符,可正确执行
若要在 awk
中使用 shell 变量,如 A = 0
,可以这样:
$ A=0
$ awk "{print \$$A}" 1.txt # 双引号内的 $ 可进行替换操作
5. var=value?export 前后差在哪?
先来了解 bash 变量(variable)。变量是利用特定“名称”(name)来存取一段可以变化的“值”(value)。
变量设定
在 bash 中,用“=”来设定或重新定义变量的内容,但要遵守以下规则:
- 等号左右两边不能使用区隔符号(IFS),也应避免使用 shell 的保留字符(meta charactor);
- 变量名称不能使用 $ 符号;
- 变量名称的第一个字母不能是数字(number);
- 变量名称长度不可超过 256 个字母;
- 变量名称及变量值的大小写是有区别的(case sensitive)。
以下是一些变量设定的常见错误和正确示例:
- 错误示例:
A= B
:不能有 IFS;1A=B
:不能以数字开头;$A=B
:名称不能有 $;a=B
:与a=b
不同。
- 正确示例:
A=" B"
:IFS 被关闭;A1=B
:并非以数字开头;A=$B
:$ 可用在变量值内;This_Is_A_Long_Name=b
:可用 _ 连接较长的名称或值,且大小写有别。
变量替换
Shell 强大的原因之一是可以在命令行中对变量作替换(substitution)处理。在命令行中,用户可以使用 $ 符号加上变量名称(除了在用 = 号定义变量名称之外)来替换变量值,然后重新组建命令行。
例如:
$ A=ls
$ B=la
$ C=/tmp
$ $A -$B $C
在执行前,$ 会对变量进行替换,最终执行的命令为 ls -la /tmp
。
利用变量替换,在设定变量时可以更灵活。例如:
A=B
B=$A
这样,B 的变量值就可继承 A 变量“当时”的变量值。但要注意,不能用“数学逻辑”来套用变量设定,如:
A=B
B=C
这并不会让 A 的变量值变成 C。
还可以利用变量替换来扩充变量值:
A=B:C:D
A=$A:E
这里,第一行设定 A 的值为 “B:C:D”,第二行将值扩充为 “A:B:C:E”。如果没有区隔符号,可能会出现问题,如:
A=BCD
A=$AE
这里第二次是将 A 的值继承 $AE 的替换结果,而非 $A 再加 E。要解决此问题,可使用更严谨的替换处理:
A=BCD
A=${A}E
这里使用 {} 将变量名称的范围明确定义出来,就可以将 A 的变量值从 BCD 扩充为 BCDE。
export 命令
在当前 shell 中定义的变量属于本地变量(local variable),只有经过 export
命令的“输出”处理,才能成为环境变量(environment variable)。
例如:
$ A=B
$ export A
或者:
$ export A=B
经过 export
输出处理后,变量 A 就能成为一个环境变量供其后的命令使用。但要注意 export
命令行也会进行变量替换。例如:
$ A=B
$ B=C
$ export $A
这里实际输出的是变量 B,因为 $A 会先被替换为 B 然后再作为 export
的参数。
取消变量
在 bash 中,使用 unset
命令来取消变量。与 export
一样,unset
命令行也会进行变量替换。
例如:
$ A=B
$ B=C
$ unset $A
实际上取消的是变量 B。
变量一旦经过 unset
取消后,整个变量就会被拿掉,而不仅是取消其变量值。例如:
$ A=
$ unset A
第一行只是将变量 A 设定为空值(null value),第二行则让变量 A 不存在。可以通过 echo $A
来验证。
例如:
$ str=
# 设为 null
$ var=${str=expr}
$ echo $var
$ echo $str
$ unset str
# 取消
$ var=${str=expr}
$ echo $var
expr
$ echo $str
expr
这里可以看到在 null 和 unset 情况下,var=${str=expr}
的结果不同。
6. exec 跟 source 差在哪?
先从 CU Shell 版的一个实例说起。例如,cd /etc/aa/bb/cc
命令在直接执行时可以改变当前工作目录,但写入 shell 脚本时却不执行,原因是一般的 shell 脚本是用 subshell 去执行的。
从进程(process)的角度来看,当执行一个 shell 脚本时,是父进程产生一个子进程去执行,子进程结束后会返回父进程,但父进程的环境不会因子进程的改变而改变。这里的环境元素包括 effective id、variable、working dir 等,其中的 working dir($PWD)就是上述问题的关键所在。当用 subshell 来跑脚本时,sub shell 的 P W D 会因为 ‘ c d ‘ 而变更,但当返回 p r i m a r y s h e l l 时, PWD 会因为 `cd` 而变更,但当返回 primary shell 时, PWD会因为‘cd‘而变更,但当返回primaryshell时,PWD 不会变更。
为了解决这个问题,可以使用 source
命令。source
命令会让脚本在当前 shell 内执行,而不是产生一个 sub - shell 来执行。由于所有执行结果都在当前 shell 内完成,所以如果脚本的环境有所改变,也会改变当前环境。
例如,原本执行脚本的方式是 ./my.script
,现在可以改为 source./my.script
或者 ../my.script
。
再来看 exec
命令,它也让脚本在同一个进程上执行,但与 source
不同的是,原有进程会被结束。
通过以下两个简单的脚本示例可以更清楚地看到它们的区别:
- 脚本 1.sh:
#!/bin/bash
A=B
echo "PID for 1.sh before exec/source/fork:$$"
export A
echo "1.sh: \$A is $A"
case $```bash
1 in
exec)
echo "using exec..."
exec./2.sh
;;
source)
echo "using source..."
../2.sh
;;
*)
echo "using fork by default..."
./2.sh
;;
esac
echo "PID for 1.sh after exec/source/fork:$$"
echo "1.sh: \$A is $A"
- 脚本 2.sh:
#!/bin/bash
echo "PID for 2.sh: $$"
echo "2.sh get \$A=$A from 1.sh"
A=C
export A
echo "2.sh: \$A is $A"
分别执行 ./1.sh fork
、./1.sh source
、./1.sh exec
并观察结果,可以发现它们之间的差异。
总之,exec
与 source/fork
的最大差异就在于原有进程是否终止。理解这些差异对于正确处理 shell 脚本中的环境和执行流程非常重要。
7. ( ) 与 { } 差在哪?
在 shell 操作中,有时需要在一定条件下一次执行多个命令,或者从一些命令执行优先顺序中得到豁免,这时就引入了“命令群组”(command group)的概念,将多个命令集中处理。
在 shell command line 中,( )
与 { }
都可用于将多个命令作群组化处理,但在技术细节上有很大不同。( )
将 command group 置于 sub - shell(nested sub - shell)去执行,{ }
则是在同一个 shell 内完成(non - named command group)。
如果对前面章节中 fork 与 source 的概念还有印象,就不难理解两者的差异。在 command group 中涉及变量及其他环境修改时,可以根据不同需求来使用 ( )
或 { }
。通常,如果所作的修改是临时的,且不想影响原有或以后的设定,就使用 nested sub - shell(( )
);反之,则用 non - named command group({ }
)。
例如,在定义函数时:
function function_name {
command1
command2
command3
...
}
或者:
function_name () {
command1
command2
command3
...
}
这里使用 { }
来包裹函数体中的命令。函数可以看作是 script 中的 script,在 shell 中定义的函数,除了可以用 unset function_name
取消外,一旦退出 shell,函数也会跟着取消。而在 script 中使用函数有很多好处,除了可以提高整体 script 的执行效能外(因为已被加载),还可以节省许多重复的代码。
如果在 shell 操作中需要不断重复执行某些命令,可以将这些命令写成函数,然后在 command line 中输入函数名就可以像执行一般的 script 一样使用它。
8. $(( )) 与 $( ) 还有 ${ } 差在哪?
在上一章介绍了 ( )
与 { }
的不同,这里继续扩展,看看 $( )
、${ }
和 $(( ))
的区别。
在 bash shell 中,$( )
与
(反引号)都用于命令替换。命令替换的作用与第五章学过的变量替换类似,都是用来重组命令行。例如:
$ echo the last sunday is $(date -d "last sunday" +%Y-%m-%d)
它会先执行 date -d "last sunday" +%Y-%m-%d
命令,然后将结果替换出来,再重组命令行,从而方便地得到上一星期天的日期。
在操作上,使用 $( )
或
都可以,但 $( )
有一些优势。一方面,
很容易与 '
(单引号)搞混,尤其对初学者来说,在一些奇怪的字形显示中,两种符号可能看起来一模一样。另一方面,在多层次的复合替换中,
需要额外的转义(`)处理,而 $( )
则比较直观。例如:
# 错误的写法
command1 `command2 `command3` `
# 正确的写法
command1 `command2 \`command3\` `
# 更好的写法
command1 $(command2 $(command3))
不过,$( )
也有一些局限性。
基本上在全部的 Unix shell 中都能使用,若写成 shell script,其移植性比较高;而 $( )
并不一定在每一种 shell 中都能使用,在 bash2 中肯定没问题。
接下来看 ${ }
,它主要用于变量替换。一般情况下,$var
与 ${var}
没有太大区别,但 ${ }
能更精确地界定变量名称的范围。例如:
$ A=B
$ echo $AB
# 可能会得到错误的结果,因为它可能会尝试替换变量 AB 的值
$ echo ${A}B
BB
此外,${ }
还有很多高级功能。假设定义了一个变量 file=/dir1/dir2/dir3/my.file.txt
,可以用 ${ }
进行以下操作:
${file#*/}
:拿掉第一条/
及其左边的字符串,结果为dir1/dir2/dir3/my.file.txt
;${file##*/}
:拿掉最后一条/
及其左边的字符串,结果为my.file.txt
;${file#*.}
:拿掉第一个.
及其左边的字符串,结果为file.txt
;${file##*.}
:拿掉最后一个.
及其左边的字符串,结果为txt
;${file%/*}
:拿掉最后条/
及其右边的字符串,结果为/dir1/dir2/dir3
;${file%%/*}
:拿掉第一条/
及其右边的字符串,结果为(空值);${file%.*}
:拿掉最后一个.
及其右边的字符串,结果为/dir1/dir2/dir3/my.file
;${file%%.*}
:拿掉第一个.
及其右边的字符串,结果为/dir1/dir2/dir3/my
。
记忆方法为:#
是去掉左边(在键盘上 #
在 $
之左边),%
是去掉右边(在键盘上 %
在 $
之右边),单一符号是最小匹配,两个符号是最大匹配。
还可以进行以下操作:
${file:0:5}
:提取最左边的 5 个字节,结果为/dir1
;${file:5:5}
:提取第 5 个字节右边的连续 5 个字节,结果为/dir2
;${file/dir/path}
:将第一个dir
替换为path
,结果为/path1/dir2/dir3/my.file.txt
;${file//dir/path}
:将全部dir
替换为path
,结果为/path1/path2/path3/my.file.txt
;${file-my.file.txt}
:假如$file
为空值,则使用my.file.txt
作默认值(保留没设定及非空值);${file:-my.file.txt}
:假如$file
没有设定或为空值,则使用my.file.txt
作默认值(保留非空值);${file+my.file.txt}
:不管$file
为何值,均使用my.file.txt
作默认值(不保留任何值);${file:+my.file.txt}
:除非$file
为空值,否则使用my.file.txt
作默认值(保留空值);${file=my.file.txt}
:若$file
没设定,则使用my.file.txt
作默认值,同时将$file
定义为非空值(保留空值及非空值);${file:=my.file.txt}
:若$file
没设定或为空值,则使用my.file.txt
作默认值,同时将$file
定义为非空值(保留非空值);${file?my.file.txt}
:若$file
没设定,则将my.file.txt
输出至 STDERR(保留空值及非空值);${file:?my.file.txt}
:若$file
没设定或为空值,则将my.file.txt
输出至 STDERR(保留非空值);${#file}
:可计算出变量值的长度,结果为 27,因为/dir1/dir2/dir3/my.file.txt
刚好是 27 个字节。
bash 还支持数组处理。例如,A=(a b c def)
就定义了一个数组。可以使用以下方式进行数组替换:
${A[@]}
或${A[*]}
:可得到a b c def
(全部数组元素);${A[0]}
:可得到a
(第一个数组元素);${A[1]}
:可得到b
(第二个数组元素);${#A[@]}
或${#A[*]}
:可得到 4(全部数组元素数量);${#A[0]}
:可得到 1(即第一个数组元素a
的长度);${A[3]}
:可得到 3(第一个数组元素def
的长度);A[3]=xyz
:则是将第 4 个数组元素重新定义为xyz
。
最后介绍 $(( ))
,它是用来作整数运算的。在 bash 中,$(( ))
的整数运算符号大致有这些:
+ - * /
:分别为“加、减、乘、除”;%
:余数运算。
例如:
$ a=5; b=7; c=2
$ echo $(( a+b*c ))
19
$ echo $(( (a+b)/c ))
6
$ echo $(( (a*b)%c))
1
在 $(( ))
中的变量,可用 $
符号来替换,也可以不用,如 $(( $a + $b * $c))
也可得到 19 的结果。
此外,$(( ))
还可作不同进位(如二进制、八进制、十六进制)作运算,但输出结果皆为十进制。例如:
$ echo $((16#2a))
42
以一个实用的例子来看:
$ umask 022
$ echo "obase=8;$(( 8#666 & (8#777 ^ 8#$(umask)) ))" | bc
644
假如当前的 umask 是 022,那么新建文件的权限即为 644。
$(( ))
还可以重新定义变量值或作测试。例如:
a=5; ((a++))
:可将$a
重新定义为 6;a=5; ((a--))
:则为a=4
;a=5; b=7; ((a < b))
:会得到 0(true)的返回值。
常见的用于 $(( ))
的测试符号有如下这些:
<
:小于;>
:大于;<=
:小于或等于;>=
:大于或等于;==
:等于;!=
:不等于。
但要注意,使用 $(( ))
作整数测试时,不要跟 [ ]
的整数测试搞混乱了。
9. $@ 与 $* 差在哪?
要说 $@ 与 $* 之前,需先从 shell script 的 positional parameter 谈起。我们已经知道变量是如何定义及替换的,这里有一些内定的变量,其名称不能随意修改,positional parameter 就是其中之一。
在 shell script 中,可用 $0
、$1
、$2
、$3
… 这样的变量分别提取命令行中的如下部分:$0
代表 shell script 名称(路径)本身,$1
就是其后的第一个参数,以此类推。
需要留意的是 IFS 的作用,若 IFS 被 quoting 处理后,positional parameter 也会改变。例如:
my.sh p1 "p2 p3" p4
由于在 p2
与 p3
之间的空格键被 soft quote 所关闭了,因此在 my.sh
中,$2
是 "p2 p3"
,$3
则是 p4
。
函数也可以读取自己的(有别于 script 的)positional parameter,唯一例外的是 $0
。例如,假设 my.sh
里有一个函数叫 my_fun
,在 script 中跑 my_fun fp1 fp2 fp3
:
#!/bin/bash
my_fun() {
echo '$0 inside function is '$0
echo '$1 inside function is '$1
echo '$2 inside function is '$2
}
echo '$0 outside function is '$0
echo '$1 outside function is '$1
my_fun fp1 "fp2 fp3"
echo '$2 outside function is '$2
执行 chmod +x my.sh
和 ./my.sh p1 "p2 p3"
后,可以看到函数内的 $0
是 my.sh
,$1
是 fp1
而非 p1
。
在使用 positional parameter 的时候,要注意一些陷阱。例如,$10
不是替换第 10 个参数,而是替换第一个参数($1
)然后再补一个 0 于其后。要获取第 10 个参数,可以使用 ${10}
,或者通过 shift
命令来实现。shift
命令的默认值为 1,即 shift
或 shift 1
都是取消 $1
,而原本的 $2
则变成 $1
、$3
变成 $2
… 若 shift 3
则是取消前面三个参数,原本的 $4
将变成 $1
。
接下来看 $#
,它可抓出 positional parameter 的数量。以前面的 my.sh p1 "p2 p3"
为例,由于 p2
与 p3
之间的 IFS 是在 soft quote 中,因此 $#
可得到 2 的值。如果 p2
与 p3
没有置于 quoting 中,$#
就可得到 3 的值。在函数中也是一样的。
例如,在 shell script 里常用以下方法测试 script 是否有读进参数:
[ $# = 0 ]
假如为 0,则表示 script 没有参数,否则就是有带参数。
最后看 $@
与 $*
,精确来讲,两者只有在 soft quote 中才有差异,否则,都表示“全部参数”($0
除外)。例如,在 command line 上跑 my.sh p1 "p2 p3" p4
,不管是 $@
还是 $*
,都可得到 p1 p2 p3 p4
。但是,如果置于 soft quote 中的话:
"$@"
则可得到 "p1"
"p2 p3"
"p4"
这三个不同的词段(word);
"$*"
则可得到 "p1 p2 p3 p4"
这一整串单一的词段。
可以修改前面的 my.sh
内容如下:
#!/bin/bash
my_fun() {
echo "$#"
}
echo 'the number of parameter in "$@" is '$(my_fun "$@")
echo 'the number of parameter in "$*" is '$(my_fun "$*")
执行 ./my.sh p1 "p2 p3" p4
就可以清楚地看到 $@ 与 $* 的差异。
10. && 与 || 差在哪?
在 shell 下运行的每一个 command 或 function,在结束的时候都会传回父行程一个值,称为 return value。在 shell command line 中可用 $?
这个变量得到最新的一个 return value,也就是刚结束的那个行程传回的值。
Return Value(RV) 的取值为 0 - 255 之间,由程序(或 script)的作者自行定义:
- 在 script 里,用
exit RV
来指定其值,若没指定,在结束时以最后一道命令之 RV 为值; - 在 function 里,则用
return RV
来代替exit RV
即可。
Return Value 的作用是用来判断行程的退出状态(exit status),只有两种:
- 0 的话为“真”(true);
- 非 0 的话为“假”(false)。
例如:
$ touch my.file
$ ls my.file
$ echo $? # first echo
0
$ ls no.file
ls: no.file: No such file or directory
$ echo $?
1
$ echo $? # second echo
0
这里,第一个 echo $?
是关于 ls my.file
的 RV,可得到 0 的值,所以为 true;第二个 echo $?
是关于 ls no.file
的 RV,则得到非 0 的值,因此为 false;第三个 echo $?
是关于第二个 echo $?
的 RV,为 0 的值,因此也为 true。
有一个命令专门用来测试某一条件并送出 return value 以供 true 或 false 的判断,它就是 test
命令。在 bash 中,可以在 command line 下打 man test
或 man bash
来了解其用法,这是最精确的参考文件。
test
命令的表示式称为 expression
,其命令格式有两种:
test expression
或
[ expression ]
需要注意 [ ]
之间的空格键。这两种格式效果相同,个人比较喜欢后者。
bash 的 test
目前支持的测试对象只有三种:
- string:字符串,也就是纯文本;
- integer:整数(0 或正整数,不含负数或小数点);
- file:文件。
以 A = 123
这个变量为例:
[ "$A" = 123 ]
:是字符串的测试,以测试$A
是否为 1、2、3 这三个连续的“文字”;[ "$A" -eq 123 ]
:是整数的测试,以测试$A
是否等于“一百二十三”;[ -e "$A" ]
:是关于文件的测试,以测试 123 这份“文件”是否存在。
当 expression
测试为“真”时,test
就送回 0(true)的 return value,否则送出非 0(false)。若在 expression
之前加上一个 !
(感叹号),则是当 expression
为“假时”才送出 0,否则送出非 0。
test
也允许多重的复合测试:
expression1 -a expression2
:当两个exrepssion
都为 true,才送出 0,否则送出非 0;expression1 -o expression2
:只需其中一个exrepssion
为 true,就送出 0,只有两者都为 false 才送出非 0。
例如:
[ -d "$file" -a -x "$file" ]
表示当 $file
是一个目录、且同时具有 x 权限时,test
才会为 true。
在 command line 中使用 test
时,别忘记命令行的“重组”特性,也就是在碰到 meta 时会先处理 meta 再重新组建命令行。例如,对于 [ string1 = string2 ]
这个 test
格式,在 =
号两边必须要有字符串,其中包括空(null)字符串(可用 soft quote 或 hard quote 取得)。
例如:
$ unset A
$ [ $A = abc ]
[: =: unary operator expected
因为 $A
未定义或为空字符串,命令行碰到 $
这个 meta 时替换 $A
的值后重组命令行变成 [ = abc ]
,导致语法错误。而
$ [ "$A" = abc ]
$ echo $?
1
这里 [ "$A" = abc ]
重组后为 [ "" = abc ]
,=
左边有软引用得到的空字符串,语法得以通过。
如果在 test
中碰到变量替换,用 soft quote 是最保险的。
&& 与 || 都是用来“组建”多个 command line 用的:
command1 && command2
:其意思是command2
只有在RV
为 0(true)的条件下执行;command1 || command2
:其意思是command2
只有在RV
为非 0(false)的条件下执行。
例如:
$ A=123
$ [ -n "$A" ] && echo "yes! it's ture."
yes! it's ture.
$ unset A
$ [ -n "$A" ] && echo "yes! it's ture."
$ [ -n "$A" ] || echo "no, it's NOT ture."
no, it's NOT ture.
第一个 &&
命令行中,因为 [ -n "$A" ]
送回 0 的 RV
值,所以执行右边的 echo
命令;第二次 [ -n "$A" ]
送回非 0 结果,不执行 echo
命令。而 ||
右边的 echo
会被执行,正是因为左边的 test
送回非 0 所引起的。
在同一命令行中,可用多个 &&
或 ||
来组建。
例如:
$ A=123
$ [ -n "$A" ] && [ "$A" -lt 100 ] || echo 'too big!'
too big!
$ unset A
$ [ -n "$A" ] && [ "$A" -lt 100 ] || echo 'too big!'
too big!
若要解决后面这个结果不符合预期的问题(当 A
取消时不应输出 too big!
),可以利用第七章介绍过的 command group 等方法。
11. > 与 < 差在哪?
谈到 I/O redirection,先认识一下 File Descriptor (FD)。程序运算通常涉及数据处理,数据从哪里读进和送出就是 file descriptor (FD) 的功用。
在 shell 程序中,最常使用的 FD 大概有三个,分别为:
- 0: Standard Input (STDIN),通常关联键盘;
- 1: Standard Output (STDOUT);
- 2: Standard Error Output (STDERR)。
在标准情况下,这些 FD 分别跟如下设备(device)关联:
- stdin(0): keyboard
- stdout(1): monitor
- stderr(2): monitor
例如:
$ mail -s test root this is a test mail. please skip.
^d (同时按 crtl 跟 d 键)
可以看出 mail
程序所读进的数据是从 stdin(键盘)读进的。但不是每个程序的 stdin 都如此,例如 cat /etc/passwd
是从文件参数读进 stdin,cat
命令后无文件参数时,则等待从键盘输入数据(按 ^d 结束)。
<
用来改变读进的数据信道(stdin),使之从指定的档案读进,其默认 FD 为 0,所以 <
与 0<
是一样的。例如:
$ cat < my.file
>
用来改变送出的数据信道(stdout, stderr),使之输出到指定的档案。例如:
$ mail -s test root < /etc/passwd
就是从 /etc/passwd
读进数据并发送邮件。
还有 <<
,这是所谓的 HERE Document,它可以让我们输入一段文本,直到读到 <<
后指定的字符串。例如:
$ cat <<FINISH
first line here
second line there
third line nowhere
FINISH
cat
会读进这 3 行句子,无需从键盘读进数据且无需按 ^d 结束输入。
1>
用于改变 stdout
的数据输出信道,2>
用于改变 stderr
的数据输出信道,因为 1 是 >
的默认值,所以 1>
与 >
相同,2 是 2>
的默认值。例如:
$ ls my.file no.such.file 1>file.out
ls: no.such.file: No such file or directory
此时 stdout
被重定向到 file.out
,监视器上仅显示错误信息。
$ ls my.file no.such.file 2>file.err
my.file
这里 stderr
被重定向到 file.err
,监视器上仅显示标准输出。
$ ls my.file no.such.file 1>file.out 2>file.err
这样监视器上就什么也没有了,因为 stdout
与 stderr
都被转到档案去了。
在重定向过程中存在一些问题,比如 file locking。例如:
$ ls my.file no.such.file 1>file.both 2>file.both
从文件系统角度,单一档案在单一时间内,只能被单一的 FD 作写入。如果 stdout(1)
与 stderr(2)
都同时写入 file.both
,可能会出现数据丢失的情况,基本是“先抢先赢”原则。
解决方法是将 stderr
导进 stdout
或将 stdout
导进 stderr
,即:
2>&1
:将stderr
并进stdout
作输出;1>&2
或>&2
:将stdout
并进stderr
作输出。
例如:
$ ls my.file no.such.file 1>file.both 2>&1
或
$ ls my.file no.such.file 2>file.both >&2
在 Linux 文件系统里,/dev/null
是一个特殊设备档。它在 I/O Redirection 中很有用:
- 若将
FD1
跟FD2
转到/dev/null
去,就可将stdout
与stderr
弄不见掉; - 若将
FD0
接到/dev/null
来,那就是读进 nothing。
例如:
$ ls my.file no.such.file 2>/dev/null
my.file
如果不想看到 stderr
(也不想存到档案去),可以这样做。
$ ls my.file no.such.file >/dev/null
ls: no.such.file: No such file or directory
若只想看到 stderr
,将 stdout
弄到 null
就行。
$ ls my.file no.such.file &>/dev/null
可以单纯只跑程序,不想看到任何输出结果。
在重定向输出到档案时,>
会覆盖原有内容,>>
则会追加内容。例如:
$ echo "1" > file.out
$ cat file.out
1
$ echo "2" > file.out
$ cat file.out
2
$ echo "3" >> file.out
$ cat file.out
2
3
若要避免意外覆盖文件,可使用 set -o noclobber
设置,此时使用 >
重定向会报错。例如:
$ set -o noclobber
$ echo "4" > file.out
-bash: file: cannot overwrite existing file
若要取消这个限制,使用 set +o noclobber
。例如:
$ set +o noclobber
$ echo "5" > file.out
$ cat file.out
5
还有临时盖写目标档案的方法,在 >
后面再加个 |
即可(注意:>
与 |
之间不能有空白)。例如:
$ set -o noclobber
$ echo "6" >| file.out
$ cat file.out
6
在涉及管道(pipe line)时,|
符号用于连接前后两个命令的 I/O,前一个命令的 stdout
接到下一个命令的 stdin
,但 stderr
不会自动连接,若要处理 stderr
可将其合并到 stdout
(如 2>&1
)。
例如,在 cm1 | cm2 | cm3...
这段 pipe line 中,若要将 cm2
的结果存到某一档案,写成 cm1 | cm2 > file | cm3
是错误的,因为这样会导致 cm3
的 stdin
为空。正确的做法可以是 cm1 | cm2 > file ; cm3 < file
,但这样会使 file I/O 变双倍。更好的方法是使用 tee
命令,例如:
cm1 | cm2 | tee file | cm3
tee
命令在不影响原本 I/O 的情况下,将 stdout
复制一份到档案去,默认会改写目标档案,若要改为增加内容可用 -a
参数达成。
12. 你要 if 还是 case 呢?
还记得在第 10 章介绍的 return value 吗?在 shell script 设计中,经常需要根据不同条件执行不同操作。虽然可以用 &&
与 ||
配合 command group 实现条件执行,但从“人类语言”上理解不够直观,更多时候我们喜欢用 if.... then... else...
这样的 keyword 来表达条件执行。
在 bash shell 中,if
判断式的基本形式为:
if comd1 then
comd2 comd3
else
comd4
comd5
fi
只要 if
后面的命令行返回值为真(通常用 test
命令来送出 return value),就会执行 then
后面的命令,否则执行 else
后的命令;fi
用于结束判断式。else
部分可以不用,但 then
是必需的,若 then
后不想跑任何 command,可用 :
(null command)代替。在 then
或 else
后面还可以再使用更进一层的条件判断式。
例如:
if comd1; then
comd2
elif comd3; then
comd4
else
comd5
fi
表示若 comd1
为 true,则执行 comd2
;否则再测试 comd3
,若为 true 则执行 comd4
;倘若 comd1
与 comd3
均不成立,那就执行 comd5
。
接下来介绍 case
判断式。虽然 if
判断式可应付大部分条件执行,但在某些场合不够灵活,特别是在 string 式样的判断上。
例如:
QQ () {
echo -n "Do you want to continue? (Yes/No): "
read YN
if [ "$YN" = Y -o "$YN" = y -o "$YN" = "Yes" -o "$YN" = "yes" -o "$YN" = "YES" ]
then
QQ
else
exit 0
fi
} QQ
这里判断 YN
的值可能有好几种式样,比较麻烦。可以用 Regular Expression 来简化代码,但还有更方便的方法,就是用 case
判断式。
例如:
QQ () {
echo -n "Do you want to continue? (Yes/No): "
read YN
case "$YN" in
[Yy]|[Yy][Ee][Ss]) QQ
;;
*)
exit 0
;;
esac
} QQ
我们常用 case
的判断式来判断某一变量在不同的值(通常是 string)时作出不同的处理,比如判断 script 参数以执行不同的命令。可以查看 /etc/init.d/*
里的 script 中的 case
用法。
例如:
case "$1" in
start)
start
;;
stop)
stop
;;
status)
rhstatus
;;
restart|reload)
restart
;;
condrestart)
[ -f /var/lock/subsys/syslog ] && restart || :
;;
*)
echo $"Usage: $0 {start|stop|status|restart|condrestart}"
exit 1
esac
13. for what? while 与 until 差在哪?
在 shell script 设计中,循环(loop)是常见的结构。bash shell 中常用的 loop 有 for
、while
和 until
三种。
for
循环是从一个清单列表中读进变量值,并“依次”的循环执行 do
到 done
之间的命令行。
例如:
for var in one two three four five
do
echo
echo '$var is '$var
echo
done
这里 for
会定义一个叫 var
的变量,其值依次是 one two three four five,因为有 5 个变量值,所以 do
与 done
之间的命令行会被循环执行 5 次,每次循环均用 echo
产生三行句子,其中第二行中不在 hard quote 之内的 $var
会依次被替换为 one two three four five,当最后一个变量值处理完毕,循环结束。
如果 for
循环没有使用 in
这个 keyword 来指定变量值清单,其值将从 $@
(或 $*
)中继承。for
循环用于处理“列表”(list)项目非常方便,其清单除了可明确指定或从 positional parameter 取得之外,也可从变量替换或命令替换取得。
例如:
for ((i=1;i<=10;i++))
do
echo "num is $i"
done
对于一些“累计变化”的项目(如整数加减),for
循环也能处理。
而 while
循环的原理与 for
循环不同,它不是逐次处理清单中的变量值,而是取决于 while
后面的命令行之 return value
:
- 若为
true
,则执行do
与done
之间的命令,然后重新判断while
后的return value
; - 若为
false
,则不再执行do
与done
之间的命令而结束循环。
例如:
num=1
while [ "$num" -le 10 ]; do
echo "num is $num"
num=$(($num + 1))
done
在这个例子中,首先定义变量 num = 1
,然后测试 $num
是否小于或等于 10
,结果为 true
,于是执行 echo
并将 num
的值加一。接着再作第二轮测试,此时 num
的值为 2
,依然小于或等于 10
,继续循环,直到 num
为 11
时,测试才会失败,循环结束。
需要注意的是,如果 while
的测试结果永远为 true
,那循环将一直永久执行下去,例如:
while :; do
echo looping...
done
这里的 :
是 bash
的 null command
,不做任何动作,除了送回 true
的 return value
,所以这个循环不会结束,称作死循环。死循环的产生有可能是故意设计的(如跑 daemon
),也可能是设计错误。若要结束死循环,可透过 signal
来终止(如按下 ctrl - c
)。
与 while
相反,until
是在 return value
为 false
时进入循环,否则结束。例如:
num=1
until [! "$num" -le 10 ]; do
echo "num is $num"
num=$(($num + 1))
done
也可以写成:
num=1
until [ "$num" -gt 10 ]; do
echo "num is $num"
num=$(($num + 1))
done
在 bash
中,还有两个与 loop
有关的命令:break
和 continue
。
break
是用来打断循环,也就是“强迫结束”循环。若 break
后面指定一个数值 n
,则“从里向外”打断第 n
个循环,默认值为 break 1
,也就是打断当前的循环。在使用 break
时需要注意的是,它与 return
(结束 function
)和 exit
(结束 script/shell
)是不同的。
continue
则与 break
相反,强迫进入下一次循环动作。若理解不来,可以简单看成在 continue
到 done
之间的句子略过而返回循环顶端。与 break
相同的是,continue
后面也可指定一个数值 n
,以决定继续哪一层(从里向外计算)的循环,默认值为 continue 1
,也就是继续当前的循环。
在 shell script
设计中,若能善用 loop
,将能大幅度提高 script
在复杂条件下的处理能力,所以需要多加练习。
至此,关于 bash
的基础概念就介绍完了。希望这些内容能给读者带来启发,并且在日后的实践中加深理解。更重要的是,要通过实际操作来巩固所学知识,提高自己在 shell
编程方面的技能。
------
补充问题: b1) [^ ] 跟 [! ] 差在哪?
本文旨在探讨通配符(Wildcard)与正则表达式(Regular Expression)的区别。这是许多初学者容易混淆的问题。
在开始之前,先回顾一下十三问之第 2 问中命令行格式 command_name options arguments
,即“命令名 选项 参数
”,以及第 5 问中变量替换特性,即先进行变量替换,再重组命令行
。
基于以上基础,探讨通配符的原理。
Part-I: Wildcard,通配符
網中人 发表于 2004-11-05 21:40
通配符是命令行处理的一部分,仅作用于参数中的路径,不适用于命令名或选项。若参数非路径,则与通配符无关。更精确地说,通配符是一种命令行路径扩展功能。提到扩展,不可忽视命令行的“重组”特性,这与变量替换和命令替换的重组特性相同,即通配符扩展后,命令行先完成重组再交由 shell 处理。
常见的通配符有以下几种:
*
:匹配 0 或多个字符。?
:匹配任意单一字符。[list]
:匹配 list 中的任意单一字符。其中,list 可为指定的个别字符,如 abcd;也可为一段 ASCII 字符的起止范围,如 a-d。[!list]
:匹配不在 list 中的任意单一字符。{string1,string2,...}
:匹配 string1 或 string2(或更多)中的任一字符串。
例如:
a*b
:a 与 b 之间可有任意长度的任意字符,也可无,如 aabcb、axyzb、a012b、ab 等。a?b
:a 与 b 之间必须且仅有一个字符,可为任意字符,如 aab、abb、acb、a0b 等。a[xyz]b
:a 与 b 之间必须且仅有一个字符,但仅限 x、y 或 z,如 axb、ayb、azb。a[!0-9]b
:a 与 b 之间必须且仅有一个字符,但不能是阿拉伯数字,如 axb、aab、a-b 等。a{abc,xyz,123}b
:a 与 b 之间只能是 abc、xyz 或 123 中的一个字符串,如 aabcb、axyzb、a123b。
注意事项如下:
[! ]
中的!
仅在首位时才具排除功能。例如:[!a]*
:表示当前目录下所有不以 a 开头的路径名称。/tmp/[a\!]*
:表示 /tmp 目录下以 a 或 ! 开头的路径名称。其中,!
前需加\
,是因为在十三问之 4 中提到,!
在命令行中有特殊含义,需用\
进行转义。
[ -]
中的-
仅在左右均有字符时才表示范围,否则仅作减号处理。例如:/tmp/*[-z]/[a-zA-Z]*
:表示 /tmp 目录下所有以 z 或 - 结尾的子目录下以英文字母(不分大小写)开头的路径名称。
- 以
*
或?
开头的通配符不能匹配隐藏文件(即以 . 开头的文件)。例如:*.txt
不能匹配.txt
,但可匹配1.txt
。1*txt
和1?txt
均可匹配1.txt
。
掌握通配符并不困难,只需多加练习,勤于思考,便能熟练运用。再次提醒:别忘“扩展+重组”这一重要特性,且仅作用于参数的路径。例如,当前目录下有 a.txt、b.txt、c.txt、1.txt、2.txt、3.txt 这几份文件。执行 ls -l [0-9].txt
命令时,因通配符处于参数位置,根据匹配的路径扩展为 1.txt 2.txt 3.txt
,再重组为 ls -l 1.txt 2.txt 3.txt
命令。因此,ls -l [0-9].txt
与 ls -l 1.txt 2.txt 3.txt
结果相同,原因即在于此。
顺带提及:eval
谈及命令行的重组特性,需深入理解。如此可逐步剖析整个命令行,避免含糊。若理解重组特性,接下来介绍一个有趣的命令——eval。在变量替换过程中,常遇复式变量问题,如:
a=1
A1=abc
已知 echo $A1
可得结果 abc。能否用 $A$a
代替 $A1
以输出 abc?此问题可用 eval 轻松解决:
eval echo \$A$a
eval 仅在命令行完成替换重组后,再次进行替换重组。
[ 本帖最后由 網中人 于 2008-11-4 02:02 编辑 ]
Part-II: Regular Expression,正则表达式
網中人 发表于 2004-11-07 20:56
正则表达式是复杂主题,此处仅作基础入门介绍。先考英文:What is expression?简言之,即“表达”,即沟通时所陈述的内容。然而,清晰表达并让接收方准确理解非易事。类似情形也出现在计算机数据处理中,尤其在描述文本内容时…那么,如何降低误解,提高表达精确度呢?答案是“标准化”,即正则表达式。
进入正则表达式介绍前,先回顾 shell 十三问第 4 问,关于引用的部分。关键在于区分命令行上的元字符与字面量字符。正则表达式表达式中的字符也分为这两种!不知读者是否困惑… 这实属正常,因极易混淆,初学者常在此处受阻!请特别注意理解… 简言之,除非将正则表达式写入特定程序脚本,否则,正则表达式也通过命令行输入。然而,不少正则表达式元字符与 shell 元字符冲突。例如,*
在正则表达式中是修饰符,在命令行中却是通配符!
如何解决此冲突?关键在于对十三问第 4 问中引用的理解!若明白 shell 引用是在命令行上关闭 shell 元字符的基本原理,则可轻松解决正则表达式元字符与 shell 元字符的冲突:使用 shell 引用关闭 shell 元字符即可!就这么简单… 仍以 *
为例,若在命令行中未引用处理,如 abc*
,则会被视为通配符扩展及重组。若置于引号中,如 "abc*"
,则可避免通配符扩展处理。
说了许久,还未正式介绍正则表达式… 读者勿急,因教学风格是先打好基础,逐步深入… 因此,还需再啰唆一个观念,才会进入正则表达式说明… 谈及正则表达式时,切勿与通配符混淆!尤其在命令行中,通配符仅作用于参数的路径。而正则表达式仅用于“字符串处理”程序中,与路径名称无关!正则表达式处理的字符串通常是纯文本或通过 stdin 读入的内容…
在正则表达式表达式中,字符主要分为两种:字面量与元字符。字面量在正则表达式中无特殊功能,如 abc
、123
等;元字符在正则表达式中具有特殊功能,需在元字符前加转义字符(\)以关闭其功能。
介绍元字符前,先了解字符集概念。字符集是将多个连续字符组成一个集合,例如:
abc
:表示三个连续字符,彼此独立,非集合。(可视为三个字符集)(abc)
:表示abc
三个连续字符的集合。(可视为一个字符集)abc|xyz
:表示abc
或xyz
两个字符集之一。[abc]
:表示单一字符,可为a
、b
或c
。(与通配符[abc]
原理相同)[^abc]
:表示单一字符,不能为a
、b
或c
。(与通配符[!abc]
原理相同).
:表示任意单一字符。(与通配符?
原理相同)
了解字符集后,再认识几个正则表达式中常见元字符:
- 锚点 标识正则表达式在句子中的位置。常见有:
^
:表示句首。如^abc
表示以abc
开头的句子。$
:表示句尾。如abc$
表示以abc
结尾的句子。\<
:表示词首。如\<abc
表示以abc
开头的词。\>
:表示词尾。如abc\>
表示以abc
结尾的词。
- 修饰符 独立时无意义,用于修改前一个字符集的出现次数。常见有:
*
:表示前一个字符集出现 0 或多次。如ab*c
表示a
与c
之间可有 0 或多个b
。?
:表示前一个字符集出现 0 或 1 次。如ab?c
表示a
与c
之间可有 0 或 1 个b
。+
:表示前一个字符集出现 1 或多次。如ab+c
表示a
与c
之间可有 1 或多个b
。{n}
:表示前一个字符集出现n
次。如ab{3}c
表示a
与c
之间必须有 3 个b
。{n,}
:表示前一个字符集出现至少n
次。如ab{3,}c
表示a
与c
之间至少有 3 个b
。{n,m}
:表示前一个字符集出现n
到m
次。如ab{3,5}c
表示a
与c
之间有 3 到 5 个b
。
识别修饰符时,易忽略“边界”字符的重要性。以 ab{3,5}c
为例,a
与 c
是边界字符。若无边界字符,易误解。例如,用 ab{3,5}
(缺 c
边界字符)能否匹配 abbbbbbbbbbc
(a 后有 10 个 b)?按修饰符,一般认为 b 应为 3 到 5 个,超此范围则不符。故可能认为此正则表达式无法匹配(上述“abbbbbbbbbbc”字符串)…然而,答案是可以!为何呢?重新解读 ab{3,5}
:表达的是 a 后接 3 到 5 个 b 即可,但 3 到 5 个 b 后未规定内容,因此正则表达式后可为任意文字,包括 b。(明白了吗?)同样,b{3,5}c
也可匹配 abbbbbbbbbbc
。但若用 ab{3,5}c
,因有 a
与 c
两个边界字符,情况则大不相同!
思考为何以下正则表达式均可匹配 abc
:
x*
ax*
abx*
ax*b
abcx*
abx*c
ax*bc
bx*c
bcx*
x*bc
- …(还有更多…) 但,若在这些正则表达式前后分别加
^
与$
锚点,结果又如何呢?
初学正则表达式时,掌握以上基本元字符即可入门。正则表达式是规范化的文字表达方式,主要用于文字处理工具,如 grep、perl、vi、awk、sed 等。常用于表示连续字符串,进行捕获或替换。然而,各工具对正则表达式表达式的具体解读或有细微差异,但基本原则一致。掌握正则表达式基本原理,即可触类旁通,实际应用时稍作调整即可。
以 grep 为例,Linux 上有 grep、egrep、fgrep 等程序,差异如下:
- grep 传统 grep 程序,无参数时,仅输出符合正则表达式字符串的句子。常见参数如下:
-v
:逆向匹配,输出不含正则表达式字符串的句子。-r
:递归模式,处理所有层级子目录中的文件。-q
:静默模式,不输出任何结果(stderr 除外。常用于获取返回值,符合为 true,否则为 false)。-i
:忽略大小写。-w
:整词匹配,类似\<word\>
。-n
:输出行号。-c
:输出符合匹配的行数。-l
:输出符合匹配的文件名。-o
:输出符合正则表达式的字符串。(gnu 新版独有,非所有版本支持。)-E
:切换为 egrep。
- egrep grep 的扩展版本,改进了传统 grep 的诸多不便。例如:
- grep 不支持
?
与+
修饰符,egrep 支持。 - grep 不支持
a|b
或(abc|xyz)
这类“或”匹配,egrep 支持。 - grep 处理
{n,m}
时需用\{
与\}
,egrep 不需。 个人建议,能用 egrep 就不用 grep。
- grep 不支持
- fgrep 不进行正则表达式处理,表达式仅作普通字符串处理,所有元字符均失效。
关于正则表达式的入门介绍至此。虽内容稍显杂乱,部分观念不够精确,但姑且算作一个交代。
[ 本帖最后由 网中人 于 2009-3-24 10:25 编辑 ]
via:
作者:网中人
整理:HAWK.Li
-
shell 十三問? - Shell-Chinaunix 網中人 表于 2003-12-09 02:48
http://bbs.chinaunix.net/thread-218853-1-1.html -
shell 十三問? - 补充问题: b1) [^ ] 跟 [! ] 差在哪? - Shell-Chinaunix 发表于 2004-11-05 21:40
http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=218853&page=16#pid2930144