Shell script (源自精华帖 shell ——十三问)

目录

 

一、为何叫做shell

二、shell prompt(PS1)与 Carriage Return(CR)的关系

四、" "(双引号)与 ' ' (单引号)差在哪?

五、var=value 与 export 前后差在哪?

①:*变量的设定

②:变量替换(substitution)

③:export

④:取消变量

六:exec 与 source 差在哪?

七:( )与 { } 差在哪?

八:$(( )) 与 $( ) 还有 ${ } 差在哪?

九: $@ 与 $* 差在哪?

十:&& 与 || 差在哪?

十一:> 与 < 差在哪?

十二:shell 中的 if  与 case 分支

十三:循环结构

 

13.1 for循环

13.2 while 循环

13.3 until 循环

十四:[^ ] 与 [! ] 差在哪?

14.1 Wildcard(通配符)

 14.2 Regular Expression


一、为何叫做shell

       在介绍shell是什么东西之前,不妨让我们重新检视使用者与计算机系统的关系。我们知道计算机的运作离不开硬件,但使用者却无法直接对硬件做驱动,硬件的驱动只能透过一个称为“操作系统(Operating System)”的软件来管控,事实上,我们每天所谈的Linux,严格来说只是一个操作系统“内核(kernel)”。然而,从使用者的角度来说,使用者也无法直接操作kernal,而是透过kernal的“外壳”程序,也就是所谓的shell,来与kernal沟通,这也正是kernal与shell的形象命名关系。
       从技术角度来说,shell是一个使用者与系统的互动界面(interface),主要是让使用者透过命令行(command line)来使用系统以完成工作。
       因此,shell最简单的定义就是——“命令解释器(Command Interpreter)”
  •  将使用者的命令翻译给核心处理
  • 将“核心”的处理结果翻译给使用者
   每次当我们完成系统登入,我们就取得一个互动模式的shell,也称为 login shell 或 primary shell。
   若从进程的角度来说,我们在shell所下达的命令,均是shell所产生的子进程。这种现象我们可以称之为“fork”。
如果是执行脚本(shell script)的话,脚本中的命令则是由另外一个非互动模式的子shell(sub shell)来执行的。也就是 primary shell产生sub shell的进程,sub shell 再产生 script 中所有命令的进程。
这里,我们必须要知道,kernal 与 shell 是不同的两套软件,而且都是可以被替换的。
  •       不同的操作系统使用不同的kernal
  •       在同一个kernal之上,也可使用不同的shell
        在linux的预设系统中,通常都可以找到好几种不同的shell,且通常会被列于如下档案里:/etc/shells不同的shell有着不同的功能,且彼此各异,或者说“大同小异”。
常见的 shell 主要分为两大主流:
        sh:
              burne shell(sh)
              burne again shell(bash)
      csh:
             c shell(csh)
             tc shell(tcsh)
             korn shell(ksh)
大部分Linux系统预设的shell都是bash,其原因大致如下两点:
  •         自由软件
  •         功能强大
 

二、shell prompt(PS1)与 Carriage Return(CR)的关系

        当你成功登陆进一个文字界面之后,大部分情形下,你会在荧幕上看到一个不断闪烁的方块或底线,我们称之为“游标(coursor)”。游标的作用就是告诉你接下来你从键盘输入的按键所插入的位置,且每输入一键游标便向右边移动一个格子,若连续输入太多的话,则自动接在下一行输入。
        假如你刚完成登录还没输入任何按键之前,你所看到的游标所在位置的同一行的左边部分,我们称之为“提示符号(prompt)”。提示符号的格式或因不同系统版本而各不相同,在Linux上,只需留意最接近游标的一个可见的提示符号,通常是如下两者之一:
  • $:给普通用户账号使用
  • #:给root(管理员)账号使用
事实上,shell prompt的意思很简单:是shell告诉使用者,您现在可以输入命令行了。
我们可以说,使用者只有在得到 shell prompt 才能打命令行,而 cursor 是指示键盘在命令行 所输入的位置,使用者每输入一个键,cursor 就往后移动一格,直到碰到命令行读进 CR(Carriage Return,由Enter键产生)字符为止。
CR的意思也很简单:
        *是使用者告诉 shell :老兄你可以执行我的命令行了
严格来说:
        *所谓命令行,就是在 shell prompt 与 CR字符之间所输入的文字
不同的命令可接受的命令行格式或有不同,一般情况下,一个标准的命令行格式为如下所列:
        Command-name Options Argument
若从技术细节来看,shell 会依据 IFS ( Internal Field Seperator) 将 command line 所输入的文字给拆解为“字段(word)”。
然后再针对特殊字符(meta)先做处理,最后再重组整行 command line。
其中的 IFS 是 shell 预设使用的字段分隔符,可以由一个及多个如下按键组成:
  • 空格键(White Space)
  • 表格键(Tab)
  • 回车键(Enter)
系统可接受的命令名称(command-name)可以从如下途径获得:
  • 明确路径所指定的外部命令
  • 命令别名(alias)
  • 自定功能(function)
  • shell 内建命令(built-in)
  • $PATH 之下的外部命令
每一个命令行均必须含有命令名称,这是不能缺少的
 
三、别人 echo 你也 echo ,试问 echo 知多少?
承接上一章所介绍的 command line ,这里我们用 echo 这个命令进一步加以说明。
echo 是一个非常简单、直接的 Linux 命令:
        *将 argument 送出至标准输出(STDOUT),通常就是显示器( monitor )上输出。
为了更好的理解,不如让我们先跑一下 echo 命令好了:
CODE:
$ echo
 
$
你会发现只有一个空白行,然后就回到 shell prompt上了。
这是因为 echo 在预设上,在显示完  argument 之后,还会送出一个换行符号( new-line charactor)。
但是上面的 command 并没有任何的 argument ,那结果就只剩一个换行符号了............
若你要取消这个换行符号,可利用 echo 的 -n option:
CODE:
$ echo -n
$
不妨让我们回到 command line 的概念上来讨论上例的 echo 命令好了:
command line 只有 command_name( echo ) 及 option(-n),并没有任何 argument。
要想看看 echo 的 argument,那还不简单:接下来,你可以试试如下的输入:
CODE:
$ echo first line
first line
$ echo -n first line
first line $
于上两个 echo 命令中,你会发现 argument 的部分显示在了你的屏幕上,而换行符则视 -n option 的有无而别。
很明显的,第二个 echo 由于换行符号被取消了,接下来的 shell prompt 就接在输出结果同一行了.......
事实上,echo 除了 -n option 之外,常用选项还有:
  • -e :启用反斜线控制字符的转换(参考下表)
  • -E :关闭反斜线控制字符的转换(预设如此)
  • -n :取消行末的换行符号(与 -e 选项下的 \c 字符同义)
关于 echo 命令所支持的反斜线控制字符如下表:
  • \a :ALERT(从系统喇叭送出的铃声)
  • \b :Backspace,也就是向左删除键
  • \c :取消行末的换行符
  • \E:ESCAPE,跳脱键
  • \f :FORMFEED,换页字符
  • \n:NEWLINE,换行字符
  • \r :RETURN ,回车键
  • \t :TAB,表格跳位键
  • \\:反斜线本身
或许,我们可以透过实例来了解 echo 的选项及控制字符:
例一:
$ echo -e "a\tb\tc\nd\te\tf"
a    b    c
d    e    f
上例运用 \t 来区隔 abc 还有 def ,及用 \n 将 def 换至下一行。
 

 
例二:
$ echo -ne "a\tb\tc\nd\te\bf\a"
a    b    c
d    f $
因为 e 字母后面是删除键( \b ),因此输出结果中就没有 e 了。
在结束时听到的一声响铃,就是\a的杰作!
由于同时使用了 -n 选项,因此 shell prompt 紧接在第二行之后。
若你不用 -n 的话,那你在 \a 后面再加个 \c ,也是同样的效果。
 

 
例三:
$ echo -e "a\tb"
a    b
$ echo -e a\tb    
atb
$ echo -e a\\tb
a    b
上例中使用了 "" 与  \  这两个command line 中的 meta ,大家可以先记住这个例子,在下一章中我会讲解 "" 与  \ 功能。
 

 
事实上,在日后的 shell 操作及 shell script设计上,echo 命令是最常用的命令之一了。
比方说,用 echo 来检查变量值:
$ a=b
$ echo $a
b
$ echo $?
0
好了,更多关于 command line 的格式,以及 echo 命令的选项,就请您自行多加练习、运用了.........
 

