Shell:脚本知识

非常多的朋友在看我们公众号过往转录组,WES,等流程分享的时候发现很难理解我们的代码,其实就是缺乏shell脚本知识,那么这篇教程你就不容错过。

内容

  • 使用多个命令
  • 创建脚本文件
  • 显示消息
  • 使用变量
  • 输入输出重定向
  • 管道
  • 数学运算
  • 退出脚本

一个脚本例子

1
2
3
4
5
6
7
8
9
10
bed=exon_probe.hg38.gene.bed
for bam in /home/project/*.bam
do
file=$(basename $bam )
sample=${file%%.*}
echo $sample
export total_reads=$(samtools idxstats $bam|awk -F '\t' '{s+=$3}END{print s}')
echo The number of reads is $total_reads
bedtools multicov -bams $bam -bed $bed |perl -alne '{$len=$F[2]-$F[1];if($len <1 ){print "$.\t$F[4]\t0" }else{$rpkm=(1000000000*$F[4]/($len* $ENV{total_reads}));print "$.\t$F[4]\t$rpkm"}}' >$sample.rpkm.txt
done

使用多个命令

如果多个命令一起使用,可以放在一行并用分号分隔。

1
2
3
$ date; who
2017年 07月 26日 星期三 09:53:43 CST
wsx tty7 2017-07-26 09:48 (:0)

创建脚本文件

在创建脚本文件时,必须在文件的第一行指定要使用的 shell,格式为:

1
#!/bin/bash

脚本文件的第一行中 # 后的惊叹号会告诉shell使用哪个 shell 来运行脚本(如果是其他编码语言脚本,像python,第一行类似)。

其他地方的 # 用作注释行。

添加名为 test1 的脚本文件,内容为:

1
2
3
4
#!/bin/bash
# This script displays the date and who's logged on
date
who

显示消息

echo 命令后面加上一个字符串,就能显示出这个文本字符串。这种方式可以添加自己的文本消息来告诉脚本用户脚本正在做什么。

1
2
$ echo This is a test
This is a test

如果文本本身带有字符串,我们需要用单引号或双引号来划定文本字符串。

1
2
$ echo "Let's see if this'll work"
Let's see if this'll work

修改下之前的test1文件,增加消息显示:

1
2
3
4
5
6
#!/bin/bash
# This script displays the date and who's logged on
echo The time and date are:
date
echo "Let's see who's logged into the system"
who

运行:

1
2
3
4
5
$ ./test1
The time and date are:
2017年 07月 26日 星期三 10:17:59 CST
Let's see who's logged into the system
wsx tty7 2017-07-26 09:48 (:0)

如果想把文本字符串和命令输出显示在同一行中,可以用 echo 语句的 -n 参数。需要在字符串的两侧加上引号,并且保证字符串尾部有一个空格(不然字符串和命令输出就粘连到一起了)。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# This script displays the date and who's logged on
echo -n "The time and date are: "
date
echo "Let's see who's logged into the system: "
who

# 运行结果输出
$ ./test1
The time and date are: 2017年 07月 26日 星期三 10:24:04 CST
Let's see who's logged into the system:
wsx tty7 2017-07-26 09:48 (:0)

使用变量

变量允许我们临时性地将信息存储在shell脚本中,以便和脚本中的其他命令一起使用。

环境变量

shell 维护着一组环境变量,用来记录特定的系统信息。比如系统的名称、登录到系统上的用户名、用户的系统 ID (也称为 UID )、用户默认主目录以及 shell 查找程序的搜索路径。

使用 set 命令显示一份完整的当前环境变量列表。 envprintenv 命令都可以显示全局变量。(这些命令输出结果比较多,不展示了。之前关于环境变量的笔记有比较详细的描述。)

在环境变量名称之前加上美元符可以使用这些环境变量。

1
2
3
4
5
6
7
8
9
10
11
$ cat test2
#! /bin/bash
# display user information from the system
echo "User info for userid: $USER"
echo UID: $UID
echo HOME: $HOME
chmod u+x test2
./test2
user info for userid: wsx
UID: 1000
HOME: /home/wsx

可以想象的到,如果我们想要使用实际的美元符而不是引用变量,肯定会出问题。这时候我们需要在美元符前面加上 \ 进行转义,以显示美元符本身。

用户变量

使用等号将值赋给用户变量。注意,在变量、等号和值之间不能出现空格!

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cat test3
#!/bin/bash
# testing variables
days=10
guest="Katie"
echo "$guest checked in $days days ago"
days=5
guest="Jessica"
echo "$guest checked in $days days ago"
$ chmod u+x test3
$ ./test3
Katie checked in 10 days ago
Jessica checked in 5 days ago

命令替换

shell 脚本最有用的特性之一就是可以从命令输出中提取信息,并将其赋给变量。
有两种方法可以将命令输出赋给变量:

  • 反引号字符 (`) [数字1前面的那个键]
  • $() 格式
1
2
3
4
# 要么用一对反引号把整个命令行命令围起来:
testing=`date`
# 要么使用$()格式
testing=$(date)

下面是一个例子,在脚本中通过命令替换获得当前日期并用它来生成唯一文件名:

1
2
3
4
5
6
7
$ cat test4
#!/bin/bash
# copy the /usr/bin directory listing to a log file
today=$(date +%y%m%d)
ls /usr/bin -al > log.$today
$ chmod u+x test4
$ ./test4

重定向输入和输出

通过几个操作符进行重定向,我们可以将命令的结果输出到另外的位置(文件)。当然,重定向可以用于输入。

输出重定向

最基本的操作符是 >。比如我们想要输出命令结果到一个指定文件:

1
2
3
4
5
$ date > test6
$ ls -l test6
-rw-rw-r-- 1 wsx wsx 43 7月 26 16:42 test6
$ cat test6
2017年 07月 26日 星期三 16:42:34 CST

如果想要将命令的输出追加到已有文件中,需要用双大于号>>来追加数据。

输入重定向

输入重定向和输出重定向正好相反。输入重定向将文件的内容重定向到命令,而非将命令的输出重定向到文件。
使用的符号是小于号<

一种简单的记忆方法是:在命令行上,命令总是在左侧,而重定向符号“指向”数据流动的方向。小于号说明数据正在从输入文件流向命令。

比如用 wc 命令检查文本的行数、词数和字节数。

1
2
$ wc < test6
1 6 43

另一种输入重定向的方法是内联输入重定向。它无需使用文件进行重定向,只需要在命令行中指定用于输入重定向的数据即可。它使用的符号是远小于号 << ,除了这个符号,我们还需要指定一个文本标记用来划分输入数据的开始和结尾。任何字符串都可以作为文本标记,但在数据的开始和结尾文本标记必须一致。

$ wc << EOF

test string1

test string2

test string3

EOF

3 6 39

它的形式为: command << marker

管道

很多生信命令行工具需要提供多个输入和输出参数,这用在管道命令里可能会导致非常低效的情形(管道只接受一个标准输入和输出)。幸好,我们可以使用命令管道来解决此类问题。

命名管道,也称为 FIFO。它是一个特殊的排序文件,命名管道有点像文件,它可以永久保留在你的文件系统上(估计本质就是文件吧~)。

比如我想查看某个文件(test1)的前两行并进行排序,操作如下:

1
2
3
4
5
6
7
8
9
10
$ cat test1
#!/bin/bash
# This script displays the date and who's logged on
echo -n "The time and date are: "
date
echo "Let's see who's logged into the system: "
who
$ cat test1 | head -2 | sort
#!/bin/bash
# This script displays the date and who's logged on

没看懂这个“命令管道”…

进程替换

有些命令需要接受多个管道的输入作为自己的输出,这个时候普通的管道已经无法完成任务了。需要用到进程替换,来避免多次创建中间文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
start=$(date +%s.%N)
echo VarScan `date`
normal_pileup="samtools mpileup -q 1 -f $reference $normal_bam";
tumor_pileup="samtools mpileup -q 1 -f $reference $tumor_bam";

# Next, issue a system call that pipes input from these commands into VarScan :
java -Djava.io.tmpdir=$TMPDIR -Xmx40g -jar VarScan.v2.3.9.jar \
somatic <($normal_pileup) <($tumor_pileup) ${sample}_varscan

java -jar ~/biosoft/VarScan/VarScan.v2.3.9.jar processSomatic ${sample}_varscan.snp

echo VarScan `date`
dur=$(echo "$(date +%s.%N) - $start" | bc)
printf "Execution time for VarScan : %.6f seconds" $dur
echo

执行数学运算

对 shell 脚本来说,执行数学运算非常麻烦。有两种实现方式。

expr命令

expr 命令允许在命令行上处理数学表达式,但是特别笨拙。(Bourne shell中)

1
2
3
4
$ exrpr 1 + 5
未找到 'exrpr' 命令,您要输入的是否是:
命令 'expr' 来自于包 'coreutils' (main)
exrpr:未找到命令

看到没有,那算了。它基本涉及的操作跟我们使用的其他语言是一致的。但是有些问题需要处理,像 * 是通配符,在运算是是做乘号处理的,需要进行转义。

使用方括号

bash shell 提供了一种更简单的方法来执行数学表达式。在 bash 中,在将一个数学运算结果赋给某个变量时,可以用美元符和方括号$[operator]将数学表达式围起来。

1
2
3
4
5
6
$ var1=$[1+5]
$ echo $var1
6
$ var2=$[$var1+2]
$ echo $var2
8

但 bash shell 计算有一个主要限制:它只支持整数运算!

浮点解决方案

最常见的方案是用内建的 bash 计算器。它实际上是一门编程语言,它允许在命令行中输入浮点表达式,然后解释并计算该表达式,最后返回结果。bash 计算器能够识别:

  • 数字(整数和浮点数)
  • 变量(简单变量和数组)
  • 注释(/* */开始的行)
  • 表达式
  • 编程语句
  • 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
