#!/ bin / sh与#!/ bin / bash相比具有最大的可移植性


36

我通常使用Ubuntu LTS服务器从我了解的符号链接的工作/bin/sh/bin/dash。通过符号链接/bin/sh到其他许多发行版本/bin/bash

从中我了解到,如果脚本#!/bin/sh在最上面使用,它可能不会在所有服务器上以相同的方式运行吗?

当人们希望这些脚本在服务器之间具有最大的可移植性时,是否建议使用哪种外壳用于脚本?


各种外壳之间略有不同。如果可移植性对您来说最重要,那么请使用#!/bin/sh并且不要使用原始外壳程序所提供的功能。
托尔比约恩Ravn的安德森

Answers:


65

Shell脚本的可移植性大约有四个级别(就shebang行而言):

  1. 最便于移植:使用#!/bin/shshebang并使用POSIX标准中指定的基本shell语法。这几乎可以在任何POSIX / unix / linux系统上运行。(好吧,除了Solaris 10和更早的版本具有真正的旧版Bourne shell,它早于POSIX就不兼容/bin/sh)。

  2. 第二个最轻便:用一个#!/bin/bash(或#!/usr/bin/env bash)家当线,并坚持bash的V3功能。这将在具有bash(在预期位置)的任何系统上工作。

  3. 第三大可移植性:使用#!/bin/bash(或#!/usr/bin/env bash)shebang行,并使用bash v4功能。在具有bash v3的任何系统上(例如macOS,出于许可原因必须使用它),该操作都将失败。

  4. 最少可移植:使用#!/bin/shshebang并使用bash扩展来扩展POSIX shell语法。在/ bin / sh的bash以外的任何系统(例如最新的Ubuntu版本)上,这将失败。永远不要这样做;这不仅是兼容性问题,而且是完全错误的。不幸的是,这是很多人犯的错误。

我的建议:使用前三个中最保守的那个,它提供了脚本所需的所有shell功能。为了获得最大的可移植性,请使用选项#1,但是以我的经验,一些bash功能(如数组)非常有用,因此我将选择#2。

您能做的最坏的事情是#4,使用错误的shebang。如果您不确定哪些基本功能是POSIX,哪些功能是bash扩展,请坚持使用bash shebang(即选项#2),或者使用非常基本的shell(如Ubuntu LTS服务器上的破折号)彻底测试脚本。Ubuntu Wiki上有很多值得关注的bashisms列表

在Unix和Linux问题“ sh兼容是什么意思?”中,有一些关于外壳的历史和差异的很好的信息以及Stackoverflow问题“ sh和bash之间的差异”

另外,请注意,shell并不是唯一一个在不同系统之间有所不同的事物。如果您习惯使用Linux,那么您就会习惯于GNU命令,这些命令具有许多非标准扩展名,这些扩展名在其他unix系统(例如bsd,macOS)上可能找不到。不幸的是,这里没有简单的规则,您只需要知道所使用命令的变化范围即可。

就可移植性而言,最讨厌的命令之一是最基本的命令之一:echo。每当您将它与要打印的字符串中的任何选项(例如echo -necho -e)或任何转义符(反斜杠)一起使用时,不同的版本都会做不同的事情。任何时候您要打印不带换行符的字符串,或者要在字符串中使用转义符的情况下,请printf改用(并了解其工作原理-比实际echo情况要复杂得多)。该ps命令也很混乱

另一个需要注意的一般事项是命令选项语法的最新/ GNUish扩展:旧(标准)命令格式是命令后跟选项(带有单破折号,每个选项为单个字母),后跟命令参数。最新的(通常是非便携式的)变体包括长选项(通常在中引入--),允许--选项位于参数之后,并用于将选项与参数分开。


12
第四个选择是一个错误的想法。请不要使用它。
pabouk

3
@pabouk我完全同意,因此我编辑了答案以使这一点更加清楚。
戈登·戴维森

1
您的第一句话有些误导。POSIX标准未对shebang进行任何指定,外部告诉使用它会导致未指定的行为。此外,POSIX并未指定posix外壳程序必须位于的位置,仅指定其名称(sh),因此/bin/sh不能保证它是正确的路径。因此,最可移植的是根本不指定任何shebang,也不是要使shebang适应所使用的操作系统。
jlliagre

2
我最近用我的剧本跌入第四名,只是不知道为什么它不起作用。毕竟,当我直接在shell中尝试这些命令时,相同的命令就可以正常工作,并且完全按照我希望它们执行的操作。只要我换#!/bin/sh#!/bin/bash虽然,脚本完美。(为我的辩护,该脚本随着时间的推移从实际上只需要sh-ism演变为一个依赖于bash行为的脚本。)
CVn

2
@(MichaelKjörling)(true)Bourne外壳几乎从未与Linux发行版捆绑在一起,而且也不符合POSIX。POSIX标准外壳程序是根据ksh行为而不是Bourne 创建的。遵循FHS的大多数Linux发行版都具有/bin/sh指向它们选择提供POSIX兼容性的shell的符号链接,通常是bashdash
jlliagre

5

./configure准备用于构建TXR语言的脚本中,我写了以下序言以提高可移植性。即使#!/bin/sh是不符合POSIX的旧Bourne Shell ,脚本也将自行引导。(我在Solaris 10 VM上构建每个发行版)。

#!/bin/sh

# use your own variable name instead of txr_shell;
# adjust to taste: search for your favorite shells

