Skip to content

Latest commit

 

History

History
1513 lines (1083 loc) · 42.3 KB

File metadata and controls

1513 lines (1083 loc) · 42.3 KB
aliases
tags
shell
linux
list
sh
bash
zsh
created 2023-08-18 12:44:52 -0700
modified 2025-11-28 02:33:19 -0800

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

相关资料


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 连接本机:

$ 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 工具

Bash-It


Shell 命令种类

LinuxShell 可执行的命令有 3 种:

  • 内建命令
  • Shell函数
  • 外部命令

内建命令

这些命令集成在 Shell 解释器中,一种是改变 Shell 本身的属性设置;另一种是 I/O 命令,如 echo 命令。

使用 type 命令可以判断命令是内建命令还是 外部命令

$ type cd
cd is a shell builtin

内建命令会显示「builtin」字样。

外部命令

是独立于 Shell 的可执行程序,如 findgrep 等。

对于外部命令,Shell 会创建一个新的进程来执行命令。

外部命令执行过程:

  1. 调用 POSIX 系统的 fork 函数接口,创建珍上命令行 Shell 进程的复制(子进程)
  2. 在子进程中,寻找外部命令
  3. 在子进程中,执行寻找到的外部命令,此时父进程牌休眠,等待子进程执行完毕
  4. 子进程执行完毕,父进程接着执行下一条命令

Tip

使用 source 执行 Shell 脚本时,不会创建子进程,而是在父进程中直接执行。所以要修改当前 Shell 本身环境变量,是使用 source 命令。


变量

命名规则:

  • 首个字符必须为字母
  • 中间不能有 空格,可以使用下划线 _
  • 不能使用标点符号
  • 不能使用 Bash 等 Shell 里的关键字

空格

shell 对于空格有严格的规定:

  • 赋值语句等号两边绝对不能有空格。
  • 字符串比较 等号两边必须有空格。
  • 所赋的值包含空格,可以用引号括起来。
  • if 语句的 [] 中,表达式前后都应有空格。而 if 语句中那个 ;] 间不能有空格。

    [!example] 小示例

    if [ "${s1}" = "hello" ];then

[!info] 相关资料

引号

数字可带可不带引号,但不带引号的数字可计算,带引号的数字不能用于计算。

[!info] 相关资料

  • 被双引号(" ")括起来的变量替换是不会被阻止的, 所以双引号被称为「部分引用」,或「弱引用」。
  • 被单引号(' ')括号起来的变量替换会被禁止,变量名只会被解释成字面的意思,不会发生变量替换,所以单引号被称为「全引用」,或「强引用」。

局部变量

默认在 shell 脚本中定义的变量都是「全局」(global)的。

使用 local 关键字修饰的变量,称为「局部变量」,其作用域只在 函数 范围。

如果出现同名,shell 函数中定义的 local 变量会屏蔽掉脚本定义的全局变量。

特殊变量

  • $0:当前脚本的文件名
  • $n:传递给脚本或函数的参数。n 是数字,表示第几个参数,从1开始。
  • $#:传递给脚本或函数的参数个数。
  • $*:传递给脚本或函数的所有参数,即以一个单字符显示所有向脚本传递的参数。
  • $?:上个命令的退出状态,或函数的返回值。0 表示没有错误,其他值表明有错误。
  • $$:当前 Shell 进程 ID。
  • $!:后台运行的最后一个进程 ID 号
  • $@:与 $* 相同,但是使用时加引号,并在引号中返回每个参数。
  • $-:显示 Shell 使用的当前选项,与 set 命令功能相同。

变量使用

变量使用时,在变量名前加上 $

示例:

local num1=5

echo $num1

Tip

$变量名 这种形式实质是 ${变量名} 的「简写形式」。在某些情况还 $变量名 这种形式可能引起错误,这时就需要用到 ${变量名} 这种标准形式。

[!info]

Shell 的变量只有使用或引用时,才需要加 $,声明、定义或赋值时,是不需要加 $,这与 PHP的变量定义规则 不同。

在没有 $ 时的变量,有以下几种情况:

  • 变量被声明或赋值,如上面的示例 local num1=5
  • 变量被 unset
  • 变量被 export

表达式

条件表达式

语法:[ expression ]

括号中的表达式前后都有空格

