Cómo separar la salida del comando a líneas individuales

12
list=`ls -a R*`
echo $list

Dentro de un script de shell, este comando echo mostrará una lista de todos los archivos del directorio actual que comienzan con R, pero en una línea. ¿Cómo puedo imprimir cada artículo en una línea?

Necesito un comando genérico para todos los escenarios pasando con ls, du, find -type -d, etc.

Anonimo
fuente
66
Nota general: no haga esto con ls, se romperá en cualquier nombre de archivo extraño . Ver aquí .
terdon

Respuestas:

12

Si la salida del comando contiene varias líneas, entonces cite sus variables para preservar esas nuevas líneas al hacer eco:

echo "$list"

De lo contrario, el shell se expandirá y dividirá el contenido variable en espacios en blanco (espacios, líneas nuevas, pestañas) y se perderán.

muru
fuente
15

En lugar de poner mal la lssalida en una variable y luego echoing, que elimina todos los colores, use

ls -a1

Desde man ls

       -1     list one file per line.  Avoid '\n' with -q or -b

No te aconsejo que hagas nada con la salida de ls, excepto mostrarlo :)

Utilice, por ejemplo, globos de shell y un forbucle para hacer algo con los archivos ...

shopt -s dotglob                  # to include hidden files*

for i in R/*; do echo "$i"; done

* sin embargo, esto no incluirá el directorio actual .o su padre..

Zanna
fuente
1
Recomiendo reemplazar: echo "$i"con printf "%s\n" "$i" (ese no se ahogará si un nombre de archivo comienza con un "-"). En realidad, la mayoría de las veces se echo somethingdebe reemplazar con printf "%s\n" "something".
Olivier Dulac
2
@OlivierDulac Todos comienzan Raquí, pero en general, sí. Sin embargo, incluso para las secuencias de comandos, intentar acomodar siempre el liderazgo -en las rutas causa problemas: la gente asume --, que solo algunos comandos admiten, significa el final de las opciones. He visto find . [tests] -exec [cmd] -- {} \;donde [cmd]no es compatible --. ¡Todos los caminos allí comienzan de .todos modos! Pero ese no es un argumento en contra de reemplazar echocon printf '%s\n'. Aquí, uno puede reemplazar todo el ciclo con printf '%s\n' R/*. Bash tiene printfuna función incorporada, por lo que efectivamente no hay límite para el número / longitud de los argumentos.
Eliah Kagan
12

Si bien ponerlo entre comillas como sugirió @muru realmente hará lo que solicitó, también puede considerar usar una matriz para esto. Por ejemplo:

IFS=$'\n' dirs=( $(find . -type d) )

El IFS=$'\n'comando le dice a bash que solo divida la salida en caracteres de nueva línea para obtener cada elemento de la matriz. Sin él, se dividirá en espacios, por a file name with spaces.txtlo que serían 5 elementos separados en lugar de uno. Sin \nembargo, este enfoque se romperá si los nombres de archivo / directorio pueden contener líneas nuevas ( ). Guardará cada línea de la salida del comando como un elemento de la matriz.

Tenga en cuenta que también he cambiado el estilo antiguo `command`de $(command)los cuales es la sintaxis preferida.

Ahora tiene una matriz llamada $dirs, cada bof cuyos elementos son una línea de la salida del comando anterior. Por ejemplo:

$ find . -type d
.
./olad
./ho
./ha
./ads
./bar
./ga
./da
$ IFS=$'\n' dirs=( $(find . -type d) )
$ for d in "${dirs[@]}"; do
    echo "DIR: $d"
  done
DIR: .
DIR: ./olad
DIR: ./ho
DIR: ./ha
DIR: ./ads
DIR: ./bar
DIR: ./ga
DIR: ./da

Ahora, debido a cierta extrañeza de bash (ver aquí ), deberá restablecer IFSel valor original después de hacer esto. Entonces, guárdelo y vuelva a alinear:

oldIFS="$IFS"
IFS=$'\n' dirs=( $(find . -type d) ) 
IFS="$oldIFS"

O hazlo manualmente:

IFS=" "$'\t\n '

Alternativamente, solo cierre la terminal actual. Su nuevo tendrá el IFS original configurado de nuevo.

terdon
fuente
Podría haber sugerido algo como esto, pero la parte duque lo hace parecer OP está más interesado en preservar el formato de salida.
muru
¿Cómo puedo hacer que esto funcione si los nombres de directorio contienen espacios?
postre
¡@ whoops! Debería funcionar con espacios (pero no líneas nuevas) ahora. Gracias por mencionarlo.
terdon el
1
Tenga en cuenta que debe restablecer o desactivar IFS después de esa asignación de matriz. unix.stackexchange.com/q/264635/70524
muru
@muru oh wow, gracias, me había olvidado de esa rareza.
terdon
2

Si debe almacenar la salida y desea una matriz, mapfilese lo pone fácil.

Primero, considere si necesita almacenar la salida de su comando. Si no lo hace, simplemente ejecute el comando.

Si decide que desea leer la salida de un comando como una matriz de líneas, es cierto que una forma de hacerlo es deshabilitar el globbing, configurar la IFSdivisión en líneas, usar la sustitución de comandos dentro de la ( )sintaxis de creación de la matriz y restablecer IFSdespués, que es más complejo de lo que parece. La respuesta de terdon cubre algo de ese enfoque. Pero le sugiero que utilice mapfile, en su lugar , el comando incorporado del shell Bash para leer texto como una matriz de líneas. Aquí hay un caso simple en el que estás leyendo un archivo:

mapfile < filename

Esto lee líneas en una matriz llamada MAPFILE. Sin la redirección de entrada , estaría leyendo desde la entrada estándar del shell (generalmente su terminal) en lugar del archivo nombrado por . Para leer en una matriz que no sea la predeterminada , pase su nombre. Por ejemplo, esto se lee en la matriz :< filenamefilenameMAPFILElines

mapfile lines < filename

Otro comportamiento predeterminado que puede elegir cambiar es que los caracteres de nueva línea al final de las líneas se dejan en su lugar; aparecen como el último carácter en cada elemento de la matriz (a menos que la entrada finalice sin un carácter de nueva línea, en cuyo caso el último elemento no tiene uno). Para masticar estas nuevas líneas para que no aparezcan en la matriz, pase la -topción a mapfile. Por ejemplo, esto se lee en la matriz recordsy no escribe caracteres de nueva línea al final de sus elementos de la matriz:

mapfile -t records < filename

También puede usar -tsin pasar un nombre de matriz; es decir, también funciona con un nombre de matriz implícito MAPFILE.

El mapfileshell integrado admite otras opciones y, alternativamente, se puede invocar como readarray. Ejecute help mapfile(y help readarray) para más detalles.

Pero no desea leer desde un archivo, desea leer desde la salida de un comando. Para lograr eso, use la sustitución del proceso . Este comando lee líneas del comando some-commandcon arguments...sus argumentos de línea de comando y los coloca en mapfilela matriz predeterminada MAPFILE, con sus caracteres de línea nueva eliminados:

mapfile -t < <(some-command arguments...)

La sustitución del proceso reemplaza con un nombre de archivo real desde el cual se puede leer la salida de la ejecución . El archivo es una tubería con nombre en lugar de un archivo normal y, en Ubuntu, se llamará como (a veces con algún otro número que no sea ), pero no necesita preocuparse por los detalles, porque el shell se encarga de eso todo detrás de escena<(some-command arguments...)some-command arguments.../dev/fd/6363

Puede pensar que podría renunciar a la sustitución de procesos utilizando en su lugar, pero eso no funcionará porque, cuando tiene una tubería de múltiples comandos separados por , Bash ejecuta ejecuta todos los comandos en subcapas . Por lo tanto, tanto y ejecutar en sus propios entornos , pero inicializados de separar del medio ambiente de la concha en la que se ejecuta la tubería. En el subnivel donde se ejecuta, el conjunto que hace son rellenados, pero luego esa matriz se descarta cuando termina el comando. nunca se crea o modifica para la persona que llama.some-command arguments... | mapfile -t|some-command arguments...mapfile -tmapfile -tMAPFILEMAPFILE

Así es como se ve el ejemplo en la respuesta de terdon , en su totalidad, si usa mapfile:

mapfile -t dirs < <(find . -type d)
for d in "${dirs[@]}"; do
    echo "DIR: $d"
done

Eso es. No necesita verificar si IFSse configuró , realizar un seguimiento de si no se configuró y con qué valor, configurarlo en una nueva línea, luego restablecerlo o reiniciarlo más tarde. Usted no tiene que desactivar el englobamiento (por ejemplo, con set -f) - que es realmente necesario si va a utilizar ese método en serio, ya que los nombres de archivo pueden contener *, ?y [--¡entonces volver a habilitar ( set +f) después.

También puede reemplazar ese ciclo en particular por completo con un solo printfcomando, aunque esto no es realmente un beneficio mapfile, ya que puede hacerlo si usa mapfileu otro método para completar la matriz. Aquí hay una versión más corta:

mapfile -t < <(find . -type d)
printf 'DIR: %s\n' "${MAPFILE[@]}"

Es importante tener en cuenta que, como menciona Terdon , operar línea por línea no siempre es apropiado y no funcionará correctamente con nombres de archivo que contengan nuevas líneas. Recomiendo no nombrar archivos de esa manera, pero puede suceder, incluso por accidente.

No existe realmente una solución única para todos.

Usted solicitó "un comando genérico para todos los escenarios", y el enfoque de utilizar mapfilealgo se acerca a ese objetivo, pero le insto a que reconsidere sus requisitos.

La tarea que se muestra arriba se logra mejor con solo un findcomando:

find . -type d -printf 'DIR: %p\n'

También puede usar un comando externo como sedagregar DIR: al comienzo de cada línea. Esto podría decirse que es algo feo, y a diferencia de ese comando find, agregará "prefijos" adicionales dentro de los nombres de archivo que contienen líneas nuevas, pero funciona independientemente de su entrada, por lo que cumple con sus requisitos y aún es preferible leer la salida en un variable o matriz :

find . -type d | sed 's/^/DIR: /'

Si necesita enumerar y también realizar una acción en cada directorio encontrado, como ejecutar some-commandy pasar la ruta del directorio como argumento, también findpuede hacerlo:

find . -type d -print -exec some-command {} \;

Como otro ejemplo, volvamos a la tarea general de agregar un prefijo a cada línea. Supongamos que quiero ver la salida de help mapfilepero numerar las líneas. Realmente no lo usaría mapfilepara esto, ni ningún otro método que lo lea en una variable de shell o matriz de shell. Suponiendo help mapfile | cat -nque no da el formato que quiero, puedo usar awk:

help mapfile | awk '{ printf "%3d: %s\n", NR, $0 }'

Leer toda la salida de un comando en una sola variable o matriz a veces es útil y apropiado, pero tiene grandes desventajas. No solo tiene que lidiar con la complejidad adicional de usar su shell cuando un comando existente o una combinación de comandos existentes ya pueden hacer lo que necesita también o mejor, sino que toda la salida del comando debe almacenarse en la memoria. A veces puede saber que no es un problema, pero a veces puede estar procesando un archivo grande.

Una alternativa que se intenta comúnmente, y a veces se hace bien, es leer la entrada línea por línea con read -run bucle. Si no necesita almacenar líneas anteriores mientras opera en líneas posteriores, y tiene que usar una entrada larga, entonces puede ser mejor que mapfile. Pero, también, debe evitarse en los casos en que simplemente puede canalizarlo a un comando que puede hacer el trabajo, que es la mayoría de los casos .

Eliah Kagan
fuente