在 Shell 脚本入门 中,我介绍了 shell 脚本的基础知识,创建了一个非常方便的 “despacer” 脚本来删除文件名中令人讨厌的空格。现在我将扩展这个想法,使其成为一个也可以替换其他字符的应用程序。这介绍了用于解析选项的 shift 方法以及创建自定义函数。
本文适用于 bash、ksh 和 zsh shell。其中的大多数原则也适用于 tcsh,但 tcsh 在语法和逻辑流程方面存在显著差异,因此值得单独撰写一篇文章。如果您正在使用 tcsh 并想为自己调整本课程,请学习函数技巧(tcsh hacks)和 条件语句 的语法,然后尝试一下。将奖励额外积分。
参数和选项解析
如果您一直在使用 shell 脚本来自动化日常任务,那么您应该熟悉这个概念:您可以从 POSIX shell 执行的任何命令也可以编写成脚本。您可以稍后在任何时候运行该脚本,以执行一系列命令,否则您将不得不手动执行这些命令。这种做法是开始编程的好方法,但 shell 脚本不仅仅是供您的计算机盲目读取和执行的脚本;它们可以即时修改,以便它们适应您在每次运行时所需的内容。
为了说明这种差异,请参考上一篇文章中的简单脚本
$ ~/bin/depspacer "foo bar.txt"
$ ls
foobar.txt
更高级的版本可能会提供选项
$ ~/bin/hello-2.0.sh --from ' ' --to '_' "foo bar.txt"
$ ls
foo_bar.txt
更好吗?当然是!
为了将即时用户输入传递到脚本中,您需要学习如何解析参数和选项。解析输入的方法有很多种,并且因语言而异,但对于 POSIX shell,您可以使用一系列 if/then 语句来顺序检查每个参数。
回想一下,POSIX 命令将自身视为 $0,第一个参数是 $1,然后是 $2,依此类推。如果您在终端中键入 despace --from ' ',那么 despace 是 $0,--from 是 $1,' ' 是 $2,等等。
当前脚本如下所示
#!/bin/sh
if [ -z "$1" ]; then
echo "Provide a \"file name\", using quotes to nullify the space."
exit 1
fi
mv -i "$1" `ls "$1" | tr -d ' '`
它通过检查是否存在参数来解析恰好一个参数。为了扩展这个概念,请使用 test 编写一个 if/then 语句来分析每个参数。这是不完整的,但这里有一个示例,假设您希望能够发出以下命令
despacer --from ' ' --to '_' foo\ bar.txt
然后
if [ "$1" = "--from" ]; then
FROM="$2"
elif [ "$3" = "--to" ]; then
TO="$4"
fi
您可能会看到一个潜在的问题。它假设用户总是首先提供 --from 参数,或者至少会提供,然后提供 --to 参数。
解决这个问题的方法是不断循环遍历参数,依次处理每个参数,直到没有剩余参数为止。为了持续解析直到没有更多参数要解析,您可以使用带有 break 回退的 while 循环。为了强制脚本从一个参数进行到下一个参数,您将使用 shift。
while [ True ]; do
if [ "$1" = "--from" ]; then
FROM="$2"
shift 2
elif [ "$1" = "--to" ]; then
TO="$2"
shift 2
elif [ "$1" = "--help" ]; then
echo "despacer [ options ] FILE"
echo "--from character to remove"
echo "--to character to insert"
echo "--help print this help message"
shift 1
else
break
fi
done
ARG="$1"
在此代码块中,$1 和 $2 的值相对于匹配到的内容。当解析器找到 --from 时,--from 是 $1,其后的任何内容都是 $2。设置一个变量 (FROM=$2),并将这些参数移开 (shift 2)。
当解析器找到 --to 时,--to 是 $2,其后的任何内容都是 $2。设置一个变量 (TO=$2),并将这些参数移开 (shift 2)。
每当找到 --help 时,它都会变成 $1,并且不期望后面有任何内容 (shift 1)。
当没有剩余内容要解析时,将触发 else,脚本使用 break 跳出循环。
您可以合理地假设现在剩下的就是您想要去除空格的文件,因此将该值分配给 $ARG。
将其集成到 despacer 应用程序中
#!/bin/sh
while [ True ]; do
if [ "$1" = "--from" ]; then
FROM="$2"
shift 2
elif [ "$1" = "--to" ]; then
TO="$2"
shift 2
elif [ "$1" = "--help" ]; then
echo "despacer [ options ] FILE"
echo "--from character to remove"
echo "--to character to insert"
echo "--help print this help message"
shift 1
else
break
fi
done
ARG="$1"
mv -i "$ARG" `ls "$ARG" | tr "$FROM" "$TO"`
试用一下
$ ./despacer --from ' ' --to '_' "foo bar.txt"
$ ls
foo_bar.txt
它工作了!再次尝试,仅使用默认操作
$ ./despacer.sh "foo bar.txt"
mv: target 'bar.txt' is not a directory
您引入了一项新功能,但也引入了一个导致其失败的错误。更糟糕的是,它破坏了应用程序以前的工作方式。
主要问题是,如果未设置 $FROM 和 $TO 的值,则 tr 命令无法成功执行。此外,如果其中一个或两个都未设置,则 tr 将失败。
更棘手的是,如果设置了 $FROM 但 $TO 保留其默认值,则 tr 失败,因为 tr 需要两个参数,除非使用 --delete(或简写 -d)选项。
但是,如果您硬编码 -d 以使 tr 工作,那么 $TO 将永远无效。
这真是一个难题。您能猜到如何解决它吗?
有很多可能的答案,但我在这里解决这个问题的方法是操纵变量设置的顺序以及最终命令的启动方式。
但是,在继续之前,您需要了解自定义函数。
函数
大多数 shell 和编程语言都提供了一种方法,可以编写一系列语句一次,然后在代码中稍后重复使用它们。这样,您只需要编写一次代码,这意味着如果其执行过程中存在任何错误或逻辑错误,您只需要在一个地方查找错误。此外,您通常最终会编写更少的代码行,这通常表明进行了良好的优化。
在这个小脚本中,您需要调整的代码已经简化为一行,因此使用函数不会给您带来太多好处,但它确实有助于保持代码的整洁并使其易于理解。
要在 bash、ksh 或 zsh 脚本中创建函数
myfunction() {
echo "world"
}
稍后在您的脚本中调用此函数,方法是使用函数名称,就像它是一个命令一样
printf "hello \n"
myfunction
您可以使用此技术为您的脚本提供不同的命令序列,具体取决于变量的设置方式。
函数式解决方案
我之前提出的难题可以通过函数和巧妙的变量设置相结合来解决。
首先,提供一个默认的 FROM 值:一个空格 (' ')。
此外,将默认的最终命令设置为包含 tr -d "$FROM" 短语的函数。
假设没有提供任何选项,这些默认值将保持不变。
如果您提供一个新的 FROM 值,那么默认命令保持不变,只有 tr 删除的字符不同。
但是,如果您提供一个新的 TO 值,那么 tr 将不再处于删除模式,因此您将创建一个不同的函数,该函数在 tr 的本机转换模式下使用它。
首先,设置初始值
#!/bin/sh
FROM=' '
ACTION=trdelete
添加两个函数
trtarget() {
mv -i "${ARG}" `ls "${ARG}" | tr "${FROM}" "${TO}"`
}
trdelete() {
mv -i "${ARG}" `ls "${ARG}" | tr -d "${FROM}"`
}
如果提供了 --to 选项,则添加 ACTION 变量的覆盖
while [ True ]; do
if [ "${1}" = "--from" ]; then
FROM="${2}"
shift 2
elif [ "${1}" = "--to" ]; then
TO="${2}"
ACTION=trtarget
shift 2
elif [ "${1}" = "--help" ]; then
echo "despacer [ options ] FILE"
echo "--from character to remove"
echo "--to character to insert"
echo "--help print this help message"
shift 1
else
break
fi
done
ARG="${1}"
最后,执行最终命令,无论它最终是什么
$ACTION
这是一个功能齐全的 bash、ksh 或 zsh 脚本,具有变量覆盖、函数和选项解析。
如果这种黑客行为让您感到兴奋,请不要止步于此!在您的系统中找到您经常执行的其他任务,并找到一种方法使其对您自己更好。并非您编写的每个脚本或应用程序都必须开天辟地。有时,您编写代码只是为了避免自己犯愚蠢的错误,或者避免必须进行大量键入或单击操作。换句话说,没有哪个任务小到无法改进,因此请深入其中并改善您的工作环境。这只会导致以后出现更大更好的事情。
4 条评论