Raku 模块分析 - 并行查找文件

并行查找文件

如果你没有使用 panda 或 zef 安装该模块, 你可以下载到本地:

use v6;

use lib "C:\\Users\\Administrator\\raku-concurrent-file-find\\lib";
use Concurrent::File::Find;

下面来看具体代码:

class X::IO::NotADirectory does X::IO is export {
    has $.path;
    method message {
        "«$.path» is not a directory"
    }
}

class X::IO::CanNotAccess does X::IO is export {
    has $.path;
    method message {
        "Cannot access «$.path»: permission denied"
    }
}

class X::IO::StaleSymlink does X::IO is export {
    has $.path;
    method message {
        "Stale symlink «$.path»"
    }
}

上面的代码自定义了 3 个与 IO 错误相关的类并导出, 每个类中有一个 $.path 属性和 message 方法。如果在查找文件/目录时出现异常则会打印出相关的信息, 每个信息中会包含该文件/目录的绝对路径。

自定义的类都遵守了 X::IO 这个角色(Role), 角色 X::IO 是一个用于 IO 错误相关的通用角色, X::IO 没有提供任何额外的方法, 它遵守 X::OS 角色,

role X::IO does X::OS {}

而 X::OS 角色的定义为:

role X:OS { has $.os-error }

它是由操作系统报告的一些错误触发的所有异常的通用角色(失败的 IO,系统调用,fork,内存分配)。

定义一个参数互斥类并导出。如果传递给 find 方法的参数之间有相互排斥, 则抛出这个异常。

class X::Paramenter::Exclusive is Exception is export {
    has $.type;
    method message {
        "Parameters {$.type} are mutual exclusive"
    }
}

下面来看 find 方法的签名。第一个参数 $dir 是需要用户指定的文件查找的起始路径, $dir 是一个 Str 类型的参数, 在 find 方法中被强制为 IO 类型。$dir 前面的 IO(Str), 表示它既可以是一个字符串, 也可以是一个 IO 路径, 如果传入的是字符串, 那么在该 find 方法中就会被强制为 IO 类型。$dir 后面的 where 从句(从句的内容是一个 Block, 直接执行)是对该参数的约束:该路径必须真实存在(否则抛出异常)、必须是一个目录(否则抛出异常)、路径必须可访问(否则抛出异常)。

where 从句的 Block 中使用了点语法, 即调用了 IO 方法, 调用者是 $_, 这里就是 $dir 了。下面还会见到这种 Block 和点语法的用法。

sub find (
    IO(Str) $dir where {
           ( .IO.e || fail X::IO::DoesNotExist.new(path => .Str ) )
        && ( .IO.d || fail X::IO::NotADirectory.new(path => .Str) )
        && ( .IO.r || fail X::IO::CanNotAccess.new(path => .Str ) )
    },
    :$name, :$exclude, :$exclude-dir, :$include, :$include-dir, :$extension,
    :&return-type = { .IO.Str },
    :$no-thread = False,
    :$file = True, :$directory, :$symlink,
    :$max-depth where { $^a ~~ Int || $^a ~~ ∞ && $^a > 0 } = ∞,
    :$recursive = True, :$follow-symlink = False,
    :$keep-going = True, :$quiet = False
) is export { ... }

find 方法的其余参数都是一个一个的 Pair 了, 它们就像袜子一样, 都是一对儿一对儿的:

副词 等价于
:$name name => $name
:$exclude exclude => $exclude
:$exclude-dir exclude-dir => $exclude-dir
:$include include => $include
:$include-dir include-dir => $include-dir
:$extension extension => $extension
:&return-type return-type => &return-type
:$no-thread = False no-thread => False
:$file = True file => True
:$directory directory => $directory
:$symlink symlink => $symlink
:$max-depth max-depth => $max-depth
:$recursive = True recursive => True
:$follow-symlink = False follow-symlink => False
:$keep-going = True keep-going => True
:$quiet = Flase quiet => False

:$no-thread = False 表示默认开启多线程, :$file = True 表示默认查找文件, :$recursive = True 表示默认开启递归查找, :$keep-going = True 表示遇到异常也会继续查找而不退出, :$quiet = Flase 表示默认静默查找。

