Linux之shell脚本编程

前言

本文部分内容摘自《Linux命令行与shell脚本编程大全》第11章笔记及构建shell脚本一文就够,仅为学习使用。

内容

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

使用多个命令

Linux shell可以让你将多个命令串起来,一次执行完成。如果要两个命令一起运行,可以把它们放在同一行中,彼此间用分号隔开。

$ date; who
lyr  tyy7  Fri Oct 11 10:40:21 CST 2019

创建脚本文件

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

#!/bin/zsh    #zsh为用户当前正在使用的shell,有bash、zsh等

脚本文件的第一行中 # 后的会告诉shell使用哪个shell来运行脚本(如果是其他编码语言脚本,像python,第一行类似)。在指定了shell之后,就可以在文件的每一行中输入命令,其他地方的#用作注释行。shell不会解释以#开头的行(除了以#!开头的第一行)。
可以通过留下注释来说明脚本的用途,这方便你以后回过头来查看该脚本。
创建名为test1的脚本,内容为:

#!/bin/zsh
#This script display the date and who's logged on
echo -n " The time and date are: "
date

现在直接运行脚本,结果可能会让你失望。

$ test1
zsh: command not found: test1

运行结果显示命令未找到。事实上,在运行一个新脚本前,我们需要让shell能找到我们的脚本文件。shell会通过PATH环境变量来查找命令。
查看一下我们当前的PATH路径

$ echo $PATH
/home/lyr/.aspera/connect/bin:/home/lyr/.aspera/connect/bin:/home/lyr/miniconda3/condabin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/mnt/c/Program Files/WindowsApps/CanonicalGroupLimited.UbuntuonWindows_1804.2019.521.0_x64__79rhkp1fndgsc

很显然,我们的脚本文件没有在这些目录范围内。要让shell找到test1脚本,我们可以采取以下两种做法之一:

  • 将shell脚本文件所处的目录添加到PATH环境变量中;
  • 在提示符中用绝对路径或相对路径来引用shell脚本文件。
    第二种方法相对简单,我们需要将脚本的确切位置告诉shell
$ ./test1
Fri Oct 11 10:46:25 CST 2019
#引用当前目录下的文件,可以在shell中使用单点操作符

显示消息

很多时候,你可能想要添加自己的文本消息来告诉脚本用户脚本正在做什么。可以通过echo命令来实现这一点。如果在echo命令后面加上了一个字符串,该命令就能显示出这个文本字符串。

$ echo This is a test
This is a test

注意,默认情况下,不需要使用引号将要显示的文本字符串划定出来。但如果文本本身带有字符串时,我们就需要用另一种引号(单引号或双引号)来划定文本字符串。

#为使未用引号划定
$ echo Let's see if this'll work
Lets see if thisll work
#使用引号划定
$ echo "Let's see if this'll work"
Let's see if this'll work

可以将echo语句添加到shell脚本中任何需要显示额外信息的地方。
创建新脚本test2

#!/bin/zsh
#This script display 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

运行新脚本

$ ./test2
The time and date are:
Fri Oct 11 15:19:12 CST 2019
Let's see who's logged into the system:

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

#!/bin/zsh
#This script display the date and who's logged on
echo  -n "The time and date are: " 
date

现在的运行结果

$ ./test3
 The time and date are: Fri Oct 11 10:53:59 CST 2019

使用变量

通常我们不会满足于只使用shell脚本中的单个命令,我们会需要在shell命令使用其他数据来处理信息,这可以通过变量来实现。变量允许你临时性地将信息存储在shell脚本中,以便和脚本中的其他命令一起使用。

环境变量

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

使用set命令显示一份完整的当前环境变量列表。envprintenv命令都可以显示全局变量。

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

#!/bin/zsh
#display user information from system
echo User info for userid: $USER
echo UID: $UID
echo HOME: $HOME

运行结果

$ ./test4
User info for userid: lyr
UID: 1000
HOME: /home/lyr

当我们想要使用实际的美元符$而不是引用变量时,我们需要在美元符$前面加上 转义符\,以显示美元符$本身。

注:你可能还见过通过${variable}形式引用的变量。变量名两侧额外的花括号通常用来帮助识别美元符后的变量名。

用户变量

