--- 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)