Unix 命令行shell基础--学习系列003
Unix 命令行shell(外壳)基础
Unix 命令行外壳是生物信息学的基础计算环境。它既是我们与大型生物信息学程序交互的接口,也是检查数据和中间结果的交互式控制台,还是构建流程和工作流的基础设施。本章将帮助你熟练掌握后续章节中频繁使用的 Unix 外壳核心概念,让你在未来章节中能专注于命令的内容,而非纠结于外壳语法。
本书假设你已熟悉一些基础内容,例如终端是什么、外壳是什么、Unix 文件系统层级结构、目录导航、文件权限、执行命令以及使用文本编辑器。如果这些概念对你来说很陌生,最好先通过基础资料复习(参见第 xvi 页的 “本书的前提假设” 获取资源)。本章将涵盖 Unix 外壳在生物信息学应用中的核心基础概念:流、重定向、管道、运行中程序的管理,以及命令替换。理解这些外壳知识,能让你做好准备,使用外壳处理数据(第 7 章)和构建流程与工作流(第 12 章)。本章还将解释为何 Unix 外壳在现代生物信息学中占据如此重要的地位。如果你已熟练掌握这些外壳知识,建议阅读本章第一节后直接跳至第 4 章。
为什么生物信息学要用 Unix?模块化与 Unix 哲学
试想,如果我们不把 Unix 外壳作为生物信息学计算环境,而是将整个项目实现为一个大型程序会怎样?我们通常不会把生物信息学项目视为 “程序”,但它完全可以是 —— 我们可以编写一个复杂的单程序,接收原始数据作为输入,经过数小时的数据处理后,输出用于发表的图表和最终结果表格。对于变异检测这类项目,这个程序需要包含原始测序读段处理、读段比对、变异检测、变异过滤和最终数据分析等步骤。这样的程序代码会极其庞大 —— 轻松达到数千行。
尽管这类程序的优点是可定制化适配特定变异检测项目,但它缺乏通用性,难以适配其他项目。由于代码量巨大,调整它以适应新项目会非常不切实际;庞大的代码库也会让定位和修复漏洞变得困难。更糟的是,除非这个单块程序专门设计了步骤间数据校验功能,否则某个步骤可能出错(而我们毫无察觉),但程序仍会继续用错误数据进行分析。虽然这个定制程序可能在计算效率上更优,但代价是脆弱、难修改、易出错(因为检查中间数据变得极其困难),且无法推广到未来的项目。
Unix 能成为生物信息学的基础计算环境,正是因为其设计哲学与这种僵化脆弱的方式完全相反。Unix 外壳的设计初衷是让用户能通过组合小型模块化程序轻松构建复杂程序。这一理念即 Unix 哲学:
Unix 哲学是:编写只做一件事且做好它的程序;编写可以相互协作的程序;编写处理文本流的程序,因为这是一种通用接口。
—— 道格・麦克罗伊
Unix 外壳提供了程序间通信的方式(管道)以及读写文件的方式(重定向)。Unix 的核心程序(我们将在第 7 章用它们在命令行分析数据)是模块化的,且设计为可与其他程序高效协作。Unix 哲学的模块化理念在生物信息学中有诸多优势:
- 模块化工作流更易发现错误并定位源头。在模块化工作流中,每个组件都是独立的,这让检查中间结果是否存在异常、隔离出问题的步骤变得更简单。相比之下,大型非模块化程序会隐藏潜在问题(你只能看到最终输出数据),且难以定位问题源头。
- 模块化工作流让我们能轻松尝试替代方法,因为独立组件可被其他组件替换。例如,如果你怀疑某个比对工具处理你的数据效果不佳,很容易用另一个工具替换它。这只有在模块化工作流中才能实现 —— 比对程序与下游的变异检测或 RNA-seq 分析步骤是分离的。
- 模块化组件让我们能为特定任务选择合适的工具和语言。这也是 Unix 环境适配生物信息学的另一原因:它允许我们组合命令行工具(用于交互式探索数据,第 7 章详述)、Python(用于更复杂的脚本)和 R(用于统计分析)。当程序设计为可与其他程序协作时,为特定任务选择专门工具无需额外成本 —— 这在生物信息学中很常见。
- 模块化程序可复用,且适用于多种数据。编写良好的模块化程序可重组并应用于不同问题和数据集,因为它们是独立的组件。最重要的是,通过重组模块化组件,可用现有工具解决新问题。
麦克罗伊的引言除了强调程序模块化和协作性,还提到了文本流。我们将在本章讨论 Unix 流,而流的概念在处理大型数据时至关重要。“大型数据” 的定义可能各异:对刚涉足测序的实验室来说,单条测序 lane 的数据可能已很大,但与大型测序中心每小时处理的数据相比,这微不足道。无论如何,单条 lane 的测序数据太大,无法装入大多数标准台式机的内存。如果我需要在这些数据中搜索 “GTGATTAACTGCGAA” 这个精确序列,不可能在记事本中打开数据并使用 “查找” 功能定位 —— 内存根本装不下这么多核苷酸。相反,工具必须依赖数据流:从数据源读取数据并实时处理。无论是通用 Unix 工具还是许多生物信息学程序,都设计为通过流接收输入并通过另一个流输出结果。正是这些文本流让我们既能将程序组合成工作流,又能在不占用大量计算机内存的情况下处理数据。
多种 Unix 外壳
全书我会统称 “Unix 外壳”,但实际上并不存在单一的 Unix 外壳。外壳是计算机程序,许多程序员都设计并实现了自己的版本。这些版本可能给新用户带来困扰,因为某些外壳的功能与其他外壳不兼容。为避免这种困扰,请确保你使用的是 Bourne-again 外壳(bash)。Bash 应用广泛,是苹果 OS X 和 Ubuntu Linux 等操作系统的默认外壳。你可以运行echo $SHELL
验证是否使用 bash 作为外壳(不过最好同时检查echo $0
的输出,因为不同外壳识别自身的方式也不同!)。我不推荐其他外壳,如 C 外壳(csh)、其衍生版本 tcsh 和 Korn 外壳(ksh),它们在生物信息学中不太常用,且可能与本书示例不兼容。Bourne 外壳(sh)是 Bourne-again 外壳(bash)的前身,但 bash 更新,通常更受青睐。
你可以用chsh
命令更改外壳。在日常生物信息学工作中,我使用 Z 外壳(zsh)并将其设为默认外壳。Z 外壳有更高级的功能(如更好的自动补全),在生物信息学中很实用。除非特别说明,本书内容在这两种外壳中均可兼容。如果你已熟练掌握外壳基础,或许想试试 Z 外壳。我在 GitHub 本章的 README 文件中提供了关于 Z 外壳的资源。
需要强调的最后一点是,Unix 外壳功能极其强大。仅通过通配符等简单功能,就能轻松将命令应用于数百个文件。但这种强大也伴随着风险:Unix 外壳不在乎命令是否输错,也不在乎命令是否会破坏文件;它的设计初衷不是阻止你做不安全的事。加里・伯恩哈特的类比很贴切:Unix 就像链锯。链锯是强大的工具,能轻松完成锯断粗木等困难任务。但不幸的是,这种强大也意味着危险:链锯同样能轻易锯断你的腿(严格来说,更容易)。例如:
$ rm -rf tmp-data/aligned-reads* # 删除所有旧的大文件
$ # 对比
$ rm -rf tmp-data/aligned-reads * # 删除当前目录下的所有文件
rm: tmp-data/aligned-reads: No such file or directory
在 Unix 中,一个空格可能意味着两种结果:要么清理一些旧文件,要么因意外删除所有内容而延误项目进度。这并非危言耸听 —— 这是使用强大工具的必然结果。相反,实验或尝试新命令时应保持谨慎(例如,在临时目录工作;若不确定命令行为,使用虚假文件或数据;并始终备份)。Unix 外壳允许你做强大(可能不安全)的事,这是其设计的重要部分:
Unix 的设计不是为了阻止用户做蠢事,因为那样也会阻止他们做聪明事。
—— 道格・格温
用聪明的方式处理重复性大型数据处理任务,是熟练生物信息学者的重要技能。外壳往往是完成这些任务最快的工具。本章将聚焦 Unix 外壳的一些基础功能,它们能让我们用简单组件构建复杂程序:流、重定向、管道、进程管理和命令替换。我们将在第 12 章学习 Unix 外壳的另一重要部分 —— 任务自动化。
流与重定向的使用
生物信息学数据通常是文本 —— 例如,测序读段文件或参考基因组中的 A、C、T、G,或基因坐标的制表符分隔文件。生物信息学的文本数据通常也很大(千兆字节或更大,无法一次性装入计算机内存)。这正是 Unix 处理文本流的理念在生物信息学中有用的原因:文本流允许我们对流式数据进行处理,而无需将所有数据存入内存。
例如,假设我们有两个大型 FASTA 文件(存储核苷酸序列的标准文本格式,偶尔也用于蛋白质序列)。一旦这些文件达到几 GB,即使是将它们合并为一个文件这样简单的任务也会变得棘手。不使用 Unix 外壳的话,如何完成这个任务?你可能会尝试打开一个文件,全选并复制内容,再粘贴到另一个文件中。但这不仅需要将两个文件加载到内存,还需要额外内存来存储复制的文件内容。这种方法无法处理我们在生物信息学中常规面对的数据规模。此外,粘贴内容到文件也违背了第 1 章的建议:将数据视为只读。如果操作出错,一个或两个文件都可能损坏。更糟的是,复制粘贴大型文件会占用大量内存,更可能导致计算机出问题。
流为这些问题提供了可扩展、稳健的解决方案。
将标准输出重定向到文件
Unix 外壳通过流简化了合并大型文件等任务。使用流可避免不必要地将大型文件加载到内存。相反,我们可以将文件内容打印到标准输出流,再将这个流从终端重定向到要保存合并结果的文件中。你可能用过cat
命令将文件内容打印到标准输出(未重定向时会显示在终端屏幕上)。例如,我们可以用cat
查看 tb1-protein.fasta 文件(可在 GitHub 本章目录中获取):
$ cat tb1-protein.fasta
>teosinte-branched-1 protein
LGVPSVKHMFPFCDSSSPMDLPLYQQLQLSPSSPKTDQSSSFYCYPCSPP
FAAADASFPLSYQIGSAAAADATPPQAVINSPDLPVQALMDHAPAPATEL
GACASGAEGSGASLDRAAAAARKDRHSKICTAGGMRDRRMRLSLDVARKF
FALQDMLGFDKASKTVQWLLNTSKSAIQEIMADDASSECVEDGSSSLSVD
GKHNPAEQLGGGGDQKPKGNCRGEGKKPAKASKAAATPKPPRKSANNAHQ
VPDKETRAKARERARERTKEKHRMRWVKLASAIDVEAAAASVPSDRPSSN
NLSHHSSLSMNMPCAAA
cat
还允许将多个文件的内容按命令参数顺序打印到标准输出流。这本质上是将这些文件连接起来,如下所示的 tb1 和 tga1 的翻译序列:
$ cat tb1-protein.fasta tga1-protein.fasta
>teosinte-branched-1 protein
LGVPSVKHMFPFCDSSSPMDLPLYQQLQLSPSSPKTDQSSSFYCYPCSPP
FAAADASFPLSYQIGSAAAADATPPQAVINSPDLPVQALMDHAPAPATEL
GACASGAEGSGASLDRAAAAARKDRHSKICTAGGMRDRRMRLSLDVARKF
FALQDMLGFDKASKTVQWLLNTSKSAIQEIMADDASSECVEDGSSSLSVD
GKHNPAEQLGGGGDQKPKGNCRGEGKKPAKASKAAATPKPPRKSANNAHQ
VPDKETRAKARERARERTKEKHRMRWVKLASAIDVEAAAASVPSDRPSSN
NLSHHSSLSMNMPCAAA
>teosinte-glume-architecture-1 protein
DSDCALSLLSAPANSSGIDVSRMVRPTEHVPMAQQPVVPGLQFGSASWFP
RPQASTGGSFVPSCPAAVEGEQQLNAVLGPNDSEVSMNYGGMFHVGGGSG
GGEGSSDGGT
虽然这些文件已被连接,但结果并未保存 —— 内容只是打印到了终端屏幕上。要将合并结果保存到文件,需要将标准输出流从终端屏幕重定向到文件。重定向是 Unix 中的重要概念,在生物信息学中会频繁使用。
我们使用>
或>>
运算符将标准输出重定向到文件。>
运算符将标准输出重定向到文件,并覆盖文件的现有内容(注意这一点,务必谨慎);而>>
运算符则追加到文件(保留现有内容,仅在末尾添加)。如果文件不存在,两种运算符都会先创建文件,再将输出重定向到其中。要合并两个 FASTA 文件,我们可像之前那样使用cat
,但将输出重定向到文件:
$ cat tb1-protein.fasta tga1-protein.fasta > zea-proteins.fasta
注意,将标准输出重定向到文件时,终端屏幕上不会显示任何内容。在我们的示例中,整个标准输出流都进入了 zea-proteins.fasta 文件。标准输出流重定向到文件的示意图如图 3-1(b)所示。
我们可以通过检查目录中最新创建的文件是否为刚创建的文件(即 zea-proteins.fasta),来验证重定向是否成功:
ls -lrt
total 24
-rw-r--r-- 1 vinceb staff 353 Jan 20 21:24 tb1-protein.fasta
-rw-r--r-- 1 vinceb staff 152 Jan 20 21:24 tga1-protein.fasta
-rw-r--r-- 1 vinceb staff 505 Jan 20 21:35 zea-proteins.fasta
ls -lrt
中的-lrt
表示以列表格式(-l
)、逆序(-r
)按时间(-t
)排序列出目录中的文件(更多细节参见man ls
)。注意这些标志被合并为-lrt
,这是一种常见的语法捷径。如果希望最新的文件显示在顶部,可以省略r
标志。
重定向标准错误
由于许多程序使用标准输出流输出数据,因此需要一个单独的流来输出错误、警告和供用户阅读的消息。标准错误流正是用于此目的(如图 3-1 所示)。与标准输出一样,标准错误默认也定向到终端。实际上,我们经常希望将标准错误流重定向到文件,以便将消息、错误和警告记录到文件中,供日后查看。
为说明如何同时重定向标准输出和标准错误,我们使用ls -l
命令列出一个存在的文件(tb1.fasta)和一个不存在的文件(leafy1.fasta)。ls -l
对存在的文件 tb1.fasta 的输出会发送到标准输出,而 “leafy1.fasta 不存在” 的错误消息会输出到标准错误。不重定向任何流时,两个流都会输出到终端:
$ ls -l tb1.fasta leafy1.fasta
ls: leafy1.fasta: No such file or directory
-rw-r--r-- 1 vinceb staff 0 Feb 21 21:58 tb1.fasta
要将每个流重定向到不同的文件,我们将上一节的>
运算符与新的标准错误重定向运算符2>
结合使用:
$ ls -l tb1.fasta leafy1.fasta > listing.txt 2> listing.stderr
$ cat listing.txt
-rw-r--r-- 1 vinceb staff 152 Jan 20 21:24 tb1.fasta
$ cat listing.stderr
ls: leafy1.fasta: No such file or directory
此外,2>
有对应的2>>
,与>>
类似(会追加到文件而非覆盖)。
文件描述符
2>
的记法可能看起来很晦涩(且难记),但标准错误的重定向运算符中包含2
是有原因的。Unix 系统上所有打开的文件(包括流)都被分配一个唯一的整数,称为文件描述符。Unix 的三个标准流 —— 标准输入(稍后会介绍)、标准输出和标准错误 —— 的文件描述符分别为 0、1 和 2。实际上,也可以用1>
作为标准输出的重定向运算符,但这在实际中不常见,可能会让合作者困惑。
有时程序会产生我们不需要或不关心的消息。重定向是让某些程序写入标准输出的诊断信息静音的有用方法:只需重定向到 stderr.txt 等日志文件即可。但在某些情况下,我们不需要将这些输出保存到文件,且将输出写入物理磁盘会减慢程序速度。幸运的是,类 Unix 操作系统有一个特殊的 “虚拟” 磁盘(称为伪设备),可将不需要的输出重定向到那里:/dev/null
。写入/dev/null
的输出会消失,因此极客们有时开玩笑称其为 “黑洞”。
使用tail -f
监控重定向的标准错误
对于可能运行数天(甚至数周、数月)的大型生物信息学程序,我们通常需要同时重定向标准输出和标准错误。两个流都被重定向后,终端上不会显示任何内容,包括我们可能想在长时间运行过程中关注的有用诊断消息。如果想跟踪这些消息,可以使用tail
程序查看输出文件的最后几行,方法是调用tail filename.txt
。例如,运行tail stderr.txt
会打印 stderr.txt 的最后 10 行。你可以用-n
选项设置tail
打印的行数。
tail
还可以用-f
(-f
表示跟随)持续监控文件。当被监控的文件更新时,tail
会将新行显示到终端屏幕,而不是像不使用该选项时那样只显示 10 行就退出。如果想停止监控文件,可以用 Control-C 中断tail
进程。关闭tail
时,写入文件的进程不会被中断。
使用标准输入重定向
Unix 外壳还提供了标准输入的重定向运算符。标准输入通常来自键盘,但使用<
重定向运算符可以直接从文件读取标准输入。尽管标准输入重定向不如>
、>>
和2>
常见,但偶尔也很有用:
$ program < inputfile > outputfile
在这个示例中,虚拟文件 inputfile 通过标准输入提供给 program,而 program 的所有标准输出都被重定向到 outputfile。
使用 Unix 管道(如cat inputfile | program > outputfile
)比<
更常见。我们稍后会看到的许多程序(如grep
、awk
、sort
)除了通过标准输入接收输入外,还可以接受文件参数。其他程序(在生物信息学中尤其常见)使用单破折号-
作为参数,表示应使用标准输入,但这是一种约定,而非 Unix 的功能。
强大的 Unix 管道:速度与美感并存
我们应该有一些像 [花园里的] 水管一样连接程序的方法 —— 需要用另一种方式处理数据时,拧上另一段即可。
—— 道格・麦克罗伊(1964 年)
在第 37 页的 “为什么生物信息学要用 Unix?模块化与 Unix 哲学” 中,麦克罗伊的引言建议 “编写可以相互协作的程序”。这要归功于 Unix 管道,而管道这一概念正是麦克罗伊本人发明的。Unix 管道与我们之前看到的重定向运算符类似,但管道不是将程序的标准输出流重定向到文件,而是将其重定向到另一个程序的标准输入。只有标准输出会通过管道传递到下一个命令;标准错误仍会打印到终端屏幕,如图 3-2 所示。
你可能会疑惑,为什么要将一个程序的标准输出直接管道到另一个程序的标准输入,而不是将输出写入文件,再将该文件读入下一个程序。在许多情况下,创建文件有助于检查中间结果,还可能便于调试工作流步骤 —— 那为什么不每次都这样做呢?
答案通常与计算效率有关 —— 读写磁盘非常慢。我们在生物信息学中(几乎会本能地)使用管道,不仅因为它是构建流程的有用方式,还因为它更快(在某些情况下,快得多)。现代磁盘的速度比内存慢几个数量级。例如,从内存读取 1 兆字节数据只需约 15 微秒,而从磁盘读取 1 兆字节数据则需要 2 毫秒。这 2 毫秒等于 2000 微秒,使得从磁盘读取数据的速度慢了 100 多倍(这是估计值;实际数值会因磁盘类型和速度而异)。
实际上,向磁盘写入或从磁盘读取(例如将标准输出重定向到文件时)通常是数据处理的瓶颈。对于大型下一代测序数据,这会显著拖慢处理速度。如果你设计的算法比旧版本快一倍,但真正的瓶颈是读写磁盘,那么你可能根本注意不到速度提升。此外,不必要地将输出重定向到文件会占用磁盘空间。对于大型下一代数据和可能众多的实验样本,这可能是个大问题。
用管道将一个程序的输出直接传递到另一个程序的输入,是一种计算高效且简单的 Unix 程序交互方式。这也是生物信息学者(以及一般软件工程师)喜欢 Unix 的另一个原因。管道允许我们用小型模块化组件构建更大、更复杂的工具。无论程序是用什么语言编写的,只要两个程序能理解彼此传递的数据,管道就能在它们之间工作。正如麦克罗伊在关于 Unix 哲学的引言中所指出的,纯文本流作为大多数程序的通用接口,是很常见的。
管道实践:用grep
和管道创建简单程序
生物信息学的黄金法则是不要信任你的工具或数据。这种怀疑态度需要不断对中间结果进行合理性检查,以确保你的方法不会使数据产生偏差,或数据中的问题不会因你的方法而加剧。然而,编写自定义脚本来检查每一份中间数据可能代价高昂,即使你是能一次写出无错代码的快速程序员。Unix 管道允许我们快速迭代地构建小型命令行程序来检查和处理数据 —— 我们将在第 7 章更深入地探讨这种方法。管道也广泛用于大型生物信息学工作流(第 12 章),因为它们避免了将不必要的文件写入磁盘所带来的延迟问题。我们将在本节学习管道的基础知识,为你在本书后续章节中使用它们做好准备。
让我们看看如何用管道将进程链接起来。假设我们正在处理一个 FASTA 文件,程序警告说序列中包含非核苷酸字符。你对此感到惊讶,因为这些序列应该只是 DNA。我们可以用 Unix 单行命令,通过管道和grep
轻松检查非核苷酸字符。Unix 工具grep
在文件或标准输入中搜索与模式匹配的字符串。这些模式可以是简单字符串,也可以是正则表达式(实际上有两种正则表达式,基本正则表达式和扩展正则表达式;更多细节参见man grep
)。如果你不熟悉正则表达式,请参见本书 GitHub 仓库的 README 获取资源。
我们的流程首先会从 FASTA 文件中移除所有标题行(以>
开头的行),因为我们只关心序列是否有非核苷酸字符。然后,FASTA 文件的其余序列可以通过管道传递给另一个grep
实例,该实例只打印包含非核苷酸字符的行。为了在终端中更容易识别这些字符,我们还可以为匹配的字符着色。完整命令如下:
$ grep -v "^>" tb1.fasta | \
grep --color -i "[^ATCG]"
CCCCAAAGACGGACCAATCCAGCAGCTTCTACTGCTAYCCATGCTCCCCTCCCTTCGCCGCCGCCGACGC
首先,我们移除以>
字符开头的 FASTA 标题行。我们的正则表达式模式是^>
,它匹配所有以>
字符开头的行。插入符号^
在正则表达式中有两种含义,但在这种情况下,它用于将模式锚定到行的开头。因为我们想排除以>
开头的行,所以用grep
的-v
选项反转匹配的行。最后,我们用管道字符(|
)将标准输出管道到下一个命令。反斜杠(\
)仅表示命令将在下一行继续,用于提高可读性。
其次,我们想找出任何不是 A、T、C 或 G 的字符。构建一个不匹配 A、T、C 或 G 的正则表达式模式最容易。为此,我们使用插入符号在正则表达式中的第二种含义。在方括号中使用插入符号时,它匹配任何不在这些方括号中的字符。因此,模式[^ATCG]
匹配任何不是 A、T、C 或 G 的字符。此外,我们用-i
忽略大小写,因为 a、t、c、g 也是有效的核苷酸(小写字符通常用于表示被屏蔽的重复序列或低复杂度序列)。最后,我们添加grep
的--color
选项,为匹配的非核苷酸字符着色。
在终端窗口中运行时,“Y” 会被高亮显示。有趣的是,根据 IUPAC 制定的标准,Y 实际上是有效的扩展模糊核苷酸代码。Y 代表嘧啶碱基:C 或 T。其他单字母 IUPAC 代码可以表示序列数据中的不确定性。例如,嘌呤碱基用 R 表示,而 D 表示 A、G 或 T 中的任意一种。
我们来讨论一下这个简单 Unix 管道的几个额外要点。首先,注意两个正则表达式都放在引号中,这是个好习惯。此外,如果我们改用grep -v > tb1.fasta
,外壳会将>
解释为重定向运算符,而不是提供给grep
的模式。不幸的是,这会错误地覆盖你的 tb1.fasta 文件!大多数生物信息学者都曾犯过这个错误,并从中吸取了教训(丢失了他们本想grep
的 FASTA 文件),所以要小心。
这个简单的 Unix 单行命令只需几秒钟就能编写和运行,非常适合这个特定任务。我们本可以编写一个更复杂的程序,明确解析 FASTA 格式,计算出现次数,并输出包含非核苷酸字符的序列名称列表。但是,对于我们的任务 —— 弄清楚程序为什么无法运行 —— 快速构建简单的命令行工具就足够了。我们将在第 7 章看到更多用 Unix 数据程序和管道构建命令行工具的示例。
组合管道和重定向
像比对工具、组装器和 SNP 调用器这样的大型生物信息学程序通常会同时使用多个流。结果(如比对的读段、组装的 contig 或 SNP 调用结果)通过标准输出流输出,而诊断消息、警告或错误则输出到标准错误流。在这种情况下,我们需要组合管道和重定向来管理运行中程序的所有流。
例如,假设我们有两个虚拟程序:program1 和 program2。第一个程序 program1 处理名为 input.txt 的输入文件,将结果输出到标准输出流,将诊断消息输出到标准错误流。第二个程序 program2 接收 program1 的标准输出作为输入并进行处理。program2 也将自己的诊断消息输出到标准错误流,将结果输出到标准输出流。棘手的是,现在有两个进程同时向标准错误和标准输出流输出内容。如果不捕获 program1 和 program2 的标准错误流,屏幕上会充斥着混乱的诊断消息,滚动速度太快,我们无法阅读。幸运的是,我们可以轻松组合管道和重定向:
$ program1 input.txt 2> program1.stderr | \
program2 2> program2.stderr > results.txt
program1 处理 input.txt 输入文件,然后将结果输出到标准输出。program1 的标准错误流被重定向到 program1.stderr 日志文件。和之前一样,反斜杠用于将这些命令分行显示,以提高可读性(在你自己的工作中可以省略)。同时,program2 将 program1 的标准输出作为其标准输入。外壳将 program2 的标准错误流重定向到 program2.stderr 日志文件,将 program2 的标准输出重定向到 results.txt。
有时,我们需要将标准错误流重定向到标准输出。例如,假设我们想用grep
在 program1 的标准输出和标准错误流中搜索 “error”。仅用管道无法实现,因为管道只将一个程序的标准输出链接到下一个程序的标准输入,而忽略标准错误。我们可以先将标准错误重定向到标准输出,然后将这个合并的流通过管道传递给grep
,来解决这个问题:
$ program1 2>&1 | grep "error"
2>&1
运算符用于将标准错误重定向到标准输出流。
管道中的 “三通管”:tee
命令
如前所述,管道通过将一个进程的标准输出连接到另一个进程的标准输入,避免了不必要的磁盘读写操作。然而,在 Unix 管道中,我们偶尔确实需要将中间文件写入磁盘。这些中间文件在调试管道或需要存储运行时间长的步骤的中间结果时很有用。就像水管工的 T 型接头一样,Unix 程序tee
会将管道的标准输出流的副本分流到中间文件,同时仍将其传递到自己的标准输出:
$ program1 input.txt | tee intermediate-file.txt | program2 > results.txt
在这里,program1 的标准输出既被写入 intermediate-file.txt,又被直接管道到 program2 的标准输入。
进程的管理与交互
当我们通过 Unix 外壳运行程序时,程序会成为进程,直到成功完成或出错终止。你的计算机上同时运行着多个进程 —— 例如,系统进程,以及你的网页浏览器、电子邮件应用程序、生物信息学程序等。在生物信息学中,我们经常处理运行时间很长的进程,因此了解如何从 Unix 外壳操作和管理进程很重要。在本节中,我们将学习操作进程的基础知识:在后台运行和管理进程、终止错误进程,以及检查进程退出状态。
后台进程
当我们在外壳中输入命令并按 Enter 键时,在命令运行期间,我们无法使用该外壳提示符。这对于短任务来说没问题,但如果要等一个长时间运行的生物信息学程序完成后才能继续在外壳中工作,会严重影响工作效率。外壳除了允许你在前台运行程序(正常运行命令时的方式),还允许你在后台运行程序。在后台运行进程会释放提示符,让你可以继续工作。
我们可以在命令末尾添加 & 符号,告诉 Unix 外壳在后台运行程序。例如:
$ program1 input.txt > results.txt &
[1] 26577
外壳返回的数字是 program1 的进程 ID(PID)。这是一个唯一的 ID,可用于稍后识别和检查 program1 的状态。我们可以用jobs
命令检查后台运行的进程:
$ jobs
[1]+ Running program1 input.txt > results.txt
要将后台进程调回前台,可以使用fg
(foreground 的缩写)。fg
会将最近的进程调回前台。如果你有多个后台运行的进程,jobs
命令输出的列表中会显示所有进程。像[1]
这样的数字是作业 ID(与系统分配给运行程序的进程 ID 不同)。要将特定的后台作业调回前台,使用fg %<num>
,其中<num>
是作业列表中的编号。如果我们想将 program1 调回前台,fg
和fg %1
的效果相同,因为只有一个后台进程:
$ fg
program1 input.txt > results.txt
后台进程与挂起信号
后台进程有一个小陷阱:尽管它们在后台运行,看似与终端断开连接,但关闭终端窗口会导致这些进程被终止。不幸的是,很多长时间运行的重要作业都是这样被意外终止的。
每当终端窗口关闭时,它会发送一个挂起信号。挂起信号(也称为 SIGHUP)源于网络连接可靠性低得多的时代。连接断开可能会导致用户无法停止异常、消耗资源的进程。为解决这个问题,挂起信号会发送给从关闭的终端启动的所有进程。几乎所有 Unix 命令行程序收到这个信号后都会停止运行。
所以要注意 —— 在后台运行进程并不能保证终端关闭时它不会终止。为防止这种情况,我们需要使用nohup
工具或将其在 Tmux 中运行,这两个主题我们将在第 4 章更详细地介绍。
也可以将已在前台运行的进程放到后台。为此,我们首先需要暂停进程,然后使用bg
命令在后台运行它。暂停进程会暂时中止它,让你可以将其放到后台。我们可以通过 Control-z 组合键发送停止信号来暂停进程。对于我们的虚拟程序 program1,操作如下:
$ program1 input.txt > results.txt # 忘记添加&符号
$ # 输入control-z
[1]+ Stopped program1 input.txt > results.txt
$ bg
[1]+ program1 input.txt > results.txt
和之前的fg
一样,我们也可以用jobs
查看暂停进程的作业 ID。如果有多个运行的进程,可以用bg %<num>
(其中<num>
是作业 ID)指定要放到后台的进程。
终止进程
有时我们需要终止进程。进程占用过多计算机资源或无响应的情况并不少见,这时需要发送特殊信号来终止进程。终止进程会永久结束它,与用停止信号暂停不同,终止是不可恢复的。如果进程当前在你的外壳中运行,可以通过输入 Control-C 来终止它。这只对前台运行的进程有效,所以如果进程在后台,需要先用前面介绍的fg
将其调回前台。
更高级的进程管理(包括用top
和ps
监控和查找进程,以及用kill
命令终止它们)超出了本章的范围。不过,GitHub 本章的 README 中有很多关于进程和资源管理的信息。
退出状态:如何通过程序判断命令是否成功运行
长时间运行的进程有一个问题:你可能不会一直等着监控它们。你如何知道它们何时完成?如何知道它们是否成功完成而没有出错?Unix 程序退出时会有一个退出状态,指示程序是正常终止还是出错终止。根据 Unix 标准,退出状态为 0 表示进程成功运行,非零状态表示发生了某种错误(希望程序也会打印易懂的错误消息)。
关于退出状态的警告
不幸的是,程序是否在遇到错误时返回非零状态取决于程序开发者。有时,程序员会忽略错误处理(这在生物信息学程序中确实存在),程序可能出错但仍返回零退出状态。这也是遵循黄金法则(即不要信任你的工具)并始终检查中间数据的又一个原因。
退出状态不会打印到终端,但外壳会将其值存储在名为$?
的外壳变量中。我们可以在运行命令后用echo
命令查看这个变量的值:
$ program1 input.txt > results.txt
$ echo $?
0
退出状态非常有用,因为它们允许我们在外壳中通过程序方式链接命令。链中的后续命令是否运行,取决于上一个命令的退出状态。外壳提供了两个运算符来实现这一点:一个运算符(&&
)仅在上一个命令成功完成时运行后续命令;另一个运算符(||
)仅在上一个命令未成功完成时运行下一个命令。如果你熟悉短路求值的概念,就会知道这些运算符分别是短路与和短路或。
通过示例理解这些运算符最好。假设我们想运行 program1,将其输出写入文件,然后让 program2 读取该输出。为避免 program1 出错终止导致 program2 读取不完整的文件,我们希望仅在 program1 返回零(成功)退出代码后才启动 program2。外壳运算符&&
仅在之前的命令以零退出状态完成时才执行后续命令:
$ program1 input.txt > intermediate-results.txt && \
program2 intermediate-results.txt > results.txt
使用||
运算符,我们可以让外壳仅在上一个命令失败(以非零状态退出)时执行命令。这对于警告消息很有用:
$ program1 input.txt > intermediate-results.txt || \
echo "warning: an error occurred"
如果想测试&&
和||
,有两个 Unix 命令专门用于返回退出成功(true
)或退出失败(false
)。例如,思考为什么会打印以下行:
$ true
$ echo $?
0
$ false
$ echo $?
1
$ true && echo "first command was a success"
first command was a success
$ true || echo "first command was not a success"
$ false || echo "first command was not a success"
first command was not a success
$ false && echo "first command was a success"
此外,如果你不关心退出状态,只想按顺序执行两个命令,可以使用分号(;
):
$ false; true; false; echo "none of the previous mattered"
none of the previous mattered
如果你只知道外壳是通过终端交互的工具,可能会开始注意到它具有完整编程语言的许多元素。事实上,它确实是!实际上,你可以像编写 Python 脚本一样编写和执行外壳脚本。将你的生物信息学外壳操作保存在带注释的外壳脚本中,并置于版本控制之下,是确保工作可重复的最佳方式。我们将在第 12 章讨论外壳脚本。
命令替换
Unix 用户喜欢让 Unix 外壳为他们工作 —— 这就是通配符和大括号扩展等外壳扩展存在的原因(如果需要复习,请回顾第 2 章)。另一种有用的外壳扩展是命令替换。命令替换会在一行中运行 Unix 命令,并将输出作为字符串返回,可用于另一个命令中。这带来了很多有用的可能性。
一个很好的例子是新年伊始,我们还没习惯使用新日期的时候。例如,2013 年的第五天,我和合作者分享了一个名为 snp-sim-01-05-2012 的新结果目录(格式为月 - 日 - 年)。尴尬过后,Unix 解决方案出现了:date
命令可以以编程方式返回当前日期作为字符串。我们可以用这个字符串自动给目录命名,包含当前日期。我们使用命令替换来运行date
程序,并将该命令替换为其输出(字符串)。通过一个更简单的示例更容易理解:
$ grep -c '^>' input.fasta
416
$ echo "There are $(grep -c '^>' input.fasta) entries in my FASTA file."
There are 416 entries in my FASTA file.
这个命令用grep
计数(-c
选项表示计数)匹配模式的行数。在这种情况下,我们的模式^>
匹配 FASTA 标题行。因为每个 FASTA 文件条目都有一个像 “>sequence-a” 这样以 “>” 开头的标题,所以这个命令会匹配每个标题并计数 FASTA 条目数量。
现在假设我们想将grep
命令的输出插入另一个命令中 —— 这正是命令替换的用途。在这种情况下,我们希望echo
打印一条消息,包含 FASTA 条目的数量并输出到标准输出。通过命令替换,我们可以直接计算并返回 FASTA 条目数量到这个字符串中!
使用这种命令替换方法,我们可以轻松创建带日期的目录,使用date +%F
命令,其中参数+%F
只是告诉date
程序以特定格式输出日期。date
有多种格式化选项,因此你的欧洲同事可以将日期指定为 “19 May 2011”,而美国同事可以指定为 “May 19, 2011”:
$ mkdir results-$(date +%F)
$ ls results-2015-04-13
一般来说,date +%F
返回的格式非常适合命名带日期的目录,因为当结果按名称排序时,这种格式的目录也会按时间顺序排序:
$ ls -l
drwxr-xr-x 2 vinceb staff 68 Feb 3 23:23 1999-07-01
drwxr-xr-x 2 vinceb staff 68 Feb 3 23:22 2000-12-19
drwxr-xr-x 2 vinceb staff 68 Feb 3 23:22 2011-02-03
drwxr-xr-x 2 vinceb staff 68 Feb 3 23:22 2012-02-13
drwxr-xr-x 2 vinceb staff 68 Feb 3 23:23 2012-05-26
drwxr-xr-x 2 vinceb staff 68 Feb 3 23:22 2012-05-27
drwxr-xr-x 2 vinceb staff 68 Feb 3 23:23 2012-07-04
drwxr-xr-x 2 vinceb staff 68 Feb 3 23:23 2012-07-05
这种日期格式(称为 ISO 8601)的巧妙之处使其很有用。
存储你的 Unix 技巧
在第 2 章中,我们用mkdir -p
和大括号扩展创建了项目目录。如果你发现自己经常创建相同的项目结构,值得将其存储起来,而不是每次都输入。为什么要重复自己呢?
早期的 Unix 用户很聪明(或者说懒惰),他们设计了一个工具来存储重复的命令组合:alias
。如果你反复运行一个巧妙的单行命令,可以用alias
将其添加到~/.bashrc
(或 OS X 上的~/.profile
)中。alias
只是将你的命令别名化为一个更短的名称。例如,如果你总是用相同的目录结构创建项目目录,可以添加如下一行:
alias mkpr="mkdir -p {data/seqs,scripts,analysis}"
对于这类小操作,没必要编写更复杂的脚本;采用 Unix 的方式,保持简单就好。另一个例子是,我们可以将date +%F
命令别名为today
:
alias today="date +%F"
现在,输入mkdir results-$(today)
会创建一个带日期的结果目录。
不过要注意:不要在项目级别的外壳脚本中使用你的别名命令!这些别名定义在你的外壳启动文件中(如~/.profile
或~/.bashrc
),不在项目目录中。如果你分发项目目录,任何需要别名定义的外壳程序都无法运行。在计算中,我们称这种做法不具可移植性 —— 如果移到你的系统外,就会出错。编写可移植的代码,即使它不会在其他地方运行,也有助于保持项目的可重复性。
和所有好东西一样,命令替换等 Unix 技巧最好适度使用。一旦你对这些技巧更熟悉,它们会成为解决日常烦恼的快速简便方案。但总的来说,最好保持简单,知道何时使用快速的 Unix 解决方案,何时使用 Python 或 R 等其他工具。我们将在第 7 章和第 8 章更深入地讨论这一点。