【问题标题】:sed -i command for in-place editing to work with both GNU sed and BSD/OSXsed -i 命令用于就地编辑以与 GNU sed 和 BSD/OSX 一起使用
【发布时间】:2011-01-20 04:54:44
【问题描述】:

我有一个makefile(在Linux 上为gmake 开发),我正在尝试移植到MacOS,但似乎sed 不想合作。我所做的是使用GCC 自动生成依赖文件,然后使用sed 稍微调整它们。 makefile的相关部分:

$(OBJ_DIR)/%.d: $(SRC_DIR)/%.cpp
  $(CPPC) -MM -MD $< -o $@
  sed -i 's|\(.*\)\.o:|$(OBJ_DIR)/\1.o $(OBJ_DIR)/\1.d $(TEST_OBJ_DIR)/\1_utest.o:|' $@

虽然它在 GNU/Linux 下运行没有问题,但在尝试在 MacOS 上构建时出现以下错误:

sed: 1: "test/obj/equipmentConta ...": undefined label 'est/obj/equipmentContainer_utest.d'
sed: 1: "test/obj/dice_utest.d": undefined label 'est/obj/dice_utest.d'
sed: 1: "test/obj/color-string_u ...": undefined label 'est/obj/color-string_utest.d'

看起来sed 正在砍掉一个字符,但我看不到解决方案。

【问题讨论】:

  • 可能,BashX 项目可以帮助您解决此类问题。

标签: macos sed gnu inplace-editing


【解决方案1】:

OS X sed 处理 -i 参数的方式与 Linux 版本不同。

您可以通过以这种方式添加-e 来生成可能对两者都“有效”的命令:

#      vv
sed -i -e 's|\(.*\)\.o:|$(OBJ_DIR)/\1.o $(OBJ_DIR)/\1.d $(TEST_OBJ_DIR)/\1_utest.o:|' $@

OS X sed -i-i 之后的下一个内容解释为就地编辑的备份副本 的文件扩展名。 (Linux 版本只有在-i 和扩展名之间没有空格时才会这样做。)显然使用它的副作用是你会得到一个带有-e 作为扩展名的备份文件,你可能不想要。请参阅此问题的其他答案以获取更多详细信息以及可以使用的更清洁的方法。

您看到的行为是因为 OS X sed 使用 s||| 作为扩展名 (!) 然后将 next 参数解释为命令 - 在这种情况下它以 t 开头,sed 将其识别为分支到标签命令,期望目标标签作为参数 - 因此您会看到错误。

如果你创建一个文件test,你可以重现错误:

$ sed -i 's|x|y|' test
sed: 1: "test": undefined label 'est'

【讨论】:

  • 我认为如果在环境中定义了 POSIXLY_CORRECT 或(较旧的)POSIX_ME_HARDER,则可以预期相同的行为。
  • OSX 的正确解决方法是为-i 设置一个空参数,但同样这与 Linux sed 不兼容。 )-: 也许改用 Perl? perl -pi -e 's|x|y|g' file
  • @TimPost: -i is not a POSIX-compliant option,所以这些环境变量不适用。
【解决方案2】:

目前接受的答案在两个非常重要的方面存在缺陷。

  1. 对于 BSD sed(OSX 版本),-e 选项被解释为 一个文件扩展名,因此创建一个带有-e 的备份文件 扩展名。

  2. 按照建议对 darwin 内核进行测试并不可靠 一种跨平台解决方案的方法,因为 GNU 或 BSD sed 可以 存在于任意数量的系统上。

更可靠的测试是简单地测试 --version 选项,该选项仅在 GNU 版本的 sed 中找到。

sed --version >/dev/null 2>&1

一旦确定了正确的 sed 版本,我们就可以以正确的语法执行命令。

-i 选项的 GNU sed 语法:

sed -i -- "$@"

-i 选项的 BSD sed 语法:

sed -i "" "$@"

最后将它们放在一个跨平台函数中执行就地编辑 sed 命令:

sedi () {
    sed --version >/dev/null 2>&1 && sed -i -- "$@" || sed -i "" "$@"
}

示例用法:

sedi 's/old/new/g' 'some_file.txt'

