shell: continuez les sauts de ligne ('\ n') dans la substitution de commande

14

Je veux pouvoir capturer la sortie exacte d'une substitution de commande, y compris les nouveaux caractères de ligne de fin .

Je me rends compte qu'ils sont supprimés par défaut, donc une manipulation peut être nécessaire pour les conserver, et je veux conserver le code de sortie d'origine .

Par exemple, étant donné une commande avec un nombre variable de sauts de ligne de fin et de code de sortie:

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

Je veux exécuter quelque chose comme:

exact_output f

Et que la sortie soit:

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

Je m'intéresse aux deux bashet POSIX sh.

Tom Hale
la source
1
La nouvelle ligne fait partie de $IFS, donc elle ne sera pas capturée comme argument.
Deathgrip
4
@Deathgrip Cela n'a rien à voir avec IFS(essayer ( IFS=:; subst=$(printf 'x\n\n\n'); printf '%s' "$subst" )seulement les nouvelles lignes dépouillées se.. \tEt `` ne sont pas, et IFSne l' affecte pas.
PSkocik

Réponses:

17

Coques POSIX

L' astuce habituelle ( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ) pour obtenir la sortie complète d'une commande est la suivante:

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

L'idée est d'ajouter et d'extra .\n. La substitution de commande ne fera que supprimer cela \n . Et vous vous déshabillez .avec ${output%.}.

Notez que dans les shells autres que zsh, cela ne fonctionnera toujours pas si la sortie a des octets NUL. Avec yash, cela ne fonctionnera pas si la sortie n'est pas du texte.

Notez également que dans certains paramètres régionaux, le caractère que vous utilisez pour insérer à la fin est important. .devrait généralement être bien, mais certains autres pourraient ne pas. Par exemple x(comme utilisé dans certaines autres réponses) ou @ne fonctionnerait pas dans un environnement local à l'aide des jeux de caractères BIG5, GB18030 ou BIG5HKSCS. Dans ces jeux de caractères, le codage d'un certain nombre de caractères se termine dans le même octet que le codage de xou @(0x78, 0x40)

Par exemple, ūdans BIG5HKSCS est 0x88 0x78 (et xest 0x78 comme en ASCII, tous les jeux de caractères sur un système doivent avoir le même codage pour tous les caractères du jeu de caractères portable qui comprend les lettres anglaises, @et .). Donc , si cmdétait printf '\x88'et nous insérons xaprès, ${output%x}ne parviendrait pas à dépouiller que xcomme $outputcontiendront en fait ū.

L'utilisation à la .place pourrait entraîner le même problème en théorie s'il y avait des caractères dont l'encodage se termine par le même encodage que ., mais pour avoir vérifié il y a quelque temps, je peux dire qu'aucun des jeux de caractères pouvant être disponibles pour une utilisation dans un environnement local dans les systèmes Debian, FreeBSD ou Solaris ont de tels caractères, ce qui est assez bon pour moi (et pourquoi je me suis installé sur .lequel est également le symbole pour marquer la fin d'une phrase en anglais, cela semble donc approprié).

Une approche plus correcte comme discuté par @Arrow serait de changer les paramètres régionaux en C uniquement pour la suppression du dernier caractère ( ${output%.}), ce qui garantirait qu'un seul octet est supprimé, mais cela compliquerait considérablement le code et pourrait potentiellement introduire des problèmes de compatibilité de sa propre.

alternatives bash / zsh

Avec bashet zsh, en supposant que la sortie n'a pas de NUL, vous pouvez également faire:

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

Pour obtenir le statut de sortie de cmd, vous pouvez le faire wait "$!"; ret=$?en bashmais pas en zsh.

rc / es / akanaga

Pour être complet, notez que rc/ es/ akangaa un opérateur pour cela. Dans ceux-ci, la substitution de commandes, exprimée sous la forme `cmd(ou `{cmd}pour des commandes plus complexes) renvoie une liste (en la divisant $ifs, espace-tab-newline par défaut). Dans ces coquilles (par opposition aux coquilles de type Bourne), le dépouillement de la nouvelle ligne n'est effectué que dans le cadre de ce $ifsfractionnement. Vous pouvez donc soit vider, $ifssoit utiliser le ``(seps){cmd}formulaire où vous spécifiez les séparateurs:

