bash中两个字符串的最长公共前缀
我有两个字符串。对于这个例子的目的,他们是这样设置: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
开始。
在SED,假设字符串不包含任何换行符:
string1="test toast"
string2="test test"
printf "%s\n%s\n" "$string1" "$string2" | sed -e 'N;s/^\(.*\).*\n\1.*$/\1/'
注意不是所有的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
GNU的sed还支持'\ x0','printf的 '%S \ X0%s' 的 “$字符串1”, “$字符串2” | sed的/ \(。* \)。* \ x0 \ 1。*/\ 1 /''更安全。如果你正在处理路径名并且想要一个通用的路径前缀,那么在'\(。*/\)'中为'\(。* \)'分支' – jthill
@jthill有一个好主意,但是sed命令也必须被修改来处理换行符,例如:'printf'%s \ x0%s \ n'“$ string1”“$ string2”| sed'H; $!d; g; s/\'。\(。* \)。* \ x0 \ 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"
这非常快,但存在一些问题。 (1)它不处理哑字节字符。这很容易修复..只是将'%c'改成'%s' ..(2)当两个字符串完全相同时,报告不正确,除了一个字符后面有'\ n',另一个没有。在这种情况下,脚本会报告更长的值...更正拖尾换行问题可能不太容易解决,因为它是“awk”的行为,会附加一个尾随换行符(导致问题)。但是,当我写这篇文章的时候,我记得有一种方法可以检测'awk'中的'last-line'(我想!)。我现在检查。 –
我在考虑'perl'的'(eof)',但是你可以通过[延迟处理每个输入行]来阻止最终的'OFS'自动输出(http://stackoverflow.com/questions/1646633/ how-to-detect-eof-in-awk)..还有一点:'echo“$ 2”'附加一个额外的'\ n'到'$ 2' –
Hi Karoly。 [Again me](http://stackoverflow.com/a/6973184/938111)!在这里,你的脚本也有类似的问题:'awk'BEGIN {FS =“”} {n = 0; while(n 它显示'/ aa/b /'而不是'/ aa/b'。请尝试改进您的[tag:awk]脚本;-)干杯 – olibre
这也可能是另一种语言简单。这里是我的解决方案:
common_bit=$(perl -le '($s,$t)[email protected];for(split//,$s){last unless $t=~/^\Q$z$_/;$z.=$_}print $z' "$string1" "$string2")
如果这不是一个衬垫,我会使用更长的变量名,更多的空白,多个支架,等我也肯定有一个更快的方法,甚至在Perl ,但是,它又是速度和空间之间的折衷:这在已经很长的单线上使用更少的空间。
好了,在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的功能。而且,我可以说,有点丑陋,太:-)
没有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是一个更好的解决方案,将被启动。 – jfg956
工具的好选择,但错误的预处理和后处理。 'echo“$ string1”'摧毁了一些字符串,当其中一个字符串是另一个字符串的前缀时,您不处理这种情况。您不需要调用'cut',因为shell完全能够从'cmp'输出中提取偏移量。这种方法的一个限制是'cmp'对字节进行操作,而不是字符。 – Gilles
@Gilles:你能告诉我一个例子,其中'echo'破坏了一个字符串吗?在bash的人,我发现用'回声-e“TOTO \ ntata”'一个例子,所以这将是安全的使用'回声-E'(对于printf的例子感谢虽然)。关于字符串是另一个字符串的前缀的情况,我没有'cmp(GNU diffutils)2.8.1'的不同输出。对于避免“切割”的可能性是真实的,对于不处理多字节字符是完全正确的。 – jfg956
这可以完全在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"
}
只是又一种使用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
的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[*]}")
这里的字符串(与所有重定向一样)可以在任何地方使用简单的命令。
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
另一种变型,使用GNU的grep:
$ string1="test toast"
$ string2="test test"
$ grep -zPo '(.*).*\n\K\1' <<< "$string1"$'\n'"$string2"
test t
这似乎比sed方法(Linux,Mac)更具可移植性, – MattK
如果使用其他语言,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
喔人,这是很好的看到别人用这种挣扎,以及:d –
@ajreal:提供的功能有相当冗长,不与琴弦的空间工作。无论如何,我的问题是重复的。对不起。将在那里发表评论 –
不是重复的:交叉点需求是不一样的。 – jfg956