Trouver des liens symboliques avec Python

Si j’appelle os.stat() sur un symlink cassé, python lève une exception OSError . Cela le rend utile pour les trouver. Cependant, il existe quelques autres raisons pour lesquelles os.stat() pourrait os.stat() une exception similaire. Existe-t-il un moyen plus précis de détecter les symlinks rompus avec Python sous Linux?

Un dicton Python commun est qu’il est plus facile de demander le pardon que la permission. Bien que je ne sois pas fan de cette déclaration dans la vraie vie, elle s’applique dans de nombreux cas. Habituellement, vous voulez éviter le code qui enchaîne deux appels système sur le même fichier, car vous ne savez jamais ce qui va arriver au fichier entre vos deux appels dans votre code.

Une erreur typique consiste à écrire quelque chose comme :

 if os.path.exists(path): os.unlink(path) 

Le second appel (os.unlink) peut échouer si quelque chose l’a supprimé après votre test if, déclencher une exception et arrêter l’exécution du rest de votre fonction. (Vous pourriez penser que cela ne se produit pas dans la vraie vie, mais nous avons juste pêché un autre bug de notre code base la semaine dernière – et c’était le genre de bogue qui laissait quelques programmeurs se gratter la tête et réclamer “Heisenbug” derniers mois)

Donc, dans votre cas particulier, je le ferais probablement:

 try: os.stat(path) except OSError, e: if e.errno == errno.ENOENT: print 'path %s does not exist or is a broken symlink' % path else: raise e 

La gêne ici est que stat retourne le même code d’erreur pour un lien symbolique qui n’est tout simplement pas là et un lien symbolique cassé.

Donc, je suppose que vous n’avez pas d’autre choix que de briser l’atomicité et de faire quelque chose comme

 if not os.path.exists(os.readlink(path)): print 'path %s is a broken symlink' % path 

os.lstat () peut être utile. Si lstat () réussit et que stat () échoue, alors c’est probablement un lien cassé.

Ce n’est pas atomique mais ça marche.

os.path.islink(filename) and not os.path.exists(filename)

En effet par RTFM (en lisant le manuel fantastique) on voit

os.path.exists (chemin)

Renvoie True si le chemin fait référence à un chemin existant. Renvoie False pour les liens symboliques rompus.

Il dit aussi:

Sur certaines plates-formes, cette fonction peut renvoyer False si l’autorisation d’exécuter os.stat () sur le fichier demandé n’est pas autorisée, même si le chemin existe physiquement.

Donc, si vous vous inquiétez des permissions, vous devriez append d’autres clauses.

Puis-je mentionner le test des liens durs sans python? / bin / test a la condition FILE1 -ef FILE2 qui est vraie lorsque les fichiers partagent un inode.

Par conséquent, quelque chose comme find . -type f -exec test \{} -ef /path/to/file \; -print find . -type f -exec test \{} -ef /path/to/file \; -print find . -type f -exec test \{} -ef /path/to/file \; -print fonctionne pour les tests de liens durs vers un fichier spécifique.

Ce qui m’amène à lire man test et les mentions de -L et -h qui fonctionnent toutes deux sur un fichier et renvoient true si ce fichier est un lien symbolique, mais cela ne vous dit pas si la cible est manquante.

J’ai trouvé que head -0 FILE1 retournerait un code de sortie de 0 si le fichier peut être ouvert et un 1 s’il ne peut pas, ce qui, dans le cas d’un lien symbolique vers un fichier normal, fonctionne comme un test pour savoir si sa cible peut être lis.

os.path

Vous pouvez essayer d’utiliser realpath () pour obtenir ce que le lien symbolique pointe, puis essayer de déterminer s’il s’agit d’un fichier valide utilisant le fichier.

(Je ne suis pas capable de l’essayer pour le moment, alors vous devrez jouer avec et voir ce que vous obtenez)

Je ne suis pas un python mais ça ressemble à os.readlink ()? La logique que j’utiliserais dans perl est d’utiliser readlink () pour trouver la cible et l’utilisation stat () pour tester si la cible existe.

Edit: J’ai tapé un peu de perl que demos readlink. Je crois que les statistiques et readlink de perl et os.stat () et os.readlink () de python sont tous deux des wrappers pour les appels système, donc cela devrait traduire un code de preuve de concept raisonnable:

 wembley 0 /home/jj33/swap > cat p my $f = shift; while (my $l = readlink($f)) { print "$f -> $l\n"; $f = $l; } if (!-e $f) { print "$f doesn't exist\n"; } wembley 0 /home/jj33/swap > ls -l | grep ^l lrwxrwxrwx 1 jj33 users 17 Aug 21 14:30 link -> non-existant-file lrwxrwxrwx 1 root users 31 Oct 10 2007 mm -> ../systems/mm/20071009-rewrite// lrwxrwxrwx 1 jj33 users 2 Aug 21 14:34 mmm -> mm/ wembley 0 /home/jj33/swap > perl p mm mm -> ../systems/mm/20071009-rewrite/ wembley 0 /home/jj33/swap > perl p mmm mmm -> mm mm -> ../systems/mm/20071009-rewrite/ wembley 0 /home/jj33/swap > perl p link link -> non-existant-file non-existant-file doesn't exist wembley 0 /home/jj33/swap > 

