2011-08-07 55 views
21

我有两个字符串。对于这个例子的目的,他们是这样设置:bash中两个字符串的最长公共前缀

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"他们将有没有重叠,因为检查开始形成之初,“一”在string1开始。

+0

喔人,这是很好的看到别人用这种挣扎,以及:d –

+0

@ajreal:提供的功能有相当冗长,不与琴弦的空间工作。无论如何,我的问题是重复的。对不起。将在那里发表评论 –

+1

不是重复的:交叉点需求是不一样的。 – jfg956

回答

26

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

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

注意不是所有的SEDS支持“\ n”个替代命令([苹果不(https://developer.apple.com/库/ MAC /文档/达尔文/参考/手册页/ MAN1/sed.1.html)),但[GNU的SED(https://www.gnu.org/software/sed/manual/sed.html)一样。读者可能需要运行'gsed'而不是'sed'。 – outis

+2

GNU的sed还支持'\ x0','printf的 '%S \ X0%s' 的 “$字符串1”, “$字符串2” | sed的/ \(。* \)。* \ x0 \ 1。*/\ 1 /''更安全。如果你正在处理路径名并且想要一个通用的路径前缀,那么在'\(。*/\)'中为'\(。* \)'分支' – jthill

+0

@jthill有一个好主意,但是sed命令也必须被修改来处理换行符,例如:'printf'%s \ x0%s \ n'“$ string1”“$ string2”| sed'H; $!d; g; s/\'。\(。* \)。* \ x0 \ 1。*/\ 1 \''' –

1

男人,这很难。这是一个非常简单的任务,但我不知道如何与外壳做到这一点:)

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

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

这非常快,但存在一些问题。 (1)它不处理哑字节字符。这很容易修复..只是将'%c'改成'%s' ..(2)当两个字符串完全相同时,报告不正确,除了一个字符后面有'\ n',另一个没有。在这种情况下,脚本会报告更长的值...更正拖尾换行问题可能不太容易解决,因为它是“awk”的行为,会附加一个尾随换行符(导致问题)。但是,当我写这篇文章的时候,我记得有一种方法可以检测'awk'中的'last-line'(我想!)。我现在检查。 –

+0

我在考虑'perl'的'(eof)',但是你可以通过[延迟处理每个输入行]来阻止最终的'OFS'自动输出(http://stackoverflow.com/questions/1646633/ how-to-detect-eof-in-awk)..还有一点:'echo“$ 2”'附加一个额外的'\ n'到'$ 2' –

+0

Hi Karoly。 [Again me](http://stackoverflow.com/a/6973184/938111)!在这里,你的脚本也有类似的问题:'awk'BEGIN {FS =“”} {n = 0; while(n <= NF){if($ n == substr(test,n,1)){printf(“%c”,$ n);} n ++;} print“”}'test =“/ aa/bc /“<<<'/ aa/bd /''=>它显示'/ aa/b /'而不是'/ aa/b'。请尝试改进您的[tag:awk]脚本;-)干杯 – olibre

3

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

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

如果这不是一个衬垫,我会使用更长的变量名,更多的空白,多个支架,等我也肯定有一个更快的方法,甚至在Perl ,但是,它又是速度和空间之间的折衷:这在已经很长的单线上使用更少的空间。

2

好了,在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的功能。而且,我可以说,有点丑陋,太:-)

3

没有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))} 
+0

尽管使用sed是一个更好的解决方案,将被启动。 – jfg956

+2

工具的好选择,但错误的预处理和后处理。 'echo“$ string1”'摧毁了一些字符串,当其中一个字符串是另一个字符串的前缀时,您不处理这种情况。您不需要调用'cut',因为shell完全能够从'cmp'输出中提取偏移量。这种方法的一个限制是'cmp'对字节进行操作,而不是字符。 – Gilles

+0

@Gilles:你能告诉我一个例子,其中'echo'破坏了一个字符串吗?在bash的人,我发现用'回声-e“TOTO \ ntata”'一个例子,所以这将是安全的使用'回声-E'(对于printf的例子感谢虽然)。关于字符串是另一个字符串的前缀的情况,我没有'cmp(GNU diffutils)2.8.1'的不同输出。对于避免“切割”的可能性是真实的,对于不处理多字节字符是完全正确的。 – jfg956

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" 
} 
+0

该解决方案处理多字节字符的一种变体是使用diff而不是cmp,并将其用作输入“printf%s”$ 1“|折叠1“。 – jfg956

+0

@jfgagne不完全,这会抑制换行符。顺便说一下,我喜欢你的sed解决方案,但它并不总是适用于多行字符串。 – Gilles

2

只是又一种使用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 
9

的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[*]}") 

这里的字符串(与所有重定向一样)可以在任何地方使用简单的命令。

5

grep的短变异(创意来自sed的一个借来的):

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

假设字符串没有新行字符。但很容易可以调整使用任何分隔符。

更新于2016年10月24日:在grep的现代版本,您可能会收到抱怨grep: unescaped^or $ not supported with -Pz,只需使用\A代替^

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

另一种变型,使用GNU的grep:

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

这似乎比sed方法(Linux,Mac)更具可移植性, – MattK

0

如果使用其他语言,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

相关问题