bash-syntax

Bash 语法 #

下标数组 #

bash
# 定义数组(空数组)
aa=()
# 或者:
declare -a aa

# 定义数组(赋初值)
aa=(a b c)
# 或者:
declare -a aa=([0]="a" [1]="b" [2]="c")

# 获取下标为0的元素(下标从0开始)
echo "${aa[0]}"

# 获取数组长度
echo "${#aa[@]}"

# 修改元素
aa[0]=x

# 添加3个元素:"d", "e", "f g"
aa+=(d e "f g")

# 保留下标1开始的所有元素(双引号不能省略)
aa=("${aa[@]:1}")
# 例:
aa=(a b c)
echo "${aa[@]:1}"   # 输出:b c
aa=("${aa[@]:1}")
echo "${aa[@]}"   # 输出:b c
declare -p aa   # 输出:declare -a aa=([0]="b" [1]="c")

# 保留下标4开始的2个元素(双引号不能省略)
aa=("${aa[@]:4:2}")

# 删除下标4到6的元素(相当于保留下标0开始的4个元素以及下标7开始的所有元素)
aa=("${aa[@]:0:4}" "${aa[@]:7}")

# 删除下标为1的元素
unset 'aa[1]'
# 例:
aa=(a b c)
unset 'aa[1]'
echo "${aa[@]}"   # 输出:a c
declare -p aa   # 输出:declare -a aa=([0]="a" [2]="c")

关联数组 #

bash
# 定义一个关联数组名为 obj
declare -A obj

# 添加 key(字符串),值为 value(字符串)
obj[key]=value

输入输出重定向 #

bash
# 将命令的错误输出、标准输出分别重定向到 err、out 文件。
COMMAND 2> ./err > ./out
COMMAND 2>> ./err >> ./out   # 追加写入
# 将当前 Shell 进程的错误输出、标准输出分别重定向到 err、out 文件。
exec 2> ./err > ./out

# 将命令的错误输出重定向到文件描述符1(即标准输出)。
COMMAND 2>&1
# 将命令的标准输出重定向到文件描述符2(即错误输出)。
COMMAND >&2
# 将命令的标准输出重定向到文件描述符3。
COMMAND >&3

# 将命令的错误输出、标准输出都重定向到 out 文件。
COMMAND &> ./out   # 相当于 COMMAND >./out 2>&1 。若将 2>&1 放在 >./out 前面,则错误输出被重定向到标准输出而不是最终的 out 文件。
COMMAND &>> ./out   # 追加写入
COMMAND &> /dev/null   # 丢弃错误输出和标准输出
exec &> /dev/null   # 将当前 Shell 进程的错误输出、标准输出重定向到 /dev/null,即接下来的命令输出都将被丢弃。

# 将命令的标准输入重定向到 in 文件。
COMMAND < ./in

# 将代码块的标准输入、标准输出分别重定向到 in、out 文件。
# (in 文件只会被打开一次,而不是每执行一条命令都去打开文件从头读取,out 文件同理)
# (exec <./in >./out  作用域为当前 Shell 进程)
{ COMMANDS; } < ./in > ./out

# 循环读取 in 文件,每次读取一行,读完为止。
while IFS= read -r line; do echo "$line"; done < ./in

命令替换(Command Substitution) #

语法格式:

bash
$(COMMANDS)
# 或者
`COMMANDS`

工作原理:

  1. 先在子 Shell 进程中执行命令 COMMANDS。
  2. 然后将其标准输出内容粘贴回命令行作为主命令的参数。

替换特点:

  • 所有尾随换行符会被删除。
  • 嵌入的换行符不会被删除(但在分词过程中可以被删除,因此是否被双引号包围至关重要)。
  • $(<FILE) 效率高于 $(cat FILE),因为前者不需要创建子进程去运行 cat 命令。

示例:

bash
# 将 ./app.pid 文件中的内容当做 kill 命令的参数。
kill "$(cat ./app.pid)"

# 循环10次。
# 注意:这里的 seq 子命令输出“1”到“10”这些数字(数字之间由一个空格分隔),
# 而 for in 语句需要这十个参数而不是一整串结果作为一个参数,因此命令替换不能用双引号包围。
for i in $(seq 1 10); do echo "$i"; done

# 这里的尾随换行符被删除,但内嵌换行符被保留。
echo -en "$(echo -en '1\n\n2\n\n')" | hexdump -C
# Output:
# 00000000  31 0a 0a 32                                       |1..2|
# 00000004

# 这里的尾随换行符被删除,同时内嵌空白字符由于分词导致被删除。
# 另外,echo 命令在输出第二个参数之前会先输出一个空格,因此最终输出“1空格2”。
echo -en $(echo -en '1\n\n2\n\n') | hexdump -C
# Output:
# 00000000  31 20 32                                          |1 2|
# 00000003

进程替换 (Process Substitution) #

语法格式:

  • 输出形式:<(COMMANDS)
  • 输入形式:>(COMMANDS)

工作原理:

  1. 创建一个特殊的 FIFO(命名管道)或使用 /dev/fd 中的文件描述符。
  2. 在后台启动括号内的命令。
  3. 将命令的输入/输出连接到这个特殊文件。
  4. 将这个文件名作为参数传递给主命令。

示例:

bash
cat <(echo 123)
cat <(cat <(echo 123))   # 可以无限套娃
# Output: 123

# 这里的 echo 命令并没有读取临时pipe文件内容,只是将文件路径打印出来。
echo <(cat ./file)
# Output: /dev/fd/63

# 判断两个目录下文件列表的差异
diff <(ls dir1) <(ls dir2)

