前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一起来学shell bash编程(2)

一起来学shell bash编程(2)

作者头像
生信菜鸟团
发布2020-11-11 13:04:11
2K0
发布2020-11-11 13:04:11
举报
文章被收录于专栏:生信菜鸟团

首先我们先回顾一下,上次推文主要介绍了bash编程的基础知识,还有一些基本的代码规范。如果你还没读过上次的推文,请不要犹豫先点击下面的链接。

一起来学shell bash编程(1)

回顾完之后,这次的推文主要学习如何在bash中写更好的loops,还有一些更加高级的shell bash编程知识。事不宜迟,开始今天的学习。

写更好的loops

糟糕的循环(loops)的示范

首先下载一些测试数据:

代码语言:javascript
复制
fastq-dump --split-files -X 10000 SRR1553607fastq-dump --split-files -X 10000 SRR1972917

接着假s为我们需要将reads的长度缩短到20 bp。我们使用 cutadapt-l20。现在让我们先编写一个糟糕的脚本:

代码语言:javascript
复制
for name in *.fastq; do    echo "cutadapt -l 20 $name -o $name.trimmed.fq"done

以便小伙伴们更好地了解发生了什么。我们这里先打印命令而不是执行命令,我们的脚本将产生以下内容:

代码语言:javascript
复制
cutadapt -l 20 SRR1553607_1.fastq -o SRR1553607_1.fastq.trimmed.fqcutadapt -l 20 SRR1553607_2.fastq -o SRR1553607_2.fastq.trimmed.fqcutadapt -l 20 SRR1972917_1.fastq -o SRR1972917_1.fastq.trimmed.fqcutadapt -l 20 SRR1972917_2.fastq -o SRR1972917_2.fastq.trimmed.fq

为什么说这个循环(loop)是一个糟糕的例子呢?第一,我们通过文件名(*fq)进行模式匹配,这样一些不是我们想处理,但是又有相同文件名的文件也会被处理。第二,此代码不断在文件名中添加扩展名,每个生成的文件现在都以我们不期待的结尾 .fastq.trimmed.fq

要解决扩展名的问题,我们需要调用更复杂的bash构造,bash的替换运算符 %.*

代码语言:javascript
复制
for name in *.fastq; do    echo "cutadapt -l 20 $name -o ${name%.*}.trimmed.fq"done

可是现在的代码显得更复杂,可读性更低了。

一个优秀的循环的例子

首先,我们需要养成一个习惯,永远不要在 *匹配的文件“模式”(例如 *.fastq*.bam等)上运行命令。因为文件的处理顺序可能与期望的不符。另外运行时可能会增加一些你不想运行的文件;这个糟糕的习惯最终会导致一些棘手的问题。

一个好的习惯是,我们需要整理出我们要处理文件的“根”,换而言之就是数据之间用于独特标识的那一部分。以上面的测试数据为例子,它们的“根“就是:

代码语言:javascript
复制
SRR1553607SRR1972917

将上面的根存进去 ids.txt中,然后我们使用更好的写命令或者循环的工具 parallel

代码语言:javascript
复制
cat ids.txt | parallel echo cutadapt -l 20 {}_1.fastq -o {}_1.trimmed.fqcat ids.txt | parallel echo cutadapt -l 20 {}_2.fastq -o {}_2.trimmed.fq
##结果显示cutadapt -l 20 SRR1553607_1.fastq -o SRR1553607_1.trimmed.fqcutadapt -l 20 SRR1972917_1.fastq -o SRR1972917_1.trimmed.fqcutadapt -l 20 SRR1553607_2.fastq -o SRR1553607_2.trimmed.fqcutadapt -l 20 SRR1972917_2.fastq -o SRR1972917_2.trimmed.fq

这个代码看起来就是更加的清楚明了,该代码根据我们给予的“根”,使用 {}进行匹配,指明了对应的输入和生成文件。

当我们用编程语言编写一个 forloop时,我们正在构建一个迭代的命令式:我们要求计算机首先完成一个工作,然后循环到最后。但通过GNU Parallel编写命令时,我们遵循所谓的描述性功能编程。就是,我们尝试用模式描述我们想要的内容,然后让计算机填写该模式并输入完整命令。

GNU Parallel的极简介绍

GNU Parallel 是一个非常好用文件并行的工具。

假设有一个名为的文件 ids.txt,其中包含:

代码语言:javascript
复制
ABC

假设我们要输出:

代码语言:javascript
复制
Hello AHello BHello C

多种方法指定GNU的并行输入

通过文件输入:

代码语言:javascript
复制
cat ids.txt | parallel echo Hello {}

在命令行中通过用3个冒号( :::)来指定输入:

代码语言:javascript
复制
parallel echo Hello {} ::: A B C

最后,当用四个冒号( ::::)分隔时,您也可以在文件末尾传递文件

代码语言:javascript
复制
parallel echo Hello {} :::: ids.txt

上面的每个命令产生相同的输出。

处理多个输入源

假如我们获取所有的排列组合:

