Befehlssubstitution: Aufteilen in Zeilenumbrüche, aber kein Leerzeichen

30

Ich weiß, dass ich dieses Problem auf mehrere Arten lösen kann, aber ich frage mich, ob es eine Möglichkeit gibt, dies nur mit Bash-Integrationen zu tun, und wenn nicht, welche Methode am effizientesten ist.

Ich habe eine Datei mit Inhalten wie

AAA
B C DDD
FOO BAR

womit ich nur meine, dass es mehrere Zeilen hat und jede Zeile Leerzeichen haben kann oder nicht. Ich möchte einen Befehl wie ausführen

cmd AAA "B C DDD" "FOO BAR"

Wenn ich benutze cmd $(< file)bekomme ich

cmd AAA B C DDD FOO BAR

und wenn ich benutze cmd "$(< file)"bekomme ich

cmd "AAA B C DDD FOO BAR"

Wie bekomme ich für jede Zeile genau einen Parameter?

Alter Pro
quelle

Antworten:

26

Tragbar:

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

Oder verwenden Sie eine Subshell, um die IFSund -Option lokal zu ändern:

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

Die Shell führt eine Feldaufteilung und eine Dateinamengenerierung für das Ergebnis einer Variablen- oder Befehlsersetzung durch, die nicht in doppelten Anführungszeichen steht. Sie müssen also die Dateinamengenerierung mit deaktivieren und die Feldaufteilung mit set -fkonfigurieren IFS, um nur Zeilenumbrüche als separate Felder zu verwenden.

Mit bash- oder ksh-Konstrukten lässt sich nicht viel erreichen. Sie können IFSeine Funktion lokalisieren, aber nicht set -f.

In bash oder ksh93 können Sie die Felder in einem Array speichern, wenn Sie sie an mehrere Befehle übergeben müssen. Sie müssen die Erweiterung zum Zeitpunkt der Erstellung des Arrays steuern. Dann "${a[@]}"dehnt sich auf die Elemente des Arrays, eine pro Wort.

set -f; IFS=$'\n'
a=($(cat <file))
set +f; unset IFS
cmd "${a[@]}"
Gilles 'SO - hör auf böse zu sein'
quelle
10

Sie können dies mit einem temporären Array tun.

Installieren:

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

Füllen Sie das Array aus:

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

Verwenden Sie das Array:

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

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

Ohne diese temporäre Variable IFSist dies nicht möglich - es sei denn, die Änderung ist für Folgendes nicht wichtig cmd:

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

Sollte es tun.

Matte
quelle
IFSbringt mich immer durcheinander. IFS=$'\n' cmd $(<input)funktioniert nicht IFS=$'\n'; cmd $(<input); unset IFSfunktioniert. Warum? Ich denke, ich werde verwenden(IFS=$'\n'; cmd $(<input))
Old Pro
6
@OldPro IFS=$'\n' cmd $(<input)funktioniert nicht, da es nur IFSin der Umgebung von festgelegt wird cmd. $(<input)wird erweitert, um den Befehl zu bilden, bevor die Zuordnung zu ausgeführt IFSwird.
Gilles 'SO- hör auf böse zu sein'
8

Es sieht so aus, als ob die kanonische Art, dies zu tun, in etwa so bashist

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

cmd "${args[@]}"

oder, wenn Ihre Version von Bash hat mapfile:

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

Der einzige Unterschied besteht zwischen dem Mapfile und der While-Read-Schleife und dem Einzeiler

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

ist, dass ersteres eine leere Zeile in ein leeres Argument umwandelt, während der Einzeiler eine leere Zeile ignoriert. In diesem Fall ist das Ein-Linien-Verhalten sowieso das, was ich vorziehen würde, also doppelter Bonus, wenn es kompakt ist.

Ich würde es verwenden, IFS=$'\n' cmd $(<file)aber es funktioniert nicht, da $(<file)es so interpretiert wird, dass es die Befehlszeile bildet, bevor es IFS=$'\n'wirksam wird.

Obwohl es in meinem Fall nicht funktioniert, habe ich jetzt erfahren, dass viele Tools das Beenden von Zeilen unterstützen, null (\000)anstatt newline (\n)dies beim Umgang mit beispielsweise Dateinamen, die häufige Ursachen für diese Situationen sind, zu vereinfachen :

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

Füttert md5 eine Liste vollqualifizierter Dateinamen als Argumente, ohne dass ein Globbing oder eine Interpolation erforderlich ist. Das führt zur nicht eingebauten Lösung

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

Auch dies ignoriert zwar leere Zeilen, erfasst jedoch Zeilen, die nur Leerzeichen enthalten.

