如何检查一个目录是不是是另一个目录的子目录

Posted

技术标签:

【中文标题】如何检查一个目录是不是是另一个目录的子目录【英文标题】:How to check whether a directory is a sub directory of another directory如何检查一个目录是否是另一个目录的子目录 【发布时间】:2011-04-18 06:58:58 【问题描述】:

我喜欢用 Python 编写一个模板系统,它允许包含文件。

例如

这是一个模板 您可以使用 safe_include`othertemplate.rst` 安全地包含文件

如您所知,包含文件可能很危险。例如,如果我在允许用户创建自己的模板的 Web 应用程序中使用模板系统,他们可能会执行类似

我想要你的密码:safe_include`/etc/password`

因此,我必须将文件的包含限制为例如位于某个子目录中的文件(例如/home/user/templates

现在的问题是:如何检查/home/user/templates/includes/inc1.rst 是否在/home/user/templates 的子目录中?

下面的代码会工作并且安全吗?

import os.path

def in_directory(file, directory, allow_symlink = False):
    #make both absolute    
    directory = os.path.abspath(directory)
    file = os.path.abspath(file)

    #check whether file is a symbolic link, if yes, return false if they are not allowed
    if not allow_symlink and os.path.islink(file):
        return False

    #return true, if the common prefix of both is equal to directory
    #e.g. /a/b/c/d.rst and directory is /a/b, the common prefix is /a/b
    return os.path.commonprefix([file, directory]) == directory

只要allow_symlink 为False,我认为它应该是安全的。如果用户能够创建此类链接,则允许符号链接当然会使其不安全。

更新 - 解决方案 如果中间目录是符号链接,则上面的代码不起作用。 为了防止这种情况,您必须使用realpath 而不是abspath

更新:在目录后面添加 / 以解决 commonprefix() Reorx 指出的问题。

这也使得allow_symlink 变得不必要,因为符号链接会扩展到它们的真实目的地

import os.path

def in_directory(file, directory):
    #make both absolute    
    directory = os.path.join(os.path.realpath(directory), '')
    file = os.path.realpath(file)

    #return true, if the common prefix of both is equal to directory
    #e.g. /a/b/c/d.rst and directory is /a/b, the common prefix is /a/b
    return os.path.commonprefix([file, directory]) == directory

【问题讨论】:

最后一个函数也不行,看这个输入输出:>>> in_directory('/usr/var2/log', '/usr/var') True ',commonprefix不应该被信任。 ***.com/questions/8854421/…的可能重复 最佳答案在问题中...... os.path.commonprefix(..) 被编写为逐个字符操作的原因违反常识,但这确实是documentation for it 所说的。 tl;dr: 对于那些仍然停留在 Python 3.4 上的人,请参阅 jme 的 inefficient pathlib-based answer;对于其他所有人,请参阅 Tom Bull 的 efficient commonpath-based answer。忽略嵌入在这个问题中的答案接受的答案——所有这些都是公然错误的。 【参考方案1】:

许多建议方法的问题

如果您要使用字符串比较或os.path.commonprefix 方法测试目录父级,则这些方法很容易出现类似命名路径或相对路径的错误。例如:

/path/to/files/myfile 将使用许多方法显示为 /path/to/file 的子路径。 /path/to/files/../../myfiles 不会被许多方法显示为/path/myfiles/myfile 的父级。事实上,确实如此。

Rob Dennis 的previous answer 提供了一种比较路径亲子关系而不遇到这些问题的好方法。 Python 3.4 添加了pathlib 模块,该模块可以以更复杂的方式执行此类路径操作,可选择不引用底层操作系统。 jme 在another previous answer 中描述了如何使用pathlib 来准确确定一条路径是否是另一条路径的子路径。如果您不想使用pathlib(不知道为什么,这非常棒),那么 Python 3.5 在os.path 中引入了一种新的基于操作系统的方法,允许您以同样准确和错误的方式执行路径父子检查 -免费方式,代码少得多。

Python 3.5 的新功能

Python 3.5 引入了函数os.path.commonpath。这是一种特定于运行代码的操作系统的方法。您可以通过以下方式使用commonpath来准确确定路径父级:

def path_is_parent(parent_path, child_path):
    # Smooth out relative path names, note: if you are concerned about symbolic links, you should use os.path.realpath too
    parent_path = os.path.abspath(parent_path)
    child_path = os.path.abspath(child_path)

    # Compare the common path of the parent and child path with the common path of just the parent path. Using the commonpath method on just the parent path will regularise the path name in the same way as the comparison that deals with both paths, removing any trailing path separator
    return os.path.commonpath([parent_path]) == os.path.commonpath([parent_path, child_path])

准确的单线

您可以在 Python 3.5 中将所有内容组合成一行 if 语句。这很丑陋,它包含对os.path.abspath 的不必要重复调用,而且它绝对不符合 PEP 8 79 字符行长指南,但如果你喜欢这种东西,这里是:

if os.path.commonpath([os.path.abspath(parent_path_to_test)]) == os.path.commonpath([os.path.abspath(parent_path_to_test), os.path.abspath(child_path_to_test)]):
    # Yes, the child path is under the parent path

Python 3.9 的新功能

pathlibPurePath 上有一个名为is_relative_to 的新方法,它直接执行此功能。如果您需要了解如何使用它,可以阅读the python documentation on how is_relative_to works。或者您可以查看my other answer 以获得更完整的使用说明。

【讨论】:

WTF 是否需要采用 1 条路径的公共路径? imo: abs_parent_path==commonpath([abs_parent_path, abs_child_path]) 应该足够了...... @MiloslavRaus 是正确的。由于os.path.abspathos.path.realpath 函数隐式地切掉尾随目录分隔符,因此无需在单个目录名(例如'/usr' == os.path.abspath('/usr/') == os.path.realpath('/usr/'))上调用os.path.commonpath。在所有其他方面,这个答案仍然是 Python 3.5+ 最有效和最强大的解决方案。 注意:is_relative_to() 方法不能准确处理../。例如:Path('/test1/../test2/myfile.txt').is_relative_to('/test2') == False。如果您的路径中有这些,则需要使用 Accurate one-liner 而不是 New for Python 3.9 建议。【参考方案2】:

Python 3.9 的新功能

pathlibPurePath 上有一个名为is_relative_to 的新方法,它直接执行此功能。你可以阅读the python documentation on how is_relative_to works,或者使用这个例子:

from pathlib import Path

child_path = Path("/path/to/file")
if child_path.is_relative_to("/path"):
    print("/path/to/file is a child of /path") # This prints
if child_path.is_relative_to("/anotherpath"):
    print("/path/to/file is a child of /anotherpath") # This does not print

【讨论】:

【参考方案3】:

在您的启发下,此方法已添加到我的实用程序中:

def is_in_basefolder(path_to_check: PosixPath, basefolder: PosixPath):
        """
        check if a given path is in base folder
        
        parameters:
            path_to_check: a path to match with base folder
            basefolder: the base folder
        """
        path = path_to_check.resolve()
        base = basefolder.resolve()
        
        if path == base:
            return True
        
        if base.stem in path.parts:
            return True
        else:
            return False

【讨论】:

了解如何使用此代码会很有用。也许给我们一个目录结构和一些我们可以看到所需输出的场景。【参考方案4】:

我用下面的函数来解决类似的问题:

def is_subdir(p1, p2):
    """returns true if p1 is p2 or its subdirectory"""
    p1, p2 = os.path.realpath(p1), os.path.realpath(p2)
    return p1 == p2 or p1.startswith(p2+os.sep)

在遇到符号链接问题后,我修改了函数。现在它检查两个路径是否都是目录。

def is_subdir(p1, p2):
    """check if p1 is p2 or its subdirectory
    :param str p1: subdirectory candidate
    :param str p2: parent directory
    :returns True if p1,p2 are directories and p1 is p2 or its subdirectory"""
    if os.path.isdir(p1) and os.path.isdir(p2):
        p1, p2 = os.path.realpath(p1), os.path.realpath(p2)
        return p1 == p2 or p1.startswith(p2+os.sep)
    else:
        return False

【讨论】:

【参考方案5】:
def is_in_directory(filepath, directory):
    return os.path.realpath(filepath).startswith(
        os.path.realpath(directory) + os.sep)

【讨论】:

【参考方案6】:

我喜欢另一个答案中提到的“other_path.parents 中的路径”,因为我是 pathlib 的忠实粉丝,但我觉得这种方法有点重(它为每个父路径创建一个路径实例到路径的根)。在这种情况下 path == other_path 也会失败,而 os.commonpath 在这种情况下会成功。

以下是一种不同的方法,与各种答案中确定的其他方法相比,它有自己的优缺点:

try:
   other_path.relative_to(path)
except ValueError:
   ...no common path...
else:
   ...common path...

这有点冗长,但可以很容易地作为函数添加到应用程序的公共实用程序模块中,甚至可以在启动时将方法添加到 Path 中。

【讨论】:

Oliver 的回答似乎有效,但如果处理潜在的相对路径,在运行 .relative_to() 之前仍需要在两条路径上调用 .resolve()【参考方案7】:

Python 3 的 pathlib 模块通过其 Path.parents 属性使这一点变得简单。例如:

from pathlib import Path

root = Path('/path/to/root')
child = root / 'some' / 'child' / 'dir'
other = Path('/some/other/path')

然后:

>>> root in child.parents
True
>>> other in child.parents
False

【讨论】:

如果您认为路径是其自身的子/父路径(例如,如果您想测试路径是/a/b/c 还是/a/b/c 的子目录),那么您可以使用@ 987654327@ Python 3 的最 Pythonic 方式,pathlib 确实让事情更容易阅读。值得一提的是,如果您在检查 A in B.parents 之前可能需要调用 .resolve() 的相对路径。 root in [child] + [p for p in child.parents] 可以简化为root in (child, *child.parents) 重要的是要提到,首先在child 上调用resolve() 是一项硬性要求。否则,如果孩子在根的任何部分内,它将返回True Python 3.9 将 is_relative_to 添加到直接执行此操作的 `pathlib' 中。【参考方案8】:

基于此处的另一个答案,经过更正,并具有用户友好的名称:

def isA_subdirOfB_orAisB(A, B):
    """It is assumed that A is a directory."""
    relative = os.path.relpath(os.path.realpath(A), 
                               os.path.realpath(B))
    return not (relative == os.pardir
            or  relative.startswith(os.pardir + os.sep))

【讨论】:

【参考方案9】:
def is_subdir(path, directory):
    path = os.path.realpath(path)
    directory = os.path.realpath(directory)
    relative = os.path.relpath(path, directory)
    return not relative.startswith(os.pardir + os.sep)

【讨论】:

os.path.relpath 在我的测试中不包括 os.sep,例如os.path.relpath("/a", "/a/b"). @TorstenBronger 好点。因此,目前的答案是错误的,除非最后一行更改为return not (relative == os.pardir or relative.startswith(os.pardir + os.sep))。顺便说一句,如果我们坚持 proper 子目录,那么也要检查使用(relative == os.curdir) 如果您不在 Python 2 上,请不要使用它,而是使用 pathlib,如其他示例所示。如果您使用的是 Python 2:请移动。【参考方案10】:

所以,我需要这个,并且由于对 commonprefx 的批评,我采取了不同的方式:

def os_path_split_asunder(path, debug=False):
    """
    http://***.com/a/4580931/171094
    """
    parts = []
    while True:
        newpath, tail = os.path.split(path)
        if debug: print repr(path), (newpath, tail)
        if newpath == path:
            assert not tail
            if path: parts.append(path)
            break
        parts.append(tail)
        path = newpath
    parts.reverse()
    return parts


def is_subdirectory(potential_subdirectory, expected_parent_directory):
    """
    Is the first argument a sub-directory of the second argument?

    :param potential_subdirectory:
    :param expected_parent_directory:
    :return: True if the potential_subdirectory is a child of the expected parent directory

    >>> is_subdirectory('/var/test2', '/var/test')
    False
    >>> is_subdirectory('/var/test', '/var/test2')
    False
    >>> is_subdirectory('var/test2', 'var/test')
    False
    >>> is_subdirectory('var/test', 'var/test2')
    False
    >>> is_subdirectory('/var/test/sub', '/var/test')
    True
    >>> is_subdirectory('/var/test', '/var/test/sub')
    False
    >>> is_subdirectory('var/test/sub', 'var/test')
    True
    >>> is_subdirectory('var/test', 'var/test')
    True
    >>> is_subdirectory('var/test', 'var/test/fake_sub/..')
    True
    >>> is_subdirectory('var/test/sub/sub2/sub3/../..', 'var/test')
    True
    >>> is_subdirectory('var/test/sub', 'var/test/fake_sub/..')
    True
    >>> is_subdirectory('var/test', 'var/test/sub')
    False
    """

    def _get_normalized_parts(path):
        return os_path_split_asunder(os.path.realpath(os.path.abspath(os.path.normpath(path))))

    # make absolute and handle symbolic links, split into components
    sub_parts = _get_normalized_parts(potential_subdirectory)
    parent_parts = _get_normalized_parts(expected_parent_directory)

    if len(parent_parts) > len(sub_parts):
        # a parent directory never has more path segments than its child
        return False

    # we expect the zip to end with the short path, which we know to be the parent
    return all(part1==part2 for part1, part2 in zip(sub_parts, parent_parts))

【讨论】:

所有字符串比较/os.path.commonprefix 方法在使用类似名称的路径或相对路径时容易出错。这是确定路径是否是另一个路径的子路径并且不受相同错误影响的更好方法。如果您使用的是 Python 3.5+,则有一个新方法 os.path.commonpath,它适用于更简单、更优雅的方法,该方法同样准确,并且在路径命名相似或指定为相对路径时不会导致错误。我在单独的答案中提供了基本实现。【参考方案11】:

我会根据文件名测试 commonprefix 的结果以获得更好的答案,如下所示:

def is_in_folder(filename, folder='/tmp/'):
    # normalize both parameters
    fn = os.path.normpath(filename)
    fd = os.path.normpath(folder)

    # get common prefix
    commonprefix = os.path.commonprefix([fn, fd])
    if commonprefix == fd:
        # in case they have common prefix, check more:
        sufix_part = fn.replace(fd, '')
        sufix_part = sufix_part.lstrip('/')
        new_file_name = os.path.join(fd, sufix_part)
        if new_file_name == fn:
            return True
        pass
    # for all other, it's False
    return False

【讨论】:

【参考方案12】:

os.path.realpath(path):返回指定文件名的规范路径,排除路径中遇到的任何符号链接(如果操作系统支持的话)。

在目录和子目录名称上使用它,然后检查后者是否以前者开头。

【讨论】:

安全漏洞:例如,参见 Reorx 对 OP 的评论。 实际上,如jgoeders所述,您需要在调用startswith时附加os.sep(如果尚未包含), 'a/b/cde' 以 'a/b/c' 开头,但不是子目录。

以上是关于如何检查一个目录是不是是另一个目录的子目录的主要内容,如果未能解决你的问题,请参考以下文章

如何检查给定目录是不是可访问?

如何在 Shell Scripting 中检查目录是不是为空? [复制]

Java中如何检查文件是不是存在于目录中

如何在linux中检查目录是不是存在。? [复制]

如何检查文件或目录是不是为“系统”文件或目录

Gsutil - 如何使用 Gsutil 检查 GCS 存储桶(子目录)中是不是存在文件