Поиск и замена в bash с использованием регулярных выражений

161

Я видел этот пример:

hello=ho02123ware38384you443d34o3434ingtod38384day
echo ${hello//[0-9]/}

Который следует этому синтаксису: ${variable//pattern/replacement}

К сожалению, patternполе не поддерживает полный синтаксис регулярных выражений (если я использую .или \s, например, оно пытается соответствовать буквенным символам).

Как я могу найти / заменить строку, используя полный синтаксис регулярных выражений?

Lanaru
источник
Нашел связанный вопрос здесь: stackoverflow.com/questions/5658085/…
jheddings
2
К вашему сведению, \sне является частью стандартного синтаксиса регулярных выражений, определенного POSIX (ни BRE, ни ERE); это расширение PCRE, и в основном недоступно из оболочки. [[:space:]]является более универсальным эквивалентом.
Чарльз Даффи
1
\sможет быть заменен [[:space:]], кстати, с .помощью ?и extglob расширения языка шаблонов базовой оболочки может использоваться для таких вещей , как дополнительные подгруппы, повторяющихся групп, и тому подобное.
Чарльз Даффи
3
Описание паттернов Bash .
выступление
Я использую это в bash версии 4.1.11 на Solaris ... echo $ {hello // [0-9]} Обратите внимание на отсутствие финального слеша.
Даниэль Листон

Ответы:

176

Используйте sed :

MYVAR=ho02123ware38384you443d34o3434ingtod38384day
echo "$MYVAR" | sed -e 's/[a-zA-Z]/X/g' -e 's/[0-9]/N/g'
# prints XXNNNNNXXXXNNNNNXXXNNNXNNXNNNNXXXXXXNNNNNXXX

Обратите внимание, что последующие -eобрабатываются по порядку. Кроме того, gфлаг для выражения будет соответствовать всем вхождениям на входе.

Вы также можете выбрать свой любимый инструмент, используя этот метод, например, perl, awk, например:

echo "$MYVAR" | perl -pe 's/[a-zA-Z]/X/g and s/[0-9]/N/g'

Это может позволить вам делать больше творческих соответствий ... Например, в приведенном выше фрагменте замена чисел не будет использоваться, если в первом выражении не будет совпадения (из-за отложенной andоценки). И, конечно, у вас есть полная языковая поддержка Perl для выполнения ваших ставок ...

jheddings
источник
Это только делает одну замену, насколько я могу судить. Есть ли способ, чтобы он заменил все вхождения шаблона, как то, что делает код, который я разместил?
Ланару
Я обновил свой ответ, чтобы продемонстрировать множественные замены, а также глобальное сопоставление с образцом. Дайте мне знать, если это поможет.
Jheddings
Спасибо! Из любопытства, почему вы переключились с однострочной версии (в своем первоначальном ответе) на двухслойную?
Ланару
9
Использование sedили других внешних инструментов стоит дорого из-за времени инициализации процесса. Я особенно искал решение для всего bash, потому что я обнаружил, что использование замен bash более чем в 3 раза быстрее, чем вызов sedкаждого элемента в моем цикле.
rr-
6
@CiroSantilli granted 事件 法轮功 纳米比亚 威 视, да, это обычная мудрость, но это не делает ее мудрой. Да, bash медленный, несмотря ни на что - но хорошо написанный bash, который избегает подоболочек, буквально на несколько порядков быстрее, чем bash, который вызывает внешние инструменты для каждой крошечной задачи. Кроме того, хорошо написанные сценарии оболочки выиграют от более быстрых интерпретаторов (например, ksh93, производительность которого наравне с awk), тогда как плохо написанные сценарии не для чего.
Чарльз Даффи
133

На самом деле это можно сделать в чистом виде:

hello=ho02123ware38384you443d34o3434ingtod38384day
re='(.*)[0-9]+(.*)'
while [[ $hello =~ $re ]]; do
  hello=${BASH_REMATCH[1]}${BASH_REMATCH[2]}
done
echo "$hello"

... дает ...

howareyoudoingtodday
Чарльз Даффи
источник
2
Что-то подсказывает мне, что вы полюбите это: stackoverflow.com/questions/5624969/… =)
nickl-
=~это ключ. Но немного неуклюже, учитывая переназначение в цикле. Решение @jheddings за 2 года до этого - еще один хороший вариант - вызов sed или perl).
Брент Фауст
3
Вызов sedили perlимеет смысл, если использовать каждый вызов для обработки более чем одной строки ввода. Вызывать такой инструмент внутри цикла, в отличие от использования цикла для обработки его выходного потока, безрассудно.
Чарльз Даффи
2
К вашему сведению, в Zsh, это просто $matchвместо $BASH_REMATCH. (Вы можете заставить его вести себя как удар setopt bash_rematch.)
Мариан
Это странно - поскольку zsh не пытается быть оболочкой POSIX, возможно, он следует букве руководства POSIX о том, что переменные all-caps используются для целей, определенных POSIX (для оболочки или системы), а переменные в нижнем регистре зарезервированы для использование приложения. Но поскольку zsh - это то, что запускает приложения, а не само приложение, это решение использовать пространство имен переменных приложения, а не пространство имен системы, выглядит ужасно извращенным.
Чарльз Даффи
95

Эти примеры также работают в bash, нет необходимости использовать sed:

#!/bin/bash
MYVAR=ho02123ware38384you443d34o3434ingtod38384day
MYVAR=${MYVAR//[a-zA-Z]/X} 
echo ${MYVAR//[0-9]/N}

Вы также можете использовать скобочные выражения класса символов

#!/bin/bash
MYVAR=ho02123ware38384you443d34o3434ingtod38384day
MYVAR=${MYVAR//[[:alpha:]]/X} 
echo ${MYVAR//[[:digit:]]/N}

вывод

XXNNNNNXXXXNNNNNXXXNNNXNNXNNNNXXXXXXNNNNNXXX

Однако @Lanaru хотел знать, если я правильно понимаю вопрос, почему «полные» или расширения PCRE и \s\S\w\W\d\Dт. Д. Не работают так, как это поддерживается в php ruby ​​python и т. Д. Эти расширения взяты из Perl-совместимых регулярных выражений (PCRE) и может быть несовместимо с другими формами регулярных выражений на основе оболочки.

Это не работает:

#!/bin/bash
hello=ho02123ware38384you443d34o3434ingtod38384day
echo ${hello//\d/}


#!/bin/bash
hello=ho02123ware38384you443d34o3434ingtod38384day
echo $hello | sed 's/\d//g'

вывод с удалением всех буквенных символов "d"

ho02123ware38384you44334o3434ingto38384ay

но следующее работает как ожидалось

#!/bin/bash
hello=ho02123ware38384you443d34o3434ingtod38384day
echo $hello | perl -pe 's/\d//g'

вывод

howareyoudoingtodday

Надеюсь, это проясняет ситуацию немного больше, но если вы еще не запутались, почему бы не попробовать это на Mac OS X, у которой включен флаг REG_ENHANCED:

#!/bin/bash
MYVAR=ho02123ware38384you443d34o3434ingtod38384day;
echo $MYVAR | grep -o -E '\d'

На большинстве разновидностей * nix вы увидите только следующий вывод:

d
d
d

NJoy!

nickl-
источник
6
Pardon? ${foo//$bar/$baz}это не POSIX.2 BRE или ERE синтаксис - это fnmatch () - сопоставление с шаблоном.
Чарльз Даффи
8
... итак, тогда как ${hello//[[:digit:]]/}работает, если бы мы хотели отфильтровывать только цифры, перед которыми стоит буква o, ${hello//o[[:digit:]]*}поведение было бы совершенно иным, чем ожидалось (так как в шаблонах fnmatch, *совпадает со всеми символами, вместо изменения непосредственно предшествующего элемента, чтобы 0 или-больше).
Чарльз Даффи
1
См. Pubs.opengroup.org/onlinepubs/9699919799/utilities/… (и все, что он включает в себя путем ссылки) для полной спецификации fnmatch.
Чарльз Даффи
1
man bash: доступен дополнительный бинарный оператор = ~ с тем же приоритетом, что и == и! =. Когда это используется, строка справа от оператора считается расширенным регулярным выражением и соответствует соответствующим образом (как в регулярном выражении (3)).
nickl-
1
@aderchox вы правы, для цифр вы можете использовать [0-9]или[[:digit:]]
nickl-
13

Если вы делаете повторные вызовы и обеспокоены производительностью, этот тест показывает, что метод BASH в ~ 15 раз быстрее, чем разветвление в sed и, вероятно, любой другой внешний процесс.

hello=123456789X123456789X123456789X123456789X123456789X123456789X123456789X123456789X123456789X123456789X123456789X

P1=$(date +%s)

for i in {1..10000}
do
   echo $hello | sed s/X//g > /dev/null
done

P2=$(date +%s)
echo $[$P2-$P1]

for i in {1..10000}
do
   echo ${hello//X/} > /dev/null
done

P3=$(date +%s)
echo $[$P3-$P2]
Джосия ДеВитт
источник
1
Если вы заинтересованы в том, как уменьшить число разветвлений, найдите в этом ответе
Ф. Хаури
8

Используйте [[:digit:]](обратите внимание на двойные скобки) в качестве шаблона:

$ hello=ho02123ware38384you443d34o3434ingtod38384day
$ echo ${hello//[[:digit:]]/}
howareyoudoingtodday

Просто хотел обобщить ответы (особенно на @ nickl-'s https://stackoverflow.com/a/22261334/2916086 ).

yegeniy
источник
1

Я знаю, что это древняя ветка, но это был мой первый успех в Google, и я хотел бы поделиться следующим, resubчто я собрал, добавив поддержку нескольких обратных ссылок $ 1, $ 2 и т. Д. ...

#!/usr/bin/env bash

############################################
###  resub - regex substitution in bash  ###
############################################

resub() {
    local match="$1" subst="$2" tmp

    if [[ -z $match ]]; then
        echo "Usage: echo \"some text\" | resub '(.*) (.*)' '\$2 me \${1}time'" >&2
        return 1
    fi

    ### First, convert "$1" to "$BASH_REMATCH[1]" and 'single-quote' for later eval-ing...

    ### Utility function to 'single-quote' a list of strings
    squot() { local a=(); for i in "$@"; do a+=( $(echo \'${i//\'/\'\"\'\"\'}\' )); done; echo "${a[@]}"; }

    tmp=""
    while [[ $subst =~ (.*)\${([0-9]+)}(.*) ]] || [[ $subst =~ (.*)\$([0-9]+)(.*) ]]; do
        tmp="\${BASH_REMATCH[${BASH_REMATCH[2]}]}$(squot "${BASH_REMATCH[3]}")${tmp}"
        subst="${BASH_REMATCH[1]}"
    done
    subst="$(squot "${subst}")${tmp}"

    ### Now start (globally) substituting

    tmp=""
    while read line; do
        counter=0
        while [[ $line =~ $match(.*) ]]; do
            eval tmp='"${tmp}${line%${BASH_REMATCH[0]}}"'"${subst}"
            line="${BASH_REMATCH[$(( ${#BASH_REMATCH[@]} - 1 ))]}"
        done
        echo "${tmp}${line}"
    done
}

resub "$@"

##################
###  EXAMPLES  ###
##################

###  % echo "The quick brown fox jumps quickly over the lazy dog" | resub quick slow
###    The slow brown fox jumps slowly over the lazy dog

###  % echo "The quick brown fox jumps quickly over the lazy dog" | resub 'quick ([^ ]+) fox' 'slow $1 sheep'
###    The slow brown sheep jumps quickly over the lazy dog

###  % animal="sheep"
###  % echo "The quick brown fox 'jumps' quickly over the \"lazy\" \$dog" | resub 'quick ([^ ]+) fox' "\"\$low\" \${1} '$animal'"
###    The "$low" brown 'sheep' 'jumps' quickly over the "lazy" $dog

###  % echo "one two three four five" | resub "one ([^ ]+) three ([^ ]+) five" 'one $2 three $1 five'
###    one four three two five

###  % echo "one two one four five" | resub "one ([^ ]+) " 'XXX $1 '
###    XXX two XXX four five

###  % echo "one two three four five one six three seven eight" | resub "one ([^ ]+) three ([^ ]+) " 'XXX $1 YYY $2 '
###    XXX two YYY four five XXX six YYY seven eight

H / T в @Charles Duffy re:(.*)$match(.*)

Дэйб Мерфи
источник