Anzahl der Zeichen in der Ausgabe eines Shell-Befehls

12

Ich schreibe ein Skript, das die Anzahl der Zeichen in der Ausgabe eines Befehls in einem einzigen Schritt berechnen muss .

Beispielsweise sollte die Verwendung des Befehls zurückgegeben readlink -f /etc/fstabwerden, 10da die Ausgabe dieses Befehls 10 Zeichen lang ist.

Dies ist bereits mit gespeicherten Variablen mit folgendem Code möglich:

variable="somestring";
echo ${#variable};
# 10

Leider funktioniert die Verwendung derselben Formel mit einer vom Befehl generierten Zeichenfolge nicht:

${#(readlink -f /etc/fstab)};
# bash: ${#(readlink -f /etc/fstab)}: bad substitution

Ich verstehe, dass dies möglich ist, indem zuerst die Ausgabe in einer Variablen gespeichert wird:

variable=$(readlink -f /etc/fstab);
echo ${#variable};

Aber ich möchte den zusätzlichen Schritt entfernen.

Ist das möglich? Die Kompatibilität mit der Almquist-Shell (sh) unter Verwendung nur eingebauter oder Standarddienstprogramme ist vorzuziehen.

user339676
quelle
1
Die Ausgabe von readlink -f /etc/fstabist 11 Zeichen. Vergessen Sie nicht die Newline. Andernfalls würden Sie sehen, /etc/fstabluser@cern:~$ wann Sie es von einer Shell ausgeführt haben.
Phil Frost
@PhilFrost Sie scheinen eine lustige Eingabeaufforderung zu haben, arbeiten Sie im CERN?
Dmitry Grigoryev

Antworten:

8

Mit GNU Ausdruck :

$ expr length + "$(readlink -f /etc/fstab)"
10

Die +gibt es eine Besonderheit GNU exprum sicherzustellen , dass das nächste Argument als String behandelt wird , selbst wenn es sich um eine sein geschieht exprOperator wie match, length, +...

Das Obige entfernt alle nachfolgenden Zeilen der Ausgabe. Um es zu umgehen:

$ expr length + "$(readlink -f /etc/fstab; printf .)" - 2
10

Das Ergebnis wurde von 2 subtrahiert, da der letzte Zeilenumbruch readlinkund das von .uns hinzugefügte Zeichen .

Mit Unicode-Zeichenfolge exprscheint dies nicht zu funktionieren, da die Länge der Zeichenfolge in Byte anstelle der Anzahl der Zeichen zurückgegeben wird (siehe Zeile 654 ).

$ LC_ALL=C.UTF-8 expr length ăaa
4

Sie können also Folgendes verwenden:

$ printf "ăaa" | LC_ALL=C.UTF-8 wc -m
3

POSIXLY:

$ expr " $(readlink -f /etc/fstab; printf .)" : ".*" - 3
10

Das Leerzeichen vor der Befehlssubstitution verhindert, dass der Befehl mit dem Zeichenfolgenanfang abstürzt. -Daher müssen wir 3 subtrahieren.

cuonglm
quelle
Vielen Dank! Es scheint, dass Ihr drittes Beispiel auch ohne das funktioniert LC_ALL=C.UTF-8, was die Dinge erheblich vereinfacht, wenn die Codierung der Zeichenfolge nicht im Voraus bekannt ist.
user339676
2
expr length $(echo "*")- Nein. Verwenden Sie mindestens doppelte Anführungszeichen : expr length "$(…)". Dadurch werden nachgestellte Zeilenumbrüche aus dem Befehl entfernt. Dies ist eine unausweichliche Funktion der Befehlssubstitution. (Sie können es
umgehen
6

Ich bin mir nicht sicher, wie ich das mit Shell-Buildins machen soll ( Gnouc ist es allerdings ), aber die Standard-Tools können helfen:

  1. Sie können verwenden, wc -mwelche Zeichen zählen. Leider zählt auch der letzte Zeilenumbruch, sodass Sie diesen zuerst entfernen müssen:

    readlink -f /etc/fstab | tr -d '\n' | wc -m
  2. Sie können natürlich verwenden awk

    readlink -f /etc/fstab | awk '{print length($0)}'
  3. Oder Perl

    readlink -f /etc/fstab | perl -lne 'print length'
terdon
quelle
Meinst du exprist ein eingebauter? In welcher Schale?
Mikeserv
5

Normalerweise mache ich das so:

$ echo -n "$variable" | wc -m
10

Um Befehle auszuführen, würde ich es so anpassen:

$ echo -n "$(readlink -f /etc/fstab)" | wc -m
10

Dieser Ansatz ähnelt dem, was Sie in Ihren beiden Schritten gemacht haben, außer dass wir sie zu einem einzigen Einzeiler kombinieren.

slm
quelle
2
Sie müssen -manstelle von verwenden -c. Mit Unicode-Zeichen wird Ihr Ansatz unterbrochen.
Cuonglm
1
Warum nicht einfach readlink -f /etc/fstab | wc -m?
Phil Frost
1
Warum verwenden Sie diese unzuverlässige Methode anstelle von ${#variable}? Verwenden Sie mindestens doppelte Anführungszeichen echo -n "$variable", aber dies schlägt immer noch fehl, wenn z. B. der Wert von variableist -e. Wenn Sie es in Kombination mit einer Befehlsersetzung verwenden, beachten Sie, dass nachfolgende Zeilenumbrüche entfernt werden.
Gilles 'SO - hör auf böse zu sein'
@philfrost b / c was ich zeigte, baute auf dem auf, was die Operation bereits dachte. Es funktioniert auch für alle cmds, die er möglicherweise zuvor in vars eingerichtet hat und deren Länge nach den Wörtern möchte. Auch Terdon hat dieses Beispiel bereits.
slm
1

Sie können externe Dienstprogramme aufrufen (siehe andere Antworten), aber dadurch wird Ihr Skript langsamer, und es ist schwierig, die Installation richtig zu machen.

Zsh

In zsh können Sie schreiben ${#$(readlink -f /etc/fstab)}, um die Länge der Befehlsersetzung zu ermitteln. Beachten Sie, dass dies nicht die Länge der Befehlsausgabe ist, sondern die Länge der Ausgabe ohne nachgestellte Zeilenumbruch.

Wenn Sie die genaue Länge der Ausgabe wünschen, geben Sie am Ende ein zusätzliches Zeichen ohne Zeilenumbruch aus und subtrahieren Sie eines.

$((${#$(readlink -f /etc/fstab; echo .)} - 1))

Wenn Sie die Nutzlast in der Ausgabe des Befehls wünschen, müssen Sie hier zwei subtrahieren , da die Ausgabe von readlink -fder kanonische Pfad plus eine neue Zeile ist.

$((${#$(readlink -f /etc/fstab; echo .)} - 2))

Dies unterscheidet sich von ${#$(readlink -f /etc/fstab)}dem seltenen, aber möglichen Fall, in dem der kanonische Pfad selbst in einer neuen Zeile endet.

Für dieses spezielle Beispiel benötigen Sie überhaupt kein externes Dienstprogramm, da zsh über ein integriertes Konstrukt verfügt, das readlink -füber den Verlaufsmodifikator äquivalent ist A.

echo /etc/fstab(:A)

Verwenden Sie den Verlaufsmodifikator in einer Parametererweiterung, um die Länge zu ermitteln:

${#${:-/etc/fstab}:A}

Wenn Sie den Dateinamen in einer Variablen filenamehaben, wäre das ${#filename:A}.

Muscheln im Bourne / POSIX-Stil

Keine der reinen Bourne / POSIX-Shells (Bourne, Ash, Mksh, Ksh93, Bash, Yash…) hat eine ähnliche Erweiterung, die ich kenne. Wenn Sie eine Parametersubstitution auf die Ausgabe einer Befehlssubstitution anwenden oder Parametersubstitutionen verschachteln müssen, verwenden Sie aufeinanderfolgende Stufen.

Sie können die Verarbeitung in eine Funktion einfügen, wenn Sie möchten.

command_output_length_sans_trailing_newlines () {
  set -- "$("$@")"
  echo "${#1}"
}

oder

command_output_length () {
  set -- "$("$@"; echo .)"
  echo "$((${#1} - 1))"
}

aber es gibt normalerweise keinen Nutzen; Mit Ausnahme von ksh93 kann ein zusätzlicher Fork die Ausgabe der Funktion verwenden, sodass Ihr Skript langsamer wird und es selten Vorteile für die Lesbarkeit gibt.

Die Ausgabe von readlink -fist wiederum der kanonische Pfad plus eine neue Zeile; Wenn Sie die Länge des kanonischen Pfades möchten, subtrahieren Sie 2 statt 1 Zoll command_output_length. Die Verwendung command_output_length_sans_trailing_newlinesliefert nur dann das richtige Ergebnis, wenn der kanonische Pfad selbst nicht in einer neuen Zeile endet.

Bytes gegen Zeichen

${#…}soll die Länge in Zeichen sein, nicht in Bytes, was bei Multibyte-Gebietsschemas einen Unterschied macht. Ziemlich aktuelle Versionen von ksh93, bash und zsh berechnen die Länge in Zeichen gemäß dem Wert von LC_CTYPEzum Zeitpunkt der ${#…}Erweiterung des Konstrukts. Viele andere gängige Shells unterstützen Multibyte-Gebietsschemas nicht wirklich: Ab Strich 0.5.7 geben mksh 46 und posh 0.12.3 ${#…}die Länge in Bytes zurück. Wenn Sie die Länge in Zeichen zuverlässig festlegen möchten, verwenden Sie das wcDienstprogramm:

$(readlink -f /etc/fstab | wc -m)

Solange $LC_CTYPEein gültiges Gebietsschema festgelegt ist, können Sie sicher sein, dass dies entweder fehlerhaft ist (auf einer alten oder eingeschränkten Plattform, die keine Multibyte-Gebietsschemas unterstützt) oder die richtige Länge in Zeichen zurückgibt. (Für Unicode bedeutet "Länge in Zeichen" die Anzahl der Codepunkte - die Anzahl der Glyphen ist aufgrund von Komplikationen wie dem Kombinieren von Zeichen eine weitere Geschichte.)

Wenn Sie die Länge in Bytes möchten, legen Sie diese LC_CTYPE=Cvorübergehend fest oder verwenden Sie wc -cstattdessen wc -m.

Das Zählen von Bytes oder Zeichen mit wcschließt alle nachfolgenden Zeilenumbrüche aus dem Befehl ein. Wenn Sie die Länge des kanonischen Pfads in Bytes angeben möchten, ist dies der Fall

$(($(readlink -f /etc/fstab | wc -c) - 1))

Subtrahieren Sie 2, um es in Zeichen zu erhalten.

Gilles 'SO - hör auf böse zu sein'
quelle
@cuonglm Nein, Sie müssen 1 subtrahieren. Fügt echo .zwei Zeichen hinzu, aber das zweite Zeichen ist eine nachgestellte neue Zeile, die durch die Befehlsersetzung entfernt wird.
Gilles 'SO - hör auf böse zu sein'
Die neue Zeile stammt aus der readlinkAusgabe plus der .von echo. Wir sind uns einig, dass echo .zwei Zeichen hinzugefügt werden, aber die nachfolgende neue Zeile wurde entfernt. Versuche es mitprintf . oder sehen Sie meine Antwort unix.stackexchange.com/a/160499/38906 .
Cuonglm
@cuonglm Die Frage stellte die Anzahl der Zeichen in der Befehlsausgabe. Die Ausgabe von readlinkist das Link-Ziel plus eine neue Zeile.
Gilles 'SO - hör auf böse zu sein'
0

Dies funktioniert in dash, erfordert jedoch, dass die Zielvariable definitiv leer oder nicht gesetzt ist. Deshalb sind dies eigentlich zwei Befehle - ich leere $lim ersten explizit :

l=;printf '%.slen is %d and result is %s\n' \
    "${l:=$(readlink -f /etc/fstab)}" "${#l}" "$l"

AUSGABE

len is 10 and result is /etc/fstab

Das sind alles Shell-Buildins - natürlich nicht einschließlich readlink-, aber wenn Sie es in der aktuellen Shell auf diese Weise auswerten, müssen Sie die Zuweisung %.svornehmen, bevor Sie die len erhalten. Deshalb verwende ich das erste Argument in der Formatzeichenfolge printfund füge es einfach erneut hinzu der Literalwert am Ende der printfArg-Liste.

Mit eval:

l=$(readlink -f /etc/fstab) eval 'l=${#l}:$l'
printf %s\\n "$l"

AUSGABE

10:/etc/fstab

Sie können sich der gleichen Sache nähern, aber anstelle der Ausgabe in einer Variablen im ersten Befehl erhalten Sie sie auf stdout:

PS4='${#0}:$0' dash -cx '2>&1' "$(readlink -f /etc/fstab)"

... was schreibt ...

10:/etc/fstab

... zum Dateideskriptor 1, ohne vars in der aktuellen Shell einen Wert zuzuweisen.

mikeserv
quelle
1
Wollte das OP das nicht vermeiden? "Ich verstehe, dass dies möglich ist, indem zuerst die Ausgabe in einer Variablen variable=$(readlink -f /etc/fstab); echo ${#variable};gespeichert wird. Ich möchte jedoch den zusätzlichen Schritt entfernen."
Terdon
@terdon, wahrscheinlich habe ich falsch verstanden, aber ich hatte den Eindruck, dass das Semikolon das Problem und nicht die Variable war. Aus diesem Grund erhalten diese die Länge und Ausgabe in einem einzigen einfachen Befehl, wobei nur Shell-Builtins verwendet werden. Die Schale nicht exec Readlink dann exec expr, zum Beispiel. Es ist wahrscheinlich nur wichtig, wenn das Erhalten des Len den Wert irgendwie verschließt, was ich zugeben muss, dass ich Schwierigkeiten habe zu verstehen, warum das sein kann, aber ich vermute, dass es einen Fall geben könnte, in dem es wichtig ist.
Mikesserv
1
Die evalArt und Weise, nebenbei bemerkt , ist wahrscheinlich die sauberste hier - es weist den Ausgang und die len auf den gleichen var name in einer einzigen Ausführung - sehr nah zu tun l=length(l):out(l). Dadurch expr length $(command) hat den Wert für die len occlude, übrigens.
Mikeserv