Shell 脚本入门

608 位读者喜欢这篇文章。
System statistics with sar and the /proc filesystem

ajmexico。Jason Baker 修改。CC BY-SA 2.0。

关于 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,本教程适用于 bashtcshkshzsh,以及可能还有其他 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

如果说有什么东西会混淆计算机和人机交互,那就是文件名中的空格。你在互联网上见过它:像 http: //example.com/omg%2ccutest%20cat%20photo%21%211.jpg 这样的 URL。或者,当运行一个简单的命令时,空格可能让你绊倒

$ 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

要获取你的 despacing 命令的输出并将其放在一边以供以后使用,请使用变量。要将命令的 *结果* 放入变量中,请使用反引号

$ 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

看起来你发现了一个错误!

这个错误实际上不是一个错误;一切都按设计工作,但不是你想要的工作方式。你的脚本正在将 $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.

这些错误令人困惑,因为它们是针对 lsmv 的,但就用户而言,他们运行的不是 lsmv,而是 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 脚本。

祝你黑客愉快!

标签
Seth Kenlon
Seth Kenlon 是一位 UNIX 极客、自由文化倡导者、独立多媒体艺术家和 D&D 爱好者。他曾在电影和计算机行业工作,通常同时从事这两个行业。

8 条评论

对于有 bash 脚本编写先验知识的人来说,以上内容可能一目了然。但是,对于 bash 新手来说,你可能就像在说希腊语。你在没有解释的情况下引入了太多概念。即使在其他语言方面有超过 30 年的编程经验,我也无法理解你在做什么。这篇文章绝不会鼓励我开始 shell 脚本编写。

最好的学习方法是实践。如果你热衷于学习,请打开终端并按照课程进行操作。它并非旨在作为任何事物的复制/粘贴解决方案,因此请尝试完成代码示例。如果你这样做,我想你会发现自己学到了一些宝贵的课程。

再说一遍,也许不是。我在课堂上成功地使用了这节课,但教学和学习风格各不相同。幸运的是,有很多很棒的 shell 脚本编写教程,包括 opensource.com 上 Wicked Cool Shell Scripts 作者 Dave Taylor 的一篇很棒的文章:https://open-source.net.cn/article/16/12/calcshell-interactive-linux-comman…

回复 作者 dragonmouth (未验证)

感谢 Seth,因为我即将编写我的第一个 Bash 脚本。

祝你好运!当然,网上有很多很棒的资源。请记住在 *真实数据* 上使用它之前,在安全的环境中测试你的脚本,这很重要。相信我。

回复 作者 JJ

很棒的文章,谢谢!

我将添加更多内容,以防有人想了解更多信息。

仅当脚本位于当前目录中时,使用 ./despace 运行脚本才有效。为了避免这种情况,你可以使用完整路径名运行它。

如果你有兴趣了解更多信息,请查看此处的更多文章或 tldp.org 上的文章。

玩得开心!

很高兴看到有人在我写的文章中引用 tldp.org!为什么?因为我自然是从 tldp.org 学到了很多我所知道的 shell 脚本编写知识!我同意,这是一个 shell 脚本编写技巧的好网站。

回复 作者 anatomasovic

./despace: 2: ./despace: NAME: 未找到

mv: 'foo bar.txt' järel puudub sihtfail
Lisainfo saamiseks proovige 'mv --help'。

这可能是由几个问题引起的。很难诊断(并且可能效率不高)在评论部分尝试诊断。我建议你在 http://linuxquestions.org 上开设一个帐户(如果你还没有帐户),并将你的 shell 脚本的内容粘贴到其中,以及你尝试的确切命令。我们将在那里尝试调试。

回复 作者 peacecop kalmer: (未验证)

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.