shell: sigue las líneas nuevas ('\ n') en la sustitución de comandos

14

Quiero poder capturar el resultado exacto de una sustitución de comando, incluidos los nuevos caracteres de línea finales .

Me doy cuenta de que están despojados de forma predeterminada, por lo que es posible que se requiera alguna manipulación para mantenerlos, y quiero mantener el código de salida original .

Por ejemplo, dado un comando con un número variable de nuevas líneas finales y código de salida:

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

Quiero ejecutar algo como:

exact_output f

Y que la salida sea:

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

Estoy interesado en ambos bashy POSIX sh.

Tom Hale
fuente
1
Newline es parte de $IFS, por lo que no se capturará como argumento.
Deathgrip
44
@Deathgrip No tiene nada que ver con IFS(try ( IFS=:; subst=$(printf 'x\n\n\n'); printf '%s' "$subst" )Solamente los saltos de línea quedan despojados.. \tY `` no, y IFSno lo afecta.
PSkocik
Ver también: tcsh preservar nuevas líneas en la sustitución de comandos `...` paratcsh
Stéphane Chazelas

Respuestas:

17

POSIX conchas

El truco habitual ( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ) para obtener el stdout completo de un comando es hacer:

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

La idea es agregar y extra .\n. La sustitución de comandos solo eliminará eso \n . Y te quitas el .con ${output%.}.

Tenga en cuenta que en shells que no sean zsh, eso todavía no funcionará si la salida tiene bytes NUL. Con yash, eso no funcionará si la salida no es texto.

También tenga en cuenta que en algunas configuraciones regionales, importa qué carácter use para insertar al final. .en general debería estar bien, pero otros podrían no estarlo. Por ejemplo x(como se usa en algunas otras respuestas) o @no funcionaría en un entorno local utilizando los conjuntos de caracteres BIG5, GB18030 o BIG5HKSCS. En esos charsets, la codificación de varios caracteres termina en el mismo byte que la codificación de xo @(0x78, 0x40)

Por ejemplo, ūen BIG5HKSCS es 0x88 0x78 (y xes 0x78 como en ASCII, todos los conjuntos de caracteres en un sistema deben tener la misma codificación para todos los caracteres del conjunto de caracteres portátil que incluye letras en inglés, @y .). Entonces, si cmdfue así printf '\x88'y lo insertamos xdespués, ${output%x}no podríamos quitar eso xcomo $outputlo que realmente contiene ū.

En .cambio, el uso podría conducir al mismo problema en teoría si hubiera caracteres cuya codificación termine en la misma codificación que ., pero por haberlo comprobado hace algún tiempo, puedo decir que ninguno de los conjuntos de caracteres que pueden estar disponibles para su uso en un entorno local un sistema Debian, FreeBSD o Solaris tiene esos caracteres, lo cual es lo suficientemente bueno para mí (y por qué me he decidido, .que también es el símbolo para marcar el final de una oración en inglés, por lo que parece apropiado).

Un enfoque más correcto según lo discutido por @Arrow sería cambiar la configuración regional a C solo para eliminar el último carácter ( ${output%.}), lo que aseguraría que solo se elimine un byte, pero eso complicaría significativamente el código y potencialmente introduciría problemas de compatibilidad de su propio.

alternativas bash / zsh

Con bashy zsh, suponiendo que la salida no tenga NUL, también puede hacer:

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

Para obtener el estado de salida cmd, se puede hacer wait "$!"; ret=$?en bashpero no en zsh.

rc / es / akanaga

Para completar, tenga en cuenta que rc/ es/ akangatener un operador para eso. En ellos, la sustitución de comandos, expresada como `cmd(o `{cmd}para comandos más complejos) devuelve una lista (al dividir en $ifs, espacio-tab-nueva línea por defecto). En esos shells (a diferencia de los shells tipo Bourne), la eliminación de la nueva línea solo se realiza como parte de esa $ifsdivisión. Por lo tanto, puede vaciar $ifso usar el ``(seps){cmd}formulario donde especifica los separadores:

