---
aliases: []
tags:
- shell
- linux
- list
- sh
- bash
- zsh
created: 2023-08-18 19:44:52
modified: 2025-11-28 10:33:19
---
# Shell 笔记
---
## 目录
---
## Shell 种类
### Bourne shell
> Bourne shell 由 Steve Bourne 在 AT&T 贝尔实验室开发,被认为是第一个 UNIX shell。它被表示为 sh。 #sh
### Bash
> GNU Bourne-Again Shell (bash) 更多被称为 Bash shell,它被设计成与 Bourne shell 兼容。Bash shell 融合了 Linux 中不同类型 shell 的有用功能,如 Korn shell 和 C shell。 #bash
### zsh
> Z Shell 或 zsh 是 sh shell 的扩展,在自定义方面做了大量改进。如果你想要一个具有更多功能的现代 shell,zsh shell 就是你要找的。 #zsh
### 相关资料
* [Linux 中有哪些不同类型的 Shell? - 知乎](https://zhuanlan.zhihu.com/p/614551538)
---
## Shell 模式
Linux 下常见有:`.bashrc`、`.profile`、`.bash_profile` 等配置文件。
这些配置文件的区别,首先得从 shell 的 4 种使用模式讲起。
#### 交互与非交互
##### 交互模式
顾名思义就是 shell 与用户存在交互行为。 #shell/mode
##### 非交互模式
与 [交互模式](#交互模式) 的情况就正好相反。 #shell/mode
> [!note]
>
> [交互模式](#交互模式) 与 [非交互模式](#非交互模式) 区别的是 bash/zsh 用于接受用户命令,还是执行运行一段脚本。
#### 登录与非登录
使用 `echo $0` 可以检查是否是登录 shell:
* 如果显示结果是 shell 名称前有一个「**连字符**」`-`,即登录 shell。
* 如果没有「**连字符**」,即非登录 shell。
要登录的 shell,无论是否交互,最后都得加载 `.profile` 文件。
而如果是
示例:
使用 ssh 连接本机:
```shell
$ ssh silascript@192.168.0.20
# silascript @ (base) in ~ [4:57:15]
$ echo $-
569XZhilms
# silascript @ (base) in ~ [4:57:25]
$ echo $0
-zsh
```
> [!note]
>
> 可以看到 `-zsh`,这是有连字符 `-` 的,说明此时的 shell 是一个 [登录shell](#登录与非登录)。
>
> 另外,`$-` 的结果是 `569XZhilms`,其中存在 `i`,证明这个 shell 还是个 [交互shell](#交互与非交互)。
>
>> [!tip]
>> 而如果只是直接通过本地的各种终端直接输入 `echo $0`,一般都是直接显示 shell 的路径,如 `/bin/zsh` 这样的。`echo $-` 结果与通过 ssh 方式连接结果一致。由此可知,在图形界面的 Linux 下,打开一个终端窗口,这种方式的 shell 都是一个非登录式的 shell。
#### shell 类型总结
* 登录式 shell:
* 正常通过某终端登录的 shell。
* su - username。
* su -l username。
* 非登录式 shell:
* su username。
* **图形终端**下打开的命令窗口。
* 自动执行的 shell 脚本。
Sell 语法相关的内容请查看: [Shell 笔记](Shell_Note.md)
---
## Shell 工具
### Bash-It
* [bash-it](https://github.com/Bash-it/bash-it.git) 是 [Bash](#Bash) 的配置框架,可以认为是 bash 版本的 [oh-my-zsh](Zsh_Note.md#zsh_plugins_mgs_ohmyzsh)。
---
## Shell 命令种类
[Linux](../Linux_Note.md)Shell 可执行的命令有 3 种:
* 内建命令
* Shell[函数](#函数)
* 外部命令
### 内建命令
这些命令集成在 Shell 解释器中,一种是改变 Shell 本身的属性设置;另一种是 I/O 命令,如 `echo` 命令。
使用 `type` 命令可以判断命令是内建命令还是 [外部命令](#外部命令)。
```shell
$ type cd
cd is a shell builtin
```
内建命令会显示「builtin」字样。
### 外部命令
是独立于 Shell 的可执行程序,如 [find](../Linux_Note.md#find)、[grep](../Linux_Note.md#linux_textprocessing_grep) 等。
对于外部命令,Shell 会创建一个新的进程来执行命令。
外部命令执行过程:
1. 调用 POSIX 系统的 `fork` 函数接口,创建珍上命令行 Shell 进程的复制(子进程)
2. 在子进程中,寻找外部命令
3. 在子进程中,执行寻找到的外部命令,此时父进程牌休眠,等待子进程执行完毕
4. 子进程执行完毕,父进程接着执行下一条命令
> [!tip]
>
> 使用 `source` 执行 Shell 脚本时,不会创建子进程,而是在父进程中直接执行。所以要修改当前 Shell 本身环境变量,是使用 `source` 命令。
---
## 变量
命名规则:
* 首个字符必须为字母
* 中间不能有 [空格](#空格),可以使用下划线 `_`
* 不能使用标点符号
* 不能使用 [Bash](#Bash) 等 Shell 里的关键字
### 空格
shell 对于空格有严格的规定:
* **赋值**语句等号两边**绝对不能有**空格。
* [字符串比较](#字符串比较) 等号两边**必须有**空格。
* 所赋的值包含空格,可以用引号括起来。
* `if` 语句的 `[]` 中,表达式前后都应有空格。而 `if` 语句中那个 `;` 与 `]` 间不能有空格。
> [!example] 小示例
> ```shell
> if [ "${s1}" = "hello" ];then
> ```
> [!info] 相关资料
>
> * [变量定义规则、shell 格式、空格注意事项汇总](https://blog.csdn.net/m0_45406092/article/details/129047592)
### 引号
数字可带可不带引号,但不带引号的数字可计算,带引号的数字不能用于计算。
> [!info] 相关资料
>
> * [单引号、双引号、不加引号和反引号用法和区别详解](https://blog.csdn.net/m0_45406092/article/details/129056037)
* 被双引号(`" "`)括起来的变量替换是不会被阻止的, 所以双引号被称为「部分引用」,或「弱引用」。
* 被单引号(`' '`)括号起来的变量替换会被禁止,变量名只会被解释成字面的意思,不会发生变量替换,所以单引号被称为「全引用」,或「强引用」。
### 局部变量
默认在 shell 脚本中定义的变量都是「全局」(global)的。
使用 `local` 关键字修饰的变量,称为「局部变量」,其作用域只在 [函数](#函数) 范围。
如果出现同名,shell 函数中定义的 local 变量会屏蔽掉脚本定义的全局变量。
### 特殊变量
* `$0`:当前脚本的文件名
* `$n`:传递给脚本或函数的参数。`n` 是数字,表示第几个参数,从**1**开始。
* `$#`:传递给脚本或函数的参数个数。
* `$*`:传递给脚本或函数的所有参数,即以一个单字符显示所有向脚本传递的参数。
* `$?`:上个命令的退出状态,或函数的返回值。`0` 表示没有错误,其他值表明有错误。
* `$$`:当前 Shell 进程 ID。
* `$!`:后台运行的最后一个进程 ID 号
* `$@`:与 `$*` 相同,但是使用时加引号,并在引号中返回每个参数。
* `$-`:显示 Shell 使用的当前选项,与 `set` 命令功能相同。
### 变量使用
变量使用时,在变量名前加上 `$`。
示例:
```
local num1=5
echo $num1
```
> [!tip]
>
> `$变量名` 这种形式实质是 `${变量名}` 的「简写形式」。在某些情况还 `$变量名` 这种形式可能引起错误,这时就需要用到 `${变量名}` 这种标准形式。
> [!info]
>
> Shell 的变量只有使用或引用时,才需要加 `$`,声明、定义或赋值时,是不需要加 `$`,这与 [PHP的变量定义规则](../../PHP/PHP_Base_Note.md#变量定义) 不同。
>
> 在没有 `$` 时的变量,有以下几种情况:
>
> * 变量被声明或赋值,如上面的示例 `local num1=5`
> * 变量被 `unset`
> * 变量被 `export`
---
## 表达式
### 条件表达式
语法:`[ expression ]`
**括号中的表达式前后都有空格**。
> [!tip] shell 的空格
>
> 因为 Sehll 是命令加选项或参数而构成的,而命令与选项或参数间是以空格间隔的,所以在 Shell 中空格是有点「微妙」的存在。
>
> 在条件表达式中,括号中的表达式前后必须都加上空格,不然就会报错。而赋值表达式中,`=` 前后就不能加空格。
```shell
if [[ xxx ]];then
xxx
elif [[ xxx ]];then
xxxx
else
xxxx
fi
```
> [!tip]
>
> Shell 中是 `elif`,不是 else if
#### 整数比较
* `-eq` 或 `equal`:等于
* `-ne` 或 `not equal`:不等于
* `-gt` 或 `greate than`:大于
* `-lt` 或 `lesser than`:小于
* `-ge` 或 `greate or equal`:大于等于
* `-le` 或 `lesser or equal`:小于等于
#### 字符串比较
* `==`:等于。`[ "a" == "a" ]`
* `!=`:不等于。`[ "a" != "a"]`
* `-n`:字符长度不等于 0 为真。`[ -n "xxx" ]`
* `-z`:字符串长度等于 0 为真。`[ -z "xxx" ]`
> [!tip]
>
> 使用 `-n` 判断字符串长度时,变量加双引号。不但 `-n`,只要是字符串比较,都加双引号,这是一个好习惯。
#### 文件测试
* `-e`:文件或目录存在为真。`[ -e path ]`
* `-f`:文件存在为真。
* `-d`:目录存在为真。
> [!info] 示例
>
> ```shell
> if [[ ! -d "/data/" ]];then
> mkdir /data
> fi
> ```
* `-r`:有读权限为真。
* `-w`:有写权限为真。
* `-x`:有执行权限为真。
#### 布尔运算
* `!`:取反。`[ ! $a -eq $b ]`
* `-a`:和,类似于其他语言中的 `&` 运算符。`[ 1 -eq 1 -a 2 -eq 2]`
* `-o`:或,类似于其他语言中的 `|` 运算符。`[ 1 -eq 1 -o 2 -eq 2]`
#### 逻辑判断
* `&&`:断路与,与其他语言一样,前后俩表达式都为 true,其最终结果才为 true;如果前面的表达式为 false,那不用判断后面的表达式,最终结果即为 false。
* `||`:断路或,与其他语言一样,前后俩表达式至少有一为 true,结果才为 true;如果前面表达式为 true,则不用判断后面的表达式。
> [!tip]
>
> 如果当前 Shell 不支持逻辑判断符,可使用 [布尔运算符](#布尔运算) 替代。
---
## 路径
```shell
# 显示当前路径
echo $PWD
```
> [!tip]
>
> `$PWD`,跟 echo 命令一起使用时,必须大写。
### 文件
#### basename
使用 `basename` 命令可以得到一个包含后缀名的文件名。
示例:
```shell
$ basename ~/MyNotes/ITNotes/常用字体.txt
常用字体.txt
```
`-s`:不显示指定的后缀名。
示例:
```shell
$ basename -s .txt ~/MyNotes/ITNotes/常用字体.txt
常用字体
```
当然 `-s` 也可以省略,而指定的不显示的后缀名作为第二个参数:
```shell
$ basename ~/MyNotes/ITNotes/常用字体.txt .txt
常用字体
```
甚至这个指定的后缀名不一定是后缀名,而是指定的任务结尾的字符,它本质只是**在结果的末尾去掉匹配的字符串**:
```shell
$ basename ~/MyNotes/ITNotes/常用字体.txt t
常用字体.tx
```
#### dirname
使用 `dirname`,可以得到文件所在的目录路径字符串。
示例:
```shell
$ dirname ~/MyNotes/ITNotes/常用字体.txt
/home/silascript/MyNotes/ITNotes
```
---
## 列表
列表实际就是有空格的字符串。
构建一个列表:
```shell
list1=("aa" "bb" 5 "cc")
```
获取列表元素,从 `0` 开始:
```shell
echo ${list1[0]}
```
获取所有列表元素:
```shell
echo ${list1[@]}
```
遍历列表:
```shell
for s_temp in "${list1[@]}"; do
echo $s_temp
done
```
获取列表长度:
```shell
echo ${#list1[@]}
```
---
## 数组
### 数组初始化
```shell
数组名=()
```
### 数组元素
#### 数组所有元素
* `*`:全部元素,是作为**一个整体**处理
* `@`:同上,但会强制单词分隔
> [!tip] for in 中使用
>
> `*` 与 `@` 区别在 `for in` 循环中会体现出区别:
>
>
>> ```shell
>> arr1=("hello world" "the man")
>>
>> for i in "${arr1[*]}";do
>> echo $i
>> done
>>``` -
>
> 这段代码会输出 `hello world then man`
>
>> ```shell
>> for i in "${arr1[@]}";do
>> echo $i
>> done
>>```
>
> 这段代码会输出:
>
>> ```shell
>> hello world
>> the man
>>
>>```
>
>
取最后一个元素:`arr[-1]`。
> [!tip] 老语法
>
> `${#arr[@]-1}`:`${#arr[@]}` 这是取数组长度(这跟 [列表](#列表) 是一样的。),那数组长度 -1 就得到数组最后一个元素的索引值。
`${!arr[*]}` 或 `${!arr[@]}` 是查看已赋值元素的下标。
> [!info]
>
> `@` 跟 `*` 的区别:
>
> * 变量使用 `*` 时,变量被 `""` 包裹,会当成一串字符串处理。
> * 变量使用 `@` 时,变量被 `""` 包裹,依然当做数组处理。
> * 变量在没有被 `""` 包裹的情况下,`@` 跟 `*` 是等效的.
### 示例
#### 1. 读取文件并将数据存放到数组中
```shell
# 地址数组
addrs_arr=()
# 读取文件
# 去除空行及以 # 起头的行
for line in `cat $addrsls_file_path | grep -v ^$ | grep -v ^\#`
do
# 将数据存放到数组
addrs_arr+=("$line")
done
```
> [!info]
>
> `grep -v` 表示反向选择。
>
> `^$` 与 `^\#` 都是 [正则表达式](Linux_Note.md#正则表达式) 的东西。`^$` 表示空行,`^\#` 表示以 `#` 符号开始的行。
>
>`grep -v ^$ | grep -v ^\#` 表示就是选项非空行及非使用 `#`「标记」的行。
#### 2. 遍历数组
方式 1:
```shell
# 遍历数组
for arr_temp in ${addrs_arr[*]}
do
echo $arr_temp
done
# 或者写成这样
for arr_temp in ${addrs_arr[@]}
do
echo $arr_temp
done
```
> [!tip] 遍历说明
>
> `arr_temp` 是一个「临时变量」,用来存储每次循环从数组中取出的数据。
>
> 这示例中的 `for` 循环类似其他高级编程语言中的 `foreach` 语句的用法。
方式 2:
```shell
for i in ${!json_data_arr[@]}
do
echo ${json_data_arr[$i]}
done
```
> [!tip]
>
> 使用 `!` 这种方式,是使用数组索引来遍历。
>
> `i` 是数组索引
#### 3. 添加元素
向数组中添加元素,可以有四种方法:
1. 按照下标进行单个添加
```shell
array_name[index]=value
```
2. 在不做任何删减时,直接使用数组长度追加元素
```shell
array_name[${#array_name[@]}]=value
```
3. 直接获取源数组的全部元素再加上新要添加的元素,一并重新赋予该数组,重新刷新定义索引
```shell
array_name=("${#array_name[@]}" value1 value2 ... valueN)
```
> [!info]
>
> 双引号不能省略,否则数组中存在包含空格的元素时会按空格将元素拆分成多个。
>
> 不能将 `@` 替换为 `*`,如果替换为 `*`,不加双引号时与 `@` 的表现一致,加双引号时,会将数组 array_name 中的所有元素作为一个元素添加到数组中。这规则在 [字符串](#字符串) 中也同样适合。
4. 使用 `+=` 直接添加,待添加元素必须用 `()` 包围起来,并且多个元素用空格分隔
```shell
array_name+=(value1 value2 ... valueN)
```
> [!important]
>
> 待添加元素必须用 `()` 包围起来,并且多个元素用空格分隔
>
>
> `()` 对于数组而言,是**构建数组的关键**。如获取一相函数返回的数组,如果没有 `()`,将被 Shell 当成字符串处理,这时使用 `${#array[@]}` 来获取数组长度将只会是 `1`,因为虽然函数使用 `echo ${array[@]}` 返回了一个数组,但接收方会将这货当成一个字符串处理,因为 Shell 中实际都是字符,因为 Shell 或 [Linux](Linux_Note.md) 大部分工具,实质是都是针对处理文本而生的,所以字符或字符串才是其本质。
>
> ```shell
> function test1(){
> local array1=(2 5 22 32 18)
> echo ${array1[@]}
> }
> # 获取返回值次构建为数组
> r_arr=($(test1))
>
> echo ${#r_arr[@]}
> ```
>
#### 4. 删除数组元素
语法:`unset 数组[引索]`
#### 5. 函数中的数组使用
##### 将一个数组作为参数向函数传递
```shell
fun1 ${ads_arr[*]}
```
> [!tip] 数组实参
>
> 在调用一个函数时,向此函数传入一个「实参」时,如果是普通变量只需要 `$变量名`,就可以。
>
> 但如果要向函数传个数组,那语法就得「变下」,得写成 `${数组名[*]}` 或 `${数组名[@]}`。如果数组变量按普通变量写法传参,那只会传入数组第一个元素,即 `${数组名[0]}`。
>
> 其实这是符合数组获取 [数组所有元素](#数组所有元素) 的语法规则的。简单说,shell 语法与其他高级编程语言有点不一样,传参得「实打实」地传入数组的「所有元素」。
##### 函数内接收外部传入的数组
```shell
# 接收外部传来的数组
local ads_array=($@)
```
> [!tip] 语法解释
>
> 函数中接收传入的数组,其语法也与接收普通变量不一样,函数内接收普通变量参数可以这样:`temp=$数字`,通过数字来指定接收哪个一参数。
>
> 而因为数组不是一个数据,而是「一堆」数据,所以得使用特殊一点语法接收,其中 `@` 跟 [数组所有元素](#数组所有元素) 的语法保持一致,另外,那对小括号 `()`,其实是就是构建一个数组的语法。即 `ads_array=()` 这个是一个 [数组初始化](#数组初始化) 语法。
>
> 也就是说,函数内接收数组,其实是初始化了一个数组用来接收。
>
###### 特殊案例
需求:第 2 个参数是一个数组,函数如何接收:
方案一:先接收第一个参数,然后使用 `shift` 命令用于实现实现位置参数左移,
语法格式:`shift [n]`
> [!info]
>
> 说明:`shift` 命令用来删除参数。`shift` 命令参数默认为 **1**,表示 从命令行删除第一个参数。当指定了参数 **n** 时,`shift` 命令就一次删除 **n** 个参数。
```shell
# 定义函数
my_function() {
local first_arg="$1"
# 移除第一个参数
shift
# 剩余的参数就是数组元素
local array=("$@")
echo "第一个参数: $first_arg"
echo "数组元素:"
for element in "${array[@]}"; do
echo " $element"
done
}
# 调用函数
my_function "hello" "apple" "banana" "cherry"
```
方案二:使用 [字符串](#shell_string) 来接收,然后然 [字符串](#shell_string) 转成数组。
```shell
# 定义函数
my_function() {
local first_arg="$1"
local array_string="$2"
# 将字符串转换回数组
IFS=',' read -ra array <<< "$array_string"
echo "第一个参数: $first_arg"
echo "数组元素:"
for element in "${array[@]}"; do
echo " $element"
done
}
# 准备数组
my_array=("apple" "banana" "cherry")
# 将数组转换为字符串(使用逗号分隔)
array_string=$(IFS=','; echo "${my_array[*]}")
# 调用函数
my_function "hello" "$array_string"
```
方案三:使用 [Bash](#Bash)4.3+ 的新特性:**名称引用**
```shell
# 定义函数
my_function() {
local first_arg="$1"
local -n array_ref="$2" # 使用 nameref
echo "第一个参数: $first_arg"
echo "数组元素:"
for element in "${array_ref[@]}"; do
echo " $element"
done
}
# 准备数组
my_array=("apple" "banana" "cherry")
# 调用函数,传递数组名称
my_function "hello" my_array
```
> [!info]
>
> `local -n` 是 Bash 4.3 及以上版本引入的 **nameref(名称引用)** 功能,它允许你创建一个对另一个变量的引用。
#### 6. 判断数组是否为空
##### 方式 1
通过 `${#数组[@]}` 语法获取数组元素的个数来判断:
```shell
arr1=()
if [ ${#arr1[@]} -eq 0 ];then
echo "数组为空"
else
echo "数组不为空"
fi
```
##### 方式 2
通过 `${数组[@]}` 语法获取数组所有元素,如果返回一个空值,则表明此数组为空:
```shell
arr1=()
isEmpty=true
for element in "${arr1[@]}"; do
isEmpty=false
break
done
if $isEmpty; then
echo "Array is empty"
fi
```
> [!info]
>
> 最「笨」的方式就是遍历数组,设个结果变量,判断每一个元素。
---
## 运算
原生 Shell 不支持数学运算,可以通过 [awk](Linux_Note.md#linux_textprocessing_awk)、`expr` 命令来实现。
```shell
num1=`expr 1+2`
echo num1
```
当然还有更便捷的方式,使用 `(())` 来进行整数运算。
---
## 字符串
### 切割
#### 方式 1
语法:`${parameter//pattern/string}`
用 string 来**替换** parameter 变量中所有匹配的 pattern
```shell
s1="hello,shell,split,test"
array=(${s1//,/ })
```
> [!info]
>
> `s1` 字符串原有使用 `,` 来分隔,而经过 `${s1//,/ }` 后,就是将 `,` 替换成空格,利用 [数组](#数组) 构建时默认按空格分隔的规则,这些子串就被替换成使用空格来分隔,那最后构建成数组时,子串自动变成数组的各个元素。
>> [!important]
>>
>> 注意最后的是 `/ ` 斜杠后是有一个空格的,如果少了,就相当于将原有的分隔符 `,` 给「移除」了,那些原来用 `,` 分隔的子串,就会合并成一起了!-- 当然如果有这种将子串合并成一个字符串的需求,可以使用这种方式来对字符串进行合并。
>
### 截取
#### 示例 1
```shell
# # 是从左向右,一个#是取第一个,##是取最后一个
# % 是从右向左 一个%是取第一个,%%是取最后一个
##
s1="https://github.com/rexdf/ChineseLocalization.git"
# 从左向右取第一个.后的字符串
# 即取到的是 com/rexdf/ChineseLocalization.git
echo "${s1#*.}"
# 从左向右取最后一个.后的字符串
# 即取到的是 git
echo "${s1##*.}"
# 从右向左取.后的第一个字符串
# 即取到的是 https://github.com/rexdf/ChineseLocalization
echo "${s1%.*}"
# 从右向左取最后一个.后的字符串
# 取到的是 http://github
echo "${s1%%.*}"
```
> [!tip]
>
> 快速记忆是从左还是从右,可以这么记:键盘上 `#` 在左,而 `%` 在右边,所以 `#` 是从左向右,`%` 是从右向左。
>
> `*` 的部分是「忽略」的部分。`#` 都是忽略左边,取右边;`%` 是忽略右边,取左边,所以 `*` 号决定使用 `#` 还是 `%`。
#### 示例 2
```shell
# core_address是 denolehov/obsidian-git 这个样子。获取 / 左右两段字符串
# 取前段 使用从右向左取,取 / 最后一段
local account=${core_address%%/*}
# 取后牌戏 使用从左向右取,同样取 / 最后一段
local p_name=${core_address##*/}
```
> [!tip]
>
> 从左往右,`*` 与 `#` 在一起,而且 `*` 总在左边
>
> 从右往左,`*` 与 `#` 总被分隔符分离,`*` 总在右边。
### 遍历
字符串是可以遍历的。
#### 示例 1
```shell
str2="hello,world"
for s_temp in $str2; do
echo $s_temp
done
```
> [!info]
>
> 示例 1 中最终输出就是 `hello,world`,就是把字符串中每一个字符「遍历」一遍。
#### 示例 2
```shell
str1="silas tom jack lucy mary"
for s_tem in $str1; do
echo $s_tem
done
```
> [!info]
>
> 示例 2 中的字符串,是带空格的;所以遍历的结果是按空格分割,将分割后每段子串依次输出。
>
> 这种以空格为分隔符的字符串,也可以这样遍历:
>
>> ```shell
>> for s_tem in ${str1[@]}; do
>> echo $s_tem
>> done
>> ```
>
> 其实这是一种 [数组样式](#2.%20遍历数组) 的遍历。
### 字符串转 [数组](#数组)
#### 按行分隔
默认情况,是按空格分隔,可以通过修改 `IFS` 的值来改变分隔符。
```shell
IFS=$'\n' # 设置内部字段分隔符为换行符
array=($string)
unset IFS # 恢复IFS为默认值
```
---
## 条件
```shell
if [ command ];then
符合该条件执行的语句
elif [ command ];then
符合该条件执行的语句
else
符合该条件执行的语句
fi
```
---
## 循环
#### 示例
##### 循环读取文件
`while` 与 `for` 读文件是有区别的:
1. `while` 是逐行读取,读完一行跳转到下一行
2. `for` 是按字符串方式读取,遇到空格后,再读取的数据就会换行显示
`while` 相对于 `for` 的读取能更好的还原数据原始性。
###### for 实现
```shell
for 变量名 in 循环列表
do
命令集
done
```
> [!info]
>
> 这种 `for` 循环语句语法中,`for` 关键字后面会有一个「变量名」,变量名依次获取 `in` 关键字后面的变量取值列表内容(以空格分隔),每次仅取一个,然后进入循环(`do` 和 `done` 之间的部分)执行循环内的所有指令,当执行到 `done` 时结束本次循环。
>
> 之后,「变量名」再继续获取变量列表里的下一个变量值,继续执行循环内的所有指令,当执行到 `done` 时结束返回,以此类推,直到取完变量列表里的最后一个值并进入循环执行到 `done` 结束为止。
```shell
for line in `cat xxx.txt`
do
echo $line
done
```
###### while 实现
写法 1:
```shell
cat xxx.txt | while read line
do
echo $line
done
```
写法 2:
```shell
while read line$*用法
do
echo $line
done < xxx.txt
```
##### 小示例
这示例使用到了 [jq](#jq) 这个 json 小工具,对 json 文件进行解析,并将解析后的结果数据输出存放到一个临时文件中。
然后对这个临时文件进行读取,在读取时,还对读到的数据进行一定需求的过滤。
```shell
function get_dl_url(){
# json文件
local json_path=$1
# 使用 jq 获取各文件下载地址并输出到临时文件中
jq -r '.assets[] | .browser_download_url' $json_path > temp.txt
# 过滤掉 main.js manifest.json styles.css 三个文件之外所有文件
for line in `cat temp.txt`
do
# 从文件地址中获取文件名
local f_name=${line##*/}
# 过滤文件
if [[ $f_name == "main.js" ]] || [[ $f_name == "manifest.json" ]] || [[ $f_name == "styles.css" ]];then
echo $line
fi
done
}
```
---
## 函数
函数定义语法:
```shell
function 函数名(){
# 函数体
}
```
> [!info] 语法解释
>
> 跟 [Javascript](../../JS/JS_Note.md) 等语言很像。
### 参数
Shell 脚本内,传递参数格式为 `$n`,**1**为执行脚本的第一个参数,**2**为执行脚本的第二个参数,以此类推。
* `$#`:传递到脚本的参数个数。
* `$*`:以一个单字符串显示所有向脚本传递的参数。
> [!tip]
>
> 如果使用引号 `"` 括起来,是以 `"$1 $2 ... $n"` 形式输出所有参数。
* `$@`:与 `$*` 相同,但是使用时加引号,并在引号中返回每一个参数。
> [!tip]
>
> 如果使用引号 `"` 括起来,是以 `"$1" "$2" ... "$n"` 形式输出所有参数。
>
> `$*` 与 `$@` 跟 [数组元素](#数组元素) 中的 `*` 和 `@` 类似,`$*` 是把参数当成一个「**整体**」处理,而 `$@` 是单个参数的组合。
* `$$`:脚本运行的当前进程 ID 号。
* `$!`:后台运行的最后一个进程 ID 号。
* `$?`:显示最后命令的退出状态。`0` 表示没有错误,其他任何值表明有错误。
#### 示例
判断是否一个参数都没传:
```shell
if [[ $# -eq 0 ]]; then
fi
```
遍历参数:
```shell
for temp in "$@"; do
done
```
### 返回值
#### 返回及获取
Shell 返回值可以有两种方式进行返回:
1. 使用 `echo` 进行返回。使用这种方式返回的返回值都是字符串,获取方式可以是使用变量接收 `$(xxx)` 函数执行结果,也可以通过 `$?` 方式获取。
2. 使用 `return` 进行返回,这种方式只能返回整型。这种方式只能通过 `$?` 方式获取。
##### 示例
```shell
# 检测目录是否存在
# 返回值:0为存在 其余都是有问题的
function validate_dir() {
local dir_path=$1
# 检测路径是否为空
if [ -z $dir_path ]; then
echo -e "\e[93m Vault路径不能为空!\n \e[0m"
return 1
fi
# 目录存在
if [ ! -d "$dir_path" ]; then
echo -e "\e[93m $dir_path \e[96mVault路径不存在!\n \e[0m"
return 1
else
# return 0
echo 0
fi
}
# 检测 Vault 路径
r_1=$(validate_dir $1)
echo $r_1
# echo $?
```
> [!tip]
>
> 示例中,因为需要判断后就结束函数,所以 `return` 是必须的,同时还要显示「错误」信息。
>
> 这样写,即满足业务需求,而且还能兼容 `r_1=$(validate_dir $1)` 及 `echo $?` 这两种获取返回值的方式。
#### 返回数组
其实函数所有返回值无论之前是什么类型,最后都是以 [字符串](#字符串) 形式返回。
如果是返回 [数组](#数组),实际返回一个带有空格的字符串。
虽然执行此函数时,获取到的这个返回值是可以遍历的,但它不能如正常数组一样,通过索引取某个元素。
要想「正常」使用,得转换成数组。
##### 示例
```shell
function test1() {
local arr1=("cat" "dog" "duck" "cock" "fish" "goose")
# 返回数组
echo ${arr1[@]}
}
# 执行函数test1 并获取返回值
an_arr=$(test1)
# for a_temp in ${an_arr[@]}; do
# echo $a_temp
# done
# for a_temp in $an_arr; do
# echo $a_temp
# done
# 转换成数组
r_arr=($an_arr)
echo ${r_arr[@]}
```
---
## 常用命令
### 解析参数
在 Shell 中,三种方式解析命令行参数:
1. 直接处理,使用 `$1 $2` 这种特殊变量进行解析。
2. 使用 [getopts](#getopts)
3. 使用 [getopt](#getopt)
#### getopts
`getopts`,是 [Bash](#Bash) 的内置命令。
但 `getopts` 只能处理*短选项*,如 `-n` 这种。
#### getopt
`getopt` 不是标准的 unix 命令,但大多数 [Linux](Linux_Note.md) 发行版都自带了。
在非 [Bash](#Bash) 的 sh 中,没有 [getopts](#getopts),所以可以使用 `getopt` 替代。
`getopt` 相较于 [getopts](#getopts) 有个优势,就是它可以处理*短选项*,也可以处理*长选项*,如 `--prefix=/home` 这种。
> [!tip]
>
> 在 [zsh](#zsh) 中,没有 `getopts`。
>
> 而且 `getopts` 因为不能处理长选项,所以为了兼容性,还是优先使用 [getopt](#getopt) 命令。
`getopt` 老版本不太好用,后来的版本解决了老版本的问题,一般称为「增强版」。
,通过 `-T` 选项,即 `getopt -T`,就能检测当前发行版是否是「增强版」了。返回值为 4,就证明当前系统装的是增强版。
```shell
# silascript @ (base) in ~ [3:21:16]
$ getopt -T
# silascript @ (base) in ~ [3:21:18] C:4
$ echo $?
4
```
### read
`read` 命令是 Shell 非常重要而常用的命令。
`read` 有时需要与一个环境变量配置使用,如 `IFS` 分隔符:
```shell
inotifywait -mrq --timefmt "%d/%m/%y#%H:%M" --format '%T#%w#%f#%e' -e create,delete,modify,attrib "$config_dir" | while IFS=\# read date time dir file event; do
```
这是一个段使用 [inotify-tools](#inotify-tools) 监控某个目录的代码,其实 `while IFS=\# read date time dir file event` 这是将监控信息读取到各个变量中,默认情况,是使用空格来分隔各个信息的,如时间 `%T`、`%w` 是事件触发的目录、`%f` 事件触发的文件及 `%e` 事件本身,但由于某些情况,监控的目录中有些目录的目录名或文件的文件名包含了空格,那这就给获取 `%w` 的值带来麻烦(指的就是 [Sublime](../../Editors/Sublime_Note.md) 的配置目录,其中有个 `Installed Packages` 目录及文件 `Package Control.sublime-package`,都存在空格),所以不得已,必须另外指定分隔符。例子中就使用 `#` 当分隔符,使用 `IFS` 来指定分隔符。
---
## 相关工具
### shellcheck
[shellcheck](https://github.com/koalaman/shellcheck) 是一个 shell 的语法检查工具。
它是用 [haskell](https://www.haskell.org/) 写的,所以装它时得把 haskell 一并给装了。
```shell
pacman -S shellcheck
```
### shfmt
[shfmt](https://github.com/mvdan/sh) 是一款 shell 脚本格式化工具。
这工具可以与多款 [文本编辑器](../../Editors/Editors_Note.md) 的 shell 格式化插件配合使用。
> [!info]
>
> [各编辑器 shell 格式化插件列表](https://github.com/mvdan/sh#related-projects)
```shell
shfmt -l -w script.sh
```
#### 参数解释
`-i`:缩进设置。默认是 0,表示制表符缩进。大于 0 空格缩进,数字是就是空格数。
`bn`: && 及 | 另起一行
`ci`:switch case 缩进
`sr`:重定向符,`>`、`>>`、`<<` 这些,后面添加空格
`fn`:函数大括号,起始那个括号另起一行
`kp`:对齐
Google 风格:[Style guides for Google-originated open-source projects](https://google.github.io/styleguide/shellguide.html#indentation)
### inotify-tools
这是一个可以监控目录变化的小工具。
#### 主要参数
* `r`:即 `recursive`,递归查询目录。
* `m`:即 `monitor`,始终保持监听,如果没有这个参数,`inotifywait` 在接收一次事件之后就会退出。
* `q`:即 `quiet`,就是只打印事件,最小化输出。
* `e`:即 `event`,我们要监听的事件类型,多个事件用 `,` 分隔。
##### 事件列表
| 事件 | 解释 |
|:-------------:|:----------------------------------------:|
| access | 文件或者目录被读 |
| modify | 文件或目录被写入 |
| attrib | 文件或者目录属性被更改 |
| close_write | 文件或目录关闭,在写模式下打开后 |
| close_nowrite | 文件或目录关闭,在只读模式打开后 |
| close | 文件或目录关闭,而不管是读/写模式 |
| open | 文件或目录被打开 |
| moved_to | 文件或者目录移动到监视目录 |
| moved_from | 文件或者目录移出监视目录 |
| move | 文件或目录移出或者移入目录 |
| create | 文件或目录被创建在监视目录 |
| delete | 文件或者目录被删除在监视目录 |
| delete_self | 文件或目录移除,之后不再监听此文件或目录 |
| unmount | 文件系统取消挂载,之后不再监听此文件系统 |
##### format
`--format` :参数也会用到,是控制输出格式的。
* `%w`: 表示发生事件的目录
* `%f`: 表示发生事件的文件
* `%e`: 表示发生的事件
* `%Xe`: 事件以“X”分隔
* `%T` : 使用由 [timefmt](#timefmt) 定义的时间格式
##### timefmt
`--timefmt`:时间格式
示例及说明:
```shell
file_dir=$1
inotifywait -mrq --timefmt "%d/%m/%y %H:%M" --format "%T %w%f %e" -e create,delete,modify $file_dir | while read date time dirfile event
```
> [!info]
>
> `while read` 后四个变量 `date`、`time`、`dirfile` 和 `event`,这是自定义的变量,用来获取 inotifywait 输出的信息。
>
> 能获取几个 inotifywait 输入信息,主要看 `--format` 这个参数。这个参数是 inotifywait 的输出格式。其中的参数值以空格分隔。如例子中 `%T %w%f %e`,能获取三块信息 `%T`,即时间信息,`%w%f` 目录或文件路径信息,`%e` 事件信息。而 `%T` 能获取几个时间信息,又是由 `--timefmt` 这个参数决定的。如例子中 `%d/%m/%y %H:%M`,使用了一个空格分隔,所以实际能获取到的时间信息是两块,所以接收这个信息的变量个数应该非常注意,如果少了或多了,那就获取到错误部分的信息了。
>
> 另外,`%w` 和 `%f`,这两个,虽然一个为目录,一个为文件,但最好不要使用空格隔开,妄想使用这种方式分别获取目录和文件,最终效果是,如果触发事件的目标是一个目录,那结果是路径最后的子目录会被当成文件来获取(因为没有真的文件存在嘛)。-- 其实对于 [Linux](../Linux_Note.md) 而言,所有东西都是文件,目录与文件区分,只是人们为了方便而区分的。在 Linux 的目录树中,常看到用 `d` 来标识其中那些为目录的「文件」,而 inotifywait 触发事件中,那些目录触发的事件问题带着 `ISDIR` 标识,这与 Linux 的 `d` 标识的设计逻辑是一致的。
### json 相关工具
#json
shell 下有多款 json 小工具:
* `jq` 或 `jshon`:shell 下的 JSON 解析器。
* `JSON.sh`、`jsonv.sh`:shell 脚本,能在 bash、zsh 等中解析 JSON。
* `JSON.awk`:JSON 解析器 awk 脚本。
* `json.tool`:python 模块。
* `undercore-cli`:基于 [NodeJS](../../Node/NodeJS_Note.md) 或 [JS](../../JS/JS_Note.md) 的 json 工具。
#### jq
#shell #tools #json #jq
[jq](https://jqlang.org) 是一个 Shell 下操作 json 的小工具。
##### 安装
```shell
yay -S jq
```
##### 语法
```shell
jq [options] [file...]
jq [options] --args [strings...]
jq [options] --jsonargs [JSON_TEXTS...]
```
###### 选项
* `-c`:紧凑而不是漂亮的输出
* `-n`:使用 `null` 作为单个输入值
* `-e`:根据输出设置退出状态代码
* `-s`:将所有输入读取(吸取)到数组中;应用过滤器;
* `-r`:输出原始字符串,而不是 JSON 文本
* `-R`:读取原始字符串,而不是 JSON 文本
* `-C`:为 JSON 着色
* `-M`:单色(不要为 JSON 着色)
* `-S`:在输出上排序对象的键
* `--tab`:使用制表符进行缩进;
* `--arg a v`:将变量 `$a` 设置为 `v`
* `--argjson a v`: 将变量 `$a` 设置为 JSON `v`
* `--slurpfile a f`:将变量 `$a` 设置为从 `f` 读取的 JSON 文本数组
* `--rawfile a f`:将变量 `$a` 设置为包含 `f` 内容的字符串
* `--args`:其余参数是字符串参数,而不是文件
* `--jsonargs`:其余的参数是 JSON 参数,而不是文件
* `--`:终止参数处理
##### 内置函数
`jq` 支持一些内置函数,如 `length`, `keys`, `values`, `tostring` 等,用于操作和处理 JSON 数据。
* `del`:直接删除目标字段,生成新对象。
###### 数组
* `map(f)`:对数组中的每个元素应用过滤器 `f`
* `sort`:对数组中的元素进行排序
* `sort_by(f)`:根据过滤器 `f` 的结果对数组排序
* `min`,`max`:找出数组最小值和最大值。
* `reverse`:反转数组元素的顺序
###### 对象操作
* `keys` :函数是获取对象所有的键,并以数组形式返回
* `values` :函数是获取对象的值。
* `map_values(f)`:对对象中每个值应用过滤器 `f`
* `has(key)`:判断对象是否有某个键
###### 字符串操作
* `contains(x)`:判断输入是否完全包含参数 `x`
* `tostring`:将输入转换成字符串
##### 示例
###### 示例 1
```shell
# 从assets 数组中获取browser_download_url元素的值
# 过滤除了main.js manifest.json styles.css三个文件外所有文件
jq -r '.assets[] | .browser_download_url | select ( contains("main.js") or contains("manifest.json") or contains("styles.css") )' $json_path
```
> [!info]
>
> * `contains()` 方法是用来判断是否包含某字符串,包含返回 `true`,否则返回 `false`
> * `select()` 选择过滤数据
###### 示例 2
```shell
local tagstr="$2"
curl http://hub-mirror.c.163.com/v2/library/${image}/tags/list | jq --arg tstr $tagstr -r '.tags[]| select(contains($tstr))'
```
> [!info]
>
> * `jq --arg` 是定义变量的选项
> * `jq --arg tstr $tagstr`: `tstr` 为形参变量,是 jq 内部使用;而 `$tagstr` 是实参,外部传进来的。要使用形参时,使用 `$` 打头,跟普通 shell 变量使用一致。
传多个参数:
```shell
dl_url=$(curl $channel_json_v3 | jq -r --arg pkg_name $package_name --arg pkg_version "$package_version" '.packages_cache.[].[]| select(.name==$pkg_name)|.releases[]| select(.version==$pkg_version).url')
```
> [!info]
>
> `pkg_name` 和 `pkg_version` 这两个是形参,用于 jq 内部引用的。
>
> `$package_name` 和 `$package_version` 是实参,是 jq 外部实际传进来的值。
>
> 注意,`select(.name==$pkg_name)` 或 `select(.version==$pkg_version)`,引用「形参」时,不要加双引号,而且 `==` 不要加空格。
>
> 「实参」`"$package_version"` 这个可以加双引号,防止传进来的字符串带有空格,被「自动切割」。
###### 示例 3
下面是 [Obsidian](../../NoteSoft/Obsidian/Obsidian_Note.md) 的 [vault](../../NoteSoft/Obsidian/Obsidian_Note.md#vault) 列表配置文件:
```json
{
"vaults": {
"88a790a7d8b3e712": {
"path": "/home/silascript/MyNotes/ITNotes",
"ts": 1763336737061,
"open": true
},
"50006d35f784463b": {
"path": "/home/silascript/MyNotes/WritingNotes",
"ts": 1763353935904
},
"38ba7ce6d75f3dc4": {
"path": "/home/silascript/MyNotes/WritingExericse",
"ts": 1763304814607
},
"8e5254dd564849f2": {
"path": "/home/silascript/MyNotes/LHP_Note",
"ts": 1763324507963
}
},
"frame": "custom",
"disableGpu": true,
"updateDisabled": true
}
```
如果想要根据 vault 的目录路径找到相应的 vault 的 ID,可以使用以下代码:
```shell
cat .config/obsidian/obsidian.json | jq -r '.vaults | map_values(select(.path=="/home/silascript/MyNotes/WritingExericse")) | keys'
```
### 其他小工具
#### yq
[yq](https://github.com/mikefarah/yq) 是类似于 [jq](#jq) 的小工具,不过它用来解析 [YAML](../../YAML/YAML_Note.md) 的,是一个轻量级的便携式命令行 YAML 处理器。它所以可以通过 [pip](../../Python/Python_Note.md#python_pip) 来安装。
因为 `yq` 是有入口程序的,所以也可以使用 [pipx](../../Python/Python_Note.md#python_pipx") 来安装:
```shell
pipx install yq
```
当然 yq 通过系统的包管理器安装:
([ArchLinux_Note](../Arch/ArchLinux_Note.md) 上包管理器中的 `yq` 是 python 版本,而 go 版的是叫 `go-yq`)
```shell
pacman -S go-yq
```
---
## 相关笔记及资料
### 笔记
* [Linux笔记](Linux_Note.md)
* [Shell示例笔记](Shell_Example.md)
* [ZSH笔记](Zsh_Note.md)
### 教程
* [Shell 编程范例 - 书栈网](https://www.bookstack.cn/read/open-shell-book/README.md)
* [Shell编程基础 - 书栈网](https://www.bookstack.cn/read/ShellBasicProgramming)
### 周边资料
* [awesome-shell](https://github.com/alebcay/awesome-shell)
* [awesome-shell_ZH-CN](https://github.com/xuxiaodong/awesome-shell/blob/master/README_ZH-CN.md#shell-%E5%8C%85%E7%AE%A1%E7%90%86)
* [Awesome Shell:命令行框架、工具包、指南清单(中译版) - Shell - 软件编程 - 深度开源](https://www.open-open.com/news/view/e54142)
* [shell echo 显示颜色 - 知乎](https://zhuanlan.zhihu.com/p/181609730)
* [实现shell脚本中的转圈、进度条等一些效果 · 艾莉亚的猫](https://yangtze736.github.io/%E6%8A%80%E6%9C%AF/2018/05/02/shell-tips/)
---
## 其他笔记
* [Shell 视频清单](Shell_Videos.md)
* [Shell 资料清单](Shell_Material.md)
* [Shell 示例](Shell_Example.md)
* [Linux 笔记](Linux_Note.md)
* [Linux 视频清单](Linux_Videos.md)