4735 字
24 分钟
Pure sh Bible

仅搬运,使用 AI 翻译
以下为原文

另请参阅: pure bash bible (📖 纯 bash 替代外部进程的方法合集)。


pure sh bible

纯 POSIX sh 替代外部进程的方法合集。



这本书的目标是记录仅使用内置 POSIX sh 功能来执行各种任务的常见和鲜为人知的方法。使用此圣经中的代码片段可以帮助从脚本中删除不需要的依赖项,并且在大多数情况下使它们更快。我在开发 KISS Linux 和其他小型项目时发现了这些技巧并发现了一些新的技巧。

下面的代码片段都使用 shellcheck 进行了代码检查。

看到有描述不正确、有缺陷或完全错误的内容?请提交 issue 或发送 pull request。如果圣经缺少某些内容,请提交 issue,我们将找到解决方案。

  • Leanpub 图书: (即将推出)
  • 为我买杯咖啡:

目录#

字符串操作#

从字符串开头删除模式#

示例函数:

Terminal window
lstrip() {
# 用法: lstrip "字符串" "模式"
printf '%s\n' "${1##$2}"
}

使用示例:

Terminal window
$ lstrip "The Quick Brown Fox" "The "
Quick Brown Fox

从字符串末尾删除模式#

示例函数:

Terminal window
rstrip() {
# 用法: rstrip "字符串" "模式"
printf '%s\n' "${1%%$2}"
}

使用示例:

Terminal window
$ rstrip "The Quick Brown Fox" " Fox"
The Quick Brown

删除字符串开头和结尾的空白字符#

这是 sedawkperl 和其他工具的替代方案。下面的函数通过找到所有开头和结尾的空白字符并将其从字符串的开头和结尾删除来工作。

示例函数:

Terminal window
trim_string() {
# 用法: trim_string " example string "
# 删除所有开头的空白字符。
# '${1%%[![:space:]]*}': 删除除开头空白字符以外的所有内容。
# '${1#${XXX}}': 从字符串开头删除空白字符。
trim=${1#${1%%[![:space:]]*}}
# 删除所有结尾的空白字符。
# '${trim##*[![:space:]]}': 删除除结尾空白字符以外的所有内容。
# '${trim%${XXX}}': 从字符串结尾删除空白字符。
trim=${trim%${trim##*[![:space:]]}}
printf '%s\n' "$trim"
}

使用示例:

Terminal window
$ trim_string " Hello, World "
Hello, World
$ name=" John Black "
$ trim_string "$name"
John Black

删除字符串中所有空白字符并截断空格#

这是 sedawkperl 和其他工具的替代方案。下面的函数通过滥用单词分割来创建一个没有开头/结尾空白字符并且空格被截断的新字符串。

示例函数:

Terminal window
# shellcheck disable=SC2086,SC2048
trim_all() {
# 用法: trim_all " example string "
# 禁用通配符匹配,使下面的单词分割安全。
set -f
# 将参数列表设置为按单词分割的字符串。
# 这会删除所有开头/结尾的空白字符并将
# 所有多个空格实例减少为单个空格(" " -> " ")。
set -- $*
# 将参数列表作为字符串打印。
printf '%s\n' "$*"
# 重新启用通配符匹配。
set +f
}

使用示例:

Terminal window
$ trim_all " Hello, World "
Hello, World
$ name=" John Black is my name. "
$ trim_all "$name"
John Black is my name.

检查字符串是否包含子字符串#

使用 case 语句:

Terminal window
case $var in
*sub_string1*)
# 执行某些操作
;;
*sub_string2*)
# 执行其他操作
;;
*)
# 其他情况
;;
esac

检查字符串是否以子字符串开头#

使用 case 语句:

Terminal window
case $var in
sub_string1*)
# 执行某些操作
;;
sub_string2*)
# 执行其他操作
;;
*)
# 其他情况
;;
esac

检查字符串是否以子字符串结尾#

使用 case 语句:

Terminal window
case $var in
*sub_string1)
# 执行某些操作
;;
*sub_string2)
# 执行其他操作
;;
*)
# 其他情况
;;
esac

根据分隔符分割字符串#

这是 cutawk 和其他工具的替代方案。

