### ----------------------------------------------------
### -- rak
### -- Licenses: Artistic-2.0
### -- Authors: Elizabeth Mattijsen
### -- File: lib/rak.rakumod
### ----------------------------------------------------
### -- Chunk 1 of 2
# The modules that we need here, with their full identities
use Git::Files:ver<0.0.8+>:auth<zef:lizmat>;    # git-files
use hyperize:ver<0.0.4+>:auth<zef:lizmat>;      # hyperize racify
use paths:ver<10.1+>:auth<zef:lizmat> 'paths';  # paths
use path-utils:ver<0.0.21+>:auth<zef:lizmat>;   # path-*
use Trap:ver<0.0.2+>:auth<zef:lizmat>;          # Trap

# code to convert a path into an object that can do .lines and .slurp
my &ioify = *.IO;

# The classes for matching and not-matching items (that have been added
# because of some context argument having been specified).
my class PairContext is Pair is export {
    method changed(--> False) { }
    method matched(--> False) { }
    method Str(--> Str:D) { self.key ~ ':' ~ self.value }
}
my class PairMatched is PairContext is export {
    method matched(--> True) { }
}

# The special case for matches that resulted in a change
my class PairChanged is PairMatched is export {
    method changed(--> True) { }
}

# Message for humans on STDERR
my sub warn-if-human-on-stdin(--> Nil) {
    note "Reading from STDIN, please enter source and ^D when done:"
      if $*IN.t;
}

# Return a Seq with ~ paths substituted for actual home directory paths
my sub paths-from-file($from) {
    my $home := $*HOME.absolute ~ '/';
    warn-if-human-on-stdin if $from eq '-';
    ($from eq '-'
      ?? $*IN.lines
      !! ioify($from.subst(/^ '~' '/'? /, $home)).lines
    ).map: *.subst(/^ '~' '/'? /, $home)
}

# Return a Map with named arguments for "paths"
my sub paths-arguments(%_) {
    my $dir  := %_<dir>:delete;
    my $file := %_<file>:delete;
    my $follow-symlinks := (%_<recurse-symlinked-dir>:delete) // False;
    my $recurse         := (%_<recurse-unmatched-dir>:delete) // False;
    my $readable-files  := (%_<is-readable>:delete)           // True;
    Map.new: (:$dir, :$file, :$recurse, :$follow-symlinks, :$readable-files);
}

# Obtain paths for given revision control system and specs
my sub uvc-paths($uvc, *@specs) {
    $uvc<> =:= True || $uvc eq 'git'
      ?? git-files @specs
      !! die "Don't know how to select files for '$uvc'";
}

my @temp-files;
END .unlink for @temp-files;

# Check if argument looks like a URL, and if so, fetch it
my sub fetch-if-url(Str:D $target) {
    if $target.contains(/^ \w+ '://' /) {
        my $proc := run 'curl', $target, :out, :err;
        if $proc.out.slurp(:close) -> $contents {
            my $io := $*TMPDIR.add('rak-' ~ $*PID ~ '-' ~ time);
            $io.spurt($contents);
            @temp-files.push: $io;
            $proc.err.slurp(:close);
            # Hide an IO object with the URL hidden in it inside
            # the absolute path of the object, so that all file
            # attribute tests, that require a string, will just
            # work without blowing up because they can't coerce
            # an IO::Path to a native string
            role HideIO { has IO::Path:D $.IO is required }
            $io.absolute but HideIO($io but $target)
        }
        else {
            $proc.err.slurp(:close);
            Empty
        }
    }
}

# Convert a given seq producing paths to a seq producing files
my sub paths-to-files($iterable, $degree, %_) {
    if %_<under-version-control>:delete -> $uvc {
        uvc-paths($uvc, $iterable)
    }
    else {
        my %paths-arguments := paths-arguments(%_);
        $iterable.map: {
            path-exists($_)
              ?? path-is-regular-file($_)
                ?? $_
                !! paths($_, |%paths-arguments).Slip
              !! fetch-if-url($_)
        }
    }
}