ifs = ''; output = `cmd

ou:

output = ``()cmd

Dans tous les cas, l'état de sortie de la commande est perdu. Vous auriez besoin de l'intégrer dans la sortie et de l'extraire ensuite, ce qui deviendrait laid.

poisson

Dans le poisson, la substitution de commande est avec (cmd)et n'implique pas un sous-shell.

set var (cmd)

Crée un $vartableau avec toutes les lignes dans la sortie de cmdif $IFSn'est pas vide, ou avec la sortie de cmddépouillé jusqu'à un (contrairement à tous dans la plupart des autres shells) caractère de nouvelle ligne si $IFSest vide.

Donc, il y a toujours un problème dans cela (printf 'a\nb')et (printf 'a\nb\n')étendre à la même chose même avec un vide $IFS.

Pour contourner cela, le mieux que j'ai pu trouver était:

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

Une alternative est de faire:

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

Coquille de Bourne

Le shell Bourne ne supportait $(...)ni le formulaire, ni l' ${var%pattern}opérateur, il peut donc être assez difficile d'y arriver. Une approche consiste à utiliser eval et à citer:

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

Ici, nous générons un

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

à transmettre à eval. En ce qui concerne l'approche POSIX, s'il 's'agissait de l'un de ces caractères dont l'encodage peut être trouvé à la fin des autres caractères, nous aurions un problème (bien pire car il deviendrait une vulnérabilité d'injection de commande), mais heureusement, comme ., ce n'est pas l'un d'entre eux, et cette technique de citation est généralement celle qui est utilisée par tout ce qui cite le code shell (notez que cela \a le problème, donc ne devrait pas être utilisé (exclut également à l' "..."intérieur de laquelle vous devez utiliser des barres obliques inverses pour certains caractères) Ici, nous ne l'utilisons qu'après un 'qui est OK).

tcsh

Voir tcsh préserver les nouvelles lignes dans la substitution de commande `... '

