¿Cómo incrustar un comando de shell en una expresión sed?

16

Tengo un archivo de texto con el siguiente formato:

keyword value
keyword value
...

Donde la palabra clave es una sola palabra y el valor es todo lo demás hasta el final de la línea. Quiero leer el archivo desde un script de shell, de manera que los valores (pero no las palabras clave) experimenten una expansión de shell.

Con sed es fácil hacer coincidir las palabras clave y las partes de valor

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

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

que produce

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

pero la pregunta es cómo puedo incrustar un comando de shell en la parte de reemplazo de la expresión sed. En este caso, me gustaría que fuera el reemplazo \1 `echo \2`.

Ernest AC
fuente
Uhm ... no estoy tan seguro de darlo como respuesta, pero el uso de DOUBLE citado con sed debería permitirle usar shell $ (comando) o $ variables dentro de la expresión.
St0rM

Respuestas:

18

El sed estándar no puede llamar a un shell ( GNU sed tiene una extensión para hacerlo , si solo le importa Linux no incrustado), por lo que tendrá que hacer parte del procesamiento fuera de sed. Hay varias soluciones; todo requiere una cita cuidadosa.

No está claro exactamente cómo desea que se expandan los valores. Por ejemplo, si una línea es

foo hello; echo $(true)  3

¿Cuál de los siguientes debe ser el resultado?

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

Discutiré varias posibilidades a continuación.

cáscara pura

Puede hacer que el shell lea la entrada línea por línea y la procese. Esta es la solución más simple, y también la más rápida para archivos cortos. Esto es lo más cercano a su requerimiento "echo \2 ":

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

read -r keyword valuese establece $keyworden la primera palabra delimitada por espacios en blanco de la línea y $valueen el resto de la línea menos los espacios en blanco finales.

Si desea expandir referencias de variables, pero no ejecutar comandos fuera de las sustituciones de comandos, coloque $valuedentro de un documento aquí . Sospecho que esto es lo que realmente estabas buscando.

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

sed entubado en una concha

Puede transformar la entrada en un script de shell y evaluar eso. Sed está preparado para la tarea, aunque no es tan fácil. De acuerdo con su echo \2requisito " " (tenga en cuenta que debemos escapar de las comillas simples en la palabra clave):

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

Al ir con un documento aquí, todavía necesitamos escapar de la palabra clave (pero de manera diferente).

{
  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

Este es el método más rápido si tiene muchos datos: no inicia un proceso separado para cada línea.

awk

Las mismas técnicas que utilizamos con sed funcionan con awk. El programa resultante es considerablemente más legible. Yendo con " echo \2":

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

Usando un documento aquí:

awk '
  NR==1 { print "cat <<EOF" }
  1 {
      kw = $1;
      sub(/^ *[^ ]+ +/, "");
      gsub(/\\\$`/, "\\&", $1);
      print "k=<" kw "> v=<" $0 ">";
  }
  END { print "EOF" }
' | sh
Gilles 'SO- deja de ser malvado'
fuente
gran respuesta. Voy a usar la solución de shell puro, ya que el archivo de entrada es realmente pequeño y el rendimiento no es una preocupación, también es limpio y legible.
Ernest AC
un poco hack pero lo suficientemente ordenado. por ejemplo, use sed para llamar a xxd para decodificar una cadena hexadecimal larga. . . gato FtH.ch13 | sed -r 's /(.* texto. *: [) ([0-9a-fA-F] *)] / \ 1 $ (echo \ 2 | xxd -r -p)] /; s / ^ ( . *) $ / echo "\ 1" / g '| bash> FtHtext.ch13 Donde FtH.ch13 tiene líneas como "prueba de texto hexadecimal foo bar: [666f6f0a62617200]"
gaoithe
14

Tener GNU sedpuede usar el siguiente comando:

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

Qué salidas:

keyword value value
keyword value  value
keyword Linux

con sus datos de entrada.

Explicación:

El comando sed suprime la salida regular usando la -nopción -rse pasa a usar expresiones regulares extendidas que nos ahorra algunos escapes de caracteres especiales en el patrón, pero no es obligatorio.

El scomando se usa para transferir la línea de entrada al comando:

echo "\1" \2

La palabra clave get ha citado el valor no. Le paso la opción e, que es específica de GNU, al scomando, que le dice a sed que ejecute el resultado de la sustitución como un comando de shell y lea los resultados en el búfer de patrón (incluso varias líneas). El uso de la opción pdespués de (!) eHace que se sedimprima el búfer de patrón después de que se haya ejecutado el comando.

hek2mgl
fuente
Puede prescindir de las opciones -ny p, es decir sed -r 's/([^ ]+) (.*)/echo "\1" \2\n/e' input. Pero gracias por esto! No sabía sobre la eopción.
Kaushal Modi
@KaushalModi ¡Oh, sí, tienes razón! Estoy sentado en la cerca cuando se trata de la eopción (introducida por GNU). ¿Eso sigue siendo sed? :)
hek2mgl
Bueno, funcionó para mí. Es GNU sed por defecto para mí (GNU sed versión 4.2.1) en la distribución RHEL.
Kaushal Modi
4

Podrías probar este enfoque:

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 entiendo bien tu intención). Es decir, inserte el "proceso" al comienzo de cada línea no vacía para convertirlo en un script como:

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

para ser evaluado ( eval) donde proceso es una función que imprime el mensaje esperado.

Stéphane Chazelas
fuente
1

Si una solución no sed es aceptable, este fragmento de PERL hará el trabajo:

$ echo "$input" | perl -ne 'chomp; /^\s*(.+?)\s+(.+)$/ && do { $v=`echo "$2"`; chomp($v); print "k=<$1> v=<$v>\n"}'
terdon
fuente
1
gracias, pero prefiero evitar usar otro lenguaje de script si puedo y mantenerlo en los comandos estándar de Unix y Bourne Shell
Ernest AC
0

SOLO BESO CORTO SED PURO

haré esto

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

y funciona

señor
fuente
¿Podría explicar cómo funciona?
elysch