Portable Check leeres Verzeichnis

7

Mit Bash and Dash können Sie nur mit der Shell nach einem leeren Verzeichnis suchen (Punktdateien ignorieren, um die Dinge einfach zu halten):

set *
if [ -e "$1" ]
then
  echo 'not empty'
else
  echo 'empty'
fi

Allerdings habe ich kürzlich erfahren, dass Zsh in diesem Fall spektakulär versagt:

% set *
zsh: no matches found: *

% echo "$? $#"
1 0

Der setBefehl schlägt also nicht nur fehl, sondern wird auch nicht gesetzt $@. Ich nehme an, ich könnte testen, ob dies der Fall $#ist 0, aber es scheint, dass Zsh die Ausführung sogar stoppt:

% { set *; echo 2; }
zsh: no matches found: *

Vergleiche mit Bash und Dash:

$ { set *; echo 2; }
2

Kann dies auf eine Weise geschehen, die in Bash, Dash und Zsh funktioniert?

terdon
quelle
Verwandte: Warum ist nullglob nicht Standard?
Stéphane Chazelas

Antworten:

4

Damit gibt es mehrere Probleme

set *
if [ -e "$1" ]
then
  echo 'not empty'
else
  echo 'empty'
fi

Code:

  • wenn die nullglobOption (ab zshjetzt aber von den meisten anderen Shells unterstützt) aktiviert ist, set *wird , setwelche Listen Sie alle Shell - Variablen (und Funktionen in einigen Muscheln)
  • Wenn die erste nicht ausgeblendete Datei einen Namen hat, der mit -oder beginnt +, wird sie von als Option behandelt set. Diese beiden Probleme können stattdessen behoben werden set -- *.
  • *Erweitert nur nicht versteckte Dateien. Es ist also kein Test, ob das Verzeichnis leer ist oder nicht, sondern ob es nicht versteckte Dateien enthält oder nicht. Mit einigen Muscheln und Sie können verwenden dotgloboder globdotOption oder das Spiel mit einer FIGNOREspeziellen variabel in Abhängigkeit von der Schale zu arbeiten um die.
  • [ -e "$1" ]Testet, ob ein stat()Systemaufruf erfolgreich ist oder nicht. Wenn die erste Datei ein Symlink zu einem unzugänglichen Speicherort ist, wird false zurückgegeben. Sie sollten keine Datei stat()(nicht einmal lstat()) benötigen, um zu wissen, ob ein Verzeichnis leer ist oder nicht. Überprüfen Sie nur, ob es Inhalt enthält.
  • * Bei der Erweiterung wird das aktuelle Verzeichnis geöffnet, alle Einträge abgerufen, alle nicht ausgeblendeten gespeichert und sortiert, was ebenfalls ineffizient ist.

Der effizienteste Weg, um zu überprüfen, ob ein Verzeichnis nicht leer ist (mit einem anderen Eintrag als .und ..), zshist das FGlob-Qualifikationsmerkmal ( Ffür vollständig , hier nicht leer):

if [ .(NF) ]; then
  print . is not empty
else
  print "it's empty or I can't read it"
fi

Nist das nullglobGlob-Qualifikationsmerkmal. Erweitert .(NF)sich also, .wenn .voll ist und sonst nichts.

