❏ 站外平台:

shell 脚本之始

作者: Seth Kenlon 译者: LCTT hkurj

| 2017-02-19 21:25   评论: 8 收藏: 5    

世界上对 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 和其它几个。在下面几个部分,我提供一些 bash 特定的例子,但最终的脚本不会用那些,所以你可以切换到 bash 中学习设置变量的课程,或做一些简单的语法调整

如果你是新手,只需使用 bash 。它是一个很好的 shell,有许多友好的功能,它是 Linux、Cygwin、WSL、Mac 默认的 shell,并且在 BSD 上也支持。

Hello world

您可以从终端窗口生成您自己的 hello world 脚本 。注意你的引号;单和双都会有不同的效果(LCTT 译注:想必你不会在这里使用中文引号吧)。

$ echo "#\!/bin/sh" > hello.sh
$ echo "echo 'hello world' " >> hello.sh

正如你所看到的,编写 shell 脚本就是这样,除了第一行之外,就是把命令“回显”或粘贴到文本文件中而已。

像应用程序一样运行脚本:

$ chmod +x hello.sh
$ ./hello.sh
hello world

不管多少,这就是一个 shell 脚本了。

现在让我们处理一些有用的东西。

去除空格

如果有一件事情会干扰计算机和人类的交互,那就是文件名中的空格。您在互联网上看到过:http://example.com/omg%2ccutest%20cat%20photophoto%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

去除空格 v2.0

脚本可以正常工作,但不完全如您的文档所述。它目前非常具体,只适用于一个名为 foo\ bar.txt 的文件,其它都不适用。

POSIX 命令会将其命令自身称为 $0,并将其后键入的任何内容依次命名为 $1$2$3 等。您的 shell 脚本作为 POSIX 命令也可以这样计数,因此请尝试用 $1 来替换 foo\ bar.txt

#!/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,一切都按设计工作,但不是你想要的。你的脚本将 $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

等等。

去除空格 v2.5

这个去除脚本已经可以发挥功用了,但在技术上它可以优化,它可以做一些可用性改进。

首先,变量实际上并不需要。 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 ,并依赖于它成功执行才能顺利运行的你或其它人来说很重要。

最后的改进是添加一些东西,以保护用户不会意外覆盖文件。理想情况下,您可以将此选项传递给脚本,所以它是可选的;但为了简单起见,这里对其进行了硬编码。 -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 脚本,接下来的生活将会轻松。

Happy hacking!


作者简介:

