如何在Perl中归一化路径?(不需要检查文件系统)

8
我需要Perl中类似于Python的os.path.normpath()函数:
通过折叠冗余分隔符和上级引用来规范化路径名,以便A//B、A/B/、A/./B和A/foo/../B都变成A/B。这种字符串操作可能会改变包含符号链接的路径的含义。例如,我想将'/a/../b/./c//d'转换为/b/c/d。
我正在处理的路径不代表本地文件树中的实际目录。没有涉及符号链接。因此,普通的字符串操作就可以胜任。
我尝试了Cwd::abs_path和File::Spec,但它们不能满足我的要求。
my $path = '/a/../b/./c//d';

File::Spec->canonpath($path);
File::Spec->rel2abs($path, '/');
# Both return '/a/../b/c/d'.
# They don't remove '..' because it might change
# the meaning of the path in case of symlinks.

Cwd::abs_path($path);
# Returns undef.
# This checks for the path in the filesystem, which I don't want.

Cwd::fast_abs_path($path);
# Gives an error: No such file or directory

可能相关的链接:

6个回答

4

考虑到File :: Spec几乎符合我的需求,我最终编写了一个函数,从 File :: Spec->canonpath()中删除 ../ 。 包括测试在内的完整代码可以在GitHub Gist上找到。

use File::Spec;

sub path_normalize_by_string_manipulation {
    my $path = shift;

    # canonpath does string manipulation, but does not remove "..".
    my $ret = File::Spec->canonpath($path);

    # Let's remove ".." by using a regex.
    while ($ret =~ s{
        (^|/)              # Either the beginning of the string, or a slash, save as $1
        (                  # Followed by one of these:
            [^/]|          #  * Any one character (except slash, obviously)
            [^./][^/]|     #  * Two characters where
            [^/][^./]|     #    they are not ".."
            [^/][^/][^/]+  #  * Three or more characters
        )                  # Followed by:
        /\.\./             # "/", followed by "../"
        }{$1}x
    ) {
        # Repeat this substitution until not possible anymore.
    }

    # Re-adding the trailing slash, if needed.
    if ($path =~ m!/$! && $ret !~ m!/$!) {
        $ret .= '/';
    }

    return $ret;
}

2

修复Tom van der Woerdt的代码:

foreach my $path ("/a/b/c/d/../../../e" , "/a/../b/./c//d") {
    my $absolute = $path =~ m!^/!;
    my @c= reverse split m@/@, $path;
    my @c_new;
    while (@c) {
        my $component= shift @c;
        next unless length($component);
        if ($component eq ".") { next; }
        if ($component eq "..") { 
            my $i=0;
            while ($c[$i] && $c[$i] =~ m/^\.{1,2}$/) {
                $i++
            }
            if ($i > $#c) {
                push @c_new, $component unless $absolute;
            } else {
                splice(@c, $i, 1);
            }
            next 
        }
        push @c_new, $component;
    }
    print "/".join("/", reverse @c_new) ."\n";
}

2

我的使用场景是将文件中的包含路径相对于另一个路径进行规范化。例如,我可能有一个文件位于“/home/me/dita-ot/plugins/org.oasis-open.dita.v1_3/rng/technicalContent/rng/concept.rng”,该文件包含以下文件相对于自身

<include href="../../base/rng/topicMod.rng"/>

我需要获取被包含文件的绝对路径。(包含文件路径可能是绝对路径或相对路径。)

Path::Tiny 看起来很有希望,但我只能使用核心模块。

我尝试过使用 chdir 到包含文件位置,然后使用 File::Spec->rel2abs() 解析路径,但在我的系统上非常慢。

最终,我编写了一个子例程来实现基于字符串的简单方法,以消除 "../" 组件:

#!/usr/bin/perl
use strict;
use warnings;

use Cwd;
use File::Basename;
use File::Spec;

sub adjust_local_path {
 my ($file, $relative_to) = @_;
 return Cwd::realpath($file) if (($relative_to eq '.') || ($file =~ m!^\/!));  # handle the fast cases

 $relative_to = dirname($relative_to) if (-f $relative_to);
 $relative_to = Cwd::realpath($relative_to);
 while ($file =~ s!^\.\./!!) { $relative_to =~ s!/[^/]+$!!; }
 return File::Spec->catdir($relative_to, $file);
}

my $included_file = '/home/chrispy/dita-ot/plugins/org.oasis-open.dita.v1_3/rng/technicalContent/rng/topic.rng';
my $source_file = '.././base/rng/topicMod.rng';
print adjust_local_path($included_file, $source_file)."\n";

