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

如果说有什么东西会混淆计算机和人机交互,那就是文件名中的空格。你在互联网上见过它: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 脚本教程,包括 Wicked Cool Shell Scripts 作者 Dave Taylor 在 opensource.com 上发布的精彩教程:https://open-source.net.cn/article/16/12/calcshell-interactive-linux-comman…

回复 ,作者:dragonmouth (未验证)

谢谢 Set,因为我正要编写我的第一个 Bash 脚本。

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

回复 ,作者:JJ

很棒的文章,谢谢!

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

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

如果你对学习更多信息感兴趣,请查看此处的更多文章或 tldp.org 上的文章。

玩得开心!

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

回复 ,作者:anatomasovic

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

mv: 缺少 'foo bar.txt' 后的目标文件
尝试 'mv --help' 获取更多信息。

这可能是由于一些问题造成的。很难在此评论区诊断(并且尝试起来可能效率不高)。我建议你在 http://linuxquestions.org 上开设一个帐户(如果你还没有帐户),并将你的 shell 脚本的当前内容以及你尝试的确切命令粘贴进去。我们将在那里尝试调试。

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

Creative Commons License本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.