# Adapt a sources sequence to apply any property filters specified
my sub make-property-filter($seq is copy, %_) {
    if %_<modified>:delete -> &modified {
        $seq = $seq.map: -> $path {
            modified(path-modified($path)) ?? $path !! Empty
        }
    }
    if %_<created>:delete -> &created {
        $seq = $seq.map: -> $path {
            created(path-created($path)) ?? $path !! Empty
        }
    }
    if %_<accessed>:delete -> &accessed {
        $seq = $seq.map: -> $path {
            accessed(path-accessed($path)) ?? $path !! Empty
        }
    }
    if %_<meta-modified>:delete -> &meta-modified {
        $seq = $seq.map: -> $path {
            meta-modified(path-meta-modified($path)) ?? $path !! Empty
        }
    }
    if %_<filesize>:delete -> &filesize {
        $seq = $seq.map: -> $path {
            filesize(path-filesize($path)) ?? $path !! Empty
        }
    }
    if %_<is-empty>:exists {
        $seq = $seq.map:
          (%_<is-empty>:delete)
            ?? -> $path { path-is-empty($path) ?? $path !! Empty }
            !! -> $path { path-is-empty($path) ?? Empty !! $path }
    }

    if %_<mode>:delete -> &mode {
        $seq = $seq.map: -> $path { mode(path-mode($path)) ?? $path !! Empty }
    }
    if %_<uid>:delete -> &uid {
        $seq = $seq.map: -> $path { uid(path-uid($path)) ?? $path !! Empty }
    }
    if %_<gid>:delete -> &gid {
        $seq = $seq.map: -> $path { gid(path-gid($path)) ?? $path !! Empty }
    }

    if %_<device-number>:delete -> &device-number {
        $seq = $seq.map: -> $path {
            device-number(path-device-number($path)) ?? $path !! Empty
        }
    }
    if %_<inode>:delete -> &inode {
        $seq = $seq.map: -> $path {
            inode(path-inode($path)) ?? $path !! Empty
        }
    }
    if %_<hard-links>:delete -> &hard-links {
        $seq = $seq.map: -> $path {
            hard-links(path-hard-links($path)) ?? $path !! Empty
        }
    }
    if %_<blocks>:delete -> &blocks {
        $seq = $seq.map: -> $path {
            blocks(path-blocks($path)) ?? $path !! Empty
        }
    }

    if %_<is-owned-by-user>:exists {
        $seq = $seq.map:
          (%_<is-owned-by-user>:delete)
            ?? -> $path { path-is-owned-by-user($path) ?? $path !! Empty }
            !! -> $path { path-is-owned-by-user($path) ?? Empty !! $path }
    }
    if %_<is-owned-by-group>:exists {
        $seq = $seq.map:
          (%_<is-owned-by-group>:delete)
            ?? -> $path { path-is-owned-by-group($path) ?? $path !! Empty }
            !! -> $path { path-is-owned-by-group($path) ?? Empty !! $path }
    }

    if %_<is-writable>:exists {
        $seq = $seq.map:
          (%_<is-writable>:delete)
            ?? -> $path { path-is-writable($path) ?? $path !! Empty }
            !! -> $path { path-is-writable($path) ?? Empty !! $path }
    }
    if %_<is-executable>:exists {
        $seq = $seq.map:
          (%_<is-executable>:delete)
            ?? -> $path { path-is-executable($path) ?? $path !! Empty }
            !! -> $path { path-is-executable($path) ?? Empty !! $path }
    }

    if %_<has-setuid>:exists {
        $seq = $seq.map:
          (%_<has-setuid>:delete)
            ?? -> $path { path-has-setuid($path) ?? $path !! Empty }
            !! -> $path { path-has-setuid($path) ?? Empty !! $path }
    }
    if %_<has-setgid>:exists {
        $seq = $seq.map:
          (%_<has-setgid>:delete)
            ?? -> $path { path-has-setgid($path) ?? $path !! Empty }
            !! -> $path { path-has-setgid($path) ?? Empty !! $path }
    }
    if %_<is-sticky>:exists {
        $seq = $seq.map:
          (%_<is-sticky>:delete)
            ?? -> $path { path-is-sticky($path) ?? $path !! Empty }
            !! -> $path { path-is-sticky($path) ?? Empty !! $path }
    }

    if %_<is-owner-readable>:exists {
        $seq = $seq.map:
          (%_<is-owner-readable>:delete)
            ?? -> $path { path-is-owner-readable($path) ?? $path !! Empty }
            !! -> $path { path-is-owner-readable($path) ?? Empty !! $path }
    }
    if %_<is-owner-writable>:exists {
        $seq = $seq.map:
          (%_<is-owner-writable>:delete)
            ?? -> $path { path-is-owner-writable($path) ?? $path !! Empty }
            !! -> $path { path-is-owner-writable($path) ?? Empty !! $path }
    }
    if %_<is-owner-executable>:exists {
        $seq = $seq.map:
          (%_<is-owner-executable>:delete)
            ?? -> $path { path-is-owner-executable($path) ?? $path !! Empty }
            !! -> $path { path-is-owner-executable($path) ?? Empty !! $path }
    }

    if %_<is-group-readable>:exists {
        $seq = $seq.map:
          (%_<is-group-readable>:delete)
            ?? -> $path { path-is-group-readable($path) ?? $path !! Empty }
            !! -> $path { path-is-group-readable($path) ?? Empty !! $path }
    }
    if %_<is-group-writable>:exists {
        $seq = $seq.map:
          (%_<is-group-writable>:delete)
            ?? -> $path { path-is-group-writable($path) ?? $path !! Empty }
            !! -> $path { path-is-group-writable($path) ?? Empty !! $path }
    }
    if %_<is-group-executable>:exists {
        $seq = $seq.map:
          (%_<is-group-executable>:delete)
            ?? -> $path { path-is-group-executable($path) ?? $path !! Empty }
            !! -> $path { path-is-group-executable($path) ?? Empty !! $path }
    }

    if %_<is-symbolic-link>:exists {
        $seq = $seq.map:
          (%_<is-symbolic-link>:delete)
            ?? -> $path { path-is-symbolic-link($path) ?? $path !! Empty }
            !! -> $path { path-is-symbolic-link($path) ?? Empty !! $path }
    }

    if %_<is-world-readable>:exists {
        $seq = $seq.map:
          (%_<is-world-readable>:delete)
            ?? -> $path { path-is-world-readable($path) ?? $path !! Empty }
            !! -> $path { path-is-world-readable($path) ?? Empty !! $path }
    }
    if %_<is-world-writable>:exists {
        $seq = $seq.map:
          (%_<is-world-writable>:delete)
            ?? -> $path { path-is-world-writable($path) ?? $path !! Empty }
            !! -> $path { path-is-world-writable($path) ?? Empty !! $path }
    }
    if %_<is-world-executable>:exists {
        $seq = $seq.map:
          (%_<is-world-executable>:delete)
            ?? -> $path { path-is-world-executable($path) ?? $path !! Empty }
            !! -> $path { path-is-world-executable($path) ?? Empty !! $path }
    }

    if %_<accept>:delete -> &code {
        $seq = $seq.map: -> $path { code(ioify $path) ?? $path !! Empty }
    }

    if %_<deny>:delete -> &code {
        $seq = $seq.map: -> $path { code(ioify $path) ?? Empty !! $path }
    }

    if %_<exec>:delete -> $command {
        $seq = $seq.map: -> $path {
            run($command.subst('$_', $path, :g)) ?? $path !! Empty
        }
    }
    if %_<shell>:delete -> $command {
        $seq = $seq.map: -> $path {
            shell($command.subst('$_', $path, :g)) ?? $path !! Empty
        }
    }

    if %_<is-text>:exists {
        $seq = $seq.map:
          (%_<is-text>:delete)
            ?? -> $path { path-is-text($path) ?? $path !! Empty }
            !! -> $path { path-is-text($path) ?? Empty !! $path }
    }

    if %_<is-moarvm>:exists {
        $seq = $seq.map:
          (%_<is-moarvm>:delete)
            ?? -> $path { path-is-moarvm($path) ?? $path !! Empty }
            !! -> $path { path-is-moarvm($path) ?? Empty !! $path }
    }

    if %_<is-pdf>:exists {
        $seq = $seq.map:
          (%_<is-pdf>:delete)
            ?? -> $path { path-is-pdf($path) ?? $path !! Empty }
            !! -> $path { path-is-pdf($path) ?? Empty !! $path }
    }

    $seq.map: &ioify
}

