Unix Shell 介绍(上)

译者声明:译者对译文不做任何担保,译者对译文不拥有任何权利并且不负担任何责任和义务。
原文:http://cm.bell-labs.com/7thEdMan/shell.bun

摘要

shell 是提供到 UNIX 操作系统的接口的一个命令编程语言。它的特征包括控制流原语、参数传递、变量和字符串替换。还可获得如 while、if then else、 case 和 for 这样的构造。在 shell 和命令之间可以有双向通信。可以把字符串值参数、典型的文件名字和标志传递给命令。命令设置的返回值可用来决定控制流,而来自命令的标准输出可用作 shell 输入。

shell 可以修改命令在其中运行的环境。输入和输出可以重定向到文件,可以调用通过“管道”通信的进程。通过按照可以由用户指定的顺序查找文件系统中的目录来找到命令。命令可以读取自终端或文件,这允许把命令过程存储起来以备将来使用。

1.0 介绍shell 既是一个命令语言又是提供到 UNIX 操作系统的接口的一个编程语言。这个备忘录用例子描述 UNIX shell。第一章覆盖多数终端用户的日常需要。熟悉 UNIX 对读本章是很有利的,否则可阅读如“UNIX for beginners”这样的文章。第 2 章描述主要意图用在 shell 过程中的那些特征。这包括 shell 提供的控制流原语(primitive)和字符串值变量。在读本章的时候编程语言的知识将是有帮助的。最后一章描述 shell 的更高级的特征。文中的“参见 pipe (2)”引用的是 UNIX 手册的一个章节。 1.1 简单命令简单命令由一个或多个用空白分隔的字组成。第一个字是要执行的命令的名字;所有余下的字被作为传递给命令的实际参数。例如,who是打印用户登录的名字的一个命令。命令ls -l打印在当前目录中的文件的一个列表。实际参数 -l 告诉 ls 打印每个文件的状态信息、大小和建立日期。 1.2 后台命令要执行一个命令,shell 通常建立一个新进程并等待它完成。可以执行一个命令而不用等待它完成。例如,cc pgm.c &调用 C 编译器来编译文件 pgm.c。尾随的 & 是指示 shell 不等待命令完成的一个操作符。为了跟踪这样一个进程,shell 在建立它之后报告它的进程编号。可以使用 ps 命令来获得当前活跃进程的一个列表。 1.3 输入输出重定向多数命令在最初连接到这个终端上的标准输出上生成输出。这个输出可以通过写操作发送到一个文件,例如,ls -l >file记号 >file 由 shell 来解释并且不作为一个实际参数传递给 ls。如果文件不存在则 shell 建立它;否则文件的最初内容被来自 ls 的输出所替代。可以使用下面的记号把输出添加到一个文件ls -l >>file在这种情况下如果 file 不存在则也建立它。

可以通过写操作使一个命令的标准输入接受自一个文件而不是终端,例如,

wc <file命令 wc 读它的标准输入(在这种情况下重定向自文件)并打印发现的字符、字和行的数目。如果只需要行的数目则可以使用wc -l <file 1.4 管道线和过滤器可以通过写‘管道’操作符 | 把一个命令的标准输出连接到另一个命令标准输入上,如在ls -l | wc中以这种方式连接的两个命令组成一个管道线与下面的表述ls -l >file; wc <file除了未使用 file 之外整体效果上等同。但这两个进程是用管道连接的(参见 pipe (2))而且是并行运行。

管道是单向的,并通过当管道中没有东西可读的时候暂停 wc 和当管道满的时候暂停 ls 来实现同步。

过滤器是读它的标准输入,以某种方式转换它,并输出结果作为输出的命令。这样的一个过滤器如 grep, 从它的输入中选择出包含指定字符串的那些行。例如,

ls | grep old打印来自 ls 的输出中包含字符串 old 的那些行,如果有的话。另一个有用的过滤器是 sort。例如,who | sort

将打印登录的用户的按字符排序的一个列表。

一个管道线可以由多于两个的命令组成,例如,

ls | grep old | wc -l打印在当前目录中的文件名字中包含字符串 old 的数目。 1.5 文件名生成许多命令接受的实际参数是文件名字。例如,ls -l main.c打印与文件 main.c 相关的信息。

shell 提供一种机制来生成匹配一个模式的文件名字的一个列表。例如,

