### ----------------------------------------------------
### -- App::Rak
### -- Licenses: Artistic-2.0
### -- Authors: Elizabeth Mattijsen
### -- File: lib/App/Rak.rakumod
### ----------------------------------------------------
### -- Chunk 4 of 4
my sub action-json-per-line(--> Nil) {
    meh-for 'json-per-line', <csv modify>;

    prepare-needle;
    %filesystem<file> //= codify-extensions %exts<#jsonl>
      unless $reading-from-stdin;
    move-filesystem-options-to-rak;
    move-result-options-to-rak;
    %rak<omit-item-number> := True
      unless %rak<unique frequencies omit-item-number>:k;

    $enc := %rak<encoding>:delete // 'utf8-c8';

    activate-output-options;
    run-rak;
    rak-results;
    rak-stats;
}

my sub action-list-custom-options(--> Nil) {
    meh-for 'list-custom-options', <filesystem listing modify csv>;

    activate-output-options;
    if %config {
        my @with;
        my @without;

        # find the ones with and without description
        for %config.sort(*.key.fc) -> (:$key, :value(@args)) {
            my $description;
            my @nodesc = @args.map: {
                if .key eq 'description' {
                    $description := .value;
                    Empty
                }
                else {
                    $_
                }
            }
            $description
              ?? (@with.push: Pair.new: $key, Pair.new: $description, @nodesc)
              !! (@without.push: Pair.new: $key, @args)
        }

        if @with {
            for @with -> (:key($option), :value($_)) {
                say "&o($option): $_.key()";
                say "  &as-cli-arguments(.value)";
            }
        }
        if @without {
            say "\nOther options without description:" if @with;
            my $format := '%' ~ (@without.map(*.key.chars).max + 2) ~ "s: %s\n";
            for @without -> (:key($option), :value(@valid)) {
                printf $format, o($option), as-cli-arguments(@valid);
            }
        }
    }

    else {
        say "No custom options where found in '$config-file'";
    }
}

my sub action-list-known-extensions(--> Nil) {
    meh-for 'list-known-extensions', <filesystem listing modify csv>;

    activate-output-options;
    printf("%9s: %s\n", .key, .value.map({$_ || '(none)'}).Str)
      for %exts.sort(*.key);
}

