### ----------------------------------------------------
### -- App::Rak
### -- Licenses: Artistic-2.0
### -- Authors: Elizabeth Mattijsen
### -- File: lib/App/Rak.rakumod
### ----------------------------------------------------
### -- Chunk 2 of 4
                                    );
                                    ++$seen if .matched;
                                }
                                last RESULT if $seen == $stop-after;
                                $last-linenr = $linenr;
                            }
                        }

                        # Not grouping
                        elsif $show-filename {
                            for @matches.map({ $_ if .value.elems }) {
                                my uint $linenr = .key;
                                breaker($linenr, $last-linenr);
                                if Slip.ACCEPTS(.value) {
                                    # Can only produce a Slip from a real
                                    # Callable, which cannot have any
                                    # highlighting, so don't bother
                                    sayer "$source:$linenr:$_" for @(.value);
                                    ++$seen;
                                }
                                else {
                                    sayer $source
                                      ~ ':' ~ $linenr
                                      ~ ':' ~ (.matched
                                      ?? line-post-proc stringify(.value)
                                      !! stringify(.value)
                                    );
                                    ++$seen if .matched;
                                }
                                last RESULT if $seen == $stop-after;
                                $last-linenr = $linenr;
                            }
                        }

                        # Not grouping and don't want to know the filename
                        else {
                            for @matches.map({ $_ if .value.elems }) {
                                my uint $linenr = .key;
                                breaker($linenr, $last-linenr);
                                if Slip.ACCEPTS(.value) {
                                    # Can only produce a Slip from a real
                                    # Callable, which cannot have any
                                    # highlighting, so don't bother
                                    sayer "$linenr:$_" for @(.value);
                                    ++$seen;
                                }
                                else {
                                    sayer $linenr
                                      ~ ':' ~ (.matched
                                      ?? line-post-proc stringify(.value)
                                      !! stringify(.value)
                                    );
                                    ++$seen if .matched;
                                }
                                last RESULT if $seen == $stop-after;
                                $last-linenr = $linenr;
                            }
                        }
                    }

                    # No line numbers expected or not showing filename
                    elsif $group-matches || !$show-filename {
                        sayer $source if $show-filename;
                        for @matches {
                            if Slip.ACCEPTS($_) {
                                # Can only produce a Slip from a real
                                # Callable, which cannot have any
                                # highlighting, so don't bother
                                sayer .Str for @$_;
                            }
                            else {
                                sayer line-post-proc stringify $_;
                            }
                            last RESULT if ++$seen == $stop-after;
                        }
                    }

                    # No line numbers and not grouping
                    elsif $show-filename {
                        for @matches {
                            if Slip.ACCEPTS($_) {
                                # Can only produce a Slip from a real
                                # Callable, which cannot have any
                                # highlighting, so don't bother
                                sayer "$source:$_" for @$_;
                            }
                            else {
                                sayer $source
                                  ~ ':' ~ line-post-proc stringify $_;
                            }
                            last RESULT if ++$seen == $stop-after;
                        }
                    }

                    # in case of --always-first without matches
                    ++$seen;
                }
            }

            # looks like frequencies output
            else {
                sayer "&stringify($outer.value):$outer.key()";
                last RESULT if ++$seen == $stop-after;
            }
        }

        # anything else
        else {
            sayer (stringify $outer).substr($source-offset);
            last RESULT if ++$seen == $stop-after;
        }
    }

    if $seen > 1 && $seen == $stop-after {
        my str $what = $writing-to-stdout ?? "showing" !! "producing";
        note "** Stopped $what results after $seen matches **";
    }
}

