L'utilisateur appelle mon script avec un chemin de fichier qui sera soit créé soit écrasé à un moment donné du script, comme foo.sh file.txt
ou foo.sh dir/file.txt
.
Le comportement de création ou d'écrasement ressemble beaucoup aux exigences pour placer le fichier du côté droit de l' >
opérateur de redirection de sortie, ou le passer comme argument à tee
(en fait, le passer comme argument à tee
est exactement ce que je fais) ).
Avant d'entrer dans les tripes du script, je veux vérifier raisonnablement si le fichier peut être créé / écrasé, mais pas réellement le créer. Cette vérification ne doit pas être parfaite, et oui je me rends compte que la situation peut changer entre la vérification et le point où le fichier est réellement écrit - mais ici, je suis OK avec une solution de type meilleur effort afin que je puisse renflouer tôt dans le cas où le chemin du fichier n'est pas valide.
Exemples de raisons pour lesquelles le fichier n'a pas pu être créé:
- le fichier contient un composant de répertoire, comme
dir/file.txt
mais le répertoiredir
n'existe pas - l'utilisateur n'a pas d'autorisations d'écriture dans le répertoire spécifié (ou le CWD si aucun répertoire n'a été spécifié
Oui, je me rends compte que la vérification des autorisations "à l'avance" n'est pas The UNIX Way ™ , je devrais plutôt essayer l'opération et demander pardon plus tard. Cependant, dans mon script particulier, cela conduit à une mauvaise expérience utilisateur et je ne peux pas changer le composant responsable.
la source
/foo/bar/file.txt
. Fondamentalement, je passe le chemin pourtee
aimertee $OUT_FILE
oùOUT_FILE
est passé sur la ligne de commande. Cela devrait "simplement fonctionner" avec des chemins absolus et relatifs, non?tee -- "$OUT_FILE"
au moins. Que faire si le fichier existe déjà ou existe mais n'est pas un fichier normal (répertoire, lien symbolique, fifo)?tee "${OUT_FILE}.tmp"
. Si le fichier existe déjà,tee
écrase, ce qui est le comportement souhaité dans ce cas. Si c'est un répertoire,tee
échouera (je pense). lien symbolique Je ne suis pas sûr à 100%?Réponses:
Le test évident serait:
Mais il crée réellement le fichier s'il n'est pas déjà là. Nous pourrions nettoyer après nous:
Mais cela supprimerait un fichier qui existait déjà, ce que vous ne voulez probablement pas.
Nous avons cependant un moyen de contourner cela:
Vous ne pouvez pas avoir un répertoire portant le même nom qu'un autre objet dans ce répertoire. Je ne peux pas penser à une situation dans laquelle vous seriez en mesure de créer un répertoire mais pas de créer un fichier. Après ce test, votre script serait libre de créer un conventionnel
/path/to/file
et de faire ce qui lui plaira.la source
if mkdir /var/run/lockdir
comme test de verrouillage. C'est atomique, alors queif ! [[ -f /var/run/lockfile ]]; then touch /var/run/lockfile
a une petite tranche de temps entre le test ettouch
où une autre instance du script en question peut commencer.D'après ce que je recueille, vous voulez vérifier que lors de l'utilisation
(notez que le
--
ou cela ne fonctionnerait pas pour les noms de fichiers commençant par -),tee
réussirait à ouvrir le fichier en écriture.C'est ça:
Nous ignorerons pour l'instant les systèmes de fichiers comme vfat, ntfs ou hfsplus qui ont des limitations sur les valeurs d'octets que les noms de fichiers peuvent contenir, quota de disque, limite de processus, selinux, apparmor ou autre mécanisme de sécurité, système de fichiers complet, pas d'inode restant, périphérique les fichiers qui ne peuvent pas être ouverts de cette façon pour une raison ou une autre, les fichiers exécutables actuellement mappés dans un espace d'adressage de processus, tous pouvant également affecter la possibilité d'ouvrir ou de créer le fichier.
Avec
zsh
:Dans
bash
ou n'importe quel shell de type Bourne, remplacez simplement leavec:
Les
zsh
fonctionnalités spécifiques ici sont$ERRNO
(qui exposent le code d'erreur du dernier appel système) et un$errnos[]
tableau associatif à traduire dans les noms de macro C standard correspondants. Et le$var:h
(à partir de csh) et$var:P
(nécessite zsh 5.3 ou supérieur).bash n'a pas encore de fonctionnalités équivalentes.
$file:h
peut être remplacé pardir=$(dirname -- "$file"; echo .); dir=${dir%??}
ou par GNUdirname
:IFS= read -rd '' dir < <(dirname -z -- "$file")
.Car
$errnos[ERRNO] == ENOENT
, une approche pourrait consister à exécuterls -Ld
le fichier et à vérifier si le message d'erreur correspond à l'erreur ENOENT. Il est cependant difficile de le faire de manière fiable et portable.Une approche pourrait être:
(en supposant que le message d'erreur se termine par la
syserror()
traduction deENOENT
et que cette traduction n'inclut pas a:
), puis, au lieu de[ -e "$file" ]
, procédez comme suit :Et recherchez une erreur ENOENT avec
La
$file:P
partie est la plus délicate à réaliser enbash
particulier sur FreeBSD.FreeBSD a une
realpath
commande et unereadlink
commande qui accepte une-f
option, mais elles ne peuvent pas être utilisées dans les cas où le fichier est un lien symbolique qui ne se résout pas. C'est la même chose avecperl
« sCwd::realpath()
.python
« sos.path.realpath()
ne semble fonctionner de façon similaire àzsh
$file:P
, donc en supposant qu'au moins une versionpython
est installée et qu'il ya unepython
commande qui fait référence à l' un d'entre eux ( ce qui est une donnée sur FreeBSD), vous pouvez faire:Mais alors, vous pourriez aussi bien faire tout cela
python
.Ou vous pourriez décider de ne pas gérer tous ces cas d'angle.
la source
tee
donc l'exigence est vraiment que le fichier peut être créé s'il n'existe pas, ou peut être tronqué à zéro et écrasé si c'est le cas (ou sinontee
gère cela).perl
,awk
,sed
oupython
(ou mêmesh
,bash
...). Les scripts shell consistent à utiliser la meilleure commande pour la tâche. Ici mêmeperl
ne dispose pas d' un équivalent dezsh
« de$var:P
(sesCwd::realpath
de BSD se comporte commerealpath
oureadlink -f
pendant que vous voudriez se comporter comme GNUreadlink -f
oupython
» sos.path.realpath()
)Une option que vous voudrez peut-être envisager est de créer le fichier au début, mais de le remplir plus tard dans votre script. Vous pouvez utiliser la
exec
commande pour ouvrir le fichier dans un descripteur de fichier (tel que 3, 4, etc.), puis utiliser ultérieurement une redirection vers un descripteur de fichier (>&3
, etc.) pour écrire du contenu dans ce fichier.Quelque chose comme:
Vous pouvez également utiliser un
trap
pour nettoyer le fichier en cas d'erreur, c'est une pratique courante.De cette façon, vous obtenez une véritable vérification des autorisations que vous pourrez créer le fichier, tout en étant en mesure de l'exécuter suffisamment tôt pour que si cela échoue, vous n'avez pas passé de temps à attendre les longues vérifications.
MISE À JOUR: Afin d'éviter de surcharger le fichier en cas d'échec des vérifications, utilisez la
fd<>file
redirection de bash qui ne tronque pas immédiatement le fichier. (Nous ne nous soucions pas de la lecture du fichier, il s'agit simplement d'une solution de contournement, nous ne le tronquons donc pas.>>
le tronquons fonctionnerait probablement aussi, mais j'ai tendance à trouver celui-ci un peu plus élégant, en gardant le drapeau O_APPEND sur de l'image.)Lorsque nous sommes prêts à remplacer le contenu, nous devons d'abord tronquer le fichier (sinon, si nous écrivons moins d'octets qu'il n'y en avait auparavant, les octets de fin y resteront.) Nous pouvons utiliser la troncature (1) commande de coreutils à cet effet, et nous pouvons lui passer le descripteur de fichier ouvert que nous avons (en utilisant le
/dev/fd/3
pseudo-fichier) de sorte qu'il n'a pas besoin de rouvrir le fichier. (Encore une fois, quelque chose de plus simple sur le plan technique: >dir/file.txt
fonctionnerait probablement, mais ne pas avoir à rouvrir le fichier est une solution plus élégante.)la source
echo -n >>file
outrue >> file
. Bien sûr, ext4 a seulement des fichiers ajoutés, mais vous pouvez vivre avec ce faux positif.my-file.txt.backup
) avant d'en créer un nouveau. Une autre solution consiste à écrire le nouveau fichier dans un fichier temporaire dans le même dossier, puis à le copier sur l'ancien fichier après la réussite du reste du script --- si cette dernière opération a échoué, l'utilisateur pourrait résoudre manuellement le problème sans perdre ses progrès.Je pense que la solution de DopeGhoti est meilleure mais cela devrait également fonctionner:
La première construction if vérifie si l'argument est un chemin complet (commence par
/
) et définit ladir
variable sur le chemin du répertoire jusqu'au dernier/
. Sinon, si l'argument ne commence pas par un/
mais contient un/
(en spécifiant un sous-répertoire), il sera définidir
sur le répertoire de travail actuel + le chemin du sous-répertoire. Sinon, il suppose le répertoire de travail actuel. Il vérifie ensuite si ce répertoire est accessible en écriture.la source
Qu'en est-il de l'utilisation de la
test
commande normale comme indiqué ci-dessous?la source
man test
, et cela[ ]
équivaut à tester (ce n'est plus de notoriété publique). Voici le one-liner que j'allais utiliser dans ma réponse inférieure, pour couvrir le cas exact, pour la postérité:if [ -w $1] && [ ! -d $1 ] ; then echo "do stuff"; fi
Vous avez mentionné que l'expérience utilisateur était à l'origine de votre question. Je répondrai sous un angle UX, car vous avez de bonnes réponses sur le plan technique.
Plutôt que d'effectuer la vérification à l'avance, que diriez-vous d'écrire les résultats dans un fichier temporaire puis à la fin, de placer les résultats dans le fichier souhaité par l'utilisateur? Comme:
Le bon avec cette approche: il produit l'opération souhaitée dans le scénario normal de chemin heureux, contourne le problème d'atomicité de test et de définition, préserve les autorisations du fichier cible lors de la création si nécessaire et est très simple à mettre en œuvre.
Remarque: si nous avions utilisé
mv
, nous conserverions les autorisations du fichier temporaire - nous ne voulons pas cela, je pense: nous voulons conserver les autorisations telles qu'elles sont définies dans le fichier cible.Maintenant, le mauvais: il nécessite deux fois plus d'espace (
cat .. >
construction), oblige l'utilisateur à effectuer un travail manuel si le fichier cible n'était pas accessible en écriture au moment où il devait l'être, et laisse le fichier temporaire en place (ce qui pourrait avoir la sécurité) ou problèmes de maintenance).la source
TL; DR:
Contrairement à
<> "${userfile}"
outouch "${userfile}"
, cela n'apportera aucune modification parasite aux horodatages du fichier et fonctionnera également avec les fichiers en écriture seule.Du PO:
Et de votre commentaire à ma réponse du point de vue UX :
Le seul test fiable concerne
open(2)
le fichier, car seul celui-ci résout toutes les questions sur l'écriture en écriture: chemin, propriété, système de fichiers, réseau, contexte de sécurité, etc. Tout autre test traitera une partie de l'écriture en écriture, mais pas d'autres. Si vous voulez un sous-ensemble de tests, vous devrez finalement choisir ce qui est important pour vous.Mais voici une autre pensée. D'après ce que je comprends:
Vous souhaitez effectuer cette pré-vérification en raison de # 1, et vous ne voulez pas jouer avec un fichier existant en raison de # 2. Alors pourquoi ne demandez-vous pas simplement au shell d'ouvrir le fichier pour l'ajout, mais n'ajoutez rien réellement?
Sous le capot, cela résout la question de l'écriture exactement comme souhaité:
mais sans modifier le contenu ou les métadonnées du fichier. Maintenant, oui, cette approche:
lsattr
et réagir en conséquence.rm
.Bien que je soutienne (dans mon autre réponse) que l' approche la plus conviviale consiste à créer un fichier temporaire que l'utilisateur doit déplacer, je pense que c'est l' approche la moins hostile à l' utilisateur pour contrôler pleinement son entrée.
la source