my sub action-modify-files(--> Nil) {
    meh-for 'modify-files', <output-file pager listing csv>;

    prepare-needle;
    move-filesystem-options-to-rak;

    my $dryrun := %modify<dryrun>:delete;
    my $backup  = %modify<backup>:delete;
    $backup = ".bak" if $backup<> =:= True;
    $backup = ".$backup" if $backup && !$backup.starts-with('.');

    my constant no-changes =
      "\n*** no changes where made because of --dryrun ***";

    %rak<sort-sources> := *.absolute;

    # Producing haystacks per file
    if %rak<produce-one> {
        my @changed-files;
        my int $nr-files-seen;

        my &original-needle := $needle;
        $needle := -> $before {
            my $after := original-needle($before);
            Pair.new:
              $after.WHAT =:= $before.WHAT && !($after eqv $before),
              $after
        }

        %rak<mapper> := -> $io, $matches --> Empty {
            ++$nr-files-seen;

            LAST {
                my int $nr-files-changed = @changed-files.elems;
                my $fb = "Processed $nr-files-seen file&s($nr-files-seen)";
                $fb ~= ", $nr-files-changed file&s($nr-files-changed) changed"
                  if $nr-files-changed;

                if $verbose {
                    $fb ~= "\n";
                    for @changed-files -> $io {
                        $fb ~= "$io.relative()";
                    }
                    $fb ~= no-changes if $dryrun;
                    $fb .= chomp;
                }
                elsif $dryrun {
                    $fb ~= no-changes;
                }

                sayer $fb;
            }

            # Get the result
            $_ := $matches.head;

            # Changes were made
            if .key {
                unless $dryrun {
                    if $backup {
                        $io.spurt(.value)
                          if $io.rename($io.sibling($io.basename ~ $backup));
                    }
                    else {
                        $io.spurt: .value;
                    }
                }
                @changed-files.push: $io;
            }
        }
    }

    # Producing haystacks per line
    elsif %rak<produce-many-pairs> {
        my @changed-files;
        my int $nr-files-seen;
        my int $nr-paragraphs-changed;

        %rak<passthru-context> := %listing<passthru-context>:delete  // True;
        %rak<mapper> := -> $io, @matches --> Empty {
            ++$nr-files-seen;

            LAST {
                my int $nr-files-changed = @changed-files.elems;
                my $fb = "Processed $nr-files-seen file&s($nr-files-seen)";
                $fb ~= ", $nr-files-changed file&s($nr-files-changed) changed"
                  if $nr-files-changed;
                $fb ~= ", $nr-paragraphs-changed line&s($nr-paragraphs-changed) changed"
                  if $nr-paragraphs-changed;

                if $verbose {
                    $fb ~= "\n";
                    for @changed-files -> ($io, $changed) {
                        $fb ~= "$io.relative():";
                        $fb ~= " $changed change&s($changed)"  if $changed;
                        $fb ~= "\n";
                    }
                    $fb .= chomp;
                    $fb ~= no-changes if $dryrun;
                }
                elsif $dryrun {
                    $fb ~= no-changes;
                }

                sayer $fb;
            }

            my int $paragraphs-changed;
            for @matches {
                ++$paragraphs-changed if .changed;
            }
            if $paragraphs-changed {
                unless $dryrun {
                    if $backup {
                        $io.spurt(@matches.map(*.value).join("\n"))
                          if $io.rename($io.sibling($io.basename ~ $backup));
                    }
                    else {
                        $io.spurt: @matches.map(*.value).join("\n");
                    }
                }
                $nr-paragraphs-changed += $paragraphs-changed;
                @changed-files.push: ($io, $paragraphs-changed);
            }
        }
    }

    # Producing haystacks per line
    else {
        my @changed-files;
        my int $nr-files-seen;
        my int $nr-lines-changed;
        my int $nr-lines-removed;

        my $joiner := %rak<with-line-endings> ?? "" !! "\n";
        %rak<passthru-context> := %listing<passthru-context>:delete  // True;
        %rak<mapper> := -> $io, @matches --> Empty {
            ++$nr-files-seen;

            LAST {
                my int $nr-files-changed = @changed-files.elems;
                my $fb = "Processed $nr-files-seen file&s($nr-files-seen)";
                $fb ~= ", $nr-files-changed file&s($nr-files-changed) changed"
                  if $nr-files-changed;
                $fb ~= ", $nr-lines-changed line&s($nr-lines-changed) changed"
                  if $nr-lines-changed;
                $fb ~= ", $nr-lines-removed line&s($nr-lines-removed) removed"
                  if $nr-lines-removed;

                if $verbose {
                    $fb ~= "\n";
                    for @changed-files -> ($io, $changed, $removed) {
                        $fb ~= "$io.relative():";
                        $fb ~= " $changed change&s($changed)"  if $changed;
                        $fb ~= " $removed removal&s($removed)" if $removed;
                        $fb ~= "\n";
                    }
                    $fb .= chomp;
                    $fb ~= no-changes if $dryrun;
                }
                elsif $dryrun {
                    $fb ~= no-changes;
                }

                sayer $fb;
            }

            my int $lines-removed;
            my int $lines-changed;
            my int $index;
            for @matches {
                ++$index;
                if .key - $index -> int $missing {
                    $lines-removed += $missing;
                    $index = .key;
                }
                ++$lines-changed if .changed;
            }
            if $lines-changed || $lines-removed {
                unless $dryrun {
                    if $backup {
                        $io.spurt(@matches.map(*.value).join($joiner)) ~ $joiner
                          if $io.rename($io.sibling($io.basename ~ $backup));
                    }
                    else {
                        $io.spurt: @matches.map(*.value).join($joiner) ~ $joiner;
                    }
                }
                $nr-lines-changed += $lines-changed;
                $nr-lines-removed += $lines-removed;
                @changed-files.push: ($io, $lines-changed, $lines-removed);
            }
        }
    }

    my $*N = 0;
    run-rak(:eagerly);
    rak-stats;
}

my sub handle-mbc(str $what, str $producer, &handler --> Nil) {
    meh-for $what, <modify>;

    prepare-needle;
    %filesystem<is-moarvm> //= True unless $reading-from-stdin;
    move-filesystem-options-to-rak;
    move-result-options-to-rak;
    %rak{$producer} := &handler;

    activate-output-options;
    run-rak;
    rak-results;
    rak-stats;
}

my sub action-mbc(--> Nil) {
    handle-mbc 'mbc', 'produce-one',
      -> $io { $MoarVMBytecode.new($io) }
}

my sub action-mbc-frames(--> Nil) {
    handle-mbc 'mbc-frames', 'produce-many',
      -> $io { $MoarVMBytecode.new($io).frames }
}

