2011-08-07 83 views
21

Tôi có hai chuỗi. Vì lợi ích của ví dụ, chúng được đặt như sau:Tiền tố chung dài nhất của hai chuỗi trong bash

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

Điều tôi muốn là tìm chồng chéo bắt đầu ở đầu chuỗi. Với chồng chéo tôi có nghĩa là chuỗi "test t" trong ví dụ trên của tôi.

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

Nếu chuỗi đã string1="atest toast"; string2="test test" họ sẽ không có sự chồng chéo kể từ ngày séc bắt đầu hình thành đầu và "a" vào đầu string1.

+0

ohh người đàn ông, thật tốt khi thấy rằng những người khác đấu tranh với điều này: D –

+0

@ajreal: Chức năng được cung cấp khá dài và không hoạt động với khoảng trống trong chuỗi. Không ít câu hỏi của tôi là một bản sao. Xin lỗi vì chuyện đó. Sẽ đăng một bình luận ở đó –

+1

Không trùng lặp: nhu cầu giao lộ không giống nhau. – jfg956

Trả lời

26

Trong sed, giả sử các chuỗi không chứa bất kỳ ký tự newline:

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

Lưu ý rằng không phải tất cả seds đều hỗ trợ" \ n "trong các lệnh thay thế ([Apple không] (https://developer.apple.com/ library/mac/documentation/Darwin/Reference/ManPages/man1/sed.1.html)), nhưng [Gnu's sed] (https://www.gnu.org/software/sed/manual/sed.html) có. Người đọc có thể cần phải chạy 'gsed' thay vì' sed'. – outis

+2

GNU sed cũng hỗ trợ '\ x0',' printf '% s \ x0% s' "$ string1" "$ string2" | sed 's/\ (. * \). * \ x0 \ 1. */\ 1 /' 'thậm chí còn an toàn hơn. Nếu bạn đang làm việc với các tên đường dẫn và muốn có một tiền tố đường dẫn chung, hãy đặt trong '\ (. */\)' Cho '\ (. * \)' – jthill

+0

@ jthill có một ý tưởng hay nhưng lệnh sed cũng phải được sửa đổi để xử lý các dòng mới, chẳng hạn như: '' printf '% s \ x0% s \ n' "$ string1" "$ string2" | sed 'H; $! d; g; s/\ '. \ (. * \). * \ x0 \ 1. */\ 1 /'' ' –

1

Người đàn ông, điều này thật khó khăn. Đó là một nhiệm vụ cực kỳ tầm thường, nhưng tôi không biết làm thế nào để làm điều này với vỏ :)

đây là một giải pháp xấu xí:

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

Điều này rất nhanh, như-là, nhưng có một vài vấn đề. (1) Nó không xử lý các ký tự mumti-byte. Điều này là dễ dàng cố định .. chỉ cần thay đổi '% c' để'% s' .. (2) Nó báo cáo không chính xác khi hai chuỗi giống hệt nhau hơn một có một dấu '\ n' và khác không. Trong trường hợp này, kịch bản báo cáo giá trị dài hơn ... Việc sửa chữa vấn đề dòng mới có lẽ không dễ dàng sửa, vì nó là hành vi của 'awk' whch sẽ nối thêm một dòng mới (gây ra vấn đề). Nhưng, khi tôi viết điều này, tôi nhớ lại rằng có một cách để phát hiện 'dòng cuối cùng' trong 'awk' (tôi nghĩ vậy). Tôi sẽ kiểm tra ngay bây giờ. –

+0

Tôi đã suy nghĩ về 'perl' của' (eof) ', nhưng bạn có thể ngăn chặn đầu ra tự động cuối cùng của' OFS' thông qua [xử lý chậm trễ của mỗi dòng đầu vào] (http://stackoverflow.com/questions/1646633/ how-to-detect-eof-in-awk) .. Một điểm nữa: 'echo '$ 2" 'gắn thêm một' \ n' không liên quan đến '$ 2' –

+0

Hi Karoly. [Một lần nữa tôi] (http://stackoverflow.com/a/6973184/938111)! Ở đây, kịch bản của bạn cũng có một vấn đề tương tự: 'awk 'BEGIN {FS =" "} {n = 0; while (n <= NF) {if ($ n == substr (test, n, 1)) {printf ("% c", $ n);} n ++;} in ""} 'test = "/ aa/bc/"<<< '/ aa/bd /'' => nó hiển thị '/ aa/b /' thay vì '/ aa/b'. Hãy cố gắng cải thiện tập lệnh [tag: awk] ;-) Chúc mừng – olibre

3

Có lẽ đơn giản bằng ngôn ngữ khác. Đây là giải pháp của tôi:

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

Nếu đây không phải là một lớp lót, tôi sẽ sử dụng tên biến dài hơn, khoảng trắng, dấu ngoặc ôm, v.v. Tôi cũng chắc chắn có cách nhanh hơn, ngay cả trong perl , nhưng, một lần nữa, đó là một sự cân bằng giữa tốc độ và không gian: điều này sử dụng ít không gian hơn trên những gì đã là một lớp lót dài.

2

Ok, trong 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}" 

Đó là thuật toán tương tự như trong các ngôn ngữ khác, nhưng chức năng bash tinh khiết. Và, tôi có thể nói, một chút xấu xí, quá :-)

3

Without sed, sử dụng tiện ích CMP để có được chỉ số của ký tự khác thứ 1 và sử dụng quá trình thay thế để nhận 2 chuỗi thành 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

Sử dụng sed là giải pháp tốt hơn, vì chỉ cần một quá trình sẽ được tung ra. – jfg956

+2

Lựa chọn công cụ tốt, nhưng sai trước và sau xử lý. 'echo" $ string1 "' mangles một vài chuỗi và bạn không xử lý trường hợp khi một trong các chuỗi là tiền tố của một chuỗi khác. Bạn không cần cuộc gọi đến 'cut' vì shell hoàn toàn có khả năng giải nén offset từ đầu ra' cmp'. Một hạn chế của phương pháp này là 'cmp' hoạt động trên byte chứ không phải ký tự. – Gilles

+0

@Gilles: Bạn có thể chỉ cho tôi một ví dụ trong đó 'echo' mangles một chuỗi không? Trong bash của người đàn ông, tôi tìm thấy một ví dụ với 'echo -e" toto \ ntata "', do đó, nó sẽ được an toàn để sử dụng 'echo -E' (cảm ơn cho ví dụ printf). Về trường hợp một chuỗi là tiền tố của một chuỗi khác, tôi không có đầu ra khác với 'cmp (GNU diffutils) 2.8.1'. Đúng về khả năng tránh "cắt", và hoàn toàn đúng về việc không làm việc trên multibytes char. – jfg956

6

Điều này có thể được thực hiện hoàn toàn bên trong. Mặc dù thực hiện thao tác chuỗi trong một vòng lặp trong bash là chậm, có một thuật toán đơn giản là lôgarit trong số hoạt động của vỏ, vì vậy bash thuần túy là một lựa chọn khả thi ngay cả đối với các chuỗi dài.

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

Hộp công cụ tiêu chuẩn bao gồm cmp để so sánh tệp nhị phân. Theo mặc định, nó cho biết độ lệch byte của các byte khác nhau đầu tiên. Có một trường hợp đặc biệt khi một chuỗi là tiền tố của một chuỗi khác: cmp tạo một thông báo khác trên STDERR; một cách dễ dàng để giải quyết vấn đề này là lấy chuỗi nào ngắn nhất.

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

Lưu ý rằng cmp hoạt động trên byte, nhưng thao tác chuỗi của bash hoạt động trên ký tự. Điều này tạo nên sự khác biệt về miền địa phương nhiều byte, ví dụ cho các miền địa phương bằng cách sử dụng bộ ký tự UTF-8. Hàm trên in ra tiền tố dài nhất của chuỗi byte. Để xử lý các chuỗi ký tự bằng phương thức này, trước tiên chúng ta có thể chuyển đổi các chuỗi thành mã hóa có độ rộng cố định. Giả sử bộ ký tự của miền địa phương là một tập con của Unicode, UTF-32 phù hợp với hóa đơn.

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

Một biến thể của giải pháp này làm việc trên các ký tự multibytes sẽ là sử dụng diff thay vì cmp, và sử dụng như đầu vào của nó 'printf% s" $ 1 "| fold -w 1'. – jfg956

+0

@jfgagne Không hoàn toàn, điều này sẽ ngăn chặn các ký tự dòng mới. Nhân tiện, tôi thích giải pháp sed của bạn, nhưng nó không phải lúc nào cũng hoạt động với các chuỗi đa luồng. – Gilles

2

Chỉ là một cách khác bằng cách sử dụng 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

Một phiên bản cải tiến của ví dụ sed, điều này thấy tiền tố chung của N chuỗi (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' 

Nếu chuỗi được lưu trữ trong một mảng, họ có thể được đường ống để sed với printf:

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

Bạn cũng có thể sử dụng một 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[*]}") 

Chuỗi tại đây (giống như tất cả các chuyển hướng) có thể đi bất cứ đâu trong một lệnh đơn giản.

5

Grep biến thể ngắn (ý tưởng vay mượn từ sed một):

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

Giả chuỗi không có ký tự dòng mới. Nhưng dễ dàng có thể được điều chỉnh để sử dụng bất kỳ dấu phân cách nào.

Cập nhật lúc 2016/10/24: Trên các phiên bản hiện đại của grep bạn có thể nhận phàn nàn grep: unescaped^or $ not supported with -Pz, chỉ cần sử dụng \A thay vì ^:

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

Tuy nhiên, biến thể khác, sử dụng GNU grep:

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

Điều này có vẻ di động hơn các phương pháp tiếp cận sed (Linux, Mac) – MattK

0

Nếu sử dụng các ngôn ngữ khác, làm thế nào về 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 đến @RickardSjogren's answer to stack overflow 18715688)

Các vấn đề liên quan