四、" "(双引号)与 ' ' (单引号)差在哪?

还是回到我们的 command line 来吧...........
        经过前面两章的学习,应该很清楚当你在 shell prompt 后面敲打键盘、直到按下 Enter 的时候,你输入的文字就是 command line了,然后 shell 才开始以进程的方式执行你所交给它的命令。但是,你又可知道:你在 command line 输入的每一个文字,对 shell 来说,是有类别之分的呢?  
简单而言,command line 的每一个 charactor ,分为如下两种:
  • literal :也就是普通纯文字,对 shell 来说没有特殊功能。
  • meta:对 shell 来说,具有特定特定功能的特殊保留字符。
Literal 没什么好谈的,凡举 abcd、12345 这些“文字”都是 literal .....
但  meta 却常常使我们感到困惑 .....
事实上,前两章我们在 command line 中已经碰到两个几乎每次都会遇到的 meta:
  • IFS :由 <space> 或 <tab> 或 <enter> 三者之一组成(我们常用 space)。
  • CR :由 <ENTER> 产生
IFS 是用来拆解 comand line 的每一个词(word)用的,因为 shell command line 是按词来处理的。
而 CR 则是用来结束 command line 用的,这也是为何我们敲 <enter> 命令就会执行命令的原因。
除了 IFS 和 CR ,常用的 meta 还有:
  • = :设定变量
  • $ : 做变量或运算替换
  • > : 重导向 stdout
  • < :重导向 stdin
  • | : 管道符
  • & : 重导向 file descriptor,或将命令置于后台运行
  • ( ): 将其内的命令置于 nested subshell 执行,或用于运算或命令替换。
  • { } :将其内的命令置于 non-named function 中执行,或用在变量替换的界定范围。
  • ;  : 在前面一个命令结束时,忽略其返回值,继续执行下一个命令。
  • && : 在前面一个命令结束时,若返回值为 true ,继续执行下一个命令。
  • || :在前面一个命令结束时,若返回值为 false,继续执行下一个命令。
  • !:执行 history 列表中的命令。
         ..............................
假如我们需要在 command line 中将这些保留字符的gongneng关闭的话,就需要 quoting 处理了。
在 bash 中常用的 quoting 有如下三种方法:
  • hard quote : ' '(单引号),凡在 hard quote 中的所有 meta 均被关闭
  • soft quote:" "(双引号),在 soft quote 中大部分 meta 都会被关闭,但某些则保留(如 $)
  • escape : \ (反斜线),只有紧接在 escape (跳脱字符)之后的单一 meta 才被关闭。
下面通过几个例子来加深对 quoting 的理解:
$ a= b c                         #空格键未被关闭,作为 IFS 处理 
$ command not found.
$ echo $a
 
$ a="b c"                      # 空格键的特殊功能已被关闭,仅作为空格键处理  
$ echo $a
b c
事实上,空格键无论在 soft quote 还是 hard quote ,均会被关闭。 Enter键亦然:
$ a='b
>c
>'       #  Enter 键被置于单引号中,因此不再作为 CR 字符处理。
$ echo "$a"
b
c
这里的 <enter> 单纯只是一个断行符号<new-line>而已,由于 command line 并没得到 CR 字符,因此进入第二个 shell prompt(PS2,以 > 符号表示),command line 并不会结束,直到第三行,我们输入的 <enter> 并不在 单引号里面,因此并没有被关闭,此时,command line 碰到 CR字符,于是结束,交给 shell 来处理。
 
上例中的 <enter> 要是被置于 soft quote 中的话,CR  也同样会被关闭。
$ a="b
>c
>"     
$ echo $a
b c
然而,由于 echo $a 时的变量没置于 双引号中,因此当变量替换完成后并作命令重组时,<enter>会被解释为 IFS ,而不是解释为 New Line字符。
此处是不是有点费解,哈哈.....
ENTER 这“家伙”的特殊功能太多了,既是 CR 又是 IFS,原义又是换行~~~,
 
同样的,用 escape 亦可关闭 CR 字符
$ a=b\
>c\
>
$ echo $a
bc
上例中,第一个 <enter> 跟第二个 <enter> 均被 escape 字符关闭了,因此也不作为 CR 来处理,但第三个<enter>由于没被跳脱,因此作为 CR 结束 command line 。但由于<enter> 键本身在 shell meta中的特殊性,在 \ 跳脱后面,不仅取消其 CR 功能,还会取消其 IFS 功能。
 
估计很多同学看到这里就不淡定了,什么乱七八糟的~~,此处如过不是很懂就先打个问号继续往后面看吧.........,等后面学了变量的替换、命令行的重组再回过头慢慢思索吧!
 
至于单引号与双引号的不同,主要是对于某些 meta 的关闭与否,以 $ 来作说明:
例一:
$ a=b\ c
$ echo "$a"
b c
$ echo '$a'
$a
在第一个 echo 命令行中,$ 被置于 双引号中,将不被关闭,因此继续处理变量替换。
因此 echo 将 a 的变量值输出到屏幕,也就得到 b c 的结果。
在第二个 echo 命令行中, $ 被置于单引号中,其功能被关闭,因此 $ 只是一个普通字符,并不会用来作变量替换处理,因此结果是 $ 符号后面接一个字母 a:$a 。
 
例二:
$ a=b\ c
$ echo '"$a"'  # 最外面的是单引号
"$a"
 
$ echo "'$a'"  # 最外面是双引号,单引号的功能被关闭了,$ 的功能不会被关闭
'b c'
 
在 shell 里面很多初学者的问题都与 quoting 的理解有关。
比方说,若我们在 awk 或 sed 的命令参数中调用之前设定的一些变量时,常会问及为何不能的问题。
解决这些问题的关键点就是:区分出 shell meta 与 command meta。
前面我们提到的那些 meta ,都是在 command line 中有特殊用途的。
比方说 { } 是将其内一系列 command line 置于不具名的函式中执行(可简单视为command block),但是,awk 却需要用 { } 来区分出 awk 的命令区段(BEGIN,MAIN,END)。
若你在 command line 中如此输入:
$ awk {print $0} a.txt
由于 { } 在 shell 中并没关闭,那 shell 就将 {print $0} 视为 command block,但同时又没有 “ ; ” 符号作命令区隔,因此就出现 awk 的语法错误结果。
要解决之,可以用单引号:
$ awk '{print $0}' a.txt
上面的 单引号 应该好理解,就是将原本的 { 、<space> 、 $ 、} 这几个 shell meta 关闭,避免掉在 shell 中遭到处理,而完整的成为 awk 参数中的 command meta。
注意:其中的 $0 是 awk 内建的 field number ,而非 awk 的变量,awk 自身的变量无需使用 $。
要是理解了 单引号的功能,再来理解 双引号 与 escape 就不难:
$ awk "{print \$0}" a.txt
$ awk \{print\ \$0\} a.txt
然而,若你要改变 awk 的 $0 的 0 值是从另一个 shell 变量读进呢?
比方说,已有变量 $a 的值是 0 ,那如何在 command line 中解决 awk 的 $$a 呢?
你可以直接否定掉 单引号 的方案:
$ awk '{print $$a}' a.txt
那是因为 $a 的 $ 在单引号中是不能替换变量的。
聪明的你应该可以想到如下操作吧~~
a=0
awk "{print \$$a}" a.txt
awk \{print\ \$$a\} a.txt
awk '{print $'$a'}' a.txt
awk '{print $'"$a"'}' a.txt
 

