#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
. Затем:
- Если
curchar
это символ графика (без пробелов), установитеcur
буквы до и послеcurchar
(останавливаясь, пока не дойдете до пробела или начала/конца строки с обеих сторон). - В противном случае, если
prevchar
это символ графика, установитеcur
буквы из предыдущей буквы в обратном порядке, пока не начнется предыдущий символ пробела/строка. - В противном случае, если
nextchar
это символ графика, установитеcur
значение букв из следующей буквы, вперед до следующего символа пробела/конца строки. - Если ни одно из вышеперечисленных условий не выполнено (имеется в виду
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. Спасибо! Я провел еще несколько тестов и в конце концов понял, что происходит. Есть еще много тонких нюансов, которые нужно обсудить, и я не тороплюсь, чтобы разобраться в них.