有一个参数很奇怪, 它后面还有一个 where 从句, 用以约束最大深度, $max-depth 必须为整型或者它的值无穷大并且为正值

:$max-depth where { $^a ~~ Int || $^a ~~ ∞ && $^a > 0 } = ∞

下面是 find 方法的函数主体:

$*SPEC.dir-sep 为路径分割符, Windows 下为 \, Linux 下为 /。使用 constant 声明了一个常量来保存路径分割符。 &max-depth 是一个可调用的 Block

constant dir-sep = $*SPEC.dir-sep;
my &max-depth = $max-depth < ∞ ?? { .IO.path.split(dir-sep).elems <= $max-depth } !! { True };
my @tests; # 保存文件查找所用的条件测试
my @types; #

下面的代码再次用到了 Block 和主题化变量 $_

@types.append({.f}) if $file;
@types.append({.d}) if $directory;
@types.append({.l}) if $symlink;

@tests.append(@types.any);
@tests.append({.basename.Str ~~ $name}) with $name;

我们往 @types 数组里面追加了一个 Block, 这个数组中的元素不是普通值, 它是一个带有占位符参数($_)的花括号块儿。 {.f} 等价于 {$_.f}。那么这里的 $_ 代表着什么呢? 从这句代码所在的上下文可以知道, find 方法的函数主体中并没有显式的声明 $_ 主题变量, 也没有 forgivenwhere 等能主题化的关键字, 所以, {.f}{.d}{.l} 中的 $_ 来自别的地方。怎么知道它来自哪个地方呢? 我们知道只有当有文件、有目录、有 sysmlink 的时候才把相关的 Block 追加到 @types 数组中。既然数组中每个元素都是 Block, 那么对每个 Block 可以进行调用。我们现在先知道这些。

@types.any 是一个 any Junction, 意思为查找时遇到文件、目录或 symlink 中的任意一种类型都可以保留下来。 with 修饰符的意思是如果定义了具体的文件名, 则在 @tests 数组中追加一个 Block, 这个 Block 检查找到的文件名和要查找的文件名是否匹配, 如果匹配的话这个 Block 的值就为 True, 否则为 Flase。

下面的代码是查找文件时用到的一些测试, exclude-tests 是要排除在外的文件名:

my @exclude-tests;
for $exclude.list -> $exclude {
    @exclude-tests.push({ .Str ~~ $exclude })        if $exclude ~~ Regex;
    @exclude-tests.push({ $exclude.(.IO) })          if $exclude ~~ Callable ^ Regex;
    @exclude-tests.push({ .Str.contains($exclude) }) if $exclude ~~ Str;
}
@tests.append(@exclude-tests.none);

$exclude 来自于传递进来的 Pair 参数, 它可以包含单个值, 也可以包含多个值, 所以使用 .list 用来遍历所有的可能。$exclude 可以是正则表达式、Callable 和普通字符串。

与上面的类似, 下面是要包含在内的文件名。 @include-tests 数组中保存的是要包含在查询结果中的文件, 数组中每一个元素都是一个 Block 每个 Block 接收一个参数作为 $_ 的值, 每个 . 号前面是有一个参数的, 这个会在调用 Block 时传入:

my @include-tests;
for $include.list -> $include {

    @include-tests.push({ .Str ~~ $include })        if $include ~~ Regex;
    @include-tests.push({ $include.(.IO) })          if $include ~~ Callable ^ Regex;
    @include-tests.push({ .Str.contains($include) }) if $include ~~ Str;
}
@tests.append(@include-tests.any) if @include-tests;

文件名后缀与之类似, 可以是正则表达式、Callable ^ Regex 和普通字符串:

my @extension-tests;
for $extension.list -> $test {
    @extension-tests.push({ .extension ~~ $test if .extension }) if $test ~~ Regex;
    @extension-tests.push({ $test.(.extension) })  if $test ~~ Callable ^ Regex;
    @extension-tests.push({ $test eq .extension }) if $test ~~ Str;
}
@tests.append(@extension-tests.any) if @extension-tests;

目录测试, 如果 $follow-symlink 为真则在该路径是目录 && sysmlink && (路径不存在就直接抛出 fail 异常), 然后返回这个目录。

