使用Python查找错误的符号链接

如果我在一个破损的symlink上调用os.stat()python会抛出一个OSErrorexception。 这使它find它们很有用。 但是, os.stat()可能会抛出类似的exception。 有没有更确切的方式来检测在Linux下与Python错误的symlinks

一个常见的Python的说法是,要求宽恕比允许要容易。 虽然在现实生活中我并不是这个陈述的忠实粉丝,但它确实适用于很多情况。 通常你想避免在同一个文件上链接两个系统调用的代码,因为你永远不知道在你的代码中的两个调用之间的文件会发生什么。

一个典型的错误是写下类似的东西

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

第二个调用(os.unlink)可能会失败,如果在你的if测试之后还有其他东西被删除了,引发一个异常,并停止执行其他的函数。 (你可能会认为这在现实生活中并没有发生,但是我们上周已经从我们的代码库中发现了另外一个类似的错误 – 那就是那种让程序员搔头并声称“Heisenbug”最近几个月)

所以,在你的具体情况下,我可能会这样做:

 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 

这里的烦恼是stat返回一个符号链接相同的错误代码,只是不存在和一个错误的符号链接。

所以,我想你别无选择,只能打破原子性,做一些类似的事情

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

os.lstat()可能会有所帮助。 如果lstat()成功并且stat()失败,那么它可能是一个断开的链接。

这不是原子,但它的工作原理。

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

事实上,通过RTFM (阅读精彩的手册),我们看到

os.path.exists(路径)

如果路径指向现有路径,则返回True。 对于错误的符号链接返回False。

它还说:

在某些平台上,如果在所请求的文件上没有授予权限来执行os.stat(),则该函数可能返回False,即使路径物理上存在。

所以,如果你担心权限,你应该添加其他子句。

我可以提到没有Python的硬链接测试? / bin / test具有文件共享inode时FILE1 -ef FILE2条件。

因此,像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 find . -type f -exec test \{} -ef /path/to/file \; -print可用于对特定文件进行硬链接测试。

这使我阅读man test和提及-L-h ,这两个文件都在一个文件上工作,如果该文件是符号链接,则返回true,但不会告诉您目标是否丢失。

我发现如果文件可以打开,文件head -0 FILE1会返回一个退出代码0如果不能打开文件,则返回1 ,在符号链接到常规文件的情况下,它可以作为测试目标是否可以读。

os.path中

您可以尝试使用realpath()来获取符号链接指向的内容,然后尝试使用is file来确定它是否是有效的文件。

(我目前无法尝试,所以你必须玩弄它,看看你得到什么)

我不是一个Python的家伙,但它看起来像os.readlink()? 我将在perl中使用的逻辑是使用readlink()来查找目标,并使用stat()来测试目标是否存在。

编辑:我抨击了一些演示readlink perl。 我相信perl的stat和readlink和python的os.stat()和os.readlink()都是系统调用的包装,所以这应该转化为概念代码的证明:

 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 > 

我有一个类似的问题:即使发生在某些父目录中,如何捕获损坏的符号链接? 我也想记录所有这些(在处理大量文件的应用程序中),但是没有太多的重复。

这是我想出来的,包括单元测试。

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) 

单元测试:

 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') # trick: 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()