适用于Mac(BSD)和Linux的sed原地标志。

335

有没有一种在Linux和Mac上都能够进行原地编辑而不备份的sed调用方法?虽然OS X附带的BSD sed似乎需要sed -i '' …,但通常随Linux发行版一起提供的GNU sed会将引号解释为空输入文件名(而不是备份扩展名),因此需要使用sed -i …

是否有任何命令行语法可以在两种系统上同时使用,以便我可以在两个系统上使用相同的脚本?


Perl不是一个选项吗?你必须使用sed吗? - Noufal Ibrahim
也许安装GNU版本并使用它!http://www.topbug.net/blog/2013/04/14/install-and-use-gnu-command-line-tools-in-mac-os-x/? 我会先尝试这个。但再说,那也有点糟糕。 - Dmitry Minkovsky
3
对于浏览此问题并有个人脚本无法在他们的OS X机器上运行的其他人来说,这可能是有趣的。尽管如此,在我的情况下,我需要它用于一个开源项目的构建系统,告诉用户先安装GNU sed将会违背原始目的(以“无处不在”的方式修补几个文件)。 - dnadlinger
@klickverbot 是的,很有道理。我最初将评论添加为答案,然后删除了它,意识到它不是你问题的答案 :)。 - Dmitry Minkovsky
2
交叉参考:Unix & Linux Stack Exchange 上的问题 如何使用 sed -i(原地编辑)实现可移植性? - MvG
显示剩余2条评论
15个回答

319

如果你真的想要用 sed -i 来达到“简单”的目的,以下方法在 GNU 和 BSD/Mac 的 sed 都可行:

sed -i'' 's/foo/bar/g' file.txt
sed -i.bak 's/foo/bar/' filename

注意没有空格和句点。

证明:

# GNU sed
% sed --version | head -1
GNU sed version 4.2.1
% echo 'foo' > file
% sed -i.bak 's/foo/bar/' ./file
% ls
file  file.bak
% cat ./file
bar

# BSD sed
% sed --version 2>&1 | head -1
sed: illegal option -- -
% echo 'foo' > file
% sed -i.bak 's/foo/bar/' ./file
% ls
file  file.bak
% cat ./file
bar

显然,你只需要删除.bak文件。


3
这是正确的方式。添加一些字符,就可以使其变得便携。删除备份文件很简单(当调用“sed”时已有文件名)。 - salezica
2
值得注意的是,在macOS Sierra上(以及可能更早的版本,我手头没有机器),当使用-i调用时,sed不接受位置参数command —— 必须提供另一个标志-e。因此,命令变成了:sed -i.bak -e 's/foo/bar/' filename - ELLIOTTCABLE
7
这将创建备份,而原帖的要求是对原文件进行就地编辑。 - Marc-André Lafortune
21
完整的解决方案是:sed -i.bak 's/foo/bar/' 文件 && rm 文件.bak - joeytwiddle
1
请注意,点号并不重要。您可以说“sed -ibak 's/foo/bar/' kinefile”,它会创建kinefilebak而不是kinefile.bak。只要您使用rm $1bak而不是rm $1.bak,它就可以正常工作。(当然,xyz.bak文件在ls列表中更加明显,*.bak*bak更自然,如果文件名在$f中,则必须说"${f}bak"而不是更简单的"$f.bak")。 - G-Man Says 'Reinstate Monica'
显示剩余3条评论

133

这个在 GNU sed 上可以工作,但在 OS X 上不行:

sed -i -e 's/foo/bar/' target.file
sed -i'' -e 's/foo/bar/' target.file

这在 OS X 上可行,但在 GNU sed 上不行:

sed -i '' -e 's/foo/bar/' target.file

在OS X上,您

  • 不能使用 sed -i -e ,因为备份文件的扩展名将被设置为 -e
  • 不能使用 sed -i'' -e ,原因相同-它需要在 -i ''之间加上一个空格。

1
@AlexanderMills 告诉 sed 下一个参数是一个命令 - 这里也可以省略。 - Benjamin W.
15
请注意,-i-i''在Shell解析时是相同的;因为sed获得了完全相同的参数,所以它无法在这两个最初的调用上表现出不同的行为。 - wchargin

53

在OSX上,我总是通过Homebrew安装GNU sed版本,以避免在脚本中出现问题,因为大多数脚本都是为GNU sed版本编写的。

brew install gnu-sed --with-default-names

然后您的BSD sed将被GNU sed替换。

或者,您可以安装没有default-names选项,但是:

  • 在安装gnu-sed后,请根据说明更改您的PATH
  • 在您的脚本中检查并选择使用gsedsed,具体取决于您的系统。

5
--with-default-names已从Homebrew-Core中删除,更多信息请参见答案。现在在安装gnu-sed时,安装说明指定您需要将gnubin添加到您的PATH中:PATH="/usr/local/opt/gnu-sed/libexec/gnubin:$PATH" - pgericson
如果您想在Mac工具的相同名称之前将它们添加到路径中,则主页为homebrew包的页面--https://formulae.brew.sh/formula/coreutils--建议将以下内容添加到您的`bashrc`:`PATH="$(brew --prefix)/opt/coreutils/libexec/gnubin:$PATH"`。 - Alex Hall

