shell: continue acompanhando novas linhas ('\ n') na substituição de comandos

14

Desejo capturar a saída exata de uma substituição de comando, incluindo os caracteres de nova linha à direita .

Sei que eles são removidos por padrão, portanto, pode ser necessária alguma manipulação para mantê-los e desejo manter o código de saída original .

Por exemplo, dado um comando com um número variável de novas linhas à direita e código de saída:

f(){ for i in $(seq "$((RANDOM % 3))"); do echo; done; return $((RANDOM % 256));}
export -f f

Eu quero executar algo como:

exact_output f

E faça com que a saída seja:

Output: $'\n\n'
Exit: 5

Estou interessado em ambos bashe no POSIX sh.

Tom Hale
fonte
1
A nova linha faz parte e $IFS, portanto, não será capturada como argumento.
Deathgrip
4
@Deathgrip Não tem nada a ver com IFS(tente ( IFS=:; subst=$(printf 'x\n\n\n'); printf '%s' "$subst" ). Somente novas linhas são removidas. \tE `` IFSnão afetam e não afetam isso.
PSkocik
Veja também: tcsh preserve newlines na substituição de comando `...` paratcsh
Stéphane Chazelas

Respostas:

17

Cartuchos POSIX

O truque usual ( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ) para obter o stdout completo de um comando é:

output=$(cmd; ret=$?; echo .; exit "$ret")
ret=$?
output=${output%.}

A idéia é adicionar e extra .\n. A substituição de comandos apenas tira isso \n . E você tira o .com ${output%.}.

Observe que em shells que não sejam zsh, isso ainda não funcionará se a saída tiver bytes NUL. Com yash, isso não funcionará se a saída não for texto.

Observe também que, em alguns locais, importa qual caractere você usa para inserir no final. .geralmente deve estar bem, mas outros podem não estar. Por exemplo x(conforme usado em outras respostas) ou @não funcionaria em um código de idioma usando os conjuntos de caracteres BIG5, GB18030 ou BIG5HKSCS. Nesses charsets, a codificação de um número de caracteres termina no mesmo byte que a codificação de xou @(0x78, 0x40)

Por exemplo, ūem BIG5HKSCS é 0x88 0x78 (e xé 0x78 como em ASCII, todos os conjuntos de caracteres em um sistema devem ter a mesma codificação para todos os caracteres do conjunto de caracteres portátil que inclui letras em inglês @e .). Então, se cmdfoi printf '\x88'e inserimos xdepois disso, ${output%x}falharia em remover isso xcomo $outputrealmente conteria ū.

Em .vez disso, o uso poderia levar ao mesmo problema na teoria se houvesse caracteres cuja codificação terminasse na mesma codificação que ., mas por ter verificado há algum tempo, posso dizer que nenhum dos charsets disponíveis para uso em um código de idioma em os sistemas Debian, FreeBSD ou Solaris têm esses caracteres que são bons o suficiente para mim (e por que eu decidi .qual também é o símbolo para marcar o final de uma frase em inglês, parece apropriado).

Uma abordagem mais correta, conforme discutida pelo @Arrow, seria alterar o código de idioma para C apenas para a remoção do último caractere ( ${output%.}), que garantiria a remoção de apenas um byte, mas isso complicaria significativamente o código e potencialmente introduziria problemas de compatibilidade de próprio.

alternativas bash / zsh

Com bashe zsh, supondo que a saída não tenha NULs, você também pode:

IFS= read -rd '' output < <(cmd)

Para obter o status de saída cmd, você pode fazer wait "$!"; ret=$?em bash, mas não em zsh.

rc / es / akanaga

Para completar, nota que rc/ es/ akangatem um operador para isso. Neles, a substituição de comando, expressa como `cmd(ou `{cmd}para comandos mais complexos), retorna uma lista (dividindo em $ifs, space-tab-newline por padrão). Nessas cascas (ao contrário das cascas tipo Bourne), a remoção da nova linha é feita apenas como parte dessa $ifsdivisão. Portanto, você pode esvaziar $ifsou usar o ``(seps){cmd}formulário em que você especifica os separadores:

