Group
Extension

Perinci-Sub-To-CLIDocData/lib/Perinci/Sub/To/CLIDocData.pm

package Perinci::Sub::To::CLIDocData;

use 5.010001;
use strict;
use warnings;
use Log::ger;

use Perinci::Object;
use Perinci::Sub::Util qw(err);

our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
our $DATE = '2022-11-14'; # DATE
our $DIST = 'Perinci-Sub-To-CLIDocData'; # DIST
our $VERSION = '0.305'; # VERSION

our %SPEC;

require Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(gen_cli_doc_data_from_meta);

sub _has_cats {
    for my $spec (@{ $_[0] }) {
        for (@{ $spec->{tags} // [] }) {
            my $tag_name = ref($_) ? $_->{name} : $_;
            if ($tag_name =~ /^category:/) {
                return 1;
            }
        }
    }
    0;
}

sub _add_category_from_spec {
    my ($cats_spec, $thing, $spec, $noun, $has_cats) = @_;
    my @cats;
    for (@{ $spec->{tags} // [] }) {
        my $tag_name = ref($_) ? $_->{name} : $_;
        if ($tag_name =~ /^category(\d+)?:(.+)/) {
            my $cat = ucfirst($2);
            my $ordering = $1 // 50;
            $cat =~ s/-/ /g;
            $cat .= " " . $noun;
            push @cats, [$cat, $ordering]; # name, ordering
        }
    }
    if (!@cats) {
        @cats = [$has_cats ? "Other $noun" : ucfirst($noun), 99]; # XXX translatable?
    }

    # old, will be removed someday
    $thing->{category} = $cats[0][0];
    # new/current
    $thing->{categories} = [map {$_->[0]} @cats];

    $cats_spec->{$_->[0]}{order} //= $_->[1] for @cats;
}

sub _add_default_from_arg_spec {
    my ($opt, $arg_spec) = @_;
    if (exists $arg_spec->{default}) {
        $opt->{default} = $arg_spec->{default};
    } elsif ($arg_spec->{schema} && exists($arg_spec->{schema}[1]{default})) {
        $opt->{default} = $arg_spec->{schema}[1]{default};
    }
}

sub _dash_prefix {
    length($_[0]) > 1 ? "--$_[0]" : "-$_[0]";
}

sub _fmt_opt {
    my $spec = shift;
    my @ospecs = @_;
    my @res;
    my $i = 0;
    for my $ospec (@ospecs) {
        my $j = 0;
        my $parsed = $ospec->{parsed};
        for (@{ $parsed->{opts} }) {
            my $opt = _dash_prefix($_);
            if ($i==0 && $j==0) {
                if ($parsed->{type}) {
                    if ($spec->{'x.schema.entity'}) {
                        $opt .= "=".$spec->{'x.schema.entity'};
                    } elsif ($spec->{'x.schema.element_entity'}) {
                        $opt .= "=".$spec->{'x.schema.element_entity'};
                    } else {
                        $opt .= "=$parsed->{type}";
                    }
                }
                # mark required option with a '*'
                $opt .= "*" if $spec->{req} && !$ospec->{is_base64} &&
                    !$ospec->{is_json} && !$ospec->{is_yaml};
            }
            push @res, $opt;
            $j++;
        }
        $i++;
    }
    join ", ", @res;
}

$SPEC{gen_cli_doc_data_from_meta} = {
    v => 1.1,
    summary => 'From Rinci function metadata, generate structure convenient '.
        'for producing CLI documentation (help/usage/POD)',
    description => <<'_',

This function calls <pm:Perinci::Sub::GetArgs::Argv>'s
`gen_getopt_long_spec_from_meta()` (or receive its result as an argument, if
passed, to avoid calling the function twice) and post-processes it: produce
command usage line, format the options, include information from metadata, group
the options by category. It also selects examples in the `examples` property
which are applicable to CLI environment and format them.

The resulting data structure is convenient to use when one wants to produce a
documentation for CLI program (including help/usage message and POD).

_
    args => {
        meta => {
            schema => 'hash*', # XXX rifunc
            req => 1,
            pos => 0,
        },
        meta_is_normalized => {
            schema => 'bool*',
        },
        common_opts => {
            summary => 'Will be passed to gen_getopt_long_spec_from_meta()',
            schema  => 'hash*',
        },
        ggls_res => {
            summary => 'Full result from gen_getopt_long_spec_from_meta()',
            schema  => 'array*', # XXX envres
            description => <<'_',

If you already call <pm:Perinci::Sub::GetArgs::Argv>'s
`gen_getopt_long_spec_from_meta()`, you can pass the _full_ enveloped result
here, to avoid calculating twice. What will be useful for the function is the
extra result in result metadata (`func.*` keys in `$res->[3]` hash).

_
        },
        per_arg_json => {
            schema => 'bool',
            summary => 'Pass per_arg_json=1 to Perinci::Sub::GetArgs::Argv',
        },
        per_arg_yaml => {
            schema => 'bool',
            summary => 'Pass per_arg_json=1 to Perinci::Sub::GetArgs::Argv',
        },
        lang => {
            schema => 'str*',
        },
        mark_different_lang => {
            schema => 'bool*',
            default => 1,
        },
    },
    result => {
        schema => 'hash*',
    },
};
sub gen_cli_doc_data_from_meta {
    require Getopt::Long::Negate::EN;

    my %args = @_;

    my $lang = $args{lang};
    my $mark_different_lang = $args{mark_different_lang} // 1;
    my $meta = $args{meta} or return [400, 'Please specify meta'];
    my $common_opts = $args{common_opts};
    unless ($args{meta_is_normalized}) {
        require Perinci::Sub::Normalize;
        $meta = Perinci::Sub::Normalize::normalize_function_metadata($meta);
    }
    my $ggls_res = $args{ggls_res} // do {
        require Perinci::Sub::GetArgs::Argv;
        Perinci::Sub::GetArgs::Argv::gen_getopt_long_spec_from_meta(
            meta=>$meta, meta_is_normalized=>1, common_opts=>$common_opts,
            per_arg_json => $args{per_arg_json},
            per_arg_yaml => $args{per_arg_yaml},
        );
    };
    $ggls_res->[0] == 200 or return $ggls_res;

    my $langprop_args = {lang=>$lang, mark_different_lang=>$mark_different_lang};
    my $args_prop = $meta->{args} // {};
    my $clidocdata = {
        option_categories => {},
        example_categories => {},
    };

    # a mapping from arg spec keys to %opts keys, so we can create a POD link
    # from a positional argument in usage line to option, later when generating
    # usage line
    my %arg_spec_to_opts;

    # a mapping from keys in func.specmeta (ospec) to %opts keys, so we can
    # create a POD link from an option in usage line to option in Options
    # section, later when generating usage line
    my %ospec_to_opts;

    my %opts;
  GEN_LIST_OF_OPTIONS: {
        my $ospecs = $ggls_res->[3]{'func.specmeta'};
        # separate groupable aliases because they will be merged with the
        # argument options
        my (@k, @k_aliases);

      OSPEC1:
        for (sort keys %$ospecs) {
            my $ospec = $ospecs->{$_};
            {
                last unless $ospec->{is_alias};
                next if $ospec->{is_code};
                my $arg_spec = $args_prop->{$ospec->{arg}};
                my $alias_spec = $arg_spec->{cmdline_aliases}{$ospec->{alias}};
                next if $alias_spec->{summary};
                push @k_aliases, $_;
                next OSPEC1;
            }
            push @k, $_;
        }

        my %negs; # key=arg, only show one negation form for each arg option

      OSPEC2:
        while (@k) {
            my $k = shift @k;
            my $ospec = $ospecs->{$k};
            my $opt;
            my $optkey;

            if ($ospec->{is_alias} || defined($ospec->{arg})) {
                my $arg_spec;
                my $alias_spec;

                if ($ospec->{is_alias}) {
                    # non-groupable alias

                    my $real_opt_ospec = $ospecs->{ $ospec->{alias_for} };

                    $arg_spec = $args_prop->{ $ospec->{arg} };
                    $alias_spec = $arg_spec->{cmdline_aliases}{$ospec->{alias}};
                    my $rimeta = rimeta($alias_spec);
                    $optkey = _fmt_opt($arg_spec, $ospec);
                    $opt = {
                        opt_parsed => $ospec->{parsed},
                        orig_opt => $k,
                        is_alias => 1,
                        alias_for => $ospec->{alias_for},
                        summary => $rimeta->langprop($langprop_args, 'summary') //
                            "Alias for "._dash_prefix($real_opt_ospec->{parsed}{opts}[0]),
                        description =>
                            $rimeta->langprop($langprop_args, 'description'),
                    };
                } else {
                    # an option for argument

                    $arg_spec = $args_prop->{$ospec->{arg}};
                    my $rimeta = rimeta($arg_spec);
                    $opt = {
                        opt_parsed => $ospec->{parsed},
                        orig_opt => $k,
                    };

                    # for bool, only display either the positive (e.g. --bool)
                    # or the negative (e.g. --nobool) depending on the default
                    if (defined($ospec->{is_neg})) {
                        my $default = $arg_spec->{default} //
                            $arg_spec->{schema}[1]{default};
                        next OSPEC2 if  $default && !$ospec->{is_neg};
                        next OSPEC2 if !$default &&  $ospec->{is_neg};
                        if ($ospec->{is_neg}) {
                            next OSPEC2 if $negs{$ospec->{arg}}++;
                        }
                    }

                    if ($ospec->{is_neg}) {
                        # for negative option, use negative summary instead of
                        # regular (positive sentence) summary
                        $opt->{summary} =
                            $rimeta->langprop($langprop_args, 'summary.alt.bool.not');
                    } elsif (defined $ospec->{is_neg}) {
                        # for boolean option which we show the positive, show
                        # the positive summary if available
                        $opt->{summary} =
                            $rimeta->langprop($langprop_args, 'summary.alt.bool.yes') //
                                $rimeta->langprop($langprop_args, 'summary');
                    } elsif (($ospec->{parsed}{type}//'') eq 's@') {
                        # for array of string that can be specified via multiple
                        # --opt, show singular version of summary if available.
                        # otherwise show regular summary.
                        $opt->{summary} =
                            $rimeta->langprop($langprop_args, 'summary.alt.plurality.singular') //
                                $rimeta->langprop($langprop_args, 'summary');
                    } else {
                        $opt->{summary} =
                            $rimeta->langprop($langprop_args, 'summary');
                    }
                    $opt->{description} =
                        $rimeta->langprop($langprop_args, 'description');

                    # find aliases that can be grouped together with this option
                    my @aliases;
                    my $j = $#k_aliases;
                    while ($j >= 0) {
                        my $aospec = $ospecs->{ $k_aliases[$j] };
                        {
                            last unless $aospec->{arg} eq $ospec->{arg};
                            push @aliases, $aospec;
                            splice @k_aliases, $j, 1;
                        }
                        $j--;
                    }

                    $optkey = _fmt_opt($arg_spec, $ospec, @aliases);
                }

                $opt->{arg_spec} = $arg_spec;
                $opt->{alias_spec} = $alias_spec if $alias_spec;

                # include keys from func.specmeta
                for (qw/arg fqarg is_base64 is_json is_yaml/) {
                    $opt->{$_} = $ospec->{$_} if defined $ospec->{$_};
                }

                # include keys from arg_spec
                for (qw/req pos slurpy greedy is_password links tags/) {
                    $opt->{$_} = $arg_spec->{$_} if defined $arg_spec->{$_};
                }

                {
                    # we don't want argument options to end up in "Other" like
                    # --help or -v, they are put at the end. so if an argument
                    # option does not have category, we'll put it in the "main"
                    # category.
                    local $arg_spec->{tags} = ['category0:main']
                        if !$arg_spec->{tags} || !@{$arg_spec->{tags}};
                    _add_category_from_spec($clidocdata->{option_categories},
                                            $opt, $arg_spec, "options", 1);
                }
                _add_default_from_arg_spec($opt, $arg_spec);

            } else {
                # option from common_opts

                my $spec = $common_opts->{$ospec->{common_opt}};

                # for bool, only display either the positive (e.g. --bool)
                # or the negative (e.g. --nobool) depending on the default
                my $show_neg = $ospec->{parsed}{is_neg} && $spec->{default};

                local $ospec->{parsed}{opts} = do {
                    # XXX check if it's single-letter, get first
                    # non-single-letter
                    my @opts = Getopt::Long::Negate::EN::negations_for_option(
                        $ospec->{parsed}{opts}[0]);
                    [ $opts[0] ];
                } if $show_neg;

                $optkey = _fmt_opt($spec, $ospec);
                my $rimeta = rimeta($spec);
                $opt = {
                    opt_parsed => $ospec->{parsed},
                    orig_opt => $k,
                    common_opt => $ospec->{common_opt},
                    common_opt_spec => $spec,
                    summary => $show_neg ?
                        $rimeta->langprop($langprop_args, 'summary.alt.bool.not') :
                            $rimeta->langprop($langprop_args, 'summary'),
                    (schema => $spec->{schema}) x !!$spec->{schema},
                    ('x.schema.entity' => $spec->{'x.schema.entity'}) x !!$spec->{'x.schema.entity'},
                    ('x.schema.element_entity' => $spec->{'x.schema.element_entity'}) x !!$spec->{'x.schema.element_entity'},
                    description =>
                        $rimeta->langprop($langprop_args, 'description'),
                    (default => $spec->{default}) x !!(exists($spec->{default}) && !$show_neg),
                };

                _add_category_from_spec($clidocdata->{option_categories},
                                        $opt, $spec, "options", 1);

            }

            $opts{$optkey} = $opt;
            $arg_spec_to_opts{ $ospec->{arg} } = $optkey if $ospec->{arg};
            $ospec_to_opts{$k} = $optkey;
        } # while @k

        # link ungrouped alias to its main opt
      OPT1:
        for my $k (keys %opts) {
            my $opt = $opts{$k};
            next unless $opt->{is_alias} || $opt->{is_base64} ||
                $opt->{is_json} || $opt->{is_yaml};
            for my $k2 (keys %opts) {
                my $arg_opt = $opts{$k2};
                next if $arg_opt->{is_alias} || $arg_opt->{is_base64} ||
                    $arg_opt->{is_json} || $arg_opt->{is_yaml};
                next unless defined($arg_opt->{arg}) &&
                    $arg_opt->{arg} eq $opt->{arg};
                $opt->{main_opt} = $k2;
                next OPT1;
            }
        }

    } # GEN_LIST_OF_OPTIONS
    $clidocdata->{opts} = \%opts;

    #use DDC; dd \%arg_spec_to_opts;
    #use DDC; dd \%ospec_to_opts;

  GEN_USAGE_LINE: {
        my @plain_args;
        my @pod_args;

        my %args_prop = %$args_prop; # copy because we want to iterate & delete
        my $max_pos = -1;
        for (values %args_prop) {
            $max_pos = $_->{pos}
                if defined($_->{pos}) && $_->{pos} > $max_pos;
        }
        my $pos = 0;
        while ($pos <= $max_pos) {
            my ($arg, $arg_spec);
            for (keys %args_prop) {
                $arg_spec = $args_prop{$_};
                if (defined($arg_spec->{pos}) && $arg_spec->{pos}==$pos) {
                    $arg = $_;
                    last;
                }
            }
            $pos++;
            next unless defined($arg);
            my $arg0 = $arg;
            if ($arg_spec->{slurpy} // $arg_spec->{greedy}) {
                # try to find the singular form
                $arg = $arg_spec->{'x.name.singular'}
                    if $arg_spec->{'x.name.is_plural'} &&
                    defined $arg_spec->{'x.name.singular'};
            }
            if ($arg_spec->{req}) {
                push @plain_args, "<$arg>";
                push @pod_args  , qq#E<lt>I<L<$arg|/"$arg_spec_to_opts{$arg0}">>E<gt>#;
            } else {
                push @plain_args, "[$arg]";
                push @pod_args  , qq#[I<L<$arg|/"$arg_spec_to_opts{$arg0}">>]#;
            }
            $plain_args[-1] .= " ..." if ($arg_spec->{slurpy} // $arg_spec->{greedy});
            $pod_args  [-1] .= " ..." if ($arg_spec->{slurpy} // $arg_spec->{greedy});
            delete $args_prop{$arg};
        }

        # XXX utilize information from args_rels

        require Getopt::Long::Util;
        require Module::Installed::Tiny;
        my @plain_opts;
        my @pod_opts;
        my %opt_locations; # key=$ARGNAME or "common:$SOMEKEY"
        for my $ospec (sort {
            ($ggls_res->[3]{'func.specmeta'}{$a}{is_neg} ? 1:0) <=> ($ggls_res->[3]{'func.specmeta'}{$b}{is_neg} ? 1:0) ||
            ($ggls_res->[3]{'func.specmeta'}{$a}{is_alias} ? 1:0) <=> ($ggls_res->[3]{'func.specmeta'}{$b}{is_alias} ? 1:0) ||
            ($ggls_res->[3]{'func.specmeta'}{$a}{is_json} ? 1:0) <=> ($ggls_res->[3]{'func.specmeta'}{$b}{is_json} ? 1:0) ||
            ($ggls_res->[3]{'func.specmeta'}{$a}{is_yaml} ? 1:0) <=> ($ggls_res->[3]{'func.specmeta'}{$b}{is_yaml} ? 1:0) ||
                $a cmp $b
            } keys %{ $ggls_res->[3]{'func.specmeta'} }) {
            my $ospecmeta = $ggls_res->[3]{'func.specmeta'}{$ospec};

            my $argprop = defined $ospecmeta->{arg} ? $args_prop{ $ospecmeta->{arg} } : undef;
            # only include args that have not been mentioned in positional
            next if defined $ospecmeta->{arg} && !$argprop;
            # only inlude common options that are not a specific action that are
            # invoked on its own

            my $copt = defined $ospecmeta->{common_opt} ? $common_opts->{ $ospecmeta->{common_opt} } : undef;

            next if defined $ospecmeta->{common_opt} && $copt->{usage};
            my ($caption_from_schema, $type, $cset);
            if ($argprop && $argprop->{schema} &&
                    ref $argprop->{schema} eq 'ARRAY' # ignore non-normalized schema for now
                ) {
                $type = $argprop->{schema}[0];
                $cset = $argprop->{schema}[1];
                #log_trace "argprop=%s, type=%s", $argprop, $type;
                if ($type eq 'array') {
                    if ($cset->{of} && ref $cset->{of} eq 'ARRAY') {
                        $caption_from_schema = $cset->{of}[0];
                    }
                } elsif ($type eq 'hash') {
                    if ($cset->{of} && ref $cset->{of} eq 'ARRAY') {
                        $caption_from_schema = $cset->{of}[0];
                    }
                } else {
                    $caption_from_schema = $type;
                }
            }
            #use DDC; dd $ospec; dd $ospecmeta;
            my $opt_link =
                defined $ospecmeta->{arg} ? $arg_spec_to_opts{ $ospecmeta->{arg} } :
                defined $ospecmeta->{alias_for} ? $ospec_to_opts{ $ospecmeta->{alias_for} } :
                $ospec_to_opts{$ospec};
            my $hres = Getopt::Long::Util::humanize_getopt_long_opt_spec({
                extended=>1,
                separator=>"|",
                value_label=>(
                    $ospecmeta->{is_json} ? 'json' :
                    $ospecmeta->{is_yaml} ? 'yaml' :
                        $argprop ?
                        ($argprop->{'x.cli.opt_value_label'} // $argprop->{caption} // $caption_from_schema) :
                        $copt->{value_label}
                ),
                opt_link => qq#/"$opt_link"#,
                value_label_link=>(
                    $ospecmeta->{is_json} ? undef :
                    $ospecmeta->{is_yaml} ? undef :
                    defined($caption_from_schema) && Module::Installed::Tiny::module_installed("Sah::Schema::$caption_from_schema") ? "Sah::Schema::$caption_from_schema" : undef
                ),
            }, $ospec);
            my $plain_opt = $hres->{plaintext};
            my $pod_opt   = $hres->{pod};

            my $key;
            if ($copt && defined $copt->{key}) {
                # group common options by key.
                $key = "00common:" . $copt->{key};
            } elsif (defined $ospecmeta->{arg}) {
                $key = $ospecmeta->{arg};
            } else {
                $key = $ospec;
                $key =~ s/[=:].+\z//;
            }
            $key =~ s/_/-/g;

            #say "D:ospec=$ospec -> key=$key, ospecmeta->{arg}=$ospecmeta->{arg}";

            $opt_locations{$key} //= scalar @plain_opts;
            push @{ $plain_opts[ $opt_locations{$key} ] }, $plain_opt;
            push @{ $pod_opts  [ $opt_locations{$key} ] }, $pod_opt;
            #use Data::Dmp; print "key: $key, ospec: $ospec, ospecmeta: ", dmp($ospecmeta), ", argprop: ", dmp($argprop), ", copt: ", dmp($copt), "\n";
        }

        $clidocdata->{compact_usage_line} = "[[prog]]".
            (keys(%args_prop) || keys(%$common_opts) ? " [options]" : ""). # XXX translatable?
            (@plain_args ? " ".join(" ", @plain_args) : "");
        $clidocdata->{usage_line} = "[[prog]]".
            (@plain_opts+@plain_args ? " ".
             join(" ",
                  (map { "[". join("|", @$_) . "]" } @plain_opts),
                  (@plain_opts && @plain_args ? ("--") : ()),
                  @plain_args,
              ) : "");
        $clidocdata->{'usage_line.alt.fmt.pod'} = "B<[[prog]]>".
            (@pod_opts+@pod_args ? " ".
             join(" ",
                  (map { "[". join("|", @$_) . "]" } @pod_opts),
                  (@pod_opts && @pod_args ? ("--") : ()),
                  @pod_args,
              ) : "");
    } # GEN_USAGE_LINE

    # filter and format examples
    my @examples;
    {
        my $examples = $meta->{examples} // [];
        my $has_cats = _has_cats($examples);

        for my $eg (@$examples) {
            my $rimeta = rimeta($eg);
            my $argv;
            my $cmdline;
            if (defined($eg->{src})) {
                # we only show shell command examples
                if ($eg->{src_plang} =~ /^(sh|bash)$/) {
                    $cmdline = $eg->{src};
                } else {
                    next;
                }
            } else {
                require String::ShellQuote;
                if ($eg->{argv}) {
                    $argv = $eg->{argv};
                } else {
                    require Perinci::Sub::ConvertArgs::Argv;
                    my $res = Perinci::Sub::ConvertArgs::Argv::convert_args_to_argv(
                        args => $eg->{args}, meta => $meta, use_pos => 1);
                    return err($res, 500, "Can't convert args to argv")
                        unless $res->[0] == 200;
                    $argv = $res->[2];
                }
                $cmdline = "[[prog]]";
                for my $arg (@$argv) {
                    my $qarg = String::ShellQuote::shell_quote($arg);
                    $cmdline .= " $qarg"; # XXX markup with color?
                }
            }
            my $egdata = {
                cmdline      => $cmdline,
                summary      => $rimeta->langprop($langprop_args, 'summary'),
                description  => $rimeta->langprop($langprop_args, 'description'),
                example_spec => $eg,
            };
            # XXX show result from $eg
            _add_category_from_spec($clidocdata->{example_categories},
                                    $egdata, $eg, "examples", $has_cats);
            push @examples, $egdata;
        }
    }
    $clidocdata->{examples} = \@examples;

    [200, "OK", $clidocdata];
}

1;
# ABSTRACT: From Rinci function metadata, generate structure convenient for producing CLI documentation (help/usage/POD)

__END__

=pod

=encoding UTF-8

=head1 NAME

Perinci::Sub::To::CLIDocData - From Rinci function metadata, generate structure convenient for producing CLI documentation (help/usage/POD)

=head1 VERSION

This document describes version 0.305 of Perinci::Sub::To::CLIDocData (from Perl distribution Perinci-Sub-To-CLIDocData), released on 2022-11-14.

=head1 SYNOPSIS

 use Perinci::Sub::To::CLIDocData qw(gen_cli_doc_data_from_meta);
 my $clidocdata = gen_cli_doc_data_from_meta(meta => $meta);

Sample function metadata (C<$meta>):

 {
   args => {
     bool1 => {
                cmdline_aliases => { z => { summary => "This is summary for option `-z`" } },
                schema => "bool",
                summary => "Another bool option",
                tags => ["category:cat1"],
              },
     flag1 => {
                cmdline_aliases => { f => {} },
                schema => ["bool", "is", 1],
                tags => ["category:cat1"],
              },
     str1  => {
                pos => 0,
                req => 1,
                schema => "str*",
                summary => "A required option as well as positional argument",
              },
   },
   examples => [
     {
       argv    => ["a value", "--bool1"],
       summary => "Summary for an example",
       test    => 0,
     },
   ],
   summary => "Function summary",
   v => 1.1,
 }
Sample result:

 do {
   my $a = [
     200,
     "OK",
     {
       "compact_usage_line" => "[[prog]] [options] <str1>",
       "example_categories" => { Examples => { order => 99 } },
       "examples" => [
         {
           categories   => ["Examples"],
           category     => "Examples",
           cmdline      => "[[prog]] 'a value' --bool1",
           description  => undef,
           example_spec => {
                             argv    => ["a value", "--bool1"],
                             summary => "Summary for an example",
                             test    => 0,
                           },
           summary      => "Summary for an example",
         },
       ],
       "option_categories" => { "Cat1 options" => { order => 50 }, "Main options" => { order => 0 } },
       "opts" => {
         "--bool1" => {
           arg         => "bool1",
           arg_spec    => {
                            cmdline_aliases => { z => { summary => "This is summary for option `-z`" } },
                            schema => ["bool", {}],
                            summary => "Another bool option",
                            tags => ["category:cat1"],
                          },
           categories  => ["Cat1 options"],
           category    => "Cat1 options",
           description => undef,
           fqarg       => "bool1",
           opt_parsed  => { opts => ["bool1"] },
           orig_opt    => "bool1",
           summary     => "Another bool option",
           tags        => 'fix',
         },
         "--flag1, -f" => {
           arg         => "flag1",
           arg_spec    => {
                            cmdline_aliases => { f => {} },
                            schema => ["bool", { is => 1 }],
                            tags => ["category:cat1"],
                          },
           categories  => ["Cat1 options"],
           category    => "Cat1 options",
           description => undef,
           fqarg       => "flag1",
           opt_parsed  => { opts => ["flag1"] },
           orig_opt    => "flag1",
           summary     => undef,
           tags        => 'fix',
         },
         "--str1=s*" => {
           arg => "str1",
           arg_spec => {
             pos => 0,
             req => 1,
             schema => ["str", { req => 1 }],
             summary => "A required option as well as positional argument",
           },
           categories => ["Main options"],
           category => "Main options",
           description => undef,
           fqarg => "str1",
           opt_parsed => { desttype => "", opts => ["str1"], type => "s" },
           orig_opt => "str1=s",
           pos => 0,
           req => 1,
           summary => "A required option as well as positional argument",
         },
         "-z" => {
           alias_for   => "bool1",
           alias_spec  => 'fix',
           arg         => "bool1",
           arg_spec    => 'fix',
           categories  => ["Cat1 options"],
           category    => "Cat1 options",
           description => undef,
           fqarg       => "bool1",
           is_alias    => 1,
           main_opt    => "--bool1",
           opt_parsed  => { opts => ["z"] },
           orig_opt    => "z",
           summary     => "This is summary for option `-z`",
           tags        => 'fix',
         },
       },
       "usage_line" => "[[prog]] [--bool1|-z|--no-bool1|--nobool1] [--flag1|-f] -- <str1>",
       "usage_line.alt.fmt.pod" => "B<[[prog]]> [B<L<--bool1|/\"-z\">>|B<L<-z|/\"-z\">>|B<L<--no-bool1|/\"-z\">>|B<L<--nobool1|/\"-z\">>] [B<L<--flag1|/\"--flag1, -f\">>|B<L<-f|/\"--flag1, -f\">>] -- E<lt>I<L<str1|/\"--str1=s*\">>E<gt>",
     },
   ];
   $a->[2]{"opts"}{"--bool1"}{tags} = $a->[2]{"opts"}{"--bool1"}{arg_spec}{tags};
   $a->[2]{"opts"}{"--flag1, -f"}{tags} = $a->[2]{"opts"}{"--flag1, -f"}{arg_spec}{tags};
   $a->[2]{"opts"}{"-z"}{alias_spec} = $a->[2]{"opts"}{"--bool1"}{arg_spec}{cmdline_aliases}{z};
   $a->[2]{"opts"}{"-z"}{arg_spec} = $a->[2]{"opts"}{"--bool1"}{arg_spec};
   $a->[2]{"opts"}{"-z"}{tags} = $a->[2]{"opts"}{"--bool1"}{arg_spec}{tags};
   $a;
 }
For a more complete sample, see function metadata for C<demo_cli_opts> in
L<Perinci::Examples::CLI>.

=head1 RINCI

Observed function argument attribute: C<x.cli.opt_value_label>, C<caption>, C<>.

=head1 FUNCTIONS


=head2 gen_cli_doc_data_from_meta

Usage:

 gen_cli_doc_data_from_meta(%args) -> [$status_code, $reason, $payload, \%result_meta]

From Rinci function metadata, generate structure convenient for producing CLI documentation (helpE<sol>usageE<sol>POD).

This function calls L<Perinci::Sub::GetArgs::Argv>'s
C<gen_getopt_long_spec_from_meta()> (or receive its result as an argument, if
passed, to avoid calling the function twice) and post-processes it: produce
command usage line, format the options, include information from metadata, group
the options by category. It also selects examples in the C<examples> property
which are applicable to CLI environment and format them.

The resulting data structure is convenient to use when one wants to produce a
documentation for CLI program (including help/usage message and POD).

This function is not exported by default, but exportable.

Arguments ('*' denotes required arguments):

=over 4

=item * B<common_opts> => I<hash>

Will be passed to gen_getopt_long_spec_from_meta().

=item * B<ggls_res> => I<array>

Full result from gen_getopt_long_spec_from_meta().

If you already call L<Perinci::Sub::GetArgs::Argv>'s
C<gen_getopt_long_spec_from_meta()>, you can pass the I<full> enveloped result
here, to avoid calculating twice. What will be useful for the function is the
extra result in result metadata (C<func.*> keys in C<< $res-E<gt>[3] >> hash).

=item * B<lang> => I<str>

(No description)

=item * B<mark_different_lang> => I<bool> (default: 1)

(No description)

=item * B<meta>* => I<hash>

(No description)

=item * B<meta_is_normalized> => I<bool>

(No description)

=item * B<per_arg_json> => I<bool>

Pass per_arg_json=1 to Perinci::Sub::GetArgs::Argv.

=item * B<per_arg_yaml> => I<bool>

Pass per_arg_json=1 to Perinci::Sub::GetArgs::Argv.


=back

Returns an enveloped result (an array).

First element ($status_code) is an integer containing HTTP-like status code
(200 means OK, 4xx caller error, 5xx function error). Second element
($reason) is a string containing error message, or something like "OK" if status is
200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
element (%result_meta) is called result metadata and is optional, a hash
that contains extra information, much like how HTTP response headers provide additional metadata.

Return value:  (hash)

=head1 HOMEPAGE

Please visit the project's homepage at L<https://metacpan.org/release/Perinci-Sub-To-CLIDocData>.

=head1 SOURCE

Source repository is at L<https://github.com/perlancar/perl-Perinci-Sub-To-CLIOptSpec>.

=head1 SEE ALSO

L<Perinci::CmdLine>, L<Perinci::CmdLine::Lite>

L<Pod::Weaver::Plugin::Rinci>

=head1 AUTHOR

perlancar <perlancar@cpan.org>

=head1 CONTRIBUTING


To contribute, you can send patches by email/via RT, or send pull requests on
GitHub.

Most of the time, you don't need to build the distribution yourself. You can
simply modify the code, then test via:

 % prove -l

If you want to build the distribution (e.g. to try to install it locally on your
system), you can install L<Dist::Zilla>,
L<Dist::Zilla::PluginBundle::Author::PERLANCAR>,
L<Pod::Weaver::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
Dist::Zilla- and/or Pod::Weaver plugins. Any additional steps required beyond
that are considered a bug and can be reported to me.

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2022, 2021, 2020, 2019, 2016, 2015, 2014 by perlancar <perlancar@cpan.org>.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=head1 BUGS

Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Perinci-Sub-To-CLIDocData>

When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.