# Statistics to show
my sub rak-stats(:$count-only --> Nil) {

    # Stop any progress reporting
    $rak.stop-progress;

    if $rak.stats {
        if $count-only && !$verbose {
            note $rak.nr-matches + $rak.nr-changes
              ~ " matches in $rak.nr-sources() files";
        }
        else {
            my str @stats;
            unless $count-only {
                @stats.push: "Statistics for '@patterns.head.value()':";
                my str $bar = '-' x @stats[0].chars;
                @stats.unshift: $bar;
                @stats.push: $bar;
            }
            @stats.push: "    Number of files: $rak.nr-sources()";

            if $rak.nr-items -> $items {
                @stats.push: "    Number of lines: $items";
            }
            if $rak.nr-matches -> $matches {
                @stats.push: "  Number of matches: $matches";
            }
            if $rak.nr-passthrus -> $passthrus {
                @stats.push: "Number of passthrus: $passthrus";
            }
            if $rak.nr-changes -> $changes {
                @stats.push: "  Number of changes: $changes";
            }

            note @stats.join("\n");
        }
    }
    elsif $rak.nr-sources == 0 {
        note "Path specification did not select any files, so there was nothing to match.";
    }
}

#-------------------------------------------------------------------------------
# Helper subroutines for setting up data structures from option handling

# Indicate the action to be performed
my sub set-source(str $name, $value, str $rak = $name --> Nil) {
    meh "Can only have one haystack at a time:\n'--$_' was specified before '--$name'"
      with $source-for;
    $source-for := $name;
    $source     := $value;
    $source-rak := $rak;
}

# Indicate the producer
my sub set-producer(str $name, $value, str $type = 'produce-one' --> Nil) {
    if Bool.ACCEPTS($value) {
        if $value {
            meh qq:to/MEH/ with $producer-for;
Can only have one producer at a time:
'--$_' was specified before '--$name'
MEH
        }
        else {
            return;  # ignore --/producer
        }
    }
    $producer-for := $name;
    %rak{$type}   := $value;
}

# Indicate the action to be performed
my sub set-action(str $name, $value --> Nil) {
    if Bool.ACCEPTS($value) {
        if $value {
            meh qq:to/MEH/ with $action-for;
Can only have one action at a time:
'--$_' was specified before '--$name'
MEH
        }
        else {
            return;  # ignore --/action
        }
    }
    $action-for := $name;
    $action     := $value;
}

# handle global boolean flag
my sub set-global-flag(str $name, $value --> Nil) {
    Bool.ACCEPTS($value)
      ?? $value
        ?? (%global{$name} := True)
        !! (%global{$name}:delete)
      !! meh "'--$name' can only be specified as a flag";
}

# handle rak boolean flag
my sub set-rak-flag(str $name, $value --> Nil) {
    Bool.ACCEPTS($value)
      ?? $value
        ?? (%rak{$name} := True)
        !! (%rak{$name}:delete)
      !! meh "'--$name' can only be specified as a flag";
}

# handle rak integer option
my sub set-rak-Int(str $name, $value --> Nil) {
    my $integer := $value.Int;
    Int.ACCEPTS($integer)
      ?? (%rak{$name} := $integer)
      !! meh "'--$name' can only be an integer value, not '$value'";
}

# handle filesystem boolean flag
my sub set-filesystem-flag(str $name, $value --> Nil) {
    Bool.ACCEPTS($value)
      ?? (%filesystem{$name} := $value)
      !! meh "'--$name' can only be specified as a flag";
}

# handle file attributes that return an Instant
my sub set-filesystem-Instant(str $name, $value --> Nil) {
    meh "Must specify an expression for '--$name'" if Bool.ACCEPTS($value);

    my $code := $value
      .subst( '.accessed',      '.&accessed',      :g)
      .subst( '.created',       '.&created',       :g)
      .subst( '.changed',       '.&meta-modified', :g)
      .subst( '.meta-modified', '.&meta-modified', :g)
      .subst( '.modified',      '.&modified',      :g)
      .subst(/ <["']> <[0..9smhdw]>+ <["']> \. ago /, {
        $init-epoch - seconds(.substr(1, *-5))
    });

    with convert-to-simple-Callable($code, $name) {
        %filesystem{$name} := $_;
    }
    else {
        meh "Problem compiling expression for '--$name': $value";
    }
}

