{
our
$VERSION
=
'2.012'
}
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;
local
$Data::Dumper::Sortkeys
= 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
;
my
$parent_class
=
'App::Easer::V2::Command'
;
while
(
@args
) {
my
$request
=
shift
@args
;
if
(
$request
eq
'-command'
) {
$registered
{
$target
} = 1;
no
strict
'refs'
;
push
@{
$target
.
'::ISA'
},
$parent_class
;
}
elsif
(
$request
eq
'-inherit'
) {
no
strict
'refs'
;
push
@{
$target
.
'::ISA'
},
$parent_class
;
}
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
;
}
elsif
(
$request
eq
'-parent'
) {
Carp::croak
"no parent class provided"
unless
@args
;
$parent_class
=
shift
@args
;
App::Easer::V2::Command->load_module(
$parent_class
);
}
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
_rw_prd (
$self
,
@n
) {
my
$slot
=
$self
->slot;
my
$name
= (
caller
(1))[3] =~ s{.*::}{}rmxs;
if
(
@n
) {
$slot
->{
$name
} =
$n
[0];
}
elsif
(
ref
(
my
$ref_to_default
=
$slot
->{
$name
})) {
my
$parent
=
$self
->parent;
$slot
->{
$name
} =
$parent
?
$parent
->
$name
:
$$ref_to_default
;
}
return
$slot
->{
$name
};
}
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
fallback_to (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
final_commit_stack (
$self
,
@r
) {
$self
->_rwa(
@r
) }
sub
force_auto_children (
$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
options_help (
$s
,
@r
) {
$s
->_rw(
@r
) }
sub
params_validate (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
parent (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
pre_execute (
$self
,
@r
) {
$self
->_rwa(
@r
) }
sub
residual_args (
$self
,
@r
) {
$self
->_rwa(
@r
) }
sub
_last_cmdline (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
_sources (
$self
,
@r
) {
$self
->_rwn(
sources
=>
@r
) }
sub
usage (
$self
,
@r
) {
$self
->_rw(
@r
) }
sub
config_hash_key (
$self
,
@r
) {
$self
->_rw_prd(
@r
) }
sub
is_root (
$self
) { !
defined
(
$self
->parent) }
sub
root (
$self
) {
my
$slot
=
$self
->slot;
return
$slot
->{root} //=
do
{
my
$retval
=
$self
;
while
(
defined
(
my
$parent
=
$retval
->parent)) {
$retval
=
$parent
;
}
$retval
;
};
}
sub
child (
$self
,
@newval
) {
my
$slot
=
$self
->slot;
if
(
@newval
) {
$slot
->{child} =
$newval
[0];
weaken(
$slot
->{child});
}
return
$slot
->{child};
}
sub
is_leaf (
$self
) { !
defined
(
$self
->child) }
sub
leaf (
$self
) {
my
$slot
=
$self
->slot;
if
(!
exists
(
$slot
->{leaf})) {
my
$retval
=
$self
;
while
(
defined
(
my
$parent
=
$retval
->child)) {
$retval
=
$parent
;
}
$slot
->{leaf} =
$retval
;
weaken(
$slot
->{leaf});
}
return
$slot
->{leaf};
}
sub
sources (
$self
,
@new
) {
my
$r
;
my
$slot
=
$self
->slot;
if
(
@new
) {
$r
=
$slot
->{sources} =
$new
[0];
}
else
{
state
$default_array
=
[
qw< +CmdLine +Environment +Parent=70 +Default=100 >
];
state
$default_hash
= {
current
=> [
qw< +CmdLine +Environment +Default +ParentSlices >
],
final
=> [ ],
};
state
$default_hash_v2_008
= {
current
=> [
qw< +CmdLine +Environment +Default +ParentSlices >
],
final
=> [ ],
};
$r
=
$slot
->{sources};
$r
=
$slot
->{sources} =
!
defined
(
$r
) ? Carp::confess()
:
$r
eq
'default-array'
?
$default_array
:
$r
eq
'default-hash'
?
$default_hash
:
$r
eq
'v2.008'
?
$default_hash_v2_008
: Carp::confess()
unless
ref
(
$r
);
}
Carp::confess()
unless
defined
(
$r
);
return
$r
->@*
if
ref
(
$r
) eq
'ARRAY'
;
return
\
$r
if
ref
(
$r
) eq
'HASH'
;
Carp::confess();
}
sub
_sources_for_phase (
$self
,
$phase
) {
my
@sources
=
$self
->sources;
return
${
$sources
[0]}->{
$phase
}
if
@sources
== 1
&&
ref
(
$sources
[0]) eq
'REF'
&&
ref
(${
$sources
[0]}) eq
'HASH'
;
return
$phase
eq
'current'
? \
@sources
: ();
}
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
$name_exact
=
ref
(
$_
) ?
undef
:
$_
;
my
$name_rx
=
qr{\A(?:$_)\z}
;
my
$ancestor
=
$self
->parent;
while
(
$ancestor
) {
push
@options
,
my
@pass
=
grep
{
my
$name
=
$self
->name_for_option(
$_
);
(
$_
->{transmit} // 0)
&& (!
$got
{
$name
}++)
&& (
(
defined
(
$name_exact
) &&
$name
eq
$name_exact
)
|| (!
$_
->{transmit_exact} &&
$name
=~ m{
$name_rx
})
);
}
$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'
],
config_hash_key
=> \
'merged'
,
default_child
=>
'help'
,
environment_prefix
=>
''
,
fallback_to
=>
undef
,
final_commit_stack
=> [],
force_auto_children
=>
undef
,
hashy_class
=> __PACKAGE__,
help_channel
=>
'-STDOUT:encoding(UTF-8)'
,
options
=> [],
params_validate
=>
undef
,
pre_execute
=> [],
residual_args
=> [],
sources
=>
'default-array'
,
(
$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
,
$sources
,
@args
) {
my
@residual_args
;
my
$slot
=
$self
->slot;
my
$last_priority
= 0;
for
my
$source
(
$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
;
my
$latest
=
$self
->_rwn(
'config'
) // {};
my
@sequence
= (
$latest
->{sequence} //= [])->@*;
my
%all_eslices_at
= (
$latest
->{all_eslices_at} // {})->%*;
my
%command_eslices_at
= (
$latest
->{command_eslices_at} // {})->%*;
if
(
$slice
) {
$last_priority
=
my
$priority
=
$meta
->{priority} //=
$last_priority
+ 10;
my
$eslice
= [
$priority
,
$src
, \
@opts
,
$locator
,
$slice
];
push
((
$all_eslices_at
{
$priority
} //= [])->@*,
$eslice
);
push
((
$command_eslices_at
{
$priority
} //= [])->@*,
$eslice
);
push
@sequence
,
$eslice
;
for
(
my
$i
=
$#sequence
;
$i
> 0; --
$i
) {
last
if
$sequence
[
$i
- 1][0] <=
$sequence
[
$i
][0];
@sequence
[
$i
- 1,
$i
] =
@sequence
[
$i
,
$i
- 1];
}
}
my
$matrix_config
=
$self
->merge_hashes(
map
{
$_
->[-1] }
map
{
$all_eslices_at
{
$_
}->@* }
sort
{
$a
<=>
$b
}
keys
(
%all_eslices_at
)
);
my
$legacy_config
=
$self
->merge_hashes(
map
{
$_
->[-1]}
@sequence
);
$self
->_rwn(
config
=> {
merged
=>
$legacy_config
,
merged_legacy
=>
$legacy_config
,
'v2.008'
=>
$matrix_config
,
sequence
=> \
@sequence
,
all_eslices_at
=> \
%all_eslices_at
,
command_eslices_at
=> \
%command_eslices_at
,
}
);
}
return
\
@residual_args
;
}
sub
collect (
$self
,
@args
) {
if
(
my
$sources
=
$self
->_sources_for_phase(
'current'
)) {
$self
->residual_args(
$self
->_collect(
$sources
,
@args
));
}
return
$self
;
}
sub
final_collect (
$self
) {
if
(
my
$sources
=
$self
->_sources_for_phase(
'final'
)) {
$self
->_collect(
$sources
);
}
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
,
@specs
,
%name_for
);
for
my
$option
(
$self
->options) {
next
unless
exists
(
$option
->{getopt});
my
$go
=
$option
->{getopt};
if
(
ref
(
$go
) eq
'ARRAY'
) {
my
(
$string
,
$callback
) =
$go
->@*;
push
@specs
,
$string
,
sub
{
$callback
->(\
%option_for
,
@_
) };
$go
=
$string
;
}
else
{
push
@specs
,
$go
;
}
my
(
$go_name
) =
$go
=~ m{\A(\w[-\w]*)}mxs;
my
$official_name
=
$self
->name_for_option(
$option
);
$name_for
{
$go_name
} =
$official_name
if
$go_name
ne
$official_name
;
}
Getopt::Long::GetOptionsFromArray(\
@args
, \
%option_for
,
@specs
)
or
die
"bailing out\n"
;
my
$strict
= !
$self
->allow_residual_options;
die
"bailing out (allow_residual_options is false and got <@args>)"
if
$strict
&&
@args
&&
$args
[0] =~ m{\A - . }mxs;
$self
->_rename_options_inplace(\
%option_for
, \
%name_for
);
$self
->_last_cmdline( {
option_for
=> \
%option_for
,
args
=> \
@args
});
return
(\
%option_for
, \
@args
);
}
sub
_rename_options_inplace (
$self
,
$collected
,
$name_for
) {
my
%renamed
;
for
my
$go_name
(
sort
{
$a
cmp
$b
}
keys
$name_for
->%*) {
next
unless
exists
$collected
->{
$go_name
};
my
$official_name
=
$name_for
->{
$go_name
};
$renamed
{
$official_name
} =
delete
(
$collected
->{
$go_name
});
}
$collected
->{
$_
} =
$renamed
{
$_
}
for
keys
%renamed
;
return
$self
;
}
sub
source_LastCmdLine (
$self
,
@ignore
) {
my
$last
=
$self
->_last_cmdline or
return
{};
return
$last
->{option_for};
}
sub
name_for_option (
$self
,
$o
) {
return
$o
->{name}
if
defined
$o
->{name};
return
$1
if
defined
$o
->{getopt} &&
$o
->{getopt} =~ m{\A(\w[-\w]*)}mxs;
return
lc
$o
->{environment}
if
defined
$o
->{environment} &&
$o
->{environment} ne
'1'
;
return
'~~~'
;
}
sub
source_Default (
$self
,
$opts
,
@ignore
) {
my
%opts
=
$opts
->@*;
my
$include_inherited
=
$opts
{include_inherited};
return
{
map
{
$self
->name_for_option(
$_
) =>
$_
->{
default
} }
grep
{
exists
$_
->{
default
} }
grep
{
$include_inherited
|| !
$_
->{inherited} }
$self
->options
};
}
sub
source_FinalDefault (
$self
,
@i
) {
return
$self
->source_Default([
include_inherited
=> 1]);
}
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
,
$opts
,
@ignore
) {
my
%opts
=
$opts
->@*;
my
$include_inherited
=
$opts
{include_inherited};
return
{
map
{
my
$en
=
$self
->environment_variable_name(
$_
);
defined
(
$en
)
&&
exists
(
$ENV
{
$en
})
? (
$self
->name_for_option(
$_
) =>
$ENV
{
$en
})
: ();
}
grep
{
$include_inherited
|| !
$_
->{inherited} }
$self
->options
};
}
sub
source_FinalEnvironment (
$self
,
@i
) {
return
$self
->source_Environment([
include_inherited
=> 1 ]);
}
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
source_ParentSlices (
$self
,
@ignore
) {
my
$parent
=
$self
->parent or
return
;
my
$latest
=
$self
->_rwn(
'config'
);
$self
->_rwn(
config
=> (
$latest
= {}))
unless
defined
$latest
;
my
$all_eslices_at
=
$latest
->{all_eslices_at} //= {};
my
$pslices_at
=
$parent
->config_hash(1)->{all_eslices_at} // {};
for
my
$priority
(
keys
(
$pslices_at
->%*)) {
my
$eslices
=
$all_eslices_at
->{
$priority
} //= [];
push
$eslices
->@*,
$pslices_at
->{
$priority
}->@*;
}
return
;
}
sub
config_hash (
$self
,
$blame
= 0) {
my
$config
=
$self
->_rwn(
'config'
) // {};
return
$config
if
$blame
;
return
$config
->{
$self
->config_hash_key} // {};
}
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
set_config_hash (
$self
,
$new
,
$full
= 0) {
if
(!
$full
) {
my
$previous
=
$self
->_rwn(
'config'
) // {};
my
$key
=
$self
->config_hash_key;
$new
= {
$previous
->%*,
merged
=>
$new
,
override
=>
$new
};
}
$self
->_rwn(
config
=>
$new
);
return
$self
;
}
sub
inject_configs (
$self
,
$data
,
$priority
= 1000) {
$self
->_collect(
[
sub
(
$self
,
$opts
,
$args
) {
my
$latest
=
$self
->_rwn(
'config'
);
$self
->_rwn(
config
=> (
$latest
= {}))
unless
$latest
;
my
$queue
=
$latest
->{all_eslices_at}{
$priority
} //= [];
push
$queue
->@*, [
$priority
,
injection
=> [],
''
,
$data
];
return
;
},
]
);
}
sub
commit (
$self
,
@n
) {
my
$commit
=
$self
->_rw(
@n
);
return
$commit
if
@n
;
return
unless
$commit
;
return
$self
->ref_to_sub(
$commit
)->(
$self
);
}
sub
final_commit (
$self
,
@n
) {
return
$self
->_rw(
@n
)
if
@n
;
my
$slot
=
$self
->slot;
my
$stack
=
$slot
->{final_commit_stack} //= [];
push
$stack
->@*,
$self
;
if
(
exists
(
$slot
->{final_commit})) {
my
$commit
=
$slot
->{final_commit};
return
unless
$commit
;
my
$propagate_to_parent
=
$self
->ref_to_sub(
$commit
)->(
$self
);
return
unless
$propagate_to_parent
;
}
my
$parent
=
$self
->parent;
return
unless
$parent
;
$parent
->final_commit_stack([
$stack
->@*]);
return
$parent
->final_commit;
}
sub
validate (
$self
,
@n
) {
my
$validator
=
$self
->_rw(
@n
);
return
$validator
if
@n
;
if
(
$validator
) {
die
"validator can only be a CODE reference\n"
unless
ref
$validator
eq
'CODE'
;
$validator
->(
$self
);
}
elsif
(
my
$params_validate
=
$self
->params_validate) {
if
(
my
$config_validator
=
$params_validate
->{config} //
undef
) {
my
@array
=
$self
->config_hash;
&Params::Validate::validate
(\
@array
,
$config_validator
);
}
if
(
my
$args_validator
=
$params_validate
->{args} //
undef
) {
my
@array
=
$self
->residual_args;
&Params::Validate::validate_pos
(\
@array
,
$args_validator
->@*);
}
}
else
{}
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
;
my
@autoloaded_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
@autoloaded_children
,
map
{
my
$prefix
=
$_
;
my
$prefix_length
=
length
(
$prefix
);
grep
{ !
$seen
{
$_
}++ }
grep
{
(
substr
(
$_
, 0,
length
$prefix
) eq
$prefix
)
&& (
index
(
$_
,
':'
,
$prefix_length
) < 0);
}
keys
%App::Easer::V2::registered
;
}
$self
->children_prefixes;
push
@children
,
sort
{
$a
cmp
$b
}
@autoloaded_children
;
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
,
$mode
=
'help'
) {
$self
->auto_help->run(
$mode
) }
sub
full_help_text (
$s
,
@as
) {
$s
->auto_help->collect_help_for(
$s
,
@as
) }
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
_reparent (
$self
,
$child
) {
$child
->parent(
$self
);
$self
->child(
$child
);
if
(!
ref
(
$child
->_sources)) {
my
(
$first
,
@rest
) =
$self
->sources;
if
(
ref
(
$first
) eq
'REF'
) {
my
$ssources
=
$$first
;
$child
->_sources(
my
$csources
= {
$ssources
->%* });
if
(
my
$next
=
$ssources
->{
next
}) {
my
@csources
=
ref
(
$next
) eq
'ARRAY'
?
$next
->@*
:
ref
(
$next
) eq
'CODE'
?
$next
->(
$child
)
: Carp::confess();
$csources
->{current} = \
@csources
;
}
}
}
$child
->pre_execute_schedule(
$self
->pre_execute);
return
$child
;
}
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
);
}
$self
->_reparent(
$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
pre_execute_schedule (
$self
,
@specs
) {
if
(
my
$spec
=
$self
->_rw) {
my
$sub
=
$self
->ref_to_sub(
$spec
) or
die
"nothing for pre_execute_schedule\n"
;
return
$sub
->(
$self
,
@specs
);
}
$self
->pre_execute([
$self
->pre_execute,
@specs
]);
return
$self
;
}
sub
pre_execute_run (
$self
) {
if
(
my
$spec
=
$self
->_rw) {
my
$sub
=
$self
->ref_to_sub(
$spec
) or
die
"nothing to pre-execute\n"
;
return
$sub
->(
$self
);
}
for
my
$spec
(
$self
->pre_execute) {
my
$sub
=
$self
->ref_to_sub(
$spec
) or
die
"nothing to pre-execute\n"
;
$sub
->(
$self
);
}
return
$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]);
$self
->final_collect;
$self
->final_commit;
$self
->pre_execute_run;
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
_build_printout_facility (
$self
) {
my
$channel
=
$self
->target->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'
;
our
@aliases
=
qw< help usage >
;
sub
aliases {
@aliases
}
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-negatable'
;
substr
$getopt
, -1, 1,
''
;
push
@retval
,
'boolean (can be negated)'
;
}
elsif
(
$getopt
=~ s<:\+ ([@%])? \z><>mxs) {
$mode
=
'optional'
;
$type
=
'i'
;
$default
=
'increment'
;
$desttype
= $1;
my
$line
=
"integer, value is optional, defaults to incrementing current value"
;
$line
.=
", list valued"
if
defined
(
$desttype
) &&
$desttype
eq
'@'
;
push
@retval
,
$line
;
}
elsif
(
substr
(
$getopt
, -1, 1) eq
'+'
) {
$mode
=
'increment'
;
substr
$getopt
, -1, 1,
''
;
push
@retval
,
'incremental integer (adds 1 every time it is provided)'
;
}
elsif
(
$getopt
=~ s<(
[:=])
([siof])
([@%])?
(?:
\{
(\d*)?
,?
(\d*)?
\}
)? \z><>mxs
)
{
$mode
= $1 eq
'='
?
'required'
:
'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
=
"$type, value is $mode"
;
$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
=
"integer, value is optional, defaults to $default"
;
$line
.=
", list valued"
if
defined
(
$desttype
) &&
$desttype
eq
'@'
;
push
@retval
,
$line
;
}
else
{
$type
=
'bool'
;
push
@retval
,
'boolean'
;
}
my
@alternatives
=
split
/\|/,
$getopt
;
if
(
$type
eq
'bool-negatable'
) {
push
@retval
,
map
{
if
(
length
(
$_
) == 1) {
"-$_"
}
else
{
"--$_ | --no-$_"
}
}
@alternatives
;
}
elsif
(
$type
eq
'bool'
||
$mode
eq
'increment'
) {
push
@retval
,
map
{
if
(
length
(
$_
) == 1) {
"-$_"
}
else
{
"--$_"
}
}
@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,
$self
->call_name));
return
0;
}
sub
collect_help_for (
$self
,
$target
,
$mode
=
'help'
) {
my
@stuff
;
my
$trim_and_prefix
=
sub
(
$text
,
$prefix
=
' '
) {
$text
=~ s{\A\s+|\s+\z}{}gmxs;
$text
=~ s{^}{
$prefix
}gmxs;
return
$text
;
};
push
@stuff
, (
$target
->help //
'no concise help yet'
),
"\n\n"
;
if
(
$mode
eq
'help'
&&
defined
(
my
$description
=
$target
->description)) {
push
@stuff
,
"Description:\n"
,
$trim_and_prefix
->(
$description
),
"\n\n"
;
}
if
(
defined
(
my
$usage
=
$target
->usage)) {
push
@stuff
,
"Usage:\n"
,
$trim_and_prefix
->(
$usage
),
"\n\n"
;
}
push
@stuff
,
sprintf
"Can be called as: %s\n\n"
,
join
', '
,
$target
->aliases
if
$target
->parent;
my
@options
=
$target
->options;
my
$options_help
=
$target
->options_help;
if
(
@options
||
defined
(
$options_help
)) {
push
@stuff
,
"Options:\n"
;
$options_help
//= {};
if
(!
ref
(
$options_help
)) {
push
@stuff
,
$trim_and_prefix
->(
$options_help
),
"\n\n"
;
}
else
{
my
$preamble
=
$options_help
->{preamble} //
undef
;
push
@stuff
,
$trim_and_prefix
->(
$preamble
),
"\n\n"
if
defined
(
$preamble
);
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
;
}
if
(
exists
(
$opt
->{
default
})) {
my
$default
=
$opt
->{
default
};
my
$print
= !
defined
(
$default
) ?
'*undef*'
: !
ref
(
$default
) ?
$default
:
do
{
require
JSON::PP; JSON::PP::encode_json(
$default
) };
push
@stuff
,
sprintf
"%15s default: %s\n"
,
''
,
$print
;
}
}
my
$postamble
=
$options_help
->{postamble} //
undef
;
push
@stuff
,
"\n"
,
$trim_and_prefix
->(
$postamble
),
"\n"
if
defined
(
$postamble
);
}
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;