Substitution de commandes: division sur la nouvelle ligne mais pas sur l'espace


30

Je sais que je peux résoudre ce problème de plusieurs manières, mais je me demande s'il existe un moyen de le faire en utilisant uniquement les bash intégrés, et sinon, quel est le moyen le plus efficace de le faire.

J'ai un fichier avec un contenu comme

AAA
B C DDD
FOO BAR

par quoi je veux seulement dire qu'il a plusieurs lignes et que chaque ligne peut ou non avoir des espaces. Je veux exécuter une commande comme

cmd AAA "B C DDD" "FOO BAR"

Si j'utilise cmd $(< file)je reçois

cmd AAA B C DDD FOO BAR

et si j'utilise cmd "$(< file)"je reçois

cmd "AAA B C DDD FOO BAR"

Comment obtenir que chaque ligne soit traitée avec exactement un paramètre?


Réponses:


26

Portablement:

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

Ou en utilisant un sous-shell pour rendre les IFSmodifications de l'option et locales:

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

Le shell effectue le fractionnement de champ et la génération de nom de fichier sur le résultat d'une substitution de variable ou de commande qui n'est pas entre guillemets. Vous devez donc désactiver la génération de nom de fichier avec set -f, et configurer la division des champs avec IFSpour ne faire que des sauts de ligne séparés.

Il n'y a pas grand-chose à gagner avec les constructions bash ou ksh. Vous pouvez rendre IFSlocal une fonction, mais pas set -f.

Dans bash ou ksh93, vous pouvez stocker les champs dans un tableau, si vous devez les passer à plusieurs commandes. Vous devez contrôler l'expansion au moment de la création de la baie. "${a[@]}"S'étend ensuite aux éléments du tableau, un par mot.

set -f; IFS=$'\n'
a=($(cat <file))
set +f; unset IFS
cmd "${a[@]}"

10

Vous pouvez le faire avec un tableau temporaire.

Installer:

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

Remplissez le tableau:

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

Utilisez le tableau:

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

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

Impossible de trouver un moyen de le faire sans cette variable temporaire - sauf si le IFSchangement n'est pas important pour cmd, dans ce cas:

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

devrait le faire.


IFSme rend toujours confus. IFS=$'\n' cmd $(<input)ne fonctionne pas. IFS=$'\n'; cmd $(<input); unset IFSfonctionne. Pourquoi? Je suppose que je vais utiliser(IFS=$'\n'; cmd $(<input))
Old Pro

6
@OldPro IFS=$'\n' cmd $(<input)ne fonctionne pas car il ne se définit que IFSdans l'environnement de cmd. $(<input)est développé pour former la commande, avant que l'affectation à ne IFSsoit effectuée.
Gilles 'SO- arrête d'être méchant'

8

On dirait que la manière canonique de le faire bashest quelque chose comme

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

cmd "${args[@]}"

ou, si votre version de bash a mapfile:

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

La seule différence que je peux trouver entre le mapfile et la boucle en lecture par rapport au one-liner

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

est que le premier convertira une ligne vide en un argument vide, tandis que le one-liner ignorera une ligne vide. Dans ce cas, le comportement à une ligne est ce que je préférerais de toute façon, donc double bonus sur le fait qu'il soit compact.

J'utiliserais IFS=$'\n' cmd $(<file)mais cela ne fonctionne pas, car $(<file)est interprété pour former la ligne de commande avant de IFS=$'\n'prendre effet.

Bien que cela ne fonctionne pas dans mon cas, j'ai maintenant appris que de nombreux outils prennent en charge les terminaisons de lignes avec lesquelles, au null (\000)lieu de newline (\n)cela, cela facilite grandement la gestion, par exemple, des noms de fichiers, qui sont des sources courantes de ces situations. :

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

alimente une liste de noms de fichiers pleinement qualifiés en tant qu'arguments à md5 sans globalisation ou interpolation ou quoi que ce soit. Cela conduit à la solution non intégrée

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

