一个 Linux Shell 脚本问题

5

我在 Linux Shell 中有一个由点分隔的字符串,

$example=This.is.My.String

我想在最后一个句点前添加一些字符串,例如,我想在最后一个句点前添加“Good.Long”,那么我将得到:

This.is.My.Goood.Long.String

2.获取最后一个点之后的部分,这样我就可以得到

String

3.将点转换为下划线,但最后一个点除外,以便我得到

This_is_My.String

如果您有时间,请简单解释一下,我仍在学习正则表达式。
非常感谢!

根据您的标签,这似乎是一个关于sed的问题,对吗?(除了第一行中的$example,它让它看起来像perl...) - Cascabel
是的,这与sed有关。我在Linux上还没有安装perl。 - DocWiki
真的 是一个关于 sed 的问题吗?从问题看来,似乎只是关于 shell 脚本的。 - johnsyweb
我建议您更改标题。这个网站上的所有线程都涉及“Linux Shell脚本问题” :-)。 - Sopalajo de Arrierez
6个回答

10

我不知道您所说的“Linux Shell”是什么意思,因此我会假设是bash。这个解决方案也适用于 zsh等等

example=This.is.My.String
before_last_dot=${example%.*}
after_last_dot=${example##*.}
echo ${before_last_dot}.Goood.Long.${after_last_dot} 
This.is.My.Goood.Long.String

echo ${before_last_dot//./_}.${after_last_dot} 
This_is_My.String

中间变量before_last_dotafter_last_dot应该可以解释我对%##运算符的使用。我认为//也很容易理解,但如果你有任何问题,我很乐意澄清。

这不使用sed(甚至不用正则表达式),而是使用bash内置的参数替换。我更喜欢在脚本中只使用一种语言,尽可能少地使用分支。


我不知道Bash如此强大!这个答案真是让我大开眼界。非常感谢! - DocWiki
很高兴能够帮助。在StackOverflow上,“非常感谢!”通常表示为点赞或接受答案(对于帮助您解决问题的人)。 - johnsyweb

3

其他用户已经给出了关于#1和#2的好答案。对于#3的一些回答存在一些缺点。在某些情况下,您需要运行两次替换操作。在另一种情况下,如果您的字符串中有其他下划线,则可能会被覆盖。这个命令可以一次性完成,并且只影响点号:

sed 's/\(.*\)\./\1\n./;h;s/[^\n]*\n//;x;s/\n.*//;s/\./_/g;G;s/\n//'
  1. It splits the line before the last dot by inserting a newline and copies the result into hold space:

    s/\(.*\)\./\1\n./;h
    
  2. removes everything up to and including the newline from the copy in pattern space and swaps hold space and pattern space:

    s/[^\n]*\n//;x
    
  3. removes everything after and including the newline from the copy that's now in pattern space

    s/\n.*//
    
  4. changes all dots into underscores in the copy in pattern space and appends hold space onto the end of pattern space

    s/\./_/g;G
    
  5. removes the newline that the append operation adds

    s/\n//
    

然后sed脚本完成并输出模式空间。

在每个编号步骤的末尾(有些由两个实际步骤组成):

步骤        模式空间                 保留空间

  1.        This.is.My\n.String       This.is.My\n.String

  2.        This.is.My\n.String       .String

  3.        This.is.My                        .String

  4.        This_is_My\n.String     .String

  5.        This_is_My.String            .String


干得好,你一定比我有更多的时间来思考它。 - Jonathan Leffler
1
@Jonathan:可能是的,但这是sed中常见的模式——“分而治之”。与Johnysweb的Bash答案非常相似。 - Dennis Williamson

3

解决方案

  1. 两种版本:
    • 复杂: sed 's/\(.*\)\([.][^.]*$\)/\1.Goood.Long\2/'
    • 简单: sed 's/.*\./&Goood.Long./' - 感谢Dennis Williamson
  2. 你想要什么?
    • 复杂: sed 's/.*[.]\([^.]*\)$/\1/'
    • 简单: sed 's/.*\.//' - 感谢glenn jackman.
  3. sed 's/\([^.]*\)[.]\([^.]*[.]\)/\1_\2/g'

对于第三个解决方案,通常需要至少运行两次完整的替换。

解释

记住,在sed中,符号\(...\)表示捕获组,可以在替换文本中用'\1'或类似方式引用。

  1. 捕获除以点开头,后跟一连串非点字符(也被捕获)之外的所有内容;将其替换为最后一点之前的内容、新内容和最后一点及其之后的内容。

  2. 忽略点之前的所有内容,只捕获最后一个点之后的非点字符序列;仅用捕获的内容进行替换。

  3. 查找并捕获一个非点字符序列、一个点(未被捕获),后跟一个非点字符序列和一个点;将第一个点替换为下划线。 全局执行此操作,但第二次及以后匹配不会影响已经匹配的任何内容。 因此,我认为你需要 ceil(log2N) 次执行,其中 N 是要被替换的点数。一次处理1个点;两次处理2或3个点;三次处理4-7个点,依此类推。


非常感谢!:) 如果您能稍微解释一下,那就太完美了。 - DocWiki
你的第二个命令可以更简单:sed 's/^.*\.//' - glenn jackman
@Glenn:嗯...是的 - 这就是在你应该注意到电话会议的情况下安排它所带来的后果。 - Jonathan Leffler
同样地,#1 可以简化为:sed 's/\(.*\.\)/\1Goood.Long./' - Dennis Williamson
我发现我错过了使用&进一步简化的机会! - Dennis Williamson
1
@Dennis:众人拾柴火焰高——即使是一行代码,也可以从建设性的代码审查中受益。谢谢。 - Jonathan Leffler

3
这是一个使用Bash的正则表达式匹配(需要Bash 3.2或更高版本)的版本。
[[ $example =~ ^(.*)\.(.*)$ ]]
echo ${BASH_REMATCH[1]//./_}.${BASH_REMATCH[2]}

这里有一个使用 IFS(内部字段分隔符)的 Bash 版本。

saveIFS=$IFS
IFS=.
array=($e)                    # *   split the string at each dot
lastword=${array[@]: -1}
unset "array[${#array}-1]"    # *
IFS=_
echo "${array[*]}.$lastword"  #     The asterisk as a subscript when inside quotes causes IFS (an underscore in this case) to be inserted between each element of the array
IFS=$saveIFS

* 在完成这些步骤后,使用declare -p array查看数组的样子。


+1 表示有多种方法可以解决问题,但有些方法比其他方法更易读。 - johnsyweb

2

1.

$ echo 'This.is.my.string' | sed 's}[^\.][^\.]*$}Good Long.&}'
This.is.my.Good Long.string

之前:一个点,然后直到结尾没有点。之后:明显的,&是与第一部分匹配的内容。

2.

$ echo 'This.is.my.string' | sed 's}.*\.}}'
string

sed贪婪匹配,因此它将尽可能扩展第一个闭合符号(.*),即到最后一个点。

3.

$ echo 'This.is.my.string' | tr . _ | sed 's/_\([^_]*\)$/\.\1/'
This_is_my.string

将所有的点转换为下划线,然后将最后一个下划线变成点。

注意:这会将'This.is.my.string_foo'转换为'This_is_my_string.foo',而不是'This_is_my.string_foo'。


1
如果你使用Awk并且有一点创意,就根本不需要正则表达式(那些复杂的东西让我眼花!)。
1. echo $example| awk -v ins="Good.long" -F . '{OFS="."; $NF = ins"."$NF;print}'

这段代码的作用:
-v ins="Good.long" 告诉 awk 创建一个名为 'ins' 的变量,并将 "Good.long" 作为其内容,
-F . 告诉 awk 使用点号作为输入字段的分隔符,
-OFS 告诉 awk 使用点号作为输出字段的分隔符,
NF 是字段数,因此 $NF 表示最后一个字段,
$NF=... 部分替换最后一个字段,它将当前最后一个字符串附加到要插入的内容(之前声明的名为“ins”的变量)。

2. echo $example| awk -F . '{print $NF}'
$NF是最后一个字段,所以就是这样了!
3. echo $example| awk -F . '{OFS="_"; $(NF-1) = $(NF-1)"."$NF; NF=NF-1; print}'

在这里我们需要有创意,因为据我所知 Awk 不允许删除字段。当然,我们将输出字段分隔符设置为下划线。

$(NF-1) = $(NF-1)"."$NF:首先,我们用点将最后一个字段粘贴到倒数第二个字段上,替换第二个字段。
然后,我们欺骗 awk 让它认为字段数等于字段数减一,从而删除最后一个字段!

请注意,您不能说 $NF="",因为那样会显示两个下划线。


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