关于 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 脚本。注意引号;单引号和双引号具有不同的效果。
$ 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 条评论