if test x$txr_shell = x ; then
  for shell in /bin/bash /usr/bin/bash /usr/xpg4/bin/sh ; do
    if test -x $shell ; then
       txr_shell=$shell
       break
    fi
  done
  if test x$txr_shell = x ; then
    echo "No known POSIX shell found: falling back on /bin/sh, which may not work"
    txr_shell=/bin/sh
  fi
  export txr_shell
  exec $txr_shell $0 ${@+"$@"}
fi

# rest of the script here, executing in upgraded shell

这里的想法是,我们找到一个比正在运行的外壳更好的外壳,然后使用该外壳重新执行脚本。在txr_shell环境变量设置,使重新执行脚本知道它是重新执行递归实例。

(在我的脚本中,txr_shell变量也随后用于两个目的:首先,将其作为信息输出的一部分打印在脚本的输出中。其次,将其作为SHELL变量安装在中Makefile,因此make将使用此变量也可以执行食谱。)

/bin/sh破折号的系统上,您可以看到上述逻辑将找到/bin/bash并重新执行该脚本。

在Solaris 10机器上,/usr/xpg4/bin/sh如果未找到Bash ,则将启动。

序言是用保守的Shell方言编写的,test用于文件存在性测试,以及${@+"$@"}用于扩展参数的技巧,以适应一些残破的旧Shell("$@"如果我们使用的是POSIX兼容Shell ,则可能只是这样)。


人们可能并不需要x,如果适当的引用都在使用,因为该规定习语环绕现在不推荐使用测试调用与情形两轮牛车-a或者-o结合多种测试。
查尔斯·达菲

@CharlesDuffy确实;在test x$whatever我正在那里犯下看起来像清漆的洋葱。如果我们不能相信破旧的外壳来进行报价,那么最后的${@+"$@"}尝试是没有意义的。
卡兹(Kaz)

2

与现代脚本语言(例如Perl,Python,Ruby,node.js甚至是Tcl)相比,Bourne Shell语言的所有变体在客观上都非常糟糕。如果您需要做的事情甚至有些复杂,那么从长远来看,如果您使用上述方法之一而不是Shell脚本,那么您会感到更快乐。

唯一的一个优势的shell语言仍然有对这些新的语言是什么自称/bin/sh是保证在任何宣称是Unix的存在。但是,有些东西甚至可能不符合POSIX。许多旧有的专有Unix /bin/sh在Unix95要求进行更改之前(是二十年前的Unix95,并且还在不断增加),在默认PATH中冻结了由PATH 实现的语言和实用程序。可能有一组Unix95,或者甚至是POSIX.1-2001,如果幸运的话,这些工具不在默认PATH(例如/usr/xpg4/bin)上的目录中,但不能保证它们存在。

但是,与Bash相比,在任意选择的Unix安装中更可能出现Perl的基础知识。(通过“ Perl的基础知识”,我的意思是/usr/bin/perl存在并且是Perl 5的某些版本,也许是很旧的版本;如果您很幸运,该版本的解释器附带的模块集也可用。)

因此:

如果您要编写的东西所有看起来都是Unix的地方都可以使用(例如“ configure”脚本),则需要使用#! /bin/sh,而无需使用任何扩展名。如今,在这种情况下,我将编写兼容POSIX.1-2001的外壳,但是如果有人要求支持生锈的铁,我将准备修补POSIXism。

但是,如果您没有编写必须在所有地方都可以使用的东西,那么一旦您想使用所有Bashism,就应该停下来,并用更好的脚本语言重写整个内容。您未来的自我会感谢您。

(因此,当它适合使用猛砸扩展第一顺序:从未二阶:只延长猛砸交互式环境-例如,提供智能标签完成和花哨的提示?。)


前几天,我不得不写一个脚本,做类似的事情find ... -print0 |while IFS="" read -rd "" file; do ...; done |tar c --null -T -。没有bashisms(read -d "")和GNU扩展(find -print0tar --null),将无法使其正常工作。在Perl中重新实现该行会更长并且更麻烦。当使用bashisms和其他非POSIX扩展名是正确的事情时,就是这种情况。
michau

@michau取决于这些省略号中的内容,它可能不再符合单线形式,但是我认为您并没有像我所说的那样使用“用更好的脚本语言重写整个内容 ”。我的方式看起来像perl -MFile::Find -e 'our @tarcmd = ("tar", "c"); find(sub { return unless ...; ...; push @tarcmd, $File::Find::name }, "."); exec @tarcmd注意,您不仅不需要任何bashisms,而且不需要任何GNU tar扩展。(如果担心命令行长度,或者您希望扫描和tar并行运行,那么您确实需要tar --null -T。)
zwol

问题是,find在我的方案中,返回的文件名超过50,000,因此您的脚本很可能会达到参数长度限制。一个不使用非POSIX扩展名的正确实现必须以增量方式生成tar输出,或者在Perl代码中使用Archive :: Tar :: Stream。当然可以做到,但是在这种情况下,Bash脚本的编写速度要快得多,样板更少,因此存在的漏洞更少。
michau

顺便说一句,我可以在一个快速和简洁的方式使用Perl,而不是击:find ... -print0 |perl -0ne '... print ...' |tar c --null -T -。但问题是:1)我使用find -print0tar --null无论如何,有啥避免bashism点read -d "",这是完全一样的那种东西(GNU扩展用于处理空分离,不像Perl中,可能会成为一个POSIX的事情总有一天)?2)Perl真的比Bash更加普及吗?我最近一直在使用Docker映像,其中很多都有Bash但没有Perl。与在古代Unices上相比,新脚本更有可能在该处运行。
michau
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.