Shell脚本入门指南
1. 什么是Shell脚本
如果你能在shell中输入命令,那么你就可以创建shell脚本(也称为Bourne shell脚本)。Shell脚本是一系列写在文件中的命令,shell会像你在终端中输入这些命令一样从文件中读取并执行它们。
1.1 Shell脚本基础
Bourne shell脚本通常以如下行开头,这表明
/bin/sh
程序将执行脚本文件中的命令。(确保脚本文件开头没有空格)
#!/bin/sh
#!
部分被称为shebang,在其他脚本中也会经常看到。在
#!/bin/sh
这行之后可以列出任何你想让shell执行的命令。例如:
#!/bin/sh
#
# 显示一条消息,然后执行ls命令
echo About to run the ls command.
ls
注意
:行首的
#
字符表示该行是注释,即shell会忽略
#
后面的所有内容。可以使用注释来解释脚本中难以理解的部分。
创建好shell脚本并设置好权限后,可以将文件放在命令路径中的某个目录下,然后在命令行中执行脚本名来运行它。如果脚本在当前工作目录中,也可以执行
./script
,或者使用完整的路径名。
与Unix系统中的任何程序一样,需要为shell脚本文件设置可执行位,同时也需要设置可读位,以便shell读取文件。最简单的方法如下:
$ chmod +rx script
chmod
命令允许其他用户读取和执行脚本。如果不想这样做,可以使用绝对模式
700
作为替代。
1.2 Shell脚本的局限性
Bourne shell相对容易处理命令和文件。然而,shell脚本只是Unix的一种编程工具,虽然脚本有相当的效能,但也有其局限性。
shell脚本的一个主要优点是可以简化和自动化那些原本需要在shell提示符下执行的任务,例如处理批量文件。但是,如果尝试分离字符串、进行重复的算术处理、访问复杂的数据库,或者想要使用复杂的函数和控制结构,最好使用Python、Perl或awk等脚本语言,甚至是C等编译语言。
此外,要注意shell脚本的大小,保持其紧凑。Bourne shell脚本并非设计用于大型脚本。
2. 引号和字面量的使用
在使用shell和脚本时,最容易混淆的一点是何时使用引号(quotes)和其他标点符号,以及为什么有时需要这样做。
2.1 字面量
使用引号时,通常是为了创建一个字面量,即希望shell原封不动地传递给命令行的字符串。除了前面看到的
$
示例,其他类似情况还包括想将字符
*
传递给
grep
等命令,而不是让shell进行扩展,以及在命令中需要使用分号
;
的情况。
在创建脚本和在命令行中工作时,只需记住shell执行命令时的情况:
1. 在执行命令之前,shell会查找变量、通配符和其他替换项,并在出现时进行替换。
2. shell将替换结果传递给命令。
例如,假设要查找
/etc/passwd
中所有与正则表达式
r.*t
匹配的条目。以下命令通常可以正常工作:
$ grep r.*t /etc/passwd
但有时该命令会神秘地失败。原因可能在于当前目录。如果该目录包含名为
r.input
和
r.output
的文件,shell会将
r.*t
扩展为
r.input r.output
,并创建如下命令:
$ grep r.input r.output /etc/passwd
避免此类问题的秘诀是首先识别可能带来问题的字符,然后应用正确类型的引号来保护它们。
2.2 单引号
创建字面量并让shell不改变字符串的最简单方法是将整个字符串放在单引号中,如下
grep
和字符
*
的示例:
$ grep 'r.*t' /etc/passwd
对于shell来说,两个单引号之间的所有字符(包括空格)组成一个单独的参数。因此,以下命令将不起作用,因为它要求
grep
命令在标准输出中查找字符串
r.*t /etc/passwd
(因为
grep
只有一个参数):
$ grep 'r.*t /etc/passwd'
当需要使用字面量时,应首先使用单引号,这样可以确保shell不会尝试进行任何替换。不过,有时可能需要更多的灵活性,这时可以使用双引号。
2.3 双引号
双引号
"
的作用与单引号类似,不同之处在于shell会扩展双引号内出现的任何变量。可以通过执行以下命令,然后将双引号替换为单引号并再次执行来感受这种差异:
$ echo "There is no * in my path: $PATH"
执行该命令时,会发现shell对
$PATH
进行了替换,但没有对
*
进行替换。
注意 :如果在显示大量文本时使用双引号,可以考虑使用here文档。
2.4 传递单引号字面量
在使用Bourne shell的字面量时,一个复杂的部分是如何将单引号字面量传递给命令。一种方法是在单引号字符前加上反斜杠:
$ echo I don\'t like contractions inside shell scripts.
反斜杠和引号必须出现在任何单引号对之外,像
'don\'t
这样的字符串会导致语法错误。
还可以将单引号放在双引号内,如下示例(输出将与前一个命令相同):
$ echo “I don't like contractions inside shell scripts.”
如果遇到困难,需要一个通用规则来将整个字符串用引号括起来而不进行替换,可以按照以下步骤操作:
1. 将所有单引号
'
替换为
'\''
(单引号、反斜杠、单引号、单引号)。
2. 将整个字符串放在单引号中。
例如,可以这样使用引号处理不常见的字符串
this isn't a forward slash: \
:
$ echo 'this isn'\''t a forward slash: \'
注意
:再次强调,将字符串放在引号中时,shell会将引号内的所有内容视为一个单独的参数。因此,
a b c
被视为三个参数,而
a "b c"
只表示两个参数。
3. 特殊变量
大多数shell脚本能够理解命令行参数并与执行的命令进行交互。为了使脚本不再只是简单的命令列表,而是成为灵活的shell脚本程序,需要了解Bourne shell的特殊变量。这些特殊变量与其他shell变量类似,但某些变量的值不能更改。
3.1 单个参数:$1, $2, …
$1
、
$2
等所有以非零正整数命名的变量包含脚本的参数值,即参数。例如,假设以下脚本名为
pshow
:
#!/bin/sh
echo First argument: $1
echo Third argument: $3
可以按如下方式执行脚本,查看参数的显示情况:
$ ./pshow one two three
First argument: one
Third argument: three
shell中包含的
shift
命令可以与参数变量一起使用,以移除第一个参数
$1
并推进其余参数。具体来说,
$2
变为
$1
,
$3
变为
$2
,依此类推。例如,假设以下脚本名为
shiftex
:
#!/bin/sh
echo Argument: $1
shift
echo Argument: $1
shift
echo Argument: $1
按如下方式执行它,查看其工作方式:
$ ./shiftex one two three
Argument: one
Argument: two
Argument: three
3.2 参数数量:$
变量
$#
存储传递给脚本的参数数量,在使用
shift
循环遍历参数时特别重要。当
$#
等于
0
时,将没有更多参数,因此
$1
将为空。
3.3 所有参数:$@
变量
$@
表示脚本的所有参数,对于将这些参数传递给脚本中的命令非常有用。例如,Ghostscript (
gs
)命令通常很长且复杂。假设想创建一个快捷方式,以150 dpi的分辨率将PostScript文件光栅化,并使用标准输出流,同时为
gs
传递其他选项留有余地。可以创建如下脚本,允许在命令行中添加额外选项:
#!/bin/sh
gs -q -dBATCH -dNOPAUSE -dSAFER -sOutputFile=- -sDEVICE=pnmraw $@
注意
:如果shell脚本中的一行对于文本编辑器来说太长,可以使用反斜杠
\
进行分割。例如,上述脚本可以修改为:
#!/bin/sh
gs -q -dBATCH -dNOPAUSE -dSAFER \
-sOutputFile=- -sDEVICE=pnmraw $@
3.4 脚本名:$0
变量
$0
包含脚本的名称,对于生成诊断消息很有用。例如,假设脚本需要报告存储在变量
$BADPARM
中的无效参数。可以使用以下行显示诊断消息,使脚本名出现在错误消息中:
echo $0: bad option $BADPARM
所有诊断消息都应该发送到标准错误。记住,如前面所述,可以使用
2>&1
将标准错误重定向到标准输出。要将输出发送到标准错误,可以使用
1>&2
。例如:
echo $0: bad option $BADPARM 1>&2
3.5 进程ID:$$
变量
$$
存储与shell相关的进程ID。
3.6 退出代码:$?
变量
$?
存储shell执行的最后一个命令的退出代码。退出代码对于掌握shell脚本至关重要,将在后面详细讨论。
4. 退出代码
当Unix程序终止时,它会为启动它的父进程留下一个退出代码。退出代码是一个数字,有时也称为错误代码或输出值。当退出代码等于零
0
时,表示程序执行没有问题。但是,如果程序出现错误,通常会以非零数字终止(但并非总是如此)。
shell将最后一个命令的退出代码存储在特殊变量
$?
中,可以在shell提示符下进行检查:
$ ls / > /dev/null
$ echo $?
0
$ ls /asdfasdf > /dev/null
ls: /asdfasdf: No such file or directory
$ echo $?
1
可以看到,成功的命令返回
0
,失败的命令返回
1
。
如果打算使用命令的退出代码,应该在执行命令后立即使用或存储该代码。例如,如果连续两次执行
echo $?
,第二个命令的输出将始终为
0
,因为第一个
echo
命令成功终止。
在编写不会正常结束脚本的shell代码时,可以使用
exit 1
等将退出代码
1
返回给执行脚本的父进程。可以根据不同情况使用不同的数字。
需要注意的是,一些程序如
diff
和
grep
使用非零退出代码来表示正常情况。例如,
grep
如果找到匹配的内容返回
0
,否则返回
1
。对于这些程序,退出代码
1
不是错误,
grep
和
diff
使用退出代码
2
表示真正的问题。如果认为某个程序使用非零退出代码表示成功,可以查看其手册页,退出代码通常在
EXIT VALUE
(退出值)或
DIAGNOSTICS
(诊断)部分进行解释。
5. 条件语句
Bourne shell有用于条件语句的特殊结构,如
if/then/else
和
case
语句。
5.1 基本的if语句
以下是一个简单的带有
if
条件的脚本示例,用于检查脚本的第一个参数是否为
hi
:
#!/bin/sh
if [ $1 = hi ]; then
echo 'The first argument was "hi"'
else
echo -n 'The first argument was not "hi" -- '
echo It was '"'$1'"'
fi
上述脚本中的
if
、
then
、
else
和
fi
是shell的关键字,其余部分是命令。需要注意的是,
[ $1 = "hi" ]
中的
[
实际上是Unix系统中的一个程序,而不是shell的特殊语法。所有Unix系统都有一个名为
[
的命令,用于在shell脚本中进行条件测试,该程序也称为
test
。
整个过程的工作方式如下:
1. shell执行
if
关键字后面的命令,并获取该命令的退出代码。
2. 如果退出代码等于
0
,shell将执行
then
关键字后面的命令,直到遇到
else
或
fi
关键字。
3. 如果退出代码不等于
0
且有
else
子句,shell将执行
else
关键字后面的命令。
4. 条件语句以
fi
结束。
5.2 处理空参数列表
上述条件语句存在一个小问题,因为一个常见的错误是
$1
可能为空,即用户可能没有提供参数。没有参数时,测试将变为
[ = hi ]
,
[
命令将因错误而中止。可以通过以下两种常见方式之一将参数放在引号中来解决这个问题:
if [ "$1" = hi ]; then
if [ x"$1" = x"hi" ]; then
5.3 使用其他命令进行测试
if
后面的内容始终是一个命令。因此,如果想将
then
关键字放在同一行,需要在测试命令后使用分号
;
。如果不使用分号,shell会将
then
作为参数传递给测试命令。如果不喜欢分号,也可以将
then
关键字放在单独的一行。
可以使用其他命令代替
[
进行测试,例如以下使用
grep
的示例:
#!/bin/sh
if grep -q daemon /etc/passwd; then
echo The daemon user is in the passwd file.
else
echo There is a big problem. daemon is not in the passwd file.
fi
5.4 elif语句
还有一个
elif
关键字,允许链接
if
条件语句,如下所示:
#!/bin/sh
if [ "$1" = "hi" ]; then
echo 'The first argument was "hi"'
elif [ "$2" = "bye" ]; then
echo 'The second argument was "bye"'
else
echo -n 'The first argument was not "hi" and the second was not "bye"-- '
echo They were '"'$1'"' and '"'$2'"'
fi
不过,不要对
elif
过于兴奋,因为后面将看到的
case
结构通常更合适。
5.5 逻辑结构 && 和 ||
偶尔会看到两种单行快速条件结构:
&&
(“and”)和
||
(“or”)。
&&
结构的工作方式如下:
comando1 && comando2
在这种情况下,shell执行
comando1
,如果退出代码等于
0
,则shell也会执行
comando2
。
||
结构类似,如果
||
前面的命令返回非零退出代码,shell将执行第二个命令。
&&
和
||
结构通常用于
if
测试,在这两种情况下,最后执行的命令的退出代码决定了shell如何处理条件语句。对于
&&
结构,如果第一个命令失败,shell将使用其退出代码进行
if
语句判断;如果第一个命令成功,shell将使用第二个命令的退出代码进行条件判断。对于
||
结构,如果第一个命令成功,shell将使用其退出代码;如果第一个命令失败,shell将使用第二个命令的退出代码。
例如:
#!/bin/sh
if [ "$1" = hi ] || [ "$1" = bye ]; then
echo 'The first argument was "'$1'"'
fi
如果条件语句包含测试命令
[
,可以使用
-a
和
-o
代替
&&
和
||
。
5.6 测试条件
[
命令的退出代码为
0
表示测试为真,非零表示测试失败。已经知道可以使用
[ str1 = str2 ]
测试字符串相等性。但要记住,shell脚本非常适合对整个文件进行操作,因为最有用的
[
测试涉及文件属性。
例如,以下行检查
arquivo
是否为普通文件(而不是目录或特殊文件):
[ -f arquivo ]
在脚本中,可以在类似以下的循环中看到
-f
测试,该循环测试当前工作目录中的所有项:
for filename in *; do
if [ -f $filename ]; then
ls -l $filename
file $filename
else
echo $filename is not a regular file.
fi
done
可以通过在测试参数前放置
!
操作符来反转测试。例如,
[ ! -f arquivo ]
如果
arquivo
不是普通文件将返回真。
此外,
-a
和
-o
标志分别对应逻辑运算符“and”(和)和“or”(或)。
测试操作可以分为三大类:文件测试、字符串测试和算术测试。
info
手册包含完整的在线文档,
test(1)
手册页包含快速参考。
5.6.1 文件测试
大多数文件测试(如
-f
)称为一元操作,因为只需要一个参数:要测试的文件。以下是两个重要的文件测试:
-
-e
:如果文件存在则返回真。
-
-s
:如果文件不为空则返回真。
还有一些操作可以检查文件的类型,例如:
| 操作符 | 测试内容 |
| ---- | ---- |
|
-f
| 普通文件 |
|
-d
| 目录 |
|
-h
| 符号链接 |
|
-b
| 块设备 |
|
-c
| 字符设备 |
|
-p
| 命名管道 |
|
-S
| 套接字 |
此外,还有一些一元操作可以检查文件的权限:
| 操作符 | 权限 |
| ---- | ---- |
|
-r
| 可读 |
|
-w
| 可写 |
|
-x
| 可执行 |
|
-u
| Setuid |
|
-g
| Setgid |
|
-k
| “Sticky” |
最后,有三个二元操作符(需要两个文件作为参数的测试)用于文件测试,但不太常见。例如,
[ arquivo1 -nt arquivo2 ]
如果
arquivo1
的修改日期比
arquivo2
新,则返回真。
-ot
(older than,更旧)操作符则相反。
-ef
用于比较两个文件,如果它们共享inode编号和设备,则返回真。
5.6.2 字符串测试
已经看到字符串二元操作符
=
,如果操作数相等则返回真。操作符
!=
如果操作数不相等则返回真。此外,还有两个字符串一元操作:
-
-z
:如果参数为空则返回真(
[ -z "" ]
返回
0
)。
-
-n
:如果参数不为空则返回真(
[ -n "" ]
返回
1
)。
5.6.3 算术测试
需要注意的是,等号
=
用于检查字符串相等性,而不是数值相等性。因此,
[ 1 = 1 ]
返回
0
(真),但
[ 01 = 1 ]
返回假。在处理数字时,应使用
-eq
代替等号:
[ 01 -eq 1 ]
返回真。以下是完整的数值比较操作符列表:
| 操作符 | 返回真的条件 |
| ---- | ---- |
|
-eq
| 等于 |
|
-ne
| 不等于 |
|
-lt
| 小于 |
|
-gt
| 大于 |
|
-le
| 小于等于 |
|
-ge
| 大于等于 |
5.7 使用case进行字符串匹配
case
关键字构成了另一种非常有用的条件结构,用于进行字符串匹配。不过文档中未给出具体示例,后续可进一步深入学习其使用方法。
通过以上内容,我们对shell脚本的基础、特殊变量、退出代码、条件语句等方面有了较为详细的了解。在实际应用中,可以根据具体需求灵活运用这些知识来编写高效的shell脚本。
6. 循环结构
在shell脚本中,循环结构是实现重复执行任务的重要工具。常见的循环结构有
for
循环和
while
循环。
6.1 for循环
for
循环用于遍历一个列表中的元素,并对每个元素执行相同的操作。其基本语法如下:
for variable in list; do
commands
done
例如,遍历当前目录下的所有文件,并打印它们的文件名:
for filename in *; do
echo $filename
done
在这个例子中,
*
是一个通配符,表示当前目录下的所有文件和文件夹。
for
循环会依次将每个文件或文件夹的名称赋值给变量
filename
,并执行
echo
命令打印出来。
6.2 while循环
while
循环会在条件为真的情况下不断执行一组命令。其基本语法如下:
while condition; do
commands
done
例如,以下脚本会不断读取用户输入,直到用户输入
quit
为止:
#!/bin/sh
input=""
while [ "$input" != "quit" ]; do
echo "Please enter a command (or 'quit' to exit):"
read input
echo "You entered: $input"
done
在这个例子中,
while
循环的条件是
[ "$input" != "quit" ]
,即只要用户输入的内容不等于
quit
,循环就会继续执行。每次循环中,脚本会提示用户输入命令,读取用户输入,并打印出来。
6.3 循环嵌套
在shell脚本中,也可以使用循环嵌套,即在一个循环中包含另一个循环。例如,以下脚本会打印一个乘法表:
#!/bin/sh
for i in 1 2 3 4 5; do
for j in 1 2 3 4 5; do
result=$((i * j))
echo "$i x $j = $result"
done
echo
done
在这个例子中,外层的
for
循环控制被乘数
i
,内层的
for
循环控制乘数
j
。每次内层循环结束后,会打印一个空行,以分隔不同的被乘数。
7. 函数
函数是shell脚本中组织代码的重要方式,可以将一些常用的操作封装成函数,提高代码的复用性和可读性。
7.1 函数定义
函数的定义语法如下:
function_name() {
commands
}
例如,定义一个简单的函数来计算两个数的和:
add_numbers() {
result=$(( $1 + $2 ))
echo $result
}
在这个例子中,
add_numbers
是函数名,
$1
和
$2
是函数的参数,分别表示两个要相加的数。函数内部计算两个数的和,并将结果通过
echo
命令返回。
7.2 函数调用
调用函数时,只需要使用函数名,并传递相应的参数即可。例如:
sum=$(add_numbers 3 5)
echo "The sum is: $sum"
在这个例子中,调用
add_numbers
函数,传递参数
3
和
5
,并将函数的返回值赋值给变量
sum
,最后打印出结果。
7.3 函数的作用域
在shell脚本中,函数内部定义的变量默认是全局变量,即可以在函数外部访问。如果需要定义局部变量,可以使用
local
关键字。例如:
function test_function() {
local local_variable="This is a local variable"
echo $local_variable
}
test_function
echo $local_variable # 这行代码会输出空,因为local_variable是局部变量
在这个例子中,
local_variable
是函数
test_function
内部的局部变量,只能在函数内部访问。
8. 脚本调试
在编写shell脚本时,难免会遇到各种错误。掌握调试技巧可以帮助我们快速定位和解决问题。
8.1 使用
set -x
set -x
命令可以开启调试模式,在执行脚本时,会将每一条执行的命令及其参数打印出来,方便我们观察脚本的执行过程。例如:
#!/bin/sh
set -x
a=10
b=20
sum=$((a + b))
echo "The sum is: $sum"
set +x # 关闭调试模式
在这个例子中,
set -x
开启调试模式,
set +x
关闭调试模式。在调试模式下,脚本执行时会输出每一条命令的详细信息。
8.2 使用
echo
语句
在脚本中插入
echo
语句可以输出变量的值或中间结果,帮助我们了解脚本的执行情况。例如:
#!/bin/sh
a=10
echo "The value of a is: $a"
b=20
echo "The value of b is: $b"
sum=$((a + b))
echo "The sum is: $sum"
在这个例子中,通过
echo
语句输出变量
a
、
b
和
sum
的值,方便我们观察脚本的执行过程。
8.3 使用
bash -n
bash -n
命令可以检查脚本的语法错误,但不会执行脚本。例如:
bash -n my_script.sh
如果脚本存在语法错误,
bash -n
会输出相应的错误信息,帮助我们快速定位问题。
9. 脚本的优化
为了提高脚本的性能和可读性,我们可以对脚本进行一些优化。
9.1 减少不必要的命令执行
在脚本中,尽量避免重复执行相同的命令。例如,如果需要多次使用某个命令的输出结果,可以将其存储在一个变量中,避免重复执行该命令。
#!/bin/sh
current_dir=$(pwd)
echo "The current directory is: $current_dir"
# 后续可以直接使用$current_dir变量,而不需要再次执行pwd命令
9.2 使用函数封装重复代码
如果脚本中有一些重复的代码块,可以将其封装成函数,提高代码的复用性。例如,以下脚本中有两段代码都是用于检查文件是否存在:
#!/bin/sh
file1="file1.txt"
if [ -e "$file1" ]; then
echo "$file1 exists."
else
echo "$file1 does not exist."
fi
file2="file2.txt"
if [ -e "$file2" ]; then
echo "$file2 exists."
else
echo "$file2 does not exist."
fi
可以将检查文件是否存在的代码封装成一个函数:
#!/bin/sh
check_file() {
if [ -e "$1" ]; then
echo "$1 exists."
else
echo "$1 does not exist."
fi
}
file1="file1.txt"
check_file $file1
file2="file2.txt"
check_file $file2
9.3 合理使用变量和注释
在脚本中,合理使用变量可以提高代码的可读性和可维护性。同时,添加适当的注释可以帮助他人(也包括自己)理解脚本的功能和逻辑。例如:
#!/bin/sh
# This script calculates the sum of two numbers
num1=10 # The first number
num2=20 # The second number
sum=$((num1 + num2)) # Calculate the sum
echo "The sum is: $sum"
10. 总结
通过以上内容,我们全面了解了shell脚本的各个方面,包括基础概念、特殊变量、退出代码、条件语句、循环结构、函数、调试和优化等。在实际应用中,我们可以根据具体需求灵活运用这些知识,编写高效、可靠的shell脚本。
以下是一个简单的mermaid流程图,展示了一个基本的shell脚本执行流程:
graph TD;
A[开始] --> B[读取脚本文件];
B --> C[解析命令];
C --> D{是否有条件判断};
D -- 是 --> E{条件是否为真};
E -- 是 --> F[执行相应命令];
E -- 否 --> G[跳过相应命令];
D -- 否 --> F;
F --> H{是否有循环};
H -- 是 --> I[执行循环体];
I --> H;
H -- 否 --> J[继续执行后续命令];
J --> K{是否结束};
K -- 否 --> C;
K -- 是 --> L[结束];
希望这些知识能帮助你更好地掌握shell脚本编程,在日常工作中提高效率。不断实践和探索,你会发现shell脚本的强大之处。
超级会员免费看
3007

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



