shell 十三问

注:作者原文为繁体行文,部分术语与简体有出入

本文在原 HAWK.Li 整理的基础上修订了部分口语化描述,增订了 “补充问题” 部分,如有内容异常,请看原文。


Shell 十三问

網中人 发表于 2003-12-09 更新于 2008-10-30

目录

  1. 为何叫做 shell?
  2. shell prompt(PS1) 与 Carriage Return(CR) 的关系?
  3. 别人 echo、你也 echo,是问 echo 知多少?
  4. " "(双引号)与’ '(单引号)差在哪?
  5. var=value?export 前后差在哪?
  6. exec 跟 source 差在哪?
  7. ( ) 与 { } 差在哪?
  8. $(( )) 与 $( ) 还有 ${ } 差在哪?
  9. $@ 与 $* 差在哪?
  10. && 与 || 差在哪?
  11. > 与 < 差在哪?
  12. 你要 if 还是 case 呢?
  13. 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 命令支持的反斜杠控制字符如下表:

控制字符含义
\aALERT / BELL(从系统喇叭送出铃声)
\bBACKSPACE,向左删除键
\c取消行末之换行符号
\EESCAPE,跳脱键
\fFORMFEED,换页字符
\nNEWLINE,换行字符
\rRETURN,回车键
\tTAB,表格跳位键
\vVERTICAL TAB,垂直表格跳位键
\oASCII 八进制编码(以 0 开首)
\xASCII 十六进制编码(以 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 并观察结果,可以发现它们之间的差异。

总之,execsource/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

由于在 p2p3 之间的空格键被 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" 后,可以看到函数内的 $0my.sh$1fp1 而非 p1

在使用 positional parameter 的时候,要注意一些陷阱。例如,$10 不是替换第 10 个参数,而是替换第一个参数($1)然后再补一个 0 于其后。要获取第 10 个参数,可以使用 ${10},或者通过 shift 命令来实现。shift 命令的默认值为 1,即 shiftshift 1 都是取消 $1,而原本的 $2 则变成 $1$3 变成 $2… 若 shift 3 则是取消前面三个参数,原本的 $4 将变成 $1

接下来看 $#,它可抓出 positional parameter 的数量。以前面的 my.sh p1 "p2 p3" 为例,由于 p2p3 之间的 IFS 是在 soft quote 中,因此 $# 可得到 2 的值。如果 p2p3 没有置于 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 testman 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

这样监视器上就什么也没有了,因为 stdoutstderr 都被转到档案去了。

在重定向过程中存在一些问题,比如 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 中很有用:

  • 若将 FD1FD2 转到 /dev/null 去,就可将 stdoutstderr 弄不见掉;
  • 若将 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 是错误的,因为这样会导致 cm3stdin 为空。正确的做法可以是 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)代替。在 thenelse 后面还可以再使用更进一层的条件判断式。

例如:

if comd1; then
    comd2
elif comd3; then
    comd4
else
    comd5
fi

表示若 comd1 为 true,则执行 comd2;否则再测试 comd3,若为 true 则执行 comd4;倘若 comd1comd3 均不成立,那就执行 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 有 forwhileuntil 三种。

for 循环是从一个清单列表中读进变量值,并“依次”的循环执行 dodone 之间的命令行。

例如:

for var in one two three four five
do
    echo
    echo '$var is '$var
    echo
done

这里 for 会定义一个叫 var 的变量,其值依次是 one two three four five,因为有 5 个变量值,所以 dodone 之间的命令行会被循环执行 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,则执行 dodone 之间的命令,然后重新判断 while 后的 return value
  • 若为 false,则不再执行 dodone 之间的命令而结束循环。

例如:

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,继续循环,直到 num11 时,测试才会失败,循环结束。

需要注意的是,如果 while 的测试结果永远为 true,那循环将一直永久执行下去,例如:

while :; do
    echo looping...
done

这里的 :bashnull command,不做任何动作,除了送回 truereturn value,所以这个循环不会结束,称作死循环。死循环的产生有可能是故意设计的(如跑 daemon),也可能是设计错误。若要结束死循环,可透过 signal 来终止(如按下 ctrl - c)。

while 相反,until 是在 return valuefalse 时进入循环,否则结束。例如:

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 有关的命令:breakcontinue