my @dir-tests = $follow-symlink
    ?? { .d && .l && ( !.e && fail X::IO::StaleSymlink.new(:path(.Str)) ); .d }
    !! { .d && ! .l };

排除目录的测试:

my @exclude-dir-tests;
for $exclude-dir.list -> $exclude {
    @exclude-dir-tests.push({ .Str ~~ $exclude })        if $exclude ~~ Regex;
    @exclude-dir-tests.push({ $exclude.(.IO) })          if $exclude ~~ Callable & !Regex;
    @exclude-dir-tests.push({ .Str.contains($exclude) }) if $exclude ~~ Str;
}
@dir-tests.append(@exclude-dir-tests.none);

包含目录的测试:

my @include-dir-tests;
for $include-dir.list -> $include {
    @include-dir-tests.push({ .Str ~~ $include })        if $include ~~ Regex;
    @include-dir-tests.push({ $include.(.IO) })          if $include ~~ Callable & !Regex;
    @include-dir-tests.push({ .Str.contains($include) }) if $include ~~ Str;
}
@dir-tests.append(@include-dir-tests.any) if @include-dir-tests;

下面是并发查找部分:

my $channel = Channel.new;
my &start = -> ( &c ) { c } if $no-thread;
my $promise = start { ... }

for 这个块中, $_ 代表当前找到的文件/文件夹。如果有错误发生(例如禁止访问)并且非安静模式下会打印出有异常的文件/目录的路径并继续,否则重新抛出rethrow这个异常:

