摘录整理自:http://billie66.github.io/TLCL/book/
为了成功地创建和运行一个 shell 脚本,我们需要做三件事情:
-
编写一个脚本。 Shell 脚本就是普通的文本文件。所以我们需要一个文本编辑器来书写它们。最好的文本 编辑器都会支持语法高亮,这样我们就能够看到一个脚本关键字的彩色编码视图。语法高亮会帮助我们查看某种常见 错误。为了编写脚本文件,vim,gedit,kate,和许多其它编辑器都是不错的候选者。
-
使脚本文件可执行。 系统会相当挑剔不允许任何旧的文本文件被看作是一个程序,并且有充分的理由! 所以我们需要设置脚本文件的权限来允许其可执行。
-
把脚本放置到 shell 能够找到的地方 当没有指定可执行文件明确的路径名时,shell 会自动地搜索某些目录, 来查找此可执行文件。为了最大程度的方便,我们会把脚本放到这些目录当中。
脚本文件格式
#!/bin/bash
# This is our first script.
echo 'Hello World!'
#!字符序列是一种特殊的结构叫做 shebang。 这个 shebang 被用来告诉操作系统将执行此脚本所用的解释器的名字。每个 shell 脚本都应该把这一文本行作为它的第一行。
文本行中,# 符号之后的所有字符都会被忽略。
保存文件并使该文件具有可执行权限后,就可以执行该脚本文件了。如果将该文件存放在PATH环境变量某一路径,则可以直接执行该文件,而不必指明路径,即把脚本放置在shell能够找到的地方(例:指明路径执行./hello_world 直接执行hello_world)PATH路径可通过指令echo $PATH查看
文本输出方法除了使用echo外,还可以使用here document,或者叫做 here script。一个 here document 是另外一种 I/O 重定向形式,我们在脚本文件中嵌入正文文本,然后把它发送给一个命令的标准输入。它这样工作:
command << token
text
token
这里的 command 是一个可以接受标准输入的命令名,token 是一个用来指示嵌入文本结束的字符串。如:
#!/bin/bash
# Program to output a HTML file
cat << _EOF_
<HTML>
<HEAD>
<TITLE>web page name</TITLE>
</HEAD>
<BODY>
page body
</BODY>
</HTML>
_EOF_
该脚本使用 cat 命令和一个 here document。这个字符串_EOF_(意思是“文件结尾”, 一个常见用法)被选作为 token,并标志着嵌入文本的结尾。注意这个 token 必须在一行中单独出现,并且文本行中 没有末尾的空格。
那么使用一个 here document 的优点是什么呢?它很大程度上和 echo 一样,除了默认情况下,here documents 中的单引号和双引号会失去它们在 shell 中的特殊含义。
运行该脚本时,只需将结果重定向到一个html文件,即可通过浏览器查看内容
如果我们把重定向操作符从 “<<” 改为 “<<-”,shell 会忽略在此 here document 中开头的 tab 字符。 这就能缩进一个 here document,从而提高脚本的可读性,即以下两个脚本效果一样
#!/bin/bash
cat << _EOF_
hello world
this is my program
_EOF_
#!/bin/bash
cat <<- _EOF_
hello world
this is my program
_EOF_
变量
#!/bin/bash
# Program to output a system information page
title="System Information Report"
echo "<HTML>
<HEAD>
<TITLE>$title</TITLE>
</HEAD>
<BODY>
<H1>$title</H1>
</BODY>
</HTML>"
许多编程语言中的变量在使用前必须声明或者定义,shell脚本在这一点上非常宽松。当shell遇到一个变量时它会自动地创建该变量,这方便我们编写脚本,同时也要求我们不能拼写错误,否则使用时会得到一个空变量。
给变量赋值:
variable=value
variable是变量的名字,value是一个字符串。不同于一些其它的编程语言,shell 不会在乎变量值的类型;它把它们都看作是字符串。注意在赋值过程中,变量名,等号和变量值之间必须没有空格。
和其它编程语言一样,shell脚本的变量也分为全局变量和局部变量。在shell函数中通过在变量前加上单词local来定义局部变量,作用域局限于定义它的函数,函数执行完,该局部变量也就不存在了。(在shell函数中定义的变量如果前面没有local单词,则该变量依然是全局变量)
shell函数
在shell脚本中用一行代码实现一个功能的方法有两种。我们可以分别编写一个脚本,并把它们放置到 环境变量 PATH 所列出的目录下(如我们编写了一个shell脚本hello_world,并放在PATH路径,则在其它脚本里可以直接调用hello_world来实现其功能),或者我们也可以把这些脚本作为 shell 函数嵌入到我们的程序中。 Shell 函数有两种语法形式:
function name {
commands
return
}
name () {
commands
return
}
name 是函数名,commands 是一系列包含在函数中的命令。上面两种形式等价,可以交替使用。
#!/bin/bash
function funct
{
echo "Step 2"
return
}
echo "Step 1"
funct
echo "Step 3"
注意为了使函数调用被识别出是 shell 函数,而不是被解释为外部程序的名字,所以在脚本中 shell 函数定义必须出现在函数调用之前。
一个函数必须至少包含一条命令。这条 return 命令(是可选的)满足要求。
if分支语句
if分支语句结构为:
if [ expression ]; then
commands
else
commands
fi
如
a=5
if [ $a = 5 ]; then
echo "a=5"
else
echo "a!=5"
fi
注意if判断条件用到了多个空格,其中方括号[]左半部分[左右必须各有一个空格,右半部分]的左边必须有一个空格;等号=左右各有一个空格
if语句可以多重嵌套,即commands语句可以包含有另外的if语句。
if语句判断expression是否为真涉及到退出状态这个概念。
当命令执行完毕后,命令(包括我们编写的脚本和 shell 函数)会给系统发送一个值,叫做退出状态。 这个值是一个 0 到 255 之间的整数,说明命令执行成功或是失败。按照惯例,一个零值说明成功,其它所有值说明失败。If 语句真正做的事情是计算命令执行成功或失败,如果 if 之后跟随一系列命令,则将计算列表中的最后一个命令
[me@linuxbox ~]$ if false; true; then echo "It's true."; fi
It's true.
if语句判断[ expression ]可以使用test expression代替,两者等价,但[ expression ]较为常用
if语句的文件判断expression有:
表达式 | 如果为真 |
---|---|
file1 -ef file2 | file1 和 file2 拥有相同的索引号(通过硬链接两个文件名指向相同的文件)。 |
file1 -nt file2 | file1新于 file2。 |
file1 -ot file2 | file1早于 file2。 |
-b file | file 存在并且是一个块(设备)文件。 |
-c file | file 存在并且是一个字符(设备)文件。 |
-d file | file 存在并且是一个目录。 |
-e file | file 存在。 |
-f file | file 存在并且是一个普通文件。 |
-g file | file 存在并且设置了组 ID。 |
-G file | file 存在并且由有效组 ID 拥有。 |
-k file | file 存在并且设置了它的“sticky bit”。 |
-L file | file 存在并且是一个符号链接。 |
-O file | file 存在并且由有效用户 ID 拥有。 |
-p file | file 存在并且是一个命名管道。 |
-r file | file 存在并且可读(有效用户有可读权限)。 |
-s file | file 存在且其长度大于零。 |
-S file | file 存在且是一个网络 socket。 |
-t fd | fd 是一个定向到终端/从终端定向的文件描述符 。 这可以被用来决定是否重定向了标准输入/输出错误。 |
-u file | file 存在并且设置了 setuid 位。 |
-w file | file 存在并且可写(有效用户拥有可写权限)。 |
-x file | file 存在并且可执行(有效用户有执行/搜索权限)。 |
字符串判断expression有:
表达式 | 如果为真... |
---|---|
string | string 不为 null。 |
-n string | 字符串 string 的长度大于零。 |
-z string | 字符串 string 的长度为零。 |
string1 = string2 string1 == string2 | string1 和 string2 相同. 单或双等号都可以,不过双等号更受欢迎。 |
string1 != string2 | string1 和 string2 不相同。 |
string1 > string2 | sting1 排列在 string2 之后。 |
string1 < string2 | string1 排列在 string2 之前。 |
警告:这个 > 和 <表达式操作符必须用引号引起来(或者是用反斜杠转义), 当与 test 一块使用的时候。如果不这样,它们会被 shell 解释为重定向操作符,造成潜在地破坏结果。
整形判断expression有:
表达式 | 如果为真... |
---|---|
integer1 -eq integer2 | integer1 等于 integer2. |
integer1 -ne integer2 | integer1 不等于 integer2. |
integer1 -le integer2 | integer1 小于或等于 integer2. |
integer1 -lt integer2 | integer1 小于 integer2. |
integer1 -ge integer2 | integer1 大于或等于 integer2. |
integer1 -gt integer2 | integer1 大于 integer2. |
目前的 bash 版本包括一个复合命令,作为加强的 test 命令替代物。它使用以下语法:
[[ expression ]]
它的功能类似于原来的[ expression ],只是增加了一些功能。
功能1:增加了一个重要的新的字符串表达式
string1 =~ regex
其返回值为真,如果 string1匹配扩展的正则表达式 regex。这就为执行比如数据验证等任务提供了许多可能性。
功能2:增加了==操作符来支持类型匹配
除了 [[ ]] 复合命令之外,bash 也提供了 (( )) 复合命令,其有利于操作整数。它支持一套完整的算术计算。
(( ))被用来执行算术真测试。如果算术计算的结果是非零值,则一个算术真测试值为真。
[me@linuxbox ~]$ if ((1)); then echo "It is true."; fi
It is true.
[me@linuxbox ~]$ if ((0)); then echo "It is true."; fi
[me@linuxbox ~]$
我们可以使用罗技操作符来表达更加复杂的表达式
操作符 | 测试 | [[ ]] and (( )) |
---|---|---|
AND | -a | && |
OR | -o | || |
NOT | ! | ! |
语法:
command1 && command2
command1 || command2
对于 && 操作符,先执行 command1,如果并且只有如果 command1 执行成功后, 才会执行 command2。对于 || 操作符,先执行 command1,如果并且只有如果 command1 执行失败后, 才会执行 command2。
读取键盘输入
read命令被用来从标准输入读取单行数据,语法格式如下:
read [-options] [variable...]
这里的 options 是下面列出的可用选项中的一个或多个,也可不用; variable 是用来存储输入数值的一个或多个变量名。 如果没有提供变量名,shell 变量 REPLY 会包含数据行。
选项 | 说明 |
---|---|
-a array | 把输入赋值到数组 array 中,从索引号零开始。 |
-d delimiter | 用字符串 delimiter 中的第一个字符指示输入结束,而不是一个换行符。 |
-e | 使用 Readline 来处理输入。这使得与命令行相同的方式编辑输入。 |
-n num | 读取 num 个输入字符,而不是整行。 |
-p prompt | 为输入显示提示信息,使用字符串 prompt。 |
-r | Raw mode. 不把反斜杠字符解释为转义字符。 |
-s | Silent mode. 不会在屏幕上显示输入的字符。当输入密码和其它确认信息的时候,这会很有帮助。 |
-t seconds | 超时. 几秒钟后终止输入。read 会返回一个非零退出状态,若输入超时。 |
-u fd | 使用文件描述符 fd 中的输入,而不是标准输入。 |
使用例子:
#!/bin/bash
echo -n "Please enter an integer -> "
read int
echo "value that enter is $int"
使用带有 -n 选项的 echo 命令,其输出结果会删除末尾的换行符,即不换行。 然后使用 read 来读入变量 int 的数值。
read 可以给多个变量赋值
#!/bin/bash
echo -n "Please enter three integers -> "
read int1 int2 int3
echo "$int1 $int2 $int3"
如果 read 命令接收到变量值数目少于期望的数字,那么额外的变量值为空;如果接收到过多输入,则多余的输入数据会被包含到最后一个变量中。如果 read 命令之后没有列出变量名,则一个 shell 变量,REPLY,将会包含 所有的输入:
#!/bin/bash
echo -n "Please enter an integers -> "
read
echo "$REPLY"
使用各种各样的选项,我们能用 read 完成有趣的事情。
#!/bin/bash
# read-single: read multiple values into default variable
read -p "Enter one or more values > "
echo "REPLY = $REPLY"
我们使用read获取标准输入时,通常使用一个或多个空格来分开输入的单词,并被read赋值给单独的变量。这种行为是由一个shell变量IFS(内部字符分隔符)配置的,IFS 的默认值包含一个空格,一个 tab,和一个换行符,每一个都会把字段分割开。我们可以修改IFS的值,改变分隔符。
#!/bin/bash
IFS=","
read -p "please enter three integers->" int1 int2 int3
echo "$int1"
echo "$int2"
echo "$int3"
此时输入的数值应该以逗号分隔,否则输入的所有数值将赋给int1.
常用的做法是先存储 IFS 的值,然后赋一个新值给IFS,再执行 read 命令,最后把 IFS 恢复原值。
while循环
while 命令的语法是:
while [ expression ]; do
commands
done
和 if 一样, while 计算一系列命令的退出状态。只要退出状态为零,它就执行循环内的命令。因此,前面的if语句判断表达式也可用于while语句。
bash 提供了两个内部命令来控制while循环。 break 命令立即终止一个循环, 且程序继续执行循环之后的语句。 continue 命令导致程序跳过循环中剩余的语句,且程序继续执行 下一次循环。
shell脚本编程中还有一个命令与while功能相似,until命令,语法格式如下:
until [ expression ]; do
commands
done
当遇到一个非零退出状态时,while语句退出循环;当遇到一个零退出状态时,until语句退出循环。也就是说,当表达式为真时,while语句执行循环;当表达式为假时,until语句执行循环。
case分支语句
case语句的语法格式为:
case word in
pattern commands ;; ...
esac
如:
#!/bin/bash
read -p "please enter an integer->" int
case $int in
0) echo "int=0";;
1) echo "int=1";;
*) echo "not equal to 0 or 1";;
esac
当与之相匹配的模式找到之后,就会执行与该模式相关联的命令。若找到一个模式之后,就不会再继续寻找。
这里 case 语句使用的模式和路径展开中使用的那些是一样的。模式以一个 “)” 为终止符。这里是一些常用的模式类型:
模式 | 描述 |
---|---|
a) | 若单词为 “a”,则匹配 |
[[:alpha:]]) | 若单词是一个字母字符,则匹配 |
???) | 若单词只有3个字符,则匹配 |
*.txt) | 若单词以 “.txt” 字符结尾,则匹配 |
*) | 匹配任意单词。把这个模式做为 case 命令的最后一个模式,是一个很好的做法, 可以捕捉到任意一个与先前模式不匹配的数值;也就是说,捕捉到任何可能的无效值。 |
可以使用竖线字符作为分隔符,把多个模式结合起来,这就创建了一个 “或” 条件模式。这对于处理诸如大小写字符很有用处。
上面展示的case语句找到一个匹配的模式后就执行该模式相关的命令,执行完就退出case语句。若想case语句能够匹配多个选项,可以使用;;&来作为行动的终止符
#!/bin/bash
# test a character
read -n 1 -p "Type a character > "
case $REPLY in
[[:upper:]]) echo "'$REPLY' is upper case." ;;&
[[:lower:]]) echo "'$REPLY' is lower case." ;;&
[[:alpha:]]) echo "'$REPLY' is alphabetic." ;;&
[[:digit:]]) echo "'$REPLY' is a digit." ;;&
[[:graph:]]) echo "'$REPLY' is a visible character." ;;&
[[:punct:]]) echo "'$REPLY' is a punctuation symbol." ;;&
[[:space:]]) echo "'$REPLY' is a whitespace character." ;;&
[[:xdigit:]]) echo "'$REPLY' is a hexadecimal digit." ;;&
esac
添加的 “;;&” 允许 case 语句继续执行下一条测试,而不是简单地终止运行。
for循环
传统的for命令语法格式为:
for variable [in words]; do
commands
done
这里的 variable 是一个变量的名字,这个变量在循环执行期间会增加,words 是一个可选的条目列表, 其值会按顺序赋值给 variable,commands 是在每次循环迭代中要执行的命令。
如:
#!/bin/bash
for i in {A..D};do
echo "$i"
done
如果省略掉 for 命令的可选项 words 部分,for 命令会默认处理位置参数,从$1开始(后面有介绍)。
最新版本的 bash 已经添加了第二种格式的 for 命令语法,该语法相似于 C 语言中的 for 语法格式。
for (( expression1; expression2; expression3 )); do
commands
done
这里的 expression1,expression2,和 expression3 都是算术表达式,commands 是每次循环迭代时要执行的命令。expression1 用来初始化循环条件,expression2 用来决定循环结束的时机,还有在每次循环迭代的末尾会执行 expression3。
#!/bin/bash
# simple_counter : demo of C style for command
for (( i=0; i<5; i=i+1 )); do
echo $i
done
位置参数
位置参数用于接收和处理命令行选项和参数,它是一个变量集合,这些变量按照从0到9命名,举例说明:
#!/bin/bash
echo "$0"
echo "$1"
echo "$2"
假设该脚本名为posit_param,位于/root/bin目录下,在终端执行命令:posit_param param1 param2,则结果如下:
[root@localhost ~]# posit_param param1 param2
/root/bin/posit_param
param1
param2
实际上通过参数展开方式你可以访问的参数个数多于9个。只要指定一个大于9的数字,用花括号把该数字括起来就可以。 例如 ${10}, ${55}, ${211},等等。
另外 shell 还提供了一个名为 $#,可以得到命令行参数个数的变量。
shell提供了一个shift命令来处理大量的位置参数。执行一次 shift 命令, 就会导致所有的位置参数 “向下移动一个位置”。事实上,用 shift 命令也可以 处理只有一个参数的情况(除了其值永远不会改变的变量 $0),即每次 shift 命令执行的时候,变量 $2 的值会移动到变量 $1 中,变量 $3 的值会移动到变量 $2 中,依次类推。 变量 $# 的值也会相应的减1。
shell 提供了两种特殊的参数。他们二者都能扩展成完整的位置参数列表,但略有不同。
参数 | 描述 |
---|---|
$* | 展开成一个从1开始的位置参数列表。当它被用双引号引起来的时候,展开成一个由双引号引起来 的字符串,包含了所有的位置参数,每个位置参数由 shell 变量 IFS 的第一个字符(默认为一个空格)分隔开。 |
$@ | 展开成一个从1开始的位置参数列表。当它被用双引号引起来的时候, 它把每一个位置参数展开成一个由双引号引起来的分开的字符串。 |
下面这个脚本在程序中展示了这些特殊参数:
#!/bin/bash
# posit-params3 : script to demonstrate $* and $@
print_params () {
echo "\$1 = $1"
echo "\$2 = $2"
echo "\$3 = $3"
echo "\$4 = $4"
}
pass_params () {
echo -e "\n" '$* :'; print_params $*
echo -e "\n" '"$*" :'; print_params "$*"
echo -e "\n" '$@ :'; print_params $@
echo -e "\n" '"$@" :'; print_params "$@"
}
pass_params "word" "words with spaces"
[me@linuxbox ~]$ posit-param3
$* :
$1 = word
$2 = words
$3 = with
$4 = spaces
"$*" :
$1 = word words with spaces
$2 =
$3 =
$4 =
$@ :
$1 = word
$2 = words
$3 = with
$4 = spaces
"$@" :
$1 = word
$2 = words with spaces
$3 =
$4 =
在Linux中,单引号和双引号的一个重要区别是双引号能保有变量的内容,而单引号只能是字符,而不会有特殊符号。
字符串展开
${#parameter}
展开成由 parameter 所包含的字符串的长度。通常,parameter 是一个字符串;然而,如果 parameter 是 @ 或者是 * 的话, 则展开结果是位置参数的个数。
[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo "${#foo}"
20
${parameter:offset}
${parameter:offset:length}
这些展开用来从 parameter 所包含的字符串中提取一部分字符。提取的字符始于 第 offset 个字符(从字符串开头算起)直到字符串的末尾,除非指定提取的长度。
[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo ${foo:5}
string is long.
[me@linuxbox ~]$ echo ${foo:5:6}
string
最新的 bash 版本已经支持字符串的大小写转换了。bash 有四个参数展开和 declare 命令的两个选项来支持大小写转换。
declare 命令可以用来把字符串规范成大写或小写字符。使用 declare 命令,我们能强制一个 变量总是包含所需的格式,无论如何赋值给它。
declare -u upper
declare -l lower
其中upper总为大写字符,lower总为小写字符
执行大小写转换的四个参数展开为:
格式 | 结果 |
---|---|
${parameter,,} | 把 parameter 的值全部展开成小写字母。 |
${parameter,} | 仅仅把 parameter 的第一个字符展开成小写字母。 |
${parameter^^} | 把 parameter 的值全部转换成大写字母。 |
${parameter^} | 仅仅把 parameter 的第一个字符转换成大写字母(首字母大写)。 |
数组
数组变量就像其它 bash 变量一样,当被访问的时候,它们会被自动地创建。因此,不需要特意定义一个数组变量,只需直接对数组变量赋值并使用。当然,也可以通过declare来定义一个数组
[me@linuxbox ~]$ declare -a a
使用 -a 选项,declare 命令的这个例子创建了数组 a
有两种方式可以给数组赋值。单个值赋值使用以下语法:
name[subscript]=value
这里的 name 是数组的名字,subscript 是一个大于或等于零的整数(或算术表达式)。注意数组第一个元素的下标是0, 而不是1。数组元素的值可以是一个字符串或整数。
多个值赋值使用下面的语法:
多个值赋值使用下面的语法:
name=(value1 value2 ...)
如:
days=(Sun Mon Tue Wed Thu Fri Sat)
还可以通过指定下标,把值赋给数组中的特定元素:
days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu [5]=Fri [6]=Sat)
注意变量展开时应使用大括号:
echo ${days[0]}
有许多常见的数组操作。比方说删除数组,确定数组大小,排序,等等。
下标 * 和 @ 可以被用来访问数组中的每一个元素。与位置参数一样,@ 表示法在两者之中更有用处。
[me@linuxbox ~]$ animals=("a dog" "a cat" "a fish")
[me@linuxbox ~]$ for i in ${animals[*]}; do echo $i; done
a
dog
a
cat
a
fish
[me@linuxbox ~]$ for i in ${animals[@]}; do echo $i; done
a
dog
a
cat
a
fish
[me@linuxbox ~]$ for i in "${animals[*]}"; do echo $i; done
a dog a cat a fish
[me@linuxbox ~]$ for i in "${animals[@]}"; do echo $i; done
a dog
a cat
a fish
使用参数展开,我们能够确定数组元素的个数
[me@linuxbox ~]$ a[100]=foo
[me@linuxbox ~]$ echo ${#a[@]} # number of array elements
1
[me@linuxbox ~]$ echo ${#a[100]} # length of element 100
3
通过使用 += 赋值运算符, 我们能够自动地把值附加到数组末尾。
[me@linuxbox~]$ foo=(a b c)
[me@linuxbox~]$ echo ${foo[@]}
a b c
[me@linuxbox~]$ foo+=(d e f)
[me@linuxbox~]$ echo ${foo[@]}
a b c d e f
删除一个数组,使用 unset 命令:
[me@linuxbox ~]$ foo=(a b c d e f)
[me@linuxbox ~]$ unset foo
也可以使用 unset 命令删除单个的数组元素:
[me@linuxbox~]$ foo=(a b c d e f)
[me@linuxbox~]$ unset 'foo[2]'