像其他UNIX操作系统和Linux一样,IBM AIX操作系统具有一些强大的工具,可以使系统管理员,开发人员和用户应对日常任务并简化其客户的业务和生活。 UNIX中的一种这样的工具是能够编写Shell脚本来自动执行任务,从而简化困难或漫长而繁琐的工作的能力。
尽管一些在UNIX上已经工作了两年的人涉足了shell脚本编写,但是他们仍然最有可能了解操作系统的来龙去脉,并且还没有掌握脚本编写的知识。 本文为那些想了解更多有关Shell脚本以及如何开始编写更高级脚本的人提供了提示。 它提供了总体编程的基本原理,包括如何简化脚本,如何使脚本尽可能灵活,如何编写干净的脚本,在脚本内部进行文档编制以及调试脚本。
把事情简单化
人们在学习如何很好地编写脚本外壳时遇到的一个问题是重复他们已经在脚本中完成的工作。 他们可以只创建一个函数来处理脚本两个区域的工作,而不是复制其工作并更改几个硬编码的值。 创建集中功能还可以标准化并提供统一的脚本。 如果函数可以在脚本的一个区域中工作,则可以肯定它也可以在脚本的其他地方工作。
例如,清单1所示的脚本应该被压缩和简化为一个更小巧,更简洁的程序。
清单1.可以简化的脚本示例
#!/usr/bin/ksh
if [[ $# -lt 2 ]]
then
echo "Usage: ${0##*/} <file name #1> <file name #2>
exit 0
fi
if [[ ! -f "${1}" ]]
then
echo "Unable to find file '${1}'"
exit 1
fi
if [[ ! -r "${1}" ]]
then
echo "Unable to read file '${1}'"
exit 2
fi
gzip ${1}
ls -l ${1}.gz
if [[ ! -f "${2}" ]]
then
echo "Unable to find file '${2}'"
exit 1
fi
if [[ ! -r "${2}" ]]
then
echo "Unable to read file '${2}'"
exit 2
fi
gzip ${2}
ls -l ${2}.gz
这个脚本看起来糟透了! (非常感谢,这只是一个示例。)应尽可能精简脚本。 为了使读者满意,清单2提供了一个更干净的版本。
清单2.清单1脚本的精简示例
#!/usr/bin/ksh
exit_msg() {
[[ $# -gt 1 ]] && echo "${0##*/} (${1}) - ${2}"
exit ${1:-0}
}
[[ $# -lt 2 ]] && exit_msg 0 "Usage: ${0##*/} <file name #1> <file name #2>
for _FNAME in $@
do
[[ ! -f "${_FNAME}" ]] && exit_msg 1 "Unable to find file '${_FNAME}'"
[[ ! -r "${_FNAME}" ]] && exit_msg 2 "Unable to read file '${_FNAME}'"
gzip ${_FNAME}
ls -l ${_FNAME}.gz
done
注意到差异了吗? 通过添加一个简单的函数来显示消息并使用适当的返回码退出,并将所有内容移动到for循环中,该脚本看起来更简洁并且更易于理解。
保持灵活性
编程和Shell脚本新手遇到的另一个问题是将静态值硬编码到程序或Shell脚本中。 这限制了脚本的灵活性,总之是不好的编程。 为了使管理员或开发人员不必经常修改脚本以使其与其他值一起使用,请使用变量并为脚本或函数提供参数。
例如,清单3是一个编写不佳且不灵活的脚本的示例。
清单3.不灵活的脚本示例
#!/bin/bash
if [[ -f /home/cormany/FileA ]]
then
echo "Found file '/home/cormany/FileA'"
elif [[ -f /home/cormany/DirA/FileA ]]
then
echo "Found file '/home/cormany/DirA/FileA'"
else
echo "Unable to find file FileA"
fi
该脚本有效,但仅限于在两个位置搜索单个文件。
为了进一步说明这一点,清单4中的脚本提供了相同的感觉,但允许用户在任何位置搜索任何文件。
清单4.使脚本更灵活
#!/bin/bash
exit_msg() {
[[ $# -gt 1 ]] && echo "${0##*/} (${1}) - ${2}"
exit ${1:-0}
}
[[ $# -lt 2 ]] && exit_msg 1 "Usage: ${0##*/} <file name> <location>"
_FNAME="${1}"
_DNAME="${2}"
[[ ! -d "${_DNAME}" ]] && exit_msg 2 "Unable to read or find directory '${_DNAME}'"
if [[ -f "${_DNAME}/${_FNAME}" ]]
then
exit_msg 0 "Found file '${_DNAME}/${_FNAME}'"
else
exit_msg 3 "Unable to find file '${_DNAME}/${_FNAME}'"
fi
此示例更加灵活,因为它允许用户在任何目录中输入他们想要搜索的任何文件。
给他们选择
在编写shell脚本时,一些用户可能会说:“拥有它真是太好了!” 或“我希望能够做到这一点!” 而其他人可能不一定同意,也可能不想执行相同的操作。 人们喜欢选择,为什么不给他们一些呢? 内置的shell命令getopt完成该工作。
清单5提供了getopt如何在AIX中工作的基本示例。
清单5. getopt示例
#!/usr/bin/ksh
_ARGS=`getopt -o x --long xxxxx -n ${0##*/} -- "$@"`
while [[ $# -gt 0 ]]
do
case "${1}" in
-x|--xxxxx) echo "Arg x hit!"; shift;;
--) shift; break;;
*) echo "Invalid Option: ${1}"; break;;
esac
done
当执行包含脚本getopt ,名为opttest,使用的有效的参数-x -OR --xxxxx , getopt识别开关,并执行的情况下开关内的代码:
# ./hm -x
Arg x hit!
再次使用无效的开关或选项:
# ./hm -a
Invalid Option: -a
文件,文件,文件
在我们的职业生涯中,我们所有人都一时成为这个问题的牺牲品。 您被要求看一个十年前由不再为公司工作的人编写的脚本。 没问题,你说? 通常,这不是问题,但是如果脚本很复杂,执行您不习惯的命令,以与您惯用的风格不同的方式编写命令,或者只是行不通,那就是获得有关最初创建脚本时该人在想什么的提示非常有用。 在其他时候,您可能是开发脚本的人,您认为该脚本将被再次使用。 或者,也许您编写了一个庞大的脚本,您已经使用了数周的时间,并且从内到外都知道,但是如果其他人看着它,那么该人将完全感到困惑。 这些只是为什么文档有时对开发人员和脚本对用户同样重要的几个例子。
从一段代码中获取清单6中所示的功能。
清单6.没有注释的脚本示例
confirm_and_exit() {
[[ ${_DEBUG_LEVEL} -ge 3 ]] && set -x
while [[ -z ${_EXIT_ANS} ]]
do
cup_echo "Are you sure you want to exit? [Y/N]
\c" ${_PROMPT_ERR_ROW} ${_PROMPT_ERR_COL}
${_TPUT_CMD} cnorm
read ${_NO_EOL_FLAG:+${_READ_FLAG:-'-n'}} ${_NO_EOL_FLAG} _EXIT_ANS
${_TPUT_CMD} civis
done
case ${_EXIT_ANS} in
[Nn]) unset _EXIT_ANS; return 0;;
[Yy]) exit_msg 0 1 "Exiting Script";;
*) invalid_selection ${_EXIT_ANS}; unset _EXIT_ANS;;
esac
return 0
}
如果您使用Shell脚本已有一段时间,则可以阅读该内容。 但是,仅学习脚本的人会看到此内容,但不知道此功能在做什么。 花一些额外的时间在脚本中添加评论可能会有所作为。 清单7显示了带有注释的相同功能。
清单7.带有注释的脚本示例
#########################################
# function confirm_and_exit
#########################################
confirm_and_exit() {
# if the debug level is set to 3 or higher, send every evaluated line to stdout
[[ ${_DEBUG_LEVEL} -ge 3 ]] && set â????x
# Continue to prompt the user until they provide a valid answer
while [[ -z ${_EXIT_ANS} ]]
do
# prompt user if they want to exit the script
# cup_echo function calls tput cup <x> <y>
# syntax:
# cup_echo <string to display> <row on stdout to display>
<column on stdout to display>
cup_echo "Are you sure you want to exit? [Y/N]
\c" ${_PROMPT_ERR_ROW} ${_PROMPT_ERR_COL}
# change cursor to normal via tput
${_TPUT_CMD} cnorm
# read value entered by user
# if _NO_EOL_FLAG is supplied, use value of _READ_FLAG or â????-nâ????
# if _NO_EOL_FLAG is supplied, use value as characters aloud on read
# assign value entered by user to variable _EXIT_ANS
read ${_NO_EOL_FLAG:+${_READ_FLAG:-'-n'}} ${_NO_EOL_FLAG} _EXIT_ANS
# change cursor to invisible via tput
${_TPUT_CMD} civis
done
# if user entered â????nâ????, return to previous block of code with return code 0
# if user entered â????yâ????, exit the script
# if user entered anything else, execute function invalid_selection
case ${_EXIT_ANS} in
[Nn]) unset _EXIT_ANS; return 0;;
[Yy]) exit_msg 0 1 "Exiting Script";;
*) invalid_selection ${_EXIT_ANS}; unset _EXIT_ANS;;
esac
# exit function with return code 0
return 0
}
对于这么小的函数来说,这似乎很乏味并且有些过大,但是对于新手shell脚本编写者或仅查看该函数的人员来说,注释的价值是无价的。
Shell脚本中注释的另一个非常有用的用法是解释可能是什么变量以及解释返回码。
清单8中的示例是从Shell脚本的开头开始的。
清单8.未记录的变量示例
#!/usr/bin/bash
trap 'exit_msg 1 0 "Signal Caught. Exiting..."' HUP INT QUIT KILL ABRT
trap 'window_size_changed' WINCH
_MSG_SLEEP_TIME=3
_RETNUM_SIZE=6
_DEBUG_LEVEL=0
_TMPDIR="/tmp"
_SP_LOG="${0##*/}.log"
_SP_REQUESTS="${HOME}/sp_requests"
_MENU_ITEMS=15
LESS="-P LINE\: %l"
export _SP_REQUESTS _TMPDIR _SP_LOG _DB_BACKUP_DIR
export _DEBUG_LEVEL _NEW_RMSYNC _RMTOTS_OFFSET_COL
同样,很难理解陷阱应该做什么或每个变量中的值是什么。 除非您完全遵循脚本,否则这些变量将毫无意义。 此外,没有提及脚本中使用的任何返回码。 这可能会使对Shell脚本中的问题进行故障诊断的难度大大超过其实际需要。 在清单8的行中添加一些注释,并添加使用返回代码及其描述的部分,以最大程度地减少混乱。 看一下下面的清单9。
清单9.记录的变量示例
#!/usr/bin/bash
#########################################################################
# traps
#########################################################################
# trap when a user is attempting to leave the script
trap 'exit_msg 1 0 "Signal Caught. Exiting..."' HUP INT QUIT KILL ABRT
trap 'window_size_changed' WINCH # trap when a user has resized the window
#########################################################################
#########################################################################
# defined/exported variables
#########################################################################
_MSG_SLEEP_TIME=3 # seconds to sleep for all messages
# (if not defined, default will is 1 second)
_CUSTNUM_SIZE=6 # length of a customer number in this location
# (if not defined, default is 6)
_DEBUG_LEVEL=0 # log debug messages. log level is accumulative
# (i.e. 1 = 1, 2 = 1 & 2, 3 = 1, 2, & 3)
# (if not defined, default is 0)
# Log levels:
# 0 = No messages
# 1 = brief messages (start script, errors, etc)
# 2 = environment setup (set / env)
# 3 = set -x (A LOT of spam)
_TMPDIR="/tmp" # directory to put work/tmp files
# (if not defined, default is /tmp)
_SP_LOG="${0##*/}.log" # log of script events
_SP_REQUESTS="${HOME}/sp_requests"
# file to customer record requests,
# also read at startup
_MENU_ITEMS=15 # default number of items to display per page
# (it not defined, default is 10)
LESS="-P LINE\: %l" # format 'less' prompt. MAN less if more info
# export the variables defined above
export _MSG_SLEEP_TIME _CUSTNUM_SIZE _DEBUG_LEVEL _TMPDIR
_SP_LOG _SP_REQUESTS _MENU_ITEMS
#########################################################################
看起来不是更好吗? 一切都是组织和详细的,初次阅读脚本的人将有更好的机会了解程序的功能。
调试
您已经完成了脚本的编写,现在是时候第一次运行该程序了。 但是,当您执行脚本时,会显示一些意外错误。 怎么办? 没有人是完美的,并且从头开始编写脚本直到获得错误为止要花费大量的时间和经验,即使在大多数情况下,即使这样也无法避免有人轻易丢失一个字符或换位几个字符。 不用担心:AIX的shell和其他类型的UNIX和Linux都涵盖了它,可以帮助调试。
例如,清单10中的外壳程序脚本make_errors (正确的说是这样)已经编写并准备好执行。
清单10.带有错误的脚本示例
#!/bin/bash
_X=1
while [[ ${_X} -le 10 ]]
do
[[ ${_X} -lt 5 ]] && echo "X is less than 5!
_Y=`expr ${_X) + 1`
if [[ ${_Y} -eq 6 ]]
echo "Y is now equal to ${_Y}"
fi
_X=${_Y}
done
但是,最初执行脚本时,显示以下错误:
# ./make_errors
./make_errors: line 11: unexpected EOF while looking for matching `"'
./make_errors: line 16: syntax error: unexpected end of file
Vim是您已经使用但不知道的一种很棒的调试工具。 Vim是强大的文本编辑器,但在调试方面也很有帮助。 如果您将.exrc或.vimrc文件设置为显示某些错误情况下的颜色,则Vim将为您完成大部分工作,如下图1所示。
图1.用Vim调试
第一个错误( line 11: unexpected EOF while looking for matching `"' )表示第11行出现了问题,但是看完该行之后,看起来没有什么问题。请看一下第9行。 echo显字符串的末尾缺少( " )。 这是一个很好的示例,说明为什么在调试时必须将脚本视为一个整体。 显示的行号可能并不总是实际错误的来源。 第11行报告了一个错误,因为第9行开始用双引号封装字符串,但是直到第11行才完全封装了该字符串。要纠正该错误,请在第9行的末尾添加双引号。
其他一些东西也公然显示为错误。 在第11行中,变量_X的值后面是用红色突出显示的闭合括号( ) 。 这是Vim为您做的事,告诉您有问题。 看起来变量_X的值以大括号( { )开头,但没有以大括号( } )结尾。 只需将)更改为}就可以了。
做得好:到目前为止,已修复两个错误。 再次运行脚本,看看会发生什么:
./make_errors: line 12: syntax error near unexpected token `fi'
./make_errors: line 12: ` fi'
另一个错误。 该错误表明第12行存在问题,但该行只有一个fi完成if语句。 怎么了 请记住上一个错误发生了什么。 并非所有错误都源于外壳程序报告问题所在的行。 Shell只是在报告发生错误的位置,但这可能意味着错误原因是在Shell报告失败之前开始的。 可以肯定的是,在这么小的脚本中,错误可能出在实际的if语句中。 回想基本的shell脚本逻辑, if语句由if , then和fi 。 综观条件语句,它看起来像有一个缺失then 。 只需将then添加到脚本中。 完成后,脚本应类似于清单11。
清单11.清单10中的更正脚本
#!/bin/bash
_X=1
while [[ ${_X} -le 10 ]]
do
[[ ${_X} -lt 5 ]] && echo "X is less than 5!"
_Y=`expr ${_X} + 1`
if [[ ${_Y} -eq 6 ]]
then
echo "Y is now equal to ${_Y}"
fi
_X=${_Y}
done
再运行一次脚本:
# ./make_errors
X is less than 5!
X is less than 5!
X is less than 5!
X is less than 5!
Y is now equal to 6
恭喜你! 该脚本现在可以按预期工作。
set -x选项
有时,执行shell脚本的基本故障排除步骤并不像上面的示例那样简单。 如果所有其他方法都失败了,那么您将把头撞在墙上,却不知道脚本为什么失败,最后一步是拔出大把枪! Ksh,Bash和其他现代Shell在其set命令中包含switch -x 。 使用set â????x选项,将评估的每个命令展开并显示到stdout。 为了使评估的代码脱颖而出, set â????x使用PS4变量的值并将其附加到显示的每一行代码中。 请记住,这可能会吸收很多文本,因此在浏览时请耐心等待。
减少上一个示例的循环计数,将set -x添加到脚本的开头以及注释,然后执行它,如清单12所示。
清单12. set -x的示例
#!/bin/bash
set -x
# loop through and display some test statements
_X=1
while [[ ${_X} -le 4 ]]
do
[[ ${_X} -lt 2 ]] && echo "X is less than 2!
_Y=`expr ${_X} + 1`
if [[ ${_Y} -eq 3 ]]
then
echo "Y is now equal to ${_Y}"
fi
_X=${_Y}
done
在执行脚本之前,将PS4更改为会突出的内容:
# export PS4="DEBUG => "
接下来,向自己发送可能非常有价值的信息,如清单13所示。
清单13. set -x的示例输出
# ./make_errors
DEBUG => _X=1
DEBUG => [[ 1 -le 4 ]]
DEBUG => [[ 1 -lt 2 ]]
DEBUG => echo 'X is less than 2!'
X is less than 2!
DDEBUG => expr 1 + 1
DEBUG => _Y=2
DEBUG => [[ 2 -eq 3 ]]
DEBUG => _X=2
DEBUG => [[ 2 -le 4 ]]
DEBUG => [[ 2 -lt 2 ]]
DDEBUG => expr 2 + 1
DEBUG => _Y=3
DEBUG => [[ 3 -eq 3 ]]
DEBUG => echo 'Y is now equal to 3'
Y is now equal to 3
DEBUG => _X=3
DEBUG => [[ 3 -le 4 ]]
DEBUG => [[ 3 -lt 2 ]]
DDEBUG => expr 3 + 1
DEBUG => _Y=4
DEBUG => [[ 4 -eq 3 ]]
DEBUG => _X=4
DEBUG => [[ 4 -le 4 ]]
DEBUG => [[ 4 -lt 2 ]]
DDEBUG => expr 4 + 1
DEBUG => _Y=5
DEBUG => [[ 5 -eq 3 ]]
DEBUG => _X=5
DEBUG => [[ 5 -le 4 ]]
如您所见,这里有很多信息:每个命令都已被评估和执行。 还要注意,shell脚本中的注释位置未显示在调试信息中。 这是因为字符串是注释,因此在求值后不会执行。 幸运的是,在您进行了原始更正之后,此脚本没有任何问题!
使用set -x时要记住的一件事是,如果您要评估的脚本具有内部功能,则set -x如果放在代码的根主体中,则会继承其子功能。 但是,如果仅set -x放置在内部函数中,则只有内部函数中调用的代码和子函数才会包含在debug选项中; shell脚本的根主体将不会,因为它对调用例程的子函数一无所知。
结论
无论是Shell脚本, C ,Java™语言还是其他正在使用的语言,我们都在不断改进我们的编程方法。 坚持简化工作,保持其清洁和灵活以及记录代码的基本规则,很快您将在学习到的调试方法的帮助下,编写一流的Shell脚本。 祝好运!
翻译自: https://www.ibm.com/developerworks/aix/library/au-speakingunix_shellscripttech/index.html