Sustitución de comandos: división en nueva línea pero no espacio

30

Sé que puedo resolver este problema de varias maneras, pero me pregunto si hay una manera de hacerlo usando solo las funciones integradas de bash, y si no, cuál es la forma más eficiente de hacerlo.

Tengo un archivo con contenido como

AAA
B C DDD
FOO BAR

con lo cual solo quiero decir que tiene varias líneas y cada línea puede o no tener espacios. Quiero ejecutar un comando como

cmd AAA "B C DDD" "FOO BAR"

Si uso cmd $(< file)obtengo

cmd AAA B C DDD FOO BAR

y si uso cmd "$(< file)"me sale

cmd "AAA B C DDD FOO BAR"

¿Cómo hago que cada línea trate exactamente un parámetro?

Viejo pro
fuente

Respuestas:

26

Portablemente:

set -f              # turn off globbing
IFS='
'                   # split at newlines only
cmd $(cat <file)
unset IFS
set +f

O usando una subshell para hacer que la IFSopción y los cambios sean locales:

( set -f; IFS='
'; exec cmd $(cat <file) )

El shell realiza la división de campo y la generación de nombre de archivo en el resultado de una variable o sustitución de comando que no está entre comillas dobles. Por lo tanto, debe desactivar la generación de nombre de archivo set -fy configurar la división de campos con IFSpara que solo las líneas nuevas separen los campos.

No hay mucho que ganar con las construcciones bash o ksh. Puede hacer IFSlocal a una función, pero no set -f.

En bash o ksh93, puede almacenar los campos en una matriz, si necesita pasarlos a múltiples comandos. Debe controlar la expansión en el momento en que crea la matriz. Luego se "${a[@]}"expande a los elementos de la matriz, uno por palabra.

set -f; IFS=$'\n'
a=($(cat <file))
set +f; unset IFS
cmd "${a[@]}"
Gilles 'SO- deja de ser malvado'
fuente
10

Puede hacer esto con una matriz temporal.

Preparar:

$ cat input
AAA
A B C
DE F
$ cat t.sh
#! /bin/bash
echo "$1"
echo "$2"
echo "$3"

Llena la matriz:

$ IFS=$'\n'; set -f; foo=($(<input))

Usa la matriz:

$ for a in "${foo[@]}" ; do echo "--" "$a" "--" ; done
-- AAA --
-- A B C --
-- DE F --

$ ./t.sh "${foo[@]}"
AAA
A B C
DE F

No puedo encontrar una manera de hacerlo sin esa variable temporal, a menos que el IFScambio no sea importante cmd, en cuyo caso:

$ IFS=$'\n'; set -f; cmd $(<input) 

Deberías hacerlo.

Estera
fuente
IFSSiempre me confunde. IFS=$'\n' cmd $(<input)no funciona IFS=$'\n'; cmd $(<input); unset IFSfunciona. ¿Por qué? Supongo que usaré(IFS=$'\n'; cmd $(<input))
Old Pro
66
@OldPro IFS=$'\n' cmd $(<input)no funciona porque solo se establece IFSen el entorno de cmd. $(<input)se expande para formar el comando, antes de que IFSse realice la asignación a .
Gilles 'SO- deja de ser malvado'
8

Parece que la forma canónica de hacer esto bashes algo así como

unset args
while IFS= read -r line; do 
    args+=("$line") 
done < file

cmd "${args[@]}"

o, si su versión de bash tiene mapfile:

mapfile -t args < filename
cmd "${args[@]}"

La única diferencia que puedo encontrar entre el mapfile y el ciclo while-read versus el one-liner

(set -f; IFS=$'\n'; cmd $(<file))

es que el primero convertirá una línea en blanco en un argumento vacío, mientras que el de una línea ignorará una línea en blanco. En este caso, el comportamiento de una línea es lo que preferiría de todos modos, así que doble bonificación por ser compacto.

Lo usaría IFS=$'\n' cmd $(<file)pero no funciona, porque $(<file)se interpreta para formar la línea de comando antes de que IFS=$'\n'surta efecto.

Aunque no funciona en mi caso, ahora he aprendido que muchas herramientas admiten líneas de terminación en null (\000)lugar de newline (\n)lo que facilita mucho esto al tratar, por ejemplo, los nombres de archivos, que son fuentes comunes de estas situaciones :

find / -name '*.config' -print0 | xargs -0 md5

alimenta una lista de nombres de archivos completamente calificados como argumentos para md5 sin ningún problema o interpolación o lo que sea. Eso lleva a la solución no incorporada

tr "\n" "\000" <file | xargs -0 cmd

aunque esto también ignora las líneas vacías, aunque captura líneas que solo tienen espacios en blanco.

Viejo pro
fuente
Usar cmd $(<file)valores sin citar (usar la habilidad de bash para dividir palabras) siempre es una apuesta arriesgada. Si hay alguna línea *, el shell la ampliará a una lista de archivos.
3

Puede usar el bash incorporado mapfilepara leer el archivo en una matriz

mapfile -t foo < filename
cmd "${foo[@]}"

o, no probado, xargspodría hacerlo

xargs cmd < filename
Glenn Jackman
fuente
De la documentación de mapfile: "mapfile no es una característica de shell común o portátil". Y, de hecho, no es compatible con mi sistema. xargstampoco ayuda.
Old Pro
Necesitarías xargs -doxargs -L
James Youngman
@ James, no, no tengo una -dopción y xargs -L 1ejecuta el comando una vez por línea, pero aún divide los argumentos en espacios en blanco.
Old Pro
1
@OldPro, bueno, usted solicitó "una forma de hacerlo utilizando solo las funciones integradas de bash" en lugar de "una función de shell común o portátil". Si su versión de bash es demasiado antigua, ¿puede actualizarla?
Glenn Jackman
mapfilees muy útil para mí, ya que toma líneas en blanco como elementos de la matriz, lo que el IFSmétodo no hace. IFStrata las nuevas líneas contiguas como un delimitador único ... Gracias por presentarlo, ya que no conocía el comando (aunque, según los datos de entrada del OP y la línea de comando esperada, parece que realmente quiere ignorar las líneas en blanco).
Peter
0
old=$IFS
IFS='  #newline
'
array=`cat Submissions` #input the text in this variable
for ...  #use parts of variable in the for loop
... 
done
IFS=$old

La mejor manera que pude encontrar. Solo funciona.

Rahul Bali
fuente
¿Y por qué funciona si configura el IFSespacio, pero la pregunta es no dividirse en el espacio?
RalfFriedl
0

Archivo

El bucle más básico (portátil) para dividir un archivo en nuevas líneas es:

#!/bin/sh
while read -r line; do            # get one line (\n) at a time.
    set -- "$@" "$line"           # store in the list of positional arguments.
done <infile                      # read from a file called infile.
printf '<%s>' "$@" ; echo         # print the results.

Que imprimirá:

$ ./script
<AAA><A B C><DE F>

Sí, con IFS predeterminado = spacetabnewline.

Por que funciona

  • El shell utilizará IFS para dividir la entrada en varias variables. Como solo hay una variable, el shell no realiza la división. Por lo tanto, no hay cambio de IFSnecesario.
  • Sí, se están eliminando los espacios / pestañas iniciales y finales, pero no parece ser un problema en este caso.
  • No, no englobamiento se hace como ninguna expansión se unquoted . Entonces, no es set -fnecesario.
  • La única matriz utilizada (o necesaria) son los parámetros posicionales tipo matriz.
  • La -ropción (sin formato) es evitar la eliminación de la mayoría de la barra invertida.

Eso no funcionará si se necesita dividir y / o pegar. En tales casos se necesita una estructura más compleja.

Si necesita (aún portátil) para:

  • Evite la eliminación de espacios / pestañas iniciales y finales, utilice: IFS= read -r line
  • Línea de división a VARs en un poco de carácter, utilice: IFS=':' read -r a b c.

Divida el archivo en algún otro carácter (no portátil, funciona con ksh, bash, zsh):

IFS=':' read -d '+' -r a b c

Expansión

Por supuesto, el título de su pregunta se trata de dividir la ejecución de un comando en nuevas líneas evitando la división en espacios.

La única forma de separarse del shell es dejar una expansión sin comillas:

echo $(< file)

Eso está controlado por el valor de IFS y, en las expansiones no citadas, también se aplica el globbing. Para hacer ese trabajo, necesitas:

  • Set IFS a nueva línea única , para conseguir la división de salto de línea única.
  • Desmarque la opción de shell globbing set +f:

    set + f IFS = '' cmd $ (<archivo)

Por supuesto, eso cambia el valor de IFS y de globbing para el resto del script.

Isaac
fuente