Comment séparer la sortie d'une commande sur des lignes individuelles

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

Dans un script shell, cette commande echo listera tous les fichiers du répertoire courant commençant par R, mais sur une seule ligne. Comment imprimer chaque article sur une seule ligne?

J'ai besoin d' une commande générique pour tous les scénarios qui se passe avec ls, du, find -type -d, etc.

Anony
la source
6
Remarque générale: ne faites pas cela avec ls, cela cassera tous les noms de fichiers étranges . Voyez ici .
terdon

Réponses:

12

Si la sortie de la commande contient plusieurs lignes, citez vos variables pour conserver ces nouvelles lignes lors de l'écho:

echo "$list"

Sinon, le shell se développera et divisera le contenu des variables sur les espaces (espaces, nouvelles lignes, tabulations) et ceux-ci seront perdus.

muru
la source
15

Au lieu de placer à mauvais escient la lssortie dans une variable, puis de l' echointégrer, ce qui supprime toutes les couleurs, utilisez

ls -a1

De man ls

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

Je ne vous conseille pas de faire quoi que ce soit avec la sortie de ls, sauf l'afficher :)

Utilisez, par exemple, des globes shell et une forboucle pour faire quelque chose avec des fichiers ...

shopt -s dotglob                  # to include hidden files*

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

* Cela ne comprend pas le répertoire courant .ou son parent ..si

