Замена средствами bash двух одинаковых символов (но не более двух и не менее двух)
В строках текста использованы символы * как литералы:
string ** string
string ***** string
Возможно ли средствами bash заменить два отдельных символа ** на 2, но оставить символы ***** без изменения? Как sed не крутил, все равно он часть ***** заменяет на 2, то есть так:
string 2 string
string 2*** string
Есть строки и без пробела после звездочек, то есть не только string ** string, но и просто string **, где искомая подстрока появляется в конце текста. Случаев повторения обособленных пар звёздочек нет. Одна строка - одно совпадение, первое и единственное.
Ответы (2 шт):
sed -e s'/\(^\|[^*]\)\*\*\($\|[^*]\)/\12\2/'
заменить две звёдочки, если
- перед ними ничего нет (
^), или стоит символ, отличный от звёздочки ([^*]) - после них ничего нет (
$), или стоит символ, отличный от звёздочки ([^*])
замена: символ, стоявший перед звёздочками (\1) , 2, символ, стоявший после звёздочек (\2)
Проверка
string**string
string ** string
string ***** string
**
***
результат
string2string
string 2 string
string ***** string
2
***
Поддерживаю использование sed. Текст ниже - размышления о возможностях непосредственно bash.
1. Пара символов ограничена пробелами
Если есть ограничения по краям (например, в виде пробелов), то замену можно сделать через Parameter Expansion. Для этого используем конструкцию
${parameter/pattern/string} # заменить первую найденную подстроку по паттерну
где parameter - это переменная с исходной строкой; pattern - что нужно заменить; string - новая подстрока.
$ echo 'string ** string
string ***** string' | while read x; do echo "${x/ \*\* / 2 }"; done
string 2 string
string ***** string
2. Замена первой обособленной пары без заданных ограничений
Чтобы разобраться с общим случаем, уточним задачу:
Заменить средствами Bash первую обособленную подстроку
**("обособленная" значит не граничит с другими*).
Из исходного вопроса не ясно, как быть в случае повторения пары. Например, "some **_** string" должна превратиться в "some 2_** string" или "some 2_2 string"?
Предположим, что нужна ровно одна замена. Тогда первым шагом мы поставим по краям заданной строки parameter отличный от * символ, чтобы пара ** не примыкала к краю:
parameter=" $parameter "
Следующим шагом мы проверяем обновленную строку на соответствие паттерну через [[ "$parameter" =~ $pattern ]], где pattern='([^*])\*\*([^*])'. В этом паттерне две группы - символ перед и после **. В результате сравнения будет заполнен массив BASH_REMATCH, в нулевой позиции которого находится найденная подстрока, а в первой и второй - соответствующие группы паттерна. Если вы знакомы с Python, то можете провести парралель с методом Match.group. Важно: кавычек вокруг паттерна быть не должно, чтобы он не воспринимался как строка; кавычки вокруг $parameter поставлены на всякий случай (я не замечал ошибок, если их убрать, но ручаться головой не буду).
Теперь применим Parameter Expansion:
parameter="${parameter/"${BASH_REMATCH[0]}"/"${BASH_REMATCH[1]}${new_str}${BASH_REMATCH[2]}"}"
Здесь new_str - подставляемая строка (в вашем случае это 2), а двойные кавычки внутри замены /"..."/"..." используются чтобы Bash работал с подставляемыми значениями как строками, а не требующми замены спецсимволами. Важно: группы не должны быть пустыми, чтобы точно позиционировать обособленную пару **; именно для этого мы добавили символы по краям строки, а вместо групп (^|[^*]) и ($|[^*]) ищем ([^*]).
Конечный результат получаем как подстроку со второго по предпоследний символ (индексация начинается с нуля):
answer="${parameter:1:-1}"
Пример:
data='** **
*** **
tricky *** **
some **-***-** string
my * string
my ** string
my *** string'
echo "$data" |\
while read param; do
param=" ${param} "
if [[ "$param" =~ ([^*])\*\*([^*]) ]]; then
param="${param/"${BASH_REMATCH[0]}"/"${BASH_REMATCH[1]}2${BASH_REMATCH[2]}"}"
fi
echo "${param:1:-1}"
done
# Результат:
# 2 **
# *** 2
# tricky *** 2
# some 2-***-** string
# my * string
# my 2 string
# my *** string
3. Замена всех обособленных пар
Чтобы заменить все вхождения обособленных пар **, мы либо модернизирум предложенный ответ с использованием sed, добавив в конец g (global), либо в предыдущем решении меняем if на while:
echo "$data" |\
while read param; do
param=" ${param} "
while [[ "$param" =~ ([^*])\*\*([^*]) ]]; do
param="${param/"${BASH_REMATCH[0]}"/"${BASH_REMATCH[1]}2${BASH_REMATCH[2]}"}"
done
echo "${param:1:-1}"
done
# Результат:
# 2 2
# *** 2
# tricky *** 2
# some 2-***-2 string
# my * string
# my 2 string
# my *** string