智能营销笔记本服务商

营销笔记本+万能采集+AI名片+智能电销+短信群发=同步管理

免费咨询热线: 15064770313

编写高度可移植和可移植的shell脚本的技术总结

介绍

本文是我通过开发ShellSpec获得的技术的总结,ShellSpec 是一个用于 shell 脚本的 BDD 测试框架,用于编写可移植和可移植的 POSIX 兼容的 shell 脚本,以及我相关文章的链接集合。如果你写了一篇相关的新文章,我会从这里链接,所以我认为在你入库后立即收藏这个页面是个好主意。智程网络科技_智能营销笔记本软件开发_大数据营销笔记本系统定制_营销软件-曲阜市智程网络科技有限公司

对于那些出于更改通知目的而存储本文的人,我们
将停止定期更新本文,因为文章数量增加并且链接集合的维护变得困难。如果您需要更新通知,请关注我。大多数文章都与shell脚本有关,所以我认为这足以达到目的。

问答

为什么要写在 shell 脚本中?

Shell脚本具有其他语言所没有的独特优势。这意味着该脚本可以在许多环境中开箱即用。无需针对目标操作系统进行编译,也无需为每种语言安装执行环境或库。只要你有一个shell,它就可以在Linux、macOS、BSD、Unix、Windows(WSL、Cygwin、MSYS)的任何环境下工作。(作为证据,ShellSpec 具有并行执行、随机执行和分析器等功能,但适用于最小配置,例如 Docker 的 busybox 映像和旧环境,例如 Debian 2.2 的 bash 2.03。)

为什么它符合 POSIX?

“POSIX 合规性”是一种工具,而不是目的。真正的目的是让 shell 脚本在更多的环境中工作。如果您只使用 POSIX 标准化功能,您可以期望它在未知环境中工作。

让它在任何地方工作都很难吗?

说清楚,这很难。事实上,仅靠 POSIX 合规性不足以使其真正意义上的在任何地方都能正常工作。shell中也存在实现差异和错误。您不能使用可与 bash 等一起使用的方便数组,并且符合 POSIX 的命令具有最少的功能。最重要的是,缺乏吸收它们的图书馆。这就像过去的 JavaScript 世界。如果你在没有任何线索的情况下继续前进,你将首先放弃。这就是为什么这篇文章是关于。

它真的在任何地方都有效吗?你有测试吗?

一般来说,这些技术都是基于ShellSpec的实现技术,除了一些之外,都是在与ShellSpec相同的环境下进行测试的。(有时候写文章的时候会出错或者发现bug……我会修复一切的。)ShellSpec的测试环境目前是“bash≧2.03”和“bosh≧2018/”。10/ 17”“busybox≧1.10.2”“dash≧0.5.2”“ksh≧93s”“mksh≧28”“posh≧0.3.14”“yash≧2.30”“zsh≧3.1.9”,OS Linux(Debian 2.2 或更高版本)、Windows(WSL、Cygwin、MSYS 基本上只有最新版本)、macOS(10.10 或更高版本)、FreeBSD(10.x 或更高版本)。不幸的是,我无法测试 UNIX,因为我没有处理它的环境。(我只是在测试 Solaris,但我有点担心,因为我只是不时手动进行,而不是 CI。)

编写高度可移植和可移植的 shell 脚本的技术

吸收兼容性差异的技术

把这篇文章做成一个链接集合是一种浪费,所以我将向你展示如何吸收我不会在其他文章中介绍的兼容性差异。

当我编写一个可移植的 shell 脚本时,我会按以下顺序决定使用哪些工具。

  1. 如果只能通过shell的功能实现,就只用shell实现

  2. 如果出于功能或性能原因难以在 shell 中实现,请使用 POSIX 指定的命令(及其选项)。

  3. 如果仍然困难,请使用特定于环境的 shell 函数和命令,但此时创建一个吸收兼容性差异的函数。

我写的大部分文章基本上都是只在shell中实现或者只使用符合POSIX的命令,所以我认为没有很多地方可以解释如何吸收3中的兼容性差异。所以我想在这里介绍一下。