此解决方案已在 OSX、Ubuntu、Freebsd、Cygwin、CentOS、Red Hat Enterprise 和 Msys 上进行了测试。

【讨论】:

  • 如果您想将此与find 结合使用,请参阅this thread
  • 在 mac el capitan 和 ubuntu 17 桌面和服务器上表现出色
  • 不适用于 Ubuntu 20.04.1 docker 映像中的 sed (GNU sed) 4.7。删除 -- 有效。
【解决方案3】:

这不是问题的完全答案,但可以通过

获得与 linux 等效的行为
brew install gnu-sed

# Add to .bashrc / .zshrc
export PATH="/usr/local/opt/gnu-sed/libexec/gnubin:$PATH"

(之前brew install gnu-sed 有一个--with-default-names 选项,但最近已被删除)

【讨论】:

  • 这对我来说绝对是最快的解决方案。
  • brew install gnu-sed 会将sed 安装为gsed,因此您无需考虑之前使用OSX 的sed 编写的脚本。
  • 我确实尝试过更新--with-default-names,只是在打开一个新的术语窗口后才注意到它,以及sed --version等。
  • 也许hash -r 安装后会有所帮助:)
  • 这不是一个可行的解决方案如果它适用于普通民众。程序员可能不得不安装软件才能使东西正常工作,但一般用户不会。由于问题的上下文是编程和生成文件(损坏的生成文件 IMO,但这是一个不同的讨论),这可能是一个解决方案。
【解决方案4】:

我已经更正了@thecarpy 发布的解决方案:

这是sed -i 的适当跨平台解决方案:

sedi() {
  case $(uname) in
    Darwin*) sedi=('-i' '') ;;
    *) sedi='-i' ;;
  esac

  LC_ALL=C sed "${sedi[@]}" "$@"
}

【讨论】:

  • @masimplo 与 linux 上的 sed -i 几乎相同,大多数东西都受支持,您必须深入手册才能了解它们的不同之处
  • 很有希望,但这里没有理由使用LC_ALL=C,除非您明确需要忽略当前语言环境的字符编码并将每个 byte 视为一个字符。
  • 虽然使用bash 数组传递动态构造的参数,正如你所做的那样,通常更健壮,@thecarpy 的答案实际上并没有什么问题(除了最好测试什么实现sed 二进制文件不是为了测试主机平台)。
【解决方案5】:

我也遇到了这个问题,想到了以下解决方案:

darwin=false;
case "`uname`" in
  Darwin*) darwin=true ;;
esac

if $darwin; then
  sedi="/usr/bin/sed -i ''"
else
  sedi="sed -i"
fi

$sedi 's/foo/bar/' /home/foobar/bar

为我工作 ;-),YMMV

我在一个多操作系统团队工作,其中 ppl 在 Windows、Linux 和 OS X 上构建。一些 OS X 用户抱怨,因为他们遇到了另一个错误 - 他们安装了 sed 的 GNU 端口,所以我必须指定完整路径。

【讨论】:

  • 您的解决方案是真正的跨平台。谢谢。
  • 这会为我创建一个 bar'' 备份文件。但不是直接在终端中写出命令时。实在想不通为什么。
  • 我不得不使用 eval。 eval "$sedi 's/foo/bar/' /home/foobar/bar".这只在 os x 上测试过。
  • 看看我下面的回答,我已经解决了生成的问题,比eval解决方案更好。
【解决方案6】:

其实在做

sed -i -e "s/blah/blah/" files

在 MacOS 中也不符合您的预期。相反,它会创建带有-e 扩展名的备份文件。

MacOS 的正确命令是

sed -i "" -e "s/blah/blah/" files