ifs = ''; output = `cmd

o:

output = ``()cmd

En cualquier caso, se pierde el estado de salida del comando. Tendría que incrustarlo en la salida y extraerlo después, lo que sería feo.

pescado

En peces, la sustitución de comandos es con (cmd)y no implica una subshell.

set var (cmd)

Crea una $varmatriz con todas las líneas en la salida de cmdif $IFSno está vacía, o con la salida de un carácter de nueva línea cmddespojado de hasta uno (a diferencia de todos en la mayoría de los demás shells) si $IFSestá vacío.

Entonces todavía hay un problema en eso (printf 'a\nb')y (printf 'a\nb\n')expandirse a lo mismo incluso con un vacío $IFS.

Para evitar eso, lo mejor que se me ocurrió fue:

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

Una alternativa es hacer:

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

Concha Bourne

El shell Bourne no admitía la $(...)forma ni el ${var%pattern}operador, por lo que puede ser bastante difícil de lograr allí. Un enfoque es utilizar eval y 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\"
  `"

Aquí estamos generando un

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

para ser pasado a eval. En cuanto al enfoque POSIX, si 'fuera uno de esos caracteres cuya codificación se puede encontrar al final de otros caracteres, tendríamos un problema (mucho peor ya que se convertiría en una vulnerabilidad de inyección de comando), pero afortunadamente, como ., no es uno de esos, y esa técnica de cita es generalmente la que usa cualquier cosa que cita código de shell (tenga en cuenta que \tiene el problema, por lo que no debe usarse (también excluye "..."dentro de lo que necesita usar barras diagonales inversas para algunos caracteres) Aquí, solo lo estamos usando después de lo 'cual está bien).

tcsh

Ver tcsh preservar nuevas líneas en la sustitución de comandos `...`

(sin tener en cuenta el estado de salida, que puede abordar guardándolo en un archivo temporal ( echo $status > $tempfile:qdespués del comando))

Stéphane Chazelas
fuente
Gracias, y especialmente por la pista sobre los diferentes conjuntos de caracteres. Si se zshpuede almacenar NULen una variable, ¿por qué no IFS= read -rd '' output < <(cmd)funcionaría? Necesita poder almacenar la longitud de una cadena ... ¿codifica ''como una cadena de 1 byte en \0lugar de una cadena de 0 bytes?
Tom Hale
1
@TomHale, sí, read -d ''se trata como read -d $'\0'( bashaunque también $'\0'es igual que en ''todas partes).
Stéphane Chazelas
Estás combinando caracteres y bytes. Comprenda que si eliminamos exactamente lo que se agregó, la entidad original no debe cambiar. No es tan difícil eliminar un byte llamado xsi eso es lo que se agregó. Por favor, eche un vistazo a mi respuesta editada.
Isaac
@ Flecha, sí, el var=value command evaltruco se discutió aquí ( también ) y en la lista de correo del grupo Austin antes. Descubrirá que no es portátil (y es bastante obvio cuando intenta cosas como a=1 command eval 'unset a; a=2'o peor que no está destinado a usarse así). Lo mismo para el savedVAR=$VAR;...;VAR=$savedVARque no hace lo que quieres cuando $VARinicialmente no está configurado. Si eso es solo para solucionar un problema teórico (un error que no se puede solucionar en la práctica), en mi opinión, no vale la pena. Aún así, te apoyaré por intentarlo.
Stéphane Chazelas
¿Tiene un enlace a donde discutió y finalmente descartó el uso de LANG=Cpara eliminar un byte de una cadena? Estás planteando inquietudes en torno al punto real, todo es fácil de resolver. (1) no hay unset sin usar (2) Pruebe la variable antes de cambiarla. @ StéphaneChazelas
Isaac
3

Para la nueva pregunta, 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

En ejecución:

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

La descripción más larga

La sabiduría habitual para los shells POSIX para lidiar con la eliminación de \nes:

agregar un x

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

Eso es necesario porque la última línea ( S ) nueva se elimina mediante la expansión del comando según la especificación POSIX :

eliminar secuencias de uno o más caracteres al final de la sustitución.


Sobre un rastro x.