代码语言:javascript
复制
parallel echo Hello {1} and {2} ::: A B  ::: 1 2
Hello A and 1Hello A and 2Hello B and 1Hello B and 2

获取一一对应的组合,使用 --link

代码语言:javascript
复制
parallel --link echo Hello {1} and {2} ::: A B  ::: 1 2
Hello A and 1Hello B and 2

更多详细的关于GNU parallel的内容,可以查阅我之前的推文:

生信小技巧:并行运行的秘密

更加高级的shell编程

自带manual的bash脚本

一个好的脚本是应该自带说明manual的。例如,一个脚本需要运行的参数,参数的使用说明等。

下面给大家一个模板例子:

代码语言:javascript
复制
bash getdata.sh
*** This script needs arguments to work! ***
Usage:   getdata.sh  PRJNUM COUNT LIMT
Parameters:  PRJNUM = SRA Bioproject number  COUNT = how many sequencing runs to download  LIMIT = how many reads to extract per sequencing run
Example:   getdata.sh PRJN2234 1000 5

下面是如何编写该说明manual的代码:

代码语言:javascript
复制
if [ $# -eq 0 ]; then    echo    echo "*** This script needs arguments to work! ***"    echo    echo "Usage:"    echo "   getdata.sh  PRJNUM COUNT LIMT"    echo    echo "Parameters:"    echo "  PRJNUM = SRA Bioproject number"    echo "  COUNT = how many sequencing runs to download"    echo "  LIMIT = how many reads to extract per sequencing run"    echo    echo "Example:"    echo "   getdata.sh PRJN2234 1000 5"    echo    exit 1fi

更好的输出定向

Bash有一个输入流( stdin)和两个输出流( stdoutstderr)。

通常命令的输出将进入标准输出( stdout),错误消息将变为标准错误( stderr)。

默认情况下,两者stdout和stderr都被定向到终端。例如,我可以输入:

代码语言:javascript
复制
ls * foo > B.txt

因为f不存在它输出:

代码语言:javascript
复制
ls: foo: No such file or directory

更加好的方式是使用 2>,将标准错误存储起来:

代码语言:javascript
复制
ls * foo > B.txt 2> err.txt

如果你遇到错误,则可以调查错误信息文档以获取消息。

如何在bash中操作文件路径?

通常,我们必须在bash中操作文件名以删除其中的各个部分。也许我们想要删除目录名称,或者仅保留文件名,或者仅保留不带扩展名的文件名,或者删除扩展名等等。

下面让我看一些例子:

代码语言:javascript
复制
FILE=/A/B/C.txt.gzecho $FILE

如预期打印:

代码语言:javascript
复制
/A/B/C.txt.gz

从名称中删除目录,并仅使用basenameshell命令保留文件名:

代码语言:javascript
复制
FILE=/A/B/C.txt.gzNAME=$(basename ${FILE})echo $NAME

打印:

代码语言:javascript
复制
C.txt.gz

要切断最右边的扩展名:

代码语言:javascript
复制
FILE=/A/B/C.txt.gzCHOP=${FILE%.*}echo $CHOP

它将打印

代码语言:javascript
复制
/A/B/C.txt

现在只获取扩展名:

代码语言:javascript
复制
FILE=/A/B/C.txt.gzCHOP=${FILE##*.}echo $CHO

它打印:

代码语言:javascript
复制
gz

如何将动态命令转换为变量?

用反引号将其括起来:

代码语言:javascript
复制
VALUE=`ls -1 | wc -l`echo "The number of files is $VALUE"

如何为变量分配默认值?

要将默认值分配给变量,请使用以下结构:

代码语言:javascript
复制
FOO=${VARIABLE:-default}

例如,要将 LIMIT变量设置为第一个参数, $1 或者 1000默认值如果未指定该参数:

代码语言:javascript
复制
LIMIT=${1:-1000}

如何构建更复杂的脚本?

编写一个脚本的最好的办法是先将需要运行的代码打印出来,而不是直接运行所有的代码:

代码语言:javascript
复制
echo fastq $SOMETHING

将每一步的命令打印到屏幕可以让我们更加直观的检查每一行的代码。如果整个流程的代码看起来都没问题,就ji执行命令,然后bash再次将它们通过管道传递给命令。

今天的学习就到这里结束了,希望本推文对大家有所帮助。

本文整理参考于:biostar中writing-better-scripts的内容,有条件的小伙伴可以自行购买和下载。https://www.biostarhandbook.com/books/scripting/writing-better-scripts.html

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 生信菜鸟团 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写更好的loops
    • 糟糕的循环(loops)的示范
      • 一个优秀的循环的例子
        • GNU Parallel的极简介绍
          • 多种方法指定GNU的并行输入
            • 处理多个输入源
            • 更加高级的shell编程
              • 自带manual的bash脚本
                • 更好的输出定向
                  • 如何在bash中操作文件路径?
                    • 如何将动态命令转换为变量?
                      • 如何为变量分配默认值?
                        • 如何构建更复杂的脚本?
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档