命令行工具组合很好很强大

带有图形化界面(GUI)的工具可能比较容易学习。所有的命令和选项一般都会在菜单和对话框中显示出来,使我们很容易就能了解这个工具能干些什么。如果“简便”和“直观”是你选择应用程序的第一要素,那么图形化工具总会是你的不二选择。

但是图形化界面带来简便的同时也带来功能贫乏,不够灵活的缺点。假定你是一个程序员,你需要清理你的Linux文件系统。要求在整个系统中找出所有超过6个月未被访问的目标文件,并在这些目录中运行make clean命令。一个图形化的文件系统浏览器能一次就完成这个任务吗?估计是不可以的。但是,联合多个非图形化的Linux命令行工具,再加之强大的Linux Shell,你可以快捷而轻松的完成这个任务。

来看看在命令行上我们到底能做哪些很强的事情。你也许并不想直接去做那些事情,但你可以了解如何来关联使用这些工具。Shell 和它的命令行程序的“关联”特性(就是允许你根据需要来组合使用各工具完成任务的能力)就是这些“强大工具”之所以强大的精要所在。

快速更正命令行输入

如果你在输入命令时有拼写错误,如何来更正?大多数的现代Shell程序允许你按向上箭头键(或CTRL+p或ESC+k)来获取之前输入的命令行,并在此基础上做修改。但是我们有更快的办法来修改错误。历史命令替代,这是一系列以!开头的Shell操作符。

  1. !$ (读作”bang dollar”) 用来扩展上一个命令行的最后一个参数。通常这是一个文件名。
    比如,你刚刚列示了文件/a/b/foo.c,现在需要用vi来编辑这个文件。这里你不必显示之前的命令行,然后改变之前的lsvi。你只需按两次键:!$就能获得之前的命令行上的文件名。Shell会在运行命令之前先显示这个扩展后的命令行:

    $ ls -l /a/b/foo.c
    -rw-r–r–  … Apr 20 11:31 /a/b/foo.c
    $ vi !$
    vi /a/b/foo.c
    … vi starts …
  2. !* 扩展之前的命令行上的所有参数。这样可以让你在新命令上重复之前的一系列的文件名称或者更正错误输入的命令。
    比如我们将命令emacs错误拼写为:emsca,你可以使用!*轻松更正这个错误:

    $ emsca /b/c/foo.d /e/f/bar.g
    emsca: Command not found.
    $ emacs !*
    emacs /b/c/foo.d /e/f/bar.g
    …emacs starts…
  3. ^old^new会将之前命令行上的字符串old替换为字符串new
    所以,之前的那个例子我们可以输入^sca^acs来达到同样的更正效果。一个单个^符号可以用来移除多余的字符。比如在下例中,我们多输入了一个g,我们可以用^g来更正:

    $ rm /some/longg/pathname
    rm: /some/longg/pathname: Not found
    $ ^g
    rm /some/long/pathname

用Subshell合并输出结果

如何才能将许多个不同命令的输出结果汇集起来在他处运用呢?在图形界面的工具里,你可以在每个命令窗口中拷贝输出结果然后粘贴到另一个窗口中,前提是这些窗口都是允许拷贝的。

在shell中,你可以使用subshell来汇集命令的输出结果。操作符()会让一个或多个命令运行在另一个shell进程中。你可以将这个subshell的输出结果重定向到其他程序的进程。Subshell可以使你在其中运行多个命令并一次性获得所有的输出结果。

比如, 你需要email给项目负责人一份三个不同项目的make的输出结果,这三个项目位于不同的目录中。方法是:

% (cd adir; make; cd ../bdir; make; \
?  cd ../cdir; make) | mail joe@foo.com

subshell操作符中运行了一系列由分号(;)-命令分隔符分割的命令。 在行末输入反斜杠(\)通知shell继续读到下一行,所以shell输出了第二提示符(这里是?)。当括号闭合后,我们将这些命令的所有的输出结果转给管道。mail程序通过管道,从标准输入中读取这些文字并将其发送给joe@foo.com。

那么将subshell的输出结果重定向到一个文件又如何呢? 输出结果会写入到哪个目录中呢?(换言之,。subshell中的cd命令会对当前的目录有影响吗?)在subshell中的改变并不会影响到它的父shell。父shell保持现有的目录不变。

比如, 在下面的命令中,make_outputs文件在subshell运行之前就会在当前工作目录中创建:

% (cd adir; make; cd ../bdir; make;
? cd ../cdir; make) > make_outputs

这种技术到处可见,不仅仅对cd命令而言。这也很自然的引出我们下一个好用命令行技术:重定向一个循环的输出。

让循环来干苦力,之一

象bash这种Bourne类型的shell不仅可以让你重定向subshell的输出(和输入)。事实上你几乎可以重定向任意shell命令体的输出,包括if和case语句,以及循环。(注:csh 和tcsh则不行)

循环并不是shell脚本程序专用的。你可以在提示符下交互的输入一个循环定义。我们现在来bash中使用循环来重新执行之前例子中的任务。这里我们还在每个make输出信息前添加一个标签。(这里bash的第二提示符是>)

