Извлечение строки из подстроки в bash (да, именно так)

#string #bash #slice

Вопрос:

У меня есть строка из нескольких слов в bash comp_line , которая может содержать любое количество пробелов внутри. Например:

 "foo bar   apple  banana q xy"
 

И у меня есть индекс на основе нуля comp_point , указывающий на один символ в этой строке, например, если comp_point он равен 4, он указывает на первую букву » b «в строке «bar».

На основе comp_point и comp_line в одиночку, я хочу извлечь слово, на которое указывает указатель, где «слово» — это последовательность букв, цифр, знаков препинания или любой другой не-пробельный символ, окруженный пробелами с обеих сторон (если слово находится в начале или конце строки, или только одно слово в строке, он должен работать таким же образом.)

Слово, которое я пытаюсь извлечь, станет cur (текущим словом)

Основываясь на этом, я придумал набор правил:

Прочитайте текущий символ curchar , предыдущий символ prevchar и следующий символ nextchar . Затем:

  1. Если curchar это символ графика (без пробелов), установите cur буквы до и после curchar (останавливаясь, пока не дойдете до пробела или начала/конца строки с обеих сторон).
  2. В противном случае, если prevchar это символ графика, установите cur буквы из предыдущей буквы в обратном порядке, пока не начнется предыдущий символ пробела/строка.
  3. В противном случае, если nextchar это символ графика, установите cur значение букв из следующей буквы, вперед до следующего символа пробела/конца строки.
  4. Если ни одно из вышеперечисленных условий не выполнено (имеется в виду curchar , nextchar и prevchar все они являются пробелами), установите cur значение "" (пустая строка)

Я написал некоторый код, который, как мне кажется, достигает этого. Правила 2, 3 и 4 относительно просты, но правило 1 сложнее всего реализовать — мне пришлось выполнить несколько сложных срезов строк. Я не убежден, что мое решение в любом случае идеально, и хочу знать, знает ли кто-нибудь лучший способ сделать это только в bash (не передавая на аутсорсинг Python или другой более простой язык).

Протестировано на https://rextester.com/l/bash_online_compiler

 #!/bin/bash
# GNU bash, version 4.4.20

comp_line="foo bar   apple  banana q xy"
comp_point=19
cur=""

curchar=${comp_line:$comp_point:1}
prevchar=${comp_line:$((comp_point - 1)):1}
nextchar=${comp_line:$((comp_point   1)):1}
echo "<$prevchar> <$curchar> <$nextchar>"

if [[ $curchar =~ [[:graph:]] ]]; then
    # Rule 1 - Extract current word
    slice="${comp_line:$comp_point}"
    endslice="${slice%% *}"
    slice="${slice#"$endslice"}"
    slice="${comp_line%"$slice"}"
    cur="${slice##* }"
else
    if [[ $prevchar =~ [[:graph:]] ]]; then
        # Rule 2 - Extract previous word
        slice="${comp_line::$comp_point}"
        cur="${slice##* }"
    else
        if [[ $nextchar =~ [[:graph:]] ]]; then
            # Rule 3 - Extract next word
            slice="${comp_line:$comp_point 1}"
            cur="${slice%% *}"
        else
            # Rule 4 - Set cur to empty string ""
            cur=""
        fi
    fi
fi

echo "Cur: <$cur>"
 

В текущем примере будет возвращено значение «банан», comp_point равное 19.

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


(Проблема XY, если кто-нибудь спросит)

Я пишу сценарий завершения вкладки и пытаюсь эмулировать функциональность COMP_WORDS и COMP_CWORD, используя COMP_LINE и COMP_POINT. Когда пользователь вводит команду для завершения вкладки, я хочу определить, какое слово они пытаются завершить на основе последних двух переменных. Я не хочу передавать этот код на аутсорсинг Python, потому что производительность сильно падает, когда Python участвует в завершении вкладки.

Комментарии:

1. Действительно приятно видеть, как кто-то вкладывает столько труда в написание вопроса. Спасибо. Тем не менее, мне интересно, почему вы не используете COMP_CWORD в первую очередь. Может ли это быть другой / фактической проблемой XY?

2. Спасибо! Более широкий контекст таков: завершение вкладки bash имеет особенность , в которой оно разбивает командные слова на COMP_WORDS основе содержимого COMP_WORDBREAKS , которое содержит двоеточие и несколько других символов. Команды, которые я пишу для завершения, используют двоеточия. Мне пришлось разработать обходное решение, которое включает объединение COMP_WORDS в одну строку и повторное разделение ее на пробелы. Но тогда мне нужно правильно определить, где находится курсор пользователя, и если пользователь выбирает символ пустого пробела, добавьте пустую строку обратно COMP_WORDS в правильное положение.

