Como incorporar um comando shell em uma expressão sed?

16

Eu tenho um arquivo de texto com o seguinte formato:

keyword value
keyword value
...

Onde palavra-chave é uma única palavra e valor é tudo o resto até o final da linha. Eu quero ler o arquivo de um script de shell, de maneira que os valores (mas não as palavras-chave) passem pela expansão do shell.

Com o sed, é fácil combinar as palavras-chave e as partes de valor

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

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

que produz

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

mas a questão é como posso incorporar um comando shell na parte de substituição da expressão sed. Nesse caso, eu gostaria que a substituição fosse \1 `echo \2`.

Ernest AC
fonte
Uhm ... Não tenho tanta certeza de dar isso como resposta, mas usar DOUBLE citado com sed deve permitir que você use shell $ (comando) ou $ variáveis ​​dentro da expressão.
St0rM

Respostas:

18

O sed padrão não pode chamar um shell (o GNU sed tem uma extensão para fazê-lo , se você se importa apenas com o Linux não incorporado), então você terá que fazer parte do processamento fora do sed. Existem várias soluções; todos requerem citações cuidadosas.

Não está claro exatamente como você deseja que os valores sejam expandidos. Por exemplo, se uma linha é

foo hello; echo $(true)  3

Qual das seguintes opções deve ser a saída?

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

Vou discutir várias possibilidades abaixo.

casca pura

Você pode fazer com que o shell leia a entrada de linha por linha e processe-a. Esta é a solução mais simples e também a mais rápida para arquivos curtos. Esta é a coisa mais próxima do seu requisito " echo \2":

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

read -r keyword valuedefine $keyworda primeira palavra da linha delimitada por espaços em branco e $valueo restante da linha menos o espaço em branco à direita.

Se você deseja expandir referências de variáveis, mas não executar comandos fora das substituições de comandos, coloque $valuedentro de um documento aqui . Eu suspeito que isso é o que você realmente estava procurando.

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

sed canalizado em uma concha

Você pode transformar a entrada em um script de shell e avaliar isso. Sed está à altura da tarefa, embora não seja tão fácil. Seguindo o seu echo \2requisito " " (observe que precisamos escapar de aspas simples na palavra-chave):

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

Indo com um documento aqui, ainda precisamos escapar da palavra-chave (mas de forma 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 é o método mais rápido se você tiver muitos dados: ele não inicia um processo separado para cada linha.

awk

As mesmas técnicas que usamos no sed trabalham com o awk. O programa resultante é consideravelmente mais legível. Indo com " echo \2":

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

Usando um documento aqui:

awk '
  NR==1 { print "cat <<EOF" }
  1 {
      kw = $1;
      sub(/^ *[^ ]+ +/, "");
      gsub(/\\\$`/, "\\&", $1);
      print "k=<" kw "> v=<" $0 ">";
  }
  END { print "EOF" }
' | sh
Gilles 'SO- parar de ser mau'
fonte
Ótima resposta. vou usar a solução pura do shell, pois o arquivo de entrada é realmente pequeno e o desempenho não é uma preocupação, também é limpo e legível.
Ernest AC
um pouco de hack, mas arrumado o suficiente. por exemplo, use sed para chamar xxd para decodificar uma sequência hexadecimal longa. . . gato FtH.ch13 | sed -r 's /(.* texto. *: [) ([0-9a-fA-F] *)] / \ 1 $ (eco \ 2 | xxd -r -p)] /; s / ^ ( . *) $ / echo "\ 1" / g '| bash> FtHtext.ch13 Onde FtH.ch13 possui linhas como "teste de texto hexadecimal com barra foo: [666f6f0a62617200]"
gaoithe
14

Tendo o GNU, sedvocê pode usar o seguinte comando:

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

Quais saídas:

keyword value value
keyword value  value
keyword Linux

com seus dados de entrada.

Explicação:

O comando sed suprime a saída regular usando a -nopção -ré passado para usar expressões regulares estendidas, o que nos salva de alguns caracteres especiais no padrão, mas isso não é necessário.

O scomando é usado para transferir a linha de entrada para o comando:

echo "\1" \2

A palavra-chave get citou o valor não. Eu passo a opção e- que é específica do GNU - para o scomando, que diz ao sed para executar o resultado da substituição como um comando shell e leio seus resultados no buffer de padrão (até várias linhas). O uso da opção pafter (!) eTorna a sedimpressão do buffer padrão após a execução do comando.

hek2mgl
fonte
Você pode ficar sem as opções -ne p, por exemplo sed -r 's/([^ ]+) (.*)/echo "\1" \2\n/e' input. Mas obrigado por isso! Eu não sabia sobre a eopção.
precisa
@KaushalModi Oh sim, você está certo! Estou sentado em cima do muro quando se trata de eopção (apresentada pelo GNU). Isso ainda é sed? :)
hek2mgl
Bem, funcionou para mim. É o GNU sed por padrão para mim (GNU sed versão 4.2.1) na distribuição RHEL.
precisa
4

Você pode tentar esta abordagem:

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')"

(se eu acertar sua intenção). Ou seja, insira "processo" no início de cada linha não vazia para torná-lo um script como:

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

a ser avaliado ( eval) onde processo é uma função que imprime a mensagem esperada.

Stéphane Chazelas
fonte
1

Se uma solução não sed for aceitável, este trecho de PERL fará o trabalho:

$ echo "$input" | perl -ne 'chomp; /^\s*(.+?)\s+(.+)$/ && do { $v=`echo "$2"`; chomp($v); print "k=<$1> v=<$v>\n"}'
terdon
fonte
1
obrigado, mas eu prefiro evitar o uso de outra linguagem de script se eu puder e mantê-lo para comandos padrão UNIX e shell Bourne
Ernest AC
0

SOMENTE CURTO SED PURO CURTO

eu farei isso

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

e está funcionando.

mr.tee
fonte
Poderia explicar como isso funciona?
elysch