29
Noufal Ibrahim所问,为什么不能使用Perl呢?任何一台Mac都会有Perl,并且很少有Linux或BSD发行版在基本系统中不包括某个版本的Perl。实际上可能缺少Perl的仅有环境之一是BusyBox(对于-i,它的工作方式类似于GNU/Linux,但无法指定备份扩展名)。
正如ismail所建议的,

由于perl无处不在,我只需执行perl -pi -e s,foo,bar,g target.file

在几乎所有情况下,这似乎比脚本、别名或其他解决方案更好地处理sed -i在GNU/Linux和BSD/Mac之间的根本不兼容性。

我喜欢这个解决方案。自2009年5月1日起,perl已成为Linux标准基础规范的一部分。http://refspecs.linuxfoundation.org/LSB_4.0.0/LSB-Languages/LSB-Languages/perllocation.html - OregonTrail
2
这绝对是可移植性最好的解决方案。 - ecnepsnai

22

以下是另一个版本,可在Linux和macOS上运行,无需使用eval,也无需删除备份文件。它使用Bash数组存储sed参数,这比使用eval更加清晰:

# Default case for Linux sed, just use "-i"
sedi=(-i)
case "$(uname)" in
  # For macOS, use two parameters
  Darwin*) sedi=(-i "")
esac

# Expand the parameters in the actual call to "sed"
sed "${sedi[@]}" -e 's/foo/bar/' target.file

这不会创建一个备份文件,也不会创建一个带引号的附加文件。


4
这是正确的答案。我会稍微简化它,但这个想法完美地发挥了作用。sedi=(-i) && [ "$(uname)" == "Darwin" ] && sedi=(-i '')sed "${sedi[@]}" -e 's/foo/bar/' target.file注:此代码行旨在将文件中所有出现的“foo”替换为“bar”。该代码行包括一个检查操作系统类型的条件以及根据不同类型的操作系统使用不同的sed参数来兼容MacOS和Linux。 - Alex Skrypnyk
好东西!我更喜欢较长的版本,因为更易读。 - nwinkler
1
这是我与不同系统兼容的最佳方式。 - Victor Choy
如果您在系统上安装了GNU sed(例如使用MacPorts或Brew),则此方法将失败。 - undefined

18

回答: 不行。

原本被接受的答案实际上并没有达到要求(如评论所述)。 (我在寻找一个解释一个file-e在我的目录中“随机”出现的原因时发现了这个答案。)

显然无法让 sed -i 在 MacOS 和 Linux 上都可靠地工作。

我的建议,不管价值多少,是不要使用 sed 来进行原地更新(具有复杂的错误模式),而是生成新文件并在之后重命名它们。 换句话说:避免使用 -i


17

无法让它正常工作。

一种方法是使用临时文件,例如:

TMP_FILE=`mktemp /tmp/config.XXXXXXXXXX`
sed -e "s/abc/def/" some/file > $TMP_FILE
mv $TMP_FILE some/file

这适用于两者


有没有什么原因,下面这段代码无法正常工作:SOME_FILE="$(sed -s "s/abc/def/" some/file)"; echo "$SOME_FILE" > some/file; - Jason Gross
看起来你会受到Bash变量的最大大小的限制,对吧?不确定它是否能处理几GB的文件。 - analogue
3
如果您正在创建临时文件,为什么不像@kine在下面建议的那样,给它一个扩展名来创建备份文件(然后可以将其删除)? - Alex Dupuy

15

-i选项不是POSIX Sed的一部分。更可移植的方法是使用Vim的Ex模式:

ex -sc '%s/alfa/bravo/|x' file
  1. % 选择所有行

  2. s 替换

  3. x 保存并关闭


这是正确的答案。POSIX的一个关键特性是可移植性。 - Kajukenbo

7

Steve Powell's answer非常正确,查阅OSX和Linux(Ubuntu 12.04)上的sed MAN页面强调了两个操作系统中“原地”使用sed时的不兼容性。

顺便说一下,在Linux版本的sed中,-i和任何引号之间不应该有空格(这表示空文件扩展名),因此:

sed Linux Man Page

#Linux
sed -i"" 

sed OSX手册页面

#OSX (notice the space after the '-i' argument)
sed -i "" 

我在脚本中通过使用别名命令和bash 'if'语句中的操作系统名称输出来解决了这个问题。尝试将与操作系统相关的命令字符串存储在变量中时,引号的解释不稳定。使用'shopt -s expand_aliases'是必要的,以便在脚本中扩展/使用定义的别名。有关shopt的用法,请参见此处

6

可在 GNU 系统和 OSX 上使用的便携脚本:

if [[ $(uname) == "Darwin" ]]; then
    SP=" " # Needed for portability with sed
fi

sed -i${SP}'' -e "s/foo/bar/g" -e "s/ping/pong/g" foobar.txt

4
没有理由在"Darwin"周围加引号 - 它只能解析为它本身。而如果不是因为[[ ]]的特殊语义,你需要在$(uname)周围加引号以确保正确处理所有可能的返回值。 - Charles Duffy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接