除了环境变量, shell脚本还允许在脚本中定义和使用用户自己的变量。定义变量允许临时存储数据并在整个脚本中使用,从而使shell脚本看起来更像一个真正的计算机程序。用户变量可以是任何由字母、数字或下划线组成的文本字符串,长度不超过20个。用户变量区分大小写。使用等号将值赋给用户变量。在变量、等号和值之间不能出现空格

shell脚本会自动决定变量值的数据类型。在脚本的整个生命周期里, shell脚本中定义的变量会一直保持着它们的值,但在shell脚本结束时会被删除掉。与系统变量类似,用户变量可通过美元符$引用。

#!/bin/zsh
#testing variables
days=10
guest="Katie"
echo $guest checked in $days ago
days=5
guest="Jessica"
echo "$guest checked in $days ago"

运行结果

$ ./test5
Katie checked in 10 ago
Jessica checked in 5 ago

变量每次被引用时,都会输出当前赋给它的值。重要的是要记住,引用一个变量值时需要使用美元符,而引用变量来对其进行赋值时则不要使用美元符。

命令替换

shell脚本中最有用的特性之一就是可以从命令输出中提取信息,并将其赋给变量。把输出赋给变量之后,就可以随意在脚本中使用了。

有两种方法可以将命令输出赋给变量:

  1. 反引号字符( `),这可不是用于字符串的那个普通的单引号字符,提示:在美式键盘上,它通常和波浪线( ~)位于同一键位。
  2. $()格式。
# 要么用一对反引号把整个命令行命令围起来
testing=`date`
# 要么使用$()格式
testing=$(date)

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

#!/bin/zsh
#copy the /usr/bin directory listing to a log list file
today=$(date +%y%m%d)   # +%y%m%d格式告诉date命令将日期显示为两位数的年月日的组合
ls /usr/bin -al > log.$today

运行结果

$ ll
total 44K
-rwxrwxrwx 1 lyr lyr 43K Oct 11 15:47 log.191011

重定向输入和输出

有些时候你想要将某个命令的输出保存到文件而不仅仅只是让它显示在显示器上,shell提供了几个操作符,可以将命令的输出重定向到另一个位置(比如文件),也可以将文件重定向到命令输入。

输出重定向

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

$ date > test6
$ ls test6
test6
$ ll test6
-rwxrwxrwx 1 lyr lyr 29 Oct 11 13:00 test6
$ cat test6
Fri Oct 11 13:00:39 CST 2019

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

$ date >> test6
$ cat test6
Fri Oct 11 13:00:39 CST 2019
Fri Oct 11 13:01:10 CST 2019

输入重定向

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

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

这里有个和wc命令一起使用输入重定向的例子,比如用wc命令检查文本的行数、词数和字节数。

$ wc < test6
 2 12 58

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

$ wc << EOF
heredoc> test string1     # 次提示符会持续提示,以获取更多的输入数据,直到你输入了作为文本标记的那个字符串
heredoc> test string2
heredoc> test string3
heredoc> EOF
 3  6 39

此处,EOF就作为了输入数据开头和结尾的文本标记。它的形式为:

command << marker
data
marker

管道

很多生信命令行工具需要提供多个输入和输出参数,shell提供了一个管道命令用来满足这种要求。

该符号由两个竖线构成,一个在另一个上面。然而管道符号的印刷体通常看起来更像是单个竖线( |)。

在美式键盘上,它通常和反斜线( \)位于同一个键。

管道被放在命令之间,将一个命令的输出重定向到另一个命令中:command1 | command2
不要以为由管道串起的两个命令会依次执行, Linux系统实际上会同时运行这两个命令,在系统内部将它们连接起来。在第一个命令产生输出的同时,输出会被立即送给第二个命令。数据传输不会用到任何中间文件或缓冲区。例如:

$ rpm -qa | sort | more

这行命令序列会先执行rpm命令,将它的输出通过管道传给sort命令,然后再将sort的输出通过管道传给more命令来显示,在显示完一屏信息后停下来。

到目前为止,管道最流行的用法之一是将命令产生的大量输出通过管道传送给more命令。ls -l命令产生了目录中所有文件的长列表。对包含大量文件的目录来说,这个列表会相当长。通过将输出管道连接到more命令,可以强制输出在一屏数据显示后停下来。

进程替换

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

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 ~/biosoft/VarScan/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命令

最开始, Bourne shell提供了一个特别的命令用来处理数学表达式。 expr命令允许在命令行上处理数学表达式,但是特别笨拙。

$ expr 1+5
1+5
$ expr 1 + 5
6

可以看到当+前后不留空格时,竟然还无法计算,真是笨拙!

尽管标准操作符在expr命令中工作得很好,但在脚本或命令行上使用它们时仍有问题出现。许多expr命令操作符在shell中另有含义(比如 * )。当它们出现在在expr命令中时,会得到一些诡异的结果。

$ expr 5 * 2
expr: syntax error

要解决这个问题,对于那些容易被shell错误解释的字符,在它们传入expr命令之前,需要使用shell的转义字符(反斜线)将其标出来。

$ expr 5 \* 2
10

在shell脚本中使用expr命令也同样复杂:

$ cat test6
#!/bin/bash
# An example of using the expr command
var1=10
var2=20
var3=$(expr $var2 / $var1)
echo The result is $var3

要将一个数学算式的结果赋给一个变量,需要使用命令替换来获取expr命令的输出:

$ chmod u+x test6
$ ./test6
The result is 2

使用方括号

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

zsh同样可以这样使用

$ var1=$[1+5]
$ echo $var1
6

这种方式不仅方便,而且因为在方括号内,不会让shell误解乘号或其他符号。

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

浮点解决方案

有几种解决方案能够克服bash中数学运算的整数限制。最常见的方案是用内建的bash计算器,叫作bc。

bc的基本用法
bash计算器实际上是一种编程语言,它允许在命令行中输入浮点表达式,然后解释并计算该表达式,最后返回结果。

bash计算器能够识别:

  • 数字(整数和浮点数)
  • 变量(简单变量和数组)
  • 注释(以#或C语言中的/* */开始的行)
  • 表达式,编程语句(例如if-then语句)
  • 函数

可以在shell提示符下通过bc命令访问bash计算器。要退出bash计算器,你必须输入quit。浮点运算是由内建变量scale控制的。必须将这个值设置为你希望在计算结果中保留的小数位数,否则无法得到期望的结果。

$ bc
bc 1.07.1
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006, 2008, 2012-2017 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
3.89 / 5
0
scale = 5
3.89 / 5
.77800
quit

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

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

第一部分options允许你设置变量。如果你需要不止一个变量,可以用分号将其分开。expression 参数定义了通过bc执行的数学表达式。一个简单例子:

$ cat test9
#!/bin/bash
var1=$(echo "scale=4; 3.44 / 5" | bc)
echo The answer is $var1

这个例子将scale变量设置成了四位小数,并在expression部分指定了特定的运算。运行这个脚本会产生如下输出。

$ chmod u+x test9
$ ./test9
The answer is .6880

这个方法适用于较短的运算,但有时你会涉及更多的数字。如果需要进行大量运算,在一个命令行中列出多个表达式就会有点麻烦。有一个方法可以解决这个问题。 bc命令能识别输入重定向,允许你将一个文件重定向到bc命令来处理。但这同样会叫人头疼,因为你还得将表达式存放到文件中。最好的办法是使用内联输入重定向,它允许你直接在命令行中重定向数据。在shell脚本中,你可以将输出赋给一个变量。

variable=$(bc << EOF
options
statements
expressions
EOF
)

记住,仍然需要命令替换符号将bc命令的输赋给变量

退出脚本

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

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

$ date
Fri Oct 11 17:40:50 CST 2019
$ echo $?
0

按照惯例,一个成功结束的命令的退出状态码是0。如果有错误,则显示一个正数值。

Linux错误退出状态码没有什么标准,但有一些参考:

状态码 描述
0 命令成功结束
1 一般性未知错误
2 不适合的shell命令
126 命令不可执行
127 没找到命令
128 无效的退出参数
128 + x 与Linux信号x相关的严重错误
130 通过Ctrl+C终止的命令
255 正常范围之外的退出状态码
$ asdf
zsh: command not found: asdf
$ echo $?
127

exit命令

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

$ 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,如果大于它,得到的是求模的结果(余数)。

参考资料

  1. 《Linux命令行与shell脚本编程大全》
  2. 《Linux命令行与shell脚本编程大全》第11章笔记
  3. 构建shell脚本一文就够

你可能感兴趣的:(Linux之shell脚本编程)