五、var=value 与 export 前后差在哪?

这次让我们暂时丢开 command line ,先来了解一下 bash 变量(variable)吧......
所谓的变量,就是利用一个特定的“名称”来存取一段可以变化的值。

①:*变量的设定

在 bash 中,你可以用 “=”来设定或重新定义变量的内容。   
        name=value
在设定变量的时候,必须遵守如下规则:
  • 等号左右两边不能使用区隔符号(IFS),也应避免使用 shell 的保留字符(meta charactor)。
  • 变量名称不能使用 $ 符号
  • 变量名称的第一个字符不能是数字
  • 变量名称长度不可超过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   # 可用 _ 连接较长的名称或值,且大小写有别。

②:变量替换(substitution)

Shell 之所以强大,其中的一个因素是它可以在命令行中对变量作替换处理。在命令行中使用者可以使用 $ 符号加上变量名称,将变量值给替换出来,然后重新组建命令行。
比方:
$ a=ls
$ b=la
$ c=/tmp
$ $a -$b $c
(注意:以上命令行的第一个 $ 是 shell prompt,并不在命令行之内。)
必须强调的是,我们所提的变量替换,只发生在 command line 上面,仔细分析最后那行 command line ,不难发现在被执行之前(在输入 CR 字符之前),$ 符号会对每一个变量作替换处理(将变量值替换出来再重组命令行),最后会得出如下命令行:
ls -la /tmp
务必要理解的一句话:
  • 若从技术细节上来看,shell会依据 IFS 将 command line 所输入的文字给拆解为“字段”(word),然后再针对特殊字符(meta)先做处理,最后再重组命令行。
这里的 $ 就是 command line 中最经典的 meta 之一了,就是作变量替换的;
在日常的 shell 操作中,我们常会使用 echo 命令来查看特定变量的值,例如:
$ echo $a -$b $c
我们已经学过,echo 命令只单纯将其 argument 送至“标准输出”(STDOUT,通常是我们的屏幕),所以上面的命令会在屏幕上得到如下结果:
ls -la /tmp
这是由于 echo 命令在执行时,会先将$a ( ls ),$b( la ),跟 $c( /tmp ) 给替换出来的结果。
利用 shell 对变量的替换处理能力,我们在设定变量时就更为灵活了。
        a=b
        b=$a
这样,b 的变量值就可继承 a 变量“当时”的值了。
当我们在设定变量的时候,请记住这点:
  • 用一个名称储存一个数值
仅此而已。
此外,我们也可利用命令行的变量替换能力来“扩充”(append)变量值:
        a=b:c:d
        a=$a:e
这样,第一行我们设定 a 的值为 "b:c:d" ,然后,第二行再将值扩充为 “b:c:d: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  的时候,请别忘记 shell 在命令行对变量的“替换”处理,
比方说:
$ a=b
$ b=c
$ export $a
上面的命令并未将 a 输出为环境变量,而是将 b 作输出,这是因为在这个命令行中,$a 会首先被替换出 b ,然后再“塞回”作 export 的参数。
要理解这个 export ,事实上需要从 process 的角度来理解才能透彻。
我将于下一章为大家说明 process 的观念,敬请期待..........
 

④:取消变量

要取消一个变量,在 bash 中可使用 unset 命令来处理:
$ unset a
与 export 一样,unset 命令行也同样会做变量替换(这其实就是 shell 的“本职工作”之一)
因此:
$ a=b
$ b=c
$ unset $a
事实上所取消的变量是 b 而不是 a。
此外,变量一旦经过 unset 取消之后,其结果是将整个变量拿掉,而不仅是取消其变量值。
如下两行其实是很不一样的:
$ a=
$ unset a
第一行只是将变量 a 设定为 “空值”(null),但第二行则让变量 a 不存在。
虽然用眼睛来看,这两种变量状态在如下命令中结果都是一样的:
$ a=
$ echo $a
 
$ unset a
$ echo $a
 
请大家务必能识别 null 与 unset 的本质区别,这在一些进阶的变量处理上是很严格的。
比方说:
$ str=            # 设为 null
$ var=${str=expr}     # 定义 var
$ echo $var
 
$ echo $str
 
$ unset str
$ var=${str=expr}
$ echo $var
expr
$ echo $str
expr
接下来就为大家讲解上例中的结构 ---- var=${str=expr}
分为以下三种情况:
  • str 为 null
            var=
            str= 
  • str 为 not null(比方为 gcx )
            var=gcx
            str=gcx
  • str 为 unset 
            var=expr
            str=expr
最后再来看看 var=${str:=expr},与上面那种结构做一下对比
  • str 为 null
            var=expr
            str=expr
  • str 为 not null (比方为 gcx )
            var=gcx
            str=gcx
  • str 为 unset
            var=expr
            str=expr
大家先记住这两个吧~~,另外几个类似的结构会在后面的章节列出来
 

六:exec 与 source 差在哪?

开始讲述着个问题之前先让我们来看一个问题:
在终端命令行 cd /etc/aa/bb/cc 可以正常切换工作目录
但是把这条命令写入 shell 脚本再运行时,为啥工作目录没有切换呢?
 
要解答这个问题,先让我们了解一下进程(process)的概念好了。
首先,我们所执行的任何程序,都是由父进程所产生出来的一个子进程,子进程在结束后 ,将返回到父进程去。此一现象在Linux系统中被称为 fork。
当子进程被产生的时候,将会从父进程那里获得一定的资源分配、及(更重要的是)继承父进程的环境;
 
让我们回到上一章所谈到的“环境变量”吧
       * 所谓环境变量其实就是那些会传给子进程的变量 。
 
简单而言,“遗传性”就是区分本地变量与环境变量的决定性指标。
然而,从遗传的角度来看,我们也不难发现环境变量的另一个重要特征:
        *环境变量只能从父进程到子进程单向继承,换句话说:在子进程中的环境如何变更,均不会影响父进程的环境。
 
接下来,再让我们了解一下命令脚本( shell script)的概念。
        所谓的 shell script 讲起来很简单,就是将你平时在 shell prompt 后所输入的多行 command line 依序写入一个文件去而已。
其中再加上一些条件判断、互动界面、参数运用、函数调用、等等技巧,得以让 script 更加“聪明”的执行,但若撇开这些技巧不谈,我们真的可以简单的看成 script 只不过依次执行预先写好的命令行而已。
结合以上两个概念( process + script ),那应该就不难理解如下这句话的意思了:
              *正常来说,当我们执行一个 shell script 时,其实是先产生一个 sub-shell 的子进程,然后 sub-shell 再去产生命令行的子进程。
然后,让我们回到本章开始时所提到的例子再重新思考:
       在shell script 中的 cd 命令为什么没有发挥作用呢?
 这是因为,一般我们跑的 shell script 是用 subshell 去执行的。
从 process 的观念来看,是父进程产生一个子进程去执行,当子进程结束后,会返回到父进程,但父进程的环境是不会因子进程的改变而改变的。
所谓的环境元素有很多,凡举 effective id,variable, working dir 等等....... 
其中的 working dir($PWD)正是同学们的疑问所在:
当用 sub shell 来跑 script 的话,sub shell 的 $PWD 会因为 cd 而变更, 但当返回  primary shell 时,$PWD 是不会变更的。
能够了解问题的原因及其原理是很好的,但是,如何解决问题恐怕是我们更感兴趣的!是吧?
那好,接下来,再让我们了解一下 source 命令好了。
当你有了 fork 的概念之后,要理解 source就不难:
        * 所谓 source 就是让 script 在当前 shell 内执行、而不是产生一个 sub-shell 来执行。