# handle file attributes that return an Int
my sub set-filesystem-Int(str $name, $value --> Nil) {
    if Bool.ACCEPTS($value) {
        meh "Must specify a condition for '--$name'";
    }
    orwith convert-to-simple-Callable($value, $name) {
        %filesystem{$name} := $_;
    }
    else {
        meh "Problem compiling condition for '--$name': $value";
    }
}

# handle file attributes that return an id
my sub set-filesystem-id(str $name, $value --> Nil) {
    meh "Must specify a condition for '--$name'" if Bool.ACCEPTS($value);

    %filesystem{$name} := do with convert-to-simple-Callable($value, $name) {
        $_
    }
    elsif (try $value.Int) -> $id {
        my $ := * == $id
    }
    else {
        meh "Must specify an integer or an expression with --$name";
    }
}

# handle file attributes that need a name
my sub set-filesystem-name(
  str $name, $value is copy, $name-getter, $id-getter
--> Nil) {
    meh "Must specify a condition or name for '--$name'"
      if Bool.ACCEPTS($value);

    # Get lookuppers
    my (&name-getter, &id-getter) = do {
        CATCH { meh-not-installed "P5$name-getter", "--$name" }
        "use P5$name-getter; &$name-getter, &$id-getter".EVAL
    }

    my sub names2ids($names) {
        $names.split(',').map: {
            name-getter($_)[2]
              // meh "Unknown user name '$_' with --$name";
        }
    }

    # See if we have a negation
    my $not := do if $value.starts-with('!') {
        $value = $value.substr(1);
        True
    }
    else {
        False
    }

    # An actual condition
    my $compiled := convert-to-simple-Callable($value, $name);
    %filesystem{$name} := do if $compiled.defined {
        $not
          ?? -> $id { with id-getter($id).head { !$compiled($_) } }
          !! -> $id { $compiled($_) with id-getter($id).head }
    }

    # List of user names (possibly negated)
    else {
        my int @ids = names2ids($value);
        my $id := @ids.head;
        my $ := @ids == 1
          ?? $not
            ?? * != $id
            !! * == $id
          !! $not
            ?? * ∉ @ids
            !! * ∈ @ids
    }
}

# handle file attributes that need a callable
my sub set-filesystem-callable(str $name, $value --> Nil) {
    if Bool.ACCEPTS($value) {
        meh "--$name cannot be specified as a flag";
    }
    orwith convert-to-simple-Callable($value, $name) {
        %filesystem{$name} := $_;
    }
    else {
        meh "Problem compiling condition for '--$name': $value";
    }
}

# handle external execution
my sub external-execution(str $name, $value --> Nil) {
    Bool.ACCEPTS($value)
      ?? meh("Must specify arguments for '--$name'")
      !! (%filesystem{$name} := $value);
}

# check sourcery availability
my sub check-sourcery(str $name) {
    unless &sourcery {
        CATCH { meh-not-installed 'sourcery', "--$name" }
        (&sourcery, &sourcery-pattern) =
          "use sourcery; &sourcery, &needle".EVAL;
    }
}

# check Edit::Files availability
my sub check-EditFiles(str $name) {
    unless &edit-files {
        CATCH { meh-not-installed 'Edit::Files', "--$name" }
        &edit-files = "use Edit::Files; &edit-files".EVAL;
    }
}

# check Git::Blame::File availability
my sub check-GitBlameFile(str $name) {
    unless $GitBlameFile {
        CATCH { meh-not-installed 'Git::Blame::File', "--$name" }
        require Git::Blame::File;
        $GitBlameFile := Git::Blame::File;
    }
}

# check MoarVM::Bytecode availability
my sub check-MoarVMBytecode(str $name) {
    unless $MoarVMBytecode {
        CATCH { meh-not-installed 'MoarVM::Bytecode', "--$name" }
        $MoarVMBytecode := "use MoarVM::Bytecode; MoarVM::Bytecode".EVAL;
    }
}