脚本的结果是:
$ ./test.pl
/home/me/dita-ot-3.1.3/plugins/org.oasis-open.dita.v1_3/rng/technicalContent/base/rng/topicMod.rng

使用realpath()的好处之一是可以解析符号链接,这正是我所需要的。在上面的例子中,dita-ot/是指向dita-ot-3.1.3/的链接。

您可以提供文件或路径作为第二个参数;如果是文件,则使用该文件的目录路径。(这对于我的目的非常方便。)


这似乎是一个有趣的解决方案,但它在很大程度上依赖于文件系统,而我的初始问题需要纯字符串操作(因为没有涉及到文件)。 - Denilson Sá Maia

1
你提到你尝试了File::Spec,但它没有达到你的要求。这是因为你可能在类Unix系统上使用它,如果你尝试cd到类似path/to/file.txt/..这样的路径,除非path/to/file.txt是一个合法的目录路径,否则它将失败。
然而,在Win32系统上,命令cd path/to/file.txt/..会起作用,只要path/to是一个真实的目录路径,无论file.txt是否是一个真实的子目录。
如果你还没有看出我要说什么,那就是File::Spec模块(除非你在Win32系统上)不会做你想要的事情,但是File::Spec::Win32模块会做你想要的事情。而且很酷的是,File::Spec::Win32应该作为一个标准模块即使在非Win32平台上也是可用的!
这段代码基本上做了你想要的事情:
use strict;
use warnings;
use feature 'say';

use File::Spec::Win32;

my $path = '/a/../b/./c//d';
my $canonpath = File::Spec::Win32->canonpath($path);
say $canonpath;   # This prints:  \b\c\d

很不幸,由于我们使用的是Win32版本的File::Spec,所以"\\"被用作目录分隔符(而不是Unix的"/")。只要原始的$path不包含任何"\\",将这些"\\"转换为"/"应该很简单。
如果您的原始$path确实包含合法的"\\"字符,找到一种方法来保留它们(以防止其被转换为"/")也不会太困难。尽管我必须说,如果您的路径实际上包含"\\"字符,那么它们可能已经给您带来了很多麻烦。
由于类Unix系统(包括Win32)据说在其路径名中不允许空字符,保留路径名中的"\\"字符的一种解决方案是首先将它们转换为空字节,然后调用File::Spec::Win32->canonpath( ... );,最后再将空字节转换回"\\"字符。这可以非常直接地完成,无需循环。
use File::Spec::Win32;

my $path = '/a/../b/./c//d';
$path =~ s[\\][\0]g;   # Converts backslashes to null bytes.
$path = File::Spec::Win32->canonpath($path);
$path =~ s[\\][/]g;   # Converts \ to / characters.
$path =~ s[\0][\\]g;   # Converts null bytes back to backslashes.
# $path is now set to:  /b/c/d

1
谁会想到Win32模块解决了一个非Windows问题呢!这真让人惊讶,我甚至会说它很晦涩。但它确实起作用。我已经将你的解决方案添加到我的测试中,最初失败了一半的测试,但修复很容易(只需保留尾部斜杠,如果需要的话)。在那个小修复之后,它通过了所有的测试!而且它在Linux上也起作用,即使文件系统中没有这样的目录。 - Denilson Sá Maia

0

如果您从右到左处理路径,则从路径中删除“。”和“..”非常简单:

my $path= "/a/../b/./c//d";
my @c= reverse split m@/@, $path;
my @c_new;
while (@c) {
    my $component= shift @c;
    next unless length($component);
    if ($component eq ".") { next; }
    if ($component eq "..") { shift @c; next }
    push @c_new, $component;
}
say "/".join("/", reverse @c_new);

(假设路径以 / 开头)

请注意,这违反了 UNIX 路径名解析 标准,特别是其中的一部分:

以两个连续斜杠开头的路径名可能会以实现定义的方式进行解释,尽管超过两个前导斜杠将被视为单个斜杠。


2
我认为当你运行 /a/b/c/d/../../../e 这样的命令时,它会失败。 - Tim Angus
2
@TimAngus 你说得对,发现得好!我发布了修复版本 :) - Georg Mavridis

0

Path::Tiny 模块正是做这件事情的:

use strict;
use warnings;
use 5.010;

use Path::Tiny;
say path('/a/../b/./c//d');

输出:

/b/c/d

2
不是我的问题。Path::Tiny 看起来的行为与 File::Spec 完全相同:/a/../b/c/d - Denilson Sá Maia

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接