my sub action-mbc-strings(--> Nil) {
    handle-mbc 'mbc-strings', 'produce-many',
      -> $io { $MoarVMBytecode.new($io).strings }
}

my sub handle-pdf(str $what, str $producer, &handler --> Nil) {
    meh-for $what, <modify>;

    prepare-needle;
    %filesystem<is-pdf> //= True unless $reading-from-stdin;
    move-filesystem-options-to-rak;
    move-result-options-to-rak;
    %rak{$producer} := &handler;

    activate-output-options;
    run-rak;
    rak-results;
    rak-stats;
}

my sub action-pdf-info(--> Nil) {
    handle-pdf 'pdf-info', 'produce-one',
      -> $file { $PDFExtract.new(:$file).info }
}

my sub action-pdf-per-file(--> Nil) {
    handle-pdf 'pdf-per-file', 'produce-one',
      -> $file { $PDFExtract.new(:$file).text }
}

my sub action-pdf-per-line(--> Nil) {
    handle-pdf 'pdf-per-line', 'produce-many',
      -> $file { $PDFExtract.new(:$file).text.lines }
}

my sub action-per-file(--> Nil) {
    meh-for 'per-file', <csv modify>;

    prepare-needle;
    move-filesystem-options-to-rak;
    move-result-options-to-rak;

    activate-output-options;
    run-rak;
    rak-results;
    rak-stats;
}

