【问题标题】:PHP glob-style matchingPHP glob 样式匹配
【发布时间】:2012-12-17 12:01:54
【问题描述】:

为了简短起见,我编写了一个访问控制系统。

该系统的要求之一是通过将其与模式匹配来检查规范/规范化路径是否可以访问。

首先想到的是PREG,问题是,模式是基于文件的,即类似于glob() 接受的模式。基本上,它只是包含?(匹配任意字符)或*(匹配任意字符)的模式。

所以简单来说,我需要在 PHP 上重新创建 glob() 的匹配功能。

示例代码:

function path_matches($path, $pattern){
    // ... ?
}

path_matches('path/index.php', 'path/*');        // true
path_matches('path2/', 'path/*');                // false
path_matches('path2/test.php', 'path2/*.php');   // true

一种可能的解决方案是将$pattern 转换为正则表达式而不是使用preg_match(),但是还有其他方法吗?

注意:我不能使用正则表达式的原因是模式将由非程序员编写。

【问题讨论】:

  • 如果 glob() 已经存在于 php 中,为什么要重新创建它? php.net/manual/en/function.glob.php
  • 呃,请再看一遍问题。 glob() 适用于实际路径,我需要重新创建其模式匹配功能。我不知道如何在我的案例中使用glob()(在不存在的路径上)。
  • 我的错,看我的回答:)

标签: php path pattern-matching glob


【解决方案1】:

使用fnmatch(),这似乎可以解决问题。

【讨论】:

  • 那会工作,但我需要一个在所有系统上工作,而不仅仅是 POSIX。为尝试 +1。
  • 此功能在 Windows 5.3 起可用
【解决方案2】:

对我来说,转换为正则表达式似乎是最好的解决方案。您需要做的就是将* 转换为.*,将? 转换为.preg_quote。然而,这并不像看起来那么简单,因为就你做事的顺序而言,这是一个先有鸡还是先有蛋的问题。

我不喜欢这个解决方案,但这是我能想到的最好的解决方案:使用正则表达式生成正则表达式。

function path_matches($path, $pattern, $ignoreCase = FALSE) {

  $expr = preg_replace_callback('/[\\\\^$.[\\]|()?*+{}\\-\\/]/', function($matches) {
    switch ($matches[0]) {
      case '*':
        return '.*';
      case '?':
        return '.';
      default:
        return '\\'.$matches[0];
    }
  }, $pattern);

  $expr = '/'.$expr.'/';
  if ($ignoreCase) {
    $expr .= 'i';
  }

  return (bool) preg_match($expr, $path);

}

编辑添加区分大小写选项。

See it working

【讨论】:

  • 我认为这应该是万无一失的。谢谢戴夫。
  • “看它工作”链接会转到一个只显示 PHP 错误的页面。
  • 这真的很不错。但是,它不能正常工作。例如,*.js 被转换为“/.*\\.js/”。这看起来是对的,但可能不是。生成的正则表达式将匹配 abc.jsx 可能不是用户想要的。更糟糕的是,生成的正则表达式会匹配 abc/def.js。
  • 您不需要在生成的正则表达式周围添加^$ 吗? glob 通常锚定在开头和结尾,而正则表达式不是默认情况下。
  • 必须在生成的正则表达式周围添加^$ 并修改'\*': return '[^\\/]*'; 的大小写以避免下降到子文件夹。
【解决方案3】:

PHP 中已经有一个函数,自 PHP 4.3.0 起包含在内。

fnmatch() 检查传递的字符串是否与给定的 shell 通配符模式匹配。

【讨论】:

    【解决方案4】:

    来自 glob() 的 PHP 文档。无论如何,我认为 preg_match 是最好的解决方案。

    http://php.net/manual/en/function.glob.php

    <?php   
    function match_wildcard( $wildcard_pattern, $haystack ) {
       $regex = str_replace(
         array("\*", "\?"), // wildcard chars
         array('.*','.'),   // regexp chars
         preg_quote($wildcard_pattern)
       );
    
       return preg_match('/^'.$regex.'$/is', $haystack);
    }
    
    $test = "foobar and blob\netc.";
    var_dump(
        match_wildcard('foo*', $test),      // TRUE
        match_wildcard('bar*', $test),      // FALSE
        match_wildcard('*bar*', $test),     // TRUE
        match_wildcard('**blob**', $test),  // TRUE
        match_wildcard('*a?d*', $test),     // TRUE
        match_wildcard('*etc**', $test)     // TRUE
    );
    ?>
    

    【讨论】:

    • 是的,我考虑过str_replace() 的方法,但我仍然无法完全确定它是否在所有情况下都安全。您还需要将preg_match() 的返回值转换为布尔值,但这是一个次要考虑。
    • 这就是 preg_quote 的用途。它将使整个字符串正则表达式安全。然后它只是使 * 和 ?又不安全了。所以,是的,它做你想做的,不能被滥用。
    • 不,我知道,我不喜欢 \*\? 的后续替换,特别是因为主题字符串/模式字符串可能合法地包含反斜杠。另外,我刚刚注意到,您没有指定/ 的分隔符。
    • 我认为戴夫的担忧是合理的。如果有更明确的方法,我更喜欢这个而不是更复杂的正则表达式。
    • 你总是可以用 [a-zA-Z0-9\] 之类的东西改变 *
    【解决方案5】:

    我认为这应该适用于将 glob-patterns 转换为 regex-patterns:

    function glob2regex($globPatt) {
        return '/'.preg_replace_callback('/./u', function($m) {
            switch($m[0]) {
                case '*': return '.*';
                case '?': return '.';
            }
            return preg_quote($m[0],'/');
        }, $globPatt).'\z/AsS';
    }
    

    如果您想防止 * 匹配目录名称,您可能希望使用 [^\\/]* 代替 *

    【讨论】: