Wie zähle ich die Anzahl der Zeilen in einer Zeichenfolgenvariablen POSIX-ly?

10

Ich weiß, dass ich das in Bash tun kann:

wc -l <<< "${string_variable}"

Grundsätzlich war alles, was ich fand, ein <<<Bash-Operator.

Aber in der POSIX-Shell <<<ist undefiniert, und ich konnte stundenlang keinen alternativen Ansatz finden. Ich bin mir ziemlich sicher, dass es eine einfache Lösung dafür gibt, aber leider habe ich sie bisher nicht gefunden.

LinuxSecurityFreak
quelle

Antworten:

11

Die einfache Antwort ist, dass wc -l <<< "${string_variable}"es sich um eine ksh / bash / zsh-Verknüpfung für handelt printf "%s\n" "${string_variable}" | wc -l.

Es gibt tatsächlich Unterschiede in der Art <<<und Weise und der Pipe-Arbeit: <<<Erstellt eine temporäre Datei, die als Eingabe an den Befehl übergeben wird, während |eine Pipe erstellt wird. In bash und pdksh / mksh (jedoch nicht in ksh93 oder zsh) wird der Befehl auf der rechten Seite der Pipe in einer Subshell ausgeführt. Aber diese Unterschiede spielen in diesem speziellen Fall keine Rolle.

Beachten Sie, dass beim Zählen von Zeilen davon ausgegangen wird, dass die Variable nicht leer ist und nicht mit einer neuen Zeile endet. Nicht mit einer neuen Zeile zu enden ist der Fall, wenn die Variable das Ergebnis einer Befehlsersetzung ist, sodass Sie in den meisten Fällen das richtige Ergebnis erhalten, aber 1 für die leere Zeichenfolge.

Es gibt zwei Unterschiede zwischen var=$(somecommand); wc -l <<<"$var"und somecommand | wc -l: Wenn Sie eine Befehlssubstitution verwenden und eine temporäre Variable am Ende Leerzeilen entfernt, wird vergessen, ob die letzte Ausgabezeile in einer neuen Zeile endete oder nicht (dies ist immer dann der Fall, wenn der Befehl eine gültige nicht leere Textdatei ausgibt). und überzählt um eins, wenn die Ausgabe leer ist. Wenn Sie sowohl das Ergebnis als auch die Anzahl der Zeilen beibehalten möchten, können Sie dies tun, indem Sie einen bekannten Text anhängen und ihn am Ende entfernen:

output=$(somecommand; echo .)
line_count=$(($(printf "%s\n" "$output" | wc -l) - 1))
printf "The exact output is:\n%s" "${output%.}"
Gilles 'SO - hör auf böse zu sein'
quelle
1
@Inian Keeping wc -lentspricht genau dem Original: <<<$fooFügt dem Wert von eine neue Zeile hinzu $foo(auch wenn diese $fooleer war). Ich erkläre in meiner Antwort, warum dies möglicherweise nicht das war, was gewünscht wurde, aber es wurde gefragt.
Gilles 'SO - hör auf böse zu sein'
2

Nicht konform mit integrierten Shell-Funktionen, Verwendung externer Dienstprogramme wie grepund awkmit POSIX-kompatiblen Optionen,

string_variable="one
two
three
four"

Damit mit grepdem Zeilenanfang übereinstimmen

printf '%s' "${string_variable}" | grep -c '^'
4

Und mit awk

printf '%s' "${string_variable}" | awk 'BEGIN { count=0 } NF { count++ } END { print count }'

Beachten Sie, dass einige der GNU-Tools, insbesondere GNU, grepdie POSIXLY_CORRECT=1Option zum Ausführen der POSIX-Version des Tools nicht berücksichtigen . In grepdem einzigen Verhalten durch Setzen der Variable beeinflusst wird der Unterschied bei der Verarbeitung der Reihenfolge der Befehlszeilen - Flags. Aus der Dokumentation (GNU- grepHandbuch) geht hervor, dass