由于所有执行结果均于当前 shell 内完成,若 script 的环境有所改变,当然也会改变当前环境了!
因此,只要我们将原来单独输入的 script 命令行变成 source 命令的参数,就可轻易解决前例提到的问题了。
比方说,原理我们是如此执行 script 的:
./my.script
现在改成这样即可:
source ./my.script
. ./my.script
说到这里,我想,各位有兴趣看看 /etc 底下的众多设定文件,应该不难理解它们被定义后,如何让其它 script 读取并继承了吧?
若然,日后你有机会写自己的 script ,应也不难专门指定一个设定文件以供不同的 script 一起“共享”了........
------ 说完了 source ,那 exec 又与 source/fork 有何不同呢?
哦 .... 要了解 exec 或许较为复杂,尤其扯上 File Descriptor 的话  .......
不过,简单来说:
        * exec 也是让 script 在同一个进程上执行,但是原有进程则被结束了 
 简而言之,原有进程是否会终止,就是 exec 与 source/fork 的最大差异了。
嗯 ....... , 光从理论去理解,或许没那么好消化,不如动手“实作+思考”来的印象深刻哦。
下面让我们写两个简单的 script ,分别命名为 1.sh 及 2.sh:
1.sh
 
#!/bin/bash
A=B
echo "PID for 1.sh before exec/source/fork:$$"   # $$ 是系统内置的一个变量-当前进程号
export A
echo "1.sh:\$A is $A"
case $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
不管你们有没有动手实操,反正我是 已经跑了一遍,运行结果如下:
PID for 1.sh before exec/source/fork:83946
1.sh:\$A is B
using fork by default ...
PID for 2.sh:83947
2.sh get \$A=B from 1.sh
2.sh:\$A is C
PID for 1.sh after exec/source/fork:83946
1.sh:\$A is B
 
PID for 1.sh before exec/source/fork:83963
1.sh:\$A is B
using source ....
PID for 2.sh:83963
2.sh get \$A=B from 1.sh
2.sh:\$A is C
PID for 1.sh after exec/source/fork:83963
1.sh:\$A is C
 
PID for 1.sh before exec/source/fork:83999
1.sh:\$A is B
using exec .....
PID for 2.sh:83999
2.sh get \$A=B from 1.sh
2.sh:\$A is C  # 后面没有了呦,原有进程已经结束了 .....
 

七:( )与 { } 差在哪?

嗯,这次轻松一下,不讲太多 ......
先说一下,为何要用()或 { } 好了。
       许多时候,我们在 shell 操作上,需要在一定条件下一次执行多个命令,也就是说,要么不执行,要么就全执行,而不是每次依序的判断是否要执行下一个命令。或是,需要从一些命令执行优先次序中得到豁免,如算术的 2*(3+4) 那样 ....... 这时候,我们就可引入 “命令群组(command group)”的概念:将多个命令集中处理。
 
     在 shell command line 中,一般人或许不太计较 ( )与 { } 这两对符号的差异,虽然两者都可将多个命令作群组化处理,但从技术细节上,却是很不一样的。
  • ( ) 将 command group 置于 sub-shell 去执行,也称 nested sub-shell。
  • { } 则是在同一个 shell 内完成,也称 non-named command group。
若你对上一章的 fork 与 souce 的概念还记得了的话,那就不难理解两者的差异了。
要是在 command group 中扯上变量及其他环境的修改,我们可以根据不同的需求来使用 ( ) 或 { }。
通常而言,若所作的修改是临时的,且不想影响原有或以后的设定,那我们就 nested sub-shell,反之,则用 non-named  command group。
是的,光从 command line来看, ( ) 与 { } 的差别就讲完了,够轻松吧 ~~~~
然而,若这两个 meta 用在其它 command meta 或领域中(如 Regular Expression),还是有很多差别的。
最后,再让我补充一个一个概念,就是  function。
        * 所谓的 function,就是用一个名字去命名一个 command group ,然后再调用这个名字去执行 command group。
从 non-named command group 来推断,大概你也可以猜到我要说的是 { } 了吧。。。。
在 bash 中 function 的定义方式有两种,
方式一:
function function_name {
        command 1
        comamnd 2
        command 3
        .........
}
方式二:
function_name(){
        command 1
        command 2
        command 3
        .........
}
用哪一种方式无所谓,只是若碰到所定义的名称与现有的命令或别名(alias)冲突的话,方式二或许会失败,但方式二起码可以少打 function 这一串英文,对懒人来说,又何乐不为呢?......
        function 在某一程度来说,也可称为“函式”,但请不要与传统编程所使用的的函式(library)搞混了,毕竟两者差异很大,惟一相同的是,我们都可以随时用“已定义的名称”来调用他们 ........
        若我们在 shell 操作中,需要不断的重复执行某些命令,我们首先想到的,或许是将命令写成命令稿(shell script )。
不过,我们也可以写成 function,然后在 command line 中打上 function_name 就可当一般的 script 来使用了。
        只是若你在 shell 中定义的 function,除了可用 unset function_name 取消外,一旦退出 shell ,function 也就跟着消失了。
然而,在 script 中使用 function 却有许多好处,除了可以提高整体 script 的执行效率外(因为已被加载),还可以节省很多重复代码。
        简单而言,若你会将多个命令写成 script 以供调用的话,那么,你可以将 function看成是 script 中的 script .......
 

八:$(( )) 与 $( ) 还有 ${ } 差在哪?

 
我们上一章介绍了 ( )与 ( )的不同,这次让我们扩展一下,看看更多的变化:$( ) 与 ${ } 又是啥玩意呢?
 
在 bash shell 中,$( ) 与 ` `(反引号)都是用来做命令替换用的。
所谓的命令替换与我们之前学过的变量替换差不多,都是用来重组命令行:
     * 完成引号里的命令行,然后将其结果替换出来,再重组命令行。
在操作上,用 $( ) 或  `  ` 都无所谓,但是我通常会选择 $( ), 因为 ` ` 会经常与 ' ' (单引号)搞混!
还有一个不用 ` ` 的原因是:在多层次的复合替换中,` `  需要额外的跳脱处理,而 $( ) 则比较直观。例如:
这是错的:
command1 `command2 `command3``
原本的意图是要在 command2 `command3` 先将 command3 替换出来给 command2 处理,
然后再将结果传给 command1 `command2....` 来处理。
然而,真正的结果在命令行中却是分成了 `command2` 与 ` ` 两段,
正确的输入应该如下:
command1 `command2 \`command3\` `
或者直接换成 $( )
command1 $(command2 $(command3))
 
接下来,再让我们看看 ${ } 吧 ........ 它其实就是用来做变量替换用的啦~
一般情况下 $var 与 ${var} 并没有啥不一样。
但是用 ${ } 会比较精确的界定变量名称的范围,比方说:
$ a=b
$ echo $ab
原本是打算先将 $a 的结果替换出来,然后再补一个 b 字母于其后,
但在命令行上,真正的结果却是只会替换变量名称为 ab 的值出来 .....
若使用 ${ } 就没问题了:
$ echo ${a}b
 
