moyen le plus court de remplacer des caractères dans une variable

18

Il existe de nombreuses façons de remplacer des caractères dans une variable.

Le moyen le plus court que j'ai découvert est trjusqu'à présent:

OUTPUT=a\'b\"c\`d_123and_a_lot_more
OUTPUT=$(echo "$OUTPUT"|tr -d "'\`\"")
echo $OUTPUT

Y at-il un moyen plus rapide? Et est-ce sûr pour les citations comme ', "et `lui-même?

rubo77
la source
Je pense que vous pouvez continuer à utiliser tr. Le PE de BASH est bon mais tr est beaucoup plus rapide dans ce cas. par exemple, echo "$OUTPUT" | tr -dc '[[:alpha:]]' puisque vous ne voulez avoir que des caractères alphanumériques
Valentin Bajrami
2
Puisque vous êtes intéressé à être averti des citations: citez toujours vos variables! echo "$OUTPUT". Ou mieux: printf "%s\n" "$OUTPUT". (Que se passe-t-il quand OUTPUT="-n"?)
musiphil
Vous pouvez également envisager de parcourir codegolf , en particulier les conseils bash .
hoosierEE

Réponses:

22

Voyons voir. Le plus court que je puisse proposer est un ajustement de votre trsolution:

OUTPUT="$(tr -d "\"\`'" <<<$OUTPUT)"

D'autres alternatives incluent la substitution de variables déjà mentionnée qui peut être plus courte que celle illustrée jusqu'à présent:

OUTPUT="${OUTPUT//[\'\"\`]}"

Et sedbien sûr, cela est plus long en termes de caractères:

OUTPUT="$(sed s/[\'\"\`]//g <<<$OUTPUT)"