# Return a matcher Callable for a given pattern.
my sub make-matcher(&pattern, %_) {
    my &matcher := %_<invert-match>:delete
      ?? -> $haystack {
             my $result := pattern($haystack);
             $result =:= True
               ?? False
               !! not-acceptable($result)
                 ?? True
                 !! $result
         }
      !! &pattern;

    my $silently := (%_<silently>:delete)<>;
    if %_<quietly>:delete {
        # the existence of a CONTROL block appears to disallow use of ternaries
        # 2022.07
        if $silently {
            if $silently =:= True || $silently eq 'out,err' | 'err,out' {
                -> $haystack {
                    CONTROL { .resume if CX::Warn.ACCEPTS($_) }
                    Trap(my $*OUT, my $*ERR);
                    matcher($haystack)
                }
            }
            elsif $silently eq 'out' {
                -> $haystack {
                    CONTROL { .resume if CX::Warn.ACCEPTS($_) }
                    Trap(my $*OUT);
                    matcher($haystack)
                }
            }
            elsif $silently eq 'err' {
                -> $haystack {
                    CONTROL { .resume if CX::Warn.ACCEPTS($_) }
                    Trap(my $*ERR);
                    matcher($haystack)
                }
            }
            else {
                die "Unexpected value for --silently: $silently"
            }
        }
        else {
            -> $haystack {
                CONTROL { .resume if CX::Warn.ACCEPTS($_) }
                matcher($haystack)
            }
        }
    }
    elsif $silently {  # and not quietly
        $silently =:= True || $silently eq 'out,err' | 'err,out'
            ?? -> $haystack { Trap(my $*OUT, my $*ERR); matcher($haystack) }
            !! $silently eq 'out'
              ?? -> $haystack { Trap(my $*OUT); matcher($haystack) }
              !! $silently eq 'err'
                ?? -> $haystack { Trap(my $*ERR); matcher($haystack) }
                !! die "Unexpected value for --silently: $silently"
    }
    else {
        &matcher
    }
}

