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

你可以从终端窗口生成你自己的 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.

这些错误令人困惑,因为它们是针对 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: not found

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.