Rechercher et remplacer dans bash à l'aide d'expressions régulières

161

J'ai vu cet exemple:

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

Ce qui suit cette syntaxe: ${variable//pattern/replacement}

Malheureusement, le patternchamp ne semble pas prendre en charge la syntaxe regex complète (si j'utilise .ou \s, par exemple, il essaie de faire correspondre les caractères littéraux).

Comment puis-je rechercher / remplacer une chaîne en utilisant la syntaxe regex complète?

Lanaru
la source
J'ai
2
FYI, \sne fait pas partie de la syntaxe d'expression régulière définie par POSIX (ni BRE ni ERE); c'est une extension PCRE, et la plupart du temps non disponible depuis le shell. [[:space:]]est l'équivalent le plus universel.
Charles Duffy
1
\speut être remplacé par [[:space:]], au fait, .par ?, et les extensions extglob du langage de modèle de shell de base peuvent être utilisées pour des choses comme des sous-groupes facultatifs, des groupes répétés, etc.
Charles Duffy
J'utilise ceci dans la version bash 4.1.11 sur Solaris ... echo $ {hello // [0-9]} Notez l'absence de la barre oblique finale.
Daniel Liston

Réponses:

176

Utilisez sed :

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

Notez que les suivants -esont traités dans l'ordre. De plus, l' gindicateur de l'expression correspondra à toutes les occurrences de l'entrée.

Vous pouvez également choisir votre outil préféré en utilisant cette méthode, c'est-à-dire perl, awk, par exemple:

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

Cela peut vous permettre de faire plus de correspondances créatives ... Par exemple, dans la capture ci-dessus, le remplacement numérique ne serait pas utilisé à moins qu'il y ait une correspondance sur la première expression (en raison d'une andévaluation paresseuse ). Et bien sûr, vous avez le support linguistique complet de Perl pour faire vos enchères ...

jheddings
la source
Cela ne fait qu'un seul remplacement pour autant que je sache. Existe-t-il un moyen de le remplacer toutes les occurrences du modèle comme le fait le code que j'ai publié?
Lanaru
J'ai mis à jour ma réponse pour démontrer plusieurs remplacements ainsi que la correspondance de modèle globale. Faites-moi savoir si cela vous a été utile.
jheddings
Merci beaucoup! Par curiosité, pourquoi êtes-vous passé d'une version à une ligne (dans votre réponse d'origine) à une version à deux lignes?
Lanaru
9
L'utilisation sedou d'autres outils externes est coûteuse en raison du temps d'initialisation du processus. J'ai particulièrement recherché une solution tout-bash, car j'ai trouvé que l'utilisation des substitutions bash était plus de 3 fois plus rapide que d'appeler sedchaque élément de ma boucle.
rr-
6
@CiroSantilli 六四 事件 法轮功 纳米比亚 威 视, d'accord, c'est la sagesse commune, mais cela ne le rend pas sage. Oui, bash est lent quoi qu'il arrive - mais bash bien écrit qui évite les sous-shell est littéralement des ordres de grandeur plus rapide que bash qui appelle des outils externes pour chaque petite tâche. De plus, les scripts shell bien écrits bénéficieront d'interpréteurs plus rapides (comme ksh93, qui a des performances comparables à celles de awk), alors que les scripts mal écrits, il n'y a rien à faire.
Charles Duffy
133

Cela peut en fait être fait en pure bash:

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

... donne ...

howareyoudoingtodday
Charles Duffy
la source
2
Quelque chose me dit que vous allez adorer ceux-ci: stackoverflow.com/questions/5624969/… =)
nickl-
=~C'est la clé. Mais un peu maladroit, vu la réaffectation dans la boucle. La solution @jheddings 2 ans avant est une autre bonne option - appeler sed ou perl).
Brent Faust du
3
L'appel sedou perlest judicieux, si vous utilisez chaque appel pour traiter plus d'une seule ligne d'entrée. Invoquer un tel outil à l'intérieur d'une boucle, par opposition à l'utilisation d'une boucle pour traiter son flux de sortie, est imprudent.
Charles Duffy
2
FYI, en zsh, c'est juste $matchau lieu de $BASH_REMATCH. (Vous pouvez le faire se comporter comme un bash avec setopt bash_rematch.)
Marian
C'est étrange - dans la mesure où zsh n'essaye pas d'être un shell POSIX, il suit sans doute la lettre des instructions POSIX concernant les variables tout en majuscules utilisées à des fins spécifiées par POSIX (shell ou pertinentes pour le système) et les variables minuscules réservées pour utilisation de l'application. Mais dans la mesure où zsh est quelque chose qui exécute des applications, plutôt qu'une application elle-même, cette décision d'utiliser l'espace de noms des variables d'application plutôt que l'espace de noms système semble terriblement perverse.
Charles Duffy
95

