Как найти совпадение двух строк в bash? [закрыто]

11

У меня есть две строки. Для примера они установлены так:

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

Я хочу найти перекрытие, начинающееся в начале строк. Под перекрытием я подразумеваю строку «test t» в моем примере выше.

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

Если бы строки были, string1="atest toast"; string2="test test"они бы не перекрывались, так как проверка начинается с начала, а "a" в начале string1.

кон-е использование
источник
Именно по этой причине люди не должны пересекать; теперь у него есть несколько разных ответов на каждом сайте, и это по теме для обоих сайтов. Я думаю, что я все равно оставлю это здесь
Майкл Мрозек

Ответы:

10

Вы можете думать о такой функции, с некоторой проверкой ошибок, чтобы добавить

common_prefix() {
  local n=0
  while [[ "${1:n:1}" == "${2:n:1}" ]]; do
    ((n++))
  done
  echo "${1:0:n}"
}
enzotib
источник
Я только что заметил, что при запуске с двумя пустыми / нулевыми аргументами он входит в цикл ∞. [[ -z "$1$2" ]] && returnисправляет это.
Peter.O
Этот метод экспоненциально медленнее (а не линейно). Поскольку длина строки удваивается, время увеличивается в 4 раза (приблизительно). Вот некоторые сравнения длины и времени строки с двоичным 64 делением Жиля : .. 0m0,005s против 0m0,003s - 128 0m0,013s против 0m0,003s - 256 0m0,041s против 0m0,003s - 512 0m0,143s против 0m0,005s - 1024 0m0,421s против 0m0,009s - 2048 0m1,575s против 0m0,012s - 4096 0m5,967s против 0m0,022s - 8192 0m24,693s против 0m0,049s -16384 1m34,004 с против 0m0,085 с - 32768 6m34,721 с против 0m0,168 с - 65536 27m34,012 с против 0m0,370 с
Peter.O
2
@ Peter.O Квадратично, а не экспоненциально.
Жиль "ТАК - перестань быть злым"
Я предполагаю, что bash хранит строки внутренне с неявной длиной, поэтому получение nсимвола th требует сканирования nсимволов, чтобы убедиться, что они не заканчивают строку нулевым байтом. Это согласуется с тем, что bash не может хранить нулевой байт в переменной.
Питер Кордес
8

Это можно сделать полностью внутри Bash. Хотя манипулирование строками в цикле в bash является медленным, существует простой алгоритм, логарифмирующий по количеству операций оболочки, поэтому чистый 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"
}
Жиль "ТАК - прекрати быть злым"
источник
Возвращаясь к этому вопросу (1 год), я переоценил лучший ответ. Все довольно просто: камень ломает ножницы, ножницы режут бумагу, бумага обматывает камень. и бинарный ест последовательно! .. даже для очень коротких строк .. и что касается умеренной строки 10000 символов, обрабатываемой последовательно через while char-by-char, я все еще жду этого, когда я пишу это ... проходит время ... все еще жду (может быть, есть что-то не так с моей системой) .. время идет .. должно быть что-то не так; это всего 10000 иттераций! Ах! терпение - добродетель (возможно, проклятие в этом случае) .. 13м53.755с .. против 0м0.322с
Peter.O
3 метода, приведенные здесь, являются самыми быстрыми из всех представленных ответов. По сути, cmpэто самый быстрый (но не основанный на символах). Следующим является iconvи то очень respectibly быстрый binary-splitответ. Спасибо, Жиль. Мне потребовался год, чтобы добраться до этой точки, но лучше поздно, чем никогда. (PS. 2 опечатки модов в iconvкоде: $в =$LC_CTYPE}и \ в UTF-32) \ ) ... PPS. на самом деле строка, о которой я упоминал выше, была длиннее 10 000 символов. Это был результат {1..10000}, то есть 48 894, но это не «меняет дифференциал»
Peter.O
6

В sed предполагается, что строки не содержат символов новой строки:

string1="test toast"
string2="test test"
printf "%s\n" "$string1" "$string2" | sed -e 'N;s/^\(.*\).*\n\1.*$/\1/'
jfg956
источник
Но дублируй с этим .
jfg956
Brilliant! переходит непосредственно к моей библиотеке советов и хитростей :-)
hmontoliu
Или для строки bash , которая не может содержать \0. С помощью метода trand \0можно обрабатывать символы новой строки в строке ....{ printf "%s" "$string1" |tr \\n \\0; echo; printf "%s" "$string2" |tr \\n \\0; echo; } | sed -e 'N;s/^\(.*\).*\n\1.*$/\1/' |tr \\0 \\n
Peter.O
Я только что проверил этот sedметод немного дальше, и кажется, что использование обратных ссылок таким образом (в шаблоне поиска) чрезвычайно дорого. Он по-прежнему превосходит последовательное побитовое зацикливание (примерно в 3 раза), но вот пример: для двух строк по 32 КБ (с разным последним байтом) требуется 2m4.880s, по сравнению с двоичным делением Жиля метод0m0.168s
Peter.O
2

Это кажется мне грубым, но вы можете сделать это с помощью грубой силы:

#!/bin/bash

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

L=1  # Prefix length

while [[ ${string1:0:$L} == ${string2:0:$L} ]]
do
    ((L = L + 1))
done

echo Overlap: ${string1:0:$((L - 1))}

Я хочу, чтобы существовал какой-нибудь умный алгоритм, но я не могу найти ни одного с помощью короткого поиска.

Брюс Эдигер
источник
2
сравните половину и повторите n * log (n), а не n ^ 2.
Жиль "ТАК - перестань быть злым"
2
Для общего ознакомления, это немного на медленной стороне. Две строки символов 32768 (последний символ отличается) заняли 6m27.689s.
Peter.O