Comment intégrer une commande shell dans une expression sed?

16

J'ai un fichier texte au format suivant:

keyword value
keyword value
...

Où mot-clé est un seul mot et la valeur est tout le reste jusqu'à la fin de la ligne. Je veux lire le fichier à partir d'un script shell, de manière à ce que les valeurs (mais pas les mots-clés) subissent une expansion shell.

Avec sed, il est facile de faire correspondre les mots-clés et les parties de valeur

input='
keyword value value
keyword "value  value"
keyword `uname`
'

echo "$input"|sed -e 's/^\([^[:space:]]*\)[[:space:]]\(.*\)$/k=<\1> v=<\2>/'

qui produit

k=<keyword> v=<value value>
k=<keyword> v=<"value  value">
k=<keyword> v=<`uname`>

mais alors la question est de savoir comment puis-je intégrer une commande shell dans la partie de remplacement de l'expression sed. Dans ce cas, je voudrais que le remplacement soit \1 `echo \2`.

Ernest AC
la source
Uhm ... Je ne suis pas sûr de donner une réponse, mais utiliser DOUBLE entre guillemets devrait vous permettre d'utiliser shell $ (commande) ou $ variables à l'intérieur de l'expression.
St0rM

Réponses:

18

Sed standard ne peut pas appeler un shell ( GNU sed a une extension pour le faire , si vous ne vous souciez que de Linux non intégré), vous devrez donc effectuer une partie du traitement en dehors de sed. Il existe plusieurs solutions; tous nécessitent un devis soigné.

On ne sait pas exactement comment vous souhaitez que les valeurs soient développées. Par exemple, si une ligne est

foo hello; echo $(true)  3

lequel des éléments suivants devrait être la sortie?

k=<foo> value=<hello; echo   3>
k=<foo> value=<hello; echo   3>
k=<foo> value=<hello; echo 3>
k=<foo> value=<foo hello
  3>

Je vais discuter de plusieurs possibilités ci-dessous.

coquille pure

Vous pouvez demander au shell de lire ligne par ligne et de la traiter. Il s'agit de la solution la plus simple et également la plus rapide pour les fichiers courts. C'est la chose la plus proche de votre besoin " echo \2":

while read -r keyword value; do
  echo "k=<$keyword> v=<$(eval echo "$value")>"
done

read -r keyword valuedéfinit $keywordle premier mot délimité par des espaces de la ligne et $valuele reste de la ligne moins les espaces de fin.

Si vous souhaitez développer des références de variables, mais pas exécuter des commandes en dehors des substitutions de commandes, placez-les $valuedans un document ici . Je soupçonne que c'est ce que vous cherchiez vraiment.

while read -r keyword value; do
  echo "k=<$keyword> v=<$(cat <<EOF
$value
EOF
)>"
done

sed passe dans une coquille

Vous pouvez transformer l'entrée en un script shell et l'évaluer. Sed est à la hauteur, mais ce n'est pas si simple. En fonction de votre echo \2exigence " " (notez que nous devons échapper les guillemets simples dans le mot-clé):

sed  -e 's/^ *//' -e 'h' \
     -e 's/[^ ]*  *//' -e 'x' \
     -e 's/ .*//' -e "s/'/'\\\\''/g" -e "s/^/echo 'k=</" \
     -e 'G' -e "s/\n/>' v=\\</" -e 's/$/\\>/' | sh

Pour aller avec un document ici, nous devons toujours échapper au mot-clé (mais différemment).

{
  echo 'cat <<EOF'
  sed -e 's/^ */k=</' -e 'h' \
      -e 's/[^ ]*  *//' -e 'x' -e 's/ .*//' -e 's/[\$`]/\\&/g' \
      -e 'G' -e "s/\n/> v=</" -e 's/$/>/'
  echo 'EOF'
 } | sh

C'est la méthode la plus rapide si vous avez beaucoup de données: elle ne démarre pas un processus séparé pour chaque ligne.

awk

Les mêmes techniques que nous avons utilisées avec sed fonctionnent avec awk. Le programme résultant est considérablement plus lisible. Aller avec " echo \2":