示例函数:

Terminal window
split() {
# 禁用通配符匹配。
# 这确保单词分割是安全的。
set -f
# 存储 'IFS' 的当前值,以便
# 稍后可以恢复它。
old_ifs=$IFS
# 将字段分隔符更改为我们要
# 分割的内容。
IFS=$2
# 创建一个参数列表,在每个
# '$2' 出现的地方进行分割。
#
# 禁用这个是安全的,因为它只是警告
# 单词分割,这是我们期望的行为。
# shellcheck disable=2086
set -- $1
# 每行打印一个列表值。
printf '%s\n' "$@"
# 恢复 'IFS' 的值。
IFS=$old_ifs
# 重新启用通配符匹配。
set +f
}

使用示例:

Terminal window
$ split "apples,oranges,pears,grapes" ","
apples
oranges
pears
grapes
$ split "1, 2, 3, 4, 5" ", "
1
2
3
4
5

删除字符串中的引号#

示例函数:

Terminal window
trim_quotes() {
# 用法: trim_quotes "字符串"
# 禁用通配符匹配。
# 这使得下面的单词分割安全。
set -f
# 存储 'IFS' 的当前值,以便
# 稍后可以恢复它。
old_ifs=$IFS
# 将 'IFS' 设置为 ["']。
IFS=\"\'
# 创建一个参数列表,在
# ["'] 处分割字符串。
#
# 禁用这个 shellcheck 错误,因为它只
# 警告单词分割,这是我们期望的。
# shellcheck disable=2086
set -- $1
# 将 'IFS' 设置为空白以删除由
# ["'] 删除留下的空格。
IFS=
# 打印去掉引号的字符串。
printf '%s\n' "$*"
# 恢复 'IFS' 的值。
IFS=$old_ifs
# 重新启用通配符匹配。
set +f
}

使用示例:

Terminal window
$ var="'Hello', \"World\""
$ trim_quotes "$var"
Hello, World

文件操作#

解析 key=val 文件#

这可以用于解析简单的 key=value 配置文件。

Terminal window
# 设置 'IFS' 告诉 'read' 在哪里分割字符串。
while IFS='=' read -r key val; do
# 跳过包含注释的行。
# (以 '#' 开头的行)。
[ "${key##\#*}" ] || continue
# '$key' 存储键。
# '$val' 存储值。
printf '%s: %s\n' "$key" "$val"
# 或者用以下内容替换 'printf'
# 使用 '$val' 的值填充名为 '$key' 的变量。
#
# 注意: 我会扩展这个检查以确保 'key' 是
# 一个有效的变量名。
# export "$key=$val"
#
# 带有错误处理的示例:
# export "$key=$val" 2>/dev/null ||
# printf 'warning %s is not a valid variable name\n' "$key"
done < "file"

获取文件的前 N 行#

head 命令的替代方案。

示例函数:

Terminal window
head() {
# 用法: head "n" "文件"
while IFS= read -r line; do
printf '%s\n' "$line"
i=$((i+1))
[ "$i" = "$1" ] && return
done < "$2"
# 在循环中使用的 'read' 将跳过
# 文件的最后一行,如果它不包含
# 换行符而是包含 EOF。
#
# 最后一行迭代被跳过,因为 'read'
# 在遇到 EOF 时以 '1' 退出。但是,'read'
# 仍然会填充变量。
#
# 这确保最后一行在适用时始终被打印。
[ -n "$line" ] && printf %s "$line"
}

使用示例:

Terminal window
$ head 2 ~/.bashrc
# Prompt
PS1='➜ '
$ head 1 ~/.bashrc
# Prompt

获取文件的行数#

wc -l 的替代方案。

示例函数:

Terminal window
lines() {
# 用法: lines "文件"
# '|| [ -n "$line" ]': 这确保以
# EOL 而不是换行符结尾的行仍然
# 在循环中被操作。
#
# 'read' 在看到 EOL 时以 '1' 退出,
# 没有添加的测试,行不会被发送
# 到循环。
while IFS= read -r line || [ -n "$line" ]; do
lines=$((lines+1))
done < "$1"
printf '%s\n' "$lines"
}

使用示例:

Terminal window
$ lines ~/.bashrc
48