不过,假如你只看到 ${ } 只能用来界定变量名称的话,那你就实在太小看 bash 了!
为了完整起见,我这里再用一些例子加以说明 ${ } 的一些特异功能:
假设我们定义了一个变量为:
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
利用 ${ } 还可以针对不同的变量状态赋值(unset 、null 、notnull)
  • ${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+my.file.txt}  :若 $file 设为空值或非空值,均使用 my.file.txt 作传回值(unset不做处理)
  • ${file:+my.file.txt}  :若 $file 为非空值,则使用 my.file.txt 作传回值(unset 或者 null 不做处理)
  • ${file?my.file.txt}  :若 $file 没设定,则将 my.file.txt 输出至 STDERR (空值及非空值时不作处理)
  • ${file:?my.file.txt} :若 $file 没设定或为空值,则将 my.file.txt 输出至 STDERR。(非空值不做处理)
  • ${#file}    : 得到27, 可以计算出变量值的长度。
tips:
        一般而言 ,:与 null 有关,若不带 :的话,null 不受影响 ,若带 : 则连 null 也受影响。      

 

接下来,再为大家介绍一下 bash 的数组(array)处理方法:
一般而言,A="a b c def" 这样的变量只是将 $A 替换为一个单一的字符串,
但是改为 A=(a b c def),则是将 $A 定义为数组 ...........
bash 的数组替换可参考如下方法:
  • ${A[@]} 或 ${A[*]} 可得到 a b c def (全部元素)
  • ${A[0]} 可得到 a (第一个元素),${A[1]}则得到第二个元素 b
  • ${#A[@]} 或 ${#A[*]} 可得到 4(有所少个元素)
  • ${#A[0]} 可得到 1 ,即第一个元素的长度
  • A[3]=xyz 则是将第四个组数重新定义为 xyz.......  
诸如此类的 .........
能够善用 bash 的 $( ) 与 ${ } 可大大提高及简化 shell 在变量上的处理能力~~~
 
好了,最后为大家介绍 $(( )) 的用途吧:它是用来作整数运算的。
在 bash 中, $(( )) 的整数运算符号大致有这些:
        + - * / %   :加、减、乘、除、取余
另外还有逻辑运算符:
        &  |  ^  ! :分别为 “AND OR XOR NOT”运算。
例:
$ a=5; b=7; c=2
$ echo $((a+b*c))
19
$ echo $(( (a+b)/c))
6
$ echo $(( (a*b)%c))
1
 

九: $@ 与 $* 差在哪?

要说 $@ 与 $* 之前,需要先从 shell script 的 positional parameter 谈起 ......
我们都已经知道变量(variable)是如何定义及替换的,这个不用再多讲了。
但是,我们还需要知道有些变量是 shell 内定的,且其名称是我们不能随意修改的,
其中就有 positional parameter 在内。
在 shell script 中,我们可用 $0 ,$1, $2 ,$3  ........这样的变量分别提取命令行中的如下部分:
$ bash script_name parameter1 parameter2 parameter3 .........
我们很容易就能猜出 $0 就是代表 shell script 名称(路径)本身,而 $1 就是其后的第一个参数,如此类推.......
需要留意的是 IFS 的作用,也就是说,若 IFS 被 quoting 处理后,那么 positional parameter 也会改变。
如下例:
my.sh p1 "p2 p3" p4
由于在 p2 与 p3 之间的空格键被 soft quoting所关闭了,因此 my.sh 中的 $2 是 "p2 p3" 而 $3 则是 p4 .........
 
还记得前面的章节中我们提到 function 时,我不是说过它是 script 中的 script 吗?
是的,function 一样可以读取自己的(有别于 script 的)position parameter,惟一例外的是 $0 而已。
举例而言,假设 my.sh 里面有一个 function 叫 my_fun , 若在 script 中跑 my_fun fp1 fp2 fp3,
那么,function 内的 $0 是 my.sh , 而 $1 则是 fp1 而非 p1 了 ........
 不如写个简单的 my.sh script 看看吧:
#!/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
echo '$2 outside function is'$2
 
my_fun fp1 "fp2 fp3"
 
然后在 command line中跑一下 script 就知道了:
chmod +x my.sh
./my.sh p1 "p2 p3"
 
 
$0 outside function is ./my.sh
$0 outside function is p1
$0 outside function is p2 p3
 
$0 inside function is ./my.sh
$1 inside function is fp1
$2 inside function is fp2 fp3
 
然而,在使用 positional parameter 的时候,我们要注意一些陷阱哦:
$10 不是替换第 10 个参数,而是替换第一个参数($1)然后再补一个 0 于其后!
如果想要在 script 中替换第10个参数,我们可以用 ${10}
 
okay,当我们对 positional parameter 有了基本概念之后,那再让我们看看其他相关的变量吧。
首先是 $# :它可以显示 positional parameter 的数量。
以前面的 my.sh p1 "p2 p3" 为例:
由于 p2 与 p3 之间的 IFS 是在 soft quoting 中的,因此  $# 可得到 2 的值
因此,我们常在 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 "$*")
 

十:&& 与 || 差在哪?

 
在解答本章题目之前,先让我们了解一个概念:return value;
我们在 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的话为真
  • 非0的话为假
举个例子来说明好了,
假设当前目录内有一份 my.file 的文件,而 no.file 是不存在的:
$ touch my.file
$ ls my.file
$ echo $?   # first echo
0
$ ls no.file 
ls:no.file: No such file or directory
$ echo $?   #second echo
1
$ echo $?   # third echo
0
 
上例的第一个 echo 是关于 ls my.file 的 RV ,可得到 0  的值,因此为 true;
第二个 echo 是关于 ls no.file 的 RV ,则得到非0 的值,因此为 false;
第三个 echo 是关于第二个 echo $? 的 RV ,为 0 的值,因此也为 true。
请记住:每一个 command 在结束时都会送回 return value 的:不管你什么样的命令.......
然而,有一个命令却是“专门”用来测试某一条件而送出 return value 以供 true 或 false的判断,它就是 test 命令了;
首先,test 的 表示式我们称之为 expression ,其命令格式有两种:
test expression
or:
[ expression ]
请务必注意 [ ]之间的空格键
用哪种格式没所谓,都是一样的效果。
其次,bash 的 test 目前支持的测试对象只有三种:
  • string :字符串,也就是纯文字。
  • integer:整数 (0 或正整数,不含负数或小数点)
  • file :文件
请初学者一定要搞清楚这三者的差异,因为 test 所用的 expression 是不一样的。
以 A=123 这个变量为例:
  • [ "$A" = 123 ]     :是字符串的测试,以测试 $A 是否为1、2、3这三个连续的“文字”。
  • [ "$A" -eq 123 ]  :是整数的测试,已测试 $A 是否等于一百二十三。
  • [ -e "$A" ]           :是关于文件的测试,以测试 123 这份文件是否存在
当 expression 测试为 “真”时,test 就送回 0(true)的返回值,否则送出非0(false)。
若在 expression 之前加上一个 “ ! ”(感叹号),则是当 expression 为假时才送出0,否则送出非0。
同时,test 也允许多重的复合测试:
  • expression1 -a expression2   :当两个 expression 都为 true ,才送出0,否则送出非0。
  • expression1 -o expression2   :只需其中一个 expression 为 true ,就送出0,只有两者都为 false才送出非 0
例如:
[ -d "$file" -a -x "$file" ] 
是表示当 $file 是一个目录、且同时具有 x 权限时,test 才会为true。
 
在 command line 中使用 test 时,请别忘记命令行的“重组”特性,
也就是在碰到 meta 时会先处理 meta 再重新组建命令行。
比方说,若 test 碰到变量或命令替换时,若不能满足 expression 格式时,将会得到语法错误的结果。
举例来说:
关于 [ string1 = string2 ] 这个 test 格式,
在等号两边必须要有字符串,其中包括空(null)字符串(可用单引号或双引号取得)
假如 $A 目前没有定义,或被定义为空格字符串的话,纳入下的写法将会失败:
unset A
$[ $A = abc ]
[: =: unary operator expected
这是因为命令行碰到 $ 这个 meta 时,会替换 $A 的值,然后再重组命令行,那就变成了:[ = abc ]
如此一来 =  号左边就没有字符串存在了,因此造成 test 的语法错误!
但是,下面这个写法则是成立的:
$ [ "$A" = abc ]
$ echo $?
1
这是因为在命令行重组后的结果为:
[ "" = abc ]
由于 = 左边我们用 双引号得到一个空字符串,而让 test 语法得以通过 .....
 
okay ,啰嗦了一大堆或许你还在嘀咕 ..... 那 ...... 那个Return Value 有啥用啊?
若你想让你的 shell 变得 “聪明”,那就得全靠它了:
  • 有了 return value ,我们可以让 shell 根据不同的状态做不同的事情 .....
 
这时候来让我们揭晓本章的答案吧~~~
&& 与 || 都是用来“组建”多个 command line用的:
  • command1 && command2 :其意思是 command2 只有在command1 RV 为 0  的条件下才执行。
  • command1 || command2     : 其意思是 command2 只有在 command1 RV 为非零的条件下才执行。
 
举例来说:
$ A=123
$ [ -n "$A" ] && echo "yes! it's true "
yes! it's true
 
$ unset A
$ [ -n "$A" ] && echo "yes! it's true "
$ [ -n "$A" ] || echo "No! it's not true"
No! it's not true
 
$ [ -n "$A" ] && echo "yes! it's true " || echo "No! it's not true"
No! it's not true
注: [ -n string ] 是测试 string大于 0 则为 true
 
接下来再思考一个问题 :
下面的判断是:当 $A 被赋值时 ,在看是否小于100,否则送出 too big!
$ A=123
$ [ -n "$A" ] && [ "$A" -le 100 ] || echo "too big!" 
too big!
若我将 A 取消,讲道理,应该不会送出文字才对呀,因为第一个条件就不成立了 .....
$ unset A
$ [ -n "$A" ] && [ "$A" -le 100 ] || echo "too big!"
too big!
为何还有文字输出呢?
该如何解决?
修改的方法之一可以用我们讲过的 command group
 
 

十一:> 与 < 差在哪?

 
11.1
谈到 I/O redirection , 不妨先让我们认识一下 File Descriptor(FD)。
 
程序的运算,在大部分情况相爱都是进行数据(data )的处理,
这些数据从哪读进?又送出到哪里呢?
这就是 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
 
不过,不见得每个程序的 stdin 都是从 keyboard 读进。
因为程序的设计者可以从档案参数读进 stdin ,如:
$ cat /etc/passwd
但,要是 cat 之后 没有档案参数则又如何呢?
$ cat
 
 
答案是:它会一直等待用户从键盘(标准输入)进行输入,回车键后又会把输入内容呈现在屏幕上(标准输出),然后继续等待用户输入 .........
 
11.2
相信经过上面的练习,你对 stdin 与 stdout 应该有所理解了吧~~
接下来,让我一起来看看 stderr 。
事实上, stderr 没什么难理解的,说穿了就是“错误信息”要往哪边送而已 ....
比方说,若读进的档案参数是不存在的,那我们在 monitor 上就看到了:
$ ls no.such.file
ls: no.sunch.file : Not sunch file or directory
 
 
若,一个命令同时产生 stdout 与 stderr 呢?
那还不简单,都送到 monitor 来就好了:
$ touch my.file
$ ls my.file no.sunch.file
ls: no.sunch.file : Not sunch file or directory
my.file
okay, 至此,关于 FD 及其名称、还有相关联的设备,相信你已经没问题了,
那好,接下来让我们看看如何改变这些 FD 的预设数据信道,
我们可用 < 来改变读进的数据信道(stdin),使之从指定的档案读进。
我们可用 > 来改变送出的数据信道(stdout,stderr),使之输出到指定的档案。
比方说:
$ cat < my.file
就是从 my.file 读进数据
$ mail -s test root < /etc/passwd
则是从 /etc/passwd 读进 .....
这样一来,stdin 将不再是从 keyboard 读进,而是从档案读进了 ....
严格拉说,<  符号之前需要指定一个 FD 的(之间不能有空白),
但是因为 0 是 < 的默认值,因此 ,<  与 0< 是一样的!
okay,这个好理解吧?
那,要是用两个 << 又是啥呢?
这就是所谓的  HERE Document,它可以让我们输入一段文本,直到读到 << 后指定的字符串。
比方说
$ cat <<FINISHED
first line
second line
third line
FINISH
这样的话,cat 会读进3行句子,而无需从 keyboard 读进数据且要等 ^d 结束输入。
 
11.3
至于 > 又如何呢?
当你搞懂了 0< 原来就是改变 stdin 的数据输入信道之后,相信要理解如下两个 redirection 就不难 了:
  • 1>
  • 2>
前者是改变 stdout 的数据输出信道,后者是改变 stderr 的数据输出信道。
两者都是将原本要送到 monitor 的数据转向输出到指定档案去。
由于 1是 >  的默认值,因此, 1> 与  >是相同的,都是改 stdout。
用上次的 ls 例子来说明一下好了:
$ ls my.file no.sunch.file 1>file.out
ls: no.sunch.file : Not sunch file or directory
这样 monitor 就只剩下 stderr 而已。因为 stdout 给写进 file.out 去了。
 
$ ls my.file no.sunch.file 2>file.err
my.file
这样 monitor 就只剩下 stdout  而已。因为 stderr 写进 file.err 去了。
 
$ ls my.file no.sunch.file 1>file.out 2>file.err
这样 monitor 就啥也没有了,因为 stdout 与 stderr 都给转到档案去了……
 
有些地方还是要注意一下的。
首先,是同时写入的问题,
举个例子
$ ls my.file no.sunch.file 1>file.both 2>file.both
假如 stdout(1)与 stderr(2)都同时在写入 file.both 的话。
则是采用“覆盖” 方式:后来写入的覆盖前面的。
那该如何解决覆盖这个问题呢?
我们可以换一个思维,将 stderr 导进 stdout 或将 stdout 导进 stderr ,而不是大家在抢同一份档案,不就行了!
  • 2>&1  就是将 stderr 并进 stdout 作输出
  • 1>&2   就是将 stdout 并进 stderr 作输出
于是前面的错误操作可以改为:
$ ls my.file no.sunch.file 1>file.both 2>&1
或者
$ ls my.file no.sunch.file 2>file.both >&2
不过,光解决了同时写入的问题还不够,我们还有其他技巧需要了解
故事还没结束,别走开!广告之后更精彩……
 
11.4
 
在 Linux 档案系统里,有个设备挡位于 /dev/null
许多人都问那是什么玩意儿?我跟你说好了:那就是“空”啦!
没错!空空如也的空就是 null 了……
这个 null 在 I/O Redirection 中可有用得很呢:
  • 若将 FD1 跟 FD2 转到  /dev/null 去,就可将 stdout 与 stderr 弄不见掉。
  • 若将 FD0 接到 /dev/null 来,那就是读进 nothing。
 
比方说,当我们在执行一个程序时,画面会同时送出 stdout 与 stderr,
假如你不想看到 stderr (也不想存到档案去),那可以:
$ ls my.file no.such.file 2>/dev/null
my.file
 
 
若要相反,只想看到 stderr 呢?还不简单:将 stdout 弄到 null 就行了
$ ls my.file no.such.file >/dev/null
ls: no.sunch.file : Not sunch file or directory
 
那接下来,假如单纯只跑程序,不想看到任何输出结果呢?
除了用 >/dev/null 2>&1 之外,你还可以如此:
$ ls my.file no.such.file &>/dev/null
(提示:将 &> 换成 >& 也行啦~~)
 
要解决同时写一份文件的“覆盖”问题,除了上面介绍的方法还有其他方法吗?
那就是 >> 了
使用 >> 可以让我们以追加的方式往文件中写入内容
$ echo "3" >> file.out
但,只要你再一次用回 > 来重导向的话,那么,旧的内容还是会被“洗”掉的:
这时你要如何避免呢?
$ set  -o noclobber
$ echo "4" > file.out
-bash: file: cannot overwrite existing file
 
那要如何取消这个限制呢?
哦,将 set -o 换成 set +o 就行:
$ set  +o noclobber
$ echo "5" > file.out
 
再问:那……有办法不取消而又“临时”盖写目标档案吗?
$ set  -o noclobber
$ echo "6" >| file.out
留意到没有,在 > 后面再加个 ‘ | ’就好(注意:> 与 | 之间不能有空格)
 
再来还有一个问题要你去参透呢;
$ echo "some text here" >file
$ cat < file
some text here
$ cat < file >file.bak
$ cat < file.bak
some text here
$ cat < file >file
$ cat <file
 
嗯? 注意到没有??
怎么最后那个 cat 命令看到的 file 竟是空的?
要理解这一现象其实不难,这只是 priority 的问题而已;
  • 在 IO Redirection 中,stdout 与 stderr 的管道会先准备好,才会从 stdin 读进数据
也就是说,在上例中,>file 会先将 file 清空,然后才读进 <file ,但这时候档案已经被清空了,因此就变成读不进任何数据了……
 
11.5
最后再补充一个 pipe line
谈到 pipe line ,我相信不少人都不会陌生:
我们在很多 command line 上常看到的 " | " 符号就是 pipe line了。
不过,究竟 pipe line 是什么东西呢?
别急,别急…… 先查一下英汉词典,看看 pipe 是什么意思?
没错 !它就是“水管”的意思……
那么,你能想象一下水管是怎么一根接着一根的吗?
又,每根水管之间的 input 跟 output 又如何呢?
灵光一闪,原来 pipe line 的 I/O 跟水管的 I/O 是一模一样的:
上一个命令的 stdout 接到下一个命令的 stdin 去了;
的确如此…… 不管在 command line 上你使用了多少个 pipe line,
前后两个 command 的 I/O 都是彼此连接的!(恭喜:你终于开窍了!)
不过…… 然而……但是…… stderr 呢?
好问题:不过也容易理解:
若水管漏水了怎么办?
也就是说:在 pipe line之间,前一个命令的 stderr 是不会接进下一个命令的 stdin 的,
其输出,若不用 2> 导到 file 去的话,它还是送到屏幕上来!
那,或许你会问
有办法将 stderr 也喂进下一个命令的 stdin 去吗? (2>&1)
或许你还有问题……
在 cm1 | cm2 | cm3 ........ 这段 pipe line中,若要将 cm2 的结果存到某一档案呢?
若你写成 cm1 | cm2 >file | cm3 的话,
那你肯定会发现 cm3 的 stdin 是空的!(当然啦,你将水管接到别的水池了!)
聪明的你或许会如此解决:
cm1 | cm2 >file ; cm3 <file
是的,你的确可以这样做,但最大的坏处就是,这样一来,file I/O 会便双倍!
在 command 执行的整个过程中, file I/O 是最常见的效率杀手。
那,上面问题还有更好的方法吗?
有的,那就是 tee 命令了。
  • 所谓 tee 命令是在不影响原本 I/O  的情况下,将 stdout 复制一份到档案去。
因此,上面的命令行可以如此打:
cm1 | cm2 | tee file | cm3
在预设上 tee 会改写目标档案,若你要改为增加内容的话,那可以用 -a 参数达成。
 

十二:shell 中的 if  与 case 分支

12.1
if结构
if command
then 
    comd1
    comd2
else
    comd3
    comd4
fi
若 then 后不想跑任何 command ,可以用 “ : ”这个 null command 代替
若有多项条件需要“依序”进行判断的话,那我们可以使用 elif 这样的 keyword:
if comd1; then
    comd2
elif comd3; then
    comd4
else
    comd5
fi
意思是说:
    若 comd1 为 true ,然后就执行comd2
    否则再测试 comd3,然后执行 comd4
    倘若 comd1 与 comd3 都不成立,那就执行 comd5
 
12.2
case 语句结构
虽然 if 判断式已可应付大部分的条件执行了,然而,在某些场合,却不够灵活,尤其是在 string 式样的判断上,
case "${var}" in 
    case1)
            comd1
            .....
            ;;
    case2)
            comd2
            .....
            ;;
    *)
            comd3
            .....
            ;;
