> For the complete documentation index, see [llms.txt](https://book.ncrnalab.org/teaching/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://book.ncrnalab.org/teaching/part-i.-programming-skills/1.linux/1.3.linux-bash.md).

# 1.3.Linux Bash

在介绍bash之前，需要先介绍linux系统的shell。这里的shell(壳)是相对于内核(kernel)来说的。shell可以理解为系统下的一个命令解析器。就linux操作系统的常规使用而言(包括生物信息数据分析)，我们极少需要直接和linux的内核进行交互，绝大部分情况下都是在通过shell和操作系统打交道。

* linux系统上有很多种Shell。首个shell，即Bourne Shell，于1978年在V7(AT\&T的第7版)UNIX上推出。后来，又演变出C shell、bash等不同版本的shell。
* shell可以通过交互式(interactive)的方式使用，即我们在终端中输入一个命令，shell将这个命令解释给操作系统，操作系统执行命令，这是我们前面介绍linux基本操作时的使用方式。
* shell也可以通过非交互式(non-interactive)的方式使用，即我们将需要执行的命令通过一个文本文件提供给shell，shell根据这个文本文件执行相应的命令。这个文本文件就是shell脚本。
* 有时我们对程序的调用会比较复杂，并且会反复使用(例如执行一个生物信息分析的流程)，还可能涉及到循环，条件判断等操作。这时我们不可能每次还一行行的在终端中输入命令，而通常会使用shell脚本。我们本节主要就来学习怎样写一些简单的shell脚本。
* bash，全称为Bourne-Again Shell。它是一个为GNU项目编写的Unix shell。bash是许多Linux系统的默认Shell，这也是我们介绍它主要的原因。

## 1) bash examples

### (1) example 1: run a bash script

* 新建文件bash.sh

```bash
$ touch bash.sh
```

* 为该文件添加可执行权限

```bash
chmod u+x bash.sh
```

* 编辑bash.sh，内容如下：

提示：

用vim编辑脚本:

* `vim bash.sh`
* `i` # 在vim中输入i 即进入insert（写入）模式，即可对bash.sh 文档进行编辑

```bash
  #!/bin/bash
  echo "hello bash" 
  exit 0
```

* `:wq` # 保存并退出

说明：

* `#!/bin/bash` : 脚本解释器的声明语句，它告诉操作系统用`/bin/bash`这个解释器去运行我们的脚本。必须写在文件的第一行。
* `echo "hello bash"` : 表示在终端输出“hello bash”
* `exit 0` : 表示返回0。在linux中，通常来说返回值0表示执行成功，其他表示失败。bash脚本执行成功默认就会返回0，所以这里的 `exit 0`是可以省略的。
* 执行bash脚本

  ```bash
  # method 1: 人为指定解释器
  bash bash.sh

  # method 2: 使用脚本内指定的解释器(脚本第一行的#!/bin/bash已经告诉操作系统使用/bin/bash这个解释器)
  # 这种方法需要脚本文件有可执行的权限，这是我们前面执行命令chmod u+x bash.sh的目的所在
  ./bash.sh
  ```

在终端输出“hello bash”即运行成功

### (2) example 2: define a variable

* 在终端中输入以下命令，创建测试文件a1.txt,b1.txt,...,e1.txt。

```bash
echo -e 'a1\n1' > a1.txt
echo -e 'b1\n2' > b1.txt
echo -e 'c1\n3' > c1.txt
echo -e 'd1\n4' > d1.txt
echo -e 'e1\n5' > e1.txt
```

* 新建一个`read.sh`脚本，复制以下内容，理解每一步的操作:

```bash
#!/bin/bash

# 定义文件名变量
# bash中=是赋值操作符，注意=号两边不能有空格
file1=a1.txt
file2=b1.txt
file3=c1.txt
file4=d1.txt
file5=e1.txt

# 对文件进行操作

## 显示a1.txt第一行。
head -n 1 ${file1}

## 将 b1.txt 的内容输出到终端
# 注意在bash中引用一个已经定义的变量需要用$variable或${variable}，否则bash会将其解释为一个字符串:
cat ${file2}

## 将 c1.txt 的最后的内容输出。
tail ${file3}

## 将 d1.txt 和 e1.txt 合并，然后输出到新的文件中 f1.txt
cat ${file4} ${file5} > f1.txt
```

* 在终端中输入以下命令，执行 `read.sh` 脚本:

```bash
bash read.sh
```

或者

```bash
chmod u+x read.sh
./read.sh
```

{% hint style="info" %}

* 我们以交互式的方式使用终端时，也可以用这里介绍的方式自定义一些变量。我们在实际工作中，一般比较简单的，而且不会重复使用的命令，才会在终端中直接运行，所以在交互式的环境中很少会自定义变量。不过在学习的过程中，大家可以通过这种方式快速熟悉bash中自定义变量的使用方法。
* bash提供了不少内建变量，例如$PWD是当前目录，$HOME是家目录等等，在自定义变量时，最好避免和这些变量重名。
* 引用自定义变量时，$variable和${variable}在我们上面的例子中含义是一样的。${...}在bash中被称为参数名扩展(Shell Parameter Expansion)，它还支持一些更复杂的操作，如对字符串去除前缀/后缀，取子串;对列表取特定元素等等，有兴趣的同学请参考GNU的文档<https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html>。
  {% endhint %}

## 2) if...else...

**基本格式**

* 单个条件判断

```bash
if condition
then 
... 
fi
```

* 也可以写成:

```bash
if condition;then 
... 
fi
```

* 对不符合条件的情况执行另一种操作:

```bash
if condition;then 
...
else
... 
fi
```

* 多个条件判断:

```bash
if condition1;then 
... 
elif condition2;then
...
else
...
fi
```

* 以下列举了一些常见的条件判断语句，条件判断一般写在方括号内(参见下面的示例)

| 表达形式                | 说明                 |
| ------------------- | ------------------ |
| -d file             | 判断是否为目录            |
| -f file             | 判断是否为文件            |
| -w file             | 判断是否有写的权限          |
| -x file             | 判断是否有执行的权限         |
| number1 -eq number2 | number1 等于 number2 |
| number1 -gt number2 | number1 大于 number2 |
| number1 -lt number2 | number1 小于 number2 |

* **示例：** 判断文件test.sh是否存在,存在则输出“file exist”；没有则输出“file not exist”。

```bash
#!/bin/bash

if [ -f test.txt ];then
echo "file exist"
else
echo "file not exist"
fi

exit 0 # 可以省略
```

* **示例：** 提示用户输入值。若输入的值小于0，则输出“negtive number”；若等于0,则输出“number zero”，否则，输出“positive number”。

```bash
#!/bin/bash

# 提示用户输入一个值
echo -n "please input a number:"

# 保存用户输入的值到num中
read num

if [ "$num" -lt "0" ];then
# 小于0,则输出“negtive number”
echo "negtive number"
elif [ "$num" -gt "0" ];then
# 大于0,则输出“positive number”
echo "positive number"
else
# 大于0,则输出"number zero"
echo "number zero"
fi

exit 0
```

## 3) for loop

**基本格式**

```bash
for variable in aa bb cc dd  
do  
echo $variable
done
# 这里bash会用空格作为分隔符，把字符串解释成"aa bb cc dd"解释成一个列表,包含"aa","bb","cc","dd"四个变量
# variable会依次取这四个值，执行代码块中的命令
```

* **示例1:** 输入当前文件夹的一级子目录中文件的名字

```bash
#!/bin/bash

# 将ls的结果保存到变量CUR_DIR中
CUR_DIR=`ls`

# 显示ls的结果
echo $CUR_DIR

for val in $CUR_DIR
do
# 若val是文件，则输出该文件名
if [ -f $val ];then
echo "FILE: $val"
fi
done

exit 0
```

* **示例2:** shuffle一个序列

```bash
#!/bin/bash

# shuffle a sequence
# input sequence
s="CGAUGCUAGCUAGCUAGUC"

# start index
si=0
L=${#s} # length of the sequence
# end index
ei=$(($L-1))
shuffled=""

for i in `seq $si $ei | shuf `;do
  # seq generate integers range from $si to $ei (include both $si and $ei)
  # shuf command randomize the order of these integers
  shuffled=$shuffled${s:$i:1}
  # ${s:$i:1}: variable expansion, take substring start from $i with length 1, this operator in bash use 0 based coordinate
  # $shuffled${s:$i:1}: concatenate string $shuffled and string ${s:$i:1}
done
echo "original sequence: $s"
echo "shuffled sequence: $shuffled"
```

{% hint style="info" %}

* 两个"\`"操作符之间的代码在程序执行的过程中会被替换成这段代码执行的结果，这被称为"Command-Substitution"，除此之外，`$(...)`操作符是实现Command-Substitution的另一种常用方式，大家可以尝试
* `${#s}`这里我们用到了前边提到的shell variable expansion，`${#variable}`可以返回一个字符串的长度
* 在取一个字符串某个位置的字符时，我们用`${s:$i:1}`来获得一个字符串位置`$i`的字符，这也是一个shell variable expansion
* `$((...))`执行算数操作，并将结果作为变量返回，可以用它来将计算结果赋给一个新的变量
* 在bash脚本中，连接两个字符串不需要单独的操作符，直接将两个变量写在一起即可，例如连接`$s1`和`$s2`用`s=$s1$s2`就可以实现
* 我们提供的这个shuffle序列的例子只是供大家学习bash脚本之用，在实际工作中用python等可以有更方便的实现，也有现成的工具实现了这样的功能，如meme提供的<https://meme-suite.org/meme/doc/fasta-shuffle-letters.html>
  {% endhint %}

## 4) break and continue

**基本格式**

> break命令允许跳出循环。\
> continue命令类似于 break命令,只有一点重要差别,它不会跳出循环,只是跳过这个循环步。

* **示例：**

  从0开始逐步递增，当数值等于5时停止。bash脚本内容如下：

```bash
#!/bin/bash

# 设置起始值为0
val=0

while true
do
if [ "$val" -eq "5" ];then
# 如果val=5，则跳出循环
break;
else
# 输出数值
echo "val=$val"
# 将数值加1
((val++))
fi
done

exit 0
```

{% hint style="info" %}
在bash脚本中，双括号`(())`和`$(())`一样用来定义算术操作，区别在于它不会将结果返回，而是直接对变量值进行修改。例如`((val++))`会将变量`$val`的数值加1。其他语言里常见的写法，如`((val+=1))`或`((val=val+1))`也都是可以的
{% endhint %}

* **示例：**

  从0开始逐步递增到10：当数值为5时，将数值递增2；否则，输出数值。

```bash
#!/bin/bash

# 设置起始值为0
val=0

while [ "$val" -le "10" ]
do
if [ "$val" -eq "5" ];then
# 如果val=5，则将数值加2
((val=$val+2))
continue;
else
# 输出数值
echo "val=$val"
# 将数值加1
((val++))
fi
done

exit 0
```

## 5) More Reading

* 《[鸟哥的Linux私房菜-基础学习篇](https://www.ctolib.com/docs/sfile/vbird-linux-basic-4e)》 (11-13章推荐章节)

> **Linux 推荐章节：**
>
> * 第11章 认识与学习bash; 第12章 正则表达式与文件格式化处理 第13章 学习shell script

## **6) Homework**

1. 参考和学习本章内容，写出一个 bash 脚本，可以使它自动读取一个文件夹（例如 bash\_homework/）的内容，将该文件夹下文件的名字输出到 filenames.txt, 子文件夹的名字输出到 dirname.txt 。

> 提示和要求：下载bash\_homework.zip并解压（可以从链接 [Files Needed](/teaching/appendix/appendix-iv.-teaching.md#teaching-files) 的 `Files` 路径下的相应文件夹中下载）。将 bash 脚本，filename.txt, dirname.txt 写到同一个文件中上交。文件格式： md（推荐），word, pdf, txt.

## **7) References**

> * <https://www.cnblogs.com/skywang12345/archive/2013/05/30/3106570.html>