for dir($dir) {
    CATCH { default { if $keep-going { warn .Str unless $quiet } else { .rethrow } } }

    # 如果这个文件/目录是link的并且不存在,就抛出异常
    if .IO.l && !.IO.e {
        X::IO::StaleSymlink.new(path=>.Str).throw;
    }
...

调用一个关闭了的 channel 会导致 X::Channel::SendOnClosed. last 不仅会退出当前块, 还会退出当前 for 循环。

CATCH { when X::Channel::SendOnClosed { last } }

@tests».(.IO) 意为对 @tests 中的每一个 Block 元素都执行一次 .(.IO) 调用,参数为 .IO, 即 $_.IO, 语法为 $someBlock.(paramter)all 代表所有测试条件都通过, 即每一个 Block 调用都返回真:

$channel.send(.&return-type) if all @tests».(.IO);

&return-type 是一个 Block, 所以可以用点语法调用, 这个 Block{ .IO.Str }, 所以调用后得到的是路径的字符串表示。

如果所有的目录测试条件通过并且最大深度为真并且递归查找开启, 则对该目录中的每一个元素(文件/目录) 按照存在和文件名排序后再对每一项执行一次当前块(&?BLOCK)

.IO.dir().sort({.e && .f}).map(&?BLOCK) if $recursive && .&max-depth && all @dir-tests».(.IO)

离开的时候关闭 Channel

LEAVE $channel.close;
return $channel.list but role :: { method channel { $channel } };

该模块还提供了一个简单的查找方法:

sub find-simple ( IO(Str) $dir,
    :$keep-going = True,
    :$no-thread = False
) is export {
    my $channel = Channel.new;

    my &start = -> ( &c ) { c } if $no-thread;

    my $promise = start {
        for dir($dir) {
            CATCH { default { if $keep-going { note .Str } else { .rethrow } } }

            if .IO.l && !.IO.e {
                X::IO::StaleSymlink.new(path=>.Str).throw;
            }
            {
                CATCH { when X::Channel::SendOnClosed { last } }
                $channel.send(.IO) if .IO.f; # 这里的 $_ 代表当前查找到的文件
                $channel.send(.IO) if .IO.d; # 这里的 $_ 代表当前查找到的目录
            }
            # 如果目录存在, 默认对该目录执行递归查找
            .IO.dir()».&?BLOCK if .IO.e && .IO.d;
        }
        LEAVE $channel.close unless $channel.closed;
    }

    return $channel.list but role :: { method channel { $channel } };
}

概要

use v6;
use Concurrent::File::Find;

find(%*ENV<HOME>
    , :extension('txt', {.contains('~')}) # ends in .txt or ends in something that contains a ~
    , :exclude('covers') # exclude any path that contains covers, both for files and directories
    , :exclude-dir('.') # exclude any directory-path that contains a .
    , :file # return file paths
    , :!directory # don't return directory paths
    , :symlink # return symlink paths
    , :max-depth(5) # but not deeper then 5 directories deep
    , :follow-symlink # follow symlinks (no loop detection yet)
    , :keep-going # on error (no access, stale symlink, etc.), keep going
    , :quiet # don't report errors on STDERR
).elems.say; # count how many files and symlinks we got

sleep 10;

my @l := find-simple(%*ENV<HOME>, :keep-going, :!no-thread); # binding to avoid eagerness

for @l {
    @l.channel.close if $++ > 5000; # hard-close the channel after 5000 found files
    .say if $++ %% 100 # print every 100th file
}

描述

例程

sub find

将由后台线程获取到的文件、目录和符号链接作为 Str列表返回。 该列表得到了一个混合到唯一方 channel 中的角色,它能可用于关闭 List 后面的 channel,以中止任何仍在进行的获取。 这是有点靠不住,当底层的 PromiseDESTROY 时可能会产生警告。 有如下所述的各种包括和排他的过滤器选项。 对于任何目录, 在返回的列表中文件被排在目录之前。只有在返回项目后,才可能递归到子目录中。

Matcher

一些参数采用匹配器或一组匹配器。 给定列表时使用的“Junction”类型取决于参数。 因为匹配器 StrRegexCallable 被接受。 除非另有说明,否则 Str 匹配文件名的某一部分并且区分大小写或匹配整个路径。 Regexp 智能匹配 IO::Path.Str 并且 CallableIO::Path 调用。

参数

IO(Str) $dir - 从哪个目录开始, 要么是 IO::Path, 要么是 Str.

:$file = True - 也返回文件

:$directory - 也返回目录

:$symlink - 也返回符号链接

:&return-type = { .IO.Str } - 默认把匹配到的项目转换为 Str。该 block 被馈以 IO::Path 对象.结果原样返回, 而不是由 find 本身使用,你可以在这里疯狂。

:$name - 返回匹配所提供的匹配器的任意目录的任意文件。使用 Str 作为匹配器需要确切的, 区分大小写的匹配。

:$include - 返回任何 IO::Path.basename 匹配所提供的匹配器的文件。使用 Str 作为匹配器需要部分匹配。

:$exclude - 不返回任何匹配了所提供的匹配器的文件。使用 Str 作为匹配器需要部分匹配。

:$include-dir - 返回或下降到匹配了所提供的匹配器的目录。使用 Str 作为匹配器需要部分匹配。

:$exclude-dir - 不返回或下降到匹配了所提供的匹配器的目录。使用 Str 作为匹配器需要部分匹配。.

:$extension - 返回任何匹配 IO::Path.extension 的项目.使用 Str 作为匹配器需要确切的, 区分大小写的匹配。

:$recursive = True - 下降到子目录。

Int :$max-depth = ∞ - 尽可能深地下降到子目录中。

:$follow-symlink = False - 跟随符号链接. 还没有循环检测.

:$keep-going = True - 发生错误时 (拒绝访问, 陈旧的符号链接, 等等.) 继续但是在标准错误 $*ERR 上输出警告。

:$quiet = False - 配合 $keep-going, 不输出警告.

:$no-thread = False - 禁止创建 Promise. 用于调试.

sub find-simple

find 一样, 但是没有了过滤器选项, 并且永远是递归的, 跟随现有的符号链接(尚没有循环检测)也没有排序。速度快可能包含少量 bug。它可能抛出 X::IO::StaleSymlink.

参数

IO(Str) $dir - Path as IO::Path or Str at where to start looking for files

:$keep-going = True - 出现错误不停止

:$no-thread = False - 不创建 Promise, 调试时有用

异常

X::IO::NotADirectory does X::IO

尝试获取不是目录的路径的内容

X::IO::CanNotAccess does X::IO

访问文件夹(目录)被操作系统拒绝。

我们的意图是返回或跟随一个确实存在,但没有目标的符号链接,

X::Paramenter::Exclusive is Exception

命名参数一起使用是互斥的。

警告

尚不支持循环检测。 只要有 readlink 和/或 stat 的便携版本,就会添加循环检测。 到那时避免 :follow-symlink 或使用 :max-depth

comments powered by Disqus