(sans prendre soin du statut de sortie, que vous pourriez résoudre en l'enregistrant dans un fichier temporaire ( echo $status > $tempfile:qaprès la commande))

Stéphane Chazelas
la source
Merci - et surtout pour l'indice sur les différents jeux de caractères. Si zshpeut stocker NULdans une variable, pourquoi ne IFS= read -rd '' output < <(cmd)fonctionnerait pas ? Il doit être capable de stocker la longueur d'une chaîne ... l'encode-t-il ''comme une chaîne de 1 octet \0plutôt que comme une chaîne de 0 octet?
Tom Hale
1
@TomHale, oui, read -d ''est traité comme read -d $'\0'( bashaussi bien qu'il y en $'\0'ait comme ''partout).
Stéphane Chazelas
Vous combinez des caractères et des octets. Veuillez comprendre que si nous supprimons exactement ce qui a été ajouté, l'entité d'origine ne doit pas changer. Ce n'est pas si difficile de supprimer un octet appelé xsi c'est ce qui a été ajouté. Veuillez jeter un œil à ma réponse modifiée.
Isaac
@Arrow, oui, l' var=value command evalastuce a été discutée ici ( aussi ) et sur la liste de diffusion austin-group avant. Vous constaterez qu'il n'est pas portable (et il est assez évident lorsque vous essayez des choses comme a=1 command eval 'unset a; a=2'ou pire qu'il n'était pas destiné à être utilisé comme ça). De même pour le savedVAR=$VAR;...;VAR=$savedVARqui ne fait pas ce que vous vouliez quand il $VARétait initialement non réglé. Si c'est pour contourner un problème théorique uniquement (un bug qui ne peut pas être atteint en pratique), OMI, ça ne vaut pas la peine. Pourtant, je vais vous soutenir pour avoir essayé.
Stéphane Chazelas
Avez-vous un lien vers l'endroit où vous avez interrompu et finalement éliminé l'utilisation de LANG=Cpour supprimer un octet d'une chaîne? Vous soulevez des préoccupations autour du vrai point, toutes sont faciles à résoudre. (1) aucun désarmement n'est utilisé (2) Testez la variable avant de la modifier. @ StéphaneChazelas
Isaac
3

Pour la nouvelle question, ce script fonctionne:

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

À l'exécution:

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

La description plus longue

La sagesse habituelle pour les coques POSIX pour gérer la suppression de \n:

ajouter un x

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

Cela est nécessaire car les dernières nouvelles lignes ( S ) sont supprimées par l'extension de commande selon la spécification POSIX :

suppression des séquences d'un ou plusieurs caractères à la fin de la substitution.


À propos d'une fuite x.

Il a été dit dans cette question qu'un xpourrait être confondu avec l'octet de fin d'un caractère dans un codage. Mais comment allons-nous deviner quel ou quel caractère est meilleur dans une langue dans un codage possible, c'est une proposition difficile, pour dire le moins.

Pourtant; C'est tout simplement incorrect .

La seule règle que nous devons suivre est d'ajouter exactement ce que nous supprimons.

Il devrait être facile de comprendre que si nous ajoutons quelque chose à une chaîne existante (ou une séquence d'octets) et que nous supprimons plus tard exactement le même quelque chose, la chaîne d'origine (ou la séquence d'octets) doit être la même.

Où allons-nous mal? Quand on mélange des caractères et des octets .

Si nous ajoutons un octet, nous devons supprimer un octet, si nous ajoutons un caractère, nous devons supprimer exactement le même caractère .

La deuxième option, l'ajout d'un caractère (et la suppression ultérieure du même caractère exact) peut devenir compliquée et complexe, et, oui, les pages de code et les encodages peuvent gêner.

Cependant, la première option est tout à fait possible et, après l'avoir expliquée, elle deviendra simple.

Ajoutons un octet, un octet ASCII (<127), et pour garder les choses aussi peu alambiquées que possible, disons un caractère ASCII dans la plage de az. Ou comme nous devrions le dire, un octet dans la plage hexadécimale 0x61- 0x7a. Permet de choisir l'un de ceux-ci, peut-être un x (vraiment un octet de valeur 0x78). Nous pouvons ajouter un tel octet avec en concaténant un x à une chaîne (supposons un é):

$ a
$ b=${a}x

Si nous regardons la chaîne comme une séquence d'octets, nous voyons:

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

Une séquence de chaînes qui se termine par un x.

Si nous supprimons ce x (valeur d'octet 0x78), nous obtenons:

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

Cela fonctionne sans problème.

Un exemple un peu plus difficile.

Disons que la chaîne qui nous intéresse se termine en octets 0xc3:

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

Et permet d'ajouter un octet de valeur 0xa9

$ b=$a$'\xa9'

La chaîne est devenue ceci maintenant:

$ echo "$b"
a test string é

Exactement ce que je voulais, les deux derniers octets sont un caractère dans utf8 (donc n'importe qui pourrait reproduire ces résultats dans sa console utf8).

Si nous supprimons un caractère, la chaîne d'origine sera modifiée. Mais ce n'est pas ce que nous avons ajouté, nous avons ajouté une valeur d'octet, qui se trouve être écrite comme un x, mais un octet de toute façon.

Ce dont nous avons besoin pour éviter de mal interpréter les octets en tant que caractères. Ce dont nous avons besoin, c'est d'une action qui supprime l'octet que nous avons utilisé 0xa9. En fait, ash, bash, lksh et mksh semblent tous faire exactement cela:

$ 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

Mais pas ksh ou zsh.

Cependant, c'est très facile à résoudre, disons à tous ces shells de supprimer les octets:

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

c'est tout, tous les shells testés fonctionnent (sauf yash) (pour la dernière partie de la chaîne):

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

Aussi simple que cela, dites au shell de supprimer un caractère LC_ALL = C, qui est exactement un octet pour toutes les valeurs d'octets de 0x00à 0xff.

Solution pour commentaires:

Pour l'exemple discuté dans les commentaires, une solution possible (qui échoue dans zsh) est:

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

Cela supprimera le problème d'encodage.

Isaac
la source
Bon à savoir que plusieurs sauts de ligne peuvent être supprimés.
Tom Hale
Je suis d'accord que la fixation des paramètres régionaux à C pour s'assurer que ${var%?}toujours un octet est toujours plus correcte en théorie, mais: 1- LC_ALLet LC_CTYPEremplacer $LANG, vous devez donc définir LC_ALL=C2- vous ne pouvez pas faire le var=${var%?}dans un sous-shell comme le ferait le changement être perdu, vous devez donc enregistrer et restaurer la valeur et l'état de LC_ALL(ou recourir à des localfonctionnalités de portée non POSIX ) 3- la modification des paramètres régionaux à mi-chemin dans le script n'est pas entièrement prise en charge dans certains shells comme yash. D'un autre côté, dans la pratique, il .n'y a jamais de problème dans les jeux de caractères réels, donc son utilisation évite de se mélanger avec LC_ALL.
Stéphane Chazelas
2

Vous pouvez sortir un caractère après la sortie normale puis le supprimer:

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

Il s'agit d'une solution compatible POSIX.

PSkocik
la source
D'après les réponses, je constate que ma question n'était pas claire. Je viens de le mettre à jour.
Tom Hale