字符替换限于每个输入行的一部分

时间:2016-11-03 17:46:41

标签: sed substitution string-substitution

有一个文件,例如。 Inventory.conf的行如:

Int/domain—home.dir=/etc/int

我需要在/之前替换=,而不是之后。 结果应该是:

Int_domain_home_dir=/etc/int

我尝试了几个sed命令,但似乎没有一个符合我的需要。

4 个答案:

答案 0 :(得分:2)

您要求的是sed解决方案,但 awk解决方案更简单,在这种情况下表现更好,因为您可以轻松地将该行拆分为2个字段按=,然后有选择地将gsub()仅应用于第一个字段,以替换感兴趣的字符:

$ awk -F= '{ gsub("[./-]", "_", $1); print $1 FS $2 }' <<< 'Int/domain-home.dir=/etc/int'
Int_domain_home_dir=/etc/int
  • -F=告诉awk将输入拆分为=字段,其中输入信号会导致$1(第1个字段)包含第一个字段在=之后,在$2之前的一半,以及在=之后的-F(第二场)。使用FS选项设置变量gsub("[./-]", "_", $1),即输入字段分隔符。

  • [./-]全局将_中的所有字符替换为$1中的. - 即所有出现的/,{{1}第1个字段中的{}或-每个都替换为_

  • print $1 FS $2打印结果:修改后的第1个字段($1),后跟FS=),然后是(未修改)第二个字段($2)。

请注意,我使用过ASCII char。即使您的示例输入包含Unicode字符,-脚本中的0x2d HYPHEN-MINUS ,代码点awk)也是如此。 EM DASH U+2014,UTF-8编码0xe2 0x80 0x94)。
如果你真的想匹配那个,只需在上面的命令中替换它,但请注意macOS上的awk版本无法正确处理。

另一种选择是使用带有ASCII音译的iconv,将em破折号转换为常规ASCII -

iconv -f utf-8 -t ascii//translit <<< 'Int/domain—home.dir=/etc/int' |
  awk -F= '{ gsub("[./-]", "_", $1); print $1 FS $2 }' 

perl 也可以提供优雅的解决方案:

$ perl -F= -ane '$F[0] =~ tr|-/.|_|; print join("=", @F)' <<<'Int/domain-home.dir=/etc/int'
Int_domain_home_dir=/etc/int
  • -F=,就像使用Awk一样,告诉Perl在将行拆分为字段时使用=作为分隔符

  • -ane激活字段拆分(a),关闭隐式输出(n),e告诉Perl next argument是要执行的表达式(命令字符串)。

  • 每行分割成的字段存储在数组@F中,其中$F[0]指的是第一个字段。

  • $F[0] =~ tr|-/.|-|-/.的所有内容翻译(替换)为_

  • print join("=", @F)从字段重建输入行 - 现在修改了第1个字段 - 并打印结果。

根据所使用的Awk实现,这实际上可能更快(见下文)。

sed不是此工作的最佳工具,也反映在解决方案的相对性能中:

来自我的macOS 10.12计算机(GNU sed 4.2.2,Mawk awk 1.3.4,perl v5.18.2,使用输入文件file的示例计时,其中包含100万份样本输入行) - 带上一粒盐,但数字的比率是有意义的;最快的解决方案:

# This answer's awk answer.
# Note: Mawk is much faster here than GNU Awk and BSD Awk.
$ time awk -F= '{ gsub("[./-]", "_", $1); print $1 FS $2 }' file >/dev/null
real    0m0.657s

# This answer's perl solution:
# Note: On macOS, this outperforms the Awk solution when using either
#       GNU Awk or BSD Awk.
$ time perl -F= -ane '$F[0] =~ tr|-/.|_|; print join("=", @F)' file >/dev/null
real    0m1.656s

# Sundeep's perl solution with tr///
$ time perl -pe 's#^[^=]+#$&=~tr|/.-|_|r#e' file >/dev/null
real    0m2.370s

# Sundeep's perl solution with s///
$ time perl -pe 's#^[^=]+#$&=~s|[/.-]|_|gr#e' file >/dev/null
real    0m3.540s

# Cyrus' solution.
$ time sed 'h;s/[^=]*//;x;s/=.*//;s/[/.-]/_/g;G;s/\n//' file >/dev/null
real    0m4.090s

# Kenavoz' solution.
# Note: The 3-byte UTF-8 em dash is NOT included in the char. set,
#       for consistency of comparison with the other solutions.
#       Interestingly, adding the em dash adds another 2 seconds or so.
$ time sed ':a;s/[-/.]\(.*=\)/_\1/;ta' file >/dev/null
real    0m9.036s

正如您所看到的,到目前为止,awk解决方案速度最快,行内部循环sed解决方案可预测性能最差,约为12倍。

答案 1 :(得分:2)

带有t循环(BRE)的Sed:

$ sed ':a;s/[-/—.]\(.*=\)/_\1/;ta;' <<< "Int/domain—home.dir=/etc/int"
Int_domain_home_dir=/etc/int

如果找到其中一个-/—.字符,则将其替换为_。接下来的文本将被捕获并使用反向引用输出=。如果先前的替换成功,t命令将循环标记:a以检查是否有进一步的替换。

修改

如果您在BSD / Mac OSX下(感谢@ mklement0):

sed -e ':a' -e 's/[-/—.]\(.*=\)/_\1/;ta'

答案 2 :(得分:1)

使用GNU sed:

echo 'Int/domain—home.dir=/etc/int' | sed 'h;s/[^=]*//;x;s/=.*//;s/[/—.]/_/g;G;s/\n//'

输出:

Int_domain_home_dir=/etc/int

请参阅:man sed。我想你也想替换点。

答案 3 :(得分:1)

如果$ echo 'Int/domain-home.dir=/etc/int' | perl -pe 's#^[^=]+#$&=~s|[/.-]|_|gr#e' Int_domain_home_dir=/etc/int 解决方案没问题:

^[^=]+
  • =字符串匹配从排队开始到但不包括第一次出现的$&=~s|[/.-]|_|gr
  • /对匹配的字符串执行另一次替换
    • 将所有.-_字符替换为r
    • e修饰符将返回修改后的字符串
  • #修饰符允许在替换部分
  • 中使用表达式而不是字符串
  • /用作分隔符,以避免在字符类[/.-]
  • 内转义$ echo 'Int/domain-home.dir=/etc/int' | perl -pe 's#^[^=]+#$&=~tr|/.-|_|r#e' Int_domain_home_dir=/etc/int

另外,正如@ mklement0所建议的那样,我们可以使用translate代替内部替换

-


请注意,我更改了示例输入,代替DataFrame,这是OP根据评论想要的内容