注:作者原文为繁体行文,部分术语与简体有出入。
本文在原 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*babcx*abx*cax*bcbx*cbcx*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
1万+

被折叠的 条评论
为什么被折叠?



