Замена средствами bash двух одинаковых символов (но не более двух и не менее двух)

В строках текста использованы символы * как литералы:

string ** string
string ***** string

Возможно ли средствами bash заменить два отдельных символа ** на 2, но оставить символы ***** без изменения? Как sed не крутил, все равно он часть ***** заменяет на 2, то есть так:

string 2 string
string 2*** string

Есть строки и без пробела после звездочек, то есть не только string ** string, но и просто string **, где искомая подстрока появляется в конце текста. Случаев повторения обособленных пар звёздочек нет. Одна строка - одно совпадение, первое и единственное.


Ответы (2 шт):

Автор решения: Pak Uula
sed -e s'/\(^\|[^*]\)\*\*\($\|[^*]\)/\12\2/'

заменить две звёдочки, если

  • перед ними ничего нет (^), или стоит символ, отличный от звёздочки ([^*])
  • после них ничего нет ($), или стоит символ, отличный от звёздочки ([^*])

замена: символ, стоявший перед звёздочками (\1) , 2, символ, стоявший после звёздочек (\2)

Проверка

string**string
string ** string
string ***** string
**
***

результат

string2string
string 2 string
string ***** string
2
***
→ Ссылка
Автор решения: Vitalizzare

Поддерживаю использование 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
→ Ссылка