# 判断ssh私钥文件和ssh公钥文件是否一对
diff <(ssh-keygen -ef ~/.ssh/id_rsa) <(ssh-keygen -ef ~/.ssh/id_rsa.pub)

# 将子命令的标准输出传递给当前命令的标准输入。
COMMAND < <(SUB_COMMAND)
# 相当于管道命令: SUB_COMMAND | COMMAND ,
# 但是需要注意,如果 COMMAND 会改变当前 Shell 环境(比如 read 命令会向当前 Shell 进程添加/修改变量),
# 则不建议使用管道命令形式,因为管道命令是在子 Shell 中执行的,无法改变当前 Shell 环境。

此处字符串(Here String) #

bash
COMMAND <<< "string"

作用:

  • 将一个字符串直接传递给一个命令的标准输入。

特点:

  • 效率高于管道(如 echo "string" | COMMAND),因为 Here String 直接在当前 Shell 中处理。
  • 自动在末尾添加一个换行符,行为与 echo 一致。若不想要末尾换行符,则建议这种 COMMAND < <(echo -n "string")

此处文档(Here Document) #

语法格式:

bash
COMMAND << delimiter
Hi
This is a document
delimiter

工作原理:

  1. << 是 Here Document 的起始标记。
  2. delimiter 是一个自定义的结束标记(常用 EOFEND)。
  3. 从下一行开始的所有内容都会被作为输入,直到遇到单独的一行 delimiter
  4. 默认支持变量替换和命令替换。

变体形式:

bash
# 忽略前导制表符(Tab)
COMMAND <<- delimiter
	Hi
	This is a document
delimiter

# 禁用变量替换和命令替换(须用引号包围结束标记)
COMMAND << 'delimiter'
$PWD is not replaced
delimiter

判断语句 #

bash
# 判断是否文件存在(与 `[` 或 `test` 一致)
[[ -f file ]]

# 判断数字大小(只支持整数)(与 `[` 或 `test` 一致)
[[ 2 -gt 1 ]]

# 判断字符串是否相等
[[ str1 = str2 ]]
[[ str1 == str2 ]]
[[ str1 != str2 ]]

# 正则判断(注意,正则特殊字符被反斜杠转义或被引号引起来的话会变成普通字符)
[[ content =~ ^[0-9]+$ ]]
[[ content =~ '$'* ]]
[[ content =~ ^\ +$ ]]

# 与、或
[[ 2 -gt 1 && 2 -lt 3 ]]
[[ 2 -gt 1 || 2 -lt 3 ]]

read 和 readarray #

read #

text
Read a line from the standard input and split it into fields.

read [-ers] [-a array] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...]

-a array   将内容分割后保存到名为 array 的数组变量中(下标从 0 开始)(单词分隔符请查阅 IFS 变量,与下面的 -d 选项无关)
-d delim   指定一个行分隔字符(默认是换行符),读取到这个分隔符后退出,分隔符不会被保存到变量中
-n nchars   读取 nchars 个字符,而不是一行(遵守分隔符)
-p prompt   读取内容之前先输出提示语
-r   不转义反斜杠字符
-s   不回显读取内容(仅当使用终端进行输入时)
-t timeout   读取超时秒数
-u fd   指定文件描述符进行读取(默认是标准输入)
Note

若指定了多个变量名称,则这些变量会按顺序接收输入的单词,最后一个变量接收剩余所有内容,单词分隔符由环境变量 IFS 指定。 此外还需要注意,即使只指定了一个变量名称,也会按照单词分割,导致前后空白字符丢失,若要保留空白字符请将 IFS 环境变量设置为空字符串。

示例:

bash
# 将输入的两个单词分配给两个变量(每个单词前后空白字符都会被移除)
read -r arg1 arg2 <<< " apple banana "
# arg1="apple"
# arg2="banana"

# 输入密码(不回显),将内容保存到 password 变量中
IFS= read -rsp "Enter password: " password

# 每次读取一行进行处理(注意,空行也会被读取变成一个空字符串)
while IFS= read -r line; do
    echo "$line"
done < ./file

readarray #

readarray 是 mapfile 指令的别名。

text
Read lines from the standard input into an indexed array variable.

readarray: readarray [-d delim] [-n count] [-O origin] [-s count] [-t] [-u fd] [-C callback] [-c quantum] [array]

-d delim   指定一个分隔字符(默认是换行符)
-t   去掉末尾的分隔符(非分隔符都会被当做正常字符被包含)
-O index   指定起始索引(默认是0)
-s count   跳过前 count 个元素
-n count   读取总数
-C callback   每读取指定个数的元素后调用回调函数
-c quantum   与 -C 选项配合使用,读取元素个数
Note

readarray 命令不会像 read 命令那样移除开头和末尾的空白字符,默认全部保留(包括分隔符),可以添加 -t 选项来去除分隔符。

示例:

bash
# 读取文件,将每一行内容都保存到 lines 数组中
readarray -t lines < ./file

# 每次读取两行(注意,空行也会被读取变成一个空字符串)
while readarray -t -n 2 arr && [ "${#arr[@]}" -gt 0 ]; do
    echo "${arr[0]}: ${arr[1]}"
done < ./file

# 将 dir 目录下的所有文件路径、目录路径保存到 names 数组中
readarray -t -d '' names < <(find ./dir -print0)
# 同样的:
readarray -t names < <(find ./dir)
# 避免这种写法:(因为输入末尾会多一个换行符,会导致数组多出一个空字符串数据)
readarray -t names <<< "$(find ./dir)"
2025年8月16日