$ bc
> bc 1.06.95
> Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc.
> This is free software with ABSOLUTELY NO WARRANTY.
> For details type `warranty'.

$ 12 * 5.4
> 64.8

$ 3.156 * (3 + 5)
> 25.248

$ quit

在脚本中使用 bc

可以用命令替换运行bc命令,并将输出赋给一个变量。基本格式如下:

1
variable=$(echo "options; expression" | bc)

options 设置变量,expression参数定义了通过 bc 执行的数学表达式。

看一个简单实例:

1
2
3
4
5
6
7
8
9
$ cat test9
#!/bin/bash
var1=$(echo "scale=4; 3.44/5" | bc)
echo The answer is $var1

$ chmod u+x test9

$ ./test9
The answer is .6880

这个例子将 scale 变量设置为四位小数,并在 expression 部分指定了特定的运算。

这个方法适用于较短的运算,但有时我们会涉及更多的数字。如果需要进行大量运算,在一个命令行中列出多个表达式就会有点麻烦。

这里有一个解决方法:使用内联输入重定向,将一个文件重定向到 bc 命令来处理。格式为:

1
2
3
4
5
variable=$(bc << EOF
options
statements
expressions
EOF)

EOF 文本字符串标识了内联重定向数据的起始。注意,仍然需要命令替换符号将 bc 命令的输出赋给变量。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cat test10
#!/bin/bash
var1=10.46
var2=43.67
var3=33.2
var4=71
var5=$(bc <<EOF
scale=4
a1 = ( $var1 * $var2)
b1 = ( $var3 * $var4)
a1 + b1
EOF)
echo The final answer for this mess is $var5

$ chmod u+x test10

$ ./test10
The final answer for this mess is 2813.9882

在普通的 shell 脚本中,数字默认当做字符串处理。这也是为什么我们脚本处理计算麻烦和我们需要特定的工具和方法来进行处理。一定要注意区分。

退出脚本

状态码

前面运行的脚本都是命令执行完成,脚本自动结束。其实我们可以用更为优雅的方式告诉 shell 命令运行完成,因为每个命令都使用退出状态码(exit status),它是一个 0-255 的整数值,我们可以捕获这个值并在脚本中使用。

Linux提供了一个专门的变量$?`来保存上个已执行命令的退出状态码。