esac
 

十三:循环结构

 
bash shell中常用的循环有三种:for 、while、until

 

13.1 for循环

for 循环是从一个清单列表中读进变量值,并“依次”的循环执行 do 到 done 之间的命令行。
例:
for var in one two three four five
do
    echo '$var is'$var
done
 
上例的执行过程为:
    1)for 会定义一个叫 var 的变量,其值依次赋为 one two three four five。
    2)因为有 5 个变量值,因此 do 与 done 之间的命令行会被循环执行5次(将 var 值依次打印出来)
    3)最后一个变量值处理完毕,循环结束
我们不难看出,在 for 循环中,变量值的多寡,决定循环的次数。
 
倘若 for 循环没有使用“in”这个关键字来指定变量清单的话,其值将从 $@ ( 或 $* )中继承:
for var; do
    ......
done
for 循环用于处理“清单”项目非常方便,
其清单除了可明确指定或从 positional parameter 取得之外,
也可从变量替换或命令替换取得......(再一次提醒,别忘了命令行的“重组”特性)
然而,对于一些“累计变化”的项目(如整数的加减),for 亦能处理:
for ((i=1;i<=10;i++))
do
    echo "num is $i"
done
 

13.2 while 循环

num=1
while [ "$num" -le 10 ];do
    echo "num is $num "
    num=$(($num + 1))