# check PDF::Extract availability
my sub check-PDFExtract(str $name) {
    unless $PDFExtract {
        CATCH { meh-not-installed 'PDF::Extract', "--$name" }
        $PDFExtract := "use PDF::Extract; Extract".EVAL;
    }
}

# check Text::CSV availability
my sub check-TextCSV(str $name) {
    unless $TextCSV {
        CATCH { meh-not-installed 'Text::CSV', "--$name" }
        require Text::CSV;
        $TextCSV := Text::CSV;
    }
}

# handle additional CSV flags
my sub set-csv-flag(str $name, $value --> Nil) {
    check-TextCSV($name);
    Bool.ACCEPTS($value)
      ?? (%csv{$name} := $value)
      !! meh("'--$name' can only be specified as a flag");
}

# handle additional CSV values
my sub set-csv-value(str $name, $value, str $type --> Nil) {
    check-TextCSV($name);
    Bool.ACCEPTS($value)
      ?? meh "'--$name' expects $type character, not a flag"
      !! (%csv{$name} := $value)
}

# Set highlight options
my sub set-highlight-flag(str $name, $value --> Nil) {
    Bool.ACCEPTS($value)
      ?? (%listing{$name} := $value)
      !! meh("'--$name' can only be specified as a flag");
}
my sub set-highlight-Int(str $name, $value --> Nil) {
    my $integer := $value.Int;
    Int.ACCEPTS($integer)
      ?? (%listing{$name} := $integer)
      !! meh "'--$name' can only be an integer value, not '$value'";
}
my sub set-highlight-Str(str $name, $value --> Nil) {
    Bool.ACCEPTS($value)
      ?? meh "'--$name' must be specified with a string"
      !! (%listing{$name} := $value);
}

# Set result options
my sub set-result-flag(str $name, $value --> Nil) {
    Bool.ACCEPTS($value)
      ?? (%result{$name} := $value)
      !! meh("'--$name' can only be specified as a flag");
}
my sub set-result-Int(str $name, $value --> Nil) {
    meh "'--$name' can *not* be specified as a flag"
      if Bool.ACCEPTS($value);

    my $integer := $value.Int;
    Int.ACCEPTS($integer)
      ?? (%result{$name} := $integer)
      !! meh "'--$name' can only be an integer value, not '$value'";
}
my sub set-result-flag-or-Int(str $name, $value --> Nil) {
    with $value.Int {
        %result{$name} := $_;
    }
    else {
        meh "'--$name' must either be an integer or a flag";
    }
}
my sub set-result-Callable(str $name, $value --> Nil) {
    if Bool.ACCEPTS($value) {
        meh("'--$name' can *not* be specified as a flag");
    }
    orwith convert-to-simple-Callable($value, $name) {
        %result{$name} := $_;
    }
    else {
        meh "Problem compiling condition for '--$name': $value";
    }
}

# Set listing options
my sub set-listing-flag(str $name, $value --> Nil) {
    Bool.ACCEPTS($value)
      ?? (%listing{$name} := $value)
      !! meh("'--$name' can only be specified as a flag");
}
my sub set-listing-Int(str $name, $value --> Nil) {
    my $integer := $value.Int;
    Int.ACCEPTS($integer)
      ?? (%listing{$name} := $integer)
      !! meh "'--$name' can only be an integer value, not '$value'";
}
my sub set-listing-Str(str $name, $value --> Nil) {
    Bool.ACCEPTS($value)
      ?? meh "'--$name' must be specified with a string"
      !! (%listing{$name} := $value);
}
my sub set-listing-flag-or-Int(str $name, $value --> Nil) {
    with $value.Int {
        %listing{$name} := $_;
    }
    else {
        meh "'--$name' must either be an integer or a flag";
    }
}

# Set pattern option
my sub set-additional-pattern(str $name, $value, str $prefix) {
    Bool.ACCEPTS($value)
      ?? meh "'--$name' must be a pattern specification, not a flag"
      !! add-pattern($prefix ~ $value)
}