my sub action-per-line(--> Nil) {
    meh-for 'per-line', <csv modify>;

    # helper sub for --backtrace and --execute-raku
    my sub produce-result($error) {
        my $context        := %result<context>:delete        // 2;
        my $before-context := %result<before-context>:delete // $context;
        my $after-context  := %result<after-context>:delete  // $context;
        meh-for 'backtrace', <filesystem result>;

        %listing<highlight> := False;
        %listing<trim>      := False if $before-context || $after-context;
        backtrace-files($error,
          :source, :$before-context, :$after-context,
          :in-backtrace(PairMatched), :added-context(PairContext)
        )
    }

    activate-output-options;
    my $pattern := @patterns.head.value;

    if %result<sourcery>:delete {
        meh-for 'sourcery', <filesystem>;

        $pattern     := $pattern.trim;
        my $sourcery := sourcery $pattern;

        # normalize the sourcery results
        my %result;
        for $sourcery -> (:key($file), :value($linenumber)) {
            (%result{$file} // (%result{$file} := [])).push: $linenumber;
        }

        # Decide on whether it's a match by the line number for that file
        my @sources = %result.keys>>.IO;
        my %linenrs = @sources.map: * => 0;
        $needle := -> $ {
            ++%linenrs{$*SOURCE} (elem) %result{$*SOURCE}
        }
        @highlights.push: implicit2explicit sourcery-pattern $pattern;
        %rak<sources> := @sources; # only these files
    }

    elsif %result<backtrace>:delete {
        %listing<whole-line> := True;
        %listing<proximate>  := 1;

        # $pattern here is supposed to be the file with the backtrace
        $rak := Rak.new: result => $reading-from-stdin
          ?? produce-result($*IN.slurp(:close))
          !! @positionals
            ?? meh "Can only specify a single file with a backtrace."
            !! $pattern
              ?? $pattern.IO.e
                ?? produce-result($pattern.IO.slurp)
                !! meh "handling backtrace from file(s) NYI"
              !! meh "Must specify a file with a backtrace.";

        rak-results;
        return;
    }

    elsif %result<execute-raku>:delete -> $raku is copy {
        %listing<whole-line> := True;
        %listing<proximate>  := 1;

        # Executing from a script
        if Bool.ACCEPTS($raku) {
            meh "Must specify name of file with Raku code to execute"
              unless $pattern;

            my $io := $pattern.IO;
            meh "File '$pattern' is not found or not readable"
              unless $io.r;

            my $proc := run $*EXECUTABLE,
              ('--ll-exception' if $verbose), $io, @positionals, :out, :err;
            if $proc.err.slurp(:close) -> $backtrace {
                $rak := Rak.new: result => produce-result($backtrace);
            }
            elsif $proc.out.slurp(:close) -> $output {
                meh $output;
            }
            else {
                meh "No error or output produced";
            }

            rak-results;
            return
        }

        # Executing from literal code or STDIN
        else {
            CATCH {
                my $gist := .gist;
                if $gist.contains('Error while compiling') {
                    meh $gist.subst(/ 'compiling' <( \s+ \S+ /, ':');
                }

                my @result;
                for produce-result($_) {
                    if .key.starts-with('EVAL_') {
                        my int $index = .value.head.key.Int;
                        my int $linenr;
                        @result.push: Pair.new:
                          "CODE",
                          $raku.lines.map({
                            (++$linenr == $index ?? PairMatched !! PairContext)
                              .new: $linenr, $_
                          }).List;
                        last;
                    }
                    else {
                        @result.push: $_;
                    }
                }
                if @result {
                    $rak := Rak.new: :@result;
                    rak-results;
                    return;
                }
                meh $gist;  # some other weird error
            }
            if $raku eq '-' {
                note "Reading from STDIN, please enter code and ^D when done:"
                  unless $reading-from-stdin;
                $raku = $*IN.slurp(:close);
            }

            # Actually run the code
            $raku.EVAL;
            meh "Did not get an execution error, so no backtrace to work with.";
        }
    }

    else {
        prepare-needle;
        move-filesystem-options-to-rak;

        # The default in rak already does the right thing
        %rak<produce-many> := $action
          if $action.defined && !($action<> =:= True);

    }

    move-result-options-to-rak;
    run-rak;
    rak-results;
    rak-stats;
}

my sub action-rename-files(--> Nil) {
    my $dryrun   := %modify<dryrun>:delete;
    my $absolute := %listing<absolute>:delete;
    meh-for 'rename-files', <output-file pager listing modify csv>;

    prepare-needle;
    activate-output-options;
    move-filesystem-options-to-rak;

    # First try to do a "git mv", if that failed, try an ordinary rename
    my sub rename-it($from, $to) {
        my $proc := run <git mv>, $from, $to, :err;
        rename $from, $to if $proc.err.slurp(:close);
    }

    # Set way to stringify paths
    my &stringify := IO::Path.^find_method($absolute
      ?? "absolute"
      !! "relative"
    );

    # Make sure the needle returns an IO::Path object to make it easier
    # for the user should they forget to add an .IO at the end
    my &needle := $needle;
    $needle := -> $old {
        my $new := needle($old);
        Bool.ACCEPTS($new) || $new =:= Empty || $new =:= Nil
          ?? $new
          !! $new.IO
    }

    %rak<find>             := True;
    %rak<omit-item-number> := True;
    %rak<map-all>          := True;
    %rak<old-new>          := True;
    %rak<sort-sources>     := *.absolute;
    %rak<mapper> := -> $, @files --> Empty {
        my @existed;
        my @done;
        for @files {
            my $destination := .value.IO;
            if $destination.e {
                @existed.push: Pair.new: stringify(.key), stringify($destination);
            }
            else {
                rename-it .key, $destination unless $dryrun;
                @done.push: Pair.new: stringify(.key), stringify($destination);
            }
        }
        my str @fb;
        if $verbose {
            if @existed {
                @fb.push: "Refused to rename @existed.elems() file&s(@existed):";
                @fb.push: "  $_.key() -> $_.value()" for @existed;
                @fb.push: "" if @done;
            }
            if @done {
                @fb.push: "Renamed @done.elems() file&s(@done):";
                @fb.push: "  $_.key() -> $_.value()" for @done;
            }
        }
        else {
            @fb.push: "Did not rename @existed.elems() file&s(@existed) because destination already existed."
              if @existed;
            @fb.push: "Renamed @done.elems() file&s(@done)."
              if @done;
        }
        @fb.push: "No files were selected for renaming."
          unless @files;
        @fb.push: "*** no files were renamed because of --dryrun ***"
          if $dryrun and @done;

        sayer @fb.join("\n");
    }

    my $*N = 0;
    run-rak(:eagerly);
    rak-stats;
}

my sub action-unicode(--> Nil) {
    my $count-only := %result<count-only>:delete;
    meh-for 'unicode', <csv modify filesystem result>;

    class Unicodes {
        method lines() {
            (0..0x10FFFF).map: {
                my $uniname := .uniname;
                $uniname
                  unless $uniname.starts-with('<') && $uniname.ends-with('>')
            }
        }
    }

    %rak<sources>          := (Unicodes,);
    %rak<omit-item-number> := True;
    %rak<batch>            := 16384;

    %rak<map-all> := $count-only;
    %rak<mapper>  := $count-only
      ?? -> $, @matches --> Empty {
             sayer @matches
               ?? "Found @matches.elems() match{'es' if @matches > 1}"
               !! 'No matches found'
         }
      !! -> $, @matches {
             '<unicode>' => @matches.map: {
                 my $chr := .uniparse;
                 "$chr.ord.fmt('%5X') $_ $chr"
             }
         }

    $smartcase  := $smartmark  := False;
    $ignorecase := $ignoremark := True;
    prepare-needle;

    run-rak;

    activate-output-options;
    %listing<show-filename> := False unless %listing<show-filename>:exists;
    %listing<trim>          := False unless %listing<trim>:exists;
    rak-results;
    rak-stats;
}

my sub action-version(--> Nil) {
    meh-only('version');

    activate-output-options;
    my proto sub MAIN(|) {*}
    use CLI::Version:ver<0.0.9+>:auth<zef:lizmat> $?DISTRIBUTION, &MAIN, 'long';
    MAIN(:version, :$verbose);
}

my sub action-vimgrep(--> Nil) {
    my $pattern := @patterns.head;

    # helper sub for --backtrace and _-execute-raku
    my sub vimgreppify($error --> Nil) {
        for backtrace-files($error) -> (:key($file), :value(@line)) {
            sayer "$file:$_\::" for @line;
        }
    }

    if %result<sourcery>:delete {
        meh-for 'vimgrep', <output-file pager result filesystem modify csv>;
        sayer "$_.key():$_.value()::" for sourcery $pattern.trim;
        return;
    }

    elsif %result<backtrace>:delete {
        meh-for 'edit', <output-file pager result filesystem modify csv>;

        $reading-from-stdin
          ?? vimgreppify($*IN.slurp(:close))
          !! @positionals
            ?? meh "Can only specify a single file with a backtrace."
            !! $pattern.IO.e
              ?? vimgreppify($pattern.IO.slurp)
              !! meh "handling backtrace from file(s) NYI";
        return;
    }

    elsif %result<execute-raku>:delete -> $raku {
        if Bool.ACCEPTS($raku) {
            meh "Must specify name of file with Raku code to execute"
              unless $pattern;

            my $io := $pattern.IO;
            meh "File '$pattern' is not found or not readable"
              unless $io.r;

            my $proc := run $*EXECUTABLE,
              ('--ll-exception' if $verbose), $io, @positionals, :out, :err;
            if $proc.err.slurp(:close) -> $backtrace {
                vimgreppify($backtrace);
                return;
            }
            elsif $proc.out.slurp(:close) -> $output {
                meh $output;
            }
            else {
                meh "No error or output produced";
            }
        }
        else {
            meh "Can only vimgrep backtraces from actual file";
        }
    }

    meh "Cannot use --vimgrep when reading from STDIN"
      if $reading-from-stdin;

    %rak<max-matches-per-source> := $_
      with %result<max-matches-per-file>:delete;
    meh-for 'vimgrep', <result modify csv>;

    prepare-needle;
    move-filesystem-options-to-rak;

    %rak<mapper> := -> $source, @matches --> Empty {
        my $path   := $source.relative;
        my $target := Regex.ACCEPTS($needle) ?? $needle !! $pattern;

        sayer $path
          ~ ':' ~ .key
          ~ ':' ~ (columns(
                    .value, $target,
                    :$ignorecase, :$smartcase, :$ignoremark, :$smartmark,
                    |(:$type if $type)
                  ).head // "0")
          ~ ':' ~ .value
          for @matches;
    }
    activate-output-options;

    run-rak(:eagerly);
    rak-stats;
}

#-------------------------------------------------------------------------------

# Helper sub to recursively handle named arguments
my sub named($original-name, $original-value, :$recurse = True) {
    my sub is-default($value) {
        $value.starts-with('[') && $value.ends-with(']')
    }

    # Can recurse and we're allowed to
    if $recurse && %config{$original-name} -> @expanded {

        # Got a flag
        if Bool.ACCEPTS($original-value) {
            if $original-value {
                for @expanded -> (:key($name), :$value)  {
                    meh("Must specify a value for -$original-name to satisfy '$name'")
                      if $value eq '!';

                    named(
                      $name,
                      is-default($value)
                        ?? $value.substr(1, *-1)
                        !! $value,
                      :recurse($name ne $original-name)
                    );
                }
            }

            # specified as negation
            else {
                for @expanded -> (:key($name), :$value)  {
                    # only negate Bools, ignore others because negation of
                    # other values doesn't make sense other than it not
                    # being specified
                    named($name, !$value, :recurse($name ne $original-name))
                      if Bool.ACCEPTS($value);
                }
            }
        }

        # Got a value
        else {
            for @expanded -> (:key($name), :$value)  {
                named(
                  $name,
                  $value eq '!' || is-default($value)
                    ?? $original-value
                    !! $value,
                  :recurse($name ne $original-name)
                );
            }
        }
    }

    # Known option
    elsif ::("&option-$original-name") -> &handler {
        handler($original-value);
    }
    else {
        @unexpected.push: Pair.new: $original-name, $original-value;
    }
}

# Find one-line description of given name
my sub description($name) {
    if $name eq 'help' | 'foo' | 'no-foo' {
        ""
    }
    elsif %?RESOURCES<help.txt>.lines.first(/ ' --' $name \W /) -> $line {
        $line.substr(1).split(/ \s+ /, 2).tail
    }
    elsif %config{$name} -> @args {
        @args.map({ .value if .key eq 'description' }).head // ""
    }
    else {
        ""
    }
}

# Quit if unexpected named arguments hash
my sub meh-unexpected() {

    my str @text;
    my str @no-match;
    for @unexpected -> (:key($option), :$value) {
        # Looks like an option from another program
        if %falsies{$option} -> $alias {
            @text.push: "Option --$option is called --$alias with 'rak'.";
        }

        # Direct match of sub-option
        elsif %sub-options{$option} -> $main-option {
            @text.push: "The --$option option only makes sense with --$main-option.";
        }

        # Direct match
        elsif description($option) -> $description {
            @text.push: qq:to/TEXT/.chomp;
The --$option option ($description)
does not make sense in this context.  If you believe this to be incorrect,
please report an issue with https://github.com/lizmat/App-Rak/issues/new .
TEXT
        }

        # There are matches
        else {
            my int $cutoff = ($option.chars / 2).Int;

            if (|@options,|%config.keys).map(-> $after {
                my $distance := StrDistance.new(:before($option), :$after).Int;
                Pair.new($after, $distance) if $distance <= $cutoff
            }).sort(*.value).head(5).List -> @suggestions {

                @text.push: "Regarding unexpected option --$option, did you mean:";
                for @suggestions -> (
                  :key($name), :value($steps)
                ) {
                    my $score := $verbose ?? " ($steps)" !! "";
                    if $name eq 'help' {
                        # noop
                    }
                    elsif description($name) -> $description {
                        @text.push: " --$name: $description?$score";
                    }
                    elsif %sub-options{$name} -> $main {
                        @text.push: " --$name: May need to include --$main then as well?$score";
                    }
                    else {
                        @text.push: " --$name?$score";
                    }
                }
            }
            else {
                @no-match.push: $option;
            }
        }
    }

    # no falsies and no suggestions
    @text.push: "Unexpected option{
        's' if @no-match > 1
    }: @no-match.map({"--$_"})."
      if @no-match;

    @text.push: "Specify --help for an overview of available options.";
    exit note @text.join("\n");
}

# Prepare the executable needle
my sub prepare-needle() {
    $needle := @patterns
      # A full blown needle
      ?? compile-needle
            @patterns,
           :$ignorecase,
           :$smartcase,
           :$ignoremark,
           :$smartmark,
           :@repos,
           :@modules,
           :matches($matches-only)
      # put in a basic noop
      !! &defined;

    if $source-rak {
        @positionals
          ?? meh("Specified path&s(@positionals) '@positionals[]' with --$source-for")
          !! (%rak{$source-rak} := $source);
    }
    elsif @positionals {
        %rak<paths> := @positionals.splice;
    }

    %rak<produce-one> := *.slurp(:enc(%rak<encoding> // 'utf8-c8'))
      if %rak<produce-one><> =:= True;
}

# Return all options as a list of Pairs
my sub as-options() {
    my @options;
    my sub add($name, $value) { @options.push: Pair.new: $name, $value }

    add('pattern', @patterns)            if @patterns;
    add('smartcase', $smartcase)         if $smartcase;
    add('ignorecase', $ignorecase)       if $ignorecase;
    add('ignoremark', $ignoremark)       if $ignoremark;
    add('type', $type)                   if $type;
    add('paths', @positionals.join(',')) if @positionals;
    add($action-for, $action)            if $action-for;

    @options.append: .pairs for
      %global, %filesystem, %csv, %listing, %modify;

    add('verbose', $verbose) if $verbose;
    @options
}

# vim: expandtab shiftwidth=4