ifs = ''; output = `cmd

ou:

output = ``()cmd

De qualquer forma, o status de saída do comando é perdido. Você precisaria incorporá-lo na saída e extraí-lo depois, o que se tornaria feio.

peixe

Nos peixes, a substituição de comandos é com (cmd)e não envolve um subshell.

set var (cmd)

Cria uma $varmatriz com todas as linhas na saída de cmdif $IFSse não estiver vazia ou com a saída de cmdstripped de até um caractere de nova linha (em oposição a todos na maioria dos outros shells) se $IFSestiver vazio.

Portanto, ainda há um problema nisso (printf 'a\nb')e (printf 'a\nb\n')expanda para a mesma coisa, mesmo com um vazio $IFS.

Para contornar isso, o melhor que pude sugerir foi:

function exact_output
  set -l IFS . # non-empty IFS
  set -l ret
  set -l lines (
    cmd
    set ret $status
    echo
  )
  set -g output ''
  set -l line
  test (count $lines) -le 1; or for line in $lines[1..-2]
    set output $output$line\n
  end
  set output $output$lines[-1]
  return $ret
end

Uma alternativa é fazer:

read -z output < (begin; cmd; set ret $status; end | psub)

Casca de Bourne

O shell Bourne não suportava o $(...)formulário nem o ${var%pattern}operador, portanto, pode ser bastante difícil de obter lá. Uma abordagem é usar eval e citar:

eval "
  output='`
    exec 4>&1
    ret=\`
      exec 3>&1 >&4 4>&-
      (cmd 3>&-; echo \"\$?\" >&3; printf \"'\") |
        awk 3>&- -v RS=\\\\' -v ORS= -v b='\\\\\\\\' '
          NR > 1 {print RS b RS RS}; {print}; END {print RS}'
    \`
    echo \";ret=\$ret\"
  `"

Aqui, estamos gerando uma

output='output of cmd
with the single quotes escaped as '\''
';ret=X

para ser passado para eval. Quanto à abordagem POSIX, se 'fosse um desses caracteres cuja codificação pode ser encontrada no final de outros caracteres, teríamos um problema (muito pior, pois se tornaria uma vulnerabilidade de injeção de comando), mas felizmente, como ., não é um deles, e essa técnica de citação geralmente é a que é usada por qualquer coisa que cite o código do shell (observe que \há um problema, portanto não deve ser usado (também exclui o "..."interior do qual você precisa usar barras invertidas para alguns caracteres)) Aqui, só o estamos usando depois de um valor 'que está correto).

tcsh

Veja tcsh preserve newlines na substituição de comando `...`

(não cuidando do status de saída, que você pode resolver salvando-o em um arquivo temporário ( echo $status > $tempfile:qapós o comando))

Stéphane Chazelas
fonte
Obrigado - e especialmente pela dica sobre os diferentes conjuntos de caracteres. Se zshpode armazenar NULem uma variável, por que não IFS= read -rd '' output < <(cmd)funcionaria? Ele precisa ser capaz de armazenar o comprimento de uma string ... ela codifica ''como uma string de 1 byte em \0vez de uma string de 0 byte?
Tom Hale
1
@ TomHale, sim, read -d ''é tratado como read -d $'\0'( bashassim $'\0'como em ''todos os lugares).
Stéphane Chazelas
Você está misturando caracteres e bytes. Por favor, entenda que se removermos exatamente o que foi adicionado, a entidade original não deverá ser alterada. Não é tão difícil remover um byte chamado xse foi isso que foi adicionado. Por favor, dê uma olhada na minha resposta editada.
Seta
@ Arrow, sim, o var=value command evaltruque foi discutido aqui ( também ) e na lista de discussão do grupo austin antes. Você verá que não é portátil (e é bastante óbvio, quando você está tentando coisas como a=1 command eval 'unset a; a=2'ou pior, que não foi feito para ser usado assim). O mesmo para o savedVAR=$VAR;...;VAR=$savedVARque não faz o que você deseja quando $VARestava inicialmente desconfigurado. Se isso é para solucionar apenas um problema teórico (um bug que não pode ser atingido na prática), IMO, não vale a pena. Ainda assim, eu vou apoiá-lo por tentar.
Stéphane Chazelas 2/17/17
Você tem um link para onde você descartou e finalmente descartou o uso de LANG=Cpara remover um byte de uma string? Você está levantando preocupações em torno do ponto real, tudo é fácil de resolver. (1) não foi usado nenhum ajuste (2) Teste a variável antes de alterá-la. @ StéphaneChazelas
Flecha
3

