退出码

linux 每个命令都使用退出状态码(exit status) 来告诉shell 该命令已经完成。每次命令执行后返回的退出状态码都会用$?变量保存。

  1. $ ls
  2. ...
  3. $ echo $?
  4. 0

linux 的退出状态码一般有以下种类:

0 命令成功结束
1 一般性未知错误
2 不适合的shell 命令
126 命令不可执行
127 为没找到命令
128 无效的退出参数
128+x 与linux 信号x 相关的严重错误
130 通过ctrl+c 终止的命令
255 正常范围外的退出码

另外,我们也可以在脚本的末尾指定exit xxx,设定xxx 使得该脚本返回指定的退出码。但退出码的范围为0-255,因此如果退出码为大于该区间的数,则shell 会通过模运算取余。如300,则会返回44,300%256=44

if 条件句

shell 中的if 与其他编程语言有所不同。它默认下的if 后的对象为一个命令(command),而非通常的条件(condition)。

if command1
then  
  command2
elif command3
then 
  command4
else command5
  if command6
  then
    command7
  fi
fi
  • 通常if条件句的执行顺序为优先执行if 中的命令,若该命令的退出码为0(成功结束),则执行紧跟ifthen 中的命令。
    若返回非零退出码,则会执行else部分。
  • elif则相当于 else + if + then,若elif 后命令的退出码为0,则会继续执行elif 后面的语句。
  • 但需要注意的是,elif 实际相当于一个新的if语句,因此elif后面的thenelse是相对于elif的退出码来执行的:为0则执行then,非零则执行else
  • 有时候在执行了一次条件之后,还可以使用嵌套if,检查更多的条件。需要注意的是,每一次调用if,都需要在当前的结尾处使用fi,声明if-then语句的结束。

test 命令

test 命令就有点像一般编程语句的默认if 选项,提供了条件判断的途径。如果test 条件成立,则会返回退出码0。

if test condition1
if [ condition2 ]

⚠️注意:中括号与条件之间需要间隔一个空格。

一共有以上两种条件测试的方法。
test 提供了三类条件的判断:

  • 数值比较
  • 字符串比较
  • 文件比较

数值比较

shell 的数值比较使用的比较操作符与字符串是不一样的。(和perl 恰恰相反)
010. linux 的结构化命令 - 图1
perl 中的命令:

$ cat test.perl 
#!/usr/bin/perl

if('as' gt 'a'){
    print "hello!\n";
}

输出

$ perl test.perl
hello!

shell 脚本中的命令:

$ cat test2
#!/bin/bash

if [ 54 -gt 44 ]
then echo -e "hello!"
fi

输出

$ test2
hello!

需要注意的是,bash shell 只能处理整数,也即如果语句中出现浮点值,则shell 会报错。

$ cat test3
#!/bin/bash