Seth Kenlon 是一位独立的多媒体艺术家,自由文化倡导者和 UNIX 极客。他是基于 Slackware 的多媒体制作项目(http://slackermedia.ml)的维护者之一


via: https://opensource.com/article/17/1/getting-started-shell-scripting

作者:Seth Kenlon 译者:hkurj 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出



最新评论

[点击展开更多楼层]
[3]
来自四川成都的 Chrome 55.0|GNU/Linux 用户 发表于 2017-02-20 18:58 的评论:
广泛使用的  Windows 默认目录 N 个都有空格。Ubuntu 仓库地址也有空格,至于 Linux 系统用命令

find /usr -iname '* *'

也可能找到隶属于某些包的目录名或文件名字包含空格, 甚至还有圆括号。

一般终端都提供自动拼写完成功能,所以这点不是问题,反倒是文件名包含中文,处理起来比较麻烦。至少要实现正常显示,即便能显示,还要看字体是否恰当,不然可能字的某部分显示不出来。要能包括 tty 在内的环境都能正常输入就更麻烦了。
[4]
来自加拿大的 Chrome 56.0|GNU/Linux 用户 发表于 2017-02-20 20:41 的评论:
反正都是要敲一个键的,敲空格和敲下划线/短横线一点没省力气,这种较劲挺没意思的,btw linux系统还默认文件可以没有后缀名,如果不是因为一开始微软“培养”了大家的用户习惯,咱是不是也能指责windows太不灵活?
[5]
来自四川成都的 Chrome 55.0|GNU/Linux 用户 发表于 2017-02-20 21:24 的评论:
扩展名多数时候,都是方便人类用户识别的。如果没有扩展名,虽然计算机也可以解析,但人不方便。想象一下,一个网站下载下来的文件,全部没有扩展名的情况。难道每访问一个文件,都要 file 一下?
而且还存在计算机也难于分别的情况, 这个当属 C 语言和 C++ 语言的源代码文件 .c 和 .cpp。
再扩展到网址,现在内网也不提倡使用单域名,互联网更是如此。
来自四川成都的 Chrome 55.0|GNU/Linux 用户 2017-02-20 21:32 7 回复
原来看到过有网友说 Google 提交单域名 search 申请,被 ICANN 驳回。微软的 Active Directory 是明确反对用单域名。而其开源实现 SAMBA 更反对用 .local 等作为一级域名,其 FAQ 对此有详细说明。当然这条回复,稍微有些跑题。
[1]
来自四川成都的 Chromium 55.0|Ubuntu 用户 发表于 2017-02-19 22:43 的评论:
我坚决反对在 *nix 系统中将诸如空格等字符从目录名或文件名中去掉的做法,这纯粹是让人适应系统,明明这些系统都是为人服务的,要说问题那也是 *nix 在处理文件系统时太随意,几乎什么字符都可以用造成,不该由用户来埋单。
[2]
来自黑龙江哈尔滨的 Firefox 51.0|Windows 10 用户 发表于 2017-02-20 08:17 的评论:
可以创建带有空格的目录名或文件名的目录和文件 就是使用这些文件太麻烦了 谁愿意才参数上在打个双引号?
[3]
来自四川成都的 Chrome 55.0|GNU/Linux 用户 发表于 2017-02-20 18:58 的评论:
广泛使用的  Windows 默认目录 N 个都有空格。Ubuntu 仓库地址也有空格,至于 Linux 系统用命令

find /usr -iname '* *'

也可能找到隶属于某些包的目录名或文件名字包含空格, 甚至还有圆括号。

一般终端都提供自动拼写完成功能,所以这点不是问题,反倒是文件名包含中文,处理起来比较麻烦。至少要实现正常显示,即便能显示,还要看字体是否恰当,不然可能字的某部分显示不出来。要能包括 tty 在内的环境都能正常输入就更麻烦了。
[4]
来自加拿大的 Chrome 56.0|GNU/Linux 用户 发表于 2017-02-20 20:41 的评论:
反正都是要敲一个键的,敲空格和敲下划线/短横线一点没省力气,这种较劲挺没意思的,btw linux系统还默认文件可以没有后缀名,如果不是因为一开始微软“培养”了大家的用户习惯,咱是不是也能指责windows太不灵活?
来自四川成都的 Chrome 55.0|GNU/Linux 用户 2017-02-20 21:20 6 回复
不要什么都扯到微软。单词之间用空格本来就是一种人类自然的书写方式,难道你想回到古汉语那种没有标点断句的时代?Linux 文件默认可以没有扩展名,Windows 也是一样的。NT 6.x 系的 Windows, 其系统卷用于引导操作系统的 BCD 文件也没有扩展, 以管理员权限执行命令 bcdedit -store %SYSTEMDRIVE%\Boot\BCD -enum 可以看到输出的配置信息。
来自湖南长沙的 Chrome 55.0|Windows 7 用户 2017-02-20 19:11 4 回复
作为一个学习shell的新手看完这篇文章,感觉耳目一新。文章的精彩之处在于循序渐进,教授你脚本的设计方法和思路。
LCTT 译者
hkurj 🌟🌟
共计翻译: 8.0 篇 | 共计贡献: 1410
贡献时间:2016-05-09 -> 2020-03-19
访问我的 LCTT 主页 | 在 GitHub 上关注我


返回顶部

分享到微信

打开微信,点击顶部的“╋”,
使用“扫一扫”将网页分享至微信。