更好地理解 UNIX? 用户输入的这些 “奇怪的” 字符。学习如何在 UNIX 中使用管道、重定向、操作符等特性。
现在,您已经在 IBM? AIX? 上工作了一段时间了。您已经学习了几个基本命令,能够在目录结构中移动、创建和修改文件、查看正在运行的进程以及管理用户和系统。这很不错,但是您希望了解 UNIX? 管理员输入的命令是什么意思。这些命令中包含许多奇怪的符号。在本文中,了解 | 、> 、>> 、< 、<< 、[[ 和 ]] 等符号在 UNIX 和 Linux? 中的意思,以及如何使用 && 、|| 、< 、<= 和 != 操作符。
管道
如果您熟悉 UNIX,那么管道(或 pipe)会是每天都要接触到的东西。管道最初是由 Malcolm McIlroy 开发的,可以使用管道把一个命令的标准输出(stdout)定向到下一个命令的标准输入(stdin),这样就形成了连续执行的命令链。可以在一个命令行上使用多个管道。在许多时候,一个命令的 stdout 用作下一个命令的 stdin,第二个命令的 stdout 又被重定向到另一个命令的 stdin,依此类推。
例如,在排除故障或执行日常检查时,大多数 UNIX 管理员首先做的事情之一是查看系统上当前正在运行的进程。清单 1 演示这样的检查。
清单 1. 日常进程检查示例
# ps –ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Jul 27 - 0:05 /etc/init
root 53442 151674 0 Jul 27 - 0:00 /usr/sbin/syslogd
root 57426 1 0 Jul 27 - 0:00 /usr/lib/errdemon
root 61510 1 0 Jul 27 - 23:55 /usr/sbin/syncd 60
root 65634 1 0 Jul 27 - 0:00 /usr/ccs/bin/shlap64
root 82002 110652 0 Jul 27 - 0:24 /usr/lpp/X11/bin/X -x abx
-x dbe -x GLX -D /usr/lib/X11//rgb -T -force :0 -auth /var/dt/A:0-SfIdMa
root 86102 1 0 Jul 27 - 0:00 /usr/lib/methods/ssa_daemon -l ssa0
root 106538 151674 0 Jul 27 - 0:01 sendmail: accepting connections
root 110652 1 0 Jul 27 - 0:00 /usr/dt/bin/dtlogin -daemon
root 114754 118854 0 Jul 27 - 20:22 dtgreet
root 118854 110652 0 Jul 27 - 0:00 dtlogin <:0> -daemon
root 131088 1 0 Jul 27 - 0:07 /usr/atria/etc/lockmgr
-a /var/adm/atria/almd -q 1024 -u 256 -f 256
root 147584 1 0 Jul 27 - 0:01 /usr/sbin/cron
root 155816 151674 0 Jul 27 - 0:04 /usr/sbin/portmap
root 163968 151674 0 Jul 27 - 0:00 /usr/sbin/qdaemon
root 168018 151674 0 Jul 27 - 0:00 /usr/sbin/inetd
root 172116 151674 0 Jul 27 - 0:03 /usr/sbin/xntpd
root 180314 151674 0 Jul 27 - 0:19 /usr/sbin/snmpmibd
root 184414 151674 0 Jul 27 - 0:21 /usr/sbin/aixmibd
root 188512 151674 0 Jul 27 - 0:20 /usr/sbin/hostmibd
root 192608 151674 0 Jul 27 - 7:46 /usr/sbin/muxatmd
root 196718 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.mountd
root 200818 151674 0 Jul 27 - 0:00 /usr/sbin/biod 6
root 213108 151674 0 Jul 27 - 0:00 /usr/sbin/nfsd 3891
root 221304 245894 0 Jul 27 - 0:05 /bin/nsrexecd
daemon 225402 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.statd
root 229498 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.lockd
root 241794 151674 0 Jul 27 - 0:51 /usr/lib/netsvc/yp/ypbind
root 245894 1 0 Jul 27 - 0:00 /bin/nsrexecd
root 253960 1 0 Jul 27 - 0:00 ./mflm_manager
root 274568 151674 0 Jul 27 - 0:00 /usr/sbin/sshd -D
root 282766 1 0 Jul 27 lft0 0:00 /usr/sbin /dev/console
root 290958 1 0 Jul 27 - 0:00 /usr/lpp/diagnostics/bin/diagd
root 315646 151674 0 Jul 27 - 0:00 /usr/sbin/lpd
root 319664 1 0 Jul 27 - 0:00 /usr/atria/etc/albd_server
root 340144 168018 0 12:34:56 - 0:00 rpc.ttdbserver 100083 1
root 376846 168018 0 Jul 30 - 0:00 rlogind
cormany 409708 569522 0 19:29:27 pts/1 0:00 -ksh
root 569522 168018 0 19:29:26 - 0:00 rlogind
cormany 733188 409708 3 19:30:34 pts/1 0:00 ps -ef
root 749668 168018 0 Jul 30 - 0:00 rlogind
|
系统上当前正在运行的进程的列表可能像 清单 1 这么简单;但是,大多数生产系统运行的进程更多,这会使 ps 的输出更长。为了把这个列表缩短到自己需要的范围,可以使用管道把 ps –ef 的标准输出重定向到 grep ,从而搜索自己真正希望看到的结果。清单 2 把 清单 1 产生的进程列表重定向到 grep ,搜索字符串 “rpc” 和 “ksh”。
清单 2. 把进程列表重定向到 grep
# ps –ef | grep –E "rpc|ksh"
root 196718 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.mountd
daemon 225402 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.statd
root 229498 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.lockd
root 340144 168018 0 12:34:56 - 0:00 rpc.ttdbserver 100083 1
cormany 409708 569522 0 19:29:27 pts/1 0:00 -ksh
cormany 733202 409708 0 19:52:20 pts/1 0:00 grep -E rpc|ksh
|
当多次把 stdout 重定向到 stdin 时,管道的使用方法可以很复杂。在下面的示例中,扩展了前面的 ps 和 grep 示例,把它的 stdout 重定向到另一个 grep ,其作用是排除包含 “grep” 或 “ttdbserver” 的字符串。当最后的 grep 操作完成时,再次使用管道把 stdout 重定向到一个 awk 语句,其作用是输出进程标识符(PID)大于 200,000 的所有进程:
# ps –ef | grep –E "rpc|ksh" | grep -vE "grep|rpc.ttdbserver" |
awk -v _MAX_PID=200000 '{if ($2 > _MAX_PID) {print "PID for
process",$8,"is greater than", _MAX_PID}}'
PID for process /usr/sbin/rpc.statd is greater than 200000
PID for process /usr/sbin/rpc.lockd is greater than 200000
PID for process -ksh is greater than 200000
|
图 1 通过图形说明命令的 stdout 重定向到后续命令的 stdin 的次序。
图 1. 管道示例
用 >、>>、< 和 << 执行数据重定向
通过命令行界面(CLI)执行命令的另一个重要方面是,能够把各种输出写到一个设备,或者把来自另一个设备的输入读取到命令中。要想写一个命令的输出,需要在执行的命令后面加上大于号(> 或 >>)和所需的目标文件名或设备。如果目标文件不存在,而且您对目标目录有写权限,那么 > 和 >> 会创建这个文件并根据您的 umask 设置权限,然后把命令的输出写到刚创建的文件中。但是,如果这个文件存在,> 会尝试打开文件并覆盖整个内容。如果希望在这个文件中追加内容,那么只需使用 >>。可以认为它的作用是把左边命令的输出数据流移动到右边的目标文件中(即 <cmd> -> <output> -> <file> )。
下面的示例执行 “管道” 一节中的 ps –ef 示例,并把输出重定向到文件 ps_out :
# ps –ef | grep –E "rpc|ksh" > ps_out
|
下面的代码执行前面扩展的管道示例并把输出重定向到同一个文件(ps_out),但是追加到当前数据后面:
# ps –ef | grep –E "rpc|ksh" | grep -vE "grep|rpc.ttdbserver" |
awk -v _MAX_PID=200000 '{if ($2 > _MAX_PID) {print "PID for
process",$8,"is greater than", _MAX_PID}}' >> ps_out
|
清单 3 给出前两个重定向的输出。
清单 3. 重定向的输出
# cat ps_out
root 196718 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.mountd
daemon 225402 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.statd
root 229498 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.lockd
root 340144 168018 0 12:34:56 - 0:00 rpc.ttdbserver 100083 1
cormany 409708 569522 0 19:29:27 pts/1 0:00 -ksh
cormany 733202 409708 0 19:52:20 pts/1 0:00 grep -E rpc|ksh
PID for process /usr/sbin/rpc.statd is greater than 200000
PID for process /usr/sbin/rpc.lockd is greater than 200000
PID for process -ksh is greater than 200000
|
当只使用 > 重定向输出时,只重定向命令的 stdout。但是,除了 stdout,还有 stderr 输出:前者表示为 1 ,后者表示为 2 。在 UNIX 中输出重定向没有区别。只需在 > 前面加上所需的输出类型(例如,1> 、2> ),告诉 shell 要把输出路由到哪里。
清单 4 尝试列出 fileA.tar.bz2 和 fileC.tar.bz2。但是,如第一个命令(ls )所示,fileC.tar.bz2 不存在。好在可以把 stdout 和 stderr 分别重定向到 ls.out 和 ls.err,这样就能够看到错误消息。
清单 4. 列出文件 fileA.tar.bz2 和 fileC.tar.bz2
# ls
fileA.tar.bz2 fileAA.tar.bz2 fileB.tar.bz2 fileBB.tar.bz2
# ls fileA.tar.bz2 fileC.tar.bz2 1> ls.out 2> ls.err
# cat ls.out
fileA.tar.bz2
# cat ls.err
ls: 0653-341 The file fileC.tar.bz2 does not exist.
|
在 AIX 中,对 stdout 和 stderr 使用 > 和 >> 时应用相同的规则。例如,以后的测试可以使用相同的输出文件,见 清单 5。
清单 5. 使用输出文件进行以后的测试
# ls fileB.tar.bz2 fileD.tar.bz2 1>> ls.out 2>> ls.err
# cat ls.out
fileA.tar.bz2
fileB.tar.bz2
# cat ls.err
ls: 0653-341 The file fileC.tar.bz2 does not exist.
ls: 0653-341 The file fileD.tar.bz2 does not exist.
|
有时候,可能需要把 stdout 和 stderr 写到同一个文件或设备。这有两种方法。第一种方法是把 1> 和 2> 重定向到同一个文件:
# ls fileA.tar.bz2 fileC.tar.bz2 1> ls.out 2> ls.out
# cat ls.out
fileA.tar.bz2
ls: 0653-341 The file fileC.tar.bz2 does not exist.
|
第二个方法更简单更快速,有经验的 UNIX 用户更喜欢采用这种方法:
# ls fileA.tar.bz2 fileC.tar.bz2 > ls.out 2>&1
# cat ls.out
fileA.tar.bz2
ls: 0653-341 The file fileC.tar.bz2 does not exist.
|
我们分解这个语句。首先,执行 ls fileA.tar.bz2 fileC.tar.bz2 。然后使用 > ls.out 把 stdout 重定向到 ls.out,使用 2>&1 把 stderr 重定向到前面重定向的 stdout(ls.out)。
请记住,可以把输出重定向到文件和其他设备。可以把数据重定向到打印机、软盘、终端类型(TTY)以及各种其他设备。例如,如果希望把一个消息发送给所有会话(或 TTY)上的某个用户,那么只需循环处理 who 并把一个消息重定向到 TTY(如果您有足够的权限的话),见 清单 6。
清单 6. 把消息重定向到一个 TTY
# for _TTY in 'who | grep "cormany" | awk '{print $2}''
> do
> _TTY="/dev/${_TTY}"
> echo "Sending message to cormany on ${_TTY}"
> echo "Test Message to cormany@${_TTY}" > ${_TTY}
> done
Sending message to cormany on /dev/pts/13
Test Message to cormany@/dev/pts/13
Sending message to cormany on /dev/pts/14
|
stdin 而不是 stdout
尽管使用 > 和 >> 对于大多数人是一个相当容易掌握的概念,但是有的人在使用小于号(< 和 <<)时常常有困难。在考虑 > 和 >> 时,认为它们把左边命令的输出数据流移动到右边的目标文件中,这样最容易理解。同样的方法也适用于 < 和 <<。在使用 < 时,本质上是用一个已经提供的 stdin 执行一个命令。也就是说,把已经提供的数据提供给左边的命令作为 stdin(即 <cmd> <- <data> )。
例如,假设希望把一个包含 ASCII 文本文件的电子邮件发送给另一个用户。可以使用管道把 cat 的 stdout 重定向到 mail 的 stdin(即 cat mail_file.out | mail –s "Here's your E-mail!" acormany@yahoo.com ),也可以把文件的内容重定向到 mail 命令的 stdin:
# mail –s "Here's your E-mail!" acormany@yahoo.com < mail_file.out
|
使用 <<(也称为 here-document)可以节省格式化时间,并且使命令执行的处理更容易。通过使用 <<,文本字符串被重定向到执行的命令作为 stdin,但是可以继续输入信息,直到到达终止标识符。只需输入命令,输入 << 和终止标识符,然后输入需要的任何内容,最后在一个新行上输入终止标识符。通过使用 here-document,可以保留空格、换行等。
例如,UNIX 必须单独处理下面五个 echo 语句:
# echo "Line 1"
Line 1
# echo "Line 2"
Line 2
# echo "Line 3"
Line 3
# echo "Line 4"
Line 4
# echo "Line 5"
Line 5
|
可以用以下代码替换多个 echo 语句,UNIX 只需处理一次执行:
# cat << EOF
> Line 1
> Line 2
> Line 3
> Line 4
> Line 5
> EOF
Line 1
Line 2
Line 3
Line 4
Line 5
|
还可以使用制表符让 shell 脚本中的内容更整洁一点,这只需要在 << 和终止标识符之间放上一个连字符(- ):
# cat <<- ATC
> Line 1
> Line 2
> Line 3
> Line 4
> Line 5
> ATC
Line 1
Line 2
Line 3
Line 4
Line 5
|
清单 7 给出的示例演示如何结合使用本文到目前为止讨论的东西。
清单 7. 组合 CLI
# cat redirect_example
#!/usr/bin/ksh
cat <<- ATC | sed "s/^/Redirect Example => /g" >> atc.out
This is an example of how to redirect
stdout to a file as well as pipe stdout into stdin
of another command (i.e. sed), all done inside
a here-document.
Cool eh?
ATC
|
现在,看看关于重定向和管道的脚本。
# ./redirect_example
# cat atc.out
Redirect Example => This is an example of how to redirect
Redirect Example => stdout to a file as well as pipe stdout into stdin
Redirect Example => of another command (i.e. sed), all done inside
Redirect Example => a here-document.
Redirect Example =>
Redirect Example => Cool eh?
|
子 shell
有时候,需要一起执行几个命令。例如,如果希望在另一个目录中执行某一操作,可以使用 清单 8 中的代码。
清单 8. 同时执行几个命令
# pwd
/home/cormany
# cd testdir
# tar –cf ls_output.tar ls.out?
# pwd
/home/cormany/testdir
|
这是有效的,但是要注意,在执行这些步骤之后,您就不再位于原来的目录中了。通过把这些命令放在它们自己的子 shell 中,它们会作为子 shell 的实例执行。清单 9 演示如何使用子 shell 执行相同的代码。
清单 9. 使用子 shell 同时执行几个命令
# pwd
/home/cormany
# (cd testdir ; tar -cf ls_output.tar ls.out?)
# pwd
/home/cormany
|
test 命令、[ ] 和 [[ ]]
在编写 shell 脚本或用任何现代语言编写程序时,运算表达式或值的能力都很重要。UNIX 一直通过 test 命令提供这一功能。正如 test 的手册页指出的,test 命令运算表达式参数的值,如果表达式的值是 True,就返回零(True)退出值。关于 test 的定义和所有可用条件的更多信息,请参见 test 手册页。
要想使用 test 命令,只需给这个命令提供适当的标志和文件名。当 test 运算完表达式时,返回到命令提示,可以在这里检查返回码,见 清单 10。
清单 10. 检查返回码
# ls –l
-rwxr-xr-x 1 cormany atc 786 Feb 22 16:11 check_file
-rw-r--r-- 1 cormany atc 0 Aug 04 20:57 emptyfile
# test -f emptyfile
# echo $?
0
# test -f badfilename
# echo $?
1
|
根据定义,如果表达式值是 True,那么 test 返回零退出值,否则返回非零退出值(即 1 )。在 清单 10 中,找到了文件 emptyfile,所以 test 返回 0 ;但是没有找到文件 badfilename,所以返回 1 。
使用 test 的另一种方法是把要运算的表达式放在单层方括号([ ] )中。使用 test 命令或把它替换为 [ ] 会返回相同的值:
# [ -f emptyfile ]
# echo $?
0
# [ -f badfilename ]
# echo $?
1
|
使用单层方括号([ ] )还是双层方括号([[ ]] )是个人习惯问题,实际上取决于您如何学习命令和 shell 脚本编程。但是请记住,这两者之间有一些差异。尽管 [ ] 和 [[ ]] 在运算期间使用相同的测试操作符,但是它们使用不同的逻辑操作符。
操作符
在 ksh(AIX 中使用的默认 shell)中,以及 UNIX 和 Linux 使用的其他 shell 中,一定要知道如何使用测试、逻辑和替换操作符。
测试操作符
在编写 shell 脚本时,测试操作符对于检查错误和检查文件状态很重要。下面只是可以在 ksh 和其他标准 UNIX shell 中使用的一部分测试操作符:
-d <file> :<file> 是一个目录
-e <flle> :<file> 存在
-f <file> :<file> 是一个常规文件
-n <string> :<string> 不是 NULL
-r <file> :用户对 <file> 有读权限
-s <file> :<file> 的大小大于 0
-w <file> :用户对 <file> 有写权限
-x <file> :用户对 <file> 有执行权限
-z <string> :<string> 是 null
-L <file> :<file> 是一个符号链接
请记住,在 UNIX 目录中,设备、符号链接和其他对象都是文件,所以上面的测试操作符适用于所有类型的文件。
每个人都有自己的 shell 脚本编程风格。无论在测试语句中使用 [[ ]] 还是 [ ] ,上面的测试操作符的作用是相同的。本文使用 [[ ]] 。清单 11 演示如何使用上面列出的几个测试操作符。
清单 11. 使用测试操作符
#!/usr/bin/ksh
while true
do
echo "\nEnter file to check: \c"
read _FNAME
if [[ ! -e "${_FNAME}" ]]
then
echo "Unable to find file '${_FNAME}'"
continue
fi
if [[ -f "${_FNAME}" ]]
then
echo "${_FNAME} is a file."
elif [[ -d "${_FNAME}" ]]
then
echo "${_FNAME} is a directory."
elif [[ -L "${_FNAME}" ]]
then
echo "${_FNAME} is a symbolic link."
else
echo "Unable to determine file type for '${_FNAME}'"
fi
[[ -r "${_FNAME}" ]] && echo "User ${USER} can read '${_FNAME}'"
[[ -w "${_FNAME}" ]] && echo "User ${USER} can write to '${_FNAME}'"
[[ -x "${_FNAME}" ]] && echo "User ${USER} can execute '${_FNAME}'"
if [[ -s "${_FNAME}" ]]
then
echo "${_FNAME} is NOT empty."
else
echo "${_FNAME} is empty."
fi
done
|
执行清单 11 中的代码并检查几个文件名,会产生 清单 12 所示的输出。
清单 12. 执行测试操作符的输出
# ls –l
-rwxr-xr-x 1 cormany atc 786 Feb 22 16:11 check_file
-rw-r--r-- 1 cormany atc 0 Aug 04 20:57 emptyfile
# ./check_file
Enter file to check: badfilename
Unable to find file 'badfilename'
Enter file to check: check_file
check_file is a file.
User cormany can read 'check_file'
User cormany can write to 'check_file'
User cormany can execute 'check_file'
check_file is NOT empty.
Enter file to check: emptyfile
emptyfile is a file.
User cormany can read 'emptyfile'
User cormany can write to 'emptyfile'
emptyfile is empty.
|
要想了解关于测试操作符的更多信息和完整列表,请执行 man test 。
逻辑操作符
UNIX 中的另一组重要操作符是逻辑操作符。与大多数现代编程语言一样,需要使用 AND 和 OR 语句对表达式或它们的值进行条件运算。
如果您读过我以前的文章(参见 参考资料),就会注意到我喜欢使用逻辑操作符,而不是编写多行代码。这会使脚本更干净,更容易管理。在编写脚本时,我会先编写 exit_msg() 函数:
exit_msg() {
[[ $# -gt 1 ]] && echo "${0##*/} (${1}) – ${2}"
exit ${1:-0}
}
|
这样就不必编写 清单 13 那样臃肿的代码。
清单 13. 不使用 exit_msg() 函数和逻辑操作符的脚本
#!/usr/bin/ksh
if [[ -n ${_NUM1} ]]
then
unset _NUM1
fi
if [[ -n ${_NUM2} ]]
then
unset _NUM2
fi
while [[ -z ${_NUM1} ]] || [[ -z ${_NUM2} ]]
do
echo "Enter 2 sets of numbers: \c"
read _NUM1 _NUM2
done
echo "Enter file to log results to: \c"
read _FNAME
if [[ ! -e "${_FNAME}" ]]
then
echo "File '${_FNAME}' doesn't exist. A new log will be created."
fi
touch "${_FNAME}"
if [[ ! -w "${_FNAME}" ]]
then
echo "Unable to write to file '${_FNAME}'"
exit 1
fi
expr ${_NUM1} \/ 1 > /dev/null 2>&1
if [[ $? -ne 0 ]]
then
echo "Number '${_NUM1}' is not numeric."
exit 2
fi
expr ${_NUM2} \/ 1 > /dev/null 2>&1
if [[ $? -ne 0 ]]
then
echo "Number '${_NUM2}' is not numeric."
exit 2
fi
echo "${_NUM1},${_NUM2}" >> "${_FNAME}"
|
只需使用 exit_msg() 这样的简单函数和几个逻辑操作符,这个脚本就可以简化成更漂亮更容易理解的程序,见 清单 14。
清单 14. 使用函数和逻辑操作符的脚本简化版本
#!/usr/bin/ksh
exit_msg() {
[[ $# -gt 1 ]] && echo "${0##*/} (${1}) - ${2}"
exit ${1:-0}
}
[[ -n ${_NUM1} ]] && unset _NUM1
[[ -n ${_NUM2} ]] && unset _NUM2
while [[ -z ${_NUM1} ]] || [[ -z ${_NUM2} ]]
do
echo "Enter 2 sets of numbers: \c"
read _NUM1 _NUM2
done
echo "Enter file to log results to: \c"
read _FNAME
[[ ! -e "${_FNAME}" ]] && echo
"File '${_FNAME}' doesn't exist. A new log will be created."
touch "${_FNAME}"
[[ ! -w "${_FNAME}" ]] && exit_msg 1
"Unable to write to file '${_FNAME}'"
expr ${_NUM1} \/ 1 > /dev/null 2>&1
[[ $? -ne 0 ]] && exit_msg 2 "Number '${_NUM1}' is not numeric."
expr ${_NUM2} \/ 1 > /dev/null 2>&1
[[ $? -ne 0 ]] && exit_msg 2 "Number '${_NUM2}' is not numeric."
echo "${_NUM1},${_NUM2}" >> "${_FNAME}"
|
前面的示例主要使用 AND (&& )和 OR (|| )逻辑操作符。除了这些之外,还可以使用 AND (–a )和 OR (–o )操作符。在使用 test 命令或单层方括号([ ] )时,使用 –a 和 –o 运算表达式。但是,如果使用双层方括号([[ ]] ),就要使用 && 和 || :
# [[ "Paul" != "Xander" && 2 -gt 0 ]]
# echo $?
0
# [ "Paul" != "Xander" -a 2 -gt 0 ]
# echo $?
0
|
比较测试操作符
另一组测试操作符称为比较测试操作符。与前一组测试操作符一样,比较测试操作符是执行错误检查或根据另一个值测试值的简便方法。前一组测试操作符主要用来测试文件或检查一个变量是否已经定义了,而比较测试操作符主要用来测试字符串和数字值,例如检查日期、检查文件大小、检查两个字符串是否相同等等。
比较测试操作符包括:
<fileA> -nt <fileB> :fileA 比 fileB 新
<fileA> -ot <fileB> :fileA 比 fileB 旧
<fileA> -ef <fileB> :fileA 和 fileB 指向同一个文件
<string> = <pattern> :字符串与模式匹配
<string> != <pattern> :字符串与模式不匹配
<stringA> < <stringB> :按照词典次序 stringA 出现在 stringB 之前
<stringA> > <stringB> :按照词典次序 stringA 出现在 stringB 之后
<exprA> -eq <exprB> :expressionA 等于 expressionB
<exprA> -ne <exprB> :expressionA 不等于 expressionB
<exprA> -lt <exprB> :expressionA 小于 expressionB
<exprA> -gt <exprB> :expressionA 大于 expressionB
<exprA> -le <exprB> :expressionA 小于或等于 expressionB
<exprA> -ge <exprB> :expressionA 大于或等于 expressionB
可以按照与其他操作符相同的格式使用比较测试操作符。可以使用 test 、[ ] 或 [[ ]] 。清单 15、清单 16 和 清单 17 分别演示如何使用文件、数字和字符串比较。
清单 15. 文件比较
# ls -l *.file
-rw-r--r-- 1 cormany atc 21 Feb 22 2006 Pauls.file
-rw-r--r-- 1 cormany atc 22 Aug 04 20:57 Xanders.file
# [[ "Pauls.file" -ot "Xanders.file" ]]
# echo $?
0
|
清单 16. 数字比较
# _PSIZE=`ls -l Pauls.file | awk '{print $5}'`
# _XSIZE=`ls -l Xanders.file | awk '{print $5}'`
# [[ ${_PSIZE} -lt ${_XSIZE} ]]
# echo $?
0
|
清单 17. 字符串比较
# [[ "cat" = "dog" ]]
# echo $?
1
|
替换操作符
在编写脚本时,很容易忘记定义一个变量或为它赋值。或者,您好几年没有改动一个脚本,现在需要修改它,但是忘了它的细节。有时候,希望告诉用户已经设置了一个值,或者设置了某些默认值。替换操作符是解决这些问题的好方法:
${var-value} :如果 <var> 存在,就返回 <var> 的值。如果 <var> 不存在,就返回 <value> 。
${var=value} :如果 <var> 存在,就返回 <var> 的值。如果 <var> 不存在,就把 <var> 设置为 <value> 并返回 <value> 。
${var+value} :如果 <var> 存在,就返回 <var> 的值。如果 <var> 不存在,就返回 NULL。
${var?value} :如果 <var> 存在,就返回 <var> 的值。如果 <var> 不存在,就退出命令或脚本并显示用 <value> 设置的错误消息。如果未设置 <value> ,就显示默认的错误消息 “Parameter null or not set”。
${var:-value} :如果 <var> 存在且不是 NULL,就返回 <var> 的值。如果 <var> 不存在或者是 NULL,就返回 <value> 。
${var:=value} :如果 <var> 存在且不是 NULL,就返回 <var> 的值。如果 <var> 不存在或者是 NULL,就把 <var> 设置为 <value> 并返回 <value> 。
${var:+value} :如果 <var> 存在且不是 NULL,就返回 <var> 的值。如果 <var> 不存在或者是 NULL,就返回 NULL。
${var:?value} :如果 <var> 存在且不是 NULL,就返回 <var> 的值。如果 <var> 不存在或者是 NULL,就退出命令或脚本并显示用 <value> 设置的错误消息。如果未设置 <value> ,就显示默认的错误消息 “Parameter null or not set”。
请注意前四个和后四个定义之间的细微差异。后一组定义在变量名和替换操作符之间包含一个冒号(: ),这会额外检查变量是否是 NULL。在考虑用替换操作符给变量赋值时,要注意的另一个重要问题是,给变量赋值应用的规则与以一般方式从命令行或脚本定义变量的规则相同。不能用新值覆盖受保护的保留变量(例如,$1 、$2 、$3 )。
清单 18 中的示例演示替换操作符的使用方法。注意,可以组合使用多个替换操作符,见脚本的最后一行。
清单 18. 使用替换操作符
# cat subops_examples
#!/usr/bin/ksh
_ARG1="${1}"
echo "Test 1A: The 1st argument is ${_ARG1-'ATC'}"
echo "Test 1B: The 1st argument is ${_ARG1:-'ATC'}"
_ARG2="${2}"
echo "Test 2A: The 2nd argument is ${_ARG2-'AMDC'}"
echo "Test 2B: The 2nd argument is ${_ARG2:-'AMDC'}"
_ARG3="${3}"
echo "Test 3A: The 3rd argument is ${_ARG3='PAC'}"
echo "Test 3B: The 3rd argument is ${_ARG3:='PAC'}"
_ARG4="${4}"
echo "Test 4A: ${4:+'The 4th argument was supplied'}"
echo "Test 5: If the 4th argument was provided, the value would be
${4:?'The 4th argument was not supplied.'}. Otherwise, we will not
see this message and get an error instead."
_ARG8="${8}"
echo "${_ARG8:=${7:-${6:-${5:-No Arguments were supplied after the 4th}}}}"
|
清单 19 显示在不提供参数的情况下执行这个脚本的情况。
清单 19. 不带参数执行脚本
# ./subops_examples
Test 1A: The 1st argument is
Test 1B: The 1st argument is ATC
Test 2A: The 2nd argument is
Test 2B: The 2nd argument is AMDC
Test 3A: The 3rd argument is
Test 3B: The 3rd argument is PAC
Test 4A:
./subops_examples[18]: 4: The 4th argument was not supplied.
|
清单 20 显示在只提供三个参数的情况下执行脚本会发生什么。
清单 20. 用三个参数执行脚本
# ./subops_examples arg1 arg2 arg3
Test 1A: The 1st argument is arg1
Test 1B: The 1st argument is arg1
Test 2A: The 2nd argument is arg2
Test 2B: The 2nd argument is arg2
Test 3A: The 3rd argument is arg3
Test 3B: The 3rd argument is arg3
Test 4A:
./subops_examples[18]: 4: The 4th argument was not supplied.
|
清单 21 显示在只提供四个参数的情况下会发生什么。
清单 21. 用四个参数执行脚本
# ./subops_examples arg1 arg2 arg3 arg4
Test 1A: The 1st argument is arg1
Test 1B: The 1st argument is arg1
Test 2A: The 2nd argument is arg2
Test 2B: The 2nd argument is arg2
Test 3A: The 3rd argument is arg3
Test 3B: The 3rd argument is arg3
Test 4A: The 4th argument was supplied
Test 5: If the 4th argument was provided, the value would be
arg4. Otherwise, we will not see this message and get an
error instead.
No Arguments were supplied after the 4th
|
清单 22 显示提供所有五个参数时的情况。
清单 22. 用所有五个参数执行脚本
# ./subops_examples arg1 arg2 arg3 arg4 arg5
Test 1A: The 1st argument is arg1
Test 1B: The 1st argument is arg1
Test 2A: The 2nd argument is arg2
Test 2B: The 2nd argument is arg2
Test 3A: The 3rd argument is arg3
Test 3B: The 3rd argument is arg3
Test 4A: The 4th argument was supplied
Test 5: If the 4th argument was provided, the value would be
arg4. Otherwise, we will not see this message and get an
error instead.
arg5
|
清单 23 显示提供七个参数时的情况。注意,因为提供了七个参数,所以第五和第六个参数被忽略。
清单 23. 用七个参数执行脚本
# ./subops_examples arg1 arg2 arg3 arg4 arg5 arg6 arg7
Test 1A: The 1st argument is arg1
Test 1B: The 1st argument is arg1
Test 2A: The 2nd argument is arg2
Test 2B: The 2nd argument is arg2
Test 3A: The 3rd argument is arg3
Test 3B: The 3rd argument is arg3
Test 4A: The 4th argument was supplied
Test 5: If the 4th argument was provided, the value would be
arg4. Otherwise, we will not see this message and get an
error instead.
arg7
|
结束语
阅读本文之后,您应该更好地理解了 UNIX 用户输入的那些 “奇怪的” 字符。您了解了如何把数据重定向到 stdin 或 stdout,如何使用管道,以及如何在 UNIX 中使用操作符;这些都有助于您编写出更强大的脚本,更好地捕捉错误,使逻辑更简洁清晰。祝您好运!
参考资料
学习
|