# Set eco-xxx option in ecosystem cache
my sub set-eco-xxx($value, str $type --> Nil) {
    my str $parameter = "eco-$type";
    if Bool.ACCEPTS($value) {
        set-source($parameter, ecosystem-cache("rea", $type), 'files-from')
          if $value;
    }
    elsif $value ~~ Str && $value eq 'rea' | 'fez' {
        set-source($parameter, ecosystem-cache($value, $type), 'files-from');
    }
    else {
        meh "'--$parameter' must be either 'rea' or 'fez'";
    }
}

# Set rakudo-xxx option in ecosystem cache
my sub set-rakudo-xxx($value, str $type --> Nil) {
    my str $parameter = "rakudo-$type";
    if Bool.ACCEPTS($value) {
        set-source($parameter, rakudo-cache($type), 'files-from')
          if $value;
    }
    else {
        meh "'--$parameter' can only be specified as a flag"; 
    }
}

#-------------------------------------------------------------------------------
# One subroutine for each supported option.  Is assumed to do right thing for
# that option by setting the appropriate global hashes.  Not expected to return
# anything.  Existence of "&option-foo" means the option exists and is
# supported.  These subroutines are **only** called if the associated option
# is actually specified (after any expansion).

my sub option-absolute($value --> Nil) {
    set-listing-flag('absolute', $value)
}

my sub option-accept($value --> Nil) {
    set-filesystem-callable('accept', $value)
}

my sub option-accessed($value --> Nil) {
    set-filesystem-Instant('accessed', $value)
}

my sub option-ack($value --> Nil) {
    %config := %ack-interface-config;
}

my sub option-after-context($value --> Nil) {
    set-result-Int('after-context', $value);
}

my sub option-allow-loose-escapes($value --> Nil) {
    set-csv-flag('allow-loose-escapes', $value);
}

my sub option-allow-loose-quotes($value --> Nil) {
    set-csv-flag('allow-loose-quotes', $value);
}

my sub option-allow-whitespace($value --> Nil) {
    set-csv-flag('allow-whitespace', $value);
}

my sub option-also-first($value --> Nil) {
    set-result-flag-or-Int('also-first', $value);
}

my sub option-always-first($value --> Nil) {
    set-result-flag-or-Int('always-first', $value);
}

my sub option-and($value --> Nil) {
    set-additional-pattern('and', $value, '&');
}

my sub option-andnot($value --> Nil) {
    set-additional-pattern('andnot', $value, '&!');
}

my sub option-auto-decompress($value --> Nil) {
    set-filesystem-flag('auto-decompress', $value);
}

my sub option-auto-diag($value --> Nil) {
    set-csv-flag('auto-diag', $value);
}

my sub option-backtrace($value --> Nil) {
    set-result-flag('backtrace', $value);
}

my sub option-backup($value --> Nil) {
    if Bool.ACCEPTS($value) {
        %modify<backup> := "bak" if $value;
    }
    else {
        %modify<backup> := $value;
    }
}

my sub option-batch($value --> Nil) {
    set-rak-Int('batch', $value);
}

my sub option-before-context($value --> Nil) {
    set-result-Int('before-context', $value);
}

my sub option-blame-per-file($value --> Nil) {
    check-GitBlameFile('blame-per-file');
    set-producer('blame-per-file', -> $io { $GitBlameFile.new($io) });
    set-action('blame-per-file', $value);
}

my sub option-blame-per-line($value --> Nil) {
    check-GitBlameFile('blame-per-line');
    set-producer(
      'blame-per-line',
      -> $io { $GitBlameFile.new($io).lines, 'produce-many' }
    );
    set-action('blame-per-line', $value);
}

my sub option-blocks($value --> Nil) {
    set-filesystem-Int('blocks', $value)
}