统计目录中的文件或目录数量#

这通过将全局匹配的输出传递给函数然后计算参数数量来工作。

示例函数:

Terminal window
count() {
# 用法: count /path/to/dir/*
# count /path/to/dir/*/
[ -e "$1" ] \
&& printf '%s\n' "$#" \
|| printf '%s\n' 0
}

使用示例:

Terminal window
# 计算目录中的所有文件。
$ count ~/Downloads/*
232
# 计算目录中的所有目录。
$ count ~/Downloads/*/
45
# 计算目录中的所有 jpg 文件。
$ count ~/Pictures/*.jpg
64

创建空文件#

touch 的替代方案。

Terminal window
:>file
# 或者(shellcheck 会对此发出警告)
>file

文件路径#

获取文件路径的目录名#

dirname 命令的替代方案。

示例函数:

Terminal window
dirname() {
# 用法: dirname "路径"
# 如果 '$1' 为空,则将 'dir' 设置为 '.',否则为 '$1'。
dir=${1:-.}
# 从字符串末尾删除所有尾随的正斜杠 '/'。
#
# "${dir##*[!/]}": 从字符串开头删除所有非正斜杠,
# 只留下尾随斜杠。
# "${dir%%"${}"}": 从原始字符串末尾删除上述
# 替换的结果(正斜杠字符串)。
dir=${dir%%"${dir##*[!/]}"}
# 如果变量*不*包含任何正斜杠,
# 将其值设置为 '.'。
[ "${dir##*/*}" ] && dir=.
# 删除最后一个正斜杠 '/' *之后*的所有内容。
dir=${dir%/*}
# 再次从字符串末尾删除所有尾随的正斜杠 '/'(见上文)。
dir=${dir%%"${dir##*[!/]}"}
# 打印结果字符串,如果为空,
# 打印 '/'。
printf '%s\n' "${dir:-/}"
}

使用示例:

Terminal window
$ dirname ~/Pictures/Wallpapers/1.jpg
/home/black/Pictures/Wallpapers/
$ dirname ~/Pictures/Downloads/
/home/black/Pictures/

获取文件路径的基本名称#

basename 命令的替代方案。

示例函数:

Terminal window
basename() {
# 用法: basename "路径" ["后缀"]
# 从字符串末尾删除所有尾随的正斜杠 '/'。
#
# "${1##*[!/]}": 从字符串开头删除所有非正斜杠,
# 只留下尾随斜杠。
# "${1%%"${}"}: 从原始字符串末尾删除上述
# 替换的结果(正斜杠字符串)。
dir=${1%${1##*[!/]}}
# 删除最后一个正斜杠 '/' 之前的所有内容。
dir=${dir##*/}
# 如果向函数传递了后缀,则从结果字符串末尾删除它。
dir=${dir%"$2"}
# 打印结果字符串,如果为空,
# 打印 '/'。
printf '%s\n' "${dir:-/}"
}

使用示例:

Terminal window
$ basename ~/Pictures/Wallpapers/1.jpg
1.jpg
$ basename ~/Pictures/Wallpapers/1.jpg .jpg
1
$ basename ~/Pictures/Downloads/
Downloads

循环#

循环遍历(小范围)数字#

seq 的替代方案,仅适用于小范围和静态数字范围。数字列表也可以替换为单词列表、变量等。

Terminal window
# 从 0 到 10 循环。
for i in 0 1 2 3 4 5 6 7 8 9 10; do
printf '%s\n' "$i"
done

循环遍历可变范围的数字#

seq 的替代方案。

Terminal window
# 从 var 到 var 循环。
start=0
end=50
while [ "$start" -le "$end" ]; do
printf '%s\n' "$start"
start=$((start+1))
done

循环遍历文件内容#

Terminal window
while IFS= read -r line || [ -n "$line" ]; do
printf '%s\n' "$line"
done < "file"

循环遍历文件和目录#

不要使用 ls

注意: 当通配符不匹配任何内容(空目录或没有匹配的文件)时,变量将包含未展开的通配符。为了避免处理未展开的通配符,请使用适当的文件条件检查变量中包含的文件的存在性。请注意,符号链接会被解析。

