shell: continua a seguire le nuove righe ('\ n') nella sostituzione dei comandi


14

Voglio essere in grado di catturare l'output esatto di una sostituzione di comando, inclusi i caratteri di nuova riga finali .

Mi rendo conto che sono rimossi per impostazione predefinita, quindi potrebbe essere necessaria una manipolazione per mantenerli e voglio mantenere il codice di uscita originale .

Ad esempio, dato un comando con un numero variabile di newline finali e codice di uscita:

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

Voglio eseguire qualcosa del tipo:

exact_output f

E avere l'output essere:

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

Sono interessato a entrambi bashe POSIX sh.


1
Newline fa parte di $IFS, quindi non verrà catturato come argomento.
Deathgrip

4
@Deathgrip Non ha nulla a che fare con IFS(prova ( IFS=:; subst=$(printf 'x\n\n\n'); printf '%s' "$subst" ). Solo le nuove righe vengono rimosse. \tE `` non farlo, e IFSnon lo influenza.
PSkocik



Risposte:


17

Conchiglie POSIX

Il solito trucco ( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ) per ottenere lo stdout completo di un comando è fare:

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

L'idea è quella di aggiungere ed extra .\n. La sostituzione del comando eliminerà solo quella \n . E spogli il .con ${output%.}.

Si noti che in shell diverse da zsh, non funzionerà comunque se l'output ha byte NUL. Con yash, ciò non funzionerà se l'output non è testo.

Si noti inoltre che in alcune versioni locali, è importante quale carattere utilizzare per inserire alla fine. .dovrebbe generalmente andare bene, ma alcuni altri potrebbero non esserlo. Ad esempio x(come usato in alcune altre risposte) o @non funzionerebbe in una locale usando i set di caratteri BIG5, GB18030 o BIG5HKSCS. In questi set di caratteri, la codifica di un numero di caratteri termina nello stesso byte della codifica di xo @(0x78, 0x40)

Ad esempio, ūin BIG5HKSCS è 0x88 0x78 (ed xè 0x78 come in ASCII, tutti i set di caratteri su un sistema devono avere la stessa codifica per tutti i caratteri del set di caratteri portatile che include lettere inglesi @e .). Quindi, se cmdfosse printf '\x88'e lo avessimo inserito xdopo, ${output%x}non sarebbe stato possibile rimuoverlo xcome $outputsarebbe effettivamente contenuto ū.

Usare .invece potrebbe portare allo stesso problema in teoria se ci fossero dei caratteri la cui codifica termina con la stessa codifica di ., ma per aver verificato qualche tempo fa, posso dire che nessuno dei set di caratteri che potrebbero essere disponibili per l'uso in una locale in un sistema Debian, FreeBSD o Solaris hanno tali caratteri che è abbastanza buono per me (e perché ho deciso .quale sia anche il simbolo per segnare la fine di una frase in inglese, quindi sembra appropriato).

Un approccio più corretto, come discusso da @Arrow, sarebbe quello di cambiare le impostazioni locali in C solo per lo stripping dell'ultimo carattere ( ${output%.}) che assicurerebbe che venga rimosso solo un byte, ma ciò complicherebbe significativamente il codice e potenzialmente introdurrebbe problemi di compatibilità di propria.

alternative bash / zsh

Con bashe zsh, supponendo che l'output non abbia NUL, puoi anche fare:

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

Per ottenere lo stato di uscita cmd, si può fare wait "$!"; ret=$?in bash, ma non in zsh.

rc / es / akanaga

Per completezza, notare che rc/ es/ akangaavere un operatore per quello. In essi, la sostituzione dei comandi, espressa come `cmd(o `{cmd}per comandi più complessi) restituisce un elenco (dividendo $ifsper impostazione predefinita, spazio-tab-newline). In quelle conchiglie (al contrario delle conchiglie simili a Bourne), lo stripping di newline viene eseguito solo come parte di quella $ifsdivisione. Quindi è possibile svuotare $ifso utilizzare il ``(seps){cmd}modulo in cui si specificano i separatori:

