关于 shell 脚本的最佳概念性介绍来自一部古老的 AT&T 培训视频。视频中,Brian W. Kernighan(awk 中的 “K”)和 Lorinda L. Cherry(bc 的合著者)演示了 UNIX 的基本原则之一是如何授权用户利用现有实用程序来创建复杂和定制的工具。
正如 Kernighan 所说:“基本上可以将 UNIX 系统程序视为 […] 可以用以创造事物的构建块。[…] 管道的概念是 [UNIX] 系统的基本贡献;你可以拿一堆程序……并将它们首尾相连,以便数据从左边的程序流向右边的程序,而系统本身会处理所有连接。程序本身对连接一无所知;就它们而言,它们只是在与终端对话。”
他谈论的是赋予日常用户编程能力。
POSIX 操作系统在比喻意义上是其自身的 API。如果你能弄清楚如何在 POSIX shell 中完成一项任务,那么你就可以自动化该任务。这就是编程,而这种日常 POSIX 编程方法的主要载体是 shell 脚本。
顾名思义,shell *脚本* 是你希望计算机执行的操作的逐行配方,就像你手动执行一样。
由于 shell 脚本由常见的日常命令组成,因此熟悉 UNIX 或 Linux(统称为 POSIX)shell 会很有帮助。你使用 shell 的次数越多,编写新脚本就越容易。这就像学习一门外语:你内化的词汇越多,就越容易形成复杂的句子。
当你打开终端窗口时,你正在打开一个 *shell*。市面上有几种 shell,本教程适用于 bash、tcsh、ksh、zsh,可能还有其他 shell。在接下来的几个部分中,我确实提供了一些 bash 特定的示例,但最终脚本放弃了这些示例,因此你可以切换到 bash 来学习有关设置变量的课程,或者进行一些简单的 语法调整。
如果你是新手,只需使用 bash。它是一个很好的 shell,具有许多友好的功能,并且是 Linux、Cygwin、WSL、Mac 上的默认 shell,也是 BSD 上的一个选项。
Hello world
你可以从终端窗口生成你自己的 hello world 脚本。注意你的引号;单引号和双引号具有不同的效果。
$ echo "#\!/bin/sh" > hello.sh
$ echo "echo 'hello world' " >> hello.sh
正如你所看到的,编写 shell 脚本(除了第一行之外)包括将命令回显或粘贴到文本文件中。
要将脚本作为应用程序运行
$ chmod +x hello.sh
$ ./hello.sh
hello world
或多或少,这就是全部内容!
现在让我们处理一些更有用的东西。
Despacer(去空格器)
如果说有一件事会混淆计算机和人类的交互,那就是文件名中的空格。你在互联网上见过它:URL 像 http: //example.com/omg%2ccutest%20cat%20photo%21%211.jpg。或者,当你运行一个简单的命令时,空格可能让你感到困惑
$ cp llama pic.jpg ~/photos
cp: cannot stat 'llama': No such file or directory
cp: cannot stat 'pic.jpg': No such file or directory
解决方案是用反斜杠或引号“转义”空格
$ touch foo\ bar.txt
$ ls "foo bar.txt"
foo bar.txt
这些都是重要的技巧,但它变得不方便,所以为什么不编写一个脚本来删除文件名中那些烦人的空格呢?
创建一个文件来保存脚本,以 “shebang”(#!)开头,让你的系统知道该文件应该在 shell 中运行
$ echo '#!/bin/sh' > despace
好的代码始于文档。定义目的是让我们知道要达到的目标。这是一个好的 README
despace is a shell script for removing spaces from file names.
Usage:
$ despace "foo bar.txt"
现在让我们弄清楚如何手动完成它,并在我们进行的过程中构建脚本。
假设你有一个名为 “foo bar.txt” 的文件,它位于一个空的目录中,尝试这样做
$ ls
hello.sh
foo bar.txt
计算机完全是关于输入和输出的。在这种情况下,输入是 ls 特定目录的请求。输出是你所期望的:该目录中文件的名称。
在 UNIX 中,输出可以通过 “管道” 作为另一个命令的输入发送。管道另一侧的任何东西都充当一种过滤器。tr 实用程序恰好是专门设计用于修改通过它的字符串的;对于此任务,请使用 --delete 选项删除引号中定义的字符。
$ ls "foo bar.txt" | tr --delete ' '
foobar.txt
现在你得到了你需要的输出。
在 BASH shell 中,你可以将输出存储为变量。将变量视为一个空盒子,你可以在其中放置信息以进行存储
$ NAME=foo
当你需要取回信息时,你可以通过引用以美元符号 ($) 开头的变量名来查看盒子。
$ echo $NAME
foo
要获取你的去空格命令的输出并将其放在一边以供以后使用,请使用变量。要将命令的 *结果* 放入变量中,请使用反引号
$ NAME=`ls "foo bar.txt" | tr -d ' '`
$ echo $NAME
foobar.txt
这让你离目标完成了一半,你有一种方法可以从源文件名确定目标文件名。
到目前为止,脚本看起来像这样
#!/bin/sh
NAME=`ls "foo bar.txt" | tr -d ' '`
echo $NAME
脚本的第二部分必须执行重命名。你可能已经知道该命令
$ mv "foo bar.txt" foobar.txt
但是,请记住在脚本中你正在使用变量来保存目标名称。你知道如何引用变量
#!/bin/sh
NAME=`ls "foo bar.txt" | tr -d ' '`
echo $NAME
mv "foo bar.txt" $NAME
你可以通过将你的第一个草稿标记为可执行文件并在你的测试目录中运行它来试用它。确保你有一个名为 “foo bar.txt” 的测试文件(或你在脚本中使用的任何名称)。
$ touch "foo bar.txt"
$ chmod +x despace
$ ./despace
foobar.txt
$ ls
foobar.txt
Despacer v2.0
该脚本有效,但与你的文档描述不完全一致。它目前非常具体,仅适用于名为 foo\ bar.txt 的文件,不适用于其他文件。
POSIX 命令将其自身称为 $0,并将其后按顺序键入的任何内容称为 $1、$2、$3 等。你的 shell 脚本算作 POSIX 命令,所以尝试将 foo\ bar.txt 替换为 $1。
#!/bin/sh
NAME=`ls $1 | tr -d ' '`
echo $NAME
mv $1 $NAME
创建一些新的测试文件,文件名中带有空格
$ touch "one two.txt"
$ touch "cat dog.txt"
然后测试你的新脚本
$ ./despace "one two.txt"
ls: cannot access 'one': No such file or directory
ls: cannot access 'two.txt': No such file or directory
看起来你发现了一个 bug!
这个 bug 实际上不是 bug,因为就其本身而言;一切都按设计工作,但不是你想要的工作方式。你的脚本正在将 $1 变量 “扩展” 为它的确切内容:“one two.txt”,并且随之而来的是你试图消除的那个麻烦的空格。
答案是将变量用引号括起来,就像你用引号括起文件名一样
#!/bin/sh
NAME=`ls "$1" | tr -d ' '`
echo $NAME
mv "$1" $NAME
再进行一两次测试
$ ./despace "one two.txt"
onetwo.txt
$ ./despace c*g.txt
catdog.txt
此脚本的行为与任何其他 POSIX 命令相同。你可以像期望使用任何 POSIX 实用程序一样,将它与其他命令结合使用。你可以将它与命令结合使用
$ find ~/test0 -type f -exec /path/to/despace {} \;
或者你可以将它用作循环的一部分
$ for FILE in ~/test1/* ; do /path/to/despace $FILE ; done
等等。
Despacer v2.5
despace 脚本是功能性的,但从技术上讲,它可以优化,并且可以使用一些可用性改进。
首先,实际上不需要变量。shell 可以一次性计算出所需的信息。
POSIX shell 具有操作顺序。就像你在数学中首先求解括号中的语句一样,shell 在执行命令之前解析反引号 (`) 或 BASH 中的 $() 中的语句。因此,语句
$ mv foo\ bar.txt `ls foo\ bar.txt | tr -d ' '`
被转换为
$ mv foo\ bar.txt foobar.txt
然后执行实际的 mv 命令,只留下 foobar.txt。
了解了这一点,你可以将 shell 脚本精简为
#!/bin/sh
mv "$1" `ls "$1" | tr -d ' '`
这看起来令人失望地简单。你可能会认为将其简化为单行脚本会使脚本变得不必要,但 shell 脚本不必有很多行才能有用。即使是保存键入简单命令的脚本仍然可以让你免于致命的拼写错误,这在涉及移动文件时尤其重要。
此外,你的脚本仍然可以改进。额外的测试揭示了一些弱点。例如,在没有参数的情况下运行 despace 会呈现一个无益的错误
$ ./despace
ls: cannot access '': No such file or directory
mv: missing destination file operand after ''
Try 'mv --help' for more information.
这些错误令人困惑,因为它们是针对 ls 和 mv 的,但就用户所知,他们运行的不是 ls 或 mv,而是 despace。
如果你考虑一下,这个小脚本甚至不应该尝试重命名文件,如果它在首先没有获得作为命令一部分的文件,所以尝试使用你所知道的关于变量的知识以及 test 函数。
If 和 test
if 语句是将你的小 despace 实用程序从脚本转变为程序的语句。这是严肃的代码领域,但别担心,它也很容易理解和使用。
if 语句是一种开关;如果某件事为真,那么你将做一件事,如果为假,你将做一些不同的事情。这种 if-then 指令正是计算机最擅长的二进制决策;你所要做的就是为计算机定义什么是真或假,以及结果要做什么。
你测试真或假的最简单方法是 test 实用程序。你不会直接调用它,而是使用它的语法。在终端中尝试这样做
$ if [ 1 == 1 ]; then echo "yes, true, affirmative"; fi
yes, true, affirmative
$ if [ 1 == 123 ]; then echo "yes, true, affirmative"; fi
$
这就是 test 的工作方式。你有各种各样的简写可供选择,你将使用的一种是 -z 选项,它检测字符字符串的长度是否为零 (0)。这个想法在你的 despace 脚本中转化为
#!/bin/sh
if [ -z "$1" ]; then
echo "Provide a \"file name\", using quotes to nullify the space."
exit 1
fi
mv "$1" `ls "$1" | tr -d ' '`
if 语句被分成单独的行以提高可读性,但概念保持不变:如果 $1 变量内的数据为空(不存在零个字符),则打印错误语句。
尝试一下
$ ./despace
Provide a "file name", using quotes to nullify the space.
$
成功!
好吧,实际上这是一个失败,但这是一个 *漂亮的* 失败,更重要的是,一个 *有帮助的* 失败。
注意语句 exit 1。这是一种 POSIX 应用程序向系统发送警报的方式,表明它遇到了错误。此功能对于你自己以及可能希望在脚本中使用 despace 的其他人非常重要,这些脚本依赖于 despace 成功才能使其他一切正常发生。
最后的改进是添加一些东西来保护用户免于意外覆盖文件。理想情况下,你会将此选项传递给脚本,使其成为可选的,但为了简单起见,你将对其进行硬编码。-i 选项告诉 mv 在覆盖已存在的文件之前请求许可
#!/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 ' '`
现在你的 shell 脚本很有帮助、很有用且很友好——而且你是一名程序员,所以不要停下来。学习新命令,在终端中使用它们,记下你所做的事情,然后编写脚本。最终,你会让自己失业,而你余生将会在你的机器人仆从运行 shell 脚本时放松身心。
祝你编程愉快!
8 条评论