J’ai eu un problème similaire: comment attraper des liens symboliques cassés, même lorsqu’ils se produisent dans un répertoire parent? Je voulais aussi tous les enregistrer (dans une application traitant un assez grand nombre de fichiers), mais sans trop de répétitions.

Voici ce que j’ai imaginé, y compris les tests unitaires.

fileutil.py :

 import os from functools import lru_cache import logging logger = logging.getLogger(__name__) @lru_cache(maxsize=2000) def check_broken_link(filename): """ Check for broken symlinks, either at the file level, or in the hierarchy of parent dirs. If it finds a broken link, an ERROR message is logged. The function is cached, so that the same error messages are not repeated. Args: filename: file to check Returns: True if the file (or one of its parents) is a broken symlink. False otherwise (ie either it exists or not, but no element on its path is a broken link). """ if os.path.isfile(filename) or os.path.isdir(filename): return False if os.path.islink(filename): # there is a symlink, but it is dead (pointing nowhere) link = os.readlink(filename) logger.error('broken symlink: {} -> {}'.format(filename, link)) return True # ok, we have either: # 1. a filename that simply doesn't exist (but the containing dir does exist), or # 2. a broken link in some parent dir parent = os.path.dirname(filename) if parent == filename: # reached root return False return check_broken_link(parent) 

Tests unitaires:

 import logging import shutil import tempfile import os import unittest from ..util import fileutil class TestFile(unittest.TestCase): def _mkdir(self, path, create=True): d = os.path.join(self.test_dir, path) if create: os.makedirs(d, exist_ok=True) return d def _mkfile(self, path, create=True): f = os.path.join(self.test_dir, path) if create: d = os.path.dirname(f) os.makedirs(d, exist_ok=True) with open(f, mode='w') as fp: fp.write('hello') return f def _mklink(self, target, path): f = os.path.join(self.test_dir, path) d = os.path.dirname(f) os.makedirs(d, exist_ok=True) os.symlink(target, f) return f def setUp(self): # reset the lru_cache of check_broken_link fileutil.check_broken_link.cache_clear() # create a temporary directory for our tests self.test_dir = tempfile.mkdtemp() # create a small tree of dirs, files, and symlinks self._mkfile('a/b/c/foo.txt') self._mklink('b', 'a/x') self._mklink('b/c/foo.txt', 'a/f') self._mklink('../..', 'a/b/c/y') self._mklink('not_exist.txt', 'a/b/c/bad_link.txt') bad_path = self._mkfile('a/XXX/c/foo.txt', create=False) self._mklink(bad_path, 'a/b/c/bad_path.txt') self._mklink('not_a_dir', 'a/bad_dir') def tearDown(self): # Remove the directory after the test shutil.rmtree(self.test_dir) def catch_check_broken_link(self, expected_errors, expected_result, path): filename = self._mkfile(path, create=False) with self.assertLogs(level='ERROR') as cm: result = fileutil.check_broken_link(filename) logging.critical('nothing') # sortingck: emit one extra message, so the with assertLogs block doesn't fail error_logs = [r for r in cm.records if r.levelname is 'ERROR'] actual_errors = len(error_logs) self.assertEqual(expected_result, result, msg=path) self.assertEqual(expected_errors, actual_errors, msg=path) def test_check_broken_link_exists(self): self.catch_check_broken_link(0, False, 'a/b/c/foo.txt') self.catch_check_broken_link(0, False, 'a/x/c/foo.txt') self.catch_check_broken_link(0, False, 'a/f') self.catch_check_broken_link(0, False, 'a/b/c/y/b/c/y/b/c/foo.txt') def test_check_broken_link_notfound(self): self.catch_check_broken_link(0, False, 'a/b/c/not_found.txt') def test_check_broken_link_badlink(self): self.catch_check_broken_link(1, True, 'a/b/c/bad_link.txt') self.catch_check_broken_link(0, True, 'a/b/c/bad_link.txt') def test_check_broken_link_badpath(self): self.catch_check_broken_link(1, True, 'a/b/c/bad_path.txt') self.catch_check_broken_link(0, True, 'a/b/c/bad_path.txt') def test_check_broken_link_badparent(self): self.catch_check_broken_link(1, True, 'a/bad_dir/c/foo.txt') self.catch_check_broken_link(0, True, 'a/bad_dir/c/foo.txt') # bad link, but shouldn't log a new error: self.catch_check_broken_link(0, True, 'a/bad_dir/c') # bad link, but shouldn't log a new error: self.catch_check_broken_link(0, True, 'a/bad_dir') if __name__ == '__main__': unittest.main()