my sub option-break($value --> Nil) {
    if Bool.ACCEPTS($value) {
        $value
          ?? set-listing-Str('break', "")
          !! (%listing<has-break> := False);
    }
    else {
        set-listing-Str('break', $value);
    }
}

my sub option-categorize($value --> Nil) {
    set-result-Callable('categorize', $value);
}

my sub option-checkout($value --> Nil) {
    set-producer('checkout', &branches, 'produce-many');
    set-action('checkout', $value);
}

my sub option-classify($value --> Nil) {
    set-result-Callable('classify', $value);
}

my sub option-context($value --> Nil) {
    set-result-Int('context', $value);
}

my sub option-count-only($value --> Nil) {
    set-result-flag('count-only', $value);
}

my sub option-created($value --> Nil) {
    set-filesystem-Instant('created', $value)
}

my sub option-csv-per-line($value --> Nil) {
    check-TextCSV('csv-per-line');
    set-producer(
      'csv-per-line',
      -> $io { $TextCSV.new(|%CSV).csv: :$headers, :file($io.path) },
      'produce-many'
    );
    set-action('csv-per-line', $value);
}

my sub option-degree($value --> Nil) {
    my $code;
    $code := convert-to-simple-Callable($value, 'degree')
      unless Bool.ACCEPTS($value);
    my $integer := $code.defined
      ?? $code(Kernel.cpu-cores).Int
      !! $value.Int;
    Int.ACCEPTS($integer)
      ?? (%rak<degree> := $integer)
      !! meh "'--degree' must be an integer, or a Callable, not '$value'";
}

my sub option-deny($value --> Nil) {
    set-filesystem-callable('deny', $value)
}

my sub option-description($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? meh "'--description' can only be specified as a string"
      !! (%global<description> := $value);
}

my sub option-device-number($value --> Nil) {
    set-filesystem-Int('device-number', $value)
}

my sub option-dir($value --> Nil) {
    %filesystem<dir> := Bool.ACCEPTS($value)
      ?? $value
      !! codify($value);
}

my sub option-dont-catch($value --> Nil) {
    set-rak-flag('dont-catch', $value);
}

my sub option-dryrun($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? (%modify<dryrun> := $value)
      !! meh "'--dryrun' can only be specified as a flag";
}

my sub option-eco-code($value --> Nil) {
    set-eco-xxx($value, "code");
}

my sub option-eco-doc($value --> Nil) {
    set-eco-xxx($value, "doc");
}

my sub option-eco-meta($value --> Nil) {
    set-producer(
      'eco-meta',
      -> $io {
          with try from-json $io.slurp(:$enc) -> \data {
              Seq.new: data.iterator
          }
      },
      'produce-many'
    );
    %result<eco-meta> := $value<> =:= True
      ?? ("rea",)
      !! (my @ecos is List = $value.split(',')).all (elem) <p6c cpan fez rea>
        ?? @ecos
        !! meh "Must specify one of p6c cpan fez rea with --eco-meta, not: $value";
    set-action('eco-meta', True);
}

my sub option-eco-provides($value --> Nil) {
    set-eco-xxx($value, "provides");
}

my sub option-eco-scripts($value --> Nil) {
    set-eco-xxx($value, "scripts");
}

my sub option-eco-tests($value --> Nil) {
    set-eco-xxx($value, "tests");
}

my sub option-edit($value --> Nil) {
    check-EditFiles('edit');
    set-action('edit', $value);
}

my sub option-encoding($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? meh("Must specify an explicit encoding with '--encoding'")
      !! (%rak<encoding> := $value);
}