Para a nova pergunta, este script funciona:

#!/bin/bash

f()           { for i in $(seq "$((RANDOM % 3 ))"); do
                    echo;
                done; return $((RANDOM % 256));
              }

exact_output(){ out=$( $1; ret=$?; echo x; exit "$ret" );
                unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
                LC_ALL=C ; out=${out%x};
                unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL
                 printf 'Output:%10q\nExit :%2s\n' "${out}" "$?"
               }

exact_output f
echo Done

Na execução:

Output:$'\n\n\n'
Exit :25
Done

A descrição mais longa

A sabedoria usual dos shells POSIX para lidar com a remoção de \né:

adicione um x

s=$(printf "%s" "${1}x"); s=${s%?}

Isso é necessário porque a última nova linha ( S ) é removida pela expansão do comando de acordo com a especificação POSIX :

removendo sequências de um ou mais caracteres no final da substituição.


Sobre uma trilha x.

Já foi dito nesta pergunta que um xpoderia ser confundido com o byte à direita de algum caractere em alguma codificação. Mas como vamos adivinhar qual ou qual personagem é melhor em alguma linguagem em alguma codificação possível, que é uma proposição difícil, para dizer o mínimo.

Contudo; Isso é simplesmente incorreto .

A única regra que precisamos seguir é adicionar exatamente o que removemos.

Deve ser fácil entender que, se adicionarmos algo a uma string existente (ou sequência de bytes) e depois removermos exatamente o mesmo, a string original (ou sequência de bytes) deverá ser a mesma.

Onde erramos? Quando misturamos caracteres e bytes .

Se adicionarmos um byte, devemos remover um byte; se adicionarmos um caractere, removeremos exatamente o mesmo caractere .

A segunda opção, adicionar um caractere (e depois remover exatamente o mesmo caractere) pode se tornar complicada e complexa e, sim, páginas de código e codificações podem atrapalhar.

No entanto, a primeira opção é bem possível e, depois de explicada, se tornará simples.

Vamos adicionar um byte, um byte ASCII (<127), e para manter as coisas o menos complicado possível, digamos um caractere ASCII no intervalo de az. Ou, como deveríamos dizer, um byte no intervalo hexadecimal 0x61- 0x7a. Vamos escolher um desses, talvez um x (realmente um byte de valor 0x78). Podemos adicionar esse byte concatenando um x a uma string (vamos assumir um é):

$ a
$ b=${a}x

Se olharmos para a string como uma sequência de bytes, veremos:

$ printf '%s' "$b" | od -vAn -tx1c
  c3  a9  78
 303 251   x

Uma sequência de cadeias que termina em um x.

Se removermos esse x (valor de byte 0x78), obtemos:

$ printf '%s' "${b%x}" | od -vAn -tx1c
  c3  a9
 303 251

Funciona sem problemas.

Um exemplo um pouco mais difícil.

Digamos que a string em que estamos interessados ​​termine em byte 0xc3:

$ a=$'\x61\x20\x74\x65\x73\x74\x20\x73\x74\x72\x69\x6e\x67\x20\xc3'

E vamos adicionar um byte de valor 0xa9

$ b=$a$'\xa9'

A string tornou-se agora:

$ echo "$b"
a test string é