ifs = ''; output = `cmd

o:

output = ``()cmd

In ogni caso, lo stato di uscita del comando viene perso. Dovresti incorporarlo nell'output ed estrarlo in seguito, il che diventerebbe brutto.

pesce

Nei pesci, la sostituzione dei comandi avviene con (cmd)e non implica una sottostruttura.

set var (cmd)

Crea un $vararray con tutte le righe nell'output di cmdif $IFSnon è vuoto, o con l'output di cmdstripped di fino a un carattere (al contrario di tutti nella maggior parte delle altre shell) se $IFSè vuoto.

Quindi c'è ancora un problema in questo (printf 'a\nb')ed (printf 'a\nb\n')espandersi alla stessa cosa anche con uno spazio vuoto $IFS.

Per ovviare a questo, il meglio che potevo inventare era:

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

Un'alternativa è fare:

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

Conchiglia Bourne

La shell Bourne non supportava il $(...)modulo né l' ${var%pattern}operatore, quindi può essere piuttosto difficile raggiungerlo. Un approccio consiste nell'utilizzare eval e citando:

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\"
  `"

Qui, stiamo generando un

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

essere passato a eval. Per quanto riguarda l'approccio POSIX, se 'fosse uno di quei personaggi la cui codifica può essere trovata alla fine di altri personaggi, avremmo un problema (molto peggio in quanto diventerebbe una vulnerabilità dell'iniezione di comando), ma per fortuna, come ., non è uno di quelli, e quella tecnica di quotazione è generalmente quella usata da tutto ciò che cita il codice della shell (nota che \ha il problema, quindi non dovrebbe essere usato (esclude anche "..."all'interno del quale è necessario usare le barre rovesciate per alcuni caratteri) Qui, lo stiamo usando solo dopo 'che è OK).

tcsh

Vedi tcsh preserva le nuove righe nella sostituzione dei comandi `...`

(non occupandosi dello stato di uscita, che potresti risolvere salvandolo in un file temporaneo ( echo $status > $tempfile:qdopo il comando))


Grazie - e soprattutto per l'indizio sui diversi set di caratteri. Se è zshpossibile archiviare NULin una variabile, perché non dovrebbe IFS= read -rd '' output < <(cmd)funzionare? Deve essere in grado di memorizzare la lunghezza di una stringa ... codifica ''come stringa di 1 byte \0anziché come stringa di 0 byte?
Tom Hale,

