# 简介

终端(Terminal)是一个命令行界面,只是一个空壳,用于传递输入到内部的命令解释器(Shell)中,一般来说 Shell 有

  • Bash
  • CMD

Bash 是一类 Linux 上常用的 Shell,主要目的是编写 Linux 的命令脚本。本文默认已经具备足够的 Linux 基础知识,只讲解 Bash 的基本事项

在 Linux 与 MacOS 中,打开终端即可使用 Bash
在 Windows 中,可以利用 WSL 进入 Linux 环境,从而使用 Bash

一般来说,例如 Kali 或者 MacOS 等系统,在进入终端后默认是 Z Shell (Zsh),而不是 Bash
相较于 Bash

  • Zsh 具有自动补全
  • Zsh 具有拼写纠错
  • Zsh 速度较慢

在终端输入 exec bash 可以回退到 Bash 环境(建议回退)

通过命令确认 Bash 可用性

bash --version

通过命令确认当前会话的环境变量

env

可以通过该指令单独查看环境变量,例如

  • 查看默认的 Shell 路径: echo ${SHELL}
  • 查看当前 Bash 版本: echo ${BASH_VERSION}
  • 查看当前 Bash 进程 ID: echo ${BASHPID}
  • 查看当前用户所属组: echo ${GROUPS}
  • 查看主机名: echo ${HOSTNAME}
  • 查看系统类型: echo ${OSTYPE}
  • 查看当前所处目录: echo ${PWD}
  • 生成一个 0 到 32767 之间的随机数: echo ${RANDOM}
  • 查看当前用户的 ID: echo ${UID}

# Bash 脚本

Bash 命令除了直接在终端输入执行,也可以通过编辑器提前编写成脚本,并保存为 .sh 文件
主流编辑器,例如 VSCode 可以自动高亮 Bash 代码

所有的脚本在第一行都固定需要显式写出 Shell 路径,对于一般的 Bash 来说,应该是以下几行之一

  • #!/bin/bash
  • #!/usr/bin/env bash

在指定路径的同时,也可以给 Bash 添加参数,例如

  • -x :在执行脚本时显示每条命令的执行过程,常用于 Debug
  • -n :检查脚本的语法而不执行,常用于 Debug
  • -r :安全模式下运行

总的来说,编写一个 shell.sh 文件,并且在开头写上

#!/bin/bash -r

等价于运行时包含参数

bash -r shell.sh

但是,编写完一个 Bash 脚本后,需要赋予执行权限

chmod u+x shell.sh

执行当前目录下的脚本

./shell.sh

通过在脚本正文中使用 set 命令,也可以实现参数的开关,例如在下例中, set -x 开启 Debug 模式, set +x 关闭 Debug 模式

#!/bin/bash
echo "This is a normal script"
set -x
echo "This is a debug script"
set +x
echo "This is a normal script again"

Bash 的注释方式是 # ,例如

# This is a comment

Bash 中输出指令是 echo ,例如输出文字:

echo "Hello World"
# output: Hello World

在 Bash 的逻辑中,只有结束当前的命令,才会开始下一段命令
至于各个命令是否正确执行,是没有直接确认的。编写 Bash 脚本时,需要考虑这一点

# 变量

Bash 中引用变量的方法是 ${VAR_NAME} ,在上文中已经给过了多个例子
需要注意的是,不同于其他语言,Bash 的变量是没有类型的,甚至没有字符串和数字的区分,所有的变量都是字符串
命名规则不再赘述,和其他语言类似

以下是一个变量赋值的例子

var1="Hello World"

以下是一个引用变量的例子

echo "I want to say ${var1}"
# output: I want to say Hello World

实际上变量不加花括号也可以运行,但是出于安全习惯考虑,建议加上
通过使用圆括号 () ,可以将命令的输出赋值给变量,例如

var2=$(ls)
echo "The current directory contains: ${var2}"
# output: The current directory contains: file1 file2 file3

通过 unset 命令,可以删除一个变量,例如