awk '
  1 {
      kw = $1;
      sub(/^ *[^ ]+ +/, "");
      gsub(/\047/, "\047\\\047\047", $1);
      print "echo \047k=<" kw ">\047 v=\\<" $0 "\\>";
  }' | sh

Utilisation d'un document ici:

awk '
  NR==1 { print "cat <<EOF" }
  1 {
      kw = $1;
      sub(/^ *[^ ]+ +/, "");
      gsub(/\\\$`/, "\\&", $1);
      print "k=<" kw "> v=<" $0 ">";
  }
  END { print "EOF" }
' | sh
Gilles 'SO- arrête d'être méchant'
la source
très bonne réponse. je vais utiliser la solution pure shell, car le fichier d'entrée est en effet petit et les performances ne sont pas un problème, il est également propre et lisible.
Ernest AC
un peu hack mais assez soigné. Par exemple, utilisez sed pour appeler xxd pour décoder une longue chaîne hexadécimale. . . cat FtH.ch13 | Texte de sed -r /(.*. *: [) ([0-9a-fA-F] *)] / \ 1 $ (echo \ 2 | xxd -r -p)] /; s / ^ ( . *) $ / echo "\ 1" / g '| bash> FtHtext.ch13 Où FtH.ch13 a des lignes comme "foo bar hex text test: [666f6f0a62617200]"
gaoithe
14

Avec GNU, sedvous pouvez utiliser la commande suivante:

sed -nr 's/([^ ]+) (.*)/echo "\1" \2\n/ep' input

Quelles sorties:

keyword value value
keyword value  value
keyword Linux

avec vos données d'entrée.

Explication:

La commande sed supprime la sortie régulière à l'aide de l' -noption. -rest passé pour utiliser des expressions régulières étendues, ce qui nous évite d'échapper des caractères spéciaux dans le modèle, mais ce n'est pas obligatoire.

La scommande est utilisée pour transférer la ligne d'entrée dans la commande:

echo "\1" \2

Le mot clé get n'a pas indiqué la valeur. Je passe l'option e- qui est spécifique à GNU - à la scommande, qui dit à sed d'exécuter le résultat de la substitution en tant que commande shell et de lire ses résultats dans le tampon de modèle (même plusieurs lignes). L'utilisation de l'option paprès (!) ePermet d' sedimprimer le tampon de motif après l'exécution de la commande.

hek2mgl
la source
Vous pouvez vous passer des options -net p, c.-à-d sed -r 's/([^ ]+) (.*)/echo "\1" \2\n/e' input. Mais merci pour ça! Je ne connaissais pas l' eoption.
Kaushal Modi
@KaushalModi Oh oui, vous avez raison! Je suis assis sur la clôture en ce qui concerne l' eoption (introduite par GNU). Est-ce encore sed? :)
hek2mgl
Eh bien, cela a fonctionné pour moi. C'est GNU sed par défaut pour moi (GNU sed version 4.2.1) sur la distribution RHEL.
Kaushal Modi
4

Vous pouvez essayer cette approche:

input='
keyword value value
keyword "value  value"
keyword `uname`
'

process() {
  k=$1; shift; v="$*"
  printf '%s\n' "k=<$k> v=<$v>"
}

eval "$(printf '%s\n' "$input" | sed -n 's/./process &/p')"

(si je comprends bien votre intention). C'est insérer "processus" au début de chaque ligne non vide pour en faire un script comme:

process keyword value value
process keyword "value  value"
process keyword `uname`

à évaluer ( eval) où process est une fonction qui imprime le message attendu.

Stéphane Chazelas
la source
1

Si une solution non sed est acceptable, cet extrait PERL fera le travail:

$ echo "$input" | perl -ne 'chomp; /^\s*(.+?)\s+(.+)$/ && do { $v=`echo "$2"`; chomp($v); print "k=<$1> v=<$v>\n"}'
terdon
la source
1
merci mais je préfère éviter d'utiliser un autre langage de script si je le peux et le garder aux commandes standard unix et au shell bourne
Ernest AC
0

SEULEMENT KISS SHORT PURE SED

je vais le faire

echo "ls_me" | sed -e "s/\(ls\)_me/\1/e" -e "s/to be/continued/g;"

et ça marche.

mr.tee
la source
Pourriez-vous expliquer comment cela fonctionne?
elysch