示例统计

stat不是 POSIX 指定的命令,但它可以在 Linux 和 macOS (BSD) 上使用。但是,规格(选项)非常不同。例如,如何获取文件大小。

# ファイルサイズの取得 stat -c %s /etc/hosts # Linux stat -f %z /etc/hosts # macOS (BSD)

以此为例,我将向您展示如何吸收兼容性。首先,为每个定义一个函数。

filesize() { # Linux   stat -c %s "$1" }  filesize() { # macOS (BSD)   stat -f %z "$1" }

1.吸收兼容性的功能

要吸收兼容性,请确定您当前的环境并定义这两个功能的适当性。uname它是否包含“使用”来确定环境LinuxDarwin是否包括(macOS)?您可能会想出一种划分方法,但我不建议这样做。即便如此,也可以使用DarwinLinux 版本(GNU 版本) 。stat(Homebrew 实际上可以做到这一点。)

最好的方法是实际执行命令并根据其行为来确定。具体可以根据stat情况来判断。stat -f /该选项与Linux 上--file-system的选项含义相同,返回根文件系统的状态。在 macOS (BSD) 中,它-f是一个格式字符串,因此它/按原样返回字符串。

# Linux $ stat -f /   File: "/"     ID: f148a0b23b752507 Namelen: 255     Type: ext2/ext3 Block size: 4096       Fundamental block size: 4096 Blocks: Total: 122661614  Free: 116726932  Available: 110478625 Inodes: Total: 31227904   Free: 30595983  # macOS (BSD) $ stat -f / /

通过这种方式,可以利用行为差异来分离要定义的功能。(由于它很长,所以函数应该在一行上。)

if [ "$(stat -f /)" = / ]; then   filesize() { stat -f %z "$1"; } # macOS (BSD) else   filesize() { stat -c %s "$1"; } # Linux fi

需要注意的是,两者都使用已经定义行为并且运行无害的选项。从“此选项仅在 Linux 上可用”这一事实来看,它可能稍后会在 macOS 上实现并停止工作。(不言而喻,使用无害选项是有原因的。)我认为您通常可以通过搜索找到可以使用的选项,但是如果您没有长选项,--version--help可以相对安心地使用它们. --version--help不会将它用于任何其他目的。

2.延迟定义

以下是如何在“首次使用时”定义此功能。

filesize() {   if [ "$(stat -f /)" = / ]; then     filesize() { stat -f %z "$1"; } # macOS (BSD)   else     filesize() { stat -c %s "$1"; } # Linux   fi    # 初回はここで定義した関数を呼び出す。次回からは直接定義した関数が使用される。   filesize "$@"  }

这种方法的优点是命令判别的时机可以延迟到实际使用时的时机,所以在不需要的时候可以降低判别成本,不使用(全局)也可以进行复杂的判别处理变量,表示可以使用参数来完成。这是一个无意义的例子,但没有使用全局变量。

filesize() {   # 位置パラメータを変数の代わりとして使うことで   # ret=$(stat -f /) のようなグローバル変数(ret)の使用を避けることが出来る   set -- "$(stat -f /)" "$@"    if [ "$1" = / ]; then     filesize() { stat -f %z "$1"; } # macOS (BSD)   else     filesize() { stat -c %s "$1"; } # Linux   fi   shift   filesize "$@"  }

但是,请记住,每次使用子外壳时都会定义此方法。

size1=$(filesize "file1") # サブシェルが使われてるので定義しても破棄される size2=$(filesize "file2") # よってここでもまた定義される filesize "file3" # サブシェルを使っていないので以降は定義された状態が続く

为避免这种情况,请更改read -r line函数的接口,以便将返回值返回给参数中指定的名称的变量。filesize

size1='' # 参考 shellcheck の未定義変数の参照という警告を騙す方法 filesize size1 "file1" # サブシェルではないので定義されたまま echo "$size1" # ファイルサイズは size1 変数に代入されている

它是一种实现。

filesize() {   if [ "$(stat -f /)" = / ]; then     filesize() { eval "$1=\$(stat -f %z \"\$2\")"; } # macOS (BSD)   else     filesize() { eval "$1=\$(stat -c %s \"\$2\")"; } # Linux   fi   filesize "$@"  }

将返回值返回给参数指定的变量的技术具有广泛的应用,因此最好记住。它不使用子shell,因此具有出色的性能。但是eval,请注意不要在变量名等中包含意外字符,因为它已被使用。它可能会导致漏洞。

3. 避免在函数中重新定义

“2.延迟定义”通常适用于所有shell,但在极少数情况下,如果您在函数中使用相同的函数名重新定义,shell可能会出现故障(错误?)。在那种状态下,shell 会掉下来,或者应该定义的函数会消失。在 ksh 和 posh 中很少出现,但似乎结合了某些条件,具体条件未知。

这种情况下,你应该放弃延迟,使用“1.实现”,但如果判断条件复杂,你会想要使用变量。但是,当您不想使用全局变量时,它是一种实现方法。

filesize() { # 1 段目   set -- "$(stat -f /)"   [ "$1" = / ] && return 0   return 1 } if filesize; then # 2 段目   filesize() { eval "$1=\$(stat -f %z \"\$2\")"; } # macOS (BSD) else   filesize() { eval "$1=\$(stat -c %s \"\$2\")"; } # Linux fi  # 参考 環境が複数ある場合 filesize &&: case $? in # 2 段目   0) filesize() { eval "$1=\$(stat -f %z \"\$2\")"; } ;; # macOS (BSD)   1) filesize() { eval "$1=\$(stat -c %s \"\$2\")"; } ;; # Linux esac

它有两个具有相同功能名称的角色。第一阶段filesize使用函数来执行环境确定处理。目标函数定义在第二行。总的来说,我觉得重用同一个函数名是个好主意,但是第一行是临时函数,只是因为不使用全局变量而使用,本来就没有必要,所以这样就好了。

提醒一下,仅此一项可能无法解决故障。在ksh中很少发生,“1.我在顶层定义了一个函数。” “2.我在顶层调用函数。” “3.我在重新定义函数。如果“”的所有条件满意,可能会出现无意义的问题。(它并不总是发生。)在这种情况下,eval将其用作解决方法以防止满足 2. 的条件。具体来说,改写如下。

# filesize &&: # ↓ eval "filesize &&:" &&:  # トップレベルで関数を定義・呼び出しをしなければ良いので # すべての処理を以下のように関数内に入れても良い initialize() {   ...   filesize &&:   ... } initialize

&&:有两种解决方法会导致set -emksh 在启用时退出。eval

最后

创建一个 POSIX 兼容的 shell 脚本可以很容易地在很多地方运行。但这是很多工作。也许第一个原因是没有库来吸收兼容性?即使在浏览器和 JavaScript 的世界里,现在都说 jQuery 已被删除,但起初我记得随着 Prototype.js 和 jQuery 等吸收兼容性的库的出现,它发生了显着变化。如果每个开发人员都被 shell 和命令的兼容性、陷阱和错误所左右,那么 shell 脚本就不能用于舒适的开发。其目的不是分享糟糕的技术诀窍,而是创造一个不需要糟糕的技术诀窍的世界。.可移植性和可移植性可以通过加载库提供的功能实现,无需考虑细节。我希望这样的趋势会来到 shell 脚本的世界。

在我看来,到目前为止,测试 shell 脚本的“库”一直很困难。有测试框架,但它们用于使用 shell 脚本编写和执行测试(用于外部命令),而不是用于测试 shell 脚本本身(尽管并非不可能)。这很难)。但是现在我们有了 ShellSpec。它是一个针对 shell 脚本本身和库进行了优化的测试框架。很多库都会从这里诞生,重新认识shell脚本的用处,最终强化shell本身(bash希望etc.之类的扩展能够标准化),用shell脚本开发会更容易。您可能认为它可以用另一种语言实现,但没有人可以想象可以在任何地方使用的标准 shell 消失的未来,对吧?无论如何,如果您使用它,最好是舒适的。