Ces exemples fonctionnent également dans bash sans avoir besoin d'utiliser sed:

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

vous pouvez également utiliser les expressions entre crochets de classe de caractères

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

production

XXNNNNNXXXXNNNNNXXXNNNXNNXNNNNXXXXXXNNNNNXXX

Ce que @Lanaru voulait savoir cependant, si je comprends bien la question, c'est pourquoi les extensions "complètes" ou PCRE, \s\S\w\W\d\Detc. ne fonctionnent pas comme prises en charge dans php ruby ​​python etc. peut ne pas être compatible avec d'autres formes d'expressions régulières basées sur un shell.

Ceux-ci ne fonctionnent pas:

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


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

sortie avec tous les caractères littéraux "d" supprimés

ho02123ware38384you44334o3434ingto38384ay

mais ce qui suit fonctionne comme prévu

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

production

howareyoudoingtodday

J'espère que cela clarifie un peu plus les choses, mais si vous n'êtes pas encore confus, pourquoi ne pas essayer ceci sur Mac OS X qui a le drapeau REG_ENHANCED activé:

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

Sur la plupart des versions de * nix, vous ne verrez que la sortie suivante:

d
d
d

nJoy!

nickl-
la source
6
Pardon? ${foo//$bar/$baz}n'est pas une syntaxe POSIX.2 BRE ou ERE - c'est une correspondance de modèle de style fnmatch ().
Charles Duffy
8
... donc, alors que ${hello//[[:digit:]]/}fonctionne, si nous voulions filtrer uniquement les chiffres précédés par la lettre o, ${hello//o[[:digit:]]*}aurait un comportement entièrement différent de celui attendu (puisque dans les modèles fnmatch, *correspond à tous les caractères, plutôt que de modifier l'élément immédiatement précédent pour être 0 ou plus).
Charles Duffy
1
Voir pubs.opengroup.org/onlinepubs/9699919799/utilities/… (et tout ce qu'il incorpore par référence) pour la spécification complète sur fnmatch.
Charles Duffy
1
man bash: Un opérateur binaire supplémentaire, = ~, est disponible, avec la même priorité que == et! =. Lorsqu'elle est utilisée, la chaîne à droite de l'opérateur est considérée comme une expression régulière étendue et mise en correspondance en conséquence (comme dans regex (3)).
nickl-
1
@aderchox vous avez raison, pour les chiffres que vous pouvez utiliser [0-9]ou[[:digit:]]
nickl-
13

Si vous effectuez des appels répétés et que vous êtes préoccupé par les performances, ce test révèle que la méthode BASH est environ 15 fois plus rapide que le passage à sed et probablement tout autre processus externe.

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]
Josiah DeWitt
la source
1
Si vous êtes intéressé par un moyen de réduire les fourches, recherchez le mot newConnector dans cette réponse à Comment définir une variable sur la sortie d'une commande dans Bash?
F.Hauri
8

Utilisez [[:digit:]](notez les doubles crochets) comme motif:

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

Je voulais juste résumer les réponses ( en particulier @ Nickl-de https://stackoverflow.com/a/22261334/2916086 ).

yegeniy
la source
1

Je sais que c'est un fil ancien, mais c'était mon premier succès sur Google, et je voulais partager ce resubqui suit que j'ai rassemblé, ce qui ajoute la prise en charge de plusieurs références inverses de 1 $, 2 $, etc.

#!/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 concernant :(.*)$match(.*)

Dabe Murphy
la source