【问题标题】:Longest common prefix of two strings in bashbash中两个字符串的最长公共前缀
【发布时间】:2011-10-21 19:59:17
【问题描述】:

我有两个字符串。为了示例,它们设置如下:

string1="test toast"
string2="test test"

我想要的是从字符串的开头找到重叠。重叠是指上面示例中的字符串“test t”。

# So I look for the command 
command "$string1" "$string2"
# that outputs:
"test t"

如果字符串是string1="atest toast"; string2="test test",它们将不会重叠,因为检查从开头开始,而“a”在string1 的开头。

【问题讨论】:

  • 哦,伙计,很高兴看到其他人也在为此苦苦挣扎:D
  • @ajreal:那里提供的函数相当冗长,并且不适用于字符串中的空格。尽管如此,我的问题还是重复的。对此感到抱歉。将在那里发表评论
  • 不重复:路口需求不一样。

标签: bash string-formatting


【解决方案1】:

伙计,这很难。这是一项极其微不足道的任务,但我不知道如何使用 shell 来执行此操作 :)

这是一个丑陋的解决方案:

echo "$2" | awk 'BEGIN{FS=""} { n=0; while(n<=NF) {if ($n == substr(test,n,1)) {printf("%c",$n);} n++;} print ""}' test="$1"

【讨论】:

  • 这非常快,按原样,但有几个问题。 (1) 它不处理多字节字符。这很容易解决.. 只需将%c 更改为%s.. (2) 当两个字符串相同时报告错误,除了一个有尾随\n 而另一个没有。在这种情况下,脚本会报告更长的值...纠正尾随换行问题可能并不那么容易解决,因为 awk 的行为将附加尾随换行符(这会导致问题)。但是,当我写这篇文章时,我记得有一种方法可以在awk 中检测到last-line(我想!)。我现在去看看。
  • 我在想perl(eof),但你可以通过delayed handling of each input line 阻止OFS 的最终自动输出。还有一点:echo "$2" 附加了一个无关紧要的\n$2
  • 嗨,卡罗利。 Again me!在这里,您的脚本也有类似的问题:awk 'BEGIN{FS=""} { n=0; while(n&lt;=NF) {if ($n == substr(test,n,1)) {printf("%c",$n);} n++;} print ""}' test="/aa/bc/" &lt;&lt;&lt; '/aa/bd/' => 它显示/aa/b/ 而不是/aa/b。请尝试改进您的 awk 脚本 ;-) 干杯
  • 第二个示例也不起作用:awk 'BEGIN{FS=""} { n=0; while(n&lt;=NF) {if ($n == substr(test,n,1)) {printf("%c",$n);} n++;} print ""}' test="f/aa" &lt;&lt;&lt; 'g/aa' => 它显示/aa 而不是空的(OP 的问题是关于前缀)。勇气和好运 ;-) 干杯
【解决方案2】:

用另一种语言可能更简单。这是我的解决方案:

common_bit=$(perl -le '($s,$t)=@ARGV;for(split//,$s){last unless $t=~/^\Q$z$_/;$z.=$_}print $z' "$string1" "$string2")

如果这不是单行的,我会使用更长的变量名、更多的空格、更多的大括号等。我也确信有一种更快的方法,即使在 perl 中,但同样,这是一种交易- 速度和空间之间的折衷:这在已经很长的单线中使用更少的空间。