#- runners ---------------------------------------------------------------------

# Not matching
my sub not-acceptable($result is copy) {
    $result := $result<>;
    $result =:= False || $result =:= Empty || $result =:= Nil
}

my sub acceptable($item is raw, $result is raw) {
   $result =:= True || $result eqv $item.value
     ?? PairMatched.new($item.key, $item.value)
     !! PairChanged.new($item.key, $result)
}

# Return a runner Callable for passthru
my sub make-passthru-runner($source, &matcher, $item-number) {
    $item-number
      ?? -> $item {
             my $*SOURCE := $source;
             my $value   := $item.value;
             my $result  := matcher($value);
             not-acceptable($result)
               ?? $item
               !! acceptable($item, $result)
         }
      !! -> $item {
             my $result := matcher($item);
             $result =:= True || not-acceptable($result)
               ?? $item
               !! $result
         }
}

# Return a runner Callable for "also first" context
my sub make-also-first-runner($source, &matcher, $items, $item-number) {
    my int $todo = $items;
    my @first;
    $item-number
      ?? -> $item {
             my $*SOURCE := $source;
             my $value   := $item.value;
             my $result  := matcher($value);
             if not-acceptable($result) {  # no match
                 @first.push($item) if $todo && $todo--;
                 Empty
             }
             else {  # match or something else was produced from match
                 --$todo if $todo;
                 @first.push(acceptable $item, $result).splice.Slip
             }
         }
      !! -> $item {
             my $*SOURCE := $source;
             my $result  := matcher($item);
             if not-acceptable($result) {  # no match
                 @first.push($item) if $todo && $todo--;
                 Empty
             }
             else {  # match or something else was produced from match
                 --$todo if $todo;
                 @first.push(
                   $result =:= True ?? $item !! $result
                 ).splice.Slip
             }
         }
}

