Comment exécuter une commande pour chaque ligne d'un fichier?

161

Par exemple, en ce moment, j'utilise ce qui suit pour modifier quelques fichiers dont j'ai écrit les chemins Unix dans un fichier:

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

Existe-t-il un moyen plus élégant et plus sûr?

faucon
la source

Réponses:

127

Lire un fichier ligne par ligne et exécuter des commandes: 4 réponses

C'est parce qu'il n'y a pas qu'une seule réponse ...

  1. shell expansion de la ligne de commande
  2. xargs outil dédié
  3. while read avec quelques remarques
  4. while read -uutilisation dédiée fd, pour traitement interactif (exemple)

Concernant la requête OP: s'exécutant chmodsur toutes les cibles listées dans le fichier , xargsest l'outil indiqué. Mais pour certaines autres applications, petite quantité de fichiers, etc ...

  1. Lire le fichier entier comme argument de ligne de commande.

    Si votre fichier n'est pas trop volumineux et que tous les fichiers sont bien nommés (sans espaces ou autres caractères spéciaux comme les guillemets), vous pouvez utiliser l' shellextension de ligne de commande . Simplement:

    chmod 755 $(<file.txt)

    Pour une petite quantité de fichiers (lignes), cette commande est la plus légère.

  2. xargs est le bon outil

    Pour une plus grande quantité de fichiers, ou presque n'importe quel nombre de lignes dans votre fichier d'entrée ...

    Pour de nombreux binutils outils, comme chown, chmod, rm, cp -t...

    xargs chmod 755 <file.txt

    Si vous avez des caractères spéciaux et / ou beaucoup de lignes dans file.txt.

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

    si votre commande doit être exécutée exactement 1 fois par entrée:

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

    Ce n'est pas nécessaire pour cet exemple, car chmodaccepter plusieurs fichiers comme argument, mais cela correspond au titre de la question.

    Dans certains cas particuliers, vous pouvez même définir l'emplacement de l'argument de fichier dans les commandes générées par xargs:

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

    Test avec seq 1 5comme entrée

    Essaye ça:

    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..

    Où la commande est effectuée une fois par ligne .

  3. while read et variantes.

    Comme OP le suggère, cat file.txt | while read in; do chmod 755 "$in"; donecela fonctionnera, mais il y a 2 problèmes:

    • cat |est une fourchette inutile , et

    • | while ... ;donedeviendra un sous - shell où l'environnement disparaîtra après ;done.

    Donc, cela pourrait être mieux écrit:

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

    Mais,

    • Vous pouvez être averti $IFSet readsignale:

      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...

      Dans certains cas, vous devrez peut-être utiliser

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

      Pour éviter les problèmes avec les noms de fichiers étranges. Et peut-être si vous rencontrez des problèmes avec UTF-8:

      while LANG=C IFS= read -r in ; do chmod 755 "$in";done <file.txt
    • Pendant que vous l'utilisez STDINpour la lecture file.txt, votre script ne peut pas être interactif (vous ne pouvez plus l'utiliser STDIN).

  4. while read -u, en utilisant dédié fd.

    Syntaxe: while read ...;done <file.txtredirigera STDINvers file.txt. Cela signifie que vous ne pourrez pas gérer le processus tant qu'il ne sera pas terminé.

    Si vous envisagez de créer un outil interactif , vous devez éviter d'utiliser STDINet utiliser un autre descripteur de fichier .

    Les descripteurs de fichiers de constantes sont: 0pour STDIN , 1pour STDOUT et 2pour STDERR . Vous pouvez les voir par:

    ls -l /dev/fd/

    ou

    ls -l /proc/self/fd/

    À partir de là, vous devez choisir le nombre inutilisé, entre 0et 63(plus, en fait, selon l' sysctloutil du superutilisateur) comme descripteur de fichier :

    Pour cette démo, j'utiliserai fd 7 :

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

    Ensuite, vous pouvez utiliser read -u 7cette méthode:

    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

    Pour fermer fd/7:

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

    Nota: Je laisse la version barrée car cette syntaxe pourrait être utile, lors de nombreuses E / S avec processus parallèles:

    mkfifo sshfifo
    exec 7> >(ssh -t user@host sh >sshfifo)
    exec 6<sshfifo
F. Hauri
la source
3
Comme xargsc'était le cas à l'origine pour répondre à ce type de besoin, certaines fonctionnalités, comme la commande de construction le plus longtemps possible dans l'environnement actuel pour invoquer chmodle moins possible dans ce cas, la réduction des fourches garantit l'efficacité. while ;do..done <$fileimplique d'exécuter 1 fork pour 1 fichier. xargspourrait exécuter 1 fork pour mille fichiers ... de manière fiable.
F.Hauri
1
pourquoi la troisième commande ne fonctionne pas dans un makefile? J'obtiens une "erreur de syntaxe près du jeton inattendu` <'", mais l'exécution directe à partir de la ligne de commande fonctionne.
Woodrow Barlow
2
Cela semble lié à la syntaxe spécifique de Makefile. Vous pouvez essayer d'inverser la ligne de commande:cat file.txt | tr \\n \\0 | xargs -0 -n1 chmod 755
F.Hauri
@ F.Hauri pour une raison quelconque, tr \\n \\0 <file.txt |xargs -0 [command]est environ 50% plus rapide que la méthode que vous avez décrite.
phil294
Octobre 2019, nouvelle modification, ajout d' un exemple de processeur de fichier interactif .
F.Hauri
150

Oui.

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

De cette façon, vous pouvez éviter un catprocessus.

catest presque toujours mauvais pour un objectif comme celui-ci. Vous pouvez en savoir plus sur l' utilisation inutile de Cat.

PP
la source
Éviter un cat est une bonne idée, mais dans ce cas, la commande indiquée estxargs
F.Hauri
Ce lien ne semble pas pertinent, peut-être que le contenu de la page Web a changé? Le reste de la réponse est génial :)
starbeamrainbowlabs
@starbeamrainbowlabs Oui. Il semble que la page a été déplacée. Je me suis ré-connecté et ça devrait aller maintenant. Merci :)
PP
1
Merci! Cela a été utile, surtout lorsque vous avez besoin de faire autre chose que d'appeler chmod(c'est-à-dire d'exécuter réellement une commande pour chaque ligne du fichier).
Per Lundberg
soyez prudent avec les contre-obliques! de unix.stackexchange.com/a/7561/28160 - " read -rlit une seule ligne à partir de l'entrée standard ( readsans -rinterpréter les contre-obliques, vous ne le voulez pas)."
That Brazilian Guy
16