if [ 54.22 -gt 44 ]
then echo -e "hello!"
fi
$ test3
/tmp/test/test3: line 3: [: 54.22: integer expression expected

总结一下:
image.png

字符串比较

str1 = str2 # 相等
str1 != str2
str1 < str2 # 小于
str1 > str2
-n str1 # 检查str1 长度是否非0
-z str1 # 检查str1 长度是否为0
  • 需要注意的是,>< 在shell 中有重定向之意,因此在实际比较中需要使用转义符\
if [ $var1 \> $var2 ]
then command

另外,字符串的比较顺序和sort命令相反。主要体现在两点:

  • 较长的字符串小于较短的字符串;
  • 大写字母小于小写字母。

这是因为比较测试采用的是ASCII 顺序,因此大写字母出现在小写字母之前。
010. linux 的结构化命令 - 图3

此外,-n与-z 选项非常重要,可以用来在操作数值或字符比较前用于确定其是否为空。

小注意事项

test 命令的数值比较与字符串比较使用不同的比较符号进行比较。因此在使用时需要注意,如果使用了字符串比较符号对数值进行比较,则shell 会将数字当作字符串值进行运算,会对结果造成影响。

文件比较

文件比较是比较测试中最丰富的类型。

-d file # 检查file 是否为一个目录
-e file # 检查file 是否存在
-f file # 是否为文件
-r file # 是否可读
-s file # 是否非空
-w file # 是否可写
-x file # 是否可执行
-O file # 是否属于当前用户
-G file # 默认组是否与当前用户相同
file1 -nt file2 # 判断文件哪个更新
file1 -ot file2 # 判断文件哪个更旧
! # 条件前面加!表示取反

复合条件测试

shell 提供了两种布尔运算符实现复合条件测试。

[ condition1 ] && [ condition2 ] # AND,需要满足两个条件才返回0
[ condition3 ] || [ condition4 ] # OR,满足其中之一即可返回0

高级特性

高级数学表达式

可以使用双括号,实现高级的数学表达式。同样地,需要在括号与语句之间,保留空格。

(( expression/condition ))

这样的表达使得数学赋值与比较变得更加灵活。
比如单方括号的条件测试,是无法执行多命令与数学运算的。

$ cat test2
#!/bin/bash

if [ 54 ** 2 -gt 44 ]
then echo -e "hello!"
fi
$ test2
/tmp/test/test2: line 3: [: too many arguments

改成双括号即可。

$ cat test2
#!/bin/bash

if (( 54 ** 2 > 44 ))
then echo -e "hello!"
fi
$ test2
hello!

需要注意,此时的数值运算就不再使用文本比较符号了。另外,双括号中的符号无需转义

双括号命令符号

val++  # 后增
val--
++val # 先增
--val 
! # 逻辑求反
~ # 位求反
** # 幂
<< # 左位移
>>
& # 位布尔和
| # 位布尔或
&& # 逻辑和
|| # 逻辑或

其中需要注意的是:

后增与先增

$ cat test2
#!/bin/bash

var1=1
var2=1
if (( ++var1 > var2++ ))
then echo -e "hello! And $var1, $var2"
fi
$ test2
hello! And 2, 2

虽然二者都执行同样的运算,且总结果来看,二者都有了一致的结果。但从then 被执行可知,if 语句的退出状态码为0,因此表达式成立。
而先增与后增也就在于,先增为在当前语句执行前就进行了增加(if 判断前),而后增则是if 判断完成后再进行增加。因此2>1,也就是条件成立。
另外,在进行先/后 增/减 表达时,需要将变量的$去掉。

$ cat test2
#!/bin/bash

var1=1
var2=1
if (( $++var1 > $var2++ ))
then echo -e "hello! And $var1, $var2"
fi
$ test2
/tmp/test/test2: line 5: ((: $++var1 > 1++ : syntax error: operand expected (error token is "$++var1 > 1++ ")

位运算

位运算在linux 中主要基于内存中二进制数据的运算,位元素的移动也就是某一位上0或1 的移动。

高级字符串表达式

可以使用双方括号,实现高级的字符串表达式。同样地,需要在括号与语句之间,保留空格。(ps:bash支持双方括号,但并非所有的shell 都支持。)

[[ expression/condition ]]

与双括号不同,双方括号内的符号和test 标准字符串比较符号一致。

双方括号的高级之处,就在于它具备了模式匹配,pattern matching,的特性。
在模式匹配下,我们可以使用正则表达式来来匹配字符串值。
[[ $a == r* ]] 表示,判定左边的变量是否符合右边的字符串模式(以r开头的任意字符串),如果符合的话,则退出码为0。

case 命令

如果需要对不同的条件进行匹配,使用if-elif… 则显得有些麻烦。

可以直接用case命令,实现模式匹配。

case $var in
a | b)
    echo 'hello.';;
c)
    echo 'good bye.';;
esac

for 循环

for var in list
do
    commands
done

当list 中的元素包含特殊字符或空格时,可以直接使用 双引号 分割每个元素,或者用转义字符转义。

for 循环也可以从命令中读取列表。

for var in $(cat $file)
do
    echo $var
done

for 循环还可以用于重命名(单引号内的代码运行优先级更高)
image.png

修改字段分隔符

当使用循环遍历文本文件时,有时候遇到下面情况。
如果我们想要遍历每行。却因为空格,拆分了本来一行中的内容。

cat ifs_test.txt
a b
c
d
$ for i in $(cat ifs_test.txt)
> do
>   echo $i
> done
a
b
c
d

这是因为shell中存在特殊的环境变量 IFS ,叫做 内部字符分隔符
默认下shell 的分隔符包括:
空格、制表符、换行符。

因此如果想要解决上面的问题,就需要修改IFS。
IFS=$'\n' ,使shell 只能识别到换行符作为分割点。

C语言风格的for循环

while 循环

image.png
更推荐右侧的语法(左侧也是正确的)。

参数拓展

使用结构化命令 增删改 的操作。
image.png
image.png

掐头

image.png

替换

$ ls
test_1  test_10  test_2  test_3  test_4  test_5  test_6  test_7  test_8  test_9

$ ls test* | while read id; do mv ${id} ${id/_/_file_}; done

$ ls
test_file_1   test_file_2  test_file_4  test_file_6  test_file_8
test_file_10  test_file_3  test_file_5  test_file_7  test_file_9

截取

linux 的索引和python一样,索引都是从0开始。
image.png
image.png

练习

注:把每一题的用到的若干命令都粘贴到对应的代码框中

  1. 软件安装与环境变量:请安装一个二进制软件 bowtie2,包括下载、解压、调用 bowtie2 命令的帮助文档,修改环境变量 PATH

不需要下载这个软件,因为服务器上已经有了,拷贝一份即可 /home/hcguo/tmp/biosoft/bowtie2-2.3.4.3-linux-x86_64.zip

# 提示,添加环境变量需要改一下安装路径
echo 'export PATH="安装路径:$PATH"' >> ~/.bashrc
  1. if 语句与状态参数:在上一题修改好了环境变量之后,回到家目录,再次调用 bowtie2 命令的帮助文档,然后使用 if 语句判断是否调用成功(提示:状态参数变量),如果成功,输出:yes ,否则输出 no
$ if [ $? -eq 0 ]; then echo success; else echo fail; fi
success
  1. if 语句与自定义变量:
  • 创建文件 file1
  • 然后判断当前目录下是否有 file1 这个文件
  • 如果是,输出:yes ;否则什么都不做
~
$ touch file1
~
$ if [ -f file1 ]
> then echo yes
> fi
yes
  1. 创建 10 个文件 test_file_1 ~ test_file_10 ,使用循环语句(建议用 while read id),将上面的 test_file_1 ~ test_file_10 进行重命名,如:test_file_1 改为 test_1
$ ls test* | while read id; do mv ${id} ${id/_file/}; done