Je ne sais pas si vous voulez dire la durée la plus courte ou en termes de temps pris. En termes de longueur, ces deux sont aussi courts que possible (ou comme je peux l'obtenir de toute façon) quand il s'agit de supprimer ces caractères spécifiques. Alors, quel est le plus rapide? J'ai testé en définissant la OUTPUTvariable sur ce que vous aviez dans votre exemple, mais j'ai répété plusieurs dizaines de fois:

$ echo ${#OUTPUT} 
4900

$ time tr -d "\"\`'" <<<$OUTPUT
real    0m0.002s
user    0m0.004s
sys     0m0.000s
$ time sed s/[\'\"\`]//g <<<$OUTPUT
real    0m0.005s
user    0m0.000s
sys     0m0.000s
$ time echo ${OUTPUT//[\'\"\`]}
real    0m0.027s
user    0m0.028s
sys     0m0.000s

Comme vous pouvez le voir, le trest clairement le plus rapide, suivi de près sed. De plus, il semble que l'utilisation echosoit en fait légèrement plus rapide que l'utilisation de <<<:

$ for i in {1..10}; do 
    ( time echo $OUTPUT | tr -d "\"\`'" > /dev/null ) 2>&1
done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0025
$ for i in {1..10}; do 
    ( time tr -d "\"\`'" <<<$OUTPUT > /dev/null ) 2>&1 
  done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0029

Étant donné que la différence est minuscule, j'ai effectué les tests ci-dessus 10 fois pour chacun des deux et il s'avère que le plus rapide est en effet celui avec lequel vous avez dû commencer:

echo $OUTPUT | tr -d "\"\`'" 

Cependant, cela change lorsque vous prenez en compte les frais généraux d'affectation à une variable, ici, l'utilisation trest légèrement plus lente que le remplacement simple:

$ for i in {1..10}; do
    ( time OUTPUT=${OUTPUT//[\'\"\`]} ) 2>&1
  done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0032

$ for i in {1..10}; do
    ( time OUTPUT=$(echo $OUTPUT | tr -d "\"\`'")) 2>&1
  done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0044

Donc, en conclusion, lorsque vous souhaitez simplement afficher les résultats, utilisez trmais si vous souhaitez réaffecter à une variable, l'utilisation des fonctionnalités de manipulation de chaîne du shell est plus rapide car elles évitent la surcharge d'exécution d'un sous-shell séparé.

terdon
la source
4
Étant donné que l'OP est intéressé à remettre la valeur modifiée dans OUTPUT, vous devrez tenir compte de la surcharge de sous-shell de substitution de commandes impliquée dans tret des sedsolutions
iruvar
@ 1_CR oui mais comme ce sera le cas quelle que soit la méthode qu'il utilise, je me suis dit que ce n'était pas pertinent.
terdon
1
Pas tout à fait, OUTPUT="${OUTPUT//[`\"\']/}" n'implique pas de substitution de commande
iruvar
@ 1_CR ah, je vois, oui, vous avez tout à fait raison et cela change le résultat. Merci, réponse modifiée.
terdon
2
Les méthodes qui impliquent une substitution de commandes ont l'inconvénient de quelque peu altérer la chaîne. (Vous pouvez l'éviter mais au prix de rendre la commande beaucoup plus complexe.) En particulier, la substitution de commande supprime les sauts de ligne de fin.
Gilles 'SO- arrête d'être méchant'
15

Vous pouvez utiliser la substitution de variable :

$ OUTPUT=a\'b\"c\`d
$ echo "$OUTPUT"
a'b"c`d

Utilisez cette syntaxe: ${parameter//pattern/string}pour remplacer toutes les occurrences du modèle par la chaîne.

$ echo "${OUTPUT//\'/x}"
axb"c`d
$ echo "${OUTPUT//\"/x}"
a'bxc`d
$ echo "${OUTPUT//\`/x}"
a'b"cxd
$ echo "${OUTPUT//[\'\"\`]/x}"
axbxcxd
le chaos
la source
@ rubo77 echo ${OUTPUT//[`\"\']/x}donneaxbxcxa
chaos
Il est incorrect de nommer l'expansion "expansion variable". C'est ce qu'on appelle "l'expansion des paramètres".
gena2x
@ gena2x - Je ne comprends pas ce que signifie votre commentaire ici?
slm
12

En bash ou zsh c'est:

OUTPUT="${OUTPUT//[\`\"\']/}"

Notez que ${VAR//PATTERN/}supprime toutes les instances du modèle. Pour plus d'informations sur l' expansion des paramètres bash

Cette solution devrait être plus rapide pour les chaînes courtes car elle n'implique pas l'exécution de programmes externes. Cependant, pour les très longues chaînes, l'inverse est vrai - il est préférable d'utiliser un outil dédié pour les opérations de texte, par exemple:

$ OUTPUT="$(cat /usr/src/linux/.config)"

$ time (echo $OUTPUT | OUTPUT="${OUTPUT//set/abc}")
real    0m1.766s
user    0m1.681s
sys     0m0.002s

$ time (echo $OUTPUT | sed s/set/abc/g >/dev/null)
real    0m0.094s
user    0m0.078s
sys     0m0.006s
gena2x
la source
1
En fait, trc'est plus rapide. Les expressions régulières et les globes sont chers, et bien qu'il n'y ait pas de programme externe ici, bash sera toujours plus lent que quelque chose comme tr.
terdon
Cela dépend fortement des données d'entrée et de la mise en œuvre de l'expression rationnelle. Dans votre réponse, vous avez pris un grand ensemble de données spécifique - mais l'ensemble de données peut être petit. Ou différent. De plus, vous ne mesurez pas le temps de l'expression rationnelle mais le temps de l'écho, donc je ne peux pas être sûr si votre comparaison est vraiment juste.
gena2x
Bons points. Cependant, vous ne pouvez pas prétendre à la vitesse sans tester. En fait, lors de l'attribution à une variable, cela semble plus rapide, mais lorsque l'impression à l'écran trgagne (voir ma réponse). Je suis d'accord que cela dépendra de nombreux facteurs, mais c'est exactement pourquoi vous ne pouvez pas dire lequel gagne sans le tester.
terdon
6

Si, par hasard, vous essayez simplement de gérer des devis pour réutiliser la coque, vous pouvez le faire sans les supprimer, et c'est aussi simple que cela:

aq() { sh -c 'for a do
       alias "$((i=$i+1))=$a"
       done; alias' -- "$@"
}

Ce shell de fonction cite tout tableau arg que vous lui donnez et incrémente sa sortie par argument itérable.

Le voici avec quelques arguments:

aq \
"here's an
ugly one" \
"this one is \$PATHpretty bad, too" \
'this one```****```; totally sucks'

PRODUCTION

1='here'"'"'s an
ugly one'
2='this one is $PATHpretty bad, too'
3='this one```****```; totally sucks'

Cette sortie est celle à partir de dashlaquelle les guillemets simples sont généralement sécurisés '"'"'. bashferait '\''.

Le remplacement d'une sélection d'octets simples, non blancs et non nuls par un autre octet unique peut probablement se faire le plus rapidement dans n'importe quel shell POSIX avec $IFSet $*.

set -f; IFS=\"\'\`; set -- $var; printf %s "$*"

PRODUCTION

"some ""crazy """"""""string ""here

Là je l'ai juste printfpour que vous puissiez le voir, mais bien sûr, si j'avais fait:

var="$*"

... plutôt que la valeur de la printfcommande $varserait ce que vous voyez dans la sortie.

Lorsque set -fj'ordonne au shell de ne pas glob - au cas où la chaîne contient des caractères qui pourraient être interprétés comme des modèles glob. Je le fais parce que l'analyseur de shells étend les modèles globaux après avoir effectué la division du champ sur les variables. le globbing peut être réactivé comme set +f. En général - dans les scripts - je trouve utile de définir mon bang comme:

#!/usr/bin/sh -f

Et puis pour activer explicitement le globbing avec set +fsur n'importe quelle ligne je pourrais le vouloir.

La division des champs se produit en fonction des caractères dans $IFS.

Il existe deux types de $IFSvaleurs: les $IFSespaces blancs et $IFSnon blancs. $IFSles espaces délimités par des espaces (espace, tabulation, nouvelle ligne) sont spécifiés pour être éliminés par séquence vers un seul champ (ou aucun du tout s'ils ne précèdent pas autre chose) - donc ...

IFS=\ ; var='      '; printf '<%s>' $var
<>

Mais tous les autres sont spécifiés pour évaluer un seul champ par occurrence - ils ne sont pas tronqués.

IFS=/; var='/////'; printf '<%s>' $var
<><><><><>

Toutes les extensions de variables sont, par défaut, $IFSdes tableaux de données délimités - elles se répartissent en champs séparés selon $IFS. Lorsque vous en "citez une, vous remplacez cette propriété de tableau et l'évaluez comme une chaîne unique.

Alors quand je fais ...

IFS=\"\'\`; set -- $var

Je mets le tableau d'arguments du shell sur les nombreux $IFSchamps délimités générés par $varl'expansion de. Lorsqu'elle est développée, ses valeurs constitutives pour les caractères contenus dans $IFSsont perdues - ce ne sont plus que des séparateurs de champs - elles le sont \0NUL.

"$*"- comme les autres extensions de variable entre guillemets doubles - remplace également les qualités de séparation de champ de $IFS. Mais, en plus , il substitue le premier octet dans $IFS pour chaque champ délimité dans "$@". Parce que "a été la première valeur dans $IFS tous les délimiteurs suivantes deviennent "en "$*". Et le "besoin n'est pas là non plus $IFSlorsque vous le divisez. Vous pouvez modifier complètement $IFS après set -- $args une autre valeur et son nouveau premier octet apparaîtra alors pour les délimiteurs de champ dans "$*". De plus, vous pouvez en supprimer toutes les traces comme:

set -- $var; IFS=; printf %s "$*"

PRODUCTION

some crazy string here
mikeserv
la source
Très sympa, +1. Je me demande si c'est vraiment plus rapide. Pourriez-vous ajouter des tests de synchronisation en les comparant aux approches dans ma réponse? J'espère que le vôtre sera plus rapide, mais j'aimerais le voir.
terdon
@terdon - cela dépend du shell. Il est presque certainement plus rapide que trdans n'importe quel shell, mais la différence est incertaine dans bashle ${var//$c/$newc/}cas. Je m'attends même à ce que ce soit plus rapide par une certaine marge, mais je ne m'inquiète généralement pas à ce sujet parce que pour ce genre de choses que j'utilise toujours dash- qui est plus rapide par ordre de grandeur en général à tous égards. Et donc c'est difficile à comparer.
mikeserv
@terdon - J'ai essayé. Mais - même en cours bash- time (IFS=\"\'`; set -- $var; printf %s "$*")et les time (var=${var//\'`/\"/})deux donnent des 0.0000srésultats pour tous les domaines. Suis-je en train de faire quelque chose de mal, pensez-vous? Il doit y avoir une barre oblique inverse avant la citation inverse, mais je ne sais pas comment mettre une citation inverse dans un champ de code de commentaire.
mikeserv