[!tip] shell 的空格

因为 Sehll 是命令加选项或参数而构成的,而命令与选项或参数间是以空格间隔的,所以在 Shell 中空格是有点「微妙」的存在。

在条件表达式中,括号中的表达式前后必须都加上空格,不然就会报错。而赋值表达式中,= 前后就不能加空格。

 if [[ xxx ]];then
	xxx
elif [[ xxx ]];then
	xxxx
else
	xxxx
fi

Tip

Shell 中是 elif,不是 else if

整数比较

  • -eqequal:等于
  • -nenot equal:不等于
  • -gtgreate than:大于
  • -ltlesser than:小于
  • -gegreate or equal:大于等于
  • -lelesser or equal:小于等于

字符串比较

  • ==:等于。[ "a" == "a" ]
  • !=:不等于。[ "a" != "a"]
  • -n:字符长度不等于 0 为真。[ -n "xxx" ]
  • -z:字符串长度等于 0 为真。[ -z "xxx" ]

Tip

使用 -n 判断字符串长度时,变量加双引号。不但 -n,只要是字符串比较,都加双引号,这是一个好习惯。

文件测试

  • -e:文件或目录存在为真。[ -e path ]

  • -f:文件存在为真。

  • -d:目录存在为真。

    [!info] 示例

     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 不支持逻辑判断符,可使用 布尔运算符 替代。


路径

# 显示当前路径
echo $PWD

Tip

$PWD,跟 echo 命令一起使用时,必须大写。

文件

basename

使用 basename 命令可以得到一个包含后缀名的文件名。

示例:

$ basename ~/MyNotes/ITNotes/常用字体.txt 
常用字体.txt

-s:不显示指定的后缀名。

示例:

$ basename -s .txt ~/MyNotes/ITNotes/常用字体.txt
常用字体

当然 -s 也可以省略,而指定的不显示的后缀名作为第二个参数:

$ basename  ~/MyNotes/ITNotes/常用字体.txt .txt
常用字体

甚至这个指定的后缀名不一定是后缀名,而是指定的任务结尾的字符,它本质只是在结果的末尾去掉匹配的字符串

$ basename  ~/MyNotes/ITNotes/常用字体.txt t   
常用字体.tx

dirname

使用 dirname,可以得到文件所在的目录路径字符串。

示例:

$ dirname ~/MyNotes/ITNotes/常用字体.txt
/home/silascript/MyNotes/ITNotes

列表

列表实际就是有空格的字符串。

构建一个列表:

list1=("aa" "bb" 5 "cc")

获取列表元素,从 0 开始:

echo ${list1[0]}

获取所有列表元素:

echo ${list1[@]}

遍历列表:

for s_temp in "${list1[@]}"; do
	echo $s_temp
done

获取列表长度:

