Perinci-Sub-To-CLIDocData
view release on metacpan or search on metacpan
lib/Perinci/Sub/To/CLIDocData.pm view on Meta::CPAN
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},
lib/Perinci/Sub/To/CLIDocData.pm view on Meta::CPAN
# 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};
lib/Perinci/Sub/To/CLIDocData.pm view on Meta::CPAN
$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",
( run in 1.434 second using v1.01-cache-2.11-cpan-d06a3f9ecfd )