shell 知:bash

文章目录

  • 1. 介绍
    • 1.1. 为什么使用shell编程
    • 1.2. Sha-Bang(#!)
  • 2. 基本
    • 2.1. 特殊字符
      • 2.1.1. 特殊字符
      • 2.1.2. 控制字符
      • 2.1.3. 空白
    • 2.2. 变量和参数
      • 2.2.1. 变量替换
      • 2.2.2. 变量赋值
      • 2.2.3. Bash变量是不区分类型的
      • 2.2.4. 特殊的变量类型
        • 2.2.4.1. 局部变量
        • 2.2.4.2. 环境变量
        • 2.2.4.3. 位置参数
    • 2.3. 引用
      • 2.3.1. 引用变量
      • 2.3.2. 转义
    • 2.4. 退出和退出状态码
    • 2.5. 条件判断
      • 2.5.1. 条件测试结构
      • 2.5.2. 文件测试操作符
      • 2.5.3. 其它比较操作符
    • 2.6. 操作符与相关主题
      • 2.6.1. 操作符
        • 2.6.1.1. 赋值
        • 2.6.1.2. 算术操作符
        • 2.6.1.3. 位操作符
        • 2.6.1.4. 逻辑操作符
        • 2.6.1.5. 混杂的操作符
      • 2.6.2. 数字常量
  • 3. 进阶
    • 3.1. 变量重游
      • 3.1.1. 内部变量
      • 3.1.2. 位置参数
      • 3.1.3. 其它的特殊参数
      • 3.1.4. 操作字符串
      • 3.1.5. 参数替换
      • 3.1.6. 指定变量的类型
      • 3.1.7. 变量的间接引用
      • 3.1.8. $RANDOM:产生随机整数
      • 3.1.9. 双圆括号结构
    • 3.2. 循环与分支
      • 3.2.1. 循环
        • 3.2.1.1. for循环
        • 3.2.1.2. while循环
        • 3.2.1.3. until循环
      • 3.2.2. 嵌套循环
      • 3.2.3. 循环控制
      • 3.2.4. 测试与分支
    • 3.3. 内部命令与內建命令
      • 3.3.1. I/O
      • 3.3.2. 文件系统
      • 3.3.3. 变量
      • 3.3.4. 脚本行为
      • 3.3.5. 命令
      • 3.3.6. 作业控制命令
        • 3.3.6.1. 作业标识符
    • 3.4. 外部命令
    • 3.5. 命令替换
    • 3.6. 算术扩展
    • 3.7. I/O重定向
      • 3.7.1. 使用exec
      • 3.7.2. 代码块重定向
    • 3.8. Here Document
      • 3.8.1. Here String
    • 3.9. 正则表达式
    • 3.10. 子shell
      • 3.10.1. 圆括号中的命令列表
    • 3.11. 受限shell
    • 3.12. 进程替换
    • 3.13. 函数
      • 3.13.1. 复杂函数和函数复杂性
        • 3.13.1.1. 退出与返回
        • 3.13.1.2. 重定向
      • 3.13.2. 局部变量
        • 3.13.2.1. 局部变量使递归变为可能
        • 3.13.2.2. 不使用局部变量的递归
    • 3.14. 别名
    • 3.15. 列表结构
      • 3.15.1. 把命令连接到一起
    • 3.16. 数组
    • 3.17. /dev和/proc
      • 3.17.1. /dev
      • 3.17.2. /proc
    • 3.18. Zero与Null
      • 3.18.1. 使用/dev/null
      • 3.18.2. 使用/dev/zero
    • 3.19. 调试
    • 3.20. 选项
    • 3.21. 陷阱
    • 3.22. 脚本编程风格
      • 3.22.1. 非官方的Shell脚本编写风格
    • 3.23. 杂项
      • 3.23.1. 交互与非交互式的shell和脚本
      • 3.23.2. Shell包装
      • 3.23.3. 测试和比较
      • 3.23.4. 递归
      • 3.23.5. 将脚本“彩色化”
      • 3.23.6. 优化
      • 3.23.7. 各种小技巧
      • 3.23.8. 安全问题
        • 3.23.8.1. 被感染的脚本
        • 3.23.8.2. 隐藏shell脚本源代码
      • 3.23.9. 可移植性问题
      • 3.23.10. Windows下的shell脚本
      • 3.23.11. 版本新特性
  • 4. 项目
  • 5. 参考

1. 介绍

1.1. 为什么使用shell编程

shell是一个命令解释器,是介于操作系统内核与用户之间的一个绝缘层。准确的说,它也是能力很强的计算机语言,一种shell程序,同时也被称为一种脚本语言。

它是非常容易使用的工具,它可以通过将系统调用、公共程序、工具和编译过的二进制程序“粘合”在一起来建立应用。
事实上,所有的UNIX命令和工具再加上公共程序,对于shell脚本来说,都是可调用的。

如果这些你还觉得不够,那么shell內建命令,比如条件测试与循环结构,也会给脚本添加强力的支持和增加灵活性。
shell脚本对于管理系统任务和其它的重复工作的例程来说,表现的非常好,根本不需要那些华而不实的成熟紧凑的程序语言。

什么时候不适合使用shell脚本:

  • 资源密集型的任务。尤其在需要考虑效率时(比如:排序、hash等)
  • 需要处理大任务的数学操作。尤其是浮点运算、精确运算或者复杂的算术运算
  • 有跨平台移植需求
  • 复杂的应用,在必须使用结构化编程的时候(需要变量的类型检查、函数原型等)
  • 至关重要的应用,比如说为了这个应用,你需要堵上你们公司的未来
  • 对于安全有很高要求的任务,比如你需要一个健壮的系统来防止入侵,破解,恶意破坏等
  • 工程的每个组成部分之间,需要连锁的依赖性
  • 需要大规模的文件操作(Bash受限于顺序地进行文件访问,而且只能使用这种笨拙的效率低下的一行接一行的处理方式)
  • 需要多维数组的支持
  • 需要数据结构的支持,比如链表、数组等数据结构
  • 需要产生或操作图形化界面
  • 需要直接操作系统硬件
  • 需要I/O或socket接口
  • 需要使用库或者遗留下来的旧代码的接口
  • 个人的闭源的应用(shell脚本把代码就放在文本文件中,全世界都能看到)

我们将开始使用Bash,Bash是“Bourne-Again shell”首字母的缩写,也是Stephen Bourne的经典的Bourne shell的一个双关语。对于所有UNIX上的shell脚本来说,Bash已经成为了事实上的标准了。

1.2. Sha-Bang(#!)

在每个脚本的开头使用sha-bang(#!),意味着告诉你的系统,这个文件的执行需要指定一个解释器。#!实际上是一个2字节的魔法数字,这是指定一个文件类型的特殊标记。换句话说,在这种情况下,指的是一个可执行的脚本。

在sha-bang之后接着的是一个路径名,这个路径名就是解释脚本中的命令的解释程序所在的路径,可能是一个shell,也可能是一个程序语言,也可能是一个工具包中的命令程序。这个解释程序从头开始解释并执行脚本中的命令(从sha-bang行下边的一行开始),忽略注释。

#!/bin/sh
#!/bin/bash
#!/usr/bin/perl
#!/usr/bin/tcl
#!/bin/sed -f
#!/usr/awk -f

2. 基本

2.1. 特殊字符

2.1.1. 特殊字符

用在脚本和其它地方的特殊字符

#

注释。

行首以#(#!是个列外)开头是注释

# This line is a comment.

注释也可以放在本行命令的后边

echo "A comment will follow." # 注释在这里

注释也可以放在本行行首空白的后面

# A tab precedes this comment.

命令是不能放在同一行上注释的后边的,因为没有办法把注释结束掉,好让同一行后边的代码生效。只能够另起一行来使用下一个命令。

当然,在echo中转义的#,是不能作为注释的。同样的,#也可以出现在特定参数替换结构中,或者是出现在数字常量表达式中。

echo "The # here does not begin a comment."
echo 'The # here does not begin a comment.'
echo The \# here does not begin a comment.

echo ${PATH#*:}         # 参数替换,不是一个注释
echo $(( 2#101011 ))    # 数值转换,不是一个注释

标准的引用和转义字符(" ’ \)可以用来转义#。某些特定的模式匹配操作也可以使用#。

;

命令分隔符[分号,即;]。
可以在同一行上写两个或两个以上的命令:

echo hello; echo there

;;

终止case选项[双分号,即;;]

case "$variable" in
    abc) echo "\$variable = abc" ;;
    xyz) echo "\$variable = xyz" ;;
esac

.

1)“点”命令[句点,即.]。等价于source命令,是一个bash的內建命令。

2)“点”作为文件名的一部分。如果点放在文件名的开头的话,那么这个文件将会成为“隐藏”文件,并且ls命令将不会正常的显示出这个文件。

3)如果作为目录名的话,一个单独的点代表当前的工作目录,而两个点表示上一次目录。

4)点经常出现在文件移动命令的目的参数(目录)的位置上

cp /home/canpool/work/* .

5)“点”字符匹配。当用作匹配字符的作用时,通常都是作为正则表达式的一部分来使用,“点”用来匹配任何的单个字符。

"

部分引用[双引号,即"]。"STRING"将会阻止(解释)STRING中大部分特殊的字符。

全引用[单引号,即’]。'STRING’将会阻止STRING中特殊字符的解释,这是一种比使用"更强烈的形式。

,

逗号操作符。逗号操作符连接了一系列的算术操作。虽然里面所有的内容都被运行了,但只有最后一项被返回。

let "t2 = ((a = 9, 15 / 3))"    # Set "a = 9" and "t2 = 15 / 3"

\

转义字符[反斜杠,即\]。一种对单字符的引用机制。

\X将会“转义”字符X。这等价于"X",也等价于’X’。\通常用来转义"和’,这样双引号和单引号就不会被解释成特殊含义了。

/

文件名路径分隔符[斜线,即/]。分隔文件名不同的部分(比如:/home/canpool/projects/Makefile)。
也可以用来作为除法算术操作符。

`

命令替换。`command`结构可以将命令的输出赋值到一个变量中。我们在后边的后置引用(backquotes)或后置标记(backticks)中也会讲解。

:

1)空命令(冒号)。等价于"NOP"(no op,一个什么也不干的命令),也可以被认为与shell的內建命令true作用相同。":“命令是一个bash的內建命令,它的退出码(exit status)是"true”(0)。

:
echo $?     # 0

2)死循环:

while :
do
    operation-1
    operation-2
    ...
    operation-n
done
# 与下边相同:
#   while true
#   do
#       ...
#   done

3)在if/then中的占位符:

if condition
then :  # 什么都不做,引出分支
else
    take-some-action
fi

4)在一个二元命令中提供一个占位符和默认参数

: $(username=`whoami`)

如果没有开头的":"的话,将会给出一个错误,除非"username"是一个命令或者內建命令。

5)在here document中提供一个命令所需的占位符

: <<TESTVARIABLES
${HOSTNAME?} ${USER?} ${MAIL?} # 如果其中某个变量没被设置, 那么就打印错误信息.
TESTVARIABLES

6)使用参数替换来评估字符串变量

: ${HOSTNAME?} ${USER?} ${MAIL?} # 如果其中某个变量没被设置, 那么就打印错误信息.

7)变量扩展/字串替换

${var:pos}
变量var从位置pos开始扩展(译者注: 也就是pos之前的字符都丢弃).
${var:pos:len}
变量var从位置pos开始, 并扩展len个字符.

8)在与>重定向操作符结合使用时,将会把一个文件清空,但是并不会修改这个文件的权限。如果之前这个文件并不存在,那么就创建这个文件。

: > data.xxx    # 文件"data.xxx"现在被清空了

与 cat /dev/null > data.xxx 的作用相同,然而,这并不会产生一个新的进程,因为":"是一个內建命令。

在与>>重定向操作符结合使用时,将不会对预先存在的目标文件(: >> target_file)产生任何影响。如果这个文件之前并不存在,那么就创建它。

注意:这只适用于正规文件,而不适用于管道,符号链接和某些特殊文件

8)也可能用来作为注释行,虽然我们不推荐这么做。使用#来注释的话,将关闭剩余行的错误检查,所以可以在注释行中写任何东西。然而,使用":"的话将不会这样。

: This is a comment that generates an error, ( if [ $x -eq 3] ).

9)":"还用来在/etc/passwd和$PATH变量中作为分隔符。

!

取反操作符(叹号)。!操作符将会反转命令的退出码的结果。也会反转测试操作符的意义。比如修改"等号"(=)为"不等号"(!=)。!操作符是Bash的关键字。

在一个不同的上下文中,!也会出现的变量的间接引用中。

在另一种上下文中,如命令行模式下,!还能反转bash的历史机制,需要注意的是,在一个脚本中,历史机制是被禁用的。

*

通配符(星号)。

1)*可以用来做文件名匹配的“通配符”,含义是,可以用来匹配给定目录下的任何文件名。

2)*也可以用在正则表达式中,用来匹配任意个数(包含0个)的字符。

3)算术操作符。在算术操作符的上下文中,*号表示乘法运算。如果要做求幂运算,使用**,这是求幂操作符。

?

测试操作符。

1)在一个特定的表达式中,?用来测试一个条件的结果。

2)在一个双括号结构中,?就是C语言的三元操作符。

3)在参数替换表达式中,?用来测试一个变量是否被set了。

通配符。

?在通配中,用来做匹配单个字符的“通配符”,在正则表达式中,也是用来表示一个字符。

$

1)变量替换(引用变量的内容)

var1=5
var2=23skidoo
echo $var1      # 5

在一个变量前面加上$用来引用这个变量的值。

2)行结束符。在正则表达式中,"$"表示行结束符。

${}

参数替换

$*,$@

位置参数

$?

退出状态码变量。$?变量保存了一个命令、一个函数或者脚本本身的退出状态码。

$$

进程ID变量。这个$$变量保存了它所在脚本的进程ID

()

1)命令组

(a=hello; echo $a)

在括号中的命令列表,将会作为一个子shell来运行。

在括号中的变量,由于是在子shell中,所以对于脚本剩下的部分是不可用的。父进程,也就是脚本本身,将不能够读取在子进程中创建的变量,也就是在子shell中创建的变量。

a=123
(a=321;)
echo "a = $a"   # a = 123,
                # 在圆括号中a变量,更像是一个局部变量

2)初始化数组

array=(element1 element2 element3)

{xxx,yyy,zzz,…}

大括号扩展

cat {file1,file2,file3} > combined_file

把file1,file2,file3连接在一起,并且重定向到combined_file中

cp file22.{txt,backup}

拷贝“file22.txt”到“file22.backup”中

一个命令可能会对大括号中的以逗号分割的文件列表起作用,通配将对大括号中的文件名做扩展。

在大括号中,不允许有空白,除非这个空白被引用或转义

echo {file1,file2}\ :{\ A," B",' C'}
file1 : A file1 : B file1 : C file2 : A file2 : B file2 : C

{}

代码块(大括号)。又被称为内部组,这个结构事实上创建了一个匿名函数(一个没有名字的函数),然而,与”标准“函数不同的是,在其中声明的变量,对于脚本其它部分的代码来说还是可见的。

a=123
{a=321;}
echo "a = $a"   # a = 321 (说明在代码块中对变量a所作的修改,影响了外边的变量)

下边代码展示了在大括号结构中代码的I/O重定向

File=/etc/fstab

{
    read line1
    read line2
} < $File

echo "First line in $File is:"
echo "$line1"
echo
echo "Second line in $File is:"
echo "$line2"

将一个代码块的结果保存到文件

{
    echo "hello"
    ...
} > "xx.test"

注意:与上面所见到的()中的命令组不同的是,{}大括号中的代码块将不会开启一个新的子shell

{} \;

路径名。一般都在find命令中使用,这不是一个shell內建命令。";"用来结束find命令序列的-exec选项,它需要被保护以防止被shell所解释。

[ ]

条件测试。
条件测试表达式在[ ]中,值得注意的是,[是一个shell內建test命令的一部分,并不是/usr/bin/test中的外部命令的一个链接。

[[ ]]

测试。
测试表达式放在[[ ]]中,(shell关键字)

[]

数组元素。
在一个array结构的上下文中,中括号用来引用数组中每个元素的编号。

array[1]=slot_1
echo ${array[1]}

字符范围。
用作正则表达式的一部分,方括号描述一个匹配的字符范围。

(( ))

整数扩展。
扩展并计算在(( ))中的整数表达式。

> &> >& >> < <>

重定向。

scriptname > filename 重定向scriptname的输出到文件filename中。如果filename存在的话,那么将会被覆盖。

command &> filename 重定向command的stdout和stderr到filename中。

command >&2 重定向command的stdout到stderr中。

scriptname >> filename 把scriptname的输出追加到文件filename中。如果filename不存在的话,将会被创建。

[i]<>filename 打开文件filename用来读写,并且分配文件描述符 i 给这个文件。如果filename不存在,这个文件将会被创建。

进程替换。

(command)>
<(command)

在一种不同的上下文中,"<“和”>“可用来做字符串比较操作。在另一种上下文中,”<“和”>"可用来做整数比较操作。

<<

用在here document中的重定向。

<<<

用在here string中的重定向。

<, >

ASCII码比较。

veg1=carrots
veg2=tomatoes
if [[ "$veg1" < "$veg2" ]]; then
    echo "1"
else
    echo "2"
fi

\<, \>

正则表达式中的单词边界。

grep '\' textfile

|

管道。分析前边命令的输出,并将输出作为后边命令的输入,这是一种产生命令链的好方法。

echo ls -l | sh

传递"echo ls -l"的输出到shell中,与一个简单的"ls -l"结果相同。

cat *.lst | sort | uniq

合并和排序所有的".lst"文件,然后删除所有重复的行。

管道是进程间通信的一个典型办法,将一个进程的stdout放到另一个进程的stdin中,标准的方法是将一个一般命令的输出,比如cat或者echo,传递到一个“过滤命令”(在这个过滤命令中将处理输入)中,然后得到结果。

cat $filename1 $filename2 | grep $search_word

当然,输出的命令也可以传递到脚本中。

管道中的每个进程的stdout必须被下一个进程作为stdin来读入,否则,数据流会阻塞,并且管道将产生一些非预期的行为。

cat file1 file2 | ls -l | sort

从"cat file1 file2"中的输出并没出现。

作为子进程的运行的管道,不能够改变脚本的变量:

variable="initial_value"
echo "new_value" | read variable
echo "variable = $variable"     # variable = initial_value

如果管道中的某个命令产生了一个异常,并中途失败,那么这个管道将过早的终止。这种行为被叫做broken pipe。并且这种状态下将发送一个SIGPIPE信号。

>|

强制重定向(即使设置了noclobber选项,就是-C选项),这将强制的覆盖一个现存文件。

||

或逻辑操作。在一个条件测试结构中,如果条件测试结构两边中的任意一边结果为true的话,||操作就会返回0(代表执行成功)。

&

后台运行命令。一个命令后边跟一个&表示在后台运行。

在一个脚本中,命令和循环都可能运行在后台。

for i in 1 2 3 4 5 6 7 8 9 10
do
    echo -n "$i "
done &

在一个脚本内后台运行一个命令,有可能造成这个脚本的挂起,等待一个按键响应。幸运的是,在后面有针对这个问题的解决办法。

&&

与逻辑操作。在一个条件测试结构中,只有在条件测试结构的两边结果都为true的时候,&&操作才会返回0(代表success)。

-

1)选项,前缀。在所有的命令内,如果想使用选项参数的话,前边都要加上"-"。

COMMAND -[Option1][Option2][...]
ls -al
sort -dfu $filename
set -- $variable
if [ $file1 -ot $file2 ]; then
    echo "File $file1 is older than $file2."
fi

if [ "$a" -eq "$b" ]; then
    echo "$a is equal to $b."
fi

if [ "$c" -eq 24 -a "$d" -eq 47 ]; then
    echo "$c equals 24 and $d equals 47."
fi

2)用于重定向stdin或stdout[破折号]。

(cd /source/director && tar cf - . ) | (cd /dest/directory && tar xpvf -)

从一个目录移动整个目录到另一个目录。

1)cd /source/directory  源目录
2)&&                    "与列表":如果'cd'命令成功了,那么就执行下面的命令
3)tar cf - .            'c'创建一个新文档,'f'后边跟'-'指定目标文件作为stdout
'-'后边的'f'(file)选项,指明作为stdout的目标文件. 并且在当前目录('.')执行
5)|                     管道...
6)(...)                 一个子shell
7)cd /dest/directory    改变当前目录到目标目录
8)&&                    "与列表",同上
9)tar xpvf -           'x'解档,'p'保证所有权和文件属性,'v'发完整消息到stdout,
                        'f'后边跟'-'从stdin读取数据

注意:在这个上下文中"-"本身并不是一个bash操作,而是一个可以被特定的UNIX工具识别的选项,这些特定的UNIX工具特指那些可以写输出到stdout的工具,比如tar,cat等。

echo "whatever" | cat -

在需要一个文件名的位置,'-'重定向输出到stdout(有时候会在tar和cf中出现),或者从stdin接受输入,而不是从一个文件中接受输入。这是在管道中使用文件导向(file-oriented)工具来作为过滤器的一种方法。

'-'可以被用来将stdout通过管道传递到其他命令中,这样就允许使用在一个文件开头添加几行的技巧。使用diff命令来和另一个文件的某一段进行比较:

grep Linux file1 | diff file2 -

3)先前的工作目录。cd -将会回到先前的工作目录,它使用$OLDPWD环境变量。

4)减号,属于算术操作。

+

加号,加法算术操作。在另一种上下文环境中,+也是一种正则表达式操作。

选项。一个命令或者过滤器的选项标记。某些命令內建命令使用+来打开特定的选项,用-来禁用这些特定的选项。

%

取模。取模算术操作。在不同的上下文中,%也是一种模式匹配操作。

~

home目录[波浪号]。相当于$HOME内部变量。

~+

当前工作目录。相当于$PWD内部变量。

~-

先前的工作目录,相当于$OLDPWD内部变量。

=~

正则表达式匹配。

^

行首,在正则表达式中,"^"表示定位到文本行的行首。

2.1.2. 控制字符

修改终端或文本显示的行为,控制字符以 CONTROL + key 这种方式进行组合(同时按下),控制字符也可以使用8进制或16进制表示法来进行表示,但是前边必须要加上转义符。

控制字符在脚本中不能正常使用。

Ctl-B       退格(非破坏性),就是退格但是不删掉前面的字符

Ctl-C       break,终结一个前台作业

Ctl-D       从一个shell中登出(与exit很相像)。
            "EOF"(文件结束)。这也能从stdin中终止输入。
            在console或者在xterm窗口中输入的时候,Ctl-D将删除光标下字符。
            当没有字符时,Ctl-D将退出当前会话,在一个xterm窗口中,
            则会产生关闭此窗口的效果。

Ctl-G       "哔"(beep),在一些老式的打印机终端上,它会响一下铃。

Ctl-H       "退格"(破坏性的),就是在退格之后,还要删掉前面的字符。

Ctl-I       水平制表符。

Ctl-J       重起一行(换一行并到行首),在脚本中,也可以使用8进制表示法,'\012'
            或者16进制表示法,'\x0a'来表示

Ctl-K       垂直制表符。当在console或者xterm窗口中输入文本时,
            Ctrl-K将会删除从光标所在处到行尾的全部字符。
            在脚本中,Ctl-K的行为有些不同。

Ctl-L       清屏(清除终端的屏幕显示)。在终端中,与clear命令的效果相同,
            当发送到打印机上时,Ctl-L会让打印机将打印纸卷到最后。

Ctl-M       回车。

Ctl-Q       恢复(XON),在一个终端中恢复stdin。

Ctl-S       挂起(XOFF),在一个终端中冻结stdin。(使用Ctl-Q可以恢复输入)

Ctl-U       删除光标到行首的所有字符。在某些设置下,
            不管光标的所在位置Ctl-U都将删除整行输入。

Ctl-V       当输入字符时,Ctl-V允许插入控制字符,比如,下边的两个例子是等价的:
            echo -e '\x0a'
            echo 

Ctl-V       主要用于文本标记。

Ctl-W       当在控制台或一个xterm窗口敲入文本时,
            Ctl-W将会删除当前光标到左边最近一个空格间的全部字符,
            在某些设置下,Ctl-W将会删除当前光标到左边第一个非字母或数字之间的全部字符。

Ctl-Z       暂停前台作业。

2.1.3. 空白

用来分隔函数,命令或变量。空白包含空格,tab,空行,或者是它们之间任意的组合体。在某些上下文中,比如变量赋值,空白是不被允许的,会产生语法错误。

空行不会影响脚本的行为,因此使用空行可以很好的划分独立的函数段以增加可读性。

特殊变量 $IFS 用来做一些输入命令的分隔符,默认情况下是空白。如果想在字符串或变量中使用空白,那么应该使用引用。

2.2. 变量和参数

变量是脚本编程中进行数据表现的一种方法,说白了,变量不过是计算机为了保留数据项,而在内存中分配的一个位置或一组位置的标识或名字。

变量既可以出现在算术操作中,也可以出现在字符串分析过程中。

2.2.1. 变量替换

变量的名字就是变量保存值的地方,引用变量的值就叫做变量替换。

$

让我们仔细的区别变量的名字和变量的值。如果variable1是一个变量的名字,那么$variable1就是引用这变量的值,即这边变量所包含的数据。

当变量“裸体”出现的时候,也就是说没有$前缀的时候,那么变量可能存在如下几种情况:
变量被声明或被赋值,变量被unset,变量被export,或者是变量处在一种特殊的情况,变量代表一种信号。
变量赋值可以使用=(比如 var1=27),也可以在read命令中或者循环头进行赋值。

被一对双引号(" “)括起来的变量替换是不会被阻止的,所以双引号被称为部分引用,有时候又被称为"弱引用”。但是如果使用单引号的话(‘’),那么变量替换就会被禁止了,变量名只会被解释成字面的意思,不会发生变量替换。所以单引号被称为全引用,有时候也被称为“强引用”。

注意:$variable事实上只是${variable}的简写形式,在某些上下文中$variable可能会引起错误,这时候你就需要用${variable}了。

一个未初始化的变量将会是“null”值,就是未赋值(但并不是代表值是0)。在给变量赋值之前就使用这个变量通常都会引起问题。

但是在执行算术操作的时候,仍然有可能使用未初始化过的变量。

echo "$uninitialized"       # (blank line)
let "uninitialized += 5"    # Add 5 to it.
echo "$uninitialized"       # 5

结论:一个未初始化的变量是没有值的,但是在做算术操作的时候,这个未初始化的变量看起来值为0。这是一个未文档化(并且可能不具可移植性)的行为。

2.2.2. 变量赋值

=

赋值操作(前后都不能有空格)。

因为=和-eq都可以用做条件测试操作,所以不要与这里的赋值操作相混淆。

注意:=既可以用做条件测试操作,也可以用于赋值操作,这需要视具体的上下文而定。

使用$(…)机制来进行变量赋值(这是一种比后置引用(反引号`)更新的一种方法),事实上这两种方法都是命令替换的一种形式。

2.2.3. Bash变量是不区分类型的

不像其它程序语言一样,Bash并不对变量区分“类型”。本质上,Bash变量都是字符串,但是依赖于具体的上下文,Bash也允许比较操作和整数操作。其中的关键因素就是,变量中的值是否只有数字。

不区分变量的类型既是幸运的事情也是悲惨的事情,它允许你在编写脚本的时候更加的灵活(但是也足够把你搞晕),并且可以让你能够更容易的编写代码。然而,这也很容易产生错误,并且让你养成糟糕的编程习惯。

这样的话,程序员就承担了区分脚本中变量类型的责任,Bash是不会为你区分变量类型的。

2.2.4. 特殊的变量类型

2.2.4.1. 局部变量

这种变量只有在代码块或者函数中才可见。

2.2.4.2. 环境变量

这种变量将影响用户接口和shell的行为。

在通常情况下,每个进程都有自己的“环境”,这个环境是由一组变量组成的,这些变量中存在进程可能需要引用的信息。在这种情况下,shell与一个一般的进程没什么区别。

每次当一个shell启动时,它都将创建适合于自己环境变量的shell变量。更新或者添加一个新的环境变量的话,这个shell都会立刻更新它自己的环境(换句话说,更改或增加的变量会立即生效),并且所有的shell子进程(即这个shell所执行的命令)都会继承这个环境。(准确地说,应该是后继生成的子进程才会继承shell的新环境变量,已经运行的子进程并不会得到它的新环境变量)。

分配给环境变量的空间是有限的,创建太多环境变量,或者给一个环境变量分配太多的空间都会引起错误。

如果一个脚本要设置一个环境变量,那么需要将这些变量“export”出来,也就是需要通知到脚本本地的环境。这是export命令的功能。

一个脚本只能够export变量到这个脚本所产生的子进程,也就是说只能够对这个脚本所产生的命令和进程起作用。如果脚本是从命令中调用的,那么这个脚本所export的变量是不能影响命令行环境的。也就是说,子进程是不能够export变量来影响产生自己的父进程的环境的。

2.2.4.3. 位置参数

从命令行传递到脚本的参数:$0,$1,$2,$3 …

$0就是脚本文件本身的名字,$1是一个参数,$2是第二个参数,$3是第三个参数,然后是第四个。$9之后的位置参数就必须用大括号括起来了,比如:${10},${11},${12}。

两个比较特殊的变量 $* 和 $@ 表示所有的位置参数。

{}标记法提供了一种提取从命令行传递到脚本的最后一个位置参数的简单办法。但是这种方法同时还需要使用间接引用。

args=$#                 # 位置参数的个数
lastarg=${!args}        # 或:lastarg=${!#}

一些脚本可能会依赖于使用不同的调用名字,来表现出不同的行为。如果想要达到这种目的,一般需要在脚本中检查$0。因为脚本只能够有一个真正的文件名,如果要产生多个名字,必须使用符号链接。

如果脚本需要一个命令行参数,而在调用的时候,这个参数没被提供,那么这就可能造成给这个参数赋一个null变量,通常情况下,这都会产生问题。一种解决这个问题的办法就是使用添加额外字符的方法,在使用这个位置参数的变量和位置参数本身的后边全部添加同样的额外字符。

shift命令会重新分配位置参数,其实就是把所有的位置参数都向左移动一个位置。

$1 <--- $2$2 <--- $3$3 <--- $4...

原来的$1就消失了,但是$0(脚本名)是不会改变的。如果传递了大量的位置参数到脚本中,那么shift命令允许你访问的未知参数的数量超过10个,当然{}标记法也提供了这样的功能。

$0参数是由调用这个脚本的进程所设置的,按照约定,这个参数一般就是脚本的名字,具体请参考execv的man页。

2.3. 引用

引用的字面意思就是将字符串用双引号括起来,它的作用就是保护字符串中的特殊字符不被shell或者shell脚本重新解释,或者扩展。(这里所说的“特殊”指的是一些字符在shell中具有的特殊意义,而不是字符的字面意思,比如通配符 *)。

某些程序和工具能够重新解释或者扩展被引用的特殊字符。引用的一个重要作用就是保护命令行参数不被shell解释,但是还是能够让正在调用的程序来扩展它。

引用还可以改掉echo’s不换行的“毛病”。

canpool@DESKTOP-ODCM7SC:~$ ls
bin  work
canpool@DESKTOP-ODCM7SC:~$ echo $(ls -l)
total 0 drwxr-xr-x 1 canpool canpool 4096 Mar 6 00:38 bin drwxr-xr-x 1 canpool canpool 4096 Apr 2 00:10 work
canpool@DESKTOP-ODCM7SC:~$ echo "$(ls -l)"
total 0
drwxr-xr-x 1 canpool canpool 4096 Mar  6 00:38 bin
drwxr-xr-x 1 canpool canpool 4096 Apr  2 00:10 work

2.3.1. 引用变量

在一个双引号中通过直接使用变量名的方法来引用变量,一般情况下都是没问题的。这么做将阻止所有在引号中的特殊字符被重新解释,包括变量名,但是$,`(后置引用)和\(转义符)除外。保留$作为特殊字符的意义是为了能够在双引号中也能够正常的引用变量(“$variable”),也就是说,这个变量将被它的值所取代。

使用双引号还能够阻止单词分割(word splitting)。如果一个参数被双引号括起来的话,那么这个参数将被认为是一个单元,即使这个参数包含有空白,那里面的单词也不会被分割开。

在echo语句中,只有在单词分割或者需要保留空白的时候,才需要把参数用双引号括起来。

单引号操作与双引号基本一样,但是不允许引用变量,因为$的特殊意义被关闭了。在单引号中,任何特殊字符都按照字面的意思进行解释,除了’。所以说单引号(“全引用”)是一种比双引号(“部分引用”)更严格的引用方法。

因为即使是转义符(\)在单引号中也是按照字面意思解释的,所以如果想在一对单引号中显示一个单引号是不行的(因为单引号对是按照就近原则完成的)。

2.3.2. 转义

转义是一种引用单个字符的方法。一个前面放上转义符(\)的字符就是告诉shell这个字符按照字面的意思进行解释。换句话说,就是这个字符失去了它的特殊含义。

在某些特定的命令和工具中,比如echo和sed,转义符往往会起到相反效果,它反倒可能会引发出这个字符的特殊含义。

特定的转义符的特殊的含义。echo和sed命令中使用:

\n              表示新的一行
\r              表示回车
\t              表示水平制表符
\v              表示垂直制表符
\b              表示后退符
\a              表示“alert”(蜂鸣或者闪烁)
\0xx            转换为八进制的ASCII吗,等监狱0xx
\"              表示引号字面的意思
\$              表示$本身字面的含义(跟踪\$后边的变量名将不能引用变量的值)
\\              表示反斜杠字面的意思

\的行为依赖于它自身是否被转义,被引用(“”),或者是否出现在命令替换或here document中。

简单的转义和引用

echo \z                 # z
echo \\z                # \z
echo '\z'               # \z
echo '\\z'              # \\z
echo "\z"               # \z
echo "\\z"              # \z

命令替换

echo `echo \z`          # z
echo `echo \\z`         # z
echo `echo \\\z`        # \z
echo `echo \\\\z`       # \z
echo `echo \\\\\\z`     # \z
echo `echo \\\\\\\z`    # \\z
echo `echo "\z"`        # \z
echo `echo "\\z"`       # \z

Here document

cat <<EOF
\z
EOF                     # \z

cat <<EOF
\\z
EOF                     # \z

赋值给变量的字符串的元素也会被转义,但是不能把一个单独的转义符赋值给变量。(单独的转义符转义了一个换行符,变成了续行符的含义)。

转义一个空格会阻止命令行参数列表的“单词分割”问题。

转义符也提供续行功能,也就是编写多行命令的功能。一般的,每一个单独行都包含一个不同的命令,但是每行结尾的转义符都会转义换行符,这样下一行会与上一行一起形成一个命令序列。

如果一个脚本以|(管道符)结束,那么就不用非得加上转义符\了。但是一个好的编程风格,还是应该在行尾加上转义符。

2.4. 退出和退出状态码

exit被用来结束一个脚本,就像在C语言中一样,它也返回一个值,并且这个值会传递给脚本的父进程,父进程会使用这个值做下一步的处理。

每个命令都会返回一个退出状态码(有时候也被称为返回状态)。成功的命令返回0,而不成功的命令返回非零值,非零值通常被解释成一个错误码。行为良好的UNIX命令,程序和工具都会返回0作为退出码来表示成功,虽然偶尔也会有例外。

同样的,脚本中的函数和脚本本身也会返回退出状态码。在脚本或者是脚本函数中执行的最后的命令会决定退出状态码。在脚本中,exit nnn命令将会把nnn退出码传递给shell(nnn必须是十进制数,范围必须是0-255)。

当脚本以不带参数的exit命令来结束时,脚本的退出状态码就由脚本中最后执行的命令来决定(就是exit之前的命令)。不带参数的exit命令与exit $?的效果是一样的,甚至脚本的结尾不写exit,也与前两者的效果相同。

$?保存了最后所执行的命令的退出状态码,当函数返回之后,$?保存函数中最后所执行的命令的退出状态码,这就是bash对函数“返回值”的处理方法。当一个脚本退出,$?保存了脚本的退出状态码,这个退出状态码也就是脚本中最后一个执行命令的退出状态码。一般情况下,0表示成功,在范围1-255的整数表示错误。

!,逻辑“非”操作符,将会反转命令或条件测试的结果,并且这会影响退出状态码。

特定的退出状态码具有保留含义,所以用户不应该在脚本中指定它。

2.5. 条件判断

每个完整并且合理的程序语言都具有条件判断的功能,并且可以根据条件测试的结果做下一步的处理,Bash有test命令,各种中括号和圆括号操作,和if/then结构。

2.5.1. 条件测试结构

  • if/then结构用来判断命令列表的退出状态码是否为0(因为在UNIX惯例,0表示“成功”),如果成功的话,那么就执行接下来的一个或多个命令。
  • 有一个专有命令 [ (左中括号,特殊字符),这个命令与test命令等价,并且出于效率上的考虑,这是一个內建命令。这个命令把它的参数作为比较表达式或者作为文件测试,并且根据比较的结果来返回一个退出状态码(0表示真,1表示假)。
  • 在版本2.02的Bash中,引入了 [[ … ]] 扩展测试命令,因为这种表现形式可能对某些语言的程序员来说更容易熟悉一些,注意 [[ 是一个关键字,并不是一个命令。

    Bash把 [[ $a -lt $b ]] 看作一个单独的元素,并且返回一个退出状态码。

    (( … ))和let …结构也能够返回退出状态码,当它们所测试的算术表达式的结果为非零的时候,将会返回退出状态码0。这些算术扩展结构被用来做算术比较。
  • if命令能够测试任何命令,并不仅仅是中括号中的条件。
  • 一个if/then结构可以包含嵌套的比较操作和条件判断操作。

    如果if和then在条件判断的同一行上的话,必须使用分号来结束if表达式。if和then都是关键字。关键字(或者命令)如果作为表达式的开头,并且如果想在同一行上再写一个新的表达式的话,那么必须使用分号来结束上一句表达式。

else if 和 elif

elif是else if的缩写形式,作用是在外部的判断结构中再嵌入一个内部的if/then结构。

if [ condition1 ]
then
    command1
    command2
    command3
elif [ condition2 ]
# 与else if一样
then
    command4
    command5
else
    default-command
fi

if test condition-true结构与if [ condition-true ]完全相同,就像我们前面看到的,左中括号 [,是调用test命令的标识。而关闭条件判断用的右中括号 ],在if/test结构中并不是严格必须的,但是在Bash的新版本中必须要求使用。

test命令在Bash中是內建命令,用来测试文件类型,或者用来比较字符串。因此,在Bash脚本中,test命令并不会调用外部的/usr/bin/test中的test命令,这是sh-utils工具包中的一部分。同样的,[也并不会调用/usr/bin/[,这是/usr/bin/test的符号链接。

canpool@DESKTOP-ODCM7SC:~$ type test
test is a shell builtin
canpool@DESKTOP-ODCM7SC:~$ type [
[ is a shell builtin
canpool@DESKTOP-ODCM7SC:~$ type [[
[[ is a shell keyword
canpool@DESKTOP-ODCM7SC:~$ type ]]
]] is a shell keyword
canpool@DESKTOP-ODCM7SC:~$ type ]
bash: type: ]: not found

test,/usr/bin/test,[ ]和/usr/bini/[都是等价命令。

[[ ]]结构比[ ]结构更加通用,这是一个扩展的test命令。

在[[ 和 ]]之间所有的字符都不会发生文件名扩展或者单词分割,但是会发生参数扩展和命令替换。

使用[[ … ]]条件判断结构,而不是[ … ],能够防止脚本中的许多逻辑错误。比如,&&,||,<,和>操作符能够正常存在于[[ ]]条件判断结构中,但是如果出现在[ ]结构中的话,会报错。

在if后面也不一定非得是test命令或者是用于条件判断的中括号结构([ ] 或 [[ ]])。"if COMMAND"结构将会返回COMMAND的退出状态码。与此相似,在中括号中的条件判断也不一定非得要if不可,也可以使用列表结构。

(( ))结构扩展并计算一个算术表达式的值,如果表达式的结果为0,那么返回的退出状态码为1,或者是“假”。而一个非零值的表达式所返回的退出状态码将为0,或者是“true”。这种情况和先前所讨论的test命令和[ ]结构的行为正好相反。

2.5.2. 文件测试操作符

如果下面的条件成立将会返回真。

-e          文件存在

-a          文件存在,这个选项的效果与-e相同,但是它已经被“弃用”了,并且不鼓励使用。

-f          表示这个文件是一个一般文件(并不是目录或者设备文件)

-s          文件大小不为零

-d          表示这是一个目录

-b          表示这是一个块设备(软盘,光驱等)

-c          表示这是一个字符设备(键盘,modem,声卡等)

-p          这个文件是一个管道

-h          这是一个符号链接

-L          这是一个符号链接

-S          表示这是一个socket

-t          文件(描述符)被关联到一个终端设备上。
            这个测试选项一般被用来检测脚本中的stdio([ -t 0 ])或者
            stdout([ -t 1 ])是否来自一个终端

-r          文件是否具有可读权限
            (指的是正在运行这个测试命令的用户是否具有读权限)

-w          文件是否具有可写权限
            (值得是正在运行这个测试命令的用户是否具有写权限)

-x          文件是否具有可执行权限
            (指的是正在运行这个测试命令的用户是否具有可执行权限)

-g          set-group-id(sgid)标记被设置到文件或目录上。
            如果目录具有sgid标记的话,那么这个目录下所创建的文件
            将属于拥有这个目录的用户组,而不必是创建这个文件的用户组,
            这个特性对于在一个工作组中共享目录非常有用。

-u          set-user-id(suid)标记被设置到文件上。
            如果一个root用户所拥有的二进制可执行文件设置了set-user-id标记位的话,
            那么普通用户也会以root 权限来运行这个文件。
            这对于需要访问系统硬件的执行程序(比如pppd和cdrecord)非常有用。
            如果没有suid标志的话,这些二进制执行程序是不能够被非root用户调用的。
            对于设置了suid标志的文件,在它的权限列中将会以s表示。

-k          设置粘贴位。对于“粘贴位”的一般了解,
            save-text-mode标志是一个文件权限的特殊类型。
            如果文件设置了这个标志,那么这个文件将会被保存到缓存中,
            这样可以提高访问速度。粘贴位如果设置在目录中,那么它将限制写权限。
            对于设置了粘贴位的文件或目录,在它们的权限标记列表中将会显示t。
            如果用户并不拥有这个设置了粘贴位的目录,但是他在这个目录下具有写权限,
            那么这个用户只能在这个目录下删除自己所拥有的文件。
            这将有效的防止用户在一个公共目录中不慎覆盖或者删除别人的文件。
            比如说/tmp目录。(当然,目录的所有者或者root用户可以随意删除或重命名其中的文件)

-O          判断你是否是文件的拥有者

-G          文件的group-id是否与你的相同

-N          从文件上一次被读取到现在为止,文件是否被修改过

f1 -nt f2   文件f1比文件f2新

f1 -ot f2   文件f1比文件f2旧

f1 -ef f2   文件f1和文件f2是相同文件的硬链接

!           “非” -- 反转上边所有测试的结果(如果没给出条件,那么返回真)

2.5.3. 其它比较操作符

二元比较操作符用来比较两个变量或数字。注意整数比较与字符串比较的区别。

整数比较

-eq         等于
            if [ "$a" -eq "$b" ]

-ne         不等于
            if [ "$a" -ne "$b" ]

-gt         大于
            if [ "$a" -gt "$b" ]

-ge         大于等于
            if [ "$a" -ge "$b" ]

-lt         小于
            if [ "$a" -lt "$b" ]

-le         小于等于
            if [ "$a" -le "$b" ]

<           小于(在双括号中使用)
            (("$a" < "$b"))

<=          小于等于(在双括号中使用)
            (("$a" <= "$b"))

>           大于(在双括号中使用)
            (("$a" > "$b"))

>=          大于等于(在双括号中使用)
            (("$a" >= "$b"))

字符串比较

=           等于
            if [ "$a" = "$b" ]

==          等于
            if [ "$a" == "$b" ]=等价。
            ==比较操作符在双中括号对和单中括号对中的行为是不同的。
            [[ $a == z* ]]      # 如果$a以"z"开头(模式匹配)那么结果将为真
            [[ $a == "z*" ]]    # 如果$a与z*相等(就是字面意思完全一样), 那么结果为真.
            [ $a == z* ]        # 文件扩展匹配(file globbing)和单词分割有效.
            [ "$a" == "z*" ]    # 如果$a与z*相等(就是字面意思完全一样), 那么结果为真.

!=          不等号
            if [ "$a" != "$b" ]
            这个操作符将在[[ ... ]]结构中使用模式匹配

<           小于,按照ASCII字符进行排序
            if [[ "$a" < "$b" ]]
            if [ "$a" \< "$b" ]
            注意"<"使用在[  ]结构中的时候需要被转义

>           大于,按照ASCII字符进行排序
            if [[ "$a" > "$b" ]]
            if [ "$a" \> "$b" ]
            注意">"使用在[  ]结构中的时候需要被转义

-z          字符串为“null”,意思就是字符串长度为零

-n          字符串不为“null”。
            当-n使用在中括号中进行条件测试的时候,必须要把字符串用双引号引用起来。
            如果采用了未引用的字符串来使用 ! -z,
            甚至是在条件测试中括号中只使用未引用的字符串的话,一般也是可以工作的,
            然而,这是一种不安全的习惯。习惯于使用引用的测试字符串才是正路。

-a          逻辑与
            exp1 -a exp2 如果表达式exp1和exp2都为真的话,那么结果为真

-o          逻辑或
            exp1 -o exp2 如果表达式exp1和exp2中至少有一个为真的话,那么结果为真。
            这与Bash中的比较操作符&&||非常相像,但是这两个操作符是用在双中括号结构中的。
                [[ condition1 && condition2 ]]
            -o和-a操作符一般都是和test命令或者是单中括号结构一起使用的
                if [ "$exp1" -a "$exp2" ]

注意事项:在一个混合测试中,即使使用引用的字符串变量也可能还不够。如果 s t r i n g 为空的话, [ − n " string为空的话,[ -n " string为空的话,[n"string" -o “ a " = " a" = " a"="b” ]可能会在某些版本的Bash中产生错误。安全的做法是附加一个额外的字符给可能的空变量,[ “x s t r i n g " ! = x − o " x string" != x -o "x string"!=xo"xa” = “x$b” ](“x”字符是可以相互抵消的)。

2.6. 操作符与相关主题

2.6.1. 操作符

2.6.1.1. 赋值
变量赋值    初始化或者修改变量的值

=           通用赋值操作符,可用于算术和字符串赋值
2.6.1.2. 算术操作符
+           加法计算
-           减法计算
*           乘法计算
/           除法计算
**          幂运算
%           模运算,或者是求余运算(返回一次除法运算的余数)
            模运算经常在其它的一些情况中出现,比如说产生特定范围的数字,
            或者格式化程序的输出。它甚至可以用来产生质数,
            事实上模运算在算术运算中的使用频率高的惊人。

+=          加-等于(把变量的值增加一个常量然后再把结果赋给变量)
            let "var +=5" var变量的值会在原来的基础上加5。
-=          减-等于(把变量的值减去一个常量然后再把结果赋给变量)
*=          乘-等于(先把变量的值乘以一个常量的值,然后再把结果赋给变量)
            let "var *= 4" var变量的结果将会在原来的基础上乘以4。
/=          除-等于(先把变量的值除以一个常量的值,然后再把结果赋给变量)
%=          取模-等于(先对变量进行模运算,即除以一个常量取模,然后把结果赋给变量)

算术操作符经常会出现在expr或let表达式中。

在Bash中的整型变量事实上是一个有符号的long(32-bit)整型值,所表示的范围是-21474833638到2147483647。如果超过这个范围进行算术操作的话,那么将不会得到你期望的结果。(溢出)

在2.05b版本之后,Bash开始支持64位整型了。

Bash不能够处理浮点运算,它会把包含小数点的数字看作字符串。
如果非要做浮点运算的话,可以在脚本中使用bc,这个命令可以进行浮点运算,或者调用数学库函数。

2.6.1.3. 位操作符

位操作符在shell脚本中很少被使用,他们最主要的用途就是操作和测试从端口或者sockets中读取的值。位翻转“Bit flipping”与编译语言的联系很紧密,比如C/C++,在这种语言中他可以运行的足够快。

<<          左移一位(每次左移都相当于乘以2)
<<=         左移-赋值
            let "var <<= 2" 这句的结果就是变量var左移2位(就是乘以4)
>>          右移一位(每次右移都将除以2)
>>=         右移-赋值
&           按位与
&=          按位与-赋值
|           按位或
|=          按位或-赋值
~           按位反
!           按位非
^           按位异或XOR
^=          按位异或-赋值
2.6.1.4. 逻辑操作符
&&          与(逻辑)
            &&也可以用在与列表中,但是使用在连接命令中时,需要依赖于具体的上下文
||          或(逻辑)
            Bash将会测试每个表达式的退出状态码,这些表达式由逻辑操作符连接起来。
2.6.1.5. 混杂的操作符
,           逗号操作符
            逗号操作符可以连接两个或多个算术运算。
            所有的操作都会被运行(可能会有副作用),但是只会返回最后操作的结果。
            逗号操作符主要用在for循环中。

2.6.2. 数字常量

shell脚本在默认情况下都是把数字作为10进制数来处理,除非这个数字采用了特殊的标记或者前缀。如果数字以0开头的话那么就是8进制数。如果数字以0x开头的话那么就是16进制数。如果数字中间嵌入了#的话,那么就被认为是BASE#NUMBER形式的标记法(有范围和符号限制)。

# 进制: 默认情况
let "dec = 32"
echo "decimal number = $dec"        # 32

# 进制: 以'0'(零)开头
let "oct = 032"
echo "octal number = $oct"          # 26
# 表达式结果是用10进制表示的.

# 进制: 以'0x'或者'0X'开头的数字
let "hex = 0x32"
echo "hexadecimal number = $hex"    # 50
# 表达式结果是用10进制表示的.

# 其他进制: BASE#NUMBER
# BASE的范围在2到64之间.
# NUMBER的值必须使用BASE范围内的符号来表示, 具体看下边的示例.

let "bin = 2#111100111001101"
echo "binary number = $bin"         # 31181

let "b32 = 32#77"
echo "base-32 number = $b32"        # 231

let "b64 = 64#@_"
echo "base-64 number = $b64"        # 4031
# 这个表示法只能工作于受限的ASCII字符范围(2 - 64).
# 个数字 + 26个小写字母 + 26个大写字符 + @ + _

echo $((36#zz)) $((2#10101010)) $((16#AF16)) $((53#1aA))
# 170 44822 3375

# 重要的注意事项:
# ---------------
# 使用一个超出给定进制的数字的话, 将会引起一个错误.

let "bad_oct = 081"
# (部分的) 错误消息输出:
# bad_oct = 081: value too great for base (error token is "081")
# Octal numbers use only digits in the range 0 - 7

3. 进阶

3.1. 变量重游

3.1.1. 内部变量

內建变量,这些变量将会影响bash脚本的行为。

$BASH           bash的二进制程序文件的路径

$BASH_ENV       这个环境变量会指向一个bash的启动文件,当一个脚本被调用的时候,
                这个启动文件将会被读取。

$BASH_SUBSHELL  这个变量用来提示shell的层次。这是一个bash的新特性,
                直到版本3的bash才被引入进来。

$BASH_VERSINFO[n]
                这是一个含有6个元素的数组,它包含了所安装的bash的版本信息。
                这与下边的$BASH_VERSION很相像,但是这个更加详细一些。

                # Bash version info:

                for n in 0 1 2 3 4 5
                do
                    echo "BASH_VERSINFO[$n] = ${BASH_VERSINFO[$n]}"
                done

                # BASH_VERSINFO[0] = 4          # 主版本号.
                # BASH_VERSINFO[1] = 4          # 次版本号.
                # BASH_VERSINFO[2] = 20         # 补丁次数.
                # BASH_VERSINFO[3] = 1          # 编译版本.
                # BASH_VERSINFO[4] = release    # 发行状态.
                # BASH_VERSINFO[5] = x86_64-pc-linux-gnu
                # 结构体系(与变量$MACHTYPE相同).

$BASH_VERSION   安装在系统上的Bash版本号
                检查$BASH_VERSION对于判断系统上到底运行的是哪个shell来说
                是一种非常好的方法,变量$SHELL有时候不能够给出正确的答案。

$DIRSTACK       在目录栈中最顶端的值。(将会受到pushd和popd的影响)
                这个內建变量与dirs命令相符,但是dirs命令会显示目录栈的整个内存

$EDITOR         脚本所调用的默认编辑器,通常情况下是vi或者是emacs

$EUID           “有效”用户ID
                不管当前用户被假定成什么用户,这个数都用来表示当前用户的标识号,
                也可能使用su命令来达到假定的目的。
                $EUID并不一定与$UID相同

$FUNCNAME       当前函数的名字

$GLOBIGNORE     一个文件名的模式匹配列表,
                如果在统配(globbing)中匹配到的文件包含有这个列表中的某个文件,
                那么这个文件将被从匹配到的结果中去掉。

$GROUPS         目前用户所属的组
                这是一个当前用户的组id列表(数组),与记录在/etc/passwd文件中的内容一样。

$HOME           用户的home目录,一般是/home/username

$HOSTNAME       hostname放在一个初始化脚本中,在系统启动的时候分配一个系统名字,
                然而,gethostname()函数可以用来设置这个bash内部变量$HOSTNAME

$HOSTTYPE       主机类型,吉祥$MACHTYPE,用来识别系统硬件

$IFS            内部域分隔符
                这个变量用来决定bash在解释字符串时如何识别域,或者单词边界
                $IFS默认为空白(空格,制表符和换行符),但这是可以修改的,
                比如,在分析逗号分隔的数据文件是,就可以设置为逗号。
                注意$*使用的是保存在$IFS中的第一个字符。

$IGNOREOF       忽略EOF:告诉shell在log out之前要忽略多少文件结束符(Control-D)

$LC_COLLATE     常在.bashrc或/etc/profile中设置,
                这个变量用来控制文件名扩展和模式匹配的展开顺序。

                如果$LC_COLLATE设置的不正确的话,
                LC_COLLATE会在文件名匹配(filename globbing)中产生不预料的结果。
                在2.05以后的bash版本中,文件名匹配将不再区分中括号结构中
                的字符范围里字符的大小写。
                比如,ls [A-M]* 既能够匹配为File1.txt也能够匹配为file1.txt。
                为了能够恢复中括号里字符的匹配行位(即区分大小写),
                可以设置变量LC_COLLATE为C,
                在文件/etc/profile或~/.bashrc中使用export LC_COLLATE=C,
                可以达到这个目的。

$LC_CTYPE       这个内部变量用来控制统配和模式匹配中的字符串解释。

$LINENO         这个变量用来记录自身在脚本中所在的行号。
                这个变量只有在脚本使用这个变量的时候才有意义,
                并且这个变量一般用于调试目的。

$MACHTYPE       机器类型。标识系统的硬件。

$OLDPWD         之前的工作目录("OLD-print-working-directory",就是之前所在的目录)

$OSTYPE         操作系统类型

$PATH           可执行文件的搜索路径,一般为/usr/bin/,/usr/X11R6/bin,
                /usr/local/bin等。
                当给出一个命令时,shell会自动生成一张哈希(hash)表,
                并且在这张哈希表中按照path变量中所列出的路径来搜索这个可执行命令。
                路径会存储在环境变量中,$PATH变量本身就一个以冒号分割的目录列表。
                通常情况下,系统都是在/etc/profile和~/.bashrc中存储$PATH的定义。

                PATH=${PATH}:/opt/bi n将会把目录/opt/bin附加到当前目录列表中。
                在脚本中,这是一种把目录临时添加到$PATH中的权宜之计。
                当这个脚本退出时,$PATH将会恢复以前的值(一个子进程,
                比如说一个脚本,是不能够修改父进程的环境变量的,
                在这里也就是不能够修改shell本身的环境变量)。

$PIPESTATUS     这个数组变量将保存最后一个运行的前台管道的退出状态码。
                相当有趣的是,这个退出状态码和最后一个命令运行的退出状态码并不一定相同。

                $PIPESTATUS数组的每个成员都保存了运行在管道中的相应命令的退出状态码。
                $PIPESTATUS[0]保存管道中第一个命令的退出状态码,
                $PIPESTATUS[1]保存第二个命令的退出状态码,一次类推。

$PPID           进程的$PPID就是这个进程的父进程的进程ID。

$PROMPT_COMMAND 这个变量保存了在主提示符$PS1显示之前需要执行的命令。

$PS1            这是主提示符,可以在命令行中见到它。

$PS2            第二提示符,当你需要额外输入的时候,你就会看到它,默认显示“>$PS3            第三提示符,它在一个select循环中显示

$PS4            第四提示符,当你使用-x选项来调用脚本时,
                这个提示符会出现在每行输出的开头。
                默认显示“+”。

$PWD            工作目录(你当前所在的目录)。这与內建命令pwd作用相同。

$REPLY          当没有参数变量提供给read命令的时候,这个变量会作为默认变量提供给read命令。
                也可以用于select菜单,但是只提供所选择变量的编号,而不是变量本身的值。

$SECONDS        这个脚本已经运行的时间(以妙为单位)

$SHELLOPTS      shell中已经激活的选项的列表,这是一个只读变量。

$SHLVL          shell级别,就是Bash被嵌套的深度。
                如果是在命令行中,那么$SHLVL为1,如果在脚本中那么$SHLVL为2。

$TMOUT          如果$TMOUT环境变量被设置为非零值time的话,
                那么经过time秒后,shell提示符将会超时。

                这将会导致登出(logout)。
                在2.05b版本的Bash中,$TMOUT变量与命令read可以在脚本中结合使用。

                还有更加复杂的办法可以在脚本中实现定时输入。
                一种办法就是建立一个定时循环,当超时的时候给脚本发个信号。
                不过这也需要有一个信号处理例程能够捕捉由定时循环所产生的中断。

$UID            用于ID号。
                当前用户的用户标识号,记录在/etc/passwd文件中。
                这是当前用户的真实id,即使只是通过使用su命令来临时改变为另一个用户标识,
                这个id也不会被改变。
                $UID是一个只读变量,不能在命令行或者脚本中修改它,并且和id內建命令很相像。

变量$ENV,$LOGNAME,$MAIL,$TERM,$USER,和$USERNAME都不是Bash的內建变量。然而这些变量经常在Bash的启动文件中被当作环境变量来设置。$SHELL是用户登录shell的名字,它可以在/etc/passwd中设置,或者也可以在“init”脚本中设置,并且它也不是Bash內建的。

3.1.2. 位置参数

$0$1$2 ...  位置参数从命令行传递到脚本,或者传递给函数,或者set给变量。

$#              命令行参数或者位置参数的个数

$*              所有的位置参数都被看作为一个单词。“$*”必须被引用起来。

$@$*相同,但是每个参数都是一个独立的引用字符串,
                这就意味着,参数是被完整传递的,

                并没有被解释或扩展。这也意味着,参数列表中每个参数都被看作为单独的单词。
                当然,“$@”应该被引用起来。

shift命令执行以后,$@将会保存命令行中剩余的参数,但是没有之前的$1,因为被丢弃了。

$@也可以作为工具使用,用来过滤传递给脚本的输入。cat "$@"结构既可以接受从stdin传递给脚本的输入,也可以接受从参数中指定的文件中传递给脚本的输入。

$*和$@中的参数有时候会表现出不一致而且令人迷惑的行为,这都依赖于$IFS的设置。

$@与$*中的参数只有在被双引号引用起来的时候才会不同。

3.1.3. 其它的特殊参数

$-              传递给脚本的标记(使用set命令)

$!              运行在后台的最后一个作业的PID(进程ID)

$_              这个变量保存之前执行的命令的最后一个参数的值。

$?              命令,函数或者是脚本本身的退出状态码

$$              脚本本身的进程ID。$$变量在脚本中经常用来构造“唯一的”临时文件名。
                这么做通常比调用mktemp命令来的简单。

3.1.4. 操作字符串

Bash所支持的字符串操作的数量多的令人惊讶。但是不幸的是,这些工具缺乏统一的标准。一些是参数替换的子集,而另外一些则受到UNIX expr命令的影响。这就导致了命令语法的不一致,还会引起冗余的功能,但是这些并没有引起混乱。

字符串长度

${#string}
expr length $string
expr "$string" : '.*'

匹配字符串开头的子串长度

expr match "$string" '$substring'
    $substring是一个正则表达式

expr "$string" : '$substring'
    $substring是一个正则表达式

索引

expr index $string $substring
    在字符串$string中所匹配到的$substring第一次所出现的位置。
    这与C语言中的strchr()函数非常相似。

提取子串

${string:position}$string从位置$position开始提取子串。
    如果$string是“*”或者“@”,那么将会提取从位置$position开始的位置参数。

${string:position:length}$string中从位置$position开始提取$length长度的子串。
    如果$string参数是“*”或“@”,那么将会从$position位置开始提取$length个位置参数,
    但是由于可能没有$length个位置参数了,那么就有几个位置参数就提取几个位置参数。

expr substr $string $position $length$string中从$position开始提取$length长度的子串。

expr match "$string" '\($substring\)'$string的开始位置提取$substring$substring是正则表达式

expr "$string" : '\($substring\)'$string的开始位置提取$substring$substring是正则表达式

expr match "$string" '.*\($substring\)'$string的结尾提取$substring$substring是正则表达式

expr "$string" : '.*\($substring\)'$string的结尾提取$substring$substring是正则表达式

子串削除

${string#substring}$string的开头位置截掉最短匹配的$substring

${string##substring}$string的开头位置截掉最长匹配的$substring

${string%substring}$string的结尾位置截掉最短匹配的$substring

${string%%substring}$string的结尾位置截掉最长匹配的$substring

子串替换

${string/substring/replacement}
    使用$replacement来替换第一个匹配的$substring

${string//substring/replacement}
    使用$replacement来替换所有匹配的$substring

${string/#substring/replacement}
    如果$substring匹配$string的开头部分,那么就用$replacement来替换$substring

${string/%substring/replacement}
    如果$substring匹配$string的结尾部分,那么就用$replacement来替换$substring

3.1.5. 参数替换

处理和(或)扩展变量

${parameter}$parameter相同,也就是变量parameter的值。
    在某些上下文中,${parameter}很少会产生混淆。
    可以把变量和字符串组合起来使用。

${parameter-default}${parameter:-default}
    ${parameter-default} 如果变量parameter没被声明,那么就使用默认值
    ${parameter:-default} 如果变量parameter没被设置,那么就使用默认值
    这两个在绝大多数的情况下都是相同的,只有在parameter已经被声明,
    但是被赋null值的时候,这个额外的:才会产生不同的结果。

${parameter=default}${parameter:=default}
    ${parameter=default} 如果变量parameter没声明,那么就把它的值设为default
    ${parameter:=default} 如果变量parameter没设置,那么就把它的值设为default
    这两种形式基本上是一样的,只有在变量$parameter被声明并且被设置为null值的时候,
    :才会引起这两种形式的不同。

${parameter+alt_value}${parameter:+alt_value}
    ${parameter+alt_value}
    如果变量parameter被声明了,那么就使用alt_value,否则就使用null字符串。

    ${parameter:+alt_value}
    如果变量parameter被设置了,那么就使用alt_value,否则就使用null字符串。

    这两种形式绝大多数情况下都一样,只有在parameter被声明并且设置为null值的时候,
    多出来的这个:才会引起这两种形式的不同。

${parameter?err_msg}${parameter:?err_msg}
    ${parameter?err_msg}
    如果parameter已经被声明,那么就使用设置的值,否则打印err_msg错误消息。

    ${parameter:?err_msg}
    如果parameter已经被设置,那么就使用设置的值,否则打印err_msg错误消息。

    这两种形式绝大多数情况下都一样,只有在parameter被声明并且设置为null值的时候,
    多出来的这个:才会引起这两种形式的不同。

变量长度/子串删除

${#var}
    字符串长度(变量$var的字符个数)。
    对于array来说,${#array}表示的是数组中第一个元素的长度。
    列外:
        ${#*}${#@}表示位置参数的个数。
        对于数组来说,${#array[*]}${#array[@]}表示数组中元素的个数。

${var#Pattern}${var##Pattern}
    从变量$var的开头删除最短或最长匹配$Pattern的子串。
    一个"#"表示匹配最短,"##"表示匹配最长。

${var%Pattern}${var%%Pattern}
    从变量$var的结尾删除最短或最长匹配$Pattern的子串。
    一个"#"表示匹配最短,"##"表示匹配最长。

变量扩展/子串替换

${var:pos}
    变量var从位置pos开始扩展(也就是pos之前的字符都丢弃)

${var:pos:len}
    变量var从位置pos开始,并扩展len个字符。

${var/Pattern/Replacement}
    使用Replacement来替换变量var中第一个匹配Pattern的字符串
    如果省略Replacement,那么第一个匹配Pattern的字符串将被替换为空,也就是被删除了。

${var//Pattern/Replacement}
    全局替换。所有在变量var匹配Pattern的字符串,都会被替换为Replacement。
    如果省略Replacement,那么所有匹配Pattern的字符串,都将被替换为空,也就是被删除掉。

${var/#Pattern/Replacement}
    如果变量var的前缀匹配Pattern,那么就使用Replacement来替换匹配到Pattern的字符串。

${var/%Pattern/Replacement}
    如果变量var的后缀匹配Pattern,那么就使用Replacement来替换匹配到Pattern的字符串。

${!varprefix*}${!varprefix@}
    匹配所有之前声明过的,并且以varprefix开头的变量。

3.1.6. 指定变量的类型

declare或者typeset內建命令(这两个命令是完全一样的)允许指定变量的具体类型。在某些编程语言中,这是指定变量类型的一种很弱的形式。declare命令是从Bash 2.0之后才被引入的命令。

declare/typeset选项

-r 只读
    declare -r var1
    (declare -r var1与readonly var1是完全一样的)
    这和C语言中的const关键字一样,都用来指定变量为只读。
    如果你尝试修改一个只读变量的值,那么会产生错误信息。

-i 整型
    declare -i number
    如果把一个变量指定为整形的话,那么即使没有exprt或者let命令,
    也允许使用特定的算术运算。

-a 数组
    declare -a indices
    变量indices将被视为数组

-f 函数
    declare -f
    如果在脚本中使用decalre -f,而不加任何参数的话,
    那么将会列出这个脚本之前定义的所有函数。
    declare -f function_name
    如果在脚本中使用declare -f function_name这种形式的话,
    将只会列出这个函数的名字。

-x export
    declare -x var3
    这句将会声明一个变量,并作为这个脚本的环境变量被导出。

-x var=$value
    declare -x var3=373
    declare命令允许在声明变量类型的同时给变量赋值。

3.1.7. 变量的间接引用

假设一个变量的值是第二个变量的名字。那么我们如何从第一个变量中取得第二个变量的值呢?比如,如果a=letter_of_alphabet并且letter_of_alphabet=z,那么我们能够通过引用变量a来获得z么?这确实是可以做到的,它被称为间接引用。它使用eval var1=\$$var2这种不平常的形式。

变量的间接引用到底有什么应用价值?它给Bash添加了一种类似于C语言指针的功能,比如,在表格查找中的用法。另外,还有一些其他非常有趣的应用。

这种使用间接引用的方法是一个小技巧。如果第二个变量更改了它的值,那么第一个变量必须被适当的解除引用。幸运的是,在Bash版本2中引入的${!variable}形式使得使用间接引用更加直观了。

3.1.8. $RANDOM:产生随机整数

$RANDOM是Bash的内部函数(并不是常量),这个函数将返回一个伪随机证书,范围在0-32767之间,它不应该被用来产生密钥。

RANDOM=$$

使用脚本的进程ID来作为随机数的种子。

/dev/urandom设备文件提供了一种比单独使用$RANDOM更好的,能够产生更加“随机”的随机数的方法。dd if=/dev/urandom of=targetfile bs=1 count=XX 能够产生一个很分散的伪随机数序列。然而,如果想要将这个数赋值到一个脚本文件的变量中,还需要可操作性,比如使用od命令,或者使用dd命令,或者通过管道传递到md5sum命令中。

date命令也可以用来产生伪随机整数序列。

3.1.9. 双圆括号结构

与let命令很相似,(( … ))结构允许算术扩展和赋值,举个简单的例子,a=$(( 5 + 3 )),将把变量“a"设为“5 + 3”,或者8。然而,双圆括号结构也被认为是在Bash中使用C语言风格变量操作的一种处理机制。

3.2. 循环与分支

对代码块的操作是构造和组织shell脚本的关键。循环和分支结构为脚本编程提供了操作代码块的工具。

3.2.1. 循环

循环就是迭代(重复)一些命令的代码块,如果循环控制条件不满足的话,就结束循环。

3.2.1.1. for循环
for arg in [list]
    这是一个基本的循环结构,它与C语言中的for循环结构有很大的不同。
    for arg in [list]
    do
        command(s)...
    done
    在循环的每次执行中,arg将顺序的访问list中列出的变量。

    list中的参数允许包含通配符。

    如果do和for向在同一行中出现,那么在它们之间需要添加一个分号:
    for arg in [list]; do

    每个[list]中的元素都可能包含多个参数。在处理参数组时,这是非常有用的。
    在这种情况下,使用set命令来强制解析每个[list]中的元素,
    并且将每个解析出来的部分都分配到一个位置参数中。
    set -- $var
    解析变量var并且设置位置参数,“--”将防止$var为空,或者是以一个破折号开头。

    可以将一个变量放在for循环的[list]位置上。
    for file in $FILES
    do
        ...
    done

    如果在for循环的[list]中有通配符(*和?),那么将会发生统配,也就是文件名扩展。
    for file in [jx]*
    do
        ls l "$file"
    done

    在一个for循环中忽略in [list]部分的话,
    将会使循环操作$@(从命令行传递给脚本的位置参数)。
    for a
    do
        echo -n "$a "
    done

    也可以使用命令替换来产生for循环的[list]NUMBERS="9 7 3 8 37.53"
    for number in `echo $NUMBERS`
    do
        echo -n "$number "
    done

    for循环的输出也可以通过管道传递到一个或多个命令中。
    for file in "$(find $directory -type l)"
    do
        echo "$file"
    done | sort
    同样,循环的stdout可以重定向到文件中。

    有一个非常像C语言for循环的语法形式,需要使用(())。
    for ((a = 1; a <= 10; a++))
    do
        echo -n "$a "
    done
3.2.1.2. while循环
while
    这种结构在循环的开头判断条件是否满足,如果条件一直满足,
    那么就一直循环下去(返回0作为退出状态码)。
    与for循环的区别是,while循环更适合在循环次数位置的情况下使用。
    while [condition]
    do
        command...
    done

    与for循环一样,如果想把do和条件判断放到同一行上的话,还是需要一个分号。
    while [dondition]; do

    需要注意以下某种特定的while循环,比如getopts结构,好像和这里所介绍的模板有点脱节。

    一个while循环可以有多个判断条件。但是只有最后一个才能够决定是否能够退出循环。
    然而这里需要一种有点特殊的循环语法。

    与for循环一样,while循环也可以通过(())来使用C风格的语法。
    while (( ... ))
    do
        ...
    done

    while循环的stdin可以使用<来重定向到一个文件。
    while循环的stdin支持管道。
3.2.1.3. until循环
until
    这个结构在循环的顶部判断条件,并且如果条件一直为false,那么就一直循环下去。
    (与while循环相反)
    until [condition-is-true]
    do
        command...
    done
    注意,until循环的条件判断在循环的顶部,这与某些编程语言是不同的。

    与for循环一样,如果想把do和条件判断放在同一行里,那么就需要使用分号
    until [condition-is-true]; do

3.2.2. 嵌套循环

嵌套循环就是在一个循环中还有一个循环,内部循环在外部循环体中。在外部循环的每次执行过程中都会触发内部循环,直到内部循环执行结束。外部循环执行了多少次,内部循环就完成多少次。当然,无论是内部循环还是外部循环的break语句都会打断处理过程。

3.2.3. 循环控制

影响循环行为的命令:break,continue

break和continue这两个循环控制命令与其它语言的类似命令的行为是相同的。break命令用来跳出循环,而continue命令只会跳过本次循环,忽略本次循环剩余的代码,进入循环的下一次迭代。

break命令可以带一个参数,一个不带参数的break命令只能退出最内层的循环,而break N可以退出N层循环。

continue命令也可以像break命令带一个参数,一个不带仓鼠的continue命令只会去掉本次循环的剩余代码。而continue N将会把N层循环的剩余代码都去掉,但是循环的次数不变。

注意:continue N结构如果用在有意义的场合中,往往都很难理解,并且技巧性很高。所以最好的方法就是尽量避免使用它。

这两个命令是shell的內建命令,而不像其它的循环命令那样,比如while和case,这两个是关键字。

3.2.4. 测试与分支

case和select结构在技术上说并不是循环,因为它们并不对可执行代码块进行迭代。但是和循环相似的是,它们也依靠在代码块顶部或底部的条件判断来决定程序的分支。

在代码块中控制程序分支

case (in) / esac
    在shell中的case结构与C/C++中的switch结构是相同的。
    它允许通过判断来选择代码块中多条路径中的一条。
    它的作用和多个if/then/else语句的作用相同,是它们的简化结构,特别适用于创建菜单。

    case "$variable" in
      "$condition1")
      command...
      ;;
      "$condition2")
      command...
      ;;
    esac

    对变量使用""并不是强制的,因为不会发生单词分割。
    每句测试行,都以右小括号)来结尾
    每个条件判断语句块都以一对分号结尾;;
    case块以esac(case的反向拼写)结尾

    case结构也可以过滤通配模式的字符串。


select
    select结构是建立菜单的另一种工具。

    select variable [in list]
    do
        command...
        break
    done

    如果忽略了in list列表,那么select命令将会使用传递到脚本的命令行参数($@),
    或者是函数参数(当select是在函数中时)

3.3. 内部命令与內建命令

内部命令指的就是包含在Bash工具包中的命令,从字面意思上看就是built in。这主要是考虑到执行效率的问题,內建命令将比外部命令执行的更快,一部分原因是因为外部命令通常都需要fork出一个单独的进程来执行,另一部分原因是特定的內建命令需要直接访问shell的内核部分。

当一个命令或者是shell本身需要初始化(或者创建)一个新的子进程来执行一个任务的时候,这种行为被称为fork。这个新产生的进程被叫做子进程,并且这个进程是从父进程中fork出来的。当子进程执行它的任务时,父进程也在运行。

注意:当父进程获得了子进程的进程ID时,父进程可以给子进程传递参数,然而反过来却不行。

一个內建命令通常会与一个系统命令同名,但是Bash在内部重新实现了这些命令。比如,Bash的echo命令与/bin/echo就不尽相同,虽然它们的行为在绝大多数情况下都是一样的。

关键字的意思就是保留字,对于shell来说关键字具有特殊的含义,并且用来构建shell语法结构。比如,“for”,“while”,“do”,和"!"都是关键字。与內建命令相似的是,关键字也是Bash的骨干部分,但是与內建命令不同的是,关键字本身并不是一个命令,而是一个比较大的命令结构的一部分。

3.3.1. I/O

echo

打印(到stdout)一个表达式或者变量。

echo Hello
echo $a

echo命令需要-e参数来打印转义字符。
通常情况下,每个echo命令都会在终端上新起一行,但是-n参数会阻止新起一行。

echo命令可以作为输入,通过管道传递到一系列命令中去。

if echo "$VAR" | grep -q txt
then
    echo ...
fi

echo命令可以与命令替换组合起来,这样可以用来设置一个变量。

a=`echo "HELLO" | tr A-Z a-z`

小心echo `command`将会删除任何由command所产生的换行符。

$IFS(内部域分隔符)一般都会将\n(换行符)包含在它的空白字符集合中。Bash因此会根据参数中的换行来分离command的输出,然后echo。最后echo将以空格代替换行来输出这些参数。

printf

printf命令,格式化输出,是echo命令的增强版。它是C语言printf()库函数的一个有限的变形,并且在语法上有些不同。

printf format-string... parameter...

这是Bash的內建版本,与/bin/printf或者/usr/bin/printf命令不同。如果想更深入的了解,请查看printf(系统命令)的man页。

read

从stdin中“读取”一个变量的值,也就是,和键盘进行交互,来取得变量的值。使用-a参数可以read数组变量。

一个不带变量参数的read命令,将会把来自键盘的输入存入到专用变量$REPLY中。

一般的,当输入给read时,输入一个\,然后回车,将会阻止产生一个新行。-r选项将会让\转义。

read命令有些有趣的选项,这些选项允许打印出一个提示符,然后在不输入ENTER的情况下,可以读入你所按下的字符的内容。

read -s -n1 -p "Hit a key" keypress
echo; echo "Keypress was "\"$keypress\""."
-s      选项意味着不打印输入
-n N    选项意味着只接受N个字符的输入
-p      选项意味着在读取输入之前打印出后边的提示符
        使用这些选项是有技巧的,因为你需要用正确的顺序来使用它们。

read命令的-n选项也可以检测方向键和一些控制按键。

arrowup='\[A'
arrowdown='\[B'
arrowrt='\[C'
arrowleft='\[D'
insert='\[2'
delete='\[3'

read -n3 key
echo -n "$key" | grep "$arrowup"
if [ "$?" -eq 0 ];
then
    echo "Up-arrow key pressed"
    exit
fi

对于read命令来说,-n选项不会检测ENTER(新行)键。

read命令的-t选项允许时间输入。

read命令也可以从重定向的文件中“读取”变量的值。如果文件中的内容超过一行,那么只有第一行被分配到这个变量中。如果read命令的参数个数超过一个,那么每个变量都会从文件中取得一个分配的字符串作为变量的值,这些字符串都是以定义的空白符来进行分隔的。

while read line
do
    echo "$line"
done < data-file

OIFS=$IFS; IFS=:
while read name passwd uid gid fullname ignore
do
    echo "$name ($fullname)"
done < /etc/passwd
IFS=$OIFS

# 在循环内部设置$IFS变量,而不用把原始的$IFS保存到临时变量中
while IFS=: read name passwd uid gid fullname ignore
do
    echo "$name ($fullname)"
done < /etc/passwd

管道输出到read命令中,使用管道echo输出来设置变量将会失败。然而,使用管道cat输出看起来能够正常运行。

cat file1 file2 |
while read line
do
    echo $line
done

3.3.2. 文件系统

cd

cd,修改目录命令,在脚本中用的最多的时候就是当命令需要在指定目录下运行时,需要用它来修改当前工作目录。

-P(physical)选项对于cd命令的意义是忽略符号链接。

cd - 将会把工作目录修改至$OLDPWD,也就是之前的工作目录。

pwd

打印出当前的工作目录。这将给出用户(或脚本)的当前工作目录。使用这个命令的结果和从內建变量$PWD中所读取的值是相同的。

pushd,popd,dirs

这几个命令可以使得工作目录书签化,就是可以按顺序向前或向后移动工作目录。压栈的动作可以保存工作目录列表。选项可以允许对目录栈做不同的操作。

pushd dir-name把路径dir-name压入目录栈,同时修改当前目录到dir-name。

popd将目录栈最上边的目录弹出,同时将当前目录修改为刚弹出来的那个目录。

dirs列出所有目录栈的内容(与$DIRSTACK变量相比较)。一个成功的pushd或者popd将会自动调用dirs命令。

对于那些并没有对当前目录做硬编码,并且需要对当前工作目录做灵活修改的脚本来说,使用这些命令是再好不过了,注意內建$DIRSTACK数组变量,这个变量可以在脚本中进行访问,并且他们保存了目录栈的内容。

3.3.3. 变量

let

let命令将执行变量的算术操作。在许多情况下,它被看作是复杂的expr命令的一个简化版本。

let a=11                # 与 'a=11' 相同
let a=a+5               # 等价于 let "a = a + 5"
                        # (双引号和空格是这句话更具可读性.)
echo "11 + 5 = $a"      # 16

let "a <<= 3"           # 等价于 let "a = a << 3"
echo "\"\$a\" (=16) left-shifted 3 places = $a"
                        # 128
let "a /= 4"            # 等价于 let "a = a / 4"
echo "128 / 4 = $a"     # 32
let "a -= 5"            # 等价于 let "a = a - 5"
echo "32 - 5 = $a"      # 27
let "a *= 10"           # 等价于 let "a = a * 10"
echo "27 * 10 = $a"     # 270
let "a %= 8"            # 等价于 let "a = a % 8"
echo "270 modulo 8 = $a (270 / 8 = 33, remainder $a)"
                        # 6

eval

eval arg1 [arg2] ... [argN]

将表达式中的参数,或者表达式列表,组合起来,然后评价它们(通常用来执行)。任何被包含在表达式中的变量都将被扩展。结果将会被转化到命令中。如果你想从命令行中或者是从脚本中产生代码,那么这个命令就非常有用了。

eval命令是有风险的,如果你有更合适的方法来实现功能的话,尽量避免使用它。eval $COMMANDS将会执行命令COMMANDS的内容,如果命令中包含有rm -rf * 这样的东西,可能就不是你想要的了。当你运行一个包含有eval命令的陌生人所编写的代码片段的时候,这是一件很危险的事情。

set

set命令用来修改内部脚本变量的值。它的一个作用就是触发选项标志位来帮助决定脚本的行为。另一个作用是以一个命令的结果(set `command`)来重新设置脚本的位置参数。脚本将会从命令的输出中重新分析出位置参数。

不使用任何选项或参数来调用set命令的话,将会列出所有的环境变量和其它所有的已经初始化过的变量。

如果使用参数–来调用set命令的话,将会明确的分配位置参数。如果–选项后边没有跟变量名的话,那么结果就使得所有位置参数都被unsets了。

unset

unset命令用来删除一个shell变量,这个命令的效果就是把这个变量设置为null。注意:这个命令对位置参数无效。

export

export命令将会使得被export的变量在所运行脚本(或shell)的所有子进程中都可用。不幸的是,没有办法将变量export到父进程中,这里所致的父进程就是调用这个脚本的脚本或shell。关于export命令的一个重要的用法就是使用在启动文件中,启动文件用来初始化和设置环境变量,这样,用户进程才能够访问环境变量。

可以在一个操作中同时进行赋值和export变量,比如:export var1=xxx

declare,typeset

declare和typeset命令被用来指定或限制变量的属性。

readonly

与declare -r作用相同,设置变量的只读属性,或者可以认为这个变量就是一个常量。设置了这种属性之后,如果你还要修改它,那么就会得到一个错误信息。这种情况与C语言中的const常量类型是相同的。

getopts

可以说这个命令是分析传递到脚本中命令行参数的最强力工具。这个命令与外部命令getopt,还有C语言中的库函数geopt的作用是相同的。它允许传递和链接多个选项到脚本中,并且能够分配多个参数到脚本中(比如:scriptname -abc -e /usr/local)。

getopts结构使用两个隐含变量。$OPTIND是参数指针(选项索引)和$OPTARG(选项参数)(可选的)可以在选项后边附加一个参数。在声明标签中,选项名后边的冒号用来提示这个选项名已经分配了一个参数。

getopts结构通常都组成一组放在一个while循环中,循环过程中每次处理一个选项和参数,然后增加隐含变量$OPTIND的值,再进行下一次的处理。

  • 通过命令行传递到脚本中的参数前边必须加上一个减号(-)。-是一个前缀,这样getopts命令把这个参数看作为一个选项。事实上,getopts不会处理不带-前缀的参数,如果第一个参数就没有-,那么将会结束选项的处理。
  • getopts的while循环模板与标准的while循环模板有些不同,没有标准循环中的中括号[]判断条件。
  • getopts结构将会取代外部命令getopt
while getopts ":abcde:fg" Option
do
    case $Option in
        a) ... ;;
        b) ... ;;
        ...
        e) ... ;; # $OPTARG变量里边将保存传递给选项“e“的参数
        ..
        g) ... ;;
    esac
done
shift $(($OPTIND - 1))

3.3.4. 脚本行为

source,.

当在命令行中调用的时候,这个命令将会执行一个脚本。当在脚本中调用的时候,source file-name 将会加载file-name文件。source一个文件(或点命令)将会在脚本中引入代码,并将这些代码附加到脚本中(与C语言中的#include指令效果相同)。最终的结果就像是在使用“source”的行上插入了相应文件的内容。在多个脚本需要引用相同的数据,或者需要使用函数库的情况下,这个命令非常有用。

如果source进来的文件本身就是一个可执行脚本的话,那么它将运行起来,然后将控制权交还给调用它的脚本。
一个source进来的可执行脚本可以使用return命令来达到这个目的。

(可选的)也可以向source文件中传递参数,这些参数将被看作位置参数。

source $filename $arg1 $arg2

你甚至可以在脚本文件中source它自身,虽然这么做看不出有什么实际的应用价值。

exit

无条件的停止一个脚本的运行。exit命令可以随意的取得一个整数参数,然后把这个参数作为这个脚本的退出状态码。
在退出一个简单脚本的时候,使用exit 0的话,是种好习惯,因为这表明成功运行。

如果不带参数调用exit命令退出的话,那么退出状态码将会是脚本中最后一个命令的退出状态码,等价于exit $?。

exec

这个shell內建命令将使用一个特定的命令来取代当前进程。一般的当shell遇到一个命令,它会forks off一个子进程来真正的运行命令。使用exec內建命令,shell就不会fork了,并且命令的执行将会替换掉当前shell。因此,在脚本中使用时,一旦exec所执行的命令执行完毕,那么它就会强制退出脚本。

exec命令还能够用来重新分配文件描述符。比如,exec < zzz-file将会用zzz-file来代替stdin。

find命令的-exec选项与shell內建的exec命令是不同的。

shopt

这个命令允许shell在空闲时修改shell选项。它经常出现在启动文件中,但在一般脚本中也常出现。

caller

将caller命令放到函数中,将会在stdout上打印出函数的调用者信息。

function1() {
    caller 0
}
function1

caller命令也可以在一个被source的脚本中返回调用者信息。当然这个调用者就是source这个脚本的脚本。就像函数一样,这是一个“子例程调用”。

3.3.5. 命令

true

这是一个返回(零)成功退出状态码的命令,但是除此之外不做任何事。

false

这是一个返回失败退出状态码的命令,但是除此之外不做任何事。

type [cmd]

与外部命令which很相像,type cmd将会给出“cmd”的完整路径。与which命令不同的是,type命令是Bash内建命令。
-a是type命令的一个非常有用的选项,它用来鉴别参数是关键字还是內建命令,也可以用来定位同名的系统命令。

hash [cmds]

在shell的hash表中,记录指定命令的路径名,所以在shell或脚本中调用这个命令的话,就不需要再在$PATH中重新搜索这个命令了。如果不带参数的调用hash命令,它将列出所有已经被hash的命令。-r选项会重新设置hash表。

bind

bind內建命令用来显示或修改readline的键绑定。

help

获得shell內建命令的一个小的使用总结。与whatis命令比较像,但help命令是內建命令。

3.3.6. 作业控制命令

jobs

在后台列出所有正在运行的作业,给出作业号。并不像ps命令那么有用。

作业和进程的概念太容易混淆了。特定的內建命令,比如kill,disown和wait命令既可以接受作业号为参数,也可以接受进程号为参数。但是fg,bg和jobs命令就只能接受作业号为参数。

disown

从shell的激活作业表中删除作业。

fg,bg

fg命令可以把一个在后台运行的作业放到前台来运行。而bg命令将会重新启动一个挂起的作业,并且在后台运行它。
如果使用fg或者bg命令的时候没有指定作业号,那么默认将对当前正在运行的作业进行操作。

wait

停止脚本的运行,知道后台运行的所有作业都结束位置,或者如果传递了作业号或进程号为参数的话,那么就直到指定作业结束位置,返回等待命令的退出状态码。

你可以使用wait命令来防止在后台作业没有完成(这会产生一个孤儿进程)之前退出脚本。

可选的,wait也可以接受一个作业标识符作为参数,比如,wait %1或者wait $PPID。

在一个脚本中,使用后台运行命令(&)可能会使这个脚本挂起,知道敲ENTER,挂起的脚本才会被恢复。
看起来只有在这个命令的结果需要输出到stdout的时候,这种现象才会出现。这是个很烦人的现象。

#!/bin/bash
# test.sh
ls -l &
echo "Done."
-----
$ ./test.sh

看起来只要在后台运行命令的后边加上一个wait命令就会解决这个问题。

#!/bin/bash
# test.sh
ls -l &
echo "Done."
wait
-----
$ ./test.sh

如果将后台运行命令的输出重定向到文件中或/dev/null中,也能解决这个问题。

suspend

这个命令的效果与Control-Z很相像,但是它挂起的是这个shell(这个shell的父进程应该在合适的时候重新恢复它)。

logout

退出一个已经登陆上的shell,也可以指定一个退出状态码

times

给出执行命令所占用的时间

kill

通过发送一个适当的结束信号,来强制结束一个进程。

kill -l将会列出所有信号。kill -9是“必杀”命令,这个命令将会结束顽固的不想被kill掉的进程。
有时候kill -15也能干这个活。一个“僵尸进程”,僵尸进程就是子进程已经结束了,但是父进程还没kill掉这个子进程,不能被登陆的用户kill掉。因为你不能杀掉一些已经死了的东西,但是init进程迟早会把它清除干净。

killall

killall命令将会通过名字来杀掉一个正在运行的进程,而不是通过进程ID。如果某个特定的命令有多个实例正在运行,那么执行一次killall命令就会把这些实例全部杀掉。

这里所指的killall命令是在/usr/bin中,而不是/etc/rc.d/init.d中的killall脚本。

command

对于命令“COMMAND”,command COMMAND会直接禁用别名和函数的查找。

注意以下Bash执行命令的优先级:

1   别名
2   关键字
3   函数
4   內建命令
5   脚本或可执行程序($PATH

这是shell用来影响脚本命令处理效果的三个命令之一。另外两个分别是builtin和enable。

builtin

当你使用builtin BUILTIN_COMMAND的时候,只会调用shell內建命令“BUILTIN_COMMAND”,而暂时禁用同名的函数,或者是同名的扩展命令。

enable

这个命令或者禁用内建命令或者回复內建命令。比如,enable -n kill将禁用內建命令kill,所以当我们调用kill命令时,使用的将是/bin/kill外部命令。

-a选项会enable所有作为参数的shell內建命令,不管它们之前是否被enable了。如果不带参数的调用enable -a,那么会恢复所有內建命令。-f filename选项将会从适当的编译过的目标文件中,让enable命令以共享库的形式来加载內建命令。

autoload

这是从ksh中的autoloader命令移植过来的。一个带有“autoload”声明的函数,在它第一次被调用的时候才会加载。这样做是为了节省系统资源。

注意,autoload命令并不是Bash核心安装时候的一部分。这个命令需要使用命令enable -f来加载。

3.3.6.1. 作业标识符
记法            含义
----------------------
%N          作业号[N]
%S          以字符串S开头的被(命令行)调用的作业
%?S         包含字符串S的被(命令行)调用的作业
%%          当前作业(前台最后结束的作业,或后台最后启动的作业)
%+          当前作业(前台最后结束的作业,或后台最后启动的作业)
%-          最后的作业
$!          最后的后台进程

3.4. 外部命令

可以访问 shell 知:外部命令 了解更多。

3.5. 命令替换

命令替换能够重新分配一个,甚至是多个命令的输出;它会将命令的输出如实地添加到另一个上下文中。

命令替换的典型用法形式是使用后置引用(`…`)。使用后置引用的(反引号)命令会产生命令行文本。

script_name=`basename $0`
echo "The name of this script is $scrit_name."

这样一来,命令的输出就能够保存到变量中,或者传递到另一个命令中作为这个命令的参数,甚至可以用来产生for循环的参数列表。

命令替换将会调用一个subshell。

命令替换可能会引起单词分割(word split)。

COMMAND `echo a b`      # 两个参数:a and b
COMMAND "`echo a b`"    # 1个参数:"a b"
COMMAND `echo`          # 无参数
COMMAND "`echo`"        # 一个空参数

即使没有引起单词分割,命令替换也会去掉多余的新行。

如果用echo命令输出一个未引用变量,而且这个变量以命令替换的结果作为值,那么这个变量中的换行符将会被删除。这可能会引起一些异常状况。

dir_listing=`ls -l`
echo $dir_listing       # 未引用,就是没用引号括起来

命令替换甚至允许将整个文件的内容放到变量中,可以使用重定向或者cat命令。

variable1=`<file1`      # 将"file1"的内容放到"variable1"中
variable2=`cat file2`   # 将"file2"的内容放到"variable2"中,但是这行会fork一个新进程

注意:不要将一个长文本文件的全部内容设置到变量中,除非你有一个非常好的原因非这么做不可,也不要将二进制文件的内容保存到变量中,即使是开玩笑也不行。

变量替换允许将一个loop的输出设置到一个变量中。这么做的关键就是将循环中echo命令的输出全部截取。

variable1=`for i in 1 2 3 4 5
do
    echo -n "$i"
done`

i=0
variable2=`while [ "$i" -lt 10 ]
do
    echo -n "$i"
    let "i += 1"
done`

对于命令替换来说,$(COMMAND)形式已经取代了后置引用"`"。

output=$(sed -n /"$1"/p $file)
File_contents1=$(cat $file)
File_contents2=$(<$file2)

$(…)形式的命令替换是允许嵌套的。

word_count=$(wc -w $(ls -l | awk '{print $9}'))

3.6. 算术扩展

算术扩展提供了一种强力工具,可以在脚本中执行(整型)算法操作。可以使用backticks,double parentheses,或者let来将字符串转换为数字表达式。

一些变化

使用后置引用的算术扩展(通常都是和expr一起使用)

z=`expr $z + 3`

使用双括号形式的算术扩展,也可以使用let命令。

后置引用形式的算术扩展已经被双括号形式所替代了,((…))和$((…)),当然也可以使用非常方便的let结构。

z=$(($z+3))
z=$((z+3))

n=0
(( n += 1 ))

let z=z+3
let "z += 3"

3.7. I/O重定向

默认情况下始终有3个“文件”处于打开转台,stdin(键盘),stdout(屏幕),和stderr(错误消息输出到屏幕上)。这3个文件和其它打开的文件都可以被重定向。对于重定向简单的解释就是捕捉一个文件,命令,程序,脚本,或者是脚本中的代码块的输出,然后将这些输出作为输入发送到另一个文件,命令,程序,或脚本中。

每个打开的文件都会被分配一个文件描述符。stdin,stdout和stderr的文件描述符分别是0,1和2。除了这3个文件,对于其它那些需要打开的文件,保留了文件描述符3到9。在某些情况下,将这些额外的文件描述符分配给stdin,stdout,或stderr作为临时的副本链接是非常有用的。在经过复杂的重定向和刷新之后需要把它们恢复成正常状态。

COMMAND_OUTPUT >
    将stdout重定向到一个文件。
    如果这个文件不存在,那就创建,否则就覆盖。
    ls -lR > dir-tree.list

: > filename
    > 操作,将会把文件“filename”变为一个空文件(就是size为0)。
    如果文件不存在,那么就创建一个0长度的文件(与'touch'的效果相同)。
    : 是一个占位符,不产生任何输出。

> filename
    > 操作,将会把文件“filename”变为一个空文件(就是size为0)。
    如果文件不存在,那么就创建一个0长度的文件(与'touch'的效果相同)。
    (与上边的": >"效果相同,但是某而写shell可能不支持这种形式)

COMMAND_OUTPUT >>
    将stdout重定向到一个文件
    如果文件不存在,那么就创建它,如果存在,那么就追加到文件后边。

    单行重定向命令(只会影响它们所在的行)

1>filename
    重定向stdout到文件“filename”
1>>filename
    重定向并追加stdout到文件“filename”
2>filename
    重定向stderr到文件“filename”
2>>filename
    重定向并追加stderr到文件“filename”
&>filename
    将stdout和stderr都重定向到文件“filename”

M>N
    "M"是一个文件描述符,如果没有明确指定的话默认为1.
    "N"是一个文件名。
    文件描述符"M"被重定向到文件“filename”
M>&N
    "M"是一个文件描述符,如果没有明确指定的话默认为1.
    "N"是另一个文件描述符。

2>&1
    重定向stderr到stdout。
    将错误消息的输出,发送到与标准输出所指向的地方
i>&j
    重定向文件描述符i到j
    指向i文件的所有输出都发送到j。
>&j
    默认的,重定向文件描述1(stdout)到j
    所有传递到stdout的输出都送到j中去

0< FILENAME
 < FILENAME
    从文件中接受输入。
    与">"是成对迷你了哪个,并且通常都是结合使用
    grep search-word < fliename

[j]<>filename
    为了读写“filename”,把文件“filename”打开,并且将文件描述符"j"分配给它。
    如果文件“filename”不存在,那么就创建它。
    如果文件描述符“j”没有指定,那默认是fd 0,stdin。

    这种应用通常是为了写到一个文件中指定的地方。
    echo 1234567890 > File      # 写字符串到”File"中
    exec 3<> File               # 打开"File"并且将fd 3分配给它
    read -n 4 <&3               # 只读取4个字符。
    echo -n . >&3               # 写一个小数点
    exec 3>&-                   # 关闭fd 3
    cat File                    # ==> 1234.67890

|
    管道
    通用目的处理和命令链工具。
    与">",很相似,但是实际上更通用。
    对于像将命令,脚本,文件和程序串起来的时候很有用。
    cat *.txt | sort | uniq > result-file
    对所有.txt文件的输出进行排序,并且删除重复行

可以将输入输出重定向和(或)管道的多个实例结合到一起写在同一行上。

command < input-file > output-file
command1 | command2 | command3 > output-file

关闭文件描述符

n<&-
    关闭输入文件描述符n。
0<&-,<&-
    关闭stdin
n>&-
    关闭输出文件描述符n。
1.&-,>&-
    关闭stdout

子进程继承了打开的文件描述符。这就是为什么管道可以工作。如果想阻止fd被继承,那么可以关掉它。

3.7.1. 使用exec

exec < filename命令会将stdin重定向到文件。从这句开始,所有的stdin就都来自于这个文件了,而不是标准输入(通常都是键盘输入)。这样就提供了一种按行读取文件的方法,并且可以使用sed和/或awk来对每一行进行分析。

同样的,exec > filename命令将会把stdout重定向到一个指定的文件中。这样所有命令的输出就都会发送到那个指定的文件,而不是stdout。

exec N > filename会影响这个脚本或当前shell。对于这个指定PID的脚本或shell来说,从这句命令执行之后,就会重定向到这个文件中,然而,
N > filename只会影响新fork出来的进程,而不会影响整个脚本或shel。

3.7.2. 代码块重定向

像while,until,和for循环代码块,甚至if/then测试结构的代码块,都可以对stdin进行重定向。即使函数也可以使用这种重定向方式。要想做到这些,都要依靠代码块结尾的<操作符。

3.8. Here Document

一个here document就是一段带有特殊目的的代码段。
它使用I/O重定向的形式将一个命令序列传递到一个交互程序或者命令中,比如ftp,cat或者ex文本编辑器。

COMMAND <<IputComesFromHERE
...
InputComesFromHERE

limit string用来界定命令序列的范围。特殊符号<<用来标识limit string。这个符号的作用就是将文件的输出重定向到程序或命令的stdin中。

-选项用来标记here document的limit string(<<-LimitString),可以抑制输出时前边的tab(不是空格),这么做可以增加一个脚本的可读性。

cat <<-ENDOFMESSAGE
    This is line 1 of the message.
    This is line 2 of the message.
    This is the last line of the message.
ENDOFMESSAGE

here document支持参数和命令替换。所以也可以给here document的消息体传递不同的参数,这样相应的也会修改输出。

在here document的开头,引用或转义“limit string”,会使得here document消息体中的参数替换被禁用。

禁用了参数替换后,将允许输出文本本身。如果你想产生脚本甚至是程序代码的话,那么可以使用这种办法。

也可以将here document的输出保存到变量中。

variable=$(cat <<SETVAR
This variable
runs over multiple lines.
SETVAR)

也可以这么使用:(冒号),做一个家命令来从一个here document中接收输出。这么做事实上就是创建了一个“匿名”的here document。这种方式,可以用来“注释”掉代码块。

结尾的limit string,就是here document最后一行的limit string,必须从第一个字符开始。它的前面不能够有任何前置的空白。而在这个limit string后边的空白也会引起异常。空白将会阻止limit string的识别。

3.8.1. Here String

here string可以看成是here document的一种定制形式。除了COMMAND <<< $WORD,就什么都没有了,$WORD将被扩展并且被送入COMMAND的stdin中。

String="This is a string of words"
read -r -a Words <<< "$String"

read命令的-a选项将会把结果值按顺序的分配给数组中的每一项。

3.9. 正则表达式

为了充分发挥shell编程的威力,你必须精通正则表达式。脚本中经常使用的某些命令,和工具包通常都支持正则表达式,比如grep,expr,sed和awk解释器。

正则表达式就是有一系列特殊字符租场的字符串,其中每个特殊字符都被称为元字符,这些元字符并不表示为它们字面上的含义,而会被解释为一些特定的含义。正则表达式其实是由普通字符和元字符共同组成的集合,这个集合用来匹配(或指定)模式。

一个正则表达式会包含下列一项或多项:

  • 一个字符集。
    这里所指的字符集只包含普通字符,这些字符只表示它们的字面含义。正则表达式的最简单形式就是只包含字符集,而不包含元字符。
  • 锚。
    锚指定了正则表达式所要匹配的文本在文本行中所处的位置。比如,^和$就是锚。
  • 修饰符。
    它们扩大或缩小(修改)了正则表达式匹配文本的范围。修饰符包含星号,括号,和反斜杠。

正则表达式最主要的目的就是用于(RE)文本搜索与字符串操作。RE能够匹配单个字符或者一个字符集。即一个字符串或者一个字符串的一部分。

1)星号 *

用来匹配它前面字符的任意多次,包括0次。

"1133*"匹配11 + 一个或多个3 + 也允许后边还有其它字符:113,1133,111312等。

2)点 .

用于匹配任意一个字符,除了换行符。

"13.“匹配13 + 至少一个任意字符(包括空格):1133,11333,但不能匹配13(因为缺少”."所能匹配的至少一个任意字符)。

3)脱字符号 ^

匹配行首,但是某些时候需要依赖上下文环境,在RE中,有时候也表示对一个字符集取反。

4)美元符 $

在RE中用来匹配行尾。

"XXXKaTeX parse error: Expected group after '^' at position 12: "匹配行尾的XXX。"^̲"匹配空行。

5)中括号 […]

在RE中将匹配中括号字符集中的某一个字符。

"[zyz]"将会匹配字符x,y,或z。

"[c-n]"匹配字符c到字符n之间的任意一个字符。

"[B-Pk-y]"匹配从B到P,或者从k到y之间的任意一个字符。

"[a-z0-9]"匹配任意小写字母或数字。

"[^b-d]"将会匹配范围在b到d之外的任意一个字符。这就是使用^对字符集取反的一个实例。

将多个中括号字符集组合使用,能够匹配一般的单词或数字。"[Yy][Ee][Ss]"能够匹配yes,Yes,YES,yEs等。

6)反斜杠 \

用来转义某个特殊含义的字符,这意味着,这个特殊字符将会被解释为字面含义。“\ " 将会被解释成字符 " "将会被解释成字符" "将会被解释成字符"”,而不是RE中匹配行尾的特殊字符。相似的,“\\“将会被解释为字符”\”。

7)转义的“尖括号” \<…\>

尖括号必须被转义才含有特殊的含义,否则它就表示尖括号的字面含义。

“\“完整匹配单词"the”,不会匹配"them”,“there”,"other"等。

8)问号 ?

匹配它前面的字符,但是只能匹配1次或0次。通常用来匹配单个字符。

9)加号 +

匹配它前面的字符,能够匹配一次多次。与前面讲的*号租用类似,但是不能匹配0个字符的情况。

10)转义“大括号” \{\}

在转义后的大括号中加上一个数字,这个数字就是它前面的RE所能匹配的次数。

大括号必须经过转义,否则,大括号仅仅表示字面含义。这种用法并不是基于RE集合汇总的一部分,仅仅是个技巧而已。

"[0-9]\{5\}"精确匹配5个数字(所匹配的字符范围是0到9)。

使用大括号形式的RE是不能够在“经典”(非POSIX兼容)的awk版本中正常运行的。然而,gawk命令中有一个–re-interval选项,使用这个选项就允许使用大括号形式的RE了(无需转义)。

echo 2222 | gawk --re-interval '/2{3}/'

11)圆括号 ()

括起来一组正则表达式。当你想使用expr进行字符串提取的时候,圆括号就有用了。如果和下面要讲的“|”操作符结合使用,也非常有用。

12)竖线 |

就是RE中的“或”操作符,使用它能够匹配一组可选字符中的任意一个。

POSIX字符类 [:class:] 这是另外一种,用于指定匹配字符范围的方法。

[:alnum:]   匹配字母和数字,等价于A-Za-z0-9
[:alpha:]   匹配字母,等价于A-Za-z
[:blank:]   匹配一个空格或是一个制表符
[:cntrl:]   匹配控制字符
[:digit:]   匹配(十进制)数字,等价于0-9
[:graph:]   可打印的图形字符。匹配ASCII码值范围在33-126之间的字符。
            与下面所提到的[:print:]类似,但是不包括空格字符(空格字符的ASCII码是32)。
[:lower:]   匹配小写字母,等价于a-z
[:print:]   可打印的图形字符。匹配ASCII码值范围在33-126之间的字符。
            与上面的[:print:]类似,但是包含空格。
[:space:]   匹配空白字符(空格和水平制表符)
[:upper:]   匹配大写字母,等价于A-Z
[:xdigit:]  匹配16进制数字,等价于0-9A-Fa-f

POSIX字符类通常都要用引号或双中括号([[ ]])引起来。

Bash本身并不会识别正则表达式。在脚本中,使用RE的是命令和工具,比如sed和awk。

Bash仅仅做的一件事是文件名扩展,这就是所谓的通配,但是这里所使用的并不是标准的RE,而是使用通配符。通配解释标准通配符,*,?,中括号括起来的字符,还有其它一些特殊字符(比如^用来表示取反匹配)。然而通配所使用的通配符有很大的局限性。包含*的字符串不能匹配以“点”开头的文件,比如.bashrc。另外,RE中使用的?,与通配中所使用的?,含义并不相同。

Bash只能对未用引号引用起来的命令行参数进行文件名扩展。

Bash在通配中解释特殊字符的行为是可以修改的。set -f命令可以禁用通配,而且shopt命令的选项nocaseglob和nullglob可以修改通配的行为。

3.10. 子shell

运行一个shell脚本的时候,会启动命令解释器的另一个实例。就好像你的命令是在命令行提示下被解释的一样,类似于批处理文件中的一系列命令。每个shell脚本都有效地运行在父shell的一个子进程中。这个父shell指的是在一个控制终端或在一个xterm窗口中给出命令提示符的那个进程。

shell脚本也能启动它自己的子进程。这些子shell能够使脚本并行的,有效的,同时运行多个子任务。

一般来说,脚本中的外部命令能够生成(fork)一个子进程,然而Bash的內建命令却不会这么做。也正是由于这个原因,內建命令比等价的外部命令要执行的块。

3.10.1. 圆括号中的命令列表

( command1; command2; command3; … )

圆括号中命令列表的命令将会运行在一个子shell中。

子shell中的变量对于子shell之外的代码块来说,是不可见的。当然,父进程也不能访问这些变量,父进程指的是产生这个子shell的shell。事实上,这些变量都是局部变量。

子shell中的目录更改不会影响到父shell。

子shell可用于为一组命令设置一个“独立的临时环境”。

COMMAND1
COMMAND2
COMMAND3
(
    IFS=:
    PATH=/bin
    unset TERMINFO
    set -C
    shift 5
    COMMAND4
    COMMAND5
    exit 3  # 只是从子shell退出
)
# 父shell不受任何影响,并且父shell的环境也没有被更改
COMMAND6
COMMAND7

进程在不同的子shell中可以并行的执行。这样就可以把一个复杂的任务分成几个小的子问题来同时处理。

(cat list1 list2 list3 | sort | uniq > list123) &
(cat list4 list5 list6 | sort | uniq > list456) &
wait # 不再执行下面的命令,知道子shell执行完毕
diff lists123 list456

使用“|”管道操作符,将I/O流重定向到一个子shell中,比如ls -al | (command)

在大括号中的命令不会启动shell。

{ command1; command2; command3; ... commandN; }

3.11. 受限shell

在受限shell中禁用的命令。

在受限模式下运行一个脚本或脚本片段,将会禁用某些命令,这些命令在正常模式下都可以运行。这是一种安全策略,目的是为了限制脚本用户的权限,并且能够让运行脚本所导致的危害降低到最小。

set -r 设置受限模式。

脚本开头以"#!/bin/bash -r"来调用,会使整个脚本在受限模式下运行。

3.12. 进程替换

进程替换于命令替换很相似。命令替换把一个命令的结果赋值给一个变量,比如dir_contents=`ls -al`或xref=$(grep word datafile)。进程替换把一个进程的输出提供给另一个进程(换句话说,它把一个命令的结果发给了另一个命令)。

进程替换的模板,用圆括号括起来的命令

>(command)
<(command)

启动进程替换。它使用/dev/fd/文件将圆括号中的进程处理结果发送给另一个进程。实际上现代的UNIX类操作系统提供的/dev/fd/n文件是与文件描述符相关的,整数n指的就是进程运行时对应数字的文件描述符。

在"<“或”>"与圆括号之间是没有空格的,如果加了空格,会产生错误。

$ echo >(true)
/dev/fd/63
$ echo <(true)
/dev/fd/63

Bash在两个文件描述符之间创建了一个管道,–fIn和fOut–,true命令的stdin被连接到fOut(dup2(fOut, 0)),然后Bash把/dev/fd/fIn作为参数传给echo。如果系统缺乏/dev/fd/文件,Bash会使用临时文件。

进程替换可以比较两个不同命令的输出,甚至能够比较同一个命令不同选项情况下的输出。

$ comm <(ls -l) <(ls -al)

使用进程替换来比较两个不同目录的内容(可以查看文件名相同,哪些文件名不同)

diff <(ls $first_directory) <(ls $second_directory)

一些进程替换的其它用法与技巧

cat <(ls -l)    # 等价于 ls -l | cat
diff <(command1) <(command2) # 给出两个命令输出的不同之处

tar cf >(bzip2 -c > file.tar.bz2) $directory_name
# 调用"tar cf /dev/fd/>> $directory_name",和"bzip2 -c > file.tar.bz2"
# 因为/dev/fd/的系统属性,所以两个命令之间的管道不必被命名。

3.13. 函数

与“真正的”编程语言一样,Bash也有函数,虽然在某些实现方面稍有限制。一个函数就是一个子程序,用于实现一些列操作的代码块,它是完成特定任务的“黑盒子”。当存在重复代码的时候,或者当一个任务只需要轻微修改就被重复使用的时候,你就需要考虑使用函数了。

function function_name {
    command...
}

function_name () {
    command...
}

C程序员肯定会更加喜欢第二种格式的写法(并且这种写法可移植性更好)。
在C中,函数的左大括号也可以写在下一行中。

function_name ()
{
    command...
}

只需要简单的调用函数名,函数就会被调用或触发。

函数定义必须在第一次调用函数之前完成。没有像C中函数“声明”的方法。

甚至可以在一个函数内嵌套另一个函数,虽然这么做并没有多大用处。

f1 () {
    f2 () {
        echo "Function \"f2\", inside \"f1\"."
    }
}

函数声明可以出现在看上去不可能出现的地方,比如说本应出现命令的地方,也可以出现函数声明。

ls -l | foo() { echo "foo"; }   # 可以这么做,但没什么用

if [ "$USER" = canpool ]; then
    canpool_greet () {
        echo "Hello, canpool"
    }
fi

canpool_greet   # 只能由canpool运行,其他用户使用的话,会引起错误。

3.13.1. 复杂函数和函数复杂性

函数可以处理传递给它的参数,并且能返回它的退出状态码给脚本,以便后续处理。

function_name $arg1 $arg2

函数以位置来引用传递过来的参数(就好像它们是位置参数),例如,$1,$2等。

也可以使用shift命令来处理传递给函数的参数。

与别的编程语言相比,shell脚本一般只会传值给函数。如果把变量名(事实上就是指针)作为参数传递给函数的话,那将被解释为字面含义,也就是被看作字符串。函数只会以字面含义来解释函数参数。

变量的间接引用提供了一种笨拙的机制,来将变量指针传递给函数。

echo_var() {
    echo "$1"
}

message=Hello
Hello=Goodbye

echo_var "$message"     # Hello
echo_var "${!message}"  # Goodbye

Hello="Hello, again!"
echo_var "$message"     # Hello
echo_var "${!message}"  # Hello, again!

将参数传递给函数之后,参数能否被解除引用。

dereference() {
    y=\$"$1"    # 变量名
    echo $y     # $Junk
    x=`eval "expr \"$y\" "`
    echo $1=$x
    eval "$1=\"Some Different Text \""  # 赋新值
}

Junk="Some Text"
echo $Junk "before"     # Some Text before

dereference Junk
echo $Junk "after"      # Some Different Text after
3.13.1.1. 退出与返回

退出状态码

函数返回一个值,被称为退出状态码。退出状态码可以有return命令明确指定,也可以由函数中最后一条命令的退出状态码来指定(如果成功则返回0,否则返回非0值)。可以在脚本中使用$?来引用退出状态码。因为有了这种机制,所以脚本函数也可以像C函数一样有“返回值”。

return

终止一个函数。return命令可选的允许带一个整型参数,这个整数将作为函数的“退出状态码”返回给调用这个函数的脚本,并且这个整数也被赋值给变量$?。

为了让函数可以返回字符串或是数组,可以使用一个在函数外可见的专用全局变量。

函数所能刚返回最大的正整数是255。return命令与退出状态码的概念被紧密联系在一起,并且退出状态码的值受此限制。幸运的是,如果想让函数返回大整数的话,有好多种不同的工作区能够应付这个情况。

如果你想获得大整数“返回值”的话,其实最简单的办法就是将“要返回的值”保存到一个全局变量中。

一种更优雅的做法是在函数中使用echo命令将“返回值输出到stdout”,然后使用命令替换来捕捉此值。

3.13.1.2. 重定向

重定向函数的stdin

函数本质上其实就是一个代码块,这就意味着它的stdin可以被重定向。

file_opt () {
    while read line; do
        echo "$line" | grep $1
    done
} < $file

还有一个办法,或许能够更好的理解重定向函数的stdin。它在函数内添加了一对大括号,并且将重定向stdin的行为放在这对添加的大括号上。

function_name () {
    {
        ...
    } < file
}

3.13.2. 局部变量

如果变量用local来声明,那么它就只能够在该变量被声明的代码块中可见。这个代码块就是局部“范围”。在一个函数中,一个局部变量只有在函数代码块中才有意义。

在函数被调用之前,所有在函数中声明的变量,在函数体外都是不可见的,当然也包括那些被明确声明为local的变量。

3.13.2.1. 局部变量使递归变为可能

局部变量允许递归,但是这种方法会产生大量的计算,因此在shell脚本中,非常明确的不推荐这种做法。

3.13.2.2. 不使用局部变量的递归

即使不使用局部变量,函数也可以递归的调用自身。

3.14. 别名

Bash别名本质上来说不过就是个简称,缩写,是一种避免输入长命令序列的手段。举个例子,如果我们添加alias lm="ls -l | more"到文件~/.bashrc中,那么每次在命令行中键入lm就可以自动转换为ls -l | more。这可以让你在命令行上少敲好多次,而且也可以额笔名记忆复杂的命令和繁多的选项。设置alias rm=“rm -i”(删除的时候提示),可以让你在犯了错误之后也不用悲伤,因为它可以让你避免意外删除重要文件。

在脚本中,别名就没那么重要了。如果把别名机制相像成C预处理其的某些功能的话,就很形象,比如说宏扩展,但不幸的是,Bash不能再别名中扩展参数。而且在脚本中,别名不能够用在“混合型结构”中,比如if/then结构,循环,和函数。还有一个限制,别名不能递归扩展。绝大多数情况下,我们期望别名能够完成的工作,都能够用函数更高效的完成。

shopt -s expand aliases 设置这个选项,打开别名功能

unalias命令用来删除之前设置的别名。

3.15. 列表结构

“与列表”和“或列表”结构能够提供一种手段,这种手段能够用来处理一串连续的命令。这样就可以有效的替换掉嵌套的if/then结构,甚至能够替换掉case语句。

3.15.1. 把命令连接到一起

与列表

command1 && command2 && command3 && ... commandN

如果每个命令执行后都返回true(0)的话,那么命令将会依次执行下去。如果其中的某个命令返回false(非零值)的话,那么这个命令链就会被打断,也就是结束执行。(那么第一个返回false的命令,就是最后一个执行的命令,其后的命令都不会执行)。

或列表

command1 || command2 || command3 || ... commandN

如果每个命令都返回false,那么命令链就会执行下去,一旦有一个命令返回true,命令链就会被打断,也就是结束执行。(第一个返回true的命令将会是最后一个执行的命令)。显然,这和“与列表”完全相反。

与列表和或列表的退出状态码由最后一个命令的退出状态所决定。

3.16. 数组

新版本的Bash支持以为数组。数组元素可以使用符号variable[xx]来初始化。另外,脚本可以使用declare -a variable语句来指定一个数组。如果想解引用一个数组元素(也就是取值),可以使用大括号,访问形式为${variable[xx]}。

a[1]=1
a[2]=2
echo ${a[2]}

b=(1 2 3 4)
echo ${b[0]}

c=([1]=1 [2]=2)
echo ${c[2]}

Bash允许把变量当成数组来操作,即使这个变量没有明确地被声明为数组。

string=abc
echo ${string[@]}   # abc
echo ${stirng[*]}   # abc
echo ${string[0]}   # abc
echo ${string[1]}   # 没有输出,数组中只有一个元素,就是字符串本身
echo ${#string[@]}  # 1

数组元素有它们独特的语法,甚至标准Bash命令和操作符,都有特殊的选项用以配合数组操作。

a=(zero one two three four five)
#   0    1   2   3     4    5   数组元素

echo ${a[0]}    # zero
echo ${a:0}     # zero  第一个元素的参数扩展,从位置0开始,即第一个字符
echo ${a:1}     # ero   第一个元素的参数扩展,从位置1开始,即第二个字符

echo ${#a[0]}   # 4     第一个数组元素的长度
echo ${#a}      # 4     第一个数组元素的长度(另一种表示形式)

echo ${#a[1]}   # 3     第二个数组元素的长度,Bash中的数组时从0开始索引的

echo ${#a[*]}   # 6     数组中的元素个数
echo ${#a[@]}   # 6     数组中的元素个数

b=(zero one two three four five five)
# 提取尾部的子串
echo ${b[@]:0}          # zero one two three four five five
                        # 所有元素
echo ${b[@]:1}          # one two three four five five
                        # element[0]后边的所有元素
echo ${b[@]:1:2}        # one two
                        # 只提取element[0]后边的两个元素
# 子串删除
# 从字符串的开头删除最短的匹配,匹配的子串也可以是正则表达式
echo ${b[@]#f*r}        # zero one two three five five
                        # 匹配将应用于数组的所有元素
                        # 匹配到了“four”,并且将它删除
# 从字符串的开头删除最长的匹配
echo ${b[@]##t*e}       # zero one two t four five five
                        # 匹配将应用于数组的所有元素
                        # 匹配到了“three”,并且将它删除
# 从字符串的结尾删除最短的匹配
echo ${b[@]%h*e}        # zero one two t four five five
                        # 匹配将应用于数组的所有元素
                        # 匹配到了“hree”,并且将它删除
# 从字符串的结尾删除最长的匹配
echo ${b[@]%%t*e}       # zero one two four five five
                        # 匹配将应用于数组的所有元素
                        # 匹配到了“three”,并且将它删除
# 子串替换
# 第一个匹配到的子串将会被替换
echo ${b[@]/fiv/XYZ}    # zero one two three four XYZe XYZe
                        # 匹配将应用于数组的所有元素
# 所有匹配到的子串都会被替换
echo ${b[@]//iv/YY}     # zero one two three four fYYe fYYe
                        # 匹配将应用于数组的所有元素
# 删除所有的匹配子串
# 如果没有指定替换字符串的话,那就意味着“删除”
echo ${b[@]//fi/}       # zero one two three four ve ve
                        # 匹配将以用于数组的所有元素
# 替换字符串前端子串
echo ${b[@]/#fi/XY}     # zero one two three four XYve XYve
                        # 匹配将以用于数组的所有元素
# 替换字符串后端子串
echo ${b[@]/%ve/ZZ}     # zero one two three four fiZZ fiZZ
                        # 匹配将以用于数组的所有元素
echo ${b[@]/%o/XX}      # zerXX one twXX three four five five

命令替换可以构造数组的独立元素。

在数组环境中,某些Bash內建命令的含义可能会有些轻微的改变。比如,unset命令可以删除数组元素,甚至能够删除整个数组。

${array_name[@]}或${array_name[*]}都与数组中的所有元素相关。同样的,为了计算数组的元素个数,可以使用${#array_name[@]}或${#array_name[*]}。${#array_name}是数组第一个元素的长度,也就是${array_name[0]}的长度(字符个数)。

空数组与包含空元素的数组

array1=('')     # "array1"包含一个空元素
array2=()       # 没有元素,"array2"为空

${array_name[@]}和${array_name[*]}的关系非常类似于$@和$*,这种数组用法用处非常广泛。

复制一个数组

array2=("${array1[@]}")
array2="${array1[@]}"

然而,如果在“缺项”数组中使用的话,将会失败,也就是说数组中存在空洞(中间的某个元素没赋值)

array1[0]=0
array1[2]=2         # array1[1]没赋值
array2=("${array1[@]}")
echo ${array2[0]}   # 0
echo ${array2[2]}   # (null),实际可能期待是2

添加一个元素到数组

array=("${array[@]}" "new element")
array[${#array[*]}]="new element"

array=(element1 element2 … elementN)初始化操作,如果有命令替换的帮助,就可以将一个文本文件的内容加载到数组。

在数组声明的时候添加一个额外的declare -a语句,能够加速后续的数组操作速度。

3.17. /dev和/proc

Linux或者UNIX机器典型地都带有/dev或/proc目录,用于特殊目的。

3.17.1. /dev

/dev目录包含物理设备的条目,这些设备可能会以硬件的形式出现,也可能不会。包含有挂载文件系统的硬驱动器分区,在/dev目录中都有对应的条目,就像df命令所展示的那样。

在其它方面,/dev目录也包含还回设备,比如/dev/loop0。一个还回设备就是一种机制,可以让一般文件访问起来就像块设备那样。这使得我们可以挂载一个完整的文件系统,这个文件系统是在一个大文件中创建的。

/dev中还有少量的伪设备用于其它特殊目的,比如/dev/null,/dev/zero,/dev/urandom,/dev/sda1,/dev/udp,和/dev/tcp。

3.17.2. /proc

/proc目录实际上是一个伪文件系统。/proc目录中的文件用来映射当前运行的系统,内核进程以及与它们相关的状态与统计信息。

/proc目录下包含有许多以不同数字命名的子目录。这些作为子目录名字的数字,代表的是当前正在运行进程的进程ID。在这些以数字命名的子目录中,每一个子目录都有许多文件用来保存对应进程的可用信息。文件stat和status保存着进程运行时的各项统计信息,文件cmdline保存着进程被调用时的命令行参数,而文件exec是一个符号链接,这个符号链接指向这个运行进程的完整路径。还有许多类似这样的文件,如果从脚本的视角来看它们的话,这些文件都非常有意思。

一般来说,在/proc目录中,进行写文件操作是非常危险的,因为这么做可能会破坏文件系统,甚至于摧毁整个机器。

3.18. Zero与Null

/dev/zero与/dev/null

3.18.1. 使用/dev/null

可以把/dev/null想象为一个“黑洞”。它非常接近于一个只写文件。所有写入它的内容都会用于丢失。而如果想从它那读取内容,则什么也读不到。但是,对于命令行和脚本来说,/dev/null却非常的有用。

禁用stdout

cat $filename >/dev/null

禁用stderr

rm $badname 2>/dev/null

禁用stsdout和stderr

cat $filename 2>/dev/null >/dev/null
cat $filename &>/dev/null

删除一个文件的内容,但是保留文件本身,并且保留所有的文件访问权限。

cat /dev/null > /var/log/messages
: > /var/log/messages   # 具有同样的效果,但是不会产生新进程。

自动清空日志文件的内容(特别适用于处理那些由商业站点发送的,令人厌恶的“cookie”)

if [ -f ~/.netscape/cookies ]; then
    rm -f ~/.netscape/cookies
fi
ln -s /dev/null ~/.netscape/cookies

3.18.2. 使用/dev/zero

类似于/dev/null,/dev/zero也是一个伪文件,但事实上它会产生一个null流(二进制的0流,而不是ASCII类型)。如果你想把其它命令的输出写入它的话,那么写入的内容会消息,而且如果你想从/dev/zero中读取一连串null的话,也非常的困难,虽然可以使用od或者一个16进制编辑器来达到这个目的。/dev/zero的主要用途就是用来创建一个指定长度,并且初始化为空的文件,这种文件一般都用作临时交换文件。

dd if=/dev/zero of=$FILE bs=$BLOCKSIZE count=$blocks    # 用零填充文件

/dev/zero还有其他的应用场合,比如当你出于特殊目的,需要“用0填充”一个指定大小的文件时,就可以使用它。举个例子,比如要将一个文件系统挂载到还回设备上,或者想“安全”的删除一个文件。

3.19. 调试

首选,调试要比编写代码困难的多,因此,如果你尽可能聪明的编写代码,你就不会在调试的时候花费很多精力。

Bash并不包含调试器,甚至都没有包含任何用于调试目的的命令和结构。脚本中的语法错误,或者拼写错误只会产生模糊的错误信息,当你调试一些非功能性脚本的时候,这些错误信息通常都不会提供有意义的帮助。

如果想调试不工作的脚本,有如下工具可用:

1)echo语句可以放在脚本中存在疑问的位置上,来观察变量的值,也可以了解脚本后续的动作。

2)使用过滤器tee来检查临界点上的进程或数据流。

3)设置选项-n -v -x

sh -n scriptname 不会运行脚本,只会检查脚本的语法错误。这等价于把set -n或set -o noexec插入脚本中。注意,某些类型的语法错误不会被这种方式检查出来。

sh -v scriptname 将会在运行脚本之前,打印出每一个命令。这等价于把set -v或set -o verbose插入脚本中。

选项-n和-v可以同时使用。sh -nv scriptname将会给出详细的语法检查。

sh -x scriptname 会打印出每个命令执行的结果,但只使用缩写形式。这等价于在脚本中插入set -x或set -o xtrace。

把set -u或set -o nounset插入到脚本中,并运行它,就会在每个试图使用未声明变量的地方给出一个unbound variable错误。

4)使用“assert”(断言)函数在脚本的临界点上测试变量或条件。

5)使用变量$LINENO和內建命令caller。

6)捕获exit。

脚本中的exit命令会触发一个信号0,这个信号终止进程,也就是终止脚本本身。捕获exit在某些情况下很有用,比如说强制“打印”变量值。trap命令必须放在脚本中第一个命令的位置上。

捕获信号 trap

可以在收到一个信号的时候指定一个处理动作;在调试的时候,这一点也非常有用。

A signal就是发往进程的一个简单消息,这个消息既可以由内核发出,也可以由另一个进程发出,发送这个消息的目的是为了通知目的进程采取一些指定动作(通常都是终止动作)。比如说,按下Control-C,就会发送一个用户中断(即INT信号)到运行中的进程。

trap '' 2   # 忽略中断2(Control-C),没有指定处理动作。
trap 'echo "Control-C disabled."' 2 # 当Control-C按下时,显示一行信息。

捕获exit

trap 'echo xxxx' EXIT   # EXIT是脚本中exit命令所产生信号的名字。

trap所指定的命令并不会马上执行,只有接收到合适的信号,这些命令才会执行。

Control-C之后,清除垃圾

trap 'rm -f xxx' TERM INT

如果使用trap命令的DEBUG参数,那么当脚本中每个命令执行完毕后,都会执行指定的动作。比方说,你可以跟踪某个变量的值。

trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\"' DEBUG

当每个命令执行后,就会打印出$variable的值。

trap ‘’ SIGNAL(两个引号之间为空)载剩余的脚本中禁用了SIGNAL信号的动作。trap SIGNAL则会恢复处理SIGNAL的动作。
当你想保护脚本的临界部分不受意外的中断骚扰,那么上面讲的这种办法就非常有用了。

trap '' 2   # 信号2就是Control-C,现在被禁用了
command
command
trap 2  # 重新恢复Control-C

Bash 3.0之后增加了如下这些特殊变量用于调试

$BASH_ARGC
$BASH_ARGV
$BASH_COMMAND
$BASH_EXECUTION_STRING
$BASH_LINENO
$BASH_SOURCE
$BASH_SUBSHELL

3.20. 选项

选项用来更改shell和脚本的行为。

set命令用来打开脚本中的选项。你可以在脚本中任何你想让选项生效的地方插入set -o option-name,或者使用更简单的形式,set -option-abbrev。这两种形式是等价的。

set -o verbose  # 打印出所有执行前的命令
set -v          # 与上面具有相同的效果

如果你想在脚本中禁用某个选项,可以使用set +o option-name或set +option-abbrev。

还有另一种可以在脚本中启用选项的方法,那就是在脚本头部,#!的后边直接指定选项。

#!/bin/bash -x

也可以从命令行中打开脚本的选项。某些不能与set命令一起用的选项就可以使用这种方法来打开。i就是其中之一,这个选项用来强制脚本以交互的方式运行。

bash -v script-name
bash -o verbose script-name

下表列出了一些有用的选项。它们都可以要使用缩写的形式来指定(开头加一个破折号),也可以使用完整名字来指定(开头双破折号,或者使用-o选项来指定)。

缩写 名称 作用
-C noclobber 防止重定向时覆盖文件(可能会被>
-D (none) 列出用双引号括起来的,以$为前缀的字符串,但是不执行脚本中的命令
-a allexport export(导出)所有定义过的变量
-b notify 当后台运行的作业终止时,给出通知(脚本中并不常见)
-c … (none) 从…中读取命令
-e errexit 当脚本发生第一个错误是,就退出脚本,换种说法就是,当一个命令返回非零值时,就退出脚本(除了until或whle loops,if-tests,list-constructs)
-f noglob 禁用文件名扩展(就是禁用globbing)
-i interactive 让脚本以交互模式运行
-n noexec 从脚本中读取命令,但是不执行它们(做语法检查)
-o option-name (none) 调用Option-Name选项
-o posix POSIX 修改Bash或被调用脚本的行为,使其符号POSIX标准
-p privileged 以“suid”身份来运行脚本(小心)
-r restricted 以受限模式来运行脚本
-s stdin 从stdin中读取命令
-t (none) 执行完第一个命令之后,就退出
-u nounset 如果尝试使用了未定义的变量,就会输出一个错误消息,然后强制退出
-v verbose 在执行每个命令之前,把每个命令打印到stdout上
-x xtrace 与-v选项类似,但是会打印完整命令
- (none) 选项结束标志,后面的参数为位置参数
(none) unset(释放)位置参数。如果制定了参数列表(-- arg1 arg2),那么位置参数将会一次设置到参数列表中

3.21. 陷阱

1)将保留字或特殊字符声明为变量名

case=value0         # case是保留字
23skidoo=value1     # 以数字开头的变量名是被shell保留使用的
_=25
echo $_             # $_是一个特殊变量,代表最后一个命令的最后一个参数
xyz((!*=value2      # Bash3.0之后,标点不能出现在变量名中

2)使用连字符或其它保留字符来做变量名(或函数名)

var-1=23
function-wahtever()
function.whatever()

3)让变量名与函数名相同。这会使得脚本的可读性变得很差

do_something() {
    ...
}
do_something=do_something
do_something do_something

这么做是合法的,但是会让人混淆。

4)不合时宜的使用空白字符。与其它编程语言相比,Bash非常讲究空白字符的使用。

var1 = 23   # Bash会把var1当作命令来执行,"="和"23"会被看作“命令var1的参数
let c = $a - $b # 'let c=$a-$b'或'let "c = $a - $b"'才是正确的
if [ $a -le 5]

5)在大括号包含的代码块中,最后一条命令没有以分号结尾。

{ls -l; df; echo "Done"}    # bash: syntax error: unexpected end of file

6)假定未初始化的变量(赋值前的变量)被“请0”。事实上,未初始化的变量值为“null”,而不是0.

echo "uninitialized_var = $uninitialized_var"

7)混淆测试符号=和-eq。请记住,=用于比较字符变量,而-eq用来比较整数。

if [ "$a" = 273 ]       # $a是整数还是字符串?
if [ "$a" -eq 273 ]     # $a为整数

8)误用了字符串比较操作符

9)有时候在“test”中括号([])结构里的变量需要被引用起来(双引号)。如果不这么做的话,可能会引起不可预料的结果。

10)脚本中的命令可能会因为脚本宿主不具备相应的运行权限而导致运行失败,如果用户在命令行中就不能调用这个命令的话,那么即使把它放到脚本中来运行,也还是会失败。这时可以通过修改命令的属性来解决这个问题,有时候甚至要给它设置suid位(当然,要以root身份来设置)。

11)试图使用-作为重定向操作符(事实上它不是),通常都会导致令人不快的结果。

command1 2> - | command2
command1 2>& - | command2   # 都没效果

12)使用Bash 2.0或更高版本的功能,可以在产生错误消息的时候,引发修复动作。但是比较老的Linux机器默认安装的可能是Bash 1.XX。

minimum_version=2
if [ "$BASH_VERSION" \< "$minimum_version" ]; then
    ...
fi

13)在非Linux机器上的Bourne shell脚本(#!/bin/sh)中使用Bash特有的功能,可能会一起不可预料的行为。Linux系统通常都会把bash别名化为sh,但是在一般的UNIX机器上却不一定会这么做。

14)使用Bash未文档化的特征,将是一种危险的举动。

15)一个带有DOS风格换行符(\r\n)的脚本将会运行失败,因为#!/bin/bash\r\n是不合法的,与我们所期望的#!/bin/bash\n不同。解决办法就是将这个脚本转换为UNIX风格的换行符。

16)以#!/bin/sh开头的Bash脚本,不能在完整的Bash兼容模式下运行。某些Bash特定的功能可能会被金庸。如果脚本需要完整的访问所有Bash专有扩展,那么它需要使用#!/bin/bash作为开头。

17)如果在here document中,即为limit string之前加上空白字符的话,将会导致脚本的异常行为。

18)脚本不能将变量export到它的父进程(即调用这个脚本的shell),或父进程的环境中。就好比我们在生物学中所学到的那样,子进程只会继承父进程,反过来则不行。

19)在子shell中设置和操作变量之后,如果尝试在子shell作用域之外使用同名变量的话,将会产生令人不快的结果。

20)将echo的输出通过管道传递个read命令可能会产生不可预料的结果。在这种情况下,read命令的行为就好像它在子shell中运行一样。可以使用set命令来代替。

21)在脚本中使用“suid”命令是非常尾箱的,因为这会危及系统安全。

22)使用shell脚本来编写CGI程序是值的商榷的。因为shell脚本的变量不是“类型安全”的,当CGI被关联的时候,可能会产生令人不快的行为。此外,它还很难抵挡住“破解的考验”。

23)Bash不能正确的处理双斜线(//)字符串。

3.22. 脚本编程风格

编写脚本时,最好养成系统化和结构化的风格。及时你在“空闲时”,“在信封后边顺便做一下草稿”也是非常有益处的,所以,在你坐下来编写代码之前,最好花几分钟的时间来规划和组织一下你的想法。

这里所描述的是一些风格上的指导原则。但是请注意,这节文档并不是想成为一个官方的Shell脚本编写风格。

3.22.1. 非官方的Shell脚本编写风格

1)习惯性的注释你的代码。这可以让别人更容易看懂(或者感谢)你的代码,而且也更便于维护。

2)避免使用“魔法数字”,也就是,避免“写死的”字符常量。可以使用有意义的变量名来代替。这使得脚本更易于理解,并且允许在不破坏应用的情况下进行修改和更新。

3)给变量和函数起一些有意义的名字。

4)退出码最好也采用具有系统性的或有意义的命名方式。可以采用/usr/include/sysexits.h中的定义作为退出码,虽然这些定义主要用于C/C++编程语言。

5)在脚本调用中使用标准化的参数标志。最后,建议使用下面的参数集

-a  全部: 返回全部信息(包括隐藏的文件信息).
-b  摘要: 缩减版本, 通常用于其它版本. 通常用于其它脚本.
-c  拷贝, 连接, 等等.
-d  日常的: 使用全天的信息,
    而不仅仅是特定用户或特定实例的信息.
-e  扩展/详细描述: (通常不包括隐藏文件信息).
-h  帮助: 详细的使用方法, 附加信息, 讨论, 帮助.
    也请参考-V.
-l  打印出脚本的输出记录.
-m  手册: 显示基本命令的man页.
-n  数字: 仅使用数字数据.
-r  递归: 这个目录中所有的文件(也包含所有子目录).
-s  安装&文件维护: 这个脚本的配置文件.
-u  用法: 列出脚本的调用方法.
-v  详细信息: 只读输出, 或多或少的会做一些格式化.
-V  版本/许可/版权Copy(right|left)/捐助(邮件列表).

6)将一个复杂的脚本分割成一些简单的模块。使用合适的函数来实现模块的功能。

7)如果有更简单的结构可以使用的话,就不要使用复杂的结构。

3.23. 杂项

3.23.1. 交互与非交互式的shell和脚本

交互式的shell会在tty上从用户输入中读取命令。另一方面,这样的shell能在启动时读取启动文件,显示一个提示符,并默认激活作业控制。也就是说,用户可以与shell交互。

shell所运行的脚本通常都是非交互的shell。但是脚本仍然可以访问它的tty。甚至可以在脚本中模拟一个交互式的shell。

让我们考虑一个需要用户输入的交互式脚本,这种脚本通常都要使用read语句。但是“现实的情况”肯定要比这复杂的多。就目前的情况来看,交互式脚本通常都绑定在一个tty设备上,换句话说,用户都是在控制终端或xterm上来调用脚本的。

初始化脚本和启动脚本都是非交互式的,因为它们都不需要人为干预,都是自动运行的。许多管理脚本和系统维护脚本也同样是非交互式的。对于那些不需要经常变化的。重复性的任务,应该交给非交互式的脚本来自动完成。

非交互式的脚本可以在后台运行, 但是如果交互式脚本在后台运行的话, 就会被挂起, 因为它们在等待永远不会到来的输入。如果想解决后台运行交互式脚本的问题, 可以使用带有expect命令的脚本, 或者在脚本中嵌入here document来提供交互式脚本所需要的输入。最简单的办法其实就是将一个文件重定向给read命令, 来提供它所需要的输入(read variable < file)。通过使用上述方法, 就可以编写出通用目的脚本, 这种脚本即可以运行在交互模式下, 也可以运行在非交互模式下.

如果脚本需要测试一下自己是否运行在交互式shell中, 那么一个简单的办法就是察看是否存在提示符(prompt)变量, 也就是察看一下变量$PS1是否被设置。(如果脚本需要用户输入, 那么脚本就需要显示提示符。)

if [ -z $PS1 ]; then
    # 非交互式
else
    # 交互式
fi

另一种办法,脚本可以测试一下标志$-中是否存在选项“i”。

case $- in
    *i*)    # 交互式shell
    ;;
    *)      # 非交互式shell
    ;;
esac

使用#!/bin/bash -i头,或者使用-i选项,可以强制脚本运行在交互模式下。注意,这么做可能会让脚本产生古怪的行为,有时候即使在没有错误的情况下,也可能会显示错误信息。

3.23.2. Shell包装

“包装“脚本指的是内嵌系统命令或工具的脚本,并且这种脚本保留了传递给命令的一系列参数。因为包装脚本中包含了许多带有参数的命令,使它能够完成特定的目的,所以这样就大大简化了命令行的输入。这对于sed和awk命令特别有用。

sed或awk脚本通常都是在命令行被调用的,使用的形式一般为sed -e 'commands’或awk ‘commands’。将这样的脚本(指的是包装了sed和awk的脚本)嵌入到Bash脚本中将会使调用更加简,并且还可以“重复利用”。也可以将sed与awk的功能结合起来使用,比如,可以将一系列sed命令的输出通过管道传递给awk。还可以保存为可执行文件,这样你就可以重复的调用它了,如果功能不满足,你还可以修改它,这么做可以让省去每次都在命令行上输入命令的麻烦。

如果那些脚本需要的是一个全功能(多合一)的工具,一把瑞士军刀,那么只能使用Perl了。Perl兼顾sed和awk的能力,并且包含了C的很大的一个自己,用于引导。它是模块化的,并且包含从面相对象编程到厨房水槽的所有功能(表示Perl无所不能)。小端的Perl脚本可以内签到shell脚本中,以至于有人声称Perl可以完全代替shell脚本。

3.23.3. 测试和比较

对于测试来说,[[ ]]结构可能比[ ]结构更合适。同样的,在算术比较中,使用(( ))结构可能会更有用。

3.23.4. 递归

脚本是否可以递归调用自身?当然可以。

./$0

过多层次的递归会耗尽脚本的栈空间,引起段错误。

3.23.5. 将脚本“彩色化”

ANSI定义了屏幕属性的转义序列集合,比如说粗体文本,前景与背景颜色。DOS批处理文件通常使用ANSI转义码来控制颜色输出,Bash脚本也就这么做的。

最简单的,也可能是最有用的ANSI转义序列是加粗文本,\033[1m … \033[0m。\033代表转义,“[1"打开加粗属性,而”[0"关闭加粗属性,"m"表示转义序列结束。

echo -e "\033[1mThis is bold text.\033[0m"

echo命令的-e选项用来启用转义序列。

转义序列中颜色与数字的对应

颜色    前景    背景
黑      30      40
红      31      41
绿      32      42
黄      33      43
蓝      34      44
洋红    35      45
青      36      46
白      37      47

然而,这里有一个严重的问题,ANSI转义序列实不可移植的,在某些终端(或控制台)上运行好好的代码,可能在其它终端上根本没办法运行。“彩色”的脚本可能会在脚本作者的机器上运行的非常好,但是在其他人的机器上就可能产生不可读的输出。因为这个原因,使得“彩色”脚本的用途大打折扣,而且很有可能使得这项技术变成华而不实的小花招,甚至成为一个“玩具”。

Moshe Jacobson的彩色工具(http://runslinux.net/projects.html#color)能够非常容易的简化ANSI转义序列的使用。这个工具使用清晰而且富有逻辑的语法代替了之前讨论的难用的结构.

Henry/teikedvl也开发了一个类似的工具(http://scriptechocolor.sourceforge.net/)用来简化彩色脚本的创建。

3.23.6. 优化

大部分shell脚本在处理不太复杂的问题的时候,使用的都是小吃店(快速但是并不优雅)的方式。正因为这样,所以优化脚本的速度并不是一个大问题。考虑一下这种情况,,当脚本正在处理一个重要任务的时候,虽然这个脚本能够处理的很好,但是它运行的速度实在太慢。在这种情况下,使用编译语言重写它其实也不是一种很合适的办法。最简单的办法其实就是重写这个脚本执行效率低下的部分。那么,是否这种办法可以处理效率低下的shell脚本的一种原则?

仔细检查脚本中循环的部分。因为重复的操作非常好使。如果有可能的话,尽量删掉循环中比较耗时的操作。

优先使用內建命令,而不是系统命令。这是因为內建命令执行得更快,并且在调用时,一般都不会产生子进程。

避免使用不必要的命令,由其是管道中的命令。

cat "$file" | grep "$word"
grep "$word" "$file"

上面的两行具有相同的效果,但是第二行运行的更快,因为它不产生子进程。

cat命令看起来经常在脚本中被滥用。

使用time和times工具来了解计算所消耗的时间。可以考虑使用C语言,甚至是汇编语言来重写时间消耗比较大的代码部分。

尝试尽量减少文件I/O操作。因为Bash在处理文件方面,显得并不是很有效率,所以可以在脚本中考虑使用更合适的工具,比如awk或perl。

使用结构化的思想来编写脚本,并且按照需求将各个模块组织并紧密结合起来。一些适用于高级语言的优化技术也可以用在脚本上,但是有些技术,比如,循环展开优化,就根本用不上。

3.23.7. 各种小技巧

1)为了记录在某个(或某些)特定会话中用户脚本的运行状态,可以将下面的代码添加到你想要跟踪记录的脚本中。添加的这段代码会将脚本名和调用次数记录到一个连续的文件中。

# 添加(>>)下面的代码, 到你想跟踪记录的脚本末尾.
whoami>> $SAVE_FILE # 记录调用脚本的用户.
echo $0>> $SAVE_FILE # 脚本名.
date>> $SAVE_FILE # 记录日期和时间.
echo>> $SAVE_FILE # 空行作为分隔符.
# 当然, 我们应该在~/.bashrc中定义并导出变量SAVE_FILE

2)>>操作符可以在文件末尾添加内容。如果你想在文件的头部添加内容怎么办,难道要粘贴到文件头?

file=data.txt
title="***This is the title line of data text file***"
echo $title | cat - $file > $file.new

cat -将stdout链接到$file,最后的结果就是生成了一个新文件。当然,sed也能做到。

3)shell脚本也可以像一个内嵌到脚本的命令那样被调用,比如Tcl或wish脚本,甚至是Mackfile。在C语言中,它们可以作为一个外部的shell命令被system()函数调用,比如,system(“script_name”);

4)将一个内嵌sed或awk的脚本内容赋值给一个变量,能够提高shell包装脚本的可读性。

5)将你最喜欢的变量定义和函数实现都放到一个文件中。在你需要的时候,通过使用点(.)命令,或者 source命令,来将这些“库文件”包含到脚本中。

6)使用特殊目的的注释头来增加脚本的条理性和可读性。

7)if-test结构有一种聪明的用法,用来注释代码块。

8)使用$?退出状态变量,因为脚本可能需要测试一个参数是否都是数字,以便于后边可以把它当做一个整数来处理。

9)函数的返回值严格限制在0-255之间。使用全局变量或者其他方法来代替函数返回值,通常都很容易产生问题。从函数中,返回一个值到脚本主体的另一个办法是,将这个“返回值”写入到stdout(通常都是用echo命令),然后将其赋值给一个变量。这种做法其实就是命令替换的一个变种。相同的技术也可以用在字符串上。这意味着函数可以“返回”非数字的值。使用这种办法甚至能够“返回”多个值。

10)将数组传递给函数,然后“返回”一个数组给脚本的主体。
使用命令替换将数组中的所有元素(元素之间用空格分隔)赋值给一个变量,这样就可以将数组传递到函数中了。然后使用命令替换或者(…)操作,将函数的输出保存到一个变量中。

11)利用双括号结构,就可以让我们使用C风格的语法,在for循环和while循环中,设置或者增加变量。

12)如果在脚本的开头设置path和umask的话,就可以增加脚本的“可移植性”,即使在那些被用户将$PATH和umask弄糟了的机器上,也可以运行。

13)一项很有用的技术是,重复地将一个过滤器的输出(通过管道)传递给这个相同的过滤器,但是这两次使用不同的参数和选项。尤其是tr和grep,非常适合于这种情况。

14)使用“匿名的here document”来注释代码块,这样就不用在每个注释行前面都加上#了。

15)如果一个脚本的运行依赖于某个命令,而且这个命令没被安装到运行这个脚本的机器上,那么在运行的时候就会产生错误。我们可以使用whatis命令来避免这种可能产生的问题。

16)在错误的情况下,if-grep test可能不会返回期望的结果,因为出错的文本是输出到stderr上,而不是stdout。将stderr重定向到stdout上(2>&1),就可以解决这个问题。

17)run-parts命令可以很方便的依次运行一组命令脚本,尤其是和cron或at组合使用的时候。

18)如果可以在shell脚本中调用X-Windows的小工具,那该有多好。目前已经有一些工具包可以完成这种功能,比如Xcript,Xmenu和widtools。头两种工具包已经不再被维护了。幸运的是,我们还可以从这里下载第三种工具包,widtools。

3.23.8. 安全问题

3.23.8.1. 被感染的脚本

在这里对脚本安全进行一个简短的介绍非常合适。shell脚本可能会包含蠕虫,特洛伊木马等。由于这些原因,用于不要用root身份来运行脚本(或者将自己不太清楚的脚本插入到/etc/rc.d里面的系统启动脚本中),除非你确定这是值得信赖的源代码,或者你已经小心的分析了这个脚本,并确定它不会产生什么危害。

Bell实验室以及其他地方的病毒研究人员等已经研究过了shell脚本病毒的实现。它们认为即使是初学者也可以很容易的编写脚本病毒。

这也是学习脚本编程的另一原因。能够很好地了解脚本,就可以让你的系统免受骇客的攻击和破坏。

3.23.8.2. 隐藏shell脚本源代码

出于安全目的,让脚本不可读,也是有必要的。如果有软件可以将脚本转化为相应的二进制可执行文件就好了。Francisco Rosales的shc - generic shell script compiler可以出色的完成这个任务。

不幸的是, 根据发表在2005年10月的Linux Journal上的一篇文章, 二进制文件, 至少在某些情况下,可以被恢复成原始的脚本代码. 但是不管怎么说, 对于那些技术不高的骇客来说, 这仍然是一种保证脚本安全的有效办法.

3.23.9. 可移植性问题

整篇文章主要描述的是,在GNU/Linux系统上,如何处理特定域Bash的脚本。但是使用sh和ksh的用户仍然会从这里找到很多有价值的东西。

碰巧,许多不同的shell脚本语言其实都是遵循POSIX 1003.2标准。如果使用–posix选项来调用Bash,或者在脚本头插入set -o posix,那么将会使Bash与这个标准非常接近地保持一致。另一种办法就是在脚本头使用#/bin/sh,而不是#/bin/bash。

注意在Linux或者某些特定的UNIX上,/bin/sh其实只是一个指向/bin/bash的链接,并且使用这种方法调用脚本的话,将会禁用Bash的扩展功能。

大多数的Bash脚本都好像运行在ksh上一样,反过来看,这是因为Chet Ramey一直致力于将ksh的特性移植到Bash的最新版本上。

对于商业UNIX机器来说,如果在脚本中包含了使用GNU特性的标准命令,那么这些脚本可能不会正常运行。但是在最近几年,这个问题得到了极大的改观,这是因为即使在“大块头”UNIX上,GNU工具包也非常好的替换掉了同类的私有工具。对于原始的UNIX来说,源代码的火山喷发加剧了这种趋势。

Bash具有的某些特性时传统的Bourne Shell所缺乏的。下面就是其中的一部分:

  • 某些扩展的调用选项
  • 使用$()形式的命令替换
  • 某些字符串处理操作符
  • 进程替换
  • Bash特有的內建命令

3.23.10. Windows下的shell脚本

即使用户使用其它的操作系统来运行类UNIX的shell脚本,其实也能够从本文的大部分课程中收益。Cygnus公司的Cygwin程序包和Mortice Kern Associates的MKS工具集都能够给Windows添加处理shell脚本的能力。

这其实暗示了Windows将来的版本可能会包含处理类Bash命令行脚本的能力。

3.23.11. 版本新特性

Bash版本3开始加入了一些新特性。

1)一个新的,更加通用的{a…z}大括号扩展操作符

for i in {1..10}    # 比下面的方式更简单
for i in $(seq 10)

2)${!array[@]}操作符,用于扩展给定数组所有元素索引。

Array=(element-zero element-one element-two element-three)
echo ${!Array[@]}   # 0 1 2 3 数组的全部索引

3)=~ 正则表达式匹配操作符,在双中括号测试表达式中的应用。(perl也有一类似的操作符)

if [[ "$variable" =~ "pattern" ]]

4. 项目

  • shcanpool - 一款简单的面向过程的shell框架

5. 参考

  • 高级Bash脚本编程指南(Advanced Bash-Scripting Guide)(Mendel Cooper 著,杨春敏 黄毅 译)

你可能感兴趣的:(shell,shell,bash,linux)