no
warnings
qw< experimental::signatures >
;
{
our
$VERSION
=
'0.003'
}
our
@EXPORT_OK
=
qw< d run >
;
sub
add_auto_commands (
$application
) {
my
$commands
=
$application
->{commands};
$commands
->{help} //= {
name
=>
'help'
,
supports
=> [
'help'
],
help
=>
'print a help message'
,
description
=>
'print help for (sub)command'
,
'allow-residual-options'
=> 0,
leaf
=> 1,
execute
=> \
&stock_help
,
};
$commands
->{commands} //= {
name
=>
'commands'
,
supports
=> [
'commands'
],
help
=>
'list sub-commands'
,
description
=>
'Print list of supported sub-commands'
,
'allow-residual-options'
=> 0,
leaf
=> 1,
execute
=> \
&stock_commands
,
};
return
$application
;
}
sub
collect (
$self
,
$spec
,
$args
) {
my
@sequence
;
my
$config
= {};
my
@residual_args
;
my
$merger
= merger(
$self
,
$spec
);
for
my
$source_spec
(sources(
$self
,
$spec
,
$args
)) {
my
(
$src
,
$src_cnf
) =
'ARRAY'
eq
ref
$source_spec
?
$source_spec
->@*
: (
$source_spec
, {});
$src
=
$self
->{factory}->(
$src
,
'collect'
);
$src_cnf
= {
$spec
->%*,
$src_cnf
->%*,
config
=>
$config
};
my
(
$slice
,
$residual_args
) =
$src
->(
$self
,
$src_cnf
,
$args
);
push
@residual_args
,
$residual_args
->@*
if
defined
$residual_args
;
push
@sequence
,
$slice
;
$config
=
$merger
->(
@sequence
);
}
return
(
$config
, \
@residual_args
);
}
sub
collect_options (
$self
,
$spec
,
$args
) {
my
$factory
=
$self
->{factory};
my
$collect
=
$spec
->{collect}
//
$self
->{application}{configuration}{collect} // \
&collect
;
my
$collector
=
$factory
->(
$collect
,
'collect'
);
(
my
$config
,
$args
) =
$collector
->(
$self
,
$spec
,
$args
);
push
$self
->{configs}->@*,
$config
;
return
$args
;
}
sub
commandline_help (
$getopt
) {
my
@retval
;
my
(
$mode
,
$type
,
$desttype
,
$min
,
$max
,
$default
);
if
(
substr
(
$getopt
, -1, 1) eq
'!'
) {
$type
=
'bool'
;
substr
$getopt
, -1, 1,
''
;
push
@retval
,
'boolean option'
;
}
elsif
(
substr
(
$getopt
, -1, 1) eq
'+'
) {
$mode
=
'increment'
;
substr
$getopt
, -1, 1,
''
;
push
@retval
,
'incremental option (adds 1 every time it is provided)'
;
}
elsif
(
$getopt
=~ s<(
[:=])
([siof])
([@%])?
(?:
\{
(\d*)?
,?
(\d*)?
\}
)? \z><>mxs
)
{
$mode
= $1 eq
'='
?
'mandatory'
:
'optional'
;
$type
= $2;
$desttype
= $3;
$min
= $4;
$max
= $5;
if
(
defined
$min
) {
$mode
=
$min
?
'optional'
:
'required'
;
}
$type
= {
s
=>
'string'
,
i
=>
'integer'
,
o
=>
'perl-extended-integer'
,
f
=>
'float'
,
}->{
$type
};
my
$line
=
"$mode $type option"
;
$line
.=
", at least $min times"
if
defined
(
$min
) &&
$min
> 1;
$line
.=
", no more than $max times"
if
defined
(
$max
) &&
length
(
$max
);
$line
.=
", list valued"
if
defined
(
$desttype
) &&
$desttype
eq
'@'
;
push
@retval
,
$line
;
}
elsif
(
$getopt
=~ s<: (\d+) ([@%])? \z><>mxs) {
$mode
=
'optional'
;
$type
=
'i'
;
$default
= $1;
$desttype
= $2;
my
$line
=
"optional integer, defaults to $default"
;
$line
.=
", list valued"
if
defined
(
$desttype
) &&
$desttype
eq
'@'
;
push
@retval
,
$line
;
}
elsif
(
$getopt
=~ s<:+ ([@%])? \z><>mxs) {
$mode
=
'optional'
;
$type
=
'i'
;
$default
=
'increment'
;
$desttype
= $1;
my
$line
=
"optional integer, current value incremented if omitted"
;
$line
.=
", list valued"
if
defined
(
$desttype
) &&
$desttype
eq
'@'
;
push
@retval
,
$line
;
}
my
@alternatives
=
split
/\|/,
$getopt
;
if
(
$type
eq
'bool'
) {
push
@retval
,
map
{
if
(
length
(
$_
) == 1) {
"-$_"
}
else
{
"--$_ | --no-$_"
}
}
@alternatives
;
}
elsif
(
$mode
eq
'optional'
) {
push
@retval
,
map
{
if
(
length
(
$_
) == 1) {
"-$_ [<value>]"
}
else
{
"--$_ [<value>]"
}
}
@alternatives
;
}
else
{
push
@retval
,
map
{
if
(
length
(
$_
) == 1) {
"-$_ <value>"
}
else
{
"--$_ <value>"
}
}
@alternatives
;
}
return
@retval
;
}
sub
commit_configuration (
$self
,
$spec
,
$args
) {
my
$commit
=
$spec
->{commit} //
return
;
$self
->{factory}->(
$commit
,
'commit'
)->(
$self
,
$spec
,
$args
);
}
sub
d (
@stuff
) {
no
warnings;
local
$Data::Dumper::Indent
= 1;
warn
Data::Dumper::Dumper(
@stuff
% 2 ? \
@stuff
: {
@stuff
});
}
sub
default_getopt_config (
$has_children
) {
my
@r
=
qw< gnu_getopt >
;
push
@r
,
qw< require_order pass_through >
if
$has_children
;
return
\
@r
;
}
sub
execute (
$self
,
$args
) {
my
$command
=
$self
->{trail}[-1][0];
my
$executable
=
$self
->{application}{commands}{
$command
}{execute}
or
die
"no executable for '$command'\n"
;
$executable
=
$self
->{factory}->(
$executable
,
'execute'
);
my
$config
=
$self
->{configs}[-1] // {};
return
$executable
->(
$self
,
$config
,
$args
);
}
sub
fetch_subcommand (
$self
,
$spec
,
$args
) {
return
unless
has_children(
$self
,
$spec
);
my
(
$child
,
$candidate
,
$candidate_from_args
);
if
(
$args
->@*) {
$candidate
=
$args
->[0];
$candidate_from_args
= 1;
}
elsif
(
exists
$spec
->{
'default-child'
}) {
$candidate
=
$child
=
$spec
->{
'default-child'
};
return
unless
defined
$child
&&
length
$child
;
}
elsif
(
exists
$self
->{application}{configuration}{
'default-child'
}) {
$candidate
=
$child
=
$self
->{application}{configuration}{
'default-child'
};
return
unless
defined
$child
&&
length
$child
;
}
else
{
$candidate
=
'help'
;
}
if
(
$child
//= get_child(
$self
,
$spec
,
$candidate
)) {
shift
$args
->@*
if
$candidate_from_args
;
return
(
$child
,
$candidate
//
$child
);
}
my
@names
=
map
{
$_
->[1] }
$self
->{trail}->@*;
shift
@names
;
my
$path
=
join
'/'
,
@names
,
$candidate
;
die
"cannot find sub-command '$path'\n"
;
}
sub
generate_factory (
$c
) {
my
$w
= \
&stock_factory
;
$w
= stock_factory(
$c
->{create},
'factory'
,
$c
)
if
defined
$c
->{create};
return
sub
(
$e
,
$d
=
''
) {
$w
->(
$e
,
$d
,
$c
) };
}
sub
get_child (
$self
,
$spec
,
$name
) {
for
my
$child
(get_children(
$self
,
$spec
)) {
my
$command
=
$self
->{application}{commands}{
$child
};
next
unless
grep
{
$_
eq
$name
}
(
$command
->{supports} //= [
$child
])->@*;
return
$child
;
}
return
;
}
sub
get_children (
$self
,
$spec
) {
return
if
$spec
->{leaf};
return
if
exists
(
$spec
->{children}) && !
$spec
->{children};
my
@children
= (
$spec
->{children} // [])->@*;
return
if
$self
->{application}{configuration}{
'auto-leaves'
}
&&
@children
== 0;
my
@auto
=
exists
$self
->{application}{configuration}{
'auto-children'
}
? ((
$self
->{application}{configuration}{
'auto-children'
} // [])->@*)
: (
qw< help commands >
);
if
(
exists
$spec
->{
'no-auto'
}) {
if
(
ref
$spec
->{
'no-auto'
}) {
my
%no
=
map
{
$_
=> 1 }
$spec
->{
'no-auto'
}->@*;
@auto
=
grep
{ !
$no
{
$_
} }
@auto
;
}
elsif
(
$spec
->{
'no-auto'
} eq
'*'
) {
@auto
= ();
}
else
{
die
"invalid no-auto, array or '*' are allowed\n"
;
}
}
return
(
@children
,
@auto
);
}
sub
get_descendant (
$self
,
$start
,
$list
) {
my
$target
=
$start
;
my
$cmds
=
$self
->{application}{commands};
my
$path
;
for
my
$desc
(
$list
->@*) {
$path
=
defined
(
$path
) ?
"$path/$desc"
:
$desc
;
my
$command
=
$cmds
->{
$target
}
or
die
"cannot find sub-command '$path'\n"
;
defined
(
$target
= get_child(
$self
,
$command
,
$desc
))
or
die
"cannot find sub-command '$path'\n"
;
}
$cmds
->{
$target
} or
die
"cannot find sub-command '$path'\n"
;
return
$target
;
}
sub
has_children (
$self
,
$spec
) { get_children(
$self
,
$spec
) ? 1 : 0 }
sub
hash_merge {
return
{
map
{
$_
->%* }
reverse
@_
};
}
sub
list_commands (
$self
,
$children
) {
my
$retval
=
''
;
open
my
$fh
,
'>'
, \
$retval
;
for
my
$child
(
$children
->@*) {
my
$command
=
$self
->{application}{commands}{
$child
};
my
$help
=
$command
->{help};
my
@aliases
= (
$command
->{supports} // [
$child
])->@*;
next
unless
@aliases
;
printf
{
$fh
}
"%15s: %s\n"
,
shift
(
@aliases
),
$help
;
printf
{
$fh
}
"%15s (also as: %s)\n"
,
''
,
join
', '
,
@aliases
if
@aliases
;
}
close
$fh
;
return
$retval
;
}
sub
load_application (
$application
) {
return
$application
if
'HASH'
eq
ref
$application
;
my
$text
;
if
(
'SCALAR'
eq
ref
$application
) {
$text
=
$$application
;
}
else
{
my
$fh
=
'GLOB'
eq
ref
$application
?
$application
:
do
{
open
my
$fh
,
'<:encoding(UTF-8)'
,
$application
or
die
"cannot open '$application'\n"
;
$fh
;
};
local
$/;
$text
= <
$fh
>;
close
$fh
;
}
return
eval
{
JSON::PP::decode_json(
$text
);
} //
eval
{
eval
$text
; } //
die
"cannot load application\n"
;
}
sub
merger (
$self
,
$spec
= {}) {
my
$merger
=
$spec
->{merge}
//
$self
->{application}{configuration}{merge} // \
&hash_merge
;
return
$self
->{factory}->(
$merger
,
'merge'
);
}
sub
name_for_option (
$o
) {
return
$o
->{name}
if
defined
$o
->{name};
return
$1
if
defined
$o
->{getopt} &&
$o
->{getopt} =~ m{\A(\w+)}mxs;
return
lc
$o
->{environment}
if
defined
$o
->{environment};
return
'~~~'
;
}
sub
params_validate (
$self
,
$spec
,
$args
) {
my
$validator
=
$spec
->{validate}
//
$self
->{application}{configuration}{validate} //
return
;
Params::Validate::validate(
$self
->{configs}[-1]->%*,
$validator
);
}
sub
print_commands (
$self
,
$target
) {
my
$command
=
$self
->{application}{commands}{
$target
};
my
$fh
=
$self
->{application}{configuration}{
'help-on-stderr'
}
? \
*STDERR
: \
*STDOUT
;
if
(
my
@children
= get_children(
$self
,
$command
)) {
print
{
$fh
} list_commands(
$self
, \
@children
);
}
else
{
print
{
$fh
}
"no sub-commands\n"
;
}
}
sub
print_help (
$self
,
$target
) {
my
$command
=
$self
->{application}{commands}{
$target
};
my
$fh
=
$self
->{application}{configuration}{
'help-on-stderr'
}
? \
*STDERR
: \
*STDOUT
;
print
{
$fh
}
$command
->{help},
"\n\n"
;
if
(
defined
(
my
$description
=
$command
->{description})) {
$description
=~ s{\A\s+|\s+\z}{}gmxs;
$description
=~ s{^}{ }gmxs;
print
{
$fh
}
"Description:\n$description\n\n"
;
}
printf
{
$fh
}
"Can be called as: %s\n\n"
,
join
', '
,
$command
->{supports}->@*
if
$command
->{supports};
my
$options
=
$command
->{options} // [];
if
(
$options
->@*) {
print
{
$fh
}
"Options:\n"
;
my
$n
= 0;
for
my
$option
(
$options
->@*) {
print
{
$fh
}
"\n"
if
$n
++;
printf
{
$fh
}
"%15s: %s\n"
, name_for_option(
$option
),
$option
->{help} //
''
;
if
(
exists
$option
->{getopt}) {
my
@lines
= commandline_help(
$option
->{getopt});
printf
{
$fh
}
"%15s command-line: %s\n"
,
''
,
shift
(
@lines
);
printf
{
$fh
}
"%15s %s\n"
,
''
,
$_
for
@lines
;
}
printf
{
$fh
}
"%15s environment : %s\n"
,
''
,
$option
->{environment} //
'*undef*'
if
exists
$option
->{environment};
printf
{
$fh
}
"%15s default : %s\n"
,
''
,
$option
->{
default
} //
'*undef*'
if
exists
$option
->{
default
};
}
print
{
$fh
}
"\n"
;
}
else
{
print
{
$fh
}
"This command has no options.\n\n"
;
}
if
(
my
@children
= get_children(
$self
,
$command
)) {
print
{
$fh
}
"Sub commands:\n"
, list_commands(
$self
, \
@children
),
"\n"
;
}
else
{
print
{
$fh
}
"no sub-commands\n\n"
;
}
}
sub
run (
$application
,
$args
) {
$application
= add_auto_commands(load_application(
$application
));
my
$self
= {
application
=>
$application
,
configs
=> [],
factory
=> generate_factory(
$application
->{factory} // {}),
helpers
=> {
'print-commands'
=> \
&print_commands
,
'print-help'
=> \
&print_help
,
},
trail
=> [[
'MAIN'
,
$application
->{commands}{MAIN}{name}]],
};
while
(
'necessary'
) {
my
$command
=
$self
->{trail}[-1][0];
my
$spec
=
$application
->{commands}{
$command
}
or
die
"no definition for '$command'\n"
;
$args
= collect_options(
$self
,
$spec
,
$args
);
validate_configuration(
$self
,
$spec
,
$args
);
commit_configuration(
$self
,
$spec
,
$args
);
my
(
$subc
,
$alias
) = fetch_subcommand(
$self
,
$spec
,
$args
) or
last
;
push
$self
->{trail}->@*, [
$subc
,
$alias
];
}
return
execute(
$self
,
$args
) // 0;
}
sub
slurp (
$file
,
$mode
=
'<:encoding(UTF-8)'
) {
open
my
$fh
,
$mode
,
$file
or
die
"open('$file'): $!\n"
;
local
$/;
return
<
$fh
>;
}
sub
sources (
$self
,
$spec
,
$args
) {
my
$s
=
$spec
->{sources}
//
$self
->{application}{configuration}{sources}
// \
&stock_DefaultSources
;
$s
=
$self
->{factory}->(
$s
,
'sources'
)->()
if
'ARRAY'
ne
ref
$s
;
return
$s
->@*;
}
sub
stock_CmdLine (
$self
,
$spec
,
$args
) {
my
@args
=
$args
->@*;
my
$goc
=
$spec
->{getopt_config}
// default_getopt_config(
scalar
(get_children(
$self
,
$spec
)));
Getopt::Long::Configure(
'default'
,
$goc
->@*);
my
%option_for
;
my
@specs
=
map
{
my
$go
=
$_
->{getopt};
ref
(
$go
) eq
'ARRAY'
? (
$go
->[0] =>
sub
{
$go
->[1]->(\
%option_for
,
@_
) })
:
$go
;
}
grep
{
exists
$_
->{getopt} } (
$spec
->{options} // [])->@*;
Getopt::Long::GetOptionsFromArray(\
@args
, \
%option_for
,
@specs
)
or
die
"bailing out\n"
;
my
$strict
= !
$spec
->{
'allow-residual-optionss'
};
if
(
$strict
&&
@args
&&
$args
[0] =~ m{\A -}mxs) {
Getopt::Long::Configure(
'default'
,
'gnu_getopt'
);
Getopt::Long::GetOptionsFromArray(\
@args
, {});
die
"bailing out\n"
;
}
return
(\
%option_for
, \
@args
);
}
sub
stock_JsonFileFromConfig (
$self
,
$spec
,
$args
) {
my
$key
=
$spec
->{
'config-option'
} //
'config'
;
return
{}
if
!
exists
(
$spec
->{config}{
$key
});
return
JSON::PP::decode_json(slurp(
$spec
->{config}{
$key
}));
}
sub
stock_JsonFiles (
$self
,
$spec
,
@ignore
) {
return
merger(
$self
,
$spec
)->(
map
{
JSON::PP::decode_json(slurp(
$_
));
}
grep
{ -e
$_
} (
$spec
->{
'config-files'
} // [])->@*
);
}
sub
stock_Default (
$self
,
$spec
,
@ignore
) {
return
{
map
{ name_for_option(
$_
) =>
$_
->{
default
} }
grep
{
exists
$_
->{
default
} } (
$spec
->{options} // [])->@*
};
}
sub
stock_Environment (
$self
,
$spec
,
@ignore
) {
return
{
map
{ name_for_option(
$_
) =>
$ENV
{
$_
->{environment}} }
grep
{
exists
(
$_
->{environment})
&&
exists
(
$ENV
{
$_
->{environment}})
} (
$spec
->{options} // [])->@*
};
}
sub
stock_Parent (
$self
,
$spec
,
@ignore
) {
$self
->{configs}[-1] // {} }
sub
stock_commands (
$self
,
$config
,
$args
) {
die
"this command does not support arguments\n"
if
$args
->@*;
my
$target
= get_descendant(
$self
,
$self
->{trail}[-2][0],
$args
);
print_commands(
$self
,
$target
);
return
0;
}
sub
stock_factory (
$executable
,
$default_subname
=
''
,
$opts
= {}) {
state
$factory
=
sub
(
$executable
,
$default_subname
) {
my
@prefixes
=
!
defined
$opts
->{prefixes} ? ()
:
'ARRAY'
eq
ref
$opts
->{prefixes} ? (
$opts
->{prefixes}->@*)
: (
$opts
->{prefixes});
push
@prefixes
, {
'+'
=>
'App::Easer#stock_'
};
SEARCH:
for
my
$expansion_for
(
@prefixes
) {
for
my
$p
(
keys
$expansion_for
->%*) {
next
if
$p
ne
substr
$executable
, 0,
length
$p
;
substr
$executable
, 0,
length
$p
,
$expansion_for
->{
$p
};
last
SEARCH;
}
}
return
eval
$executable
if
$executable
=~ s{\A \s* = \s* }{}mxs;
my
(
$package
,
$sname
) =
split
m{\
$sname
=
$default_subname
unless
defined
$sname
&&
length
$sname
;
if
(
my
$s
=
$package
->can(
$sname
)) {
return
$s
}
(
my
$path
=
"$package.pm"
) =~ s{::}{/}gmxs;
require
$path
;
if
(
my
$s
=
$package
->can(
$sname
)) {
return
$s
}
die
"no '$sname' in '$package'\n"
;
};
state
$cache
= {};
my
$args
;
(
$executable
,
$args
) = (
$executable
->{executable},
$executable
)
if
'HASH'
eq
ref
$executable
;
$executable
=
$cache
->{
$executable
.
' '
.
$default_subname
} //=
$factory
->(
$executable
,
$default_subname
)
if
'CODE'
ne
ref
$executable
;
return
$executable
unless
$args
;
return
sub
{
$executable
->(
$args
,
@_
) };
}
sub
stock_help (
$self
,
$config
,
$args
) {
print_help(
$self
, get_descendant(
$self
,
$self
->{trail}[-2][0],
$args
));
return
0;
}
sub
stock_DefaultSources { [
qw< +CmdLine +Environment +Parent +Default >
] }
sub
stock_SourcesWithFiles {
[
qw< +CmdLine +Environment +Parent +JsonFileFromConfig +JsonFiles
+Default >
]
}
sub
validate_configuration (
$self
,
$spec
,
$args
) {
my
$from_spec
=
$spec
->{validate};
my
$from_self
=
$self
->{application}{configuration}{validate};
my
$validator
;
if
(
defined
$from_spec
&&
'HASH'
ne
ref
$from_spec
) {
$validator
=
$self
->{factory}->(
$from_spec
,
'validate'
);
}
elsif
(
defined
$from_self
&&
'HASH'
ne
ref
$from_self
) {
$validator
=
$self
->{factory}->(
$from_self
,
'validate'
);
}
else
{
$validator
= \
¶ms_validate
;
}
$validator
->(
$self
,
$spec
,
$args
);
}
exit
run(
$ENV
{APPEASER} // {
commands
=> {
MAIN
=> {
name
=>
'main app'
,
help
=>
'this is the main app'
,
description
=>
'Yes, this really is the main app'
,
options
=> [
{
name
=>
'foo'
,
description
=>
'option foo!'
,
getopt
=>
'foo|f=s'
,
environment
=>
'FOO'
,
default
=>
'bar'
,
},
],
execute
=>
sub
(
$global
,
$conf
,
$args
) {
my
$foo
=
$conf
->{foo};
say
"Hello, $foo!"
;
return
0;
},
'default-child'
=>
''
,
},
},
},
[
@ARGV
]
)
unless
caller
;
1;