Zanna
la source
1
Je recommanderais de remplacer: echo "$i"par printf "%s\n" "$i" (que l'on ne s'étouffera pas si un nom de fichier commence par un "-"). En fait, la plupart du temps echo somethingdevrait être remplacé par printf "%s\n" "something".
Olivier Dulac
2
@OlivierDulac Ils commencent tous par Rici, mais en général, oui. Pourtant, même pour les scripts, essayer de toujours prendre en compte les -chemins de début entraîne des problèmes: les gens supposent --que seules certaines commandes prennent en charge signifient la fin des options. J'ai vu find . [tests] -exec [cmd] -- {} \;[cmd]ne supporte pas --. Chaque chemin commence de .toute façon! Mais ce n'est pas un argument contre le remplacement echopar printf '%s\n'. Ici, on peut remplacer la boucle entière par printf '%s\n' R/*. Bash a printfune fonction intégrée, donc il n'y a effectivement aucune limite au nombre / longueur d'arguments.
Eliah Kagan
12

Tout en le mettant entre guillemets comme @muru l'a suggéré fera en effet ce que vous avez demandé, vous pouvez également envisager d'utiliser un tableau pour cela. Par exemple:

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

Le IFS=$'\n'dit à bash de ne diviser la sortie que sur les caractères de nouvelle ligne pour obtenir chaque élément du tableau. Sans cela, il se divisera en espaces, il y a file name with spaces.txtaurait donc 5 éléments distincts au lieu d'un. Cette approche ne fonctionnera pas si les noms de vos fichiers / répertoires peuvent contenir des sauts de ligne ( \n). Il enregistrera chaque ligne de la sortie de la commande en tant qu'élément du tableau.

Notez que j'ai aussi changé le style ancien `command`à $(command)qui est la syntaxe préférée.

Vous avez maintenant un tableau appelé $dirs, chaque bof dont les éléments est une ligne de la sortie de la commande précédente. Par exemple:

$ 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

Maintenant, en raison d'une certaine étrangeté bash (voir ici ), vous devrez réinitialiser IFSla valeur d'origine après avoir fait cela. Donc, soit enregistrez-le et reconcevoir:

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

Ou faites-le manuellement:

IFS=" "$'\t\n '

Sinon, fermez simplement le terminal actuel. Votre nouveau aura à nouveau l'IFS d'origine.

terdon
la source
J'aurais peut-être suggéré quelque chose comme ça, mais la partie duqui donne l'impression que OP est plus intéressée par la préservation du format de sortie.
muru
Comment puis-je faire fonctionner cela si les noms de répertoire contiennent des espaces?
dessert
@dessert whoops! Cela devrait fonctionner avec des espaces (mais pas des sauts de ligne) maintenant. Merci de l'avoir signalé.
terdon
1
Notez que vous devez réinitialiser ou désactiver IFS après cette affectation de tableau. unix.stackexchange.com/q/264635/70524
muru
@muru oh wow, merci, j'avais oublié cette bizarrerie.
terdon
2

Si vous devez stocker la sortie et que vous voulez un tableau, mapfilec'est facile.

Tout d'abord, déterminez si vous avez besoin de stocker la sortie de votre commande. Si vous ne le faites pas, exécutez simplement la commande.

Si vous décidez que vous souhaitez lire la sortie d'une commande sous forme de tableau de lignes, il est vrai qu'une façon de le faire consiste à désactiver la globalisation, à définir la IFSrépartition sur les lignes, à utiliser la substitution de commandes à l'intérieur de la ( )syntaxe de création du tableau et à la réinitialiser IFSpar la suite, ce qui est plus complexe qu'il n'y paraît. La réponse de terdon couvre une partie de cette approche. Mais je vous suggère d'utiliser à mapfilela place la commande intégrée du shell Bash pour lire le texte sous forme de tableau de lignes. Voici un cas simple où vous lisez un fichier:

mapfile < filename

Cela lit les lignes dans un tableau appelé MAPFILE. Sans la redirection d'entrée , vous liriez à partir de l' entrée standard du shell (généralement votre terminal) au lieu du fichier nommé par . Pour lire dans un tableau autre que celui par défaut , passez son nom. Par exemple, cela lit dans le tableau :< filenamefilenameMAPFILElines

mapfile lines < filename

Un autre comportement par défaut que vous pourriez choisir de changer est que les caractères de nouvelle ligne à la fin des lignes sont laissés en place; ils apparaissent comme le dernier caractère de chaque élément du tableau (sauf si l'entrée s'est terminée sans caractère de nouvelle ligne, auquel cas le dernier élément n'en a pas). Pour couper ces nouvelles lignes afin qu'elles n'apparaissent pas dans le tableau, passez l' -toption à mapfile. Par exemple, cela lit le tableau recordset n'écrit pas de caractères de fin de ligne dans ses éléments de tableau:

mapfile -t records < filename

Vous pouvez également utiliser -tsans passer de nom de tableau; c'est-à-dire qu'il fonctionne aussi avec un nom de tableau implicite de MAPFILE.

Le mapfileshell intégré prend en charge d'autres options, et il peut également être appelé en tant que readarray. Exécutez help mapfile(et help readarray) pour plus de détails.

Mais vous ne voulez pas lire à partir d'un fichier, vous voulez lire à partir de la sortie d'une commande. Pour ce faire, utilisez la substitution de processus . Cette commande lit les lignes de la commande some-commandavec arguments...comme arguments de ligne de commande et les place dans le mapfiletableau par défaut de MAPFILE, avec leurs caractères de fin de ligne supprimés:

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

La substitution de processus remplace par un nom de fichier réel à partir duquel la sortie de l'exécution peut être lue. Le fichier est un canal nommé plutôt qu'un fichier normal et, sur Ubuntu, il sera nommé comme (parfois avec un autre numéro que ), mais vous n'avez pas besoin de vous préoccuper des détails, car le shell s'en charge tous dans les coulisses.<(some-command arguments...)some-command arguments.../dev/fd/6363

Vous pourriez penser que vous pourriez renoncer à la substitution de processus en utilisant à la place, mais cela ne fonctionnera pas car, lorsque vous avez un pipeline de plusieurs commandes séparées par , Bash s'exécute exécute toutes les commandes en sous-coquilles . Ainsi , les deux et courir dans leurs propres environnements , initialisées à partir mais distincte de l'environnement de la coquille dans laquelle vous exécutez le pipeline. Dans le sous-shell où s'exécute, le tableau est rempli, mais ce tableau est rejeté à la fin de la commande. n'est jamais créé ou modifié pour l'appelant.some-command arguments... | mapfile -t|some-command arguments...mapfile -tmapfile -tMAPFILEMAPFILE

Voici à quoi ressemble l'exemple de la réponse de terdon , si vous utilisez mapfile:

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

C'est ça. Vous n'avez pas besoin de vérifier si elle a IFSété définie , de savoir si elle a été définie et avec quelle valeur, de la définir sur une nouvelle ligne, puis de la réinitialiser ou de la réinitialiser plus tard. Vous ne devez pas désactiver globbing (par exemple, set -f) - ce qui est vraiment nécessaire si vous voulez utiliser cette méthode au sérieux, puisque les noms de fichiers peuvent contenir *, ?et [--then réactiviez ( set +f) par la suite.

Vous pouvez également remplacer cette boucle particulière entièrement par une seule printfcommande - bien que ce ne soit pas réellement un avantage mapfile, car vous pouvez le faire que vous utilisiez mapfileune autre méthode pour remplir le tableau. Voici une version plus courte:

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

Il est important de garder à l'esprit que, comme le mentionne Terdon , l'exploitation ligne par ligne n'est pas toujours appropriée et ne fonctionnera pas correctement avec les noms de fichiers contenant des sauts de ligne. Je déconseille de nommer les fichiers de cette façon, mais cela peut arriver, y compris par accident.

Il n'y a pas vraiment de solution unique.

Vous avez demandé «une commande générique pour tous les scénarios» et l'approche consistant à utiliser mapfilequelque peu l'approche de cet objectif, mais je vous invite à reconsidérer vos besoins.

La tâche illustrée ci-dessus est mieux réalisée avec une seule findcommande:

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

Vous pouvez également utiliser une commande externe comme sedajouter DIR: au début de chaque ligne. C'est sans doute quelque peu moche, et contrairement à cette commande find, elle ajoutera des «préfixes» supplémentaires dans les noms de fichiers qui contiennent des nouvelles lignes, mais cela fonctionne indépendamment de son entrée, donc il répond en quelque sorte à vos besoins et il est toujours préférable de lire la sortie dans un variable ou tableau :

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

Si vous devez répertorier et également effectuer une action sur chaque répertoire trouvé, telle que l'exécution some-commandet la transmission du chemin du répertoire en tant qu'argument, findvous pouvez également le faire:

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

Comme autre exemple, revenons à la tâche générale d'ajouter un préfixe à chaque ligne. Supposons que je veuille voir la sortie de help mapfilemais numéroter les lignes. Je n'utiliserais pas réellement mapfilepour cela, ni aucune autre méthode qui le lit dans une variable shell ou un tableau shell. Supposons help mapfile | cat -nque je ne donne pas la mise en forme que je veux, je peux utiliser awk:

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

La lecture de toutes les sorties d'une commande dans une seule variable ou un seul tableau est parfois utile et appropriée, mais elle présente des inconvénients majeurs. Non seulement vous devez faire face à la complexité supplémentaire de l'utilisation de votre shell lorsqu'une commande existante ou une combinaison de commandes existantes peut déjà faire ce dont vous avez besoin ou mieux, mais la sortie entière de la commande doit être stockée en mémoire. Parfois, vous savez peut-être que ce n'est pas un problème, mais parfois vous traitez un fichier volumineux.

Une alternative qui est couramment tentée - et parfois bien faite - consiste à lire l'entrée ligne par ligne read -rdans une boucle. Si vous n'avez pas besoin de stocker les lignes précédentes lorsque vous travaillez sur des lignes ultérieures et que vous devez utiliser une entrée longue, cela peut être mieux que mapfile. Mais cela devrait également être évité dans les cas où vous pouvez simplement le rediriger vers une commande qui peut faire le travail, ce qui est la plupart des cas .

Eliah Kagan
la source