Se ha dicho en esta pregunta que xpodría confundirse con el byte final de algún carácter en alguna codificación. Pero, ¿cómo vamos a adivinar qué o qué personaje es mejor en algún idioma en alguna codificación posible? Esa es una propuesta difícil, por decir lo menos.

Sin embargo; Eso es simplemente incorrecto .

La única regla que debemos seguir es agregar exactamente lo que eliminamos.

Debería ser fácil comprender que si agregamos algo a una cadena existente (o secuencia de bytes) y luego eliminamos exactamente lo mismo, la cadena original (o secuencia de bytes) debe ser la misma.

¿Dónde nos equivocamos? Cuando mezclamos caracteres y bytes .

Si agregamos un byte, debemos eliminar un byte, si agregamos un carácter debemos eliminar exactamente el mismo carácter .

La segunda opción, agregar un carácter (y luego eliminar exactamente el mismo carácter) puede volverse complicado y complejo, y, sí, las páginas de códigos y las codificaciones pueden interferir.

Sin embargo, la primera opción es bastante posible y, después de explicarla, se volverá simple.

Agreguemos un byte, un byte ASCII (<127), y para mantener las cosas lo menos complicadas posible, digamos un carácter ASCII en el rango de az. O como deberíamos decirlo, un byte en el rango hexadecimal 0x61- 0x7a. Elija cualquiera de esos, tal vez una x (realmente un byte de valor 0x78). Podemos agregar dicho byte concatenando una x a una cadena (supongamos que un é):

$ a
$ b=${a}x

Si miramos la cadena como una secuencia de bytes, vemos:

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

Una secuencia de cuerdas que termina en una x.

Si eliminamos esa x (valor de byte 0x78), obtenemos:

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

Funciona sin problemas.

Un ejemplo un poco más difícil.

Digamos que la cadena que nos interesa termina en byte 0xc3:

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

Y agreguemos un byte de valor 0xa9

$ b=$a$'\xa9'

La cadena se ha convertido en esto ahora:

$ echo "$b"
a test string é

Exactamente lo que quería, los últimos dos bytes son un carácter en utf8 (para que cualquiera pueda reproducir estos resultados en su consola utf8).

Si eliminamos un carácter, se cambiará la cadena original. Pero eso no es lo que agregamos, agregamos un valor de byte, que se escribe como una x, pero un byte de todos modos.

Lo que necesitamos para evitar malinterpretar bytes como caracteres. Lo que necesitamos es una acción que elimine el byte que usamos 0xa9. De hecho, ash, bash, lksh y mksh parecen hacer exactamente eso:

$ 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

Pero no ksh o zsh.

Sin embargo, eso es muy fácil de resolver, digamos a todos esos shells que eliminen los bytes:

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

eso es todo, todos los shells probaron el trabajo (excepto yash) (para la última parte de la cadena):

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

Así de simple, dígale al shell que elimine un carácter LC_ALL = C, que es exactamente un byte para todos los valores de byte de 0x00a 0xff.

Solución para comentarios:

Para el ejemplo discutido en los comentarios, una posible solución (que falla en zsh) es:

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

Eso eliminará el problema de la codificación.

Isaac
fuente
Es bueno saber que se puede eliminar más de una nueva línea final.
Tom Hale
Estoy de acuerdo en que arreglar la configuración regional en C para asegurarse de que ${var%?}siempre elimine un byte es más correcto en teoría, pero: 1- LC_ALLy LC_CTYPEanular $LANG, por lo que necesitaría establecer LC_ALL=C2- no puede hacer var=${var%?}en una subcapa como el cambio lo haría perderse, por lo que necesitaría guardar y restaurar el valor y el estado LC_ALL(o recurrir a localfunciones de alcance que no sean POSIX ) 3- cambiar la configuración regional a mitad del guión no es totalmente compatible con algunos shells como yash. Por otro lado, en la práctica .nunca es un problema en los charsets de la vida real, por lo que usarlo evita mezclarse con LC_ALL.
Stéphane Chazelas
2

Puede generar un carácter después de la salida normal y luego despojarlo:

#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 es una solución compatible con POSIX.

PSkocik
fuente
Según las respuestas, veo que mi pregunta no estaba clara. Acabo de actualizarlo.
Tom Hale