no
warnings
qw< experimental::signatures >
;
{
our
$VERSION
=
'2.001'
}
our
@EXPORT_OK
=
qw< appeaser_api d dd run >
;
sub
appeaser_api { __PACKAGE__ =~ s{.*::}{}rmxs }
sub
d {
warn
dd(
@_
) }
sub
dd (
@stuff
) {
no
warnings;
local
$Data::Dumper::Indent
= 1;
Data::Dumper::Dumper(
@stuff
== 0 ? []
: (
ref
(
$stuff
[0]) ||
@stuff
% 2) ? \
@stuff
: {
@stuff
}
);
}
sub
run (
$app
,
@args
) {
my
$class
=
'App::Easer::V2::Command'
;
my
$instance
=
ref
(
$app
) eq
'HASH'
?
$class
->new(
$app
)
:
ref
(
$app
) eq
'ARRAY'
?
$class
->instantiate(
$app
->@*)
:
$class
->instantiate(
$app
);
return
$instance
->run(
@args
);
}
sub
import
(
$package
,
@args
) {
my
$target
=
caller
;
my
@args_for_exporter
;
our
%registered
;
while
(
@args
) {
my
$request
=
shift
@args
;
if
(
$request
eq
'-command'
) {
$registered
{
$target
} = 1;
no
strict
'refs'
;
push
@{
$target
.
'::ISA'
},
'App::Easer::V2::Command'
;
}
elsif
(
$request
eq
'-inherit'
) {
no
strict
'refs'
;
push
@{
$target
.
'::ISA'
},
'App::Easer::V2::Command'
;
}
elsif
(
$request
eq
'-register'
) {
$registered
{
$target
} = 1;
}
elsif
(
$request
eq
'-spec'
) {
Carp::croak
"no specification provided"
unless
@args
;
Carp::croak
"invalid specification provided"
unless
ref
(
$args
[0]) eq
'HASH'
;
no
strict
'refs'
;
no
warnings
'once'
;
${
$target
.
'::app_easer_spec'
} =
shift
@args
;
}
else
{
push
@args_for_exporter
,
$request
}
}
$package
->export_to_level(1,
$package
,
@args_for_exporter
);
}
sub
slot (
$self
) {
return
$self
->{blessed(
$self
)} //= {} }
sub
_rwn (
$self
,
$name
,
@newval
) {
my
$vref
= \
$self
->slot->{
$name
};
$$vref
=
$newval
[0]
if
@newval
;
return
$$vref
;
}
sub
_rw (
$s
,
@n
) {
$s
->_rwn((
caller
(1))[3] =~ s{.*::}{}rmxs,
@n
) }
sub
_rwa (
$self
,
@n
) {
my
$aref
=
$self
->_rwn((
caller
(1))[3] =~ s{.*::}{}rmxs,
@n
);
Carp::confess()
unless
defined
$aref
;
return
$aref
->@*;
}
sub
_rwad (
$self
,
@n
) {
my
$aref
=
$self
->_rwn((
caller
(1))[3] =~ s{.*::}{}rmxs,
@n
) // [];
return
wantarray
?
$aref
->@* : [
$aref
->@*];
}
sub
aliases (
$self
,
@r
) {
if
(
my
@aliases
=
$self
->_rwad(
@r
)) {
return
@aliases
}
if
(
defined
(
my
$name
=
$self
->_rwn(
'name'
))) {
return
$name
}
return
;
}
sub
allow_residual_options (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
auto_environment (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
call_name (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
children (
$self
,
@r
) {
$self
->_rwa(
@r
) }
sub
children_prefixes (
$self
,
@r
) {
$self
->_rwa(
@r
) }
sub
default_child (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
description (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
environment_prefix (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
execution_reason (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
force_auto_children (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
fallback_to (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
hashy_class (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
help (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
help_channel (
$slf
,
@r
) {
$slf
->_rw(
@r
) }
sub
name (
$s
,
@r
) {
$s
->_rw(
@r
) // (
$s
->aliases)[0] //
'**no name**'
}
sub
params_validate (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
parent (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
residual_args (
$self
,
@r
) {
$self
->_rwa(
@r
) }
sub
sources (
$self
,
@r
) {
$self
->_rwa(
@r
) }
sub
supports (
$self
,
$what
) {
any {
$_
eq
$what
}
$self
->aliases;
}
sub
options (
$self
,
@r
) {
return
map
{
$self
->resolve_options(
$_
) }
$self
->_rwa(
@r
);
}
sub
resolve_options (
$self
,
$spec
) {
return
$spec
if
ref
(
$spec
) eq
'HASH'
;
$spec
= [
inherit_options
=>
$spec
]
unless
ref
$spec
;
Carp::confess(
"invalid spec $spec"
)
unless
ref
(
$spec
) eq
'ARRAY'
;
my
(
$method_name
,
@names
) =
$spec
->@*;
my
$method
=
$self
->can(
$method_name
)
or Carp::confess(
"cannot find method $method_name in $self"
);
return
$self
->
$method
(
@names
);
}
sub
inherit_options (
$self
,
@names
) {
my
%got
;
map
{
my
@options
;
if
(
$_
eq
'+parent'
) {
@options
=
grep
{
$_
->{transmit} // 0 }
$self
->parent->options;
}
else
{
my
$namerx
=
qr{\A(?:$_)\z}
;
my
$ancestor
=
$self
->parent;
while
(
$ancestor
) {
push
@options
,
my
@pass
=
grep
{
my
$name
=
$self
->name_for_option(
$_
);
(!
$_
->{transmit_exact})
&&
$name
=~ m{
$namerx
}
&& !
$got
{
$name
};
}
$ancestor
->options;
$ancestor
=
$ancestor
->parent;
}
}
map
{ +{
transmit
=> 1,
$_
->%*,
inherited
=> 1} }
@options
;
}
@names
;
}
sub
new (
$pkg
,
@args
) {
my
$pkg_spec
=
do
{
no
strict
'refs'
; ${
$pkg
.
'::app_easer_spec'
} };
my
$slot
= {
aliases
=> [],
allow_residual_options
=> 0,
auto_environment
=> 0,
children
=> [],
children_prefixes
=> [
$pkg
.
'::Cmd'
],
default_child
=>
'help'
,
environment_prefix
=>
''
,
fallback_to
=>
undef
,
force_auto_children
=>
undef
,
hashy_class
=> __PACKAGE__,
help_channel
=>
'-STDOUT:encoding(UTF-8)'
,
options
=> [],
params_validate
=>
undef
,
sources
=> [
qw< +CmdLine +Environment +Parent=70 +Default=100 >
],
(
$pkg_spec
// {})->%*,
(
@args
&&
ref
$args
[0] ?
$args
[0]->%* :
@args
),
};
my
$self
=
bless
{
$pkg
=>
$slot
},
$pkg
;
return
$self
;
}
sub
merge_hashes (
$self
,
@hrefs
) {
my
(
%retval
,
%is_overridable
);
for
my
$href
(
@hrefs
) {
for
my
$src_key
(
keys
$href
->%*) {
my
$dst_key
=
$src_key
;
my
$this_overridable
= 0;
$retval
{
$dst_key
} =
$href
->{
$src_key
}
if
$is_overridable
{
$dst_key
} || !
exists
(
$retval
{
$dst_key
});
$is_overridable
{
$dst_key
} = 0
unless
$this_overridable
;
}
}
return
\
%retval
;
}
sub
collect (
$self
,
@args
) {
my
@sequence
;
my
@slices
;
my
$config
= {};
my
@residual_args
;
my
$last_priority
= 0;
for
my
$source
(
$self
->sources) {
my
(
$src
,
@opts
) =
ref
(
$source
) eq
'ARRAY'
?
$source
->@* :
$source
;
my
$meta
= (
@opts
&&
ref
$opts
[0]) ?
shift
@opts
: {};
my
$locator
=
$src
;
if
(!
ref
(
$src
)) {
(
$src
,
my
$priority
) =
split
m{=}mxs,
$src
;
$meta
->{priority} =
$priority
if
defined
$priority
;
$locator
=
$src
=~ s{\A \+}{source_}rmxs;
}
my
$sub
=
$self
->ref_to_sub(
$locator
)
or
die
"unhandled source for $locator\n"
;
my
(
$slice
,
$residuals
) =
$sub
->(
$self
, \
@opts
, \
@args
);
push
@residual_args
,
$residuals
->@*
if
defined
$residuals
;
$last_priority
=
my
$priority
=
$meta
->{priority} //=
$last_priority
+ 10;
push
@sequence
, [
$priority
,
$src
, \
@opts
,
$locator
,
$slice
];
for
(
my
$i
=
$#sequence
;
$i
> 0; --
$i
) {
last
if
$sequence
[
$i
- 1][0] <=
$sequence
[
$i
][0];
@sequence
[
$i
- 1,
$i
] =
@sequence
[
$i
,
$i
- 1];
}
$config
=
$self
->merge_hashes(
map
{
$_
->[-1]}
@sequence
);
$self
->_rwn(
config
=> {
merged
=>
$config
,
sequence
=> \
@sequence
});
}
$self
->residual_args(\
@residual_args
);
return
$self
;
}
sub
getopt_config (
$self
,
@n
) {
my
$value
=
$self
->_rw(
@n
);
if
(!
defined
$value
) {
my
@r
=
qw< gnu_getopt >
;
push
@r
,
qw< require_order pass_through >
if
$self
->list_children;
push
@r
,
qw< pass_through >
if
$self
->allow_residual_options;
$value
=
$self
->_rw(\
@r
);
}
return
$value
->@*;
}
sub
source_CmdLine (
$self
,
$ignore
,
$args
) {
my
@args
=
$args
->@*;
Getopt::Long::Configure(
'default'
,
$self
->getopt_config);
my
%option_for
;
my
@specs
=
map
{
my
$go
=
$_
->{getopt};
ref
(
$go
) eq
'ARRAY'
? (
$go
->[0] =>
sub
{
$go
->[1]->(\
%option_for
,
@_
) })
:
$go
;
}
grep
{
exists
$_
->{getopt} }
$self
->options;
Getopt::Long::GetOptionsFromArray(\
@args
, \
%option_for
,
@specs
)
or
die
"bailing out\n"
;
my
$strict
= !
$self
->allow_residual_options;
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
name_for_option (
$self
,
$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} &&
$o
->{environment} ne
'1'
;
return
'~~~'
;
}
sub
source_Default (
$self
,
@ignore
) {
return
{
map
{
$self
->name_for_option(
$_
) =>
$_
->{
default
} }
grep
{
exists
$_
->{
default
} }
grep
{ !
$_
->{inherited} }
$self
->options
};
}
sub
source_FromTrail (
$self
,
$trail
,
@ignore
) {
my
$conf
=
$self
->config_hash;
for
my
$key
(
$trail
->@*) {
return
{}
unless
defined
$conf
->{
$key
};
$conf
=
$conf
->{
$key
};
die
"invalid trail $trail->@* for configuration gathering"
unless
ref
(
$conf
) eq
'HASH'
;
}
return
$conf
;
}
sub
environment_variable_name (
$self
,
$ospec
) {
my
$env
=
exists
$ospec
->{environment} ?
$ospec
->{environment}
:
$self
->auto_environment ? 1
:
undef
;
return
$env
unless
(
$env
//
''
) eq
'1'
;
my
@prefixes
;
for
(
my
$instance
=
$self
;
$instance
;
$instance
=
$instance
->parent) {
unshift
@prefixes
,
$instance
->environment_prefix //
''
;
}
return
uc
(
join
''
,
@prefixes
,
$self
->name_for_option(
$ospec
));
}
sub
source_Environment (
$self
,
@ignore
) {
return
{
map
{
my
$en
=
$self
->environment_variable_name(
$_
);
defined
(
$en
)
&&
exists
(
$ENV
{
$en
})
? (
$self
->name_for_option(
$_
) =>
$ENV
{
$en
})
: ();
}
grep
{ !
$_
->{inherited} }
$self
->options
};
}
sub
source_JsonFileFromConfig (
$self
,
$key
,
@ignore
) {
$key
=
$key
->[0] //
'config'
;
defined
(
my
$filename
=
$self
->config(
$key
)) or
return
{};
return
JSON::PP::decode_json(
$self
->slurp(
$filename
));
}
sub
slurp (
$self
,
$file
,
$mode
=
'<:encoding(UTF-8)'
) {
open
my
$fh
,
$mode
,
$file
or
die
"open('$file'): $!\n"
;
local
$/;
return
<
$fh
>;
}
sub
source_JsonFiles (
$self
,
$candidates
,
@ignore
) {
return
$self
->merge_hashes(
map
{ JSON::PP::decode_json(
$self
->slurp(
$_
)) }
grep
{ -e
$_
}
$candidates
->@*
);
}
sub
source_Parent (
$self
,
@ignore
) {
my
$parent
=
$self
->parent or
return
{};
return
$parent
->config_hash(0);
}
sub
config_hash (
$self
,
$blame
= 0) {
my
$config
=
$self
->_rwn(
'config'
) // {};
return
$config
if
$blame
;
return
$config
->{merged} // {};
}
sub
config (
$self
,
@keys
) {
my
$hash
=
$self
->config_hash(0);
return
$hash
->{
$keys
[0]}
if
@keys
== 1;
return
$hash
->@{
@keys
};
}
sub
set_config (
$self
,
$key
,
@value
) {
my
$hash
=
$self
->config_hash(0);
delete
$hash
->{
$key
};
$hash
->{
$key
} =
$value
[0]
if
@value
;
return
$self
;
}
sub
commit (
$self
,
@n
) {
my
$commit
=
$self
->_rw(
@n
);
return
$commit
if
@n
;
return
unless
$commit
;
return
$self
->ref_to_sub(
$commit
)->(
$self
);
}
sub
validate (
$self
) {
my
$validator
=
$self
->params_validate //
return
;
if
(
my
$config_validator
=
$validator
->{config} //
undef
) {
my
@array
=
$self
->config_hash;
Params::Validate::validate(
@array
,
$config_validator
);
}
if
(
my
$args_validator
=
$validator
->{args} //
undef
) {
my
@array
=
$self
->residual_args;
Params::Validate::validate_pos(
@array
,
$args_validator
->@*);
}
return
$self
;
}
sub
find_matching_child (
$self
,
$command
) {
return
unless
defined
$command
;
for
my
$candidate
(
$self
->list_children) {
my
(
$child
) =
$self
->inflate_children(
$candidate
);
return
$child
if
$child
->supports(
$command
);
}
return
;
}
sub
_inflate_default_child (
$self
) {
defined
(
my
$default
=
$self
->default_child)
or
die
"undefined default child\n"
;
return
undef
if
$default
eq
'-self'
;
my
$child
=
$self
->find_matching_child(
$default
)
or
die
"no child matching the default $default\n"
;
return
$child
;
}
sub
find_child (
$self
) {
my
@candidates
=
$self
->list_children or
return
(
undef
,
'-leaf'
);
my
@residuals
=
$self
->residual_args;
if
(
@residuals
) {
if
(
my
$child
=
$self
->find_matching_child(
$residuals
[0])) {
return
(
$child
,
@residuals
);
}
}
elsif
(
defined
(
my
$default
=
$self
->default_child)) {
return
(
$self
->_inflate_default_child,
'-default'
);
}
my
$fallback
=
$self
->fallback;
if
(
defined
$fallback
) {
return
(
undef
,
'-fallback'
)
if
$fallback
eq
'-self'
;
return
(
$self
->_inflate_default_child,
'-default'
)
if
$fallback
eq
'-default'
;
if
(
my
$child
=
$self
->find_matching_child(
$fallback
)) {
return
(
$child
,
-fallback
=>
@residuals
);
}
}
die
"cannot find sub-command '$residuals[0]'\n"
;
}
sub
list_children (
$self
) {
my
@children
=
$self
->children;
my
@expanded_inc
=
map
{
my
(
$v
,
$dirs
) = File::Spec->splitpath(
$_
,
'no-file'
);
[
$v
, File::Spec->splitdir(
$dirs
)];
}
@INC
;
my
%seen
;
push
@children
,
map
{
my
@parts
=
split
m{::}mxs,
$_
.
'x'
;
substr
(
my
$bprefix
=
pop
@parts
, -1, 1,
''
);
map
{
my
(
$v
,
@dirs
) =
$_
->@*;
my
$dirs
= File::Spec->catdir(
@dirs
,
@parts
);
if
(
opendir
my
$dh
, File::Spec->catpath(
$v
,
$dirs
,
''
)) {
grep
{ !
$seen
{
$_
}++ }
map
{
substr
(
my
$lastpart
=
$_
, -3, 3,
''
);
join
'::'
,
@parts
,
$lastpart
;
}
grep
{
my
$path
= File::Spec->catpath(
$v
,
$dirs
,
$_
);
(-e
$path
&& !-d
$path
)
&&
substr
(
$_
, 0,
length
(
$bprefix
)) eq
$bprefix
&&
substr
(
$_
, -3, 3) eq
'.pm'
}
sort
{
$a
cmp
$b
}
readdir
$dh
;
}
else
{ () }
}
@expanded_inc
;
}
$self
->children_prefixes;
push
@children
,
map
{
my
$prefix
=
$_
;
grep
{ !
$seen
{
$_
}++ }
grep
{
my
$this_prefix
=
substr
$_
, 0,
length
$prefix
;
$this_prefix
eq
$prefix
;
}
keys
%App::Easer::V2::registered
;
}
$self
->children_prefixes;
push
@children
,
$self
->auto_children
if
$self
->force_auto_children //
@children
;
return
@children
;
}
sub
_auto_child (
$self
,
$name
,
$inflate
= 0) {
my
$child
= __PACKAGE__ .
'::'
.
ucfirst
(
lc
(
$name
));
(
$child
) =
$self
->inflate_children(
$child
)
if
$inflate
;
return
$child
;
}
sub
auto_children (
$self
,
$inflate
= 0) {
map
{
$self
->_auto_child(
$_
,
$inflate
) }
qw< help commands tree >
;
}
sub
auto_commands (
$self
) {
return
$self
->_auto_child(
'commands'
, 1) }
sub
auto_help (
$self
) {
return
$self
->_auto_child(
'help'
, 1) }
sub
auto_tree (
$self
) {
return
$self
->_auto_child(
'tree'
, 1) }
sub
run_help (
$self
) {
return
$self
->auto_help->run(
$self
->name) }
sub
full_help_text (
$s
) {
return
$s
->auto_help->collect_help_for(
$s
) }
sub
load_module (
$sop
,
$module
) {
my
$file
=
"$module.pm"
=~ s{::}{/}grmxs;
eval
{
require
$file
} or Carp::confess(
"module<$module>: $EVAL_ERROR"
);
return
$module
;
}
sub
ref_to_sub (
$self
,
$spec
) {
Carp::confess(
"undefined specification in ref_to_sub"
)
unless
defined
$spec
;
return
$spec
if
ref
(
$spec
) eq
'CODE'
;
my
(
$class
,
$function
) =
ref
(
$spec
) eq
'ARRAY'
?
$spec
->@*
:
$spec
=~ m{\A (?: (.*) :: )? (.*) \z}mxs;
return
$self
->can(
$function
)
unless
length
(
$class
//
''
);
$self
->load_module(
$class
)
unless
$class
->can(
$function
);
return
$class
->can(
$function
);
}
sub
instantiate (
$sop
,
$class
,
@args
) {
$sop
->load_module(
$class
)
unless
$class
->can(
'new'
);
return
$class
->new(
@args
);
}
sub
inflate_children (
$self
,
@hints
) {
my
$hashy
=
$self
->hashy_class;
map
{
my
$child
=
$_
;
if
(!blessed(
$child
)) {
$child
=
ref
(
$child
) eq
'ARRAY'
?
$self
->instantiate(
$child
->@*)
:
ref
(
$child
) eq
'HASH'
?
$self
->instantiate(
$hashy
,
$child
)
:
$self
->instantiate(
$child
);
}
$child
->parent(
$self
);
$child
;
}
grep
{
defined
$_
}
@hints
;
}
sub
fallback (
$self
) {
my
$fto
=
$self
->fallback_to;
return
$fto
if
!
defined
(
$fto
) ||
$fto
!~ m{\A(?: 0 | [1-9]\d* )\z};
my
@children
=
$self
->list_children;
return
$children
[
$fto
]
if
$fto
<=
$#children
;
return
undef
;
}
sub
execute (
$self
) {
my
$spec
=
$self
->_rw or
die
"nothing to search for execution\n"
;
my
$sub
=
$self
->ref_to_sub(
$spec
) or
die
"nothing to execute\n"
;
return
$sub
->(
$self
);
}
sub
run (
$self
,
$name
,
@args
) {
$self
->call_name(
$name
);
$self
->collect(
@args
);
$self
->commit;
$self
->validate;
my
(
$child
,
@child_args
) =
$self
->find_child;
return
$child
->run(
@child_args
)
if
defined
$child
;
$self
->execution_reason(
$child_args
[0]);
return
$self
->execute;
}
push
our
@ISA
,
'App::Easer::V2::Command'
;
sub
aliases {
'commands'
}
sub
allow_residual_options { 0 }
sub
description {
'Print list of supported sub-commands'
}
sub
help {
'list sub-commands'
}
sub
name {
'commands'
}
sub
target (
$self
) {
my
(
$subc
,
@rest
) =
$self
->residual_args;
die
"this command does not support many arguments\n"
if
@rest
;
my
$target
=
$self
->parent;
$target
=
$target
->find_matching_child(
$subc
)
if
defined
$subc
;
die
"cannot find sub-command '$subc'\n"
unless
defined
$target
;
return
$target
;
}
sub
list_commands_for (
$self
,
$target
=
undef
) {
$target
//=
$self
->target;
my
@lines
;
for
my
$command
(
$target
->inflate_children(
$target
->list_children)) {
my
$help
=
$command
->help //
'(**missing help**)'
;
my
@aliases
=
$command
->aliases;
next
unless
@aliases
;
push
@lines
,
sprintf
'%15s: %s'
,
shift
(
@aliases
),
$help
;
push
@lines
,
sprintf
'%15s (also as: %s)'
,
''
,
join
', '
,
@aliases
if
@aliases
;
}
return
unless
@lines
;
return
join
"\n"
,
@lines
;
}
sub
help_channel (
$self
) {
$self
->target->help_channel }
sub
_build_printout_facility (
$self
) {
my
$channel
=
$self
->help_channel;
my
$refch
=
ref
$channel
;
return
$channel
if
$refch
eq
'CODE'
;
my
$fh
;
if
(
$refch
eq
'GLOB'
) {
$fh
=
$channel
;
}
elsif
(
$refch
eq
'SCALAR'
) {
open
$fh
,
'>'
,
$channel
or
die
"open(): $!\n"
;
}
elsif
(
$refch
) {
die
'invalid channel'
;
}
else
{
(
$channel
,
my
$binmode
) =
split
m{:}mxs,
$channel
, 2;
if
(
$channel
eq
'-'
||
lc
(
$channel
) eq
'-stdout'
) {
$fh
= \
*STDOUT
;
}
elsif
(
lc
(
$channel
) eq
'-stderr'
) {
$fh
= \
*STDERR
;
}
else
{
open
$fh
,
'>'
,
$channel
or
die
"open('$channel'): $!\n"
;
}
binmode
$fh
,
$binmode
if
length
(
$binmode
//
''
);
}
return
sub
(
$cmd
,
@stuff
) {
print
{
$fh
}
@stuff
;
return
$cmd
;
}
}
sub
printout (
$self
,
@stuff
) {
my
$pof
=
$self
->_rw;
$self
->_rw(
$pof
=
$self
->_build_printout_facility)
unless
$pof
;
$pof
->(
$self
,
@stuff
);
}
sub
execute (
$self
) {
my
$target
=
$self
->target;
my
$name
=
$target
->call_name //
$target
->name;
if
(
defined
(
my
$commands
=
$self
->list_commands_for(
$target
))) {
$self
->printout(
"sub-commands for $name\n"
,
$commands
,
"\n"
);
}
else
{
$self
->printout(
"no sub-commands for $name\n"
);
}
}
push
our
@ISA
,
'App::Easer::V2::Command::Commands'
;
sub
aliases {
'help'
}
sub
allow_residual_options { 0 }
sub
description {
'Print help for (sub)command'
}
sub
help {
'print a help command'
}
sub
name {
'help'
}
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
execute (
$self
) {
$self
->printout(
$self
->collect_help_for(
$self
->target));
return
0;
}
sub
collect_help_for (
$self
,
$target
=
undef
) {
$target
//=
$self
->target;
my
@stuff
;
push
@stuff
,
$target
->help,
"\n\n"
;
if
(
defined
(
my
$description
=
$target
->description)) {
$description
=~ s{\A\s+|\s+\z}{}gmxs;
$description
=~ s{^}{ }gmxs;
push
@stuff
,
"Description:\n$description\n\n"
;
}
push
@stuff
,
sprintf
"Can be called as: %s\n\n"
,
join
', '
,
$target
->aliases
if
$target
->parent;
if
(
my
@options
=
$target
->options) {
push
@stuff
,
"Options:\n"
;
my
$n
= 0;
for
my
$opt
(
@options
) {
push
@stuff
,
"\n"
if
$n
++;
push
@stuff
,
sprintf
"%15s: %s\n"
,
$target
->name_for_option(
$opt
),
$opt
->{help} //
''
;
if
(
exists
$opt
->{getopt}) {
my
@lines
= __commandline_help(
$opt
->{getopt});
push
@stuff
,
sprintf
"%15s command-line: %s\n"
,
''
,
shift
(
@lines
);
push
@stuff
,
map
{
sprintf
"%15s %s\n"
,
''
,
$_
}
@lines
;
}
if
(
defined
(
my
$env
=
$self
->environment_variable_name(
$opt
))) {
push
@stuff
,
sprintf
"%15s environment: %s\n"
,
''
,
$env
;
}
push
@stuff
,
sprintf
"%15s default: %s\n"
,
''
,
$opt
->{
default
} //
'*undef*'
if
exists
$opt
->{
default
};
}
push
@stuff
,
"\n"
;
}
else
{
push
@stuff
,
"This command has no option\n"
;
}
if
(
defined
(
my
$commands
=
$self
->list_commands_for(
$target
))) {
push
@stuff
,
"Sub-commands:\n"
,
$commands
,
"\n"
;
}
else
{
push
@stuff
,
"No sub-commands\n"
;
}
return
join
''
,
@stuff
;
}
push
our
@ISA
,
'App::Easer::V2::Command::Commands'
;
sub
aliases {
'tree'
}
sub
description {
'Print tree of supported sub-commands'
}
sub
help {
'print sub-commands in a tree'
}
sub
name {
'tree'
}
sub
options {
return
(
{
getopt
=>
'include_auto|include-auto|I!'
,
default
=> 0,
environment
=> 1,
},
);
}
sub
list_commands_for (
$self
,
$target
) {
my
$exclude_auto
=
$self
->config(
'include_auto'
) ? 0 : 1;
my
@lines
;
for
my
$command
(
$target
->inflate_children(
$target
->list_children)) {
my
(
$name
) =
$command
->aliases or
next
;
next
if
$name
=~ m{\A(?: help | commands | tree)\z}mxs &&
$exclude_auto
;
my
$help
=
$command
->help //
'(**missing help**)'
;
push
@lines
,
sprintf
'- %s (%s)'
,
$name
,
$help
;
if
(
defined
(
my
$subtree
=
$self
->list_commands_for(
$command
))) {
push
@lines
,
$subtree
=~ s{^}{ }rgmxs;
}
}
return
unless
@lines
;
return
join
"\n"
,
@lines
;
}
1;