在 Linux 上,删除 -i"" 之间的空格(请参阅 related answer

sed -i"" -e "s/blah/blah/" files

【讨论】:

  • 对,但您的答案不适用于 Linux,这是主要问题:需要同时适用于两者。使用 -i -e 仍然可以删除 -e 扩展文件。
  • 我想对于“工作”的慷慨定义就是这样。这是一个聪明的技巧,但它会产生额外的文件。 Urkle 的答案正是我想要的。
  • 警告 如果您尝试查找所有带有 -e 文件扩展名的文件并使用 find . -type f | grep "-e$" | xargs rm 之类的名称删除它们,您将删除所有文件。需要使用"\-e",然后在没有管道的情况下先运行到xargs rm
  • @JamesRobinson,只需使用find . -name "*-e" -delete。始终在没有 -delete 标志的情况下运行,但只是为了确认您将要删除的内容。
  • 看来-i "" 中的空格需要删除才能使其在Linux 上也能正常工作:请参阅stackoverflow.com/a/14813278/15690
【解决方案7】:

martin clayton's helpful answer 很好地解释了这个问题[1],但正如他所说的那样,一个解决方案具有潜在的不良副作用。

以下是无副作用的解决方案

警告:单独解决 -i 语法问题,如下所示,可能还不够,因为 GNU sed 和 BSD/macOS sed 之间还有许多其他差异(对于综合讨论,见我的this answer)。


-i 的解决方法:临时创建一个备份文件,然后清理它:

使用 非空 后缀(备份文件文件扩展名)选项参数(不是空字符串的值),您可以 使用-i 的方式与BSD/macOS sed 和GNU sed 一起使用,直接将后缀附加到-i 选项

这可以用来创建一个备份文件临时您可以立即清理:

sed -i.bak 's/foo/bar/' file && rm file.bak

显然,如果您确实想保留备份,只需省略 &amp;&amp; rm file.bak 部分。


符合 POSIX 的解决方法,使用临时文件和 mv

如果仅要就地编辑单个文件,-i选项可以绕过以避免不兼容。

如果您将sed 脚本和其他选项限制为POSIX-compliant features,以下是完全可移植 解决方案(注意-i 不是 POSIX 兼容)。

sed 's/foo/bar' file > /tmp/file.$$ && mv /tmp/file.$$ file
  • 此命令只是将修改写入临时文件,如果 sed 命令成功 (&amp;&amp;),则将原始文件替换为临时文件。

    • 如果您确实想保留原始文件作为备份,请添加另一个 mv 命令,首先重命名原始文件。
  • 警告:基本上,-i 也是这样做的,只是它试图保留原始文件的权限和扩展属性 (macOS);但是,如果原始文件是符号链接,则此解决方案和-i 都会将符号链接替换为常规文件
    见下半部分我的this answer 的详细信息,了解-i 的工作原理。


[1] 更深入的解释见我的this answer

【讨论】:

  • 这里的“解决方法”是迄今为止最好的解决方案(最简单/最轻量级/最简单)。 This question 是关于一个稍微不同的问题,但包含对同一 -i 问题的良好讨论。
  • 谢谢,@NormanGray。在给定的约束(仅 1 个输入文件,不保留权限、创建日期、扩展属性)内,这是真的。
  • 解决方法 2 是迄今为止最简单的。这也是最安全的 - 就地编辑可能会失败,并且您已经丢失了原件。
  • @JonathanLeffler:我同意sed -i.bak ... file &amp;&amp; rm file.bak 解决方案是最简单和最好的解决原始问题的方法,因此我已将其移至现在显示为第一个解决方法(并且我已删除编号以免混淆)。但是,使用符合 POSIX 的解决方法,使用 &amp;&amp; 仅在 sed 报告成功时替换原来的,您看到什么问题?我想缺少替换原件的权限可能会导致mv 失败,这会令人不快,但您不会丢失原件。
  • 同意 POSIX 兼容的解决方案也可以很好地工作 — 并且可以在本地 sed(可能)根本不支持 -i 的 Solaris、AIX 或 HP-UX 等平台上运行。所以,在实际使用-i的方案中,sed -i.bak …"$file" &amp;&amp; rm -f "$file.bak"方案是最好的,完全不使用-i的方案也有优点。 (如果原始文件本身就是一个符号链接,或者如果它有多个链接,那么所有解决方案都会出现问题。当您移动/删除文件时,权限/所有权可能会成为问题,但这可能也适用于使用 -i 的解决方案。 )
猜你喜欢
  • 2011-08-07
  • 1970-01-01
  • 2022-11-26
  • 2011-11-26
  • 2014-08-08
  • 2019-11-04
  • 2021-09-24
相关资源
最近更新 更多