done
while 循环的原理与 for 循环稍有不同:
它不是逐次处理清单中的变量值,而是取决于 while 后面判断式的 return value:
    若为 true ,则执行 do 与 done 之间的命令,然后重新判断 while 后的 return value。
    若为 false,则不再执行 do 与 done 之间的命令而结束循环
我们不难发现:
若 while 的测试结果永远为 true 的话,那循环将一直永久执行下去:
while :; do
    echo looping.....
done
上例的 “ : ”是 bash 的 null command,不做任何动作,除了送回 true 的 return value。
因此这个循环不会结束,称为死循环。
死循环的产生有可能是故意设计的(如跑 daemon),也可能是设计错误。
若要结束死循环,可通过 signal 来终止(如按下 ctrl+c)。
关于 process 与 signal 好像很有意思,但作者没有写下去,可惜了 ......
 

13.3 until 循环

 
与 while 相反,until 是在 return value为 false 时进入循环,否则结束。
例:循环10次
num=1
until [ "$num" -gt 10 ]; do
    echo "num is $num"
    num=$(($num + 1))
done
 
或者
num=1
until [ !"$num" -le 10 ]; do
    echo "num is $num"
    num=$(($num + 1))
done
在结束本章之前,再给大家补充两个与循环有关的命令:
  • break
  • continue
这两个命令常用在复合式循环里,也就是在 do......done 之间又有更进一层的循环,
当然,用在单一循环中也未尝不可啦 ....
 
break 是用来打断循环,也就是“强迫结束”循环。
若 break 后面指定一个数值 n 的话,则“从里向外”打断第 n 个循环,
默认值为 break 1 ,也就是打断当前的循环。
在使用 break 时需要注意的是,它与 return 及 exit 是不同的:
  • break 是结束循环
  • return 是结束 function
  • exit 是结束,script/shell
而 continue 则与 break 相反:强迫进入下一次循环动作。
若你理解不来的话,那你可简单的看成:在 continue 到 done 之间的句子略过而返回循环顶端 .....
与 break 相同的是:continue 后面也可指定一个数值 n ,以决定继续哪一层(从里向外计算)的循环,
默认值为 continue 1 ,也就是继续当前的循环。
 
 

十四:[^ ] 与 [! ] 差在哪?

 
这道题目说穿了,就是要探讨 Wildcard 与 Regular Expression 的差别的。
这也是很多初学 shell 的朋友很容易混乱的地方。
首先先让我们来回顾两个知识点:
  • 命令行的格式: command_name options arguments
  • shell中命令行的执行步骤:先替换、再重组
 

14.1 Wildcard(通配符)