Ответ №1:

Еще один способ в bash без массива.

 #!/bin/bash

string="foo bar   apple  banana q xy"

wordAtIndex() {
  local index=$1 string=$2 ret='' last first
  if [ "${string:index:1}" != " " ] ; then
    last="${string:index}"
    first="${string:0:index}"
    ret="${first##* }${last%% *}"
  fi
  echo "$ret"
}

for ((i=0; i < "${#string}";   i)); do
 printf '%s <-- "%s"n' "${string:i:1}" "$(wordAtIndex "$i" "$string")"
done
 

Комментарии:

1. Спасибо за это. Хотя мне также любопытен ваш синтаксис. Почему круглые скобки вокруг [ ] в вашем условии if? И если уж на то пошло, разве люди обычно не советуют использовать вместо этого двойные квадратные скобки [[ ]] ? Или в вашем случае предпочтительнее использовать один?

2. @Лу, ты прав, здесь не нужны скобки. Я обновляю ответ. Я обычно использую sh, а не bash, где [[ ]] недоступно. Это простой тест здесь, поэтому нет необходимости использовать расширенный тест bash здесь.

Ответ №2:

если кто-нибудь знает лучший способ сделать это только в bash

Используйте регулярные выражения. С ^.{4} помощью вы можете пропустить первые четыре буквы, чтобы перейти к индексу 4. С [[:graph:]]* помощью вы можете сопоставить остальную часть слова с этим индексом. * является жадным и будет соответствовать как можно большему количеству графических символов.

 wordAtIndex() {
  local index=$1 string=$2 left right indexFromRight
  [[ "$string" =~ ^.{$index}([[:graph:]]*) ]]
  right=${BASH_REMATCH[1]}
  ((indexFromRight=${#string}-index-1))
  [[ "$string" =~ ([[:graph:]]*).{$indexFromRight}$ ]]
  left=${BASH_REMATCH[1]}
  echo "$left${right:1}"
}
 

А вот полный тест для вашего примера:

 string="foo bar   apple  banana q xy"
for ((i=0; i < "${#string}";   i)); do
  printf '%s <-- "%s"n' "${string:i:1}" "$(wordAtIndex "$i" "$string")"
done
 

Это выводит входную строку вертикально слева, и в каждом индексе извлекается слово, на которое указывает индекс справа.

 f <-- "foo"
o <-- "foo"
o <-- "foo"
  <-- ""
b <-- "bar"
a <-- "bar"
r <-- "bar"
  <-- ""
  <-- ""
  <-- ""
a <-- "apple"
p <-- "apple"
p <-- "apple"
l <-- "apple"
e <-- "apple"
  <-- ""
  <-- ""
b <-- "banana"
a <-- "banana"
n <-- "banana"
a <-- "banana"
n <-- "banana"
a <-- "banana"
  <-- ""
q <-- "q"
  <-- ""
x <-- "xy"
y <-- "xy"
 

Комментарии:

1. Спасибо за это! У меня еще не было времени протестировать его, но это выглядит как хорошее решение. Ваш тест в настоящее время не соответствует моим требованиям — символ пробела после «яблока» должен возвращать «яблоко», а символ пробела перед «бананом» должен возвращать «банан», например. Только второе пространство между «баром» и «яблоком» должно возвращать пустое пространство, так как оно имеет место до и после него. Но я уверен, что смогу поработать с вашим решением, чтобы разобраться в своих требованиях.

2. Кроме того, я не понимаю, что делает синтаксис двойных квадратных скобок [[]] . Обычно я бы использовал это в заявлении if или с amp;amp; или || . Что это делает, когда у вас есть двойные квадратные скобки сами по себе?

3. Я только что понял, что вел себя глупо. Ваше решение решает правило 1 моего первоначального решения, которое было главным, что я пытался оптимизировать. Я все еще могу без проблем использовать свое оригинальное решение для правил 2, 3 и 4. Так что это должно отлично сработать :). Спасибо!

4. @Лу, я рад, что это сработало для тебя. Что касается [[ ]] внешнего if , amp;amp; , || : [[ Это встроенная команда, точно так cd же , как или read . Наиболее распространенный вариант использования основан на его статусе выхода (например if [[ ... ]] , или [[ ... ]] amp;amp; ), но существует нечто большее, чем просто статус выхода. наш случай [[ string =~ ^.a(b*) ]] переменной BASH_REMATCH задан и позволяет нам извлечь только часть того, что было сопоставлено. BASH_REMATCH[1] относится к группе (b*) .

5. Спасибо! Я провел еще несколько тестов и в конце концов понял, что происходит. Есть еще много тонких нюансов, которые нужно обсудить, и я не тороплюсь, чтобы разобраться в них.