【讨论】:

    【解决方案3】:

    好的,在 bash 中:

    #!/bin/bash
    
    s="$1"
    t="$2"
    l=1
    
    while [ "${t#${s:0:$l}}" != "$t" ]
    do
      (( l = l + 1 ))
    done
    (( l = l - 1 ))
    
    echo "${s:0:$l}"
    

    它与其他语言的算法相同,但具有纯 bash 功能。而且,我可以说,也有点丑:-)

    【讨论】:

      【解决方案4】:

      在 sed 中,假设字符串不包含任何换行符:

      string1="test toast"
      string2="test test"
      printf "%s\n%s\n" "$string1" "$string2" | sed -e 'N;s/^\(.*\).*\n\1.*$/\1/'
      

      【讨论】:

      • 请注意,并非所有 seds 都支持替换命令中的 "\n" (Apple's doesn't),但 Gnu's sed 支持。读者可能需要运行 gsed 而不是 sed
      • GNU sed 也支持\x0printf '%s\x0%s' "$string1" "$string2" | sed 's/\(.*\).*\x0\1.*/\1/' 更加安全。如果您正在处理路径名并想要一个公共路径前缀,请在 \(.*/\) 中替换 \(.*\)
      • @jthill 有一个好主意,但也必须修改 sed 命令以处理换行符,例如:printf '%s\x0%s\n' "$string1" "$string2" | sed 'H;$!d;g;s/\`.\(.*\).*\x0\1.*/\1/'
      【解决方案5】:

      不使用 sed,使用 cmp 实用程序获取第一个不同字符的索引,并使用进程替换将 2 个字符串获取到 cmp:

      string1="test toast"
      string2="test test"
      first_diff_char=$(cmp <( echo "$string1" ) <( echo "$string2" ) | cut -d " " -f 5 | tr -d ",")
      echo ${string1:0:$((first_diff_char-1))}
      

      【讨论】:

      • 使用 sed 是一个更好的解决方案,因为只需要启动一个进程。
      • 工具的选择不错,但前处理和后处理错误。 echo "$string1" 破坏了一些字符串,当其中一个字符串是另一个字符串的前缀时,您不会处理这种情况。您不需要调用cut,因为shell 完全能够从cmp 输出中提取偏移量。这种方法的一个限制是cmp 对字节而不是字符进行操作。
      • @Gilles:你能给我看一个echo 破坏字符串的例子吗?在 bash 的人中,我找到了一个带有 echo -e "toto\ntata" 的示例,所以使用 echo -E 是否安全(尽管感谢 printf 示例)。关于字符串是另一个字符串的前缀的情况,我没有与cmp (GNU diffutils) 2.8.1 不同的输出。关于避免cut 的可能性是正确的,关于不处理多字节字符是完全正确的。
      • 在 bash 下,只有一个参数,echo 只会破坏 ^-[neE]+$;虽然如果设置了xpg_echo,那么echo 也会破坏反斜杠。此外echo 添加了一个换行符,这解释了为什么您没有看到foofoobar 的前缀:您将foo\nfoobar\n 传递给cut。尝试使用echo -nfoo\nfoo\nbar
      【解决方案6】:

      这可以完全在 bash 中完成。虽然在 bash 中循环执行字符串操作很慢,但是有一个简单的算法,它与 shell 操作的数量成对数,所以即使对于长字符串,纯 bash 也是一个可行的选择。

      longest_common_prefix () {
        local prefix= n
        ## Truncate the two strings to the minimum of their lengths
        if [[ ${#1} -gt ${#2} ]]; then
          set -- "${1:0:${#2}}" "$2"
        else
          set -- "$1" "${2:0:${#1}}"
        fi
        ## Binary search for the first differing character, accumulating the common prefix
        while [[ ${#1} -gt 1 ]]; do
          n=$(((${#1}+1)/2))
          if [[ ${1:0:$n} == ${2:0:$n} ]]; then
            prefix=$prefix${1:0:$n}
            set -- "${1:$n}" "${2:$n}"
          else
            set -- "${1:0:$n}" "${2:0:$n}"
          fi
        done
        ## Add the one remaining character, if common
        if [[ $1 = $2 ]]; then prefix=$prefix$1; fi
        printf %s "$prefix"
      }
      

      标准工具箱包括cmp 用于比较二进制文件。默认情况下,它表示第一个不同字节的字节偏移量。当一个字符串是另一个字符串的前缀时,有一种特殊情况:cmp 在 STDERR 上产生不同的消息;解决这个问题的一个简单方法是取最短的字符串。

      longest_common_prefix () {
        local LC_ALL=C offset prefix
        offset=$(export LC_ALL; cmp <(printf %s "$1") <(printf %s "$2") 2>/dev/null)
        if [[ -n $offset ]]; then
          offset=${offset%,*}; offset=${offset##* }
          prefix=${1:0:$((offset-1))}
        else
          if [[ ${#1} -lt ${#2} ]]; then
            prefix=$1
          else
            prefix=$2
          fi
        fi
        printf %s "$prefix"
      }
      

      请注意,cmp 对字节进行操作,但 bash 的字符串操作对字符进行操作。这在多字节语言环境中有所不同,例如使用 UTF-8 字符集的语言环境。上面的函数打印一个字节串的最长前缀。要使用这种方法处理字符串,我们可以先将字符串转换为固定宽度的编码。假设语言环境的字符集是 Unicode 的子集,UTF-32 就符合要求。

      longest_common_prefix () {
        local offset prefix LC_CTYPE="${LC_ALL:=LC_CTYPE}"
        offset=$(unset LC_ALL; LC_MESSAGES=C cmp <(printf %s "$1" | iconv -t UTF-32)
                                                 <(printf %s "$2" | iconv -t UTF-32) 2>/dev/null)
        if [[ -n $offset ]]; then
          offset=${offset%,*}; offset=${offset##* }
          prefix=${1:0:$((offset/4-1))}
        else
          if [[ ${#1} -lt ${#2} ]]; then
            prefix=$1
          else
            prefix=$2
          fi
        fi
        printf %s "$prefix"
      }
      

      【讨论】:

      • 此解决方案的一个变体处理多字节字符将使用 diff 而不是 cmp,并使用 printf %s "$1" | fold -w 1 作为其输入。
      • @jfgagne 不完全是,这会抑制换行符。顺便说一句,我喜欢你的 sed 解决方案,但它也不总是适用于多行字符串。
      【解决方案7】:

      仅使用 Bash 的另一种方式。

      string1="test toast"
      string2="test test"
      len=${#string1}
      
      for ((i=0; i<len; i++)); do
         if [[ "${string1:i:1}" == "${string2:i:1}" ]]; then
            continue
         else
            echo "${string1:0:i}"                       
            i=len
         fi
      done
      

      【讨论】:

        【解决方案8】:

        sed 示例的改进版本,它找到 N 个字符串的公共前缀 (N>=0):

        string1="test toast"
        string2="test test"
        string3="teaser"
        { echo "$string1"; echo "$string2"; echo "$string3"; } | sed -e 'N;s/^\(.*\).*\n\1.*$/\1\n\1/;D'
        

        如果字符串存储在数组中,则可以使用printf 将它们通过管道传送到 sed:

        strings=("test toast" "test test" "teaser")
        printf "%s\n" "${strings[@]}" | sed -e '$!{N;s/^\(.*\).*\n\1.*$/\1\n\1/;D;}'
        

        您也可以使用here-string

        strings=("test toast" "test test" "teaser")
        oIFS=$IFS
        IFS=$'\n'
        <<<"${strings[*]}" sed -e '$!{N;s/^\(.*\).*\n\1.*$/\1\n\1/;D;}'
        IFS=$oIFS
        # for a local IFS:
        (IFS=$'\n'; sed -e '$!{N;s/^\(.*\).*\n\1.*$/\1\n\1/;D;}' <<<"${strings[*]}")
        

        here-string(与所有重定向一样)可以在一个简单命令中的任何位置。

        【讨论】:

        • 这是一个更好的现实世界解决方案,因为人们不知道有多少字符串并且需要处理整个字符串数组。在我的例子中,一个由四个字符串组成的数组包含 25 级深的子目录,前 19 级是常见的。 How to quickly find the deepest subdirectory
        • 我非常喜欢这个,但更喜欢聪明的 grep 技术。这可以转换为使用 grep 吗?只在前两行工作太严格了!
        • 不错。在my answer 这个问题中,我将它概括为处理嵌入式换行符。
        【解决方案9】:

        Grep 短变体(从 sed 借来的想法):

        $ echo -e "String1\nString2" | grep -zoP '^(.*)(?=.*?\n\1)'
        String
        

        假设字符串没有换行符。但 easy 可以调整为使用任何分隔符。

        2016 年 10 月 24 日更新:在现代版本的 grep 上,您可能会收到投诉 grep: unescaped ^ or $ not supported with -Pz,只需使用 \A 而不是 ^

        $ echo -e "String1\nString2" | grep -zoP '\A(.*)(?=.*?\n\1)'
        String
        

        【讨论】:

        • 您可以通过 tail -1 管道输出该命令的输出,以获得两个以上字符串的最长公共前缀。
        【解决方案10】:

        另一个变体,使用 GNU grep:

        $ string1="test toast"
        $ string2="test test"
        $ grep -zPo '(.*).*\n\K\1' <<< "$string1"$'\n'"$string2"
        test t
        

        【讨论】:

        • 这似乎比 sed 方法(Linux、Mac)更便携
        • 但是为什么要使用 z 标志呢?
        • 抱歉,我正在查看 BSD 联机帮助页,而 z 标志与此处无关。但是对于 GNU,z 标志在行尾查找空字节,这意味着多行输入可以进行正则表达式匹配以产生 OP 想要的内容。还不错。
        • 在 macOS 上,您需要 brew install grep 以获取 GNU grep 为 ggrep。我的 MO 一直是通过逐步“安装它们”来逐渐用 GNU utils 感染我的 mac 运行时,例如ln -s /usr/local/bin/gdate /usr/local/bin/date,因为我的 /usr/local/bin 在我的 $PATH 中更早,这样我可以保持 BSD 实用程序不变,例如/usr/bin/date 同时减少了我在 shell 脚本中为 Darwin 分支的需要,因为它们越来越依赖 GNU 功能。
        【解决方案11】:

        如果使用其他语言,python怎么样:

        cmnstr() { python -c "from difflib import SequenceMatcher
        s1, s2 = ('''$1''', '''$2''')
        m = SequenceMatcher(None,s1,s2).find_longest_match(0,len(s1),0,len(s2))
        if m.a == 0: print(s1[m.a: m.a+m.size])"
        }
        $ cmnstr x y
        $ cmnstr asdfas asd
        asd
        

        (h/t 到@RickardSjogren's answer to stack overflow 18715688)

        【讨论】:

          【解决方案12】:

          另一个基于python的答案,这个基于os.path模块的原生commonprefix函数

          #!/bin/bash
          cat mystream | python -c $'import sys, os; sys.stdout.write(os.path.commonprefix(sys.stdin.readlines()) + b\'\\n\')'
          

          长格式,那是

          import sys
          import os
          sys.stdout.write(
              os.path.commonprefix(sys.stdin.readlines()) + b'\n'
          )
          

          /!\ 注意: 在使用此方法处理之前,流的整个文本将作为 python 字符串对象加载到内存中


          如果不需要在内存中缓冲整个流,我们可以使用通信属性和每个输入对之间的前缀共性检查

          $!/bin/bash
          cat mystream | python -c $'import sys\nimport os\nfor line in sys.stdin:\n\tif not os.path.isfile(line.strip()):\n\t\tcontinue\n\tsys.stdout.write(line)\n') | pythoin sys.stdin:\n\tprefix=os.path.commonprefix([line] + ([prefix] if prefix else []))\nsys.stdout.write(prefix)''
          

          长格式

          import sys
          import os
          prefix = None
          for line in sys.stdin:
              prefix=os.path.commonprefix(
                  [line] + ([prefix] if prev else [])
              )
          sys.stdout.write(prefix)
          

          这两种方法都应该是二进制安全的,因为它们不需要将输入/输出数据进行 ascii 或 utf-8 编码,如果遇到编码错误,python 3 将 sys.stdin 重命名为 sys.stdin .buffer 和 sys.stdout 到 sys.stdout.buffer,它不会在使用时自动解码/编码输入/输出流

          【讨论】:

            【解决方案13】:

            如果你有安装 python 包的选项,你可以使用这个python utility

            # install pythonp
            pythonp -m pip install pythonp
            
            echo -e "$string1\n$string2" | pythonp 'l1,l2=lines
            res=itertools.takewhile(lambda a: a[0]==a[1], zip(l1,l2)); "".join(r[0] for r in res)'
            

            【讨论】:

              【解决方案14】:

              我已经概括了@ack 的答案以适应嵌入的换行符。

              我将使用以下字符串数组作为测试用例:

              a=(
                $'/a\n/b/\nc  d\n/\n\ne/f'
                $'/a\n/b/\nc  d\n/\ne/f'
                $'/a\n/b/\nc  d\n/\ne\n/f'
                $'/a\n/b/\nc  d\n/\nef'
              )
              

              通过检查我们可以看到最长的公共前缀是

              $'/a\n/b/\nc  d\n/\n'
              

              我们可以计算这个并将结果保存到一个变量中:

              longest_common_prefix=$(
                printf '%s\0' "${a[@]}" \
                | sed -zE '$!{N;s/^(.*).*\x00\1.*$/\1\x00\1/;D;}' \
                | tr \\0 x # replace trailing NUL with a dummy character ①
              )
              longest_common_prefix=${longest_common_prefix%x} # Remove the dummy character
              echo "${longest_common_prefix@Q}" # ②
              

              结果:

              $'/a\n/b/\nc  d\n/\n'
              

              正如预期的那样。 ✔️

              我在此处的路径规范上下文中应用了此技术:https://unix.stackexchange.com/a/639813


              ①为了在此命令替换中保留任何尾随换行符,我们使用了 usual technique 附加一个随后被截断的虚拟字符。我们使用tr \\0 x 一步将删除尾随NUL 与添加虚拟字符(我们选择x)结合起来。

              ${parameter@Q} 扩展结果是“一个字符串,它是以可重复用作输入的格式引用的参数值”。 –bash reference manual。需要 bash 4.4+ (discussion)。否则,您可以使用以下方法之一检查结果:

              【讨论】:

                猜你喜欢
                • 2012-01-24
                • 1970-01-01
                • 1970-01-01
                • 2017-08-03
                • 2018-05-07
                • 2019-05-03
                • 2020-12-12
                • 2012-02-25
                • 2017-03-19
                相关资源
                最近更新 更多