echo ${#list1[@]}

数组

数组初始化

数组名=()

数组元素

数组所有元素

  • *:全部元素,是作为一个整体处理

  • @:同上,但会强制单词分隔

    [!tip] for in 中使用

    *@ 区别在 for in 循环中会体现出区别:

     arr1=("hello world" "the man")
     
     for i in "${arr1[*]}";do
     echo $i
    done
    ```  - 

    这段代码会输出 hello world then man

     for i in "${arr1[@]}";do
    echo $i
    done

    这段代码会输出:

     hello world
     the man
    

取最后一个元素:arr[-1]

[!tip] 老语法

${#arr[@]-1}${#arr[@]} 这是取数组长度(这跟 列表 是一样的。),那数组长度 -1 就得到数组最后一个元素的索引值。

${!arr[*]}${!arr[@]} 是查看已赋值元素的下标。

[!info]

@ 跟 * 的区别:

  • 变量使用 * 时,变量被 "" 包裹,会当成一串字符串处理。
  • 变量使用 @ 时,变量被 "" 包裹,依然当做数组处理。
  • 变量在没有被 "" 包裹的情况下,@ 跟 * 是等效的.

示例

1. 读取文件并将数据存放到数组中

# 地址数组
addrs_arr=()

# 读取文件
# 去除空行及以 # 起头的行
for line in `cat $addrsls_file_path | grep -v ^$ | grep -v ^\#` 
do
	# 将数据存放到数组
	addrs_arr+=("$line")
done

[!info]

grep -v 表示反向选择。

^$^\# 都是 正则表达式 的东西。^$ 表示空行,^\# 表示以 # 符号开始的行。

grep -v ^$ | grep -v ^\# 表示就是选项非空行及非使用 #「标记」的行。

2. 遍历数组

方式 1:

# 遍历数组
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:

for i in ${!json_data_arr[@]}
do
	echo ${json_data_arr[$i]}
done

Tip

使用 ! 这种方式,是使用数组索引来遍历。

i 是数组索引

3. 添加元素

向数组中添加元素,可以有四种方法:

  1. 按照下标进行单个添加
array_name[index]=value
  1. 在不做任何删减时,直接使用数组长度追加元素
array_name[${#array_name[@]}]=value
  1. 直接获取源数组的全部元素再加上新要添加的元素,一并重新赋予该数组,重新刷新定义索引
array_name=("${#array_name[@]}" value1 value2 ... valueN)

[!info]

双引号不能省略,否则数组中存在包含空格的元素时会按空格将元素拆分成多个。

不能将 @ 替换为 *,如果替换为 *,不加双引号时与 @ 的表现一致,加双引号时,会将数组 array_name 中的所有元素作为一个元素添加到数组中。这规则在 字符串 中也同样适合。

  1. 使用 += 直接添加,待添加元素必须用 () 包围起来,并且多个元素用空格分隔
array_name+=(value1 value2 ... valueN)

Important

待添加元素必须用 () 包围起来,并且多个元素用空格分隔

() 对于数组而言,是构建数组的关键。如获取一相函数返回的数组,如果没有 (),将被 Shell 当成字符串处理,这时使用 ${#array[@]} 来获取数组长度将只会是 1,因为虽然函数使用 echo ${array[@]} 返回了一个数组,但接收方会将这货当成一个字符串处理,因为 Shell 中实际都是字符,因为 Shell 或 Linux 大部分工具,实质是都是针对处理文本而生的,所以字符或字符串才是其本质。

function test1(){
	local array1=(2 5 22 32 18)
	echo ${array1[@]}
}
# 获取返回值次构建为数组
r_arr=($(test1))

echo ${#r_arr[@]}

4. 删除数组元素

语法:unset 数组[引索]

5. 函数中的数组使用

将一个数组作为参数向函数传递
fun1 ${ads_arr[*]}

[!tip] 数组实参

在调用一个函数时,向此函数传入一个「实参」时,如果是普通变量只需要 $变量名,就可以。

但如果要向函数传个数组,那语法就得「变下」,得写成 ${数组名[*]}${数组名[@]}。如果数组变量按普通变量写法传参,那只会传入数组第一个元素,即 ${数组名[0]}

其实这是符合数组获取 数组所有元素 的语法规则的。简单说,shell 语法与其他高级编程语言有点不一样,传参得「实打实」地传入数组的「所有元素」。

函数内接收外部传入的数组
# 接收外部传来的数组
local ads_array=($@)

[!tip] 语法解释

函数中接收传入的数组,其语法也与接收普通变量不一样,函数内接收普通变量参数可以这样:temp=$数字,通过数字来指定接收哪个一参数。

而因为数组不是一个数据,而是「一堆」数据,所以得使用特殊一点语法接收,其中 @数组所有元素 的语法保持一致,另外,那对小括号 (),其实是就是构建一个数组的语法。即 ads_array=() 这个是一个 数组初始化 语法。

也就是说,函数内接收数组,其实是初始化了一个数组用来接收。

特殊案例

需求:第 2 个参数是一个数组,函数如何接收:

方案一:先接收第一个参数,然后使用 shift 命令用于实现实现位置参数左移,

语法格式:shift [n]

[!info]

说明:shift 命令用来删除参数。shift 命令参数默认为 1,表示 从命令行删除第一个参数。当指定了参数 n 时,shift 命令就一次删除 n 个参数。

# 定义函数
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"

方案二:使用 字符串 来接收,然后然 字符串 转成数组。

# 定义函数
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"

方案三:使用 Bash4.3+ 的新特性:名称引用

# 定义函数
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

通过 ${#数组[@]} 语法获取数组元素的个数来判断:

arr1=()
if [ ${#arr1[@]} -eq 0 ];then
	echo "数组为空"
else
	echo "数组不为空"
fi
方式 2

通过 ${数组[@]} 语法获取数组所有元素,如果返回一个空值,则表明此数组为空:

arr1=()
isEmpty=true
for element in "${arr1[@]}"; do
    isEmpty=false
    break
done

if $isEmpty; then
    echo "Array is empty"
fi

[!info]

最「笨」的方式就是遍历数组,设个结果变量,判断每一个元素。


运算

原生 Shell 不支持数学运算,可以通过 awkexpr 命令来实现。

num1=`expr 1+2`
echo num1

当然还有更便捷的方式,使用 (()) 来进行整数运算。


字符串

切割

方式 1

语法:${parameter//pattern/string}

用 string 来替换 parameter 变量中所有匹配的 pattern

s1="hello,shell,split,test"  
array=(${s1//,/ })

[!info]

s1 字符串原有使用 , 来分隔,而经过 ${s1//,/ } 后,就是将 , 替换成空格,利用 数组 构建时默认按空格分隔的规则,这些子串就被替换成使用空格来分隔,那最后构建成数组时,子串自动变成数组的各个元素。

[!important]

注意最后的是 / 斜杠后是有一个空格的,如果少了,就相当于将原有的分隔符 , 给「移除」了,那些原来用 , 分隔的子串,就会合并成一起了!-- 当然如果有这种将子串合并成一个字符串的需求,可以使用这种方式来对字符串进行合并。

截取

示例 1

# # 是从左向右,一个#是取第一个,##是取最后一个
# % 是从右向左 一个%是取第一个,%%是取最后一个
## 
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

# core_address是 denolehov/obsidian-git 这个样子。获取 / 左右两段字符串

# 取前段 使用从右向左取,取 / 最后一段
local account=${core_address%%/*}
# 取后牌戏 使用从左向右取,同样取 / 最后一段
local p_name=${core_address##*/}

Tip

从左往右,*# 在一起,而且 * 总在左边

从右往左,*# 总被分隔符分离,* 总在右边。

遍历

字符串是可以遍历的。

示例 1

str2="hello,world"

for s_temp in $str2; do
	echo $s_temp
done

[!info]

示例 1 中最终输出就是 hello,world,就是把字符串中每一个字符「遍历」一遍。

示例 2

str1="silas tom jack lucy mary"

for s_tem in $str1; do
	echo $s_tem
done

[!info]

示例 2 中的字符串,是带空格的;所以遍历的结果是按空格分割,将分割后每段子串依次输出。

这种以空格为分隔符的字符串,也可以这样遍历:

for s_tem in ${str1[@]}; do
	 echo $s_tem
done

其实这是一种 数组样式 的遍历。

字符串转 数组

按行分隔

默认情况,是按空格分隔,可以通过修改 IFS 的值来改变分隔符。

IFS=$'\n' # 设置内部字段分隔符为换行符

array=($string)

unset IFS # 恢复IFS为默认值

条件

if [ command ];then
     符合该条件执行的语句
elif [ command ];then
     符合该条件执行的语句
else
     符合该条件执行的语句
fi

循环

示例

循环读取文件

whilefor 读文件是有区别的:

  1. while 是逐行读取,读完一行跳转到下一行
  2. for 是按字符串方式读取,遇到空格后,再读取的数据就会换行显示

while 相对于 for 的读取能更好的还原数据原始性。

for 实现
for 变量名 in 循环列表
do
  命令集
done
    

[!info]

这种 for 循环语句语法中,for 关键字后面会有一个「变量名」,变量名依次获取 in 关键字后面的变量取值列表内容(以空格分隔),每次仅取一个,然后进入循环(dodone 之间的部分)执行循环内的所有指令,当执行到 done 时结束本次循环。

之后,「变量名」再继续获取变量列表里的下一个变量值,继续执行循环内的所有指令,当执行到 done 时结束返回,以此类推,直到取完变量列表里的最后一个值并进入循环执行到 done 结束为止。

for line in `cat xxx.txt`
do
	echo $line
done
while 实现

写法 1:

cat xxx.txt | while read line
do
  echo $line
done

写法 2:

while read line$*用法
do
  echo $line
done < xxx.txt
小示例

这示例使用到了 jq 这个 json 小工具,对 json 文件进行解析,并将解析后的结果数据输出存放到一个临时文件中。

然后对这个临时文件进行读取,在读取时,还对读到的数据进行一定需求的过滤。

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
}

函数

函数定义语法:

function 函数名(){
	# 函数体
}

[!info] 语法解释

Javascript 等语言很像。

参数

Shell 脚本内,传递参数格式为 $n1为执行脚本的第一个参数,2为执行脚本的第二个参数,以此类推。

  • $#:传递到脚本的参数个数。
  • $*:以一个单字符串显示所有向脚本传递的参数。

Tip

如果使用引号 " 括起来,是以 "$1 $2 ... $n" 形式输出所有参数。

  • $@:与 $* 相同,但是使用时加引号,并在引号中返回每一个参数。

Tip

如果使用引号 " 括起来,是以 "$1" "$2" ... "$n" 形式输出所有参数。

$*$@数组元素 中的 *@ 类似,$* 是把参数当成一个「整体」处理,而 $@ 是单个参数的组合。

  • $$:脚本运行的当前进程 ID 号。
  • $!:后台运行的最后一个进程 ID 号。
  • $?:显示最后命令的退出状态。0 表示没有错误,其他任何值表明有错误。

示例

判断是否一个参数都没传:

if [[ $# -eq 0 ]]; then

fi

遍历参数:

for temp in "$@"; do

done

返回值

返回及获取

Shell 返回值可以有两种方式进行返回:

  1. 使用 echo 进行返回。使用这种方式返回的返回值都是字符串,获取方式可以是使用变量接收 $(xxx) 函数执行结果,也可以通过 $? 方式获取。
  2. 使用 return 进行返回,这种方式只能返回整型。这种方式只能通过 $? 方式获取。
示例
# 检测目录是否存在
# 返回值: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 $? 这两种获取返回值的方式。

返回数组

其实函数所有返回值无论之前是什么类型,最后都是以 字符串 形式返回。

如果是返回 数组,实际返回一个带有空格的字符串。

虽然执行此函数时,获取到的这个返回值是可以遍历的,但它不能如正常数组一样,通过索引取某个元素。

要想「正常」使用,得转换成数组。

示例
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
  3. 使用 getopt

getopts

getopts,是 Bash 的内置命令。

getopts 只能处理短选项,如 -n 这种。

getopt

getopt 不是标准的 unix 命令,但大多数 Linux 发行版都自带了。

在非 Bash 的 sh 中,没有 getopts,所以可以使用 getopt 替代。

getopt 相较于 getopts 有个优势,就是它可以处理短选项,也可以处理长选项,如 --prefix=/home 这种。

Tip

zsh 中,没有 getopts

而且 getopts 因为不能处理长选项,所以为了兼容性,还是优先使用 getopt 命令。

getopt 老版本不太好用,后来的版本解决了老版本的问题,一般称为「增强版」。

,通过 -T 选项,即 getopt -T,就能检测当前发行版是否是「增强版」了。返回值为 4,就证明当前系统装的是增强版。

# silascript @ (base) in ~ [3:21:16] 
$ getopt -T
# silascript @ (base) in ~ [3:21:18] C:4
$ echo $?
4

read

read 命令是 Shell 非常重要而常用的命令。

read 有时需要与一个环境变量配置使用,如 IFS 分隔符:

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 监控某个目录的代码,其实 while IFS=\# read date time dir file event 这是将监控信息读取到各个变量中,默认情况,是使用空格来分隔各个信息的,如时间 %T%w 是事件触发的目录、%f 事件触发的文件及 %e 事件本身,但由于某些情况,监控的目录中有些目录的目录名或文件的文件名包含了空格,那这就给获取 %w 的值带来麻烦(指的就是 Sublime 的配置目录,其中有个 Installed Packages 目录及文件 Package Control.sublime-package,都存在空格),所以不得已,必须另外指定分隔符。例子中就使用 # 当分隔符,使用 IFS 来指定分隔符。


相关工具

shellcheck

shellcheck 是一个 shell 的语法检查工具。

它是用 haskell 写的,所以装它时得把 haskell 一并给装了。

pacman -S shellcheck

shfmt

shfmt 是一款 shell 脚本格式化工具。

这工具可以与多款 文本编辑器 的 shell 格式化插件配合使用。

[!info]

各编辑器 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

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:时间格式

示例及说明:

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 后四个变量 datetimedirfileevent,这是自定义的变量,用来获取 inotifywait 输出的信息。

能获取几个 inotifywait 输入信息,主要看 --format 这个参数。这个参数是 inotifywait 的输出格式。其中的参数值以空格分隔。如例子中 %T %w%f %e,能获取三块信息 %T,即时间信息,%w%f 目录或文件路径信息,%e 事件信息。而 %T 能获取几个时间信息,又是由 --timefmt 这个参数决定的。如例子中 %d/%m/%y %H:%M,使用了一个空格分隔,所以实际能获取到的时间信息是两块,所以接收这个信息的变量个数应该非常注意,如果少了或多了,那就获取到错误部分的信息了。

另外,%w%f,这两个,虽然一个为目录,一个为文件,但最好不要使用空格隔开,妄想使用这种方式分别获取目录和文件,最终效果是,如果触发事件的目标是一个目录,那结果是路径最后的子目录会被当成文件来获取(因为没有真的文件存在嘛)。-- 其实对于 Linux 而言,所有东西都是文件,目录与文件区分,只是人们为了方便而区分的。在 Linux 的目录树中,常看到用 d 来标识其中那些为目录的「文件」,而 inotifywait 触发事件中,那些目录触发的事件问题带着 ISDIR 标识,这与 Linux 的 d 标识的设计逻辑是一致的。

json 相关工具

#json

shell 下有多款 json 小工具:

  • jqjshon:shell 下的 JSON 解析器。
  • JSON.shjsonv.sh:shell 脚本,能在 bash、zsh 等中解析 JSON。
  • JSON.awk:JSON 解析器 awk 脚本。
  • json.tool:python 模块。
  • undercore-cli:基于 NodeJSJS 的 json 工具。

jq

#shell #tools #json #jq

jq 是一个 Shell 下操作 json 的小工具。

安装
yay -S jq
语法
jq [options] <jq filter> [file...]
jq [options] --args <jq filter> [strings...]
jq [options] --jsonargs <jq filter> [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 支持一些内置函数,如 lengthkeysvaluestostring 等,用于操作和处理 JSON 数据。

  • del:直接删除目标字段,生成新对象。
数组
  • map(f):对数组中的每个元素应用过滤器 f
  • sort:对数组中的元素进行排序
  • sort_by(f):根据过滤器 f 的结果对数组排序
  • minmax:找出数组最小值和最大值。
  • reverse:反转数组元素的顺序
对象操作
  • keys :函数是获取对象所有的键,并以数组形式返回
  • values :函数是获取对象的值。
  • map_values(f):对对象中每个值应用过滤器 f
  • has(key):判断对象是否有某个键
字符串操作
  • contains(x):判断输入是否完全包含参数 x
  • tostring:将输入转换成字符串
示例
示例 1
# 从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
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 $tagstrtstr 为形参变量,是 jq 内部使用;而 $tagstr 是实参,外部传进来的。要使用形参时,使用 $ 打头,跟普通 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_namepkg_version 这两个是形参,用于 jq 内部引用的。

$package_name$package_version 是实参,是 jq 外部实际传进来的值。

注意,select(.name==$pkg_name)select(.version==$pkg_version),引用「形参」时,不要加双引号,而且 == 不要加空格。

「实参」"$package_version" 这个可以加双引号,防止传进来的字符串带有空格,被「自动切割」。

示例 3

下面是 Obsidianvault 列表配置文件:

{
  "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,可以使用以下代码:

cat .config/obsidian/obsidian.json | jq -r '.vaults | map_values(select(.path=="/home/silascript/MyNotes/WritingExericse")) | keys'

其他小工具

yq

yq 是类似于 jq 的小工具,不过它用来解析 YAML 的,是一个轻量级的便携式命令行 YAML 处理器。它所以可以通过 pip 来安装。

因为 yq 是有入口程序的,所以也可以使用 pipx 来安装:

pipx install yq

当然 yq 通过系统的包管理器安装:

ArchLinux_Note 上包管理器中的 yq 是 python 版本,而 go 版的是叫 go-yq

pacman -S go-yq

相关笔记及资料

笔记

教程

周边资料


其他笔记