# Return a runner Callable for "always first" context
my sub make-always-first-runner($source, &matcher, $items, $item-number) {
    my int $todo = $items;
    $item-number
      ?? -> $item {
             my $*SOURCE := $source;
             my $value   := $item.value;
             my $result  := matcher($value);
             if not-acceptable($result) {  # no match
                 ($todo && $todo--) ?? $item !! Empty
             }
             else {  # match or something else was produced from match
                 --$todo if $todo;
                 acceptable $item, $result
             }
         }
      !! -> $item {
             my $*SOURCE := $source;
             my $result  := matcher($item);
             if not-acceptable($result) {  # no match
                 ($todo && $todo--) ?? $item !! Empty
             }
             else {  # match or something else was produced from match
                 --$todo if $todo;
                 $result =:= True ?? $item !! $result
             }
         }
}

# Return a runner Callable for passthru context
my sub make-passthru-context-runner($source, &matcher, $item-number) {
    my $after;
    my @before;
    $item-number
      ?? -> $item {
             my $*SOURCE := $source;
             my $value   := $item.value;
             my $result  := matcher($value);
             if not-acceptable($result) {  # no match
                 if $after {
                     $item
                 }
                 else {
                     @before.push($item);
                     Empty
                 }
             }
             else {  # match or something else was produced from match
                 $after = True;
                 @before.push(acceptable $item, $result).splice.Slip
             }
         }
      !! -> $item {
             my $*SOURCE := $source;
             my $result  := matcher($item);
             if not-acceptable($result) {  # no match
                 if $after {
                     $item
                 }
                 else {
                     @before.push($item);
                     Empty
                 }
             }
             else {  # match or something else was produced from match
                 $after = True;
                 @before.push(
                   $result =:= True ?? $item !! $result
                 ).splice.Slip
             }
         }
}