my sub option-eol($value --> Nil) {
    check-TextCSV('eol');

    my constant %line-endings =
      lf   => "\n",
      cr   => "\r",
      crlf => "\r\n";

    Bool.ACCEPTS($value)
      ?? meh("Must specify an explicit line-ending with '--eol'")
      !! (%csv<eol> := (%line-endings{$value} // $value));
}

my sub option-escape($value --> Nil) {
    set-csv-value('escape', $value, 'an escape');
}

my sub option-exec($value --> Nil) {
    external-execution('exec', $value);
}

my sub option-execute-raku($value --> Nil) {
    %result<execute-raku> := $value;
}

my sub option-extensions($value --> Nil) {
    meh "'--extensions' can only be specified as a string"
      if Bool.ACCEPTS($value);

    my @unknown;
    my @extensions = $value.split(',').map: {
        if .starts-with('#') {
            if %exts{$_} -> \extensions {
                extensions.Slip
            }
            else {
                @unknown.push: $_;
                Empty
            }
        }
        elsif $_ eq '*' {
            @known-extensions.Slip
        }
        else {
            $_
        }
    }

    @unknown
      ?? meh("No extensions known for group&s(@unknown) @unknown[]")
      !! (%filesystem<extensions> := codify-extensions @extensions);
}

my sub option-file($value --> Nil) {
    %filesystem<file> := Bool.ACCEPTS($value)
      ?? $value
      !! codify($value);
}

my sub option-file-separator-null($value --> Nil) {
    set-result-flag('file-separator-null', $value);
}

my sub option-files-from($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? meh "'--files-from' must be a file specification, not a flag"
      !! set-source('files-from', $value);
}

my sub option-files-with-matches($value --> Nil) {
    set-result-flag('files-with-matches', $value);
}

my sub option-files-without-matches($value --> Nil) {
    set-result-flag('files-without-matches', $value);
}

my sub option-filesize($value --> Nil) {
    set-filesystem-Int('filesize', $value)
}

my sub option-find($value --> Nil) {
    set-result-flag('find', $value);
}

my sub option-formula($value --> Nil) {
    set-csv-flag('formula', $value);
}

my sub option-frequencies($value --> Nil) {
    set-result-flag('frequencies', $value);
}

my sub option-gid($value --> Nil) {
    set-filesystem-id('gid', $value);
}

my sub option-group($value --> Nil) {
    set-filesystem-name('group', $value, 'getgrnam', 'getgrgid');
}

my sub option-group-matches($value --> Nil) {
    set-listing-flag('group-matches', $value);
}

my sub option-hard-links($value --> Nil) {
    set-filesystem-Int('hard-links', $value);
}

my sub option-has-setgid($value --> Nil) {
    set-filesystem-flag('has-setgid', $value);
}

my sub option-has-setuid($value --> Nil) {
    set-filesystem-flag('has-setuid', $value);
}

my sub option-headers($value --> Nil) {
    CATCH {
        meh "Could not compile --headers='$value':\n$_.message()";
    }
    %csv<headers> := Bool.ACCEPTS($value) || $value (elem) <skip auto lc uc>
      ?? $value
      !! List.ACCEPTS(my $compiled := $value.EVAL)
        ?? $compiled.are =:= Pair
          ?? %($compiled)  # convert list of Pairs to hash
          !! $compiled.Array
        !! $compiled;
}

my sub option-help($value --> Nil) {
    set-action('help', $value);
}

my sub option-highlight($value --> Nil) {
    set-listing-flag('highlight', $value);
}

my sub option-highlight-after($value --> Nil) {
    set-listing-Str('highlight-after', $value);
}

my sub option-highlight-before($value --> Nil) {
    set-listing-Str('highlight-before', $value);
}

my sub option-human($value --> Nil) {
    set-listing-flag('human', $value);
}

my sub option-ignorecase($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? ($ignorecase := $value)
      !! meh "'--ignorecase' must be specified as a flag";
}

my sub option-ignoremark($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? ($ignoremark := $value)
      !! meh "'--ignoremark' must be specified as a flag";
}

my sub option-inode($value --> Nil) {
    set-filesystem-Int('inode', $value)
}

my sub option-invert-match($value --> Nil) {
    set-result-flag('invert-match', $value);
}

my sub option-is-empty($value --> Nil) {
    set-filesystem-flag('is-empty', $value);
}

my sub option-is-executable($value --> Nil) {
    set-filesystem-flag('is-executable', $value);
}

my sub option-is-group-executable($value --> Nil) {
    set-filesystem-flag('is-group-executable', $value);
}

my sub option-is-group-readable($value --> Nil) {
    set-filesystem-flag('is-group-readable', $value);
}

my sub option-is-group-writable($value --> Nil) {
    set-filesystem-flag('is-group-writable', $value);
}

my sub option-is-moarvm($value --> Nil) {
    set-filesystem-flag('is-moarvm', $value);
}

my sub option-is-owned-by-group($value --> Nil) {
    set-filesystem-flag('is-owned-by-group', $value);
}

my sub option-is-owned-by-user($value --> Nil) {
    set-filesystem-flag('is-owned-by-user', $value);
}

my sub option-is-owner-executable($value --> Nil) {
    set-filesystem-flag('is-owner-executable', $value);
}

my sub option-is-owner-readable($value --> Nil) {
    set-filesystem-flag('is-owner-readable', $value);
}

my sub option-is-owner-writable($value --> Nil) {
    set-filesystem-flag('is-owner-writable', $value);
}

my sub option-is-pdf($value --> Nil) {
    set-filesystem-flag('is-pdf', $value);
}

my sub option-is-readable($value --> Nil) {
    set-filesystem-flag('is-readable', $value);
}

my sub option-is-sticky($value --> Nil) {
    set-filesystem-flag('is-sticky', $value);
}

my sub option-is-symbolic-link($value --> Nil) {
    set-filesystem-flag('is-symbolic-link', $value);
}

my sub option-is-text($value --> Nil) {
    set-filesystem-flag('is-text', $value);
}

my sub option-is-world-executable($value --> Nil) {
    set-filesystem-flag('is-world-executable', $value);
}

my sub option-is-world-readable($value --> Nil) {
    set-filesystem-flag('is-world-readable', $value);
}

my sub option-is-world-writable($value --> Nil) {
    set-filesystem-flag('is-world-writable', $value);
}

my sub option-is-writable($value --> Nil) {
    set-filesystem-flag('is-writable', $value);
}

my sub option-json-per-elem($value --> Nil) {
    set-producer(
      'json-per-elem',
      -> $io {
          with try from-json $io.slurp(:$enc) -> \data {
              Seq.new: data.iterator
          }
      },
      'produce-many'
    );
    set-action('json-per-elem', $value);
}

my sub option-json-per-file($value --> Nil) {
    set-producer('json-per-file',-> $io { try from-json $io.slurp(:$enc) });
    set-action('json-per-file', $value);
}

my sub option-json-per-line($value --> Nil) {
    set-producer(
      'json-per-line',
      -> $io {
          with try $io.lines(:$enc) -> $seq {
              $seq.map: { (try from-json($_)) // Empty }
          }
      },
      'produce-many'
    );

    set-action('json-per-line', $value);
}

my sub option-keep-meta($value --> Nil) {
    set-csv-flag('keep-meta', $value);
}

my sub option-list-custom-options($value --> Nil) {
    set-action('list-custom-options', $value);
}

my sub option-list-expanded-options($value --> Nil) {
    set-global-flag('list-expanded-options', $value);
}

my sub option-list-known-extensions($value --> Nil) {
    set-action('list-known-extensions', $value);
}

my sub option-matches-only($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? ($matches-only := $value)
      !! meh "'--matches-only' must be specified as a flag";
}

my sub option-max-matches-per-file($value --> Nil) {
    set-result-flag-or-Int('max-matches-per-file', $value);
}

my sub option-mbc($value --> Nil) {
    check-MoarVMBytecode('mbc');
    set-action('mbc', $value);
}

my sub option-mbc-frames($value --> Nil) {
    check-MoarVMBytecode('mbc-frames');
    set-action('mbc-frames', $value);
}

my sub option-mbc-strings($value --> Nil) {
    check-M