POSIXLY_CORRECT

Wenn gesetzt, verhält sich grep so, wie es POSIX erfordert. Ansonsten grepverhält es sich eher wie andere GNU-Programme. POSIX erfordert, dass Optionen, die auf Dateinamen folgen, als Dateinamen behandelt werden. Standardmäßig werden solche Optionen an die Spitze der Operandenliste gesetzt und als Optionen behandelt.

Siehe Wie verwende ich POSIXLY_CORRECT in grep?

Inian
quelle
2
Ist wc -lhier sicher noch lebensfähig?
Michael Homer
@MichaelHomer: Nach dem, was ich beobachtet habe, wc -lbraucht es einen richtigen, durch Zeilenumbrüche getrennten Stream (mit einem abschließenden '\ n' am Ende, um richtig zu zählen). Man kann kein einfaches FIFO verwenden, um damit zu arbeiten printf, z. B. printf '%s' "${string_variable}" | wc -lkönnte es nicht wie erwartet funktionieren, <<<würde aber aufgrund des \nvom Herestring angehängten Nachlaufs
Inian
1
Das war es, was printf '%s\n'du getan hast, bevor du es herausgenommen hast ...
Michael Homer
1

Die Here-Zeichenfolge <<<ist so ziemlich eine einzeilige Version des Here-Dokuments <<. Ersteres ist keine Standardfunktion, letzteres jedoch. Sie können <<auch in diesem Fall verwenden. Diese sollten gleichwertig sein:

wc -l <<< "$somevar"

wc -l << EOF
$somevar
EOF

Beachten Sie jedoch, dass beide am Ende eine zusätzliche neue Zeile hinzufügen $somevar, z. B. diese wird gedruckt 6, obwohl die Variable nur fünf Zeilen enthält:

s=$'foo\n\n\nbar\n\n'
wc -l <<< "$s"

Mit printfkönnen Sie entscheiden, ob Sie den zusätzlichen Zeilenumbruch wünschen oder nicht:

printf "%s\n" "$s" | wc -l         # 6
printf "%s"   "$s" | wc -l         # 5

Beachten Sie jedoch, dass wcnur vollständige Zeilen (oder die Anzahl der Zeilenumbrüche in der Zeichenfolge) gezählt werden. grep -c ^sollte auch das letzte Zeilenfragment zählen.

s='foo'
printf "%s" "$s" | wc -l           # 0 !

printf "%s" "$s" | grep -c ^       # 1

(Natürlich können Sie die Zeilen auch vollständig in der Shell zählen, indem Sie sie mithilfe der ${var%...}Erweiterung einzeln in einer Schleife entfernen ...)

ilkkachu
quelle
0

In den überraschend häufigen Fällen, in denen Sie tatsächlich alle nicht leeren Zeilen innerhalb einer Variablen auf irgendeine Weise verarbeiten müssen (einschließlich deren Zählung), können Sie IFS auf eine neue Zeile setzen und dann den Wortaufteilungsmechanismus der Shell verwenden, um zu brechen die nicht leeren Zeilen auseinander.

Hier ist zum Beispiel eine kleine Shell-Funktion, die die nicht leeren Zeilen in allen angegebenen Argumenten summiert:

lines() (
IFS='
'
set -f #disable pathname expansion
set -- $*
echo $#
)

Hier werden Klammern anstelle von geschweiften Klammern verwendet, um den zusammengesetzten Befehl für den Funktionskörper zu bilden. Dadurch wird die Funktion in einer Subshell ausgeführt, sodass die IFS-Variablen- und Pfadnamen-Erweiterungseinstellung der Außenwelt nicht bei jedem Aufruf verschmutzt wird.

Wenn Sie über nicht leere Zeilen iterieren möchten, können Sie dies auf ähnliche Weise tun:

IFS='
'
set -f
for line in $lines
do
    printf '[%s]\n' $line
done