# Return a runner Callable for paragraph context around items
my multi sub make-paragraph-context-runner(
  $source, &matcher, $item-number, uint $max-matches
) {
    my uint $matches-seen;
    my $after;
    my @before;
    $item-number
      ?? -> $item {
             my $value := $item.value;
             if $matches-seen == $max-matches {  # enough matches seen
                 $after
                   ?? $value
                     ?? $item
                     !! (last)
                   !! (last)
             }
             else {  # must still try to match
                 my $*SOURCE := $source;
                 my $result  := matcher($value);
                 if not-acceptable($result) {  # no match
                     if $after {
                         if $value {
                             $item
                         }
                         else {
                             $after = False;
                             Empty
                         }
                     }
                     else {
                         $value
                           ?? @before.push($item)
                           !! @before.splice;
                         Empty
                     }
                 }
                 else {  # match or something else was produced from match
                     ++$matches-seen;
                     $after = True;
                     @before.push(acceptable $item, $result).splice.Slip
                 }
             }
         }
      # no item numbers needed
      !! -> $item {
             if $matches-seen == $max-matches {  # enough matches seen
                 $after
                   ?? $item
                     ?? $item
                     !! (last)
                   !! (last)
             }
             else {  # must still try to match
                 my $*SOURCE := $source;
                 my $result  := matcher($item);
                 if not-acceptable($result) {  # no match
                     if $after {
                         if $item {
                             $item
                         }
                         else {
                             $after = False;
                             Empty
                         }
                     }
                     else {
                         $item
                           ?? @before.push($item)
                           !! @before.splice;
                         Empty
                     }
                 }
                 else {  # match or something else was produced from match
                     ++$matches-seen;
                     $after = True;
                     @before.push(
                       $result =:= True ?? $item !! $result
                     ).splice.Slip
                 }
             }
         }
}
my multi sub make-paragraph-context-runner($source, &matcher, $item-number) {
    my $after;
    my @before;
    $item-number
      ?? -> $item {
             my $*SOURCE := $source;
             my $value   := $item.value;
             my $result  := matcher($value);
             if not-acceptable($result) {  # no match
                 if $after {
                     if $value {
                         $item
                     }
                     else {
                         $after = False;
                         Empty
                     }
                 }
                 else {
                     $value
                       ?? @before.push($item)
                       !! @before.splice;
                     Empty
                 }
             }
             else {  # match or something else was produced from match
                 $after = True;
                 @before.push(acceptable $item, $result).splice.Slip
             }
         }
      # no item numbers needed
      !! -> $item {
             my $*SOURCE := $source;
             my $result  := matcher($item);
             if not-acceptable($result) {  # no match
                 if $after {
                     if $item {
                         $item
                     }
                     else {
                         $after = False;
                         Empty
                     }
                 }
                 else {
                     $item
                       ?? @before.push($item)
                       !! @before.splice;
                     Empty
                 }
             }
             else {  # match or something else was produced from match
                 $after = True;
                 @before.push(
                   $result =:= True ?? $item !! $result
                 ).splice.Slip
             }
         }
}