bien que cela, aussi, ignore les lignes vides, bien qu'il capture les lignes qui n'ont que des espaces.


Utiliser des cmd $(<file)valeurs sans citer (en utilisant la capacité de bash à diviser les mots) est toujours un pari risqué. Si une ligne existe, *elle sera étendue par le shell à une liste de fichiers.

3

Vous pouvez utiliser le bash intégré mapfilepour lire le fichier dans un tableau

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

ou, non testé, xargspourrait le faire

xargs cmd < filename

De la documentation de mapfile: "mapfile n'est pas une fonction shell commune ou portable". Et en effet, il n'est pas pris en charge sur mon système. xargsn'aide pas non plus.
Old Pro

Vous auriez besoin xargs -douxargs -L
James Youngman

@James, non, je n'ai pas d' -doption et xargs -L 1exécute la commande une fois par ligne, mais divise toujours les arguments sur les espaces.
Old Pro

1
@OldPro, vous avez bien demandé "un moyen de le faire en utilisant uniquement les bash intégrés" au lieu de "une fonction shell commune ou portable". Si votre version de bash est trop ancienne, pouvez-vous la mettre à jour?
glenn jackman

mapfileest très pratique pour moi, car il saisit les lignes vides en tant qu'éléments de tableau, ce que la IFSméthode ne fait pas. IFStraite les nouvelles lignes contiguës comme un seul délimiteur ... Merci de la présenter, car je n'étais pas au courant de la commande (bien que, sur la base des données d'entrée de l'OP et de la ligne de commande attendue, il semble qu'il veuille réellement ignorer les lignes vides).
Peter.O

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

Le meilleur moyen que j'ai pu trouver. Fonctionne juste.


Et pourquoi cela fonctionne-t-il si vous définissez l' IFSespace, mais la question est de ne pas diviser l'espace?
RalfFriedl

0

Fichier

La boucle la plus basique (portable) pour diviser un fichier sur des sauts de ligne est:

#!/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.

Qui imprimera:

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

Oui, avec IFS par défaut = spacetabnewline.

Pourquoi ça marche

  • IFS sera utilisé par le shell pour diviser l'entrée en plusieurs variables. Comme il n'y a qu'une seule variable, aucun fractionnement n'est effectué par le shell. Donc, aucun changement de IFSnécessaire.
  • Oui, les espaces / tabulations de début et de fin sont supprimés, mais cela ne semble pas être un problème dans ce cas.
  • Non, aucun globbing n'est effectué car aucune expansion n'est non citée . Donc, pas set -fbesoin.
  • Le seul tableau utilisé (ou nécessaire) est les paramètres positionnels de type tableau.
  • L' -roption (brute) consiste à éviter la suppression de la plupart des barres obliques inverses.

Cela ne fonctionnera pas si le fractionnement et / ou la globalisation sont nécessaires. Dans de tels cas, une structure plus complexe est nécessaire.

Si vous avez besoin (toujours portable) de:

  • Évitez de supprimer les espaces / tabulations de début et de fin, utilisez: IFS= read -r line
  • Ligne de séparation à vars sur un certain caractère, utilisez: IFS=':' read -r a b c.

Divisez le fichier sur un autre caractère (non portable, fonctionne avec ksh, bash, zsh):

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

Expansion

Bien sûr, le titre de votre question concerne le fractionnement d'une exécution de commande sur les sauts de ligne en évitant le fractionnement sur les espaces.

La seule façon d'obtenir le fractionnement du shell est de laisser une expansion sans guillemets:

echo $(< file)

Cela est contrôlé par la valeur de l'IFS et, sur les extensions non cotées, le globbing est également appliqué. Pour effectuer ce travail, vous avez besoin de:

  • Définissez IFS sur la nouvelle ligne uniquement , pour obtenir le fractionnement sur la nouvelle ligne uniquement.
  • Désactivez l'option shell globbing set +f:

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

Bien sûr, cela change la valeur d'IFS et de globbing pour le reste du script.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.