Terminal window
# 贪婪示例。
for file in *; do
[ -e "$file" ] || [ -L "$file" ] || continue
printf '%s\n' "$file"
done
# 目录中的 PNG 文件。
for file in ~/Pictures/*.png; do
[ -f "$file" ] || continue
printf '%s\n' "$file"
done
# 遍历目录。
for dir in ~/Downloads/*/; do
[ -d "$dir" ] || continue
printf '%s\n' "$dir"
done

变量#

根据另一个变量命名变量#

Terminal window
$ var="world"
$ eval "hello_$var=value"
$ eval printf '%s\n' "\$hello_$var"
value

转义序列#

与普遍看法相反,使用原始转义序列没有问题。使用 tput 抽象了与手动打印相同的 ANSI 序列。更糟糕的是,tput 实际上不是可移植的。有许多 tput 变体,每个都有不同的命令和语法(尝试在 FreeBSD 系统上使用 tput setaf 3)。原始序列是可以的。

文本颜色#

注意: 需要 RGB 值的序列仅在真彩色终端仿真器中有效。

序列作用
\033[38;5;<NUM>m设置文本前景色。0-255
\033[48;5;<NUM>m设置文本背景色。0-255
\033[38;2;<R>;<G>;<B>m将文本前景色设置为 RGB 颜色。R, G, B
\033[48;2;<R>;<G>;<B>m将文本背景色设置为 RGB 颜色。R, G, B

文本属性#

序列作用
\033[m重置文本格式和颜色。
\033[1m粗体文本。
\033[2m淡化文本。
\033[3m斜体文本。
\033[4m下划线文本。
\033[5m慢闪烁。
\033[7m交换前景和背景颜色。
\033[8m隐藏文本。
\033[9m删除线文本。

光标移动#

序列作用
\033[<LINE>;<COLUMN>H将光标移动到绝对位置。line, column
\033[H将光标移动到起始位置(0,0)。
\033[<NUM>A将光标向上移动 N 行。num
\033[<NUM>B将光标向下移动 N 行。num
\033[<NUM>C将光标向右移动 N 列。num
\033[<NUM>D将光标向左移动 N 列。num
\033[s保存光标位置。
\033[u恢复光标位置。

擦除文本#

序列作用
\033[K从光标位置擦除到行尾。
\033[1K从光标位置擦除到行首。
\033[2K擦除整个当前行。
\033[J从当前行擦除到屏幕底部。
\033[1J从当前行擦除到屏幕顶部。
\033[2J清屏。
\033[2J\033[H清屏并将光标移动到 0,0

参数扩展#

前缀和后缀删除#

参数作用
${VAR#PATTERN}从字符串开头删除模式的最短匹配。
${VAR##PATTERN}从字符串开头删除模式的最长匹配。
${VAR%PATTERN}从字符串末尾删除模式的最短匹配。
${VAR%%PATTERN}从字符串末尾删除模式的最长匹配。

长度#

参数作用
${#VAR}变量的字符长度。

默认值#

参数作用
${VAR:-STRING}如果 VAR 为空或未设置,使用 STRING 作为其值。
${VAR-STRING}如果 VAR 未设置,使用 STRING 作为其值。
${VAR:=STRING}如果 VAR 为空或未设置,将 VAR 的值设置为 STRING
${VAR=STRING}如果 VAR 未设置,将 VAR 的值设置为 STRING
${VAR:+STRING}如果 VAR 不为空,使用 STRING 作为其值。
${VAR+STRING}如果 VAR 已设置,使用 STRING 作为其值。
${VAR:?STRING}如果为空或未设置则显示错误。
${VAR?STRING}如果未设置则显示错误。

条件表达式#

用于 [ ] if [ ]; thentest

文件条件#

表达式作用
-bfile如果文件存在且是块特殊文件。
-cfile如果文件存在且是字符特殊文件。
-dfile如果文件存在且是目录。
-efile如果文件存在。
-ffile如果文件存在且是常规文件。
-gfile如果文件存在且设置了 set-group-id 位。
-hfile如果文件存在且是符号链接。
-pfile如果文件存在且是命名管道(FIFO)。
-rfile如果文件存在且可读。
-sfile如果文件存在且大小大于零。
-tfd如果文件描述符已打开且指向终端。
-ufile如果文件存在且设置了 set-user-id 位。
-wfile如果文件存在且可写。
-xfile如果文件存在且可执行。
-Lfile如果文件存在且是符号链接。
-Sfile如果文件存在且是套接字。

变量条件#

表达式作用
-zvar如果字符串长度为零。
-nvar如果字符串长度非零。

变量比较#

表达式作用
var = var2等于。
var != var2不等于。
var -eq var2等于(代数比较)。
var -ne var2不等于(代数比较)。
var -gt var2大于(代数比较)。
var -ge var2大于或等于(代数比较)。
var -lt var2小于(代数比较)。
var -le var2小于或等于(代数比较)。

算术运算符#

赋值#

运算符作用
=初始化或更改变量的值。

算术运算#

运算符作用
+加法
-减法
*乘法
/除法
%取模
+=加等于(增加变量
-=减等于(减少变量
*=乘等于(乘以变量
/=除等于(除以变量
%=模等于(变量除法的余数

位运算#

运算符作用
<<按位左移
<<=左移等于
>>按位右移
>>=右移等于
&按位与
&=按位与等于
|按位或
|=按位或等于
~按位非
^按位异或
^=按位异或等于

逻辑运算#

运算符作用
!
&&
||

其他运算#

运算符作用示例
,逗号分隔符((a=1,b=2,c=3))

算术运算#

三元测试#

Terminal window
# 如果 var2 大于 var,将 var 的值设置为 var2。
# 'var2 > var': 要测试的条件。
# '? var2': 如果测试成功。
# ': var': 如果测试失败。
var=$((var2 > var ? var2 : var))

检查数字是否为浮点数#

示例函数:

Terminal window
is_float() {
# 用法: is_float "数字"
# 测试检查输入是否包含
# '.'。这会过滤掉整数。
[ -z "${1##*.*}" ] &&
printf %f "$1" >/dev/null 2>&1
}

使用示例:

Terminal window
$ is_float 1 && echo true
$
$ is_float 1.1 && echo true
$ true

检查数字是否为整数#

示例函数:

Terminal window
is_int() {
# 用法: is_int "数字"
printf %d "$1" >/dev/null 2>&1
}

使用示例:

Terminal window
$ is_int 1 && echo true
$ true
$ is_int 1.1 && echo true
$

陷阱#

陷阱允许脚本在各种信号上执行代码。在 pxltrm用 bash 编写的像素艺术编辑器)中,陷阱用于在窗口大小调整时重绘用户界面。另一个用例是在脚本退出时清理临时文件。

应该在脚本开始附近添加陷阱,这样任何早期错误也会被捕获。

在脚本退出时执行某些操作#

Terminal window
# 在脚本退出时清屏。
trap 'printf \\033[2J\\033[H\\033[m' EXIT
# 在脚本退出时运行函数。
# 'clean_up' 是函数的名称。
trap clean_up EXIT

忽略终端中断 (CTRL+C, SIGINT)#

Terminal window
trap '' INT

过时语法#

命令替换#

使用 $() 而不是 ` `

Terminal window
# 正确。
var="$(command)"
# 错误。
var=`command`
# $() 可以轻松嵌套,而 `` 不能。
var="$(command "$(command)")"

内部和环境变量#

打开用户首选的文本编辑器#

Terminal window
"$EDITOR" "$file"
# 注意: 这个变量可能为空,设置一个后备值。
"${EDITOR:-vi}" "$file"

获取当前工作目录#

这是 pwd 内置命令的替代方案。

Terminal window
"$PWD"

获取当前 shell 的 PID#

"$"

获取当前 shell 选项#

"$-"

后记#

感谢阅读!如果这本圣经以任何方式帮助了您,并且您想要回报,请考虑捐赠。捐赠给了我时间来使这成为最好的资源。不能捐赠?没关系,给仓库加星并与您的朋友分享吧!

继续摇滚。🤘

附原许可证:

The MIT License (MIT)
Copyright (c) 2019 Dylan Araps
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Pure sh Bible
https://github.com/dylanaraps/pure-sh-bible
作者
Dylan Araps
发布于
2025-10-31
许可协议
MIT