Alter Pro
quelle
Die Verwendung von cmd $(<file)Werten ohne Anführungszeichen (Verwendung der Fähigkeit von Bash, Wörter zu trennen) ist immer eine riskante Wette. Wenn eine Zeile vorhanden ist *, wird sie von der Shell zu einer Liste von Dateien erweitert.
3

Sie können die integrierte Bash-Funktion verwenden mapfile, um die Datei in ein Array einzulesen

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

oder, ungetestet, xargskönnte es tun

xargs cmd < filename
Glenn Jackman
quelle
Aus der Mapfile-Dokumentation: "Mapfile ist keine verbreitete oder portable Shell-Funktion". Und in der Tat wird es auf meinem System nicht unterstützt. xargshilft auch nicht.
Old Pro
Sie müssten xargs -doderxargs -L
James Youngman
@James, nein, ich habe keine -dOption und führe xargs -L 1den Befehl einmal pro Zeile aus, teile aber immer noch Args in Leerzeichen auf.
Old Pro
1
@OldPro, nun, Sie haben nach einer Möglichkeit gefragt, "nur Bash-Built-Ins zu verwenden", anstatt nach "einer allgemeinen oder portablen Shell-Funktion". Wenn Ihre Version von Bash zu alt ist, können Sie es aktualisieren?
glenn Jackman
mapfileist sehr praktisch für mich, da es leere Zeilen als Array-Elemente erfasst, was die IFSMethode nicht tut. IFSBehandelt zusammenhängende Zeilenumbrüche als ein einzelnes Trennzeichen. Vielen Dank für die Darstellung, da mir der Befehl nicht bekannt war (auf der Grundlage der Eingabedaten des OP und der erwarteten Befehlszeile scheint er jedoch tatsächlich Leerzeilen ignorieren zu wollen).
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

Der beste Weg, den ich finden konnte. Funktioniert einfach.

Rahul Bali
quelle
Und warum funktioniert es, wenn Sie IFSauf Raum setzen, aber die Frage ist, nicht auf Raum zu teilen?
RalfFriedl
0

Datei

Die einfachste Schleife (portabel) zum Teilen einer Datei in Zeilenumbrüche ist:

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

Welches wird drucken:

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

Ja, mit Standard IFS = spacetabnewline.

Warum es funktioniert

  • IFS wird von der Shell verwendet, um die Eingabe in mehrere Variablen aufzuteilen . Da es nur eine Variable gibt, wird von der Shell keine Aufteilung durchgeführt. Also keine Änderung IFSnötig.
  • Ja, führende und nachfolgende Leerzeichen / Tabulatoren werden entfernt, aber dies scheint in diesem Fall kein Problem zu sein.
  • Nein, es wird kein Globbing durchgeführt, da keine Erweiterung nicht angegeben ist . Also nicht set -fnötig.
  • Das einzige verwendete (oder benötigte) Array sind die arrayartigen Positionsparameter.
  • Die -r(rohe) Option besteht darin, das Entfernen der meisten Backslashs zu vermeiden.

Dies funktioniert nicht , wenn eine Aufteilung und / oder Verschiebung erforderlich ist. In solchen Fällen ist eine komplexere Struktur erforderlich.

Wenn Sie (noch tragbar) benötigen, um:

  • Vermeiden Sie das Entfernen von führenden und nachfolgenden Leerzeichen / Tabulatoren. Verwenden Sie: IFS= read -r line
  • Split Linie zu Vars auf einige Zeichen, Verwendung: IFS=':' read -r a b c.

Teilen Sie die Datei auf ein anderes Zeichen auf (nicht portierbar, funktioniert mit ksh, bash, zsh):

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

Erweiterung

Im Titel Ihrer Frage geht es natürlich darum, eine Befehlsausführung in Zeilenumbrüche aufzuteilen und die Aufteilung in Leerzeichen zu vermeiden.

Die einzige Möglichkeit, die Shell zu spalten, besteht darin, eine Erweiterung ohne Anführungszeichen zu hinterlassen:

echo $(< file)

Dies wird durch den Wert von IFS gesteuert, und bei nicht zitierten Erweiterungen wird auch Globbing angewendet. Um diese Arbeit zu machen, brauchen Sie:

  • Set IFS neue Linie nur , um Splitting nur auf Newline zu bekommen.
  • Deaktivieren Sie die Globbing-Shell-Option set +f:

    setze + f IFS = '' cmd $ (<Datei)

Dies ändert natürlich den Wert von IFS und Globbing für den Rest des Skripts.

Isaac
quelle