1
2
3
4
5
$ date
2017年 07月 27日 星期四 10:44:18 CST

$ echo $?
0

statuscode

1
2
3
4
5
6
7
$ asfg
未找到 'asfg' 命令,您要输入的是否是:
命令 'asdfg' 来自于包 'aoeui' (universe)
asfg:未找到命令

$ echo $?
127

exit命令

默认,shell 脚本会以脚本最后的一个命令的退出状态码退出。

但是我们可以改变这种默认行为,返回自己的退出状态码。exit命令允许在脚本结束时指定一个状态退出码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cat test13
#!/bin/bash
# testing the exit status
var1=10
var2=30
var3=$[$var1 + $var2]
echo The answer is $var3
exit 5

## 上面就是脚本的内容,用cat可以查看文本文件,下面就添加可执行权限并且执行该脚本

$ chmod u+x test13

$ ./test13
The answer is 40

$ echo $?
5

注意最大 255,如果大于它,得到的是求模的结果(余数)。

脚本高级知识

还有一些脚本高级知识,不予讲解,感兴趣的同学可以自行购买相关书籍和专业视频自学:

  • 处理信号
  • 以后台模式运行脚本
  • 禁止挂起
  • 作业控制
  • 修改脚本优先级
  • 脚本执行自动化

强烈推荐马哥 linux 视频。

转自 - 生信技能树:构建shell脚本一文就够