¿Cómo ejecuta un comando para cada línea de un archivo?

161

Por ejemplo, en este momento estoy usando lo siguiente para cambiar un par de archivos cuyas rutas de Unix escribí en un archivo:

cat file.txt | while read in; do chmod 755 "$in"; done

¿Existe una forma más elegante y segura?

halcón
fuente

Respuestas:

127

Leer un archivo línea por línea y ejecutar comandos: 4 respuestas

Esto se debe a que no solo hay 1 respuesta ...

  1. shell expansión de línea de comando
  2. xargs herramienta dedicada
  3. while read con algunas observaciones
  4. while read -uutilizando dedicado fd, para procesamiento interactivo (muestra)

En cuanto a la solicitud OP: correr chmoden todos los objetivos que figuran en el expediente , xargses la herramienta indicada. Pero para algunas otras aplicaciones, pequeña cantidad de archivos, etc.

  1. Lea todo el archivo como argumento de línea de comando.

    Si su archivo no es demasiado grande y todos los archivos están bien nombrados (sin espacios u otros caracteres especiales como comillas), puede usar la shellexpansión de línea de comando . Simplemente:

    chmod 755 $(<file.txt)

    Para una pequeña cantidad de archivos (líneas), este comando es el más ligero.

  2. xargs es la herramienta correcta

    Para una mayor cantidad de archivos, o casi cualquier número de líneas en su archivo de entrada ...

    Para muchos binutils herramientas, como chown, chmod, rm, cp -t...

    xargs chmod 755 <file.txt

    Si tiene caracteres especiales y / o muchas líneas en file.txt.

    xargs -0 chmod 755 < <(tr \\n \\0 <file.txt)

    si su comando necesita ejecutarse exactamente 1 vez por entrada:

    xargs -0 -n 1 chmod 755 < <(tr \\n \\0 <file.txt)

    Esto no es necesario para esta muestra, ya que chmodacepta varios archivos como argumento, pero coincide con el título de la pregunta.

    Para algún caso especial, incluso podría definir la ubicación del argumento del archivo en los comandos generados por xargs:

    xargs -0 -I '{}' -n 1 myWrapper -arg1 -file='{}' wrapCmd < <(tr \\n \\0 <file.txt)

    Prueba con seq 1 5como entrada

    Prueba esto:

    xargs -n 1 -I{} echo Blah {} blabla {}.. < <(seq 1 5)
    Blah 1 blabla 1..
    Blah 2 blabla 2..
    Blah 3 blabla 3..
    Blah 4 blabla 4..
    Blah 5 blabla 5..

    Donde el comando se realiza una vez por línea .

  3. while read y variantes.

    Como sugiere OP cat file.txt | while read in; do chmod 755 "$in"; donefuncionará, pero hay 2 problemas:

    • cat |es un tenedor inútil , y

    • | while ... ;donese convertirá en una subshell donde el entorno desaparecerá después ;done.

    Entonces esto podría estar mejor escrito:

    while read in; do chmod 755 "$in"; done < file.txt

    Pero,

    • Se le puede advertir $IFSy readbanderas:

      help read
      read: read [-r] ... [-d delim] ... [name ...]
          ...
          Reads a single line from the standard input... The line is split
          into fields as with word splitting, and the first word is assigned
          to the first NAME, the second word to the second NAME, and so on...
          Only the characters found in $IFS are recognized as word delimiters.
          ...
          Options:
            ...
            -d delim   continue until the first character of DELIM is read, 
                       rather than newline
            ...
            -r do not allow backslashes to escape any characters
          ...
          Exit Status:
          The return code is zero, unless end-of-file is encountered...

      En algunos casos, es posible que deba usar

      while IFS= read -r in;do chmod 755 "$in";done <file.txt

      Para evitar problemas con nombres de archivos extraños. Y tal vez si encuentra problemas con UTF-8:

      while LANG=C IFS= read -r in ; do chmod 755 "$in";done <file.txt
    • Mientras usa STDINpara leer file.txt, su script no puede ser interactivo (ya no puede usarlo STDIN).

  4. while read -u, utilizando dedicado fd.

    Sintaxis: while read ...;done <file.txtredirigirá STDINa file.txt. Eso significa que no podrá lidiar con el proceso hasta que finalicen.

    Si planea crear una herramienta interactiva , debe evitar el uso STDINy utilizar algún descriptor de archivo alternativo .

    Los descriptores de archivos constantes son: 0para STDIN , 1para STDOUT y 2para STDERR . Podrías verlos por:

    ls -l /dev/fd/

    o

    ls -l /proc/self/fd/

    A partir de ahí, debe elegir el número no utilizado, entre 0y 63(más, de hecho, dependiendo de la sysctlherramienta de superusuario) como descriptor de archivo :

    Para esta demostración, usaré fd 7 :

    exec 7<file.txt      # Without spaces between `7` and `<`!
    ls -l /dev/fd/

    Entonces podría usar de read -u 7esta manera:

    while read -u 7 filename;do
        ans=;while [ -z "$ans" ];do
            read -p "Process file '$filename' (y/n)? " -sn1 foo
            [ "$foo" ]&& [ -z "${foo/[yn]}" ]&& ans=$foo || echo '??'
        done
        if [ "$ans" = "y" ] ;then
            echo Yes
            echo "Processing '$filename'."
        else
            echo No
        fi
    done 7<file.txt

    done

    Cerrar fd/7 :

    exec 7<&-            # This will close file descriptor 7.
    ls -l /dev/fd/

    Nota: Dejé la versión marcada porque esta sintaxis podría ser útil, al hacer muchas E / S con procesos paralelos:

    mkfifo sshfifo
    exec 7> >(ssh -t user@host sh >sshfifo)
    exec 6<sshfifo