var1="Hello World"
unset var1
echo "The value of var1 is: ${var1}"
# output: The value of var1 is:

# 数组

Bash 中可以配置一维数组,并从序号 0 开始访问
赋值方法为 array_name=(value1 value2 value3) ,访问方法为 ${array_name[index]} ,如果序号为 * ,则代表访问整个数组,例如

my_array=(apple banana cherry)
echo ${my_array[0]}
# output: apple
echo ${my_array[1]}
# output: banana
echo ${my_array[*]}
# output: apple banana cherry

unset 命令也可以用于删除数组元素,例如

my_array=(apple banana cherry)
unset my_array[1]
echo ${my_array[*]}
# output: apple cherry

可以通过指定序号来直接替换数组元素,例如

my_array=(apple banana cherry)
my_array[1]="grape"
echo ${my_array[*]}
# output: apple grape cherry

# 运算符

以下是 Bash 中常见的运算符示例

  • + :加法
  • - :减法
  • * :乘法
  • / :除法
  • % :取模
  • += :加法赋值
  • -= :减法赋值

通过 let 命令,可以执行算术运算,例如

let result="4 * 5"
echo ${result}
# output: 20

Bash 不是没有变量类型吗,为什么可以计算?: let 会把给出的内容当作算术表达式解释,也就是说 let result="4 * 5" 会被解释为 result=4 * 5 ,从而得到结果 20

实际上更推荐以下方式:通过圆括号可以将内容视为表达式命令,例如 (4 * 5) ,直接在 Bash 中执行也可以得到结果
利用上文中提到的 $() 引用输出方法,可以将表达式赋值写为

result=$(echo $((4 * 5)))
echo ${result}
# output: 20

此外,运算也可以使用 expr ,比起命令,这更应该说是一个外部程序,也不常见
这个程序将后文的内容当作参数传递,并且给出合理的结果,例如

result=$(expr 4 \* 5)
echo ${result}
# output: 20

需要注意的是: * 在 Shell 中是一个特殊字符,代表通配符,所以需要使用 \ 进行转义


Bash 中主要有以下几种控制运算符

  • & : 将命令放在后台执行(不会等待运行结束)
  • && : 连接两个命令,只有前一个命令成功执行(返回值为 0)时才会执行后一个命令
  • () : 将命令视为一个整体执行
  • ; : 将多个命令分开执行,前一个命令执行完后无论成功与否都会执行下一个命令
  • ;; : 用于终止 case 语句
  • | : 将前一个命令的输出作为后一个命令的输入,称为管道
  • || : 连接两个命令,只有前一个命令失败执行(返回值不为 0)时才会执行后一个命令

例如:先尝试创建 test 文件,成功了再创建 test123 文件

touch test && touch test123

lsps 视为一个整体执行,直接返回当前目录下的文件和当前运行的进程。如果不使用括号, ls 会先执行,返回当前目录下的文件,然后 ps 会执行,返回当前运行的进程。中间会被输入截断

(ls; ps)

例如:在命令失败后回报错误信息(虚构的命令)

lzl || echo "Command failed"

流是程序与运行环境之间的交互方式,Bash 中有三种流

  • 标准输入(stdin):默认从键盘输入数据,文件描述符为 0
  • 标准输出(stdout):默认将数据输出到屏幕,文件描述符为 1
  • 标准错误(stderr):默认将错误信息输出到屏幕,文件描述符为 2

通过流,Bash 可以实现输入输出的重定向,使得命令的输入输出可以被重定向到文件或者其他命令中,此时常用下列重定向运算符:

  • > :将标准输出重定向到文件,覆盖原有内容
  • >> :将标准输出重定向到文件,追加内容
  • &>, >& :将标准输出和标准错误重定向到文件,覆盖原有内容.
  • &>> :将标准输出和标准错误重定向到文件,追加内容
  • < :将标准输入重定向到文件,从文件中读取数据
  • << :将标准输入重定向到一个字符串,直到遇到指定的结束标记,该功能被称为 Here Document(Heredoc)
  • | :将一个命令的标准输出重定向到另一个命令的标准输入,称为管道

