Der Benutzer ruft mein Skript mit einem Dateipfad auf, der an einem bestimmten Punkt im Skript entweder erstellt oder überschrieben wird, z foo.sh file.txt
oder foo.sh dir/file.txt
.
Das Verhalten beim Erstellen oder Überschreiben entspricht weitgehend den Anforderungen, um die Datei auf der rechten Seite des Ordners zu platzieren >
Ausgabe-Umleitungsoperators zu setzen oder sie als Argument an zu übergeben tee
(tatsächlich tee
ist es genau das, was ich tue, wenn ich sie als Argument an übergebe) ).
Bevor ich in den Eingeweiden des Skripts zu bekommen, möchte ich eine angemessene Kontrolle machen , wenn die Datei kann erstellt werden / überschrieben, aber es nicht wirklich schaffen. Diese Prüfung muss nicht perfekt sein, und es ist mir klar, dass sich die Situation zwischen der Prüfung und dem Punkt, an dem die Datei tatsächlich geschrieben wird, ändern kann Lösung besten Aufwand finden , sodass ich frühzeitig aussteigen kann In dem Fall, dass der Dateipfad ungültig ist.
Beispiele für Gründe, aus denen die Datei nicht erstellt werden konnte:
- Die Datei enthält eine Verzeichniskomponente wie
dir/file.txt
aber das Verzeichnisdir
existiert nicht - Der Benutzer hat keine Schreibberechtigung für das angegebene Verzeichnis (oder die CWD, wenn kein Verzeichnis angegeben wurde)
Ja, ich stelle fest, dass das Überprüfen von Berechtigungen "im Voraus" nicht The UNIX Way ™ ist , sondern ich sollte den Vorgang einfach ausprobieren und später um Verzeihung bitten. In meinem speziellen Skript führt dies jedoch zu einer schlechten Benutzererfahrung, und ich kann die verantwortliche Komponente nicht ändern.
quelle
/foo/bar/file.txt
. Grundsätzlich übergebe ich den Pfadtee
gerne dahin,tee $OUT_FILE
woOUT_FILE
auf der Kommandozeile übergeben wird. Das sollte "nur" mit absoluten und relativen Pfaden funktionieren, oder?tee -- "$OUT_FILE"
zumindest brauchen . Was ist, wenn die Datei bereits existiert oder existiert, aber keine reguläre Datei ist (Verzeichnis, Symlink, FIFO)?tee "${OUT_FILE}.tmp"
. Wenn die Datei bereits vorhanden ist, wirdtee
überschrieben. Dies ist in diesem Fall das gewünschte Verhalten. Wenn es ein Verzeichnis ist,tee
wird es fehlschlagen (glaube ich). symlink Ich bin nicht 100% sicher?Antworten:
Der naheliegende Test wäre:
Die Datei wird jedoch tatsächlich erstellt, wenn sie noch nicht vorhanden ist. Wir könnten nach uns selbst aufräumen:
Dies würde jedoch eine bereits vorhandene Datei entfernen, die Sie wahrscheinlich nicht möchten.
Wir haben jedoch einen Ausweg:
Sie können kein Verzeichnis mit demselben Namen wie ein anderes Objekt in diesem Verzeichnis haben. Ich kann mir keine Situation vorstellen, in der Sie ein Verzeichnis erstellen, aber keine Datei erstellen könnten. Nach diesem Test steht es Ihrem Skript frei, ein konventionelles Skript zu erstellen
/path/to/file
und damit zu tun, was es will.quelle
if mkdir /var/run/lockdir
als Verriegelungstest verwendet. Dies ist atomar, wobeiif ! [[ -f /var/run/lockfile ]]; then touch /var/run/lockfile
zwischen dem Test undtouch
dem Start einer anderen Instanz des fraglichen Skripts eine kurze Zeit liegt .Nach dem, was ich zusammengetragen habe, möchten Sie dies bei der Verwendung überprüfen
(Beachten Sie, dass das
--
oder nicht für Dateinamen funktioniert, die mit - beginnen.)tee
Die Datei konnte zum Schreiben geöffnet werden.Das ist das:
Wir werden vorerst Dateisysteme wie vfat, ntfs oder hfsplus ignorieren, die Einschränkungen hinsichtlich der möglichen Bytewerte von Dateinamen, Datenträgerkontingent, Prozesslimit, Selinux, Apparmor oder anderen Sicherheitsmechanismen in der Art, vollständiges Dateisystem, kein Inode mehr, Gerät haben Dateien, die aus irgendeinem Grund nicht auf diese Weise geöffnet werden können, Dateien, die ausführbare Dateien sind, die derzeit in einem Prozessadressraum zugeordnet sind, was sich auch auf die Fähigkeit auswirken kann, die Datei zu öffnen oder zu erstellen.
Mit
zsh
:bash
Ersetzen Sie in oder in einer Bourne-ähnlichen Shell einfach diemit:
Die
zsh
-spezifischen Funktionen sind$ERRNO
(die den Fehlercode des letzten Systemaufrufs offenlegen) und das$errnos[]
assoziative Array, das in die entsprechenden Standard-C-Makronamen übersetzt werden soll. Und das$var:h
(von csh) und$var:P
(benötigt zsh 5.3 oder höher).bash hat noch keine entsprechenden Features.
$file:h
kann ersetzt werdendir=$(dirname -- "$file"; echo .); dir=${dir%??}
, oder mit GNUdirname
:IFS= read -rd '' dir < <(dirname -z -- "$file")
.Ein
$errnos[ERRNO] == ENOENT
Ansatz könnte darin bestehen,ls -Ld
die Datei zu verarbeiten und zu prüfen, ob die Fehlermeldung dem ENOENT-Fehler entspricht. Dies zuverlässig und portabel zu tun, ist jedoch schwierig.Ein Ansatz könnte sein:
(unter der Annahme, dass die Fehlermeldung mit der Übersetzung von endet und dass diese Übersetzung kein a enthält ) und führen Sie dann stattdessen Folgendes aus:
syserror()
ENOENT
:
[ -e "$file" ]
Und mit auf einen ENOENT-Fehler prüfen
Der
$file:P
Teil ist der schwierigstebash
, der unter FreeBSD erreicht werden kann.FreeBSD verfügt zwar über einen
realpath
Befehl und einenreadlink
Befehl, der eine-f
Option akzeptiert , sie können jedoch nicht in den Fällen verwendet werden, in denen es sich bei der Datei um einen Symlink handelt, der nicht aufgelöst werden kann. Das ist das selbe mitperl
'sCwd::realpath()
.python
'sos.path.realpath()
scheint ähnlich zu funktionierenzsh
$file:P
, wenn also mindestens eine Version vonpython
installiert ist und einpython
Befehl auf eine von ihnen verweist (was unter FreeBSD nicht gegeben ist), können Sie Folgendes tun:Aber dann kannst du das Ganze genauso gut machen
python
.Oder Sie könnten entscheiden, nicht alle diese Eckfälle zu behandeln.
quelle
tee
der Voraussetzung, dass die Datei tatsächlich erstellt werden kann, wenn sie nicht vorhanden ist, oder auf null gekürzt und überschrieben werden kann wenn ja (oder wie auch immertee
).perl
,awk
,sed
oderpython
(oder sogarsh
,bash
...). Beim Shell-Scripting geht es darum, den besten Befehl für die Aufgabe zu verwenden. Hierperl
gibt es nicht einmal ein Äquivalent vonzsh
's$var:P
(esCwd::realpath
verhält sich wie BSDrealpath
oderreadlink -f
während Sie möchten, dass es sich wie GNUreadlink -f
oderpython
' s verhältos.path.realpath()
)Möglicherweise möchten Sie die Datei zu einem frühen Zeitpunkt erstellen, sie jedoch erst später in Ihrem Skript einfügen. Mit dem
exec
Befehl können Sie die Datei in einem Dateideskriptor (z. B. 3, 4 usw.) öffnen und später eine Umleitung zu einem Dateideskriptor (>&3
usw.) verwenden, um Inhalte in diese Datei zu schreiben.Etwas wie:
Sie können auch a verwenden
trap
, um die Datei bei einem Fehler zu bereinigen. Dies ist eine gängige Praxis.Auf diese Weise erhalten Sie eine echte Prüfung auf Berechtigungen, mit denen Sie die Datei erstellen können, während Sie sie gleichzeitig so früh ausführen können, dass Sie nicht auf die langen Prüfungen gewartet haben, wenn dies fehlschlägt.
AKTUALISIERT: Um zu vermeiden, dass die Datei bei fehlgeschlagenen Überprüfungen überladen wird, verwenden Sie die bash-
fd<>file
Umleitung, die die Datei nicht sofort abschneidet. (Es ist uns nicht wichtig, aus der Datei zu lesen. Dies ist nur eine Problemumgehung, sodass wir sie nicht abschneiden. Das Anhängen mit>>
würde wahrscheinlich auch funktionieren. Ich finde diese Datei jedoch etwas eleganter und halte das O_APPEND-Flag aus des Bildes.)Wenn wir bereit sind, den Inhalt zu ersetzen, müssen wir die Datei zuerst abschneiden (andernfalls bleiben die nachfolgenden Bytes erhalten, wenn wir weniger Bytes schreiben, als zuvor in der Datei enthalten waren. ) Befehl von coreutils für diesen Zweck, und wir können ihm den geöffneten Dateideskriptor übergeben, den wir haben (unter Verwendung der
/dev/fd/3
Pseudodatei), damit er die Datei nicht erneut öffnen muss. (Auch hier würde technisch gesehen: >dir/file.txt
wahrscheinlich etwas Einfacheres funktionieren, aber die Datei nicht erneut öffnen zu müssen, ist eine elegantere Lösung.)quelle
echo -n >>file
odertrue >> file
. Natürlich hat ext4 nur anhängbare Dateien, aber Sie könnten mit diesem falschen Positiv leben.my-file.txt.backup
vor dem Erstellen eines neuen Namens unter einen neuen Namen zu verschieben (z. B. ). Eine andere Lösung besteht darin, die neue Datei in eine temporäre Datei im selben Ordner zu schreiben und sie dann über die alte Datei zu kopieren, nachdem der Rest des Skripts erfolgreich war. Wenn dieser letzte Vorgang fehlgeschlagen ist, kann der Benutzer das Problem manuell beheben, ohne zu verlieren sein oder ihr Fortschritt.Ich denke, die Lösung von DopeGhoti ist besser, aber das sollte auch funktionieren:
Das erste if-Konstrukt prüft, ob das Argument ein vollständiger Pfad ist (beginnt mit
/
) und setzt diedir
Variable bis zum letzten auf den Verzeichnispfad/
. Wenn das Argument nicht mit einem beginnt,/
sondern ein enthält/
(wobei ein Unterverzeichnis angegeben wird), wird esdir
auf das aktuelle Arbeitsverzeichnis + den Pfad des Unterverzeichnisses gesetzt. Andernfalls wird das aktuelle Arbeitsverzeichnis angenommen. Anschließend wird geprüft, ob dieses Verzeichnis beschreibbar ist.quelle
Was ist mit der Verwendung eines normalen
test
Befehls wie unten beschrieben?quelle
man test
, und das[ ]
entspricht einem Test (nicht mehr allgemein bekannt). Hier ist ein Einzeiler, den ich in meiner minderwertigen Antwort verwenden wollte, um den genauen Fall für die Nachwelt abzudecken:if [ -w $1] && [ ! -d $1 ] ; then echo "do stuff"; fi
Sie erwähnten, dass die Benutzererfahrung Ihre Frage vorantreibt. Ich werde aus einem UX-Blickwinkel antworten, da Sie auf technischer Seite gute Antworten haben.
Wie wäre es, die Ergebnisse in eine temporäre Datei zu schreiben und sie dann ganz am Ende in die vom Benutzer gewünschte Datei zu legen, anstatt die Prüfung im Voraus durchzuführen ? Mögen:
Das Gute an diesem Ansatz: Es erzeugt die gewünschte Operation im normalen Happy-Path-Szenario, umgeht das Atomicity-Problem von Test und Set, behält die Berechtigungen der Zieldatei bei, während es bei Bedarf erstellt wird, und ist kinderleicht zu implementieren.
Hinweis: Hätten wir verwendet
mv
, würden wir die Berechtigungen der temporären Datei behalten - wir wollen das nicht, denke ich: wir wollen die Berechtigungen behalten, die für die Zieldatei festgelegt wurden.Das Schlimme: Es benötigt das Doppelte des Speicherplatzes (
cat .. >
Konstrukt), zwingt den Benutzer zu manueller Arbeit, wenn die Zieldatei zum erforderlichen Zeitpunkt nicht beschreibbar war, und lässt die temporäre Datei herumliegen (was möglicherweise Sicherheit bietet oder Wartungsprobleme).quelle
TL; DR:
Im Gegensatz zu
<> "${userfile}"
odertouch "${userfile}"
werden hierdurch keine falschen Änderungen an den Zeitstempeln der Datei vorgenommen und es wird auch mit Nur-Schreib-Dateien gearbeitet.Aus dem OP:
Und von Ihrem Kommentar zu meiner Antwort aus einer UX-Perspektive :
Der einzige zuverlässige Test
open(2)
bezieht sich auf die Datei, da nur dadurch alle Fragen zur Schreibbarkeit gelöst werden: Pfad, Eigentümer, Dateisystem, Netzwerk, Sicherheitskontext usw. Bei jedem anderen Test wird ein Teil der Schreibbarkeit behandelt, andere jedoch nicht. Wenn Sie eine Untergruppe von Tests wünschen, müssen Sie letztendlich auswählen, was für Sie wichtig ist.Aber hier ist ein anderer Gedanke. Von dem, was ich verstehe:
Sie möchten diese Vorabprüfung aufgrund von Nummer 1 durchführen und möchten aufgrund von Nummer 2 nicht mit einer vorhandenen Datei fummeln. Warum fordern Sie die Shell nicht einfach auf, die Datei zum Anhängen zu öffnen, sondern hängen tatsächlich nichts an?
Unter der Haube löst dies die Beschreibbarkeitsfrage genau wie gewünscht:
aber ohne den Inhalt oder die Metadaten der Datei zu ändern. Nun ja, dieser Ansatz:
lsattr
und entsprechend reagieren.rm
.Während ich (in meiner anderen Antwort) behaupte, dass der benutzerfreundlichste Ansatz darin besteht, eine temporäre Datei zu erstellen, die der Benutzer verschieben muss, denke ich, dass dies der am wenigsten benutzerfeindliche Ansatz ist, um seine Eingaben vollständig zu überprüfen.
quelle