1
@TomHale, sì, read -d ''viene trattato come read -d $'\0'( bashanche se c'è $'\0'lo stesso di ''ovunque).
Stéphane Chazelas,

Stai unendo personaggi e byte. Ti preghiamo di comprendere che se rimuoviamo esattamente ciò che è stato aggiunto, l'entità originale non deve cambiare. Non è così difficile rimuovere un byte chiamato xse è quello che è stato aggiunto. Dai un'occhiata alla mia risposta modificata.
Isacco,

@Arrow, sì, il var=value command evaltrucco è stato discusso qui ( anche ) e nella mailing list del gruppo austin prima. Scoprirai che non è portatile (ed è abbastanza ovvio quando stai provando cose del genere a=1 command eval 'unset a; a=2'o peggio che non doveva essere usato in quel modo). Lo stesso per quello savedVAR=$VAR;...;VAR=$savedVARche non fa quello che vuoi quando $VARinizialmente era disinserito. Se questo è solo per aggirare un problema teorico (un bug che non può essere risolto in pratica), IMO, non vale la pena. Ti supporterò comunque per averci provato.
Stéphane Chazelas,

Hai un link a dove hai discuso e infine scartato l'uso di LANG=Cper rimuovere un byte da una stringa? Stai sollevando preoccupazioni intorno al punto reale, tutti sono facili da risolvere. (1) non viene utilizzato unset (2) Testare la variabile prima di modificarla. @ StéphaneChazelas
Isaac,

3

Per la nuova domanda, questo script funziona:

#!/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

In esecuzione:

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

La descrizione più lunga

La solita saggezza per le shell POSIX da affrontare con la rimozione di \nè:

aggiungi un x

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

Che è richiesto perché l'ultima nuova linea ( S ) vengono rimosse dalla espansione comando per specifiche POSIX :

rimuovendo le sequenze di uno o più caratteri alla fine della sostituzione.


A proposito di un finale x.

In questa domanda è stato detto che un xpotrebbe essere confuso con il byte finale di alcuni caratteri in alcune codifiche. Ma come indovineremo cosa o quale personaggio è meglio in qualche lingua in qualche possibile codifica, questa è una proposta difficile, per non dire altro.

Tuttavia; Questo è semplicemente errato .

L'unica regola che dobbiamo seguire è aggiungere esattamente ciò che rimuoviamo.

Dovrebbe essere facile capire che se aggiungiamo qualcosa a una stringa esistente (o sequenza di byte) e successivamente rimuoviamo esattamente lo stesso qualcosa, la stringa originale (o sequenza di byte) deve essere la stessa.

Dove sbagliamo? Quando mescoliamo caratteri e byte .

Se aggiungiamo un byte, dobbiamo rimuovere un byte, se aggiungiamo un carattere dobbiamo rimuovere esattamente lo stesso carattere .

La seconda opzione, l'aggiunta di un carattere (e la successiva rimozione dello stesso carattere esatto) può diventare complicata e complessa e, sì, le pagine di codice e le codifiche possono interferire.

Tuttavia, la prima opzione è del tutto possibile e, dopo averla spiegata, diventerà semplicemente semplice.

Aggiungiamo un byte, un byte ASCII (<127) e per mantenere le cose il meno contorte possibile, diciamo un carattere ASCII nell'intervallo di az. O come dovremmo dirlo, un byte nell'intervallo esadecimale 0x61- 0x7a. Consente di scegliere uno di quelli, forse una x (davvero un byte di valore 0x78). Possiamo aggiungere tale byte concatenando una x a una stringa (supponiamo che un é):

$ a
$ b=${a}x

Se consideriamo la stringa come una sequenza di byte, vediamo:

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

Una sequenza di stringhe che termina con una x.

Se rimuoviamo quella x (valore byte 0x78), otteniamo:

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

Funziona senza problemi.

Un esempio un po 'più difficile.

Diciamo che la stringa a cui siamo interessati termina in byte 0xc3:

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

E consente di aggiungere un byte di valore 0xa9

$ b=$a$'\xa9'

La stringa è diventata questa ora:

$ echo "$b"
a test string é

Esattamente quello che volevo, gli ultimi due byte sono un carattere in utf8 (quindi chiunque potrebbe riprodurre questi risultati nella propria console utf8).

Se rimuoviamo un carattere, la stringa originale verrà modificata. Ma non è quello che abbiamo aggiunto, abbiamo aggiunto un valore byte, che sembra essere scritto come una x, ma comunque un byte.

Ciò di cui abbiamo bisogno per evitare di interpretare erroneamente i byte come caratteri. Ciò di cui abbiamo bisogno è un'azione che rimuova il byte che abbiamo usato 0xa9. In effetti, ash, bash, lksh e mksh sembrano fare esattamente questo:

$ 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

Ma non ksh o zsh.

Tuttavia, questo è molto facile da risolvere, diciamo a tutte quelle shell di fare la rimozione dei byte:

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

tutto qui, tutte le shell testate funzionano (tranne yash) (per l'ultima parte della stringa):

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

Proprio così semplice, dire alla shell di rimuovere un carattere LC_ALL = C, che è esattamente un byte per tutti i valori di byte da 0x00a 0xff.

Soluzione per i commenti:

Per l'esempio discusso nei commenti, una possibile soluzione (che fallisce in 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

Ciò eliminerà il problema della codifica.


Buono a sapersi che è possibile rimuovere più di una nuova riga finale.
Tom Hale,


Concordo sul fatto che fissare le impostazioni locali su C per assicurarsi che ${var%?}rimuova sempre un byte sia più corretto in teoria, ma: 1- LC_ALLe LC_CTYPEsovrascrivi $LANG, quindi è necessario impostare LC_ALL=C2- non è possibile eseguire l'operazione var=${var%?}in una subshell come farebbe la modifica va perso, quindi è necessario salvare e ripristinare il valore e lo stato di LC_ALL(o ricorrere a localfunzionalità dell'ambito non POSIX ) 3- La modifica della locale a metà dello script non è completamente supportata in alcune shell come yash. D'altra parte, in pratica .non è mai un problema nei set di caratteri della vita reale, quindi usarlo evita di confondersi con LC_ALL.
Stéphane Chazelas,

2

È possibile generare un carattere dopo l'output normale e quindi rimuoverlo:

#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}
}

Questa è una soluzione conforme a POSIX.


Sulla base delle risposte, vedo che la mia domanda non è chiara. L'ho appena aggiornato.
Tom Hale,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.