si vous avez un bon sélecteur (par exemple tous les fichiers .txt dans un répertoire), vous pouvez faire:

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

bash pour la boucle

ou une variante de la vôtre:

while read line; do chmod 755 "$line"; done <file.txt
d.raev
la source
Ce qui ne fonctionne pas, c'est que s'il y a des espaces dans la ligne, l'entrée est divisée par des espaces et non par une ligne.
Michael Fox
@Michael Fox: Les lignes avec des espaces peuvent être prises en charge en modifiant le séparateur. Pour le changer en nouvelles lignes, définissez la variable d'environnement 'IFS' avant le script / la commande. Ex: export IFS = '$ \ n'
codesniffer
Typo dans mon dernier commentaire. Devrait être: export IFS = $ '\ n'
codesniffer
14

Si vous savez que vous n'avez aucun espace dans l'entrée:

xargs chmod 755 < file.txt

S'il peut y avoir des espaces dans les chemins, et si vous avez des xargs GNU:

tr '\n' '\0' < file.txt | xargs -0 chmod 755
Glenn Jackman
la source
Je connais xargs, mais (malheureusement) cela semble moins une solution fiable que les fonctionnalités intégrées de bash comme while et read. De plus, je n'ai pas GNU xargs, mais j'utilise OS X et xargs a également une option -0 ici. Merci d'avoir répondu.
hawk
1
@hawk No: xargsest robuste. Cet outil est très ancien et son code est fortement revisité . Son objectif était initialement de construire des lignes en respectant les limitations du shell (64kchar / ligne ou quelque chose comme ça). Maintenant, cet outil pourrait fonctionner avec de très gros fichiers et réduire considérablement le nombre de fork à la commande finale. Voir ma réponse et / ou man xargs.
F.Hauri
@hawk Moins d'une solution fiable de quelle manière? Si cela fonctionne sous Linux, Mac / BSD et Windows (oui, les bundles GNU xargs de MSYSGIT), alors il est aussi fiable que possible.
Camilo Martin
1
Pour ceux qui arrivent encore à trouver cela à partir des résultats de recherche ... vous pouvez installer GNU xargs sur macOS en utilisant Homebrew ( brew install findutils), puis invoquer GNU xargs avec à la gxargsplace, par exemplegxargs chmod 755 < file.txt
Jase
13

Si vous souhaitez exécuter votre commande en parallèle pour chaque ligne, vous pouvez utiliser GNU Parallel

parallel -a <your file> <program>

Chaque ligne de votre fichier sera passée au programme en tant qu'argument. Par défaut, parallelexécute autant de threads que vos processeurs comptent. Mais vous pouvez le spécifier avec-j

Janisz
la source
3

Je vois que vous avez tagué bash, mais Perl serait également un bon moyen de le faire:

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

Vous pouvez également appliquer une expression régulière pour vous assurer que vous obtenez les bons fichiers, par exemple pour ne traiter que les fichiers .txt:

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

Pour "prévisualiser" ce qui se passe, remplacez simplement les contre-crochets par des guillemets doubles et ajoutez print:

perl -p -e 'if(/\.txt$/) print "chmod 755 $_"' file.txt
1,618
la source
2
Pourquoi utiliser des backticks? Perl a une chmodfonction
glenn jackman
1
Vous voudriez perl -lpe 'chmod 0755, $_' file.txt- utiliser -lpour la fonction "auto-chomp"
glenn jackman
1

Vous pouvez également utiliser AWK qui peut vous donner plus de flexibilité pour gérer le fichier

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

si votre fichier a un séparateur de champ comme:

champ1, champ2, champ3

Pour n'obtenir que le premier champ que vous faites

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

Vous pouvez consulter plus de détails sur la documentation GNU https://www.gnu.org/software/gawk/manual/html_node/Very-Simple.html#Very-Simple

Brunocrt
la source
0

La logique s'applique à de nombreux autres objectifs. Et comment lire .sh_history de chaque utilisateur depuis / home / filesystem? Et s'il y en avait des milliers?

#!/bin/ksh
last |head -10|awk '{print $1}'|
 while IFS= read -r line
 do
su - "$line" -c 'tail .sh_history'
 done

Voici le script https://github.com/imvieira/SysAdmin_DevOps_Scripts/blob/master/get_and_run.sh

BladeMight
la source
0

Je sais qu'il est tard mais quand même

Si, par hasard, vous rencontrez un fichier texte Windows enregistré avec \r\nau lieu de \n, vous pourriez être confus par la sortie si votre commande a sth après la ligne de lecture comme argument. Alors supprimez \r, par exemple:

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