# Return a runner Callable for numeric context around items
my multi sub make-numeric-context-runner(
  $source, &matcher, $item-number, $before, $after, uint $max-matches
) {
    my uint $matches-seen;
    if $before {
        my $todo;
        my @before;
        $item-number
          ?? -> $item {
                 if $matches-seen == $max-matches {  # seen enough matches
                     if $todo {
                         --$todo; $item
                     }
                     else {
                         last
                     }
                 }
                 else {
                     my $*SOURCE := $source;
                     my $value   := $item.value;
                     my $result  := matcher($value);
                     if not-acceptable($result) {  # no match
                         if $todo {
                             --$todo; $item
                         }
                         else {
                             @before.shift if @before.elems == $before;
                             @before.push: $item;
                             Empty
                         }
                     }

                     else {  # match or something was produced from match
                         ++$matches-seen;
                         $todo = $after;
                         @before.push(acceptable $item, $result).splice.Slip
                     }
                 }
             }
          # no item numbers needed
          !! -> $item {
                 if $matches-seen == $max-matches {  # seen enough matches
                     if $todo {
                         --$todo; $item
                     }
                     else {
                         last
                     }
                 }
                 else {
                     my $*SOURCE := $source;
                     my $result  := matcher($item);
                     if not-acceptable($result) {  # no match
                         if $todo {
                             --$todo; $item
                         }
                         else {
                             @before.shift if @before.elems == $before;
                             @before.push: $item;
                             Empty
                         }
                     }

                     else {  # match or something was produced from match
                         ++$matches-seen;
                         $todo = $after;
                         @before.push(
                           $result =:= True ?? $item !! $result
                         ).splice.Slip
                     }
                 }
             }
    }
    else {
        my $todo;
        $item-number
          ?? -> $item {
                 if $matches-seen == $max-matches {  # seen enough matches
                     if $todo {
                         --$todo; $item
                     }
                     else {
                         last
                     }
                 }
                 else {
                     my $*SOURCE := $source;
                     my $value   := $item.value;
                     my $result  := matcher($value);
                     if not-acceptable($result) {  # no match
                         if $todo {
                             --$todo; $item
                         }
                         else {
                             Empty
                         }
                     }
                     else {  # match or something was produced from match
                         ++$matches-seen;
                         $todo = $after;
                         acceptable($item, $result)
                     }
                 }
             }
          # no item numbers needed
          !! -> $item {
                 if $matches-seen == $max-matches {  # seen enough matches
                     if $todo {
                         --$todo; $item
                     }
                     else {
                         last
                     }
                 }
                 else {
                     my $*SOURCE := $source;
                     my $result  := matcher($item);
                     if not-acceptable($result) {  # no match
                         if $todo {
                             --$todo; $item
                         }
                         else {
                             Empty
                         }
                     }
                     else {  # match or something was produced from match
                         ++$matches-seen;
                         $todo = $after;
                         $result =:= True ?? $item !! $result
                     }
                 }
             }
    }
}
my multi sub make-numeric-context-runner(
  $source, &matcher, $item-number, $before, $after
) {
    if $before {
        my $todo;
        my @before;

        $item-number
          ?? -> $item {
                 my $*SOURCE := $source;
                 my $value   := $item.value;
                 my $result  := matcher($value);
                 if not-acceptable($result) {  # no match
                     if $todo {
                         --$todo; $item
                     }
                     else {
                         @before.shift if @before.elems == $before;
                         @before.push: $item;
                         Empty
                     }
                 }
                 else {  # match or something was produced from match
                     $todo = $after;
                     @before.push(acceptable $item, $result).splice.Slip
                 }
             }
          # no item numbers needed
          !! -> $item {
                 my $*SOURCE := $source;
                 my $result  := matcher($item);
                 if not-acceptable($result) {  # no match
                     if $todo {
                         --$todo; $item
                     }
                     else {
                         @before.shift if @before.elems == $before;
                         @before.push: $item;
                         Empty
                     }
                 }
                 else {  # match or something was produced from match
                     $todo = $after;
                     @before.push(
                       $result =:= True ?? $item !! $result
                     ).splice.Slip
                 }
             }
    }
    else {
        my $todo;
        $item-number
          ?? -> $item {
                 my $*SOURCE := $source;
                 my $value   := $item.value;
                 my $result  := matcher($value);
                 if not-acceptable($result) {  # no match
                     if $todo {
                         --$todo; $item
                     }
                     else {
                         Empty
                     }
                 }
                 else {  # match or something was produced from match
                     $todo = $after;
                     acceptable($item, $result)
                 }
             }
          # no item numbers needed
          !! -> $item {
                 my $*SOURCE := $source;
                 my $result  := matcher($item);
                 if not-acceptable($result) {  # no match
                     if $todo {
                         --$todo; $item
                     }
                     else {
                         Empty
                     }
                 }
                 else {  # match or something was produced from match
                     $todo = $after;
                     $result =:= True ?? $item !! $result
                 }
             }
    }
}

# Base case of a runner from a matcher without any context
my multi sub make-runner($source, &matcher, $item-number, uint $max-matches) {
    my uint $matches-seen;
    $item-number
      ?? -> $item {
             last if $matches-seen == $max-matches;

             my $*SOURCE := $source;
             my $value   := $item.value;
             my $result  := matcher($value);
             if not-acceptable($result) {
                 Empty
             }
             else {
                 ++$matches-seen;
                 acceptable($item, $result)
             }
         }
      # no item numbers needed
      !! -> $item {
             last if $matches-seen == $max-matches;

             my $*SOURCE := $source;
             my $result  := matcher($item);
             if not-acceptable($result) {
                 Empty
             }
             else {
                 ++$matches-seen;
                 $result =:= True ?? $item !! $result
             }
         }
}
my multi sub make-runner($source, &matcher, $item-number) {
    $item-number
      ?? -> $item {
             my $*SOURCE := $source;
             my $value   := $item.value;
             my $result  := matcher($value);
             not-acceptable($result)
               ?? Empty
               !! acceptable($item, $result)
         }
      # no item numbers needed
      !! -> $item {
             my $*SOURCE := $source;
             my $result  := matcher($item);
             not-acceptable($result)
               ?? Empty
               !! $result =:= True ?? $item !! $result
         }
}