Exatamente o que eu queria, os últimos dois bytes são um caractere no utf8 (para que qualquer um possa reproduzir esses resultados em seu console utf8).

Se removermos um caractere, a string original será alterada. Mas não foi isso que adicionamos, adicionamos um valor de byte, que passa a ser escrito como x, mas de qualquer maneira.

O que precisamos para evitar interpretar mal bytes como caracteres. O que precisamos é de uma ação que remova o byte que usamos 0xa9. De fato, ash, bash, lksh e mksh parecem fazer exatamente isso:

$ c=$'\xa9'
$ echo ${b%$c} | od -vAn -tx1c
 61  20  74  65  73  74  20  73  74  72  69  6e  67  20  c3  0a
  a       t   e   s   t       s   t   r   i   n   g     303  \n

Mas não ksh ou zsh.

No entanto, isso é muito fácil de resolver, vamos dizer a todos os shells para remover o byte:

$ LC_ALL=C; echo ${b%$c} | od -vAn -tx1c 

é isso, todos os shells testados funcionam (exceto yash) (para a última parte da string):

ash             :    s   t   r   i   n   g     303  \n
dash            :    s   t   r   i   n   g     303  \n
zsh/sh          :    s   t   r   i   n   g     303  \n
b203sh          :    s   t   r   i   n   g     303  \n
b204sh          :    s   t   r   i   n   g     303  \n
b205sh          :    s   t   r   i   n   g     303  \n
b30sh           :    s   t   r   i   n   g     303  \n
b32sh           :    s   t   r   i   n   g     303  \n
b41sh           :    s   t   r   i   n   g     303  \n
b42sh           :    s   t   r   i   n   g     303  \n
b43sh           :    s   t   r   i   n   g     303  \n
b44sh           :    s   t   r   i   n   g     303  \n
lksh            :    s   t   r   i   n   g     303  \n
mksh            :    s   t   r   i   n   g     303  \n
ksh93           :    s   t   r   i   n   g     303  \n
attsh           :    s   t   r   i   n   g     303  \n
zsh/ksh         :    s   t   r   i   n   g     303  \n
zsh             :    s   t   r   i   n   g     303  \n

Simples assim, diga ao shell para remover um caractere LC_ALL = C, que é exatamente um byte para todos os valores de by 0x00a 0xff.

Solução para comentários:

Para o exemplo discutido nos comentários, uma solução possível (que falha no zsh) é:

#!/bin/bash

LC_ALL=zh_HK.big5hkscs

a=$(printf '\210\170');
b=$(printf '\170');

unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
LC_ALL=C ; a=${a%"$b"};
unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL

printf '%s' "$a" | od -vAn -c

Isso removerá o problema da codificação.

Seta
fonte
É bom saber que mais de uma nova linha final pode ser removida.
Tom Hale #
Concordo que fixar o código de idioma em C para garantir que ${var%?}sempre retire um byte é mais correto em teoria, mas: 1 - LC_ALLe LC_CTYPEsubstitua $LANG, portanto, você precisará definir LC_ALL=C2 - você não pode fazer isso var=${var%?}em um subshell, como seria a alteração ser perdido, portanto, você precisará salvar e restaurar o valor e o estado LC_ALL(ou recorrer a localrecursos de escopo que não sejam do POSIX ). 3- A alteração do código do idioma no meio do script não é totalmente suportada em alguns shells como o yash. Por outro lado, na prática .nunca é um problema em conjuntos de caracteres da vida real, portanto, usá-lo evita a combinação com LC_ALL.
Stéphane Chazelas 2/17/17
2

Você pode produzir um caractere após a saída normal e depois removê-lo:

#capture the output of "$@" (arguments run as a command)
#into the exact_output` variable
exact_output() 
{
    exact_output=$( "$@" && printf X ) && 
    exact_output=${exact_output%X}
}

Esta é uma solução compatível com POSIX.

PSkocik
fonte
Com base nas respostas, vejo que minha pergunta não estava clara. Acabei de atualizar.
Tom Hale