F. Hauri
fuente
3
Como xargsse creó inicialmente para responder a este tipo de necesidad, algunas características, como construir el comando durante el mayor tiempo posible en el entorno actual para invocar chmoden este caso lo menos posible, reducir las horquillas aseguran la eficiencia. while ;do..done <$fileimplica ejecutar 1 fork para 1 archivo. xargspodría ejecutar 1 fork por mil archivos ... de manera confiable.
F. Hauri
1
¿Por qué el tercer comando no funciona en un archivo MAKE? Recibo un "error de sintaxis cerca del token inesperado` <'", pero la ejecución directa desde la línea de comando funciona.
Woodrow Barlow
2
Esto parece vinculado a la sintaxis específica de Makefile. Puede intentar invertir la línea de comando:cat file.txt | tr \\n \\0 | xargs -0 -n1 chmod 755
F. Hauri
@ F.Hauri, por alguna razón, tr \\n \\0 <file.txt |xargs -0 [command]es aproximadamente un 50% más rápido que el método que describió.
phil294
Octubre de 2019, nueva edición, agregue una muestra de procesador de archivos interactivo .
F. Hauri
150

Si.

while read in; do chmod 755 "$in"; done < file.txt

De esta manera puede evitar un catproceso.

catcasi siempre es malo para un propósito como este. Puede leer más sobre el uso inútil del gato.

PÁGINAS
fuente
Evitar uno cat es una buena idea, pero en este caso, el comando indicado esxargs
F. Hauri
Ese enlace no parece ser relevante, ¿tal vez el contenido de la página web ha cambiado? Sin embargo, el resto de la respuesta es increíble :)
starbeamrainbowlabs
@starbeamrainbowlabs Sí. Parece que la página se ha movido. Me he reenlazado y debería estar bien ahora. Gracias :)
PP
1
¡Gracias! Esto fue útil, especialmente cuando necesita hacer algo más que llamar chmod(es decir, ejecutar realmente un comando para cada línea del archivo).
Por Lundberg
¡Ten cuidado con las barras invertidas! de unix.stackexchange.com/a/7561/28160 - " read -rlee una sola línea desde la entrada estándar ( readsin -rinterpreta barras invertidas, no quiere eso)".
Ese chico brasileño
16

si tiene un buen selector (por ejemplo, todos los archivos .txt en un directorio) puede hacer:

for i in *.txt; do chmod 755 "$i"; done

