Shell 使用总结

shell 声名

解析器声名,#!/bin/sh是指此脚本使用/bin/sh来解释执行,#!是特殊的表示符,其后面根的是此解释此脚本的 shell 解释器的路径。

#!/bin/sh
# 默认:执行脚本的时候,如果遇到不存在的变量,默认忽略它,继续向下执行。
# 设置执行脚本如果遇到不存在的变量,则报错
set -u
# 设执行输出结果前,打印执行的是哪一条指令
set -x

shell 解释器有很多,常用的是 Bash(解释器#!/bin/bash) 和 Dash(解释器#!/bin/sh),Linux 操作系统缺省的 shell 是 Bash,但 Bash 过于复杂,便基于 Bash 精简出符合 POSIX 标准的 Dash,二者大体相同,但标记为#!/bin/sh的脚本不应使用任何 POSIX 没有规定的特性(如 let 等命令, 只有#!/bin/bash可以)。

不同系统下书写的 shell 脚本可能有有细微差距,例如在 Windows 下写的的 shell 脚本,可能会由于空格不一致的原因,在 Mac 环境运行报错。解决办法,转换格式即可,假设 shell-test.sh 脚本是 Windows 下生成的脚本,我们使用 vim 转换为 Mac 下可运行的脚本。

vi shell-test.sh
# 显示 dos
:set ff
# 转换为unix格式
:set ff=unix
# 退出并保存
:wq

shell 变量

shell 变量的声名

  1. 只能使用数字,字母和下划线,且不能以数字开头;
  2. 变量名区分大小写;
  3. 声名变量”=“前后不能有空格(初学者很容易踩此坑)。
#!/bin/sh
echo "测试变量声名"
# 声名字符串变量 hello world!
myVar="hello world!"
# 声名数字变量 88 
muNum=88
# 打印结果 hello world!
echo $myVar
echo $muNum

$ 关键字的使用

$ 关键字有很多用途,例如cd "$(dirname "$0")"命令可将执行环境切换到当前 shell 文件所在目录。

  1. $0当前脚本的文件名;
  2. $n传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是$1,第二个参数是$2
  3. $#传递给脚本或函数的参数个数;
  4. $*$@传递给脚本或函数的所有参数;
  5. $?上个命令的退出状态,或函数的返回值;
  6. $!当前 shell 最后运行的后台 PID;
  7. $$获取当前shell的进程号(PID)
  8. $()与``(反引号)都是用来做命令替换的;
  9. ${var}返回变量值;
  10. ${#var}计算 shell 字符串变量的长度;
  11. $(())是用来作整数运算的。

我们使用 shell-test.sh 脚本进行测试。

#!/bin/sh

# 定义一个变量 my_var
my_var="abcdefg"

# 自定义方法 MyFunc
MyFunc() {
# 输出当前脚本的文件名 /Users/lifei/Desktop/shell-test.sh
echo "$0"
# 输出函数的第 1 个参数 qwertyuiop
echo "$1"
# 输出函数的第 2 个参数 1234
echo "$2"
# 输出函数的第 3 个参数 7890
echo "$3"
# 输出传递给脚本或函数的参数个数 3 
echo "$#"
# 输出传递给脚本或函数的所有参数 qwertyuiop 1234 7890
echo "$*"
# 输出传递给脚本或函数的所有参数 qwertyuiop 1234 7890
echo "$@"
# 输出上个命令的退出状态,或函数的返回值,输出 0 表示上个命令执行成功
echo "$?"
# 输出当前 shell 最后运行的后台 PID,可能为空
echo "$!"
# 输出获取当前shell的进程号(PID),不固定,例如 29488
echo "$$"
# 输出命令 1234 + 1 的执行结果 1235
echo "$(expr $2 + 1)"
# 输出命令 1234 + 1 的执行结果 1235
echo "`expr $2 + 1`"
# 输出 echo mmmmmm 命令的执行结果 mmmmmm
echo "`echo mmmmmm`"
# 输出 my_var 变量的值的长度 7
echo "${#my_var}"
# 输出 88 + 11 的结果 99
echo "$((88 + 11))"
}

# 切换到当前目录下,这样讲脚本拖入终端即可运行
cd "$(dirname "$0")" || exit 0
# 执行函数 myFunc,并传入 3 个参数
MyFunc "qwertyuiop" 1234 7890

字符串截取

使用 # 和 % 可快速的匹配截取字符串,其中 # 去掉左边字符,% 是去掉右边字符,单一符号是最小匹配,两个符号是最大匹配。假设我们定义了一个变量为:

file=/dir1/dir2/dir3/my.file.txt

可以用${ }分别替换得到不同的值:

  • ${file#*/}:删掉第一个 / 及其左边的字符串:dir1/dir2/dir3/my.file.txt
  • ${file##*/}:删掉最后一个 / 及其左边的字符串:my.file.txt
  • ${file#*.}:删掉第一个 . 及其左边的字符串:file.txt
  • ${file##*.}:删掉最后一个 . 及其左边的字符串:txt
  • ${file%/*}:删掉最后一个 / 及其右边的字符串:/dir1/dir2/dir3
  • ${file%%/*}:删掉第一个 / 及其右边的字符串:(空值)
  • ${file%.*}:删掉最后一个 . 及其右边的字符串:/dir1/dir2/dir3/my.file
  • ${file%%.*}:删掉第一个 . 及其右边的字符串:/dir1/dir2/dir3/my
  • ${file:0:5}:提取最左边的5个字节:/dir1
  • ${file:5:5}:提取第5个字节右边的连续5个字节:/dir2
  • ${file/dir/path}:将第一个dir替换为path:/path1/dir2/dir3/my.file.txt
  • ${file//dir/path}:将全部dir替换为path:/path1/path2/path3/my.file.txt

sed 的使用

sed 可以说是最有用的 shell 命令之一了,但在 Mac 上使用 sed 命令还是有一些坑需要注意的。

Mac 强制要求备份

例如有一个 test.txt 的文本文件,我们需要将文本里面所有的 abcd 更换为 xyzw,当我们执行 sed 编辑命令:

# 将文本里面所有的 abcd 更换为 xyzw
sed -i 's/abcd/xyzw/g' test.txt

报错:sed: 1: “test.txt”: undefined label ‘est.txt’。原因是 Mac 强制要求备份,否则报错,解决方案加上一个备份文件名即可。

# 备份文件名任意即可,也可为空,以下写法都可以
sed -i '' 's/abcd/xyzw/g' test.txt
sed -i '.bak' 's/abcd/xyzw/g' test.txt

转义字符 \

例如在 test.txt 的文本文件中搜索以 abcd 开头的行,并在前面加上双斜杠//,当我们执行 sed 编辑命令:

# 搜索所有以 abcd 开头的行,并在前面加上双斜杠//
sed -i "" "s/^abcd///&/g" test.txt

报错:sed: 1: “s/^abcd///&/g”: bad flag in substitute command: ‘/’。原因是 / 发生了转义,我们需要使用 \/ 来进行转义。

# ^abcd 表示以 abcd开头的行,\/ 表示 /,& 表示匹配到字符串,g 表示替换全部
sed -i "" "s/^abcd/\/\/&/g" test.txt

sed 基本使用

sed 模式选择的区别,经常使用的选项是sed -nsed -i,其他的作为了解。

  • -n∶使用安静(silent)模式。在一般 sed 的用法中,所有来自 STDIN的资料一般都会被列出到萤幕上。但如果加上 -n 参数后,则只有经过 sed 特殊处理的那一行(或者动作)才会被列出来;
  • -e∶直接在指令列模式上进行 sed 的动作编辑;
  • -f∶直接将 sed 的动作写在一个档案内, -f filename 则可以执行 filename 内的sed 动作;
  • -r∶sed 的动作支援的是延伸型正规表示法的语法(预设是基础正规表示法语法);
  • -i∶直接修改读取的档案内容,而不是由萤幕输出。

还有常用命令,例如删除命令sed '1d' test表示删除第一行。

  • a∶新增,a 的后面可以接字串,而这些字串会在新的一行出现(目前的下一行);
  • c∶取代,c 的后面可以接字串,这些字串可以取代 n1,n2 之间的行;
  • d∶删除,因为是删除啊,所以 d 后面通常不接任何咚咚;
  • i∶插入, i 的后面可以接字串,而这些字串会在新的一行出现(目前的上一行);
  • p∶列印,亦即将某个选择的资料印出。通常 p 会与参数 sed -n 一起运作;
  • s∶取代,可以直接进行取代的工作哩!通常这个 s 的动作可以搭配正规表示法,例如 1,20s/old/new/g。

使用 test.txt 的文本文件举例。

# 读取 test.txt 文本,并删除第一行,输出剩余的行
sed '1d' test.txt
# 直接编辑 test.txt 文本,并删除第一行
sed -i '' '1d' test.txt
# 读取 test.txt 文本,删除最后一行,输出剩余的行
sed '$d' test.txt
# 读取 test.txt 文本,删除第一行到第五行,输出剩余的行
sed '1,5d' test.txt
# 读取 test.txt 文本,删除第二行到最后一行,输出剩余的行
sed '2,$d' test.txt
# 显示第一行
sed -n '1p' test.txt
# 显示最后一行
sed -n '$p' test.txt
# 显示第一行到第二行
sed -n '1,2p' test.txt
# 显示第二行到最后一行
sed -n '2,$p' test.txt
# 查询并输出包括关键字 abcd 所有所在行
sed -n '/abcd/p' test.txt
# 查询包括关键字 \ 所在所有行,使用反斜线 \ 屏蔽特殊含义
sed -n '/\\/p' test.txt
# 在 1 到 10 行搜索所有以 abcd 开头的行,并在前面加上双斜杠//
sed -i "" "1,10s/^abcd/\/\/&/g" test.txt

其他

shell 使用过程中可能会遇到各种各样的情况,例如需要拷贝至剪贴板等需求,这里只是总结一些常用操作,如下 shell 使用示例。

假设有这样一个需求,逐行读取文件 test.txt 里面的文本,判断是否是空行,如果不是空行,就保存到另外一个文件里面 dst.txt 里面,并在文本前面加上来源行号,最后将 dst.txt 文本文件的所有行拷贝至剪贴板。

#!/bin/sh
# 切换运行环境至当前 shell 文件所在目录
cd "$(dirname "$0")" || exit 0
# 定义当前读取的行为1
row=1
# 逐行读取当前目录下的test.txt
while read -r line
do
row=$((row + 1))
# 判断当前行是否为空行,空行继续
if [ "${line}x" = "x" ]; then
continue
fi
# 保存有字符的行到另外一个文件
echo "${row}:${line}">>dst.txt
done < test.txt

# 将新文本第一行至最后一行拷贝至剪贴板
sed -n '1,$p' dst.txt | pbcopy

如果您觉得有所帮助,请在GitHub Shell上赏个Star ⭐️,您的鼓励是我前进的动力。