$ for d in [a-c]dir
> do
>   cd$d>   echo “======= $d =======”
>   make
>   cd ..
> done | mail joe@foo.com

shell将通配符[a-c]dir扩展为adir bdir cdir,并且将这些名称存储在shell变量d中,然后运行从do到done运行循环体中的命令。循环的输出被管道重定向至mail程序。Joe收到的信息最后是这样的:

======= adir =======
…make output from adir…
======= bdir =======
…make output from bdir…
======= cdir =======
…make output from cdir…

当然循环不只是在重定向他的输出信息时有用,它们对任何需要重复执行的命令都很有帮助。

例如,我们使用netpbm工具 (来自 http://netpbm.sourceforge.net) 为一堆TIFF格式的图片文件制作PNG格式的缩略图。要求缩略图高度为100像素(pixels)。下例中在shell提示符下输入一个循环来实现这个任务。

$ for file in *.tif
> do
>   echo “===== doing $file ====”
>   tifftopnm $file |
>   pnmscale -height 100 |
>   pnmtopng > ${file%.*}.png
> done
 
==== doing img0321.tif ====
tifftopnm: writing PPM file
==== doing img0343.tif ====
tifftopnm: writing PPM file
…
$ ls
img0321.png  img0343.png  img0369.png
img0321.tif  img0343.tif  img0369.tif
…

为了使生成的.png文件的基本文件名与处理对象的文件名相同 (比如,从img0321.tif获得img0321.png)我们使用了参数扩展操作符${file%.*}。该操作符从$file扩展文件名,同时移除文件扩展名和前面的点。然后再这之后添加.png以获得我们需要的文件名。(在csh和tcsh中,使用${file:r})

如果你有很多的tiff文件需要做这样的转换,上面的命令会节省很多的时间。

让循环来干苦力,之二

我们现在回过头来看看文章之初提到的那个例子:搜索目录树,移除旧文件。有很多的方法来解决这个问题。使用类似Perl这样的脚步语言是方法之一。但是,如果你的窗口中已经有shell程序了,为什么不直接输入几个命令来完成这个任务呢?

shell实际上是一个编程语言解释器。它的语言就是那些命令行。这里我们会用到一个循环和Linux的find命令工具。

我们在之前看到了如何将shell的循环输出重定向。其实你也完全可以把输入重定向给shell循环。在下例中,while循环重复运行一个命令直到这个命令返回非零退出状态。我们用到的这个命令就是read,他从标准输入读入一行输入并且将之存储与一个shell变量中。

1: $ find /proj -type d -print |
2: > while read dir
3: > do
4: >   cd$dir|| break
5: >   test “$(echo *.o)” = ‘*.o’ && continue
6: >   test -z “$(find *.o -atime +180 -type f -print)&& continue
7: >   echo “Cleaning $dir8: >   make clean
9: > done 2>&1 | less

第一行上find的输出结果重定向给了第二行上的while循环。标准输入通过管道来自find的输出。当find没有输出时,read返回一个非零退出状态,这个循环被中断。

在循环体中,第四行用来改变当前目录。如果不成功,cd返回一个非零退出状态, 而||操作符会执行break命令来结束循环。(为使问题简化,我们给find命令的是一个绝对路径,/proj,因此它输出的也是绝对路径。如果我们使用的是相对路径名,我们需要在循环的末尾添加另一个cd命令,使得当下一个路径名传递到循环体之前,能返回到起始目录。)

在第五行 $(echo *.o) 要求shell扩展通配符,如果shell返回的是*.o字串,表明没有匹配到任何目标文件,所以test返回"true"(零)状态,并&&操作符会执行contine命令,这样就会返回到循环体的开头部分(从find命令获取另一个目录名)。第六行只有在找到目标文件时才会被执行。 test -z用来测试来自find *.o的输出是否为空。如果不存在超过180天未被访问的目标文件,find 没有输出,test -z测试成功,continue命令将会使执行顺序返回到循环体开头处以获取下一个目录。但是,如果test -z测试不成功,表示该目录下有过期文件,第七行和第八行在终端上显示目录的名称并且执行make clean命令。

第九行将循环输出的结果重定向到less从而按页显示结果。在输出到less之前,2>&1操作符将所有出错信息从标准出错接口(文件描述符2)重定向到标准输出接口(文件描述符1),这样包括错误信息的所有信息都传递给了less程序。这样的设置保证了你能看见所有的循环输出信息。

这个冗长的解释,让人觉得似乎这种东东搞起来很麻烦,不过你如果了解Shell技术,那么可能用一两分钟就能写出这样一个循环。(这比使用图形化的文件管理器点来点去要快得多了。)

你还可以添加一些判但以增强这个脚本功能,比如:跳过RCS目录,检查makefile是否存在,等等。你也可以用ls -lut列出所有的目标文件,或者运行make -n clean来演练一下而并不真的操作。

Leave a Response