bash for loop

o una variante tuya:

while read line; do chmod 755 "$line"; done <file.txt
d.raev
fuente
Lo que no funciona es que si hay espacios en la línea, la entrada se divide por espacios, no por línea.
Michael Fox
@Michael Fox: las líneas con espacios se pueden admitir cambiando el separador. Para cambiarlo a líneas nuevas, configure la variable de entorno 'IFS' antes del script / comando. Ej: export IFS = '$ \ n'
codesniffer
Error tipográfico en mi último comentario. Debería ser: export IFS = $ '\ n'
codesniffer
14

Si sabe que no tiene ningún espacio en blanco en la entrada:

xargs chmod 755 < file.txt

Si puede haber espacios en blanco en las rutas, y si tiene xargs GNU:

tr '\n' '\0' < file.txt | xargs -0 chmod 755
Glenn Jackman
fuente
Sé sobre xargs, pero (lamentablemente) parece una solución menos confiable que las funciones integradas de bash como while y read. Además, no tengo GNU xargs, pero estoy usando OS X y xargs también tiene una opción -0 aquí. Gracias por la respuesta.
halcón
1
@hawk No: xargses robusto. Esta herramienta es muy antigua y su código se revisa fuertemente . Su objetivo era inicialmente construir líneas con respecto a las limitaciones de shell (64kchar / line o algo así). Ahora esta herramienta podría funcionar con archivos muy grandes y puede reducir mucho el número de fork a comando final. Ver mi respuesta y / o man xargs.
F. Hauri
@hawk ¿Menos de una solución confiable de qué manera? Si funciona en Linux, Mac / BSD y Windows (sí, los paquetes de MSYSGIT GNU xargs), entonces es lo más confiable posible.
Camilo Martin
1
Para aquellos que todavía lo encuentran en los resultados de búsqueda ... puede instalar GNU xargs en macOS usando Homebrew ( brew install findutils), y luego invocar GNU xargs con gxargs, por ejemplo,gxargs chmod 755 < file.txt
Jase
13

Si desea ejecutar su comando en paralelo para cada línea, puede usar GNU Parallel

parallel -a <your file> <program>

Cada línea de su archivo se pasará al programa como argumento. De forma predeterminada, parallelejecuta tantos subprocesos como su CPU cuenta. Pero puedes especificarlo con-j

janisz
fuente
3

Veo que etiquetaste bash, pero Perl también sería una buena manera de hacer esto:

perl -p -e '`chmod 755 $_`' file.txt

También puede aplicar una expresión regular para asegurarse de obtener los archivos correctos, por ejemplo, para procesar solo archivos .txt:

perl -p -e 'if(/\.txt$/) `chmod 755 $_`' file.txt

Para "previsualizar" lo que está sucediendo, simplemente reemplace las comillas con comillas dobles y anteponga print:

perl -p -e 'if(/\.txt$/) print "chmod 755 $_"' file.txt
1.618
fuente
2
¿Por qué usar backticks? Perl tiene una chmodfunción
glenn jackman
1
Usted querría perl -lpe 'chmod 0755, $_' file.txt- utilizar -lpara la función "auto-chomp"
Glenn Jackman
1

También puede usar AWK, que puede darle más flexibilidad para manejar el archivo

awk '{ print "chmod 755 "$0"" | "/bin/sh"}' file.txt

si su archivo tiene un separador de campo como:

campo1, campo2, campo3

Para obtener solo el primer campo que haces

awk -F, '{ print "chmod 755 "$1"" | "/bin/sh"}' file.txt

Puede consultar más detalles sobre la documentación de GNU https://www.gnu.org/software/gawk/manual/html_node/Very-Simple.html#Very-Simple

brunocrt
fuente
0

Sé que es tarde pero aún así

Si por casualidad se encuentra con un archivo de texto guardado de Windows en \r\nlugar de \n, puede confundirse con la salida si su comando tiene algo después de leer la línea como argumento. Entonces elimine \r, por ejemplo:

cat file | tr -d '\r' | xargs -L 1 -i echo do_sth_with_{}_as_line
EP
fuente