需要提醒的是,重定向运算符使得 Bash 将输出送到了文件而不是终端中,所以不会显示输出

例如:将 Hello World 存入 output.txt 文件中,再在尾部追加内容

echo "Hello World" > output.txt
cat output.txt
# output: Hello World
echo "Goodbye" >> output.txt
cat output.txt
# output: Hello World
#         Goodbye

例如:将所有的输出和错误信息都重定向到 log.txt 文件中,这位于日志记录中非常常见

ls -l / &> log.txt

此外,通过编号可以实现将输出和错误信息分别重定向到不同的文件中,例如

ls -l / 1> output.txt 2> error.txt

例如:将 output.txt 文件的内容作为标准输入,传递给 cat 命令(这里 cat 根本不知道文件名)

echo "Hello World" > output.txt
cat < output.txt
# output: Hello World

利用分段符号 EOF ,可以将多行文本输入到命令中,例如

cat << EOF
This is a multi-line text input
And it will be passed to the cat command
EOF
# output: This is a multi-line text input
#         And it will be passed to the cat command

例如:将前者的输出传递给后者,实现管道

ls -l / | grep "bin"
# output: drwxr-xr-x  2 root root 4096 Jan 1 00:00 bin

# 位置参数

用户在执行 Bash 脚本时,可以赋予其参数,例如

./script.sh arg1 arg2 arg3

这里 arg1arg2arg3 就是位置参数,在脚本中可以通过 $1$2$3 来访问这些参数,例如

#!/bin/bash
echo "The first argument is: ${1}"
echo "The second argument is: ${2}"
echo "The third argument is: ${3}"
# output: The first argument is: arg1
#         The second argument is: arg2  
#         The third argument is: arg3

通过 $@ 可以访问所有的位置参数,通过 $# 可以访问位置参数的数量,例如

#!/bin/bash
echo "All arguments: ${@}"
echo "Number of arguments: ${#}"
# output: All arguments: arg1 arg2 arg3
#         Number of arguments: 3

此外,

  • $0 代表脚本的名称
  • $* 代表将所有的位置参数作为一个字符串访问
  • $? 代表上一个命令的退出状态码

# 输入提示符

输入提示符指的是在脚本执行过程中要求用户输入数据的情况,可以通过 read 命令来实现
例如:提示用户输入姓名,并将其存储在变量 name

echo "Please enter your name:"
read name
echo "Hello, ${name}!"
# output: Please enter your name:
#         Hello, [user input]!

如果希望输入时不要将 \ 进行转义,可以使用 -r 参数,例如

# 退出状态码

Bash 的指令是否执行成功,可以由退出状态码来判断,退出状态码是一个 0~255 之间的整数

  • 0:表示命令执行成功
  • 非 0:表示命令执行失败,具体的非 0 数值可以代表不同的错误类型,具体数值的含义可以通过查阅相关文档来了解,例如
    • 126:命令无法执行,可能是因为权限不足或者文件不可执行
    • 127:命令未找到,可能是因为命令不存在或者路径错误

可以通过这样的例子来获取退出状态码

#!/bin/bash
ls -l > /dev/null
echo "The exit status of the last command is: $?"
# output: The exit status of the last command is: 0

慎重使用路径 /dev/null ,它是一个特殊的设备文件,代表一个黑洞,任何写入它的数据都会被丢弃,而任何从它读取的数据都会得到 EOF(End of File)。因此,在上面的例子中, ls -l > /dev/null 的作用是将 ls -l 命令的输出重定向到 /dev/null ,从而丢弃输出,只关注命令的执行结果。

活用退出状态码,可以在执行诸多命令前进行检查,例如下载前检查磁盘空间

在脚本中,可以通过 exit 命令来指定退出状态码,例如

#!/bin/bash
echo "This script will exit with status code 1"
exit 1

退出状态码不止是脚本,也是指令的,例如

ps -ef
echo $?
# output: 0