首先,wildcard 也是属于 command line 的处理工序,作用于 argument 里的 path 之上,没错,它不用在 command_name 也不用在 options 上,而且,argument 不是 path 的话,那也与 wildcard 无关,换句更为精确的定义来讲,wildcard 是一种命令行的路径扩展( path expansion )功能,提到这个扩展,那就不要忘记了 command line 的“重组”特性了!是的,这与变量替换及命令替换的重组特性是一样的!也就是在 wildcard 进行扩展后,命令行会先完成重组才会交给 shell 来处理。
了解了 wildcard 的扩展与重组特性后,接下来,让我们了解一些常见的 wildcard 吧:
  • *  :匹配 0 或 多个字符
  • ?:匹配任意单一字符
  • [list] :匹配 list 中的任意单一字符,(也可以为一段 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 这三个字符串之一,如:aabcd 、axyzb、 a123b 等。
注意:
1、[! ] 中的 !只有放在第一顺位时,才有排除之功,
举例来说:
[!a]* 表示当前目录下所有不以 a 开头的路径名称。
/tmp/*[-z]/[a-zA-z]* 表示 /tmp 目录下所有以 z 或 - 结尾的子目录下以英文字母开头的路径名称
 
 2、[ -] 中的 - 左右两边均有字符时,才表示一段范围,否则仅作“-”字符来处理,
举例来说:
/tmp/*[-z]/[a-zA-Z]* 表示 /tmp 目录下所有以 z 或 - 结尾的子目录下以英文字母(不分大小 写)开头的路径名称。
 
3、以 * 或  ? 开头的 wildcard 不能匹配隐藏文件(即以 . 开头的文件)
举例来说:
*.txt 并不能匹配  .txt  但可匹配 1.txt 这样的路径名称。
但 1*txt 及 1?txt 均可匹配 1.txt 这样的路径名称。
 
再次提醒:别忘了“扩充+重组”这个重要特性,而且只作用在 argument 的 path 之上,
比方说,假设当前目录下有 a.txt  b.txt  c.txt   1.txt    2.txt  3.txt 这几份文件。
当我们在命令行中下达 ls -l [0-9].txt 的命令行时,
因为 wildcard 处于 argument 的位置上,于是根据其匹配的路径,扩展为 1.txt  2.txt   3.txt,
再重组出 ls -l 1.txt 2.txt 3.txt 这样的命令行。
 

 14.2 Regular Expression

 接下来的 Regular Expression(正则表达式)是个大题目 ... ,这里仅将一些常用的语法。
我们在 shell 的命令行中,关键是要能够区分 shell command line上的 meta 与 literal 这两种不同的字符类别。
然而,RE 表达式里的字符也是分为 meta 与 literal 这两种!
呵,不知道广大同学有没有被我搞混乱了呢? .......
简单而言,除非你将 RE 写在特定程序使用的脚本里。
否则,我们的 RE 也是透过 command line 输入的。
然而,不少 RE 所使用的 meta 字符,跟 shell meta 字符是冲突的。
比方说, * 这个字符,在 RE 里是一个modifier(后述),在 command line 上,却是个 wildcard。
那么,我们该如何解决这样的冲突呢?挂件就是看你对 quoting 的理解程度了!
若你明白到 shell quoting 就是在 command line 上关闭 shell meta 这一基本原理,
那你就能很轻松的解决 RE meta 与 shell meta 的冲突问题了:
------用 shell quoting 关掉 shell meta 就是了!
就这么简单!
再以刚提到的 * 字符为例,若在 command line 中没有 quoting 处理的话,如 abc*, 那就会被作为 wildcard expansion来扩充及重组了。
若将其置于 quoting 中,如 “abc*”,则可避免 wildcard expansion 的处理。
最后再啰嗦一句:
当我们在谈到 RE 时,千万别跟 wildcard 搞混在一起!
尤其在 command line 的位置里,wildcard 只作用于 argument 的 path 之上。
但是 RE 却只作用于 “字符串处理”的程序之中,这与路径名称一点关系也没有!
RE 所处理的字符串通常是指纯文档或透过 stdin 读进的内容 ....
 
接下来让我们揭开 RE 的神秘面纱吧 ....
在 RE 的表达式里,主要分两种字符(character):literal 与 meta
所谓 literal 就是在 RE 里不具有特殊功能的字符,如:abc 、123这些。
而 meta 在 RE 里具有特殊的功能,要关闭之需要在 meta 前面使用 escape(\)字符。
 
在介绍 meta 之前,先让我们来认识一下字符组合( character set )会更好些。
所谓 char.set 就是 将多个连续的字符作一个集合,比方说:
  • abc :表示 abc 三个连续的字符,但彼此独立而非集合。(可简单视为三个char.set)
  • (abc):表示 abc 这三个连续字符的集合,(可简单视为一个char.set)
  • abc|xyz : 表示 abc 或 xyz 这两个 char.set 之一。
  • [abc] : 表示单一字符,可为 a 或 b 或 c。(与 Wildcard 中的 [abc]原理相同 )
  • [^abc] :表示单一字符,不为 a 或 b 或 c 即可。(与 Wildcard 中的 [!abc] 原理相同)
  • . :表示任意单一字符。(与 Wildcard 中的 ? 原理相同)
 
在 认识了 char.set 这个概念之后,然后再让我们多认识几个 RE 中常见的 meta 字符:
    锚点(anchor)
       用以标识 RE 于句子中的位置所在,常见有:
        ^ :表示句首,如 ^abc 表示以 abc 开头的句子。
        $ :表示句尾,如 abc$ 表示以 abc 结尾的句子。
        \< :表示词首,如 \<abc 表示以 abc 开头的词。
        \>:表示词尾,如 abc\> 表示以 abc 结尾的词。
 
  修饰字符(modifier)
       独立表示时本身不具意义,专门用以修饰前一个 char.set 的出现次数,常见有:
       *       :表示前一个 char.set 的出现次数为0或多次。如 ab*c 表示 a 与 c 之间可有0 或多个 b 存在。
       ?      :表示前一个char.set 的出现次数为0或1次。如 ab?c 表示 a 与 c 之间可有 0 或 1个b 存在。
       +      :表示前一个char.set 的出现次数为 1 或多次。如ab+c 表示 a 与 c 之间可有1或多个b存在。
       {n}    :表示前一个 char.set 出现次数必须为 n 次。如 ab{3}c 表示 a 与 c 之间必须有3个 b 存在。
       {n,}   :表示前一个 char.set 的出现次数至少为 n 次。如 ab{3,}c 表示 a 与 c 之间至少有3个b存在。
       {n,m}:表示前一个 char.set 的出现次数为 n 到 m 次。如 ab{3,5}c 表示 a与c之间有3到5个b存在。
 
然而,当我们在识别 modifier 时,却很容易忽略“边界(boundary)”字符的重要性。
以刚提到的 ab{3,5}c 为例,这里的 a 与 c就是边界字符了。
若没有边界字符的帮忙,我们很容易作出错误的解读。
比方说:我们用 ab{3,5} 这个 RE (少了 c 这个边界字符)可以抓到 abbbbbbbb (a后面有8个b)这个“字串”吗?
从刚才的 modifier 我们一般会认为我们要的 b 是 3到 5个,若超出了此范围,就不是我们要表达的,
因此,我们或许会很轻率的认为这个 RE 抓不到结果……
然而答案却是可以的!为什么呢?
让我们重新解读 ab(3,5) 这个 RE 看看:
我们要表达的是 a 后面接 3 到 5个 b即可,但 3 到 5 个 b 后面我们却没规定是什么,
因此 RE 后面可以是任意的文字,当然包括 b 也可以啦!(明白了吗?)
同样的,我们用 b{3,5}c 同样可以抓到 abbbbbbbbbbbbbc  这串字的。
但我们若使用 ab{3,5}c 这样的 RE 时,由于同时又 a 与 c 这两个边界字符,那就截然不同了!
 
有空再思考一下,为何我们用下面这些 RE 都能抓到 abc 这串字呢?
x*
ax*,abx*,ax*b
abcx* , abx*c , ax*bc
bx*c , bcx*, x*bc
(还有很多……)
但,若我们在这些 RE 前后分别加一个 ^ 与 $ 这样的 anchor ,那又如何呢?
 
刚学 RE 时,只要能掌握上面这些基本的 meta 大概就可以入门了。
一如前述,RE 是一种规范化的文字表达方式,主要用于某些文字处理工具之间,如 grep ,vi , awk, sed 等等,常用以标识一段连续的字符串,捕获之或替换之。
然而,每种工具对 RE 表达式的具体解读或有一些细微差异,不过,基本原则还是一致的。只要能掌握 RE 的基本原理,那就一通百通了,只要在使用时稍加变通即可。
 

                                                                                                                                                                                                                2020-12-04 23:30

 
 
 
 
 
 
 
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值