Nachdem die lstat()auf dem Verzeichnis, wenn zshes hat Funde eine Link-Zähler größer als 2 ist , dann das bedeutet , es hat zumindest ein Unterverzeichnis so leer ist , nicht, so dass wir dieses Verzeichnis nicht einmal öffnen müssen (das bedeutet auch, dass In diesem Fall können wir feststellen, dass das Verzeichnis nicht leer ist, auch wenn wir keinen Lesezugriff darauf haben. Andernfalls öffnet zsh das Verzeichnis, liest seinen Inhalt und stoppt beim ersten Eintrag, der weder .noch ..ohne Lesen, Speichern oder Sortieren alles ist.

Bei POSIX-Shells (die zshsich in der shEmulation nur (mehr) POSIX verhalten ) ist es sehr umständlich zu überprüfen, ob ein Verzeichnis nur mit Globs nicht leer ist.

Ein Weg ist mit:

set .[!.]* '.[!.]'[*] .[.]?* [*] *
if [ "$#$1$2$3$4$5" = '5.[!.]*.[!.][*].[.]?*[*]*' ]; then
  echo "empty or can't read"
else
  echo not empty
fi

(unter der Annahme, dass keine globbezogene Option gegenüber der Standardeinstellung geändert wird (POSIX gibt nur an noglob) und dass die Variablen GLOBIGNORE(für bash) und FIGNORE(für ksh) nicht festgelegt sind und dass (für yash) keiner der Dateinamen Folgen von Bytes enthält, die keine gültigen Zeichen bilden ).

Die Idee ist, dass in POSIX-Shells ein Globus, wenn er nicht übereinstimmt, nicht erweitert wird (eine Fehlfunktion, die die Bourne-Shell Ende der 70er Jahre eingeführt hat). Also mit set -- *, wenn wir bekommen $1== *, wir wissen nicht , ob es war , weil es keine Übereinstimmung war oder ob es eine Datei mit dem Namen *.

Ihr (fehlerhafter) Ansatz, um das zu umgehen, war zu verwenden [ -e "$1" ]. Hier verwenden wir stattdessen set -- [*] *. Dies ermöglicht es, die beiden Fälle zu unterscheiden, denn wenn keine Datei vorhanden ist, bleibt die oben genannte bestehen [*] *, und wenn eine Datei aufgerufen *wird, wird dies * *. Ähnliches tun wir für versteckte Dateien. Das ist etwas umständlich, da die Bourne-Shell (ebenfalls behoben durch zshdie Forsyth-Shell, pdksh und fish) eine weitere Fehlfunktion aufweist, bei der die Erweiterung von .*die speziellen (Pseudo-) Einträge enthält .und von ..gemeldet wird readdir().

Damit es in all diesen Muscheln funktioniert, können Sie Folgendes tun:

cwd_empty()
  if [ -n "$ZSH_VERSION" ]; then
    eval '! [ .(NF) ]'
  else
    set .[!.]* '.[!.]'[*] .[.]?* [*] *
    [ "$#$1$2$3$4$5" = '5.[!.]*.[!.][*].[.]?*[*]*' ]
  fi

In jedem Fall ist die Syntax zshvon standardmäßig nicht mit der POSIX sh-Syntax kompatibel, da sie die meisten Hauptprobleme in der Bourne-Shell (lange bevor POSIX.2 erstmals veröffentlicht wurde) auf nicht abwärtskompatible Weise behoben hat, einschließlich dieser *links unexpanded wenn es keine Übereinstimmung ist (pre-Bourne - Shells nicht das Problem hatte, csh, tcshund fishnicht entweder) und .*darunter .und ..aber einige andere wie Split + glob auf Parameter oder arithmetische Erweiterung durchgeführt, so dass Sie nicht erwarten können Code geschrieben in der POSIX sh, um immer zu arbeiten, es zshsei denn, Sie aktivieren die shEmulation.

Diese shEmulation ist besonders vorhanden, damit Sie POSIX-Code in verwenden können zsh.

Wenn Sie möchten , sourceeine Datei in POSIX geschrieben shinnen zsh, können Sie tun:

emulate sh -c 'source that-file'

Dann wird diese Datei in der shEmulation ausgewertet und jede darin deklarierte Funktion behält diesen Emulationsmodus bei.

Stéphane Chazelas
quelle
6

Während das Standardverhalten von zsh darin besteht, einen Fehler auszugeben, wird dies durch die nomatchOption gesteuert . Sie können die Option deaktivieren, um die Option wie *Bash und Dash an Ort und Stelle zu belassen:

setopt -o nonomatch

Während dieser Befehl in keinem der anderen Befehle funktioniert, können Sie dies einfach ignorieren:

setopt -o nonomatch 2>/dev/null || true ; set *

Dies wird setoptauf zsh ausgeführt und unterdrückt die Fehlerausgabe ( 2>/dev/null) und den Rückkehrcode ( || true) des fehlgeschlagenen Befehls auf den anderen.

Wie geschrieben ist es problematisch, wenn beispielsweise eine Datei vorhanden ist -e: Dann werden Sie set -edie Shell-Optionen ausführen und ändern, um sie zu beenden, wenn ein Befehl fehlschlägt. Es gibt schlechtere Ergebnisse, wenn Sie kreativ sind. set -- *wird sicherer sein und die Optionsänderungen verhindern.

Michael Homer
quelle
3

Die Syntax von Zsh ist nicht mit sh kompatibel. Es ist nah genug, um wie sh auszusehen, aber nicht nah genug, um sh-Code unverändert auszuführen.

Wenn Sie sh-Code in zsh ausführen möchten, z. B. weil Sie eine sh-Funktion oder ein Snippet für sh geschrieben haben, das Sie in einem zsh-Skript verwenden emulatemöchten , können Sie das integrierte Programm verwenden . So beschaffen Sie beispielsweise eine für sh in einem zsh-Skript geschriebene Datei:

emulate sh -c 'source /path/to/file.sh'

Um eine Funktion oder ein Skript in sh-Syntax zu schreiben und die Ausführung in zsh zu ermöglichen, setzen Sie dies am Anfang:

emulate -L sh 2>/dev/null || true

In der sh-Syntax unterstützt zsh alle POSIX-Konstrukte (es ist ungefähr so ​​POSIX-kompatibel wie bash --posixoder ksh93 oder mksh). Es unterstützt auch einige ksh- und bash-Erweiterungen wie Arrays (0-indiziert in ksh, in bash und darunter emulate sh, aber 1-indiziert in nativem zsh) und [[ … ]]. Wenn Sie POSIX sh plus ksh globs möchten, verwenden Sie emulate … ksh …anstelle von emulate … sh …und fügen Sie if [[ -n $BASH ]]; then shopt -s extglob; fifür Bash hinzu (beachten Sie, dass dies nicht lokal für das Skript / die Funktion ist).

Die native zsh-Methode zum Auflisten aller Einträge in einem Verzeichnis mit Ausnahme von .und ..ist

set -- *(DN)

Dabei werden die Glob-Qualifizierer verwendet D , um Punktdateien einzuschließen und Neine leere Liste zu erstellen, wenn keine Übereinstimmungen vorhanden sind.

Die native zsh-Methode zum Auflisten aller Einträge in einem Verzeichnis außer .und ..ist viel komplizierter. Sie müssen Punktdateien auflisten. Wenn Sie Dateien im aktuellen Verzeichnis oder in einem Pfad auflisten, der nicht als absolut garantiert ist, müssen Sie vorsichtig sein, falls ein Dateiname mit einem Bindestrich beginnt. Hier ist eine Möglichkeit, dies zu tun, indem Sie die Muster verwenden, ..?* .[!.]* *um alle Dateien außer .und ..aufzulisten und nicht erweiterte Muster zu entfernen.

set -- ..?*
if [ "$#" -eq 1 ] && ! [ -e "$1" ] && ! [ -L "$1" ]; then shift; fi
set -- .[!.]* "$@"
if [ "$#" -eq 1 ] && ! [ -e "$1" ] && ! [ -L "$1" ]; then shift; fi
set -- * "$@"
if [ "$#" -eq 1 ] && ! [ -e "$1" ] && ! [ -L "$1" ]; then shift; fi

Wenn Sie nur testen möchten, ob ein Verzeichnis leer ist, gibt es einen viel einfacheren Weg.

if ls -A /path/to/directory/ | grep -q '^'; then
  echo "/path/to/directory is not empty"
else
  echo "/path/to/directory is empty"
fi
Gilles 'SO - hör auf böse zu sein'
quelle
kürzerif ls -A | read q; then echo not empty; else echo empty; fi
2

Der settragbarste Weg wäre via und globstar für alle POSIX-kompatiblen Shells. Dies wurde in Gilles 'Antwort auf eine verwandte Frage gezeigt. Ich habe die Methode leicht in eine Funktion umgewandelt:

rm -rf empty_dir/
mkdir empty_dir/
pwd
cd empty_dir/
pwd
dir_empty(){

    # https://stackoverflow.com/a/9911082/3701431
    if [ -n "$ZSH_VERSION" ]; then
        # https://unix.stackexchange.com/a/310553/85039
        setopt +o nomatch 
    fi    

    set -- * .*
    echo "$@"
    for i; do
        [ "$i" = "." ] || [ "$i" = ".." ] && continue
        [ -e "$i" ] && echo "Not empty" && return 1
    done
    echo "Empty" && return 0
}
dir_empty
touch  '*'
dir_empty

Das große Problem mit zshist , dass während kshund bashverhalten sich in mehr oder weniger konsequent - das ist , wenn wir tun , set * .*werden Sie drei Positionsparameter haben * . ..in wirklich leeres Verzeichnis - in zshSie erhalten * .*als Positionsparameter. Glücklicherweise for i ; do ... donefunktioniert zumindest das Durchlaufen von Positionsparametern konsistent. Der Rest ist nur eine Iteration und eine Überprüfung auf das Vorhandensein des Dateinamens mit .und ..übersprungen.


Probieren Sie es online in ksh!

Probieren Sie es online in zsh!

Sergiy Kolodyazhnyy
quelle
@Drei Ich habe die Antwort angepasst, um zu überprüfen zsh. Leider haben wir uns zshentschieden, den seltsamen Weg anstelle eines ähnlichen Verhaltens wie bei bashanderen Shells zu gehen, da laut POSIX: "Wenn das Muster keinem Pfadnamen entspricht, wird die zurückgegebene Anzahl übereinstimmender Pfade auf 0 gesetzt und der Inhalt von pglob- > gl_pathv sind implementierungsdefiniert. " Quelle
Sergiy Kolodyazhnyy
Wenn Sie sich nomatchüberall umdrehen, besteht die Gefahr, dass Code beschädigt wird, der die Standardeinstellung (und die sehr vernünftige) voraussetzt, shund bashdass dies falsch ist.
Daher
@thrig Nun, wenn man bedenkt, dass andere bisher keinen Weg gefunden haben, Glob ohne zu arbeiten nomatch, ist das das Beste, was wir bekommen haben. Wir können es natürlich auch zurückschalten, bevor die Funktion beendet wird. Oder wir könnten einfach Shell-Wege aufgeben und einfach etwas anderes verwenden, wie find zum Beispiel.
Sergiy Kolodyazhnyy
@Drei Ich habe die Antwort noch einmal überarbeitet. Wahrscheinlich ist dies das Beste, was ich tun kann, da zshFunktionen anstelle von Konsistenz bevorzugt werden. Hoffe das hilft etwas.
Sergiy Kolodyazhnyy
1
Die Erweiterung von .*nicht enthalten .und ..ist das, was Sie im Allgemeinen wollen und wurde in der gemeinfreien Version von kshzuvor durchgeführt zsh(pdksh hat es von der Forsyth-Shell geerbt, auf der es basiert). In anderen Schalen , ob .und ..enthalten ist , hängt davon ab , ob readdir()kehrt sie oder nicht , die nicht garantiert ist.
Stéphane Chazelas
0

Ich bin mir bei der set *Lösung nicht sicher . Wie wäre es damit?

dir="./empty"
has_file=false

for i in "$dir"/*; do 
  if test ! "$i" = "$dir/*"; then
    has_file=true
    echo "$i"
  if
done

echo "$has_file"

Getestet und das funktioniert gut für zsh, bash, ash, ksh, bourneAKAheirloom

Nick Bull
quelle