#- rak -------------------------------------------------------------------------

# Stub the Rak class here
our class Rak { ... }

proto sub rak(|) is export {*}
multi sub rak(&pattern, *%n) {
    rak &pattern, %n
}
multi sub rak(&pattern, %n) {
    # any execution error will be caught and become a return state
    my $CATCH := !(%n<dont-catch>:delete);
    CATCH {
        $CATCH
          ?? (return Rak.new: exception => $_)
          !! .rethrow;
    }

    # Determine how we make IO::Path-like objects
    &ioify = $_ with %n<ioify>:delete;

    # Some settings we always need
    my $batch     := %n<batch>:delete;
    my $degree    := %n<degree>:delete;
    my $enc       := %n<encoding>:delete // 'utf8-c8';

    # Get any of the context flags, and a flag to rule them all
    my $any-context := (my $passthru := %n<passthru>:delete)
      || (my $also-first-context   := %n<also-first>:delete)
      || (my $always-first-context := %n<always-first>:delete)
      || (my $passthru-context     := %n<passthru-context>:delete)
      || (my $context              := %n<context>:delete)
      || (my $before-context       := %n<before-context>:delete)
      || (my $after-context        := %n<after-context>:delete)
      || (my $paragraph-context    := %n<paragraph-context>:delete);

    my sub get-sources-seq() {
        my $seq := do if %n<files-from>:delete -> $files-from {
            paths-from-file($files-from).map: {
                path-exists($_) ?? $_ !! fetch-if-url($_)
            }
        }
        elsif %n<paths-from>:delete -> $paths-from {
            paths-to-files(paths-from-file($paths-from), $degree, %n)
        }
        elsif %n<paths>:delete -> $paths {
            my $home := $*HOME.absolute ~ '/';
            paths-to-files(
              $paths.map(*.subst(/^ '~' '/'? /, $home)),
              $degree,
              %n
            ).&hyperize($batch, $degree)
        }
        elsif %n<under-version-control>:delete -> $uvc {
            uvc-paths($uvc)
        }
        elsif !($*IN.t) {
            return $*IN
        }
        else {
            paths(".", |paths-arguments(%n)).&hyperize($batch, $degree)
        }

        # Step 2. filtering on properties
        make-property-filter($seq, %n);
    }

    # Step 1: sources sequence
    my $sources-seq = %n<sources>:delete // get-sources-seq;

    # sort sources if we want them sorted
    if %n<sort-sources>:delete -> $sort {
        $sources-seq = Bool.ACCEPTS($sort)
          ?? $sources-seq.sort(*.fc)
          !! $sources-seq.sort($sort)
    }

    # Some flags that we need
    my $eager := %n<eager>:delete;
    my $sources-only;
    my $sources-without-only;
    my $unique;
    my $sort;
    my $frequencies;
    my $classify;
    my $categorize;
    my $item-number;
    my $max-matches-per-source;

    if %n<sources-only>:delete {
        $sources-only := True;
        $item-number  := False;
        $max-matches-per-source := 1;
    }
    elsif %n<sources-without-only>:delete {
        $sources-without-only := $sources-only := True;
        $item-number  := False;
        $max-matches-per-source := 1;
    }
    else {
        $max-matches-per-source := %n<max-matches-per-source>:delete;
        if %n<unique>:delete {
            $unique      := $eager := True;
            $item-number := $max-matches-per-source := False;

            $sort := %n<sort>:delete;