break 是用来打断循环,也就是“强迫结束”循环。若 break 后面指定一个数值 n,则“从里向外”打断第 n 个循环,默认值为 break 1,也就是打断当前的循环。在使用 break 时需要注意的是,它与 return(结束 function)和 exit(结束 script/shell)是不同的。

continue 则与 break 相反,强迫进入下一次循环动作。若理解不来,可以简单看成在 continuedone 之间的句子略过而返回循环顶端。与 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。

注意事项如下:

  1. [! ] 中的 ! 仅在首位时才具排除功能。例如:
    • [!a]*:表示当前目录下所有不以 a 开头的路径名称。
    • /tmp/[a\!]*:表示 /tmp 目录下以 a 或 ! 开头的路径名称。其中,! 前需加 \,是因为在十三问之 4 中提到,! 在命令行中有特殊含义,需用 \ 进行转义。
  2. [ -] 中的 - 仅在左右均有字符时才表示范围,否则仅作减号处理。例如:
    • /tmp/*[-z]/[a-zA-Z]*:表示 /tmp 目录下所有以 z 或 - 结尾的子目录下以英文字母(不分大小写)开头的路径名称。
  3. *? 开头的通配符不能匹配隐藏文件(即以 . 开头的文件)。例如:
    • *.txt 不能匹配 .txt,但可匹配 1.txt
    • 1*txt1?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].txtls -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 读入的内容…

在正则表达式表达式中,字符主要分为两种:字面量与元字符。字面量在正则表达式中无特殊功能,如 abc123 等;元字符在正则表达式中具有特殊功能,需在元字符前加转义字符(\)以关闭其功能。

介绍元字符前,先了解字符集概念。字符集是将多个连续字符组成一个集合,例如:

  • abc:表示三个连续字符,彼此独立,非集合。(可视为三个字符集)
  • (abc):表示 abc 三个连续字符的集合。(可视为一个字符集)
  • abc|xyz:表示 abcxyz 两个字符集之一。
  • [abc]:表示单一字符,可为 abc。(与通配符 [abc] 原理相同)
  • [^abc]:表示单一字符,不能为 abc。(与通配符 [!abc] 原理相同)
  • .:表示任意单一字符。(与通配符 ? 原理相同)

了解字符集后,再认识几个正则表达式中常见元字符:

  • 锚点 标识正则表达式在句子中的位置。常见有:
    • ^:表示句首。如 ^abc 表示以 abc 开头的句子。
    • $:表示句尾。如 abc$ 表示以 abc 结尾的句子。
    • \<:表示词首。如 \<abc 表示以 abc 开头的词。
    • \>:表示词尾。如 abc\> 表示以 abc 结尾的词。
  • 修饰符 独立时无意义,用于修改前一个字符集的出现次数。常见有:
    • *:表示前一个字符集出现 0 或多次。如 ab*c 表示 ac 之间可有 0 或多个 b
    • ?:表示前一个字符集出现 0 或 1 次。如 ab?c 表示 ac 之间可有 0 或 1 个 b
    • +:表示前一个字符集出现 1 或多次。如 ab+c 表示 ac 之间可有 1 或多个 b
    • {n}:表示前一个字符集出现 n 次。如 ab{3}c 表示 ac 之间必须有 3 个 b
    • {n,}:表示前一个字符集出现至少 n 次。如 ab{3,}c 表示 ac 之间至少有 3 个 b
    • {n,m}:表示前一个字符集出现 nm 次。如 ab{3,5}c 表示 ac 之间有 3 到 5 个 b

识别修饰符时,易忽略“边界”字符的重要性。以 ab{3,5}c 为例,ac 是边界字符。若无边界字符,易误解。例如,用 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,因有 ac 两个边界字符,情况则大不相同!

思考为何以下正则表达式均可匹配 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。
  • fgrep 不进行正则表达式处理,表达式仅作普通字符串处理,所有元字符均失效。

关于正则表达式的入门介绍至此。虽内容稍显杂乱,部分观念不够精确,但姑且算作一个交代。

[ 本帖最后由 网中人 于 2009-3-24 10:25 编辑 ]


via:

作者:网中人
整理:HAWK.Li

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值