前言
我是水木,这是生信编程技巧系列的第三篇,在这个系列里面我会力求用简洁易懂的语言分享一些我自己经常用到的能够提高分析效率的编程技巧,希望也能提升你的分析效率!
本系列的前2篇文章如下:
今天介绍一个新的shell编程小技巧,即使用一行shell命令对多个变量赋值。
太长不看版
假如有一个基因注释的文件(a.tsv
),其内容如下:
CHROM START END STRAND GENEID
1 65419 71585 + ENSG00000186092
1 450703 451697 - ENSG00000284733
该文件的前五列分别是该基因的染色体名称、起始坐标、终止坐标、链的方向以及基因ID。
如果仅使用一行代码从中提取出基因ENSG00000284733的染色体名称、起始坐标、终止坐标、链的方向并赋值给chrom
, start
, end
和strand
四个变量,那么可以采用如下两种方式:
方式1:
$ myfile=a.tsv
$ read -r chrom start end strand < <(grep ENSG00000284733 $myfile |cut -f1-4)
方式2:采用Here-String
和read
命令
$ read -r chrom start end strand <<< "$(grep ENSG00000284733 $myfile |cut -f1-4)"
下面是完整版本,包括详细的例子和代码。
用过Python语言的用户应该知道,Python支持在一行代码中对多个变量同时赋予不同的值,例如如下示例:
a, b, c = 1, 2, 3 # 通过逗号来对不同的变量分隔,从而对多个变量单独赋予不同的值
print(f'a is {a}') # 打印每个变量的值,这里用到了python的f-string语法,
# 即在字符串前面加入f以后,在字符串里面可以使用大括号({})来显示变量的值,
# 这种用法相比python传统的%符号更加好用
print(f'b is {b}')
print(f'c is {c}')
运行上述代码以后的结果如下:
a is 1
b is 2
c is 3
这种在一行内对多个变量赋值的方式实在是简洁至极!不需要写很多行代码,也不会混乱,并且非常高效。
然而在shell中并不能使用上述语法进行多个变量赋值,在shell中变量名与赋值符号(=)之间不可以有空格,否则变量名就会被作为shell命令进行执行。
如果向上面的例子一样对三个变量进行赋值,传统的做法(每行单独赋值)如下:
$ a=1
$ b=2
$ c=3
$ echo "a is $a"
$ echo "b is $b"
$ echo "c is $c"
但是这样就存在一个代码冗余的问题,当需要赋值的变量较多时就需要写很多行几乎一模一样的命令,并当使用某个命令的结果进行赋值时,上述用法就会更加冗余。
下面来看一个实际分析中会遇到的情况,假如有一个基因注释文件(名为a.tsv
)如下:
CHROM START END STRAND GENEID
1 65419 71585 + ENSG00000186092
1 450703 451697 - ENSG00000284733
该文件的前五列分别是该基因的染色体名称、起始坐标、终止坐标、链的方向以及基因ID。
如果要从该文件中取出基因ENSG00000284733的染色体名称、起始坐标、终止坐标和链的方向,那么可以使用如下代码:
$ myfile=a.tsv
# 下面依次在该文件中搜索该基因所在的行,然后使用cut取出特定的信息
$ chrom=$(grep ENSG00000284733 $myfile |cut -f1)
$ start=$(grep ENSG00000284733 $myfile |cut -f2)
$ end=$(grep ENSG00000284733 $myfile |cut -f3)
$ strand=$(grep ENSG00000284733 $myfile |cut -f4)
$ echo "chrom is $chrom"
$ echo "start is $start"
$ echo "end is $end"
$ echo "strand is $strand"
上述命令的结果如下:
chrom is 1
start is 450703
end is 451697
strand is -
在上面这个例子中,需要从一个文件中取出某个基因的四个信息,因此上述命令复制了四次,实在有些冗余,并且这样也增加了不必要的运行时间,尤其是当myfile
文件内容很多时,使用grep
命令搜索将会非常慢,从而造成不必要的时间浪费。
这里用到了shell的命令替换(Command Substitution)语法,它指的是将命令的标准输出值赋值给某个变量,其用法有两种:
$(shell command)
shell command
个人更推荐使用第一种用法,它对于某些字符串变量的处理会比第二种用法更好。
下面来介绍一下shell中在一行代码同时对多个变量赋值的两种方式。
方式1:使用read命令和进程替换
shell的read
命令可以从标准输入中读取结果并赋值到变量中,其基本用法如下:
read -r var1 var2 var3
而进程替换已经在前面的推文中介绍过,请参考shell编程小技巧:进程替换。简单来说,进程替换指的是通过<(shell command)
语法将其中shell command的结果作为一个文件对待。
那么就可以将进程替换与read
命令结合起来对多个变量进行赋值,例如可以采用下面的代码来从基因注释文件中取出该基因的信息:
$ myfile=a.tsv
# 第一个<表示标准输入,表示将右边的文件内容“输入”到左边的read命令中
# 第二个<()是进程替换的语法,<()内部的命令表示对该文件进行搜索,并取出第1列至第4列
$ read -r chrom start end strand < <(grep ENSG00000284733 $myfile |cut -f1-4)
$ echo "chrom is $chrom"
$ echo "start is $start"
$ echo "end is $end"
$ echo "strand is $strand"
输出结果与上面一致。
注意,这里read
命令加了-r
参数,表示禁用反斜杠(\
)的转义,即当文件中有转义字符时会原样输出,不会进行转义,因此建议大家在使用read
时总是指定-r
参数。
此外,read
的默认分隔符为空格和制表符(Tab),如果上述进程替换中得到的输出不是由空格或制表符所分隔,那么需要在read
命令前面指定IFS
变量来指定分隔符,例如将分隔符指定为@
符号,命令如下:
$ IFS='@' read -r a b < <(echo "shell@python") # 这里通过IFS指定分隔符,其全称为Internal Field Separator
$ echo "a is $a"
a is shell
$ echo "b is $b"
b is python
方式2:使用命令替换和Here-String
Here-String指的是将某个字符串重定向到某个命令作为该命令的输入,其基本格式为COMMAND <<< "some strings"
:
一个简单的示例如下:
$ cat <<< "This is a string" # 等价于 cat "This is a string"
This is a string
# 也可以将变量放到<<<右边,它会将变量的结果传递给左边的命令
$ var="This is a string"
$ cat <<< "$var"
This is a string
因此使用Here-String
结合read
命令来对多个变量赋值,代码如下:
$ read -r a b c <<< "1 2 3"
$ echo "a is $a"
$ echo "b is $b"
$ echo "c is $c"
# 结果与上面一致
这里对右边字符串以空格为分隔符进行了切分,将1, 2, 3分别赋值给了a, b, c变量。
那么更进一步,将<<<
右边的字符串替换为上述提取基因信息的命令,便可以采用如下代码来提取基因的信息并赋值给不同的变量:
$ read -r chrom start end strand <<< "$(grep ENSG00000284733 $myfile |cut -f1-4)"
$ echo "chrom is $chrom"
$ echo "start is $start"
$ echo "end is $end"
$ echo "strand is $strand"
输出结果与上面一致。
总结
本文介绍了两种方式来使用一行shell
命令对多个变量进行赋值:
- 方式1:采用进程替换和
read
命令,read -r var1 var2 var3 < <(shell command)
- 方式2:采用
Here-String
和read
命令,read -r var1 var2 var3 <<< $(shell command)
本文参考链接:https://www.baeldung.com/linux/bash-multiple-variable-assignment