ls -l *.c生成在当前目录中的结束于 .c 的所有文件名字,作为给 ls 的实际参数。字符 * 是匹配包括空串的任何字符串的一个模式。一般的模式可以指定如下。
*
匹配包括空串的任何字符串。
?
匹配任何单一字符。
[...]
匹配包围的字符中的任何一个。用减号分隔的一对字符匹配在词法上位于这两个字符之间的任何字符(含这两个字符)。
例如,[a-z]*匹配在当前目录中开始于 a 到 z 中的一个字母的所有名字。/usr/fred/test/?匹配在目录 /usr/fred/test 中由一个单一字符组成的所有名字。如果没有找到匹配这个模式的名字,则把这个模式不做变动的作为实际参数传递。

这个机制对保存键入和依据某个模式选择名字二者都有用。它还用于查找文件。例如,

echo /usr/fred/*/core找到并打印在 /usr/fred 的子目录中所有 core 文件的名字。(echo 是标准 UNIX 命令,打印它的由空白分隔的实际参数)。最后的特征可能是昂贵的,它需要检索 /usr/fred 的所有子目录。

对针对模式的一般规则有一个例外。在一个文件名字开始处的字符‘.’必须被显式的匹配。

echo *将回显在当前目录中不以‘ .’开始的所有文件名字。echo .*将回显以‘ .’开始的所有文件名字。这避免了无意中匹配了名字‘ .’ 和‘ ..’,它们分别意味着‘当前目录’和‘父目录’。(注意 ls 抑制针对‘ .’和‘ ..’的信息。) 1.6 引用对 shell 有特定意义的字符,如 < > * ? | & 叫做元字符。在附录 B 中给出元字符的一个完整列表。以 \ 为前导的任何字符是被引用的并失去了它的特殊意义,如果有的话。删除 \ 所以echo \?将回显一个单一 ?,并且echo \\将回显一个单一的 \。为了允许长字符串在多于一行上延续,忽略序列 \换行

\ 便于引用单一字符。当多于一个字符需要引用的时候上述机制就是蠢笨的和错误的倾向。字符串可以通过用单引号包围来引用。例如,

echo xx'****'xx将回显xx****xx引用的字符串不可以包含单引号但可以包含并保留换行。这种引用机制是最简单的并建议偶尔使用。

还有第三种引用机制使用双引号,它防止对一些但不是全部元字符的解释。详情参见 3.4 节。

1.7 提示在从终端使用 shell 的时候,在读一个命令之前它发出一个提示。缺省的这个提示是‘ $’。可用下面的方法改变它,例如,PS1=yesdear设置提示为字符串 yesdear。如果键入了换行并且需要进一步的输入,则 shell 将发出提示‘ >’。有时这是缺少引号所导致的。如果这是意外的则一次中断(DEL)将使 shell 返回来读另一个命令。这个提示可以用下面的方法改变,例如,PS2=more 1.8 shell 和登录紧随 login (1) 之后调用 shell 来读取和执行在终端上键入的命令。如果这个用户的登录目录中包含文件 .profile 则假定它包含命令并且在 shell 从终端读取任何命令之前读取它。 1.9 总结
ls
打印在当前目录中的文件的名字。
ls >file
把来自 ls 的输出放置到 file 中。
ls | wc -l
打印在当前目录中文件的数目。
ls | grep old
打印包含字符串 old 的那些文件名字。
ls | grep old | wc -l
打印名字中包含字符串 old 的那些文件的数目。
cc pgm.c &
在后台运行 cc。

2.0 shell 过程可以用 shell 读取和执行包含在文件中的命令。例如,sh file [ args ... ]调用 shell 来从 file 读取命令。这样的文件叫做命令过程或 shell 过程。实际参数可以提供给调用并在 file 中用位置参数 $1$2、....来引用。例如,如果文件 wg 包含who | grep $1则sh wg fred等价于who | grep fredUNIX 文件有三个独立的属性,读、写和执行。可以使用 UNIX 命令 chmod (1)来使文件可执行。例如,chmod +x wg将确保文件 wg 有可执行状态。此后,命令wg fred等同于sh wg fred这允许 shell 过程和程序被交替使用。在任何一种情况下都建立一个新进程来运行命令。

同为位置参数提供名字一样,在调用中位置参数的数目可获得为 $#。被执行的文件的名字可获得为 $0

一个特殊的 shell 参数是 $* 被用来替换除了 $0 之外的所有位置参数。它的典型用途是提供一些缺省实际参数,如在

nroff -T450 -ms $*它简单的把那些给 shell 的实际参数准备转给这个命令。

译注:shell 还有一个内置命令 . file。它读这个文件中的命令并执行之。

2.1 控制流 - forshell 过程的一个常见用途是遍历(loop through)实际参数 ( $1, $2, ...)并且对每一个实际参数执行命令一次。这样的一个过程的例子是 tel 它查找包含如下行的文件 /usr/lib/telnos ...
fred mh0123
bert mh0789
...tel 的文本是for i
do grep $i /usr/lib/telnos; done命令tel fred打印在 /usr/lib/telnos 中包含 fred 的那些行。tel fred bert打印包含 fred 的行随后是包含 bert 的行。

for 循环记号由 shell 识别并有一般形式

for name in w1 w2 ...
do command-list
done命令列表(command-list)是由换行或分号分隔或结束的一个或多个命令的一个序列。进一步,保留字如 dodone 只有紧随一个换行或分号之后才被识别。名字(name)是一个 shell 变量,在每次执行 do 后面的命令列表时它将被依次设置为字 w1 w2 ...。如果省略了 in w1 w2 ... 则为每个位置参数执行一次循环;就是说,假定为 in $*。

使用 for 循环的另一个例子是 create 命令,它的文本是

for i do >$i; done命令create alpha beta确保两个空文件 alpha 和 beta 存在并且是空的。使用记号 >file 主动的建立一个文件或清除它的内容。 还要注意在 done 之前需要一个分号(或换行)。 2.2 控制流 - case case 记号提供一种多路分支。例如,case $# in
1) cat >>$1 ;;
2) cat >>$2 <$1 ;;
*) echo \'usage: append [ from ] to\' ;;
esac是一个 append 命令。在调用时带有一个实际参数如append file $# 是字符串 1 并使用 cat 命令把标准输入复制到 file 的末端。append file1 file2添加 file1 的内容到 file2 上。如果提供给 append 的实际参数数目不是 1 或 2 则打印指示正确用法的一个消息。

case 命令的一般形式是

case word in
pattern ) command-list ;;
...
esacshell 按模式(pattern)出现的次序对每个模式尝试匹配字(word)。如果找到一个匹配则执行相关的命令列表(command-list)并且 case 的执行完成。因为 * 是匹配任何字符串的模式它可以用作缺省情况。

一句警告: shell 不做检查来确保只有一个模式匹配 case 实际参数。找到的一个匹配定义要执行的命令集。在下面的例子中在第二个 * 之后的命令将永不执行。

case $# in
*) ... ;;
*) ... ;;
esac使用 case 构造的另一个例子是区别一个实际参数的不同形式。下列的例子是 cc 命令的一个片断。for i
do case $i in
-[ocs]) ... ;;
-*) echo \'unknown flag $i\' ;;
*.c) /lib/c0 $i ... ;;
*) echo \'unexpected argument $i\' ;;
esac
done为了允许同一个命令与多于一个模式相关联, case 命令提供了由 | 分隔的可选择的模式。例如,case $i in
-x|-y) ...
esac等价于case $i in
-[xy]) ...
esac使用普通的引用惯例所以case $i in
\?) ...将匹配字符 ?2.3 立即文档译注:here document 翻译成立即文档属于意译,参照寻址方式中立即寻址的先例。

在章节 2.1 中 shell 过程 tel 使用文件 /usr/lib/telnos 来为 grep 提供数据。一种替代方式是在这个 shell 过程中包含这些数据作为立即文档,如

for i
do grep $i <<!
...
fred mh0123
bert mh0789
...
!
done在这个例子中 shell 接收在 <<!! 之间的行作为 grep 的输入。字符串 ! 是任意的,这个文档被由紧随 << 之后的字符串构成的一行所终结。

下面的过程 edg 展示了文档在提供给 grep 之前,要替换其中的参数。

ed $3 <<%
g/$1/s//$2/g
w
%调用edg string1 string2 file等价于命令ed file <<%
g/string1/s//string2/g
w
%并在 file 中把所有出现的 string1 改变成 string2。使用 \ 引用特殊字符 $ 来防止替换如ed $3 <<+
1,\$s/$1/$2/g
w
+(这个版本的 edg 除了 ed 在没有字符串 $1 出现时打印一个 ? 之外等同于第一个版本)。通过引用终结字符串可以完全防止在立即文档内的替换,例如,grep $i <<\#
...
#文档被不加修改的提供给 grep。如果在立即文档中不需要参数替换,后一种形式更有效率。 2.4 shell 变量shell 提供字符串值的变量。变量名字开始于一个字母并由字母、数字和下划线组成。可以通过下列写法给出变量的值,例如,user=fred box=m000 acct=mh0000它向变量 userboxacct 赋值。可以通过下列写法设置一个变量为空串,例如,null=通过把 $ 前导于变量的名字来把它替换成变量的值;例如,echo $user将回显 fred。

可以交互式的使用变量为经常使用的字符串提供简写。例如,

b=/usr/fred/bin
mv pgm $b将把文件 pgm 从当前目录移动到目录 /usr/fred/bin。对于参数(或变量)替换可以有一种更一般的记号,如echo ${user}它等价于echo $user并在参数名字后跟随着一个字母或数字的时候有用。例如,tmp=/tmp/ps
ps a >${tmp}a将把 ps 的输出定向到文件 /tmp/psa,而ps a >$tmpa将导致替换变量 tmpa 的值。

除了 $? 下列都是由 shell 作最初的设置。$? 在每次命令执行之后设置。

$?
最近执行的命令的退出状态(返回代码),是一个十进制数字符串。多数命令如果成功完成则返回一个零退出状态,否则返回一个非零退出状态。测试返回代码的值在以后的 ifwhile 命令中处理。
$#
位置参数的数目(十进制)。例如用在 append 命令中检查参数的数目。
$$
这个 shell 的进程编号(十进制)。因为过程编号在现存的进程中是唯一的,这个字符串经常用来生成唯一的临时文件名字。例如,ps a >/tmp/ps$$
...
rm /tmp/ps$$
$!
在后台运行的最后的进程的编号(十进制)。
$-
当前的 shell 标志,比如 -x-v
一些变量对 shell 有特殊意义并应该避免作一般使用。
$MAIL
在交互使用的时候,shell 在发出提示之前察看这个变量指定的文件。如果指定的文件自从上次察看之后已经被修改了,shell 在提示下一个命令之前打印消息 you have mail。 这个变量典型的在用户登录目录下的文件 .profile 中设置。例如,MAIL=/usr/mail/fred
$HOME
cd 命令的缺省实际参数。使用当前目录来解析不以 / 开始的文件名引用,并使用 cd 命令变更它。例如,cd /usr/fred/bin使当前目录成为 /usr/fred/bin。cat wn将在终端上打印在这个目录中的文件 wn。命令。没有实际参数的 cd 等价于cd $HOME这个变量典型的也在这个用户的登录 .profile 中设置。
$PATH
包含命令的目录的一个列表(查找路径)。shell 通过在这个目录列表中查找可执行文件来执行每个命令。如果未设置 $PATH 则缺省的查找当前目录、 /bin /usr/bin。否则 $PATH 由用 : 分隔的目录名字组成。例如,PATH= :/usr/fred/bin :/bin :/usr/bin指定以当前目录(在第一个 : 之前的空串)、 /usr/fred/bin、/bin/usr/bin 的次序查找。在这种方式下单个用户可以有他们自己‘专有’命令,可在当前目录下单独访问。如果命令名字包含一个 / 则不使用这种目录查找;对执行这个命令作一次单一的尝试。
$PS1
主要的 shell 提示字符串,缺省是‘ $’。
$PS2
在需要进一步输入时的 shell 提示,缺省是‘ >’。
$IFS
空白解释使用的字符集合(参见章节 3.4)。
2.5 test 命令test 命令尽管不是 shell 的一部分,但意图由 shell 程序使用。例如,test -f file译注: 在当前版本的 shell 工具中,有一个与 test 等同的命令 [,它接受与 test 一样的实际参数,但要求在实际参数列表的最后附加一个 ] 作为实际参数。上面的例子也可以写成[ -f file ]

如果 file 存在则返回零退出状态否则返回非零退出状态。通常 test 计算一个谓词并返回这个结果作为退出状态。下面给出某些经常使用的 test 实际参数,详细的规定请参见 test (1)。

test s
如果实际参数 s 不是空串则为真
test -f file
如果 file 存在则为真
test -r file
如果 file 可读则为真
test -w file
如果 file 可写则为真
test -d file
如果 file 是目录则为真
2.6 控制流 - while for 循环的动作和 case 分支由 shell 可获得的数据决定。还提供 whileuntil 循环和 if then else 分支,它们的动作由命令返回的退出状态决定。 while 循环有一般形式 while command-list1
do command-list2
done while 命令测试的值是紧随 while 之后的最后的简单命令的退出状态。每轮循环都执行 command-list1;如果返回一个零退出状态则执行 command-list2;否则中止循环。例如,while test $1
do ...
shift
done等价于for i
do ...
doneshift 是重命名位置参数 $2, $3, ...$1, $2, ... 并丢弃 $1 的一个命令。

另一种 while/until 循环的用法是等待直到某个外部的事件发生并接着运行某个命令。在 until 循环中中止条件是反过来的。例如,

until test -f file
do sleep 300; done
commands将循环直到 file 存在。每轮循环都在再次尝试之前等待 5 分钟。(推测另一个进程最终会建立这个文件。) 2.7 控制流 - if还可以获得下面形式的一般的条件分支, if command-list
then command-list
else command-list
fi它测试紧随 if 之后的最后一个简单命令的返回值。

if 命令可以与 test 命令联合使用来测试文件的存在如

if test -f file
then process file
else do something else
fi

在2.10 节给出使用 ifcasefor 构造的一个例子。

形如下面的多重测试 if 命令

if ...
then ...
else if ...
then ...
else if ...
...
fi
fi
fi可以使用 if 记号的一种扩展而写成 if ...
then ...
elif ...
then ...
elif ...
...
fi下列例子是改变一组文件的‘最近修改时间’的 touch 命令。这个命令可以与 make (1)联合使用来重新编译一组文件。flag=
for i
do case $i in
-c) flag=N ;;
*) if test -f $i
then ln $i junk$$; rm junk$$
elif test $flag
then echo file \'$i\' does not exist
else >$i
fi
esac
done在这命令中使用 -c 来强制后面的文件如果不存在则建立之。否则,如果文件不存在,则打印一个错误消息。如果遇到 -c 参数则把 shell 变量 flag 设置为非空字符串。命令ln ...; rm ...制作到这个文件的一个连接接着删除它,这导致更新最后的修改日期。

序列

if command1
then command2
fi可以写成command1 && command2反过来,command1 || command2只在 command1 失败时执行 command2。在这些情况下返回值是最后的简单命令的返回值。 2.8 命令组合可以用两种方式组合命令, { command-list ; }( command-list )第一个命令列表被简单的执行。第二种形式把命令列表作为一个单独的进程执行。例如,(cd x; rm junk )在目录 x 中执行 rm junk 而不改变调用 shell 的当前目录。

命令

cd x; rm junk有相同的效果但把调用 shell 留在目录 x 中。 2.9 调试 shell 过程shell 提供两种跟踪机制来帮助调试 shell 过程。第一种在过程中调用为set -v( v 是 verbose冗余)并导致打印过程的行,同读到的一样。这对分离语法错误有用。可以用下列写法调用它而不用修改过程sh -v proc ...这里的 proc 是 shell 过程的名字。这个标志可以与 -n 标志联合使用,它防止随后的命令执行。(注意在终端上的 set -n 将放弃(render)终端不用直到键入一个文件结束符。)

命令

set -x将产生执行跟踪。紧随参数替换之后按实际上执行的那样打印每个命令。(在终端上尝试一下这种效果)。通过如下表述来关闭这些标志set -而 shell 标志的当前设置可以获得为 $-2.10 man 命令下面是用来打印 UNIX 手册章节的 man 命令。调用它的例子如下$ man sh
$ man -t ed
$ man 2 fork打印手册的第一章的 sh。因为没有指定章节,使用第 1 章。第二个例子将用打印机打印( -t 选项)手册章节 ed。 最后一个例子打印第二章的 fork 手册页。cd /usr/man

: '冒号是注释命令'
: '缺省是 nroff ($N), 章节 1 ($s)'
N=n s=1

for i
do case $i in
[1-9]*) s=$i ;;
-t) N=t ;;
-n) N=n ;;
-*) echo unknown flag \'$i\' ;;
*) if test -f man$s/$i.$s
then ${N}roff man0/${N}aa man$s/$i.$s
else : 'look through all manual sections'
found=no
for j in 1 2 3 4 5 6 7 8 9
do if test -f man$j/$i.$j
then man $j $i
found=yes
fi
done
case $found in
no) echo \'$i: manual page not found\'
esac
fi
esac
done 1.? man 命令的一个版本

译注:在当前版本的 shell 中把以‘#’开始的一行作为注释


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值