Das Manipulieren von IFS auf diese Weise ist eine häufig übersehene Technik, die sich auch zum Parsen von Pfadnamen eignet, die Leerzeichen aus tabulatorgetrennten Spalteneingaben enthalten können. Sie müssen sich jedoch darüber im Klaren sein, dass das absichtliche Entfernen des Leerzeichens, das normalerweise in der IFS-Standardeinstellung für Leerzeichen-Tabulator-Zeilenumbruch enthalten ist, dazu führen kann, dass die Wortaufteilung an Stellen deaktiviert wird, an denen Sie sie normalerweise erwarten würden.

Wenn Sie beispielsweise Variablen verwenden, um eine komplizierte Befehlszeile für etwas wie zu erstellen ffmpeg, möchten Sie diese möglicherweise -vf scale=$scalenur einschließen , wenn die Variable scaleauf etwas nicht Leeres festgelegt ist. Normalerweise mit Ihnen könnte dies zu erreichen , ${scale:+-vf scale=$scale}aber wenn IFS nicht seinen üblichen Raumzeichen zum Zeitpunkt dieser Parameter Expansion erfolgt sind, zwischen der Raum -vfund scale=wird nicht als Worttrennzeichen verwendet werden und ffmpegwird alle weitergegeben werden -vf scale=$scaleals ein einziges Argument, was es nicht verstehen wird.

Um dies zu beheben, müssen Sie entweder sicherstellen, dass IFS normaler eingestellt wurde, bevor Sie die ${scale}Erweiterung durchführen, oder zwei Erweiterungen durchführen : ${scale:+-vf} ${scale:+scale=$scale}. Die Wortaufteilung, die die Shell beim anfänglichen Parsen von Befehlszeilen durchführt, im Gegensatz zur Aufteilung während der Erweiterungsphase der Verarbeitung dieser Befehlszeilen, hängt nicht von IFS ab.

Etwas anderes, das sich lohnen könnte, wenn Sie so etwas tun, wäre, zwei globale Shell-Variablen zu erstellen, die nur einen Tabulator und nur eine neue Zeile enthalten:

t=' '
n='
'

Auf diese Weise können Sie einfach schließen $tund $nin Erweiterungen , wo Sie Tabs und Zeilenumbrüche müssen, anstatt Littering den gesamten Code mit zitierte Leerzeichen. Wenn Sie in einer POSIX-Shell, für die es keinen anderen Mechanismus gibt, lieber Leerzeichen in Anführungszeichen vermeiden printfmöchten , kann dies hilfreich sein, obwohl Sie ein wenig herumfummeln müssen, um das Entfernen von nachgestellten Zeilenumbrüchen in Befehlserweiterungen zu umgehen:

nt=$(printf '\n\t')
n=${nt%?}
t=${nt#?}

Manchmal funktioniert es gut, IFS so einzustellen, als wäre es eine Umgebungsvariable pro Befehl. Hier ist beispielsweise eine Schleife, die einen Pfadnamen liest, der Leerzeichen und einen Skalierungsfaktor aus jeder Zeile einer durch Tabulatoren getrennten Eingabedatei enthalten darf:

while IFS=$t read -r path scale
do
    ffmpeg -i "$path" ${scale:+-vf scale=$scale} "${path%.*}.out.mkv"
done <recode-queue.txt

In diesem Fall wird readIFS nur auf eine Registerkarte gesetzt, sodass die Eingabezeile, die es liest, nicht auch auf Leerzeichen aufgeteilt wird. Funktioniert IFS=$t set -- $lines jedoch nicht : Die Shell wird erweitert $lines, setwenn die Argumente des eingebauten Geräts vor dem Ausführen des Befehls erstellt werden. Daher kommt die temporäre Einstellung von IFS auf eine Weise, die nur während der Ausführung des integrierten Geräts selbst gilt, zu spät. Aus diesem Grund setzen die Code-Schnipsel, die ich vor allem gegeben habe, IFS in einem separaten Schritt und müssen sich mit dem Problem der Aufbewahrung befassen.

flabdablet
quelle