our
$VERSION
=
"0.47"
;
class Command::Tree {
is
=>
'Command::V2'
,
is_abstract
=> 1,
doc
=>
'base class for commands which delegate to sub-commands'
,
};
sub
resolve_class_and_params_for_argv {
my
$self
=
shift
;
my
@argv
=
@_
;
if
(
$argv
[0] and
$argv
[0] !~ /^\-/
and
my
$class_for_sub_command
=
$self
->class_for_sub_command(
$argv
[0]) ) {
shift
@argv
;
return
$class_for_sub_command
->resolve_class_and_params_for_argv(
@argv
);
}
elsif
(
@argv
== 1 and
$argv
[0] =~ /^(\-)?\-h(elp)?$/ ) {
return
(
$self
, {
help
=> 1 });
}
else
{
return
(
$self
,
undef
);
}
}
sub
resolve_option_completion_spec {
my
$class
=
shift
;
my
@completion_spec
;
my
@sub
=
eval
{
$class
->sub_command_names };
if
($@) {
$class
->warning_message(
"Couldn't load class $class: $@\nSkipping $class..."
);
return
;
}
for
my
$sub
(
@sub
) {
my
$sub_class
=
$class
->class_for_sub_command(
$sub
);
my
$sub_tree
=
$sub_class
->resolve_option_completion_spec()
if
defined
(
$sub_class
);
if
(
$sub_tree
) {
push
@completion_spec
,
'>'
.
$sub
=>
$sub_tree
;
}
else
{
if
(
defined
$sub_class
) {
print
"WARNING: $sub has sub_class $sub_class of ($class) but could not resolve option completion spec for it.\n"
.
"Setting $sub to non-delegating command, investigate to correct tab completion.\n"
;
}
else
{
print
"WARNING: $sub has no sub_class so could not resolve option completion spec for it.\n"
.
"Setting $sub to non-delegating command, investigate to correct tab completion.\n"
;
}
push
@completion_spec
,
$sub
=>
undef
;
}
}
push
@completion_spec
,
"help!"
=>
undef
;
return
\
@completion_spec
}
sub
help_brief {
my
$self
=
shift
;
if
(
my
$doc
=
$self
->__meta__->doc) {
return
$doc
;
}
else
{
my
@parents
=
$self
->__meta__->ancestry_class_metas;
for
my
$parent
(
@parents
) {
if
(
my
$doc
=
$parent
->doc) {
return
$doc
;
}
}
return
""
;
}
}
sub
doc_help {
my
$self
=
shift
;
my
$command_name
=
$self
->command_name;
my
$text
;
$text
=
sprintf
(
"Sub-commands for %s:\n%s"
,
Term::ANSIColor::colored(
$command_name
,
'bold'
),
$self
->help_sub_commands,
);
return
$text
;
}
sub
doc_manual {
my
$self
=
shift
;
my
$pod
=
$self
->_doc_name_version;
my
$manual
=
$self
->_doc_manual_body;
my
$help
=
$self
->help_detail;
if
(
$manual
or
$help
) {
$pod
.=
"=head1 DESCRIPTION:\n\n"
;
my
$txt
=
$manual
||
$help
;
if
(
$txt
=~ /^\=/) {
$pod
.=
$manual
;
}
else
{
$txt
=~ s/\n/\n\n/g;
$pod
.=
$txt
;
}
}
my
$sub_commands
=
$self
->help_sub_commands(
brief
=> 1);
$pod
.=
"=head1 SUB-COMMANDS\n\n"
.
$sub_commands
.
"\n\n"
;
$pod
.=
$self
->_doc_footer();
$pod
.=
"\n\n=cut\n\n"
;
return
"\n$pod"
;
}
sub
sorted_sub_command_classes {
no
warnings;
my
@c
=
map
{ [
$_
->sub_command_sort_position,
$_
] }
shift
->sub_command_classes;
return
map
{
$_
->[1] }
sort
{
(
$a
->[0] <=>
$b
->[0])
||
(
$a
->[0] cmp
$b
->[0])
}
@c
;
}
sub
sorted_sub_command_names {
my
$class
=
shift
;
my
@sub_command_classes
=
$class
->sorted_sub_command_classes;
my
@sub_command_names
=
map
{
$_
->command_name_brief }
@sub_command_classes
;
return
@sub_command_names
;
}
sub
sub_commands_table {
my
$class
=
shift
;
my
@sub_command_names
=
$class
->sorted_sub_command_names;
my
$max_length
= 0;
for
(
@sub_command_names
) {
$max_length
=
length
(
$_
)
if
(
$max_length
<
length
(
$_
));
}
$max_length
||= 79;
my
$col_spacer
=
'_'
x
$max_length
;
my
$n_cols
= floor(80/
$max_length
);
my
$n_rows
= ceil(
@sub_command_names
/
$n_cols
);
my
@tb_rows
;
for
(
my
$i
= 0;
$i
<
@sub_command_names
;
$i
+=
$n_cols
) {
my
$end
=
$i
+
$n_cols
- 1;
$end
=
$#sub_command_names
if
(
$end
>
$#sub_command_names
);
push
@tb_rows
, [
@sub_command_names
[
$i
..
$end
]];
}
my
@col_alignment
;
for
(
my
$i
= 0;
$i
<
$n_cols
;
$i
++) {
push
@col_alignment
, {
sample
=>
"&$col_spacer"
};
}
my
$tb
= Text::Table->new(
@col_alignment
);
$tb
->load(
@tb_rows
);
return
$tb
;
}
sub
_categorize_sub_commands {
my
$class
=
shift
;
my
@sub_command_classes
=
$class
->sorted_sub_command_classes;
my
%categories
;
my
@order
;
for
my
$sub_command_class
(
@sub_command_classes
) {
next
if
$sub_command_class
->_is_hidden_in_docs();
my
$category
=
$sub_command_class
->sub_command_category ||
''
;
unless
(
exists
$categories
{
$category
}) {
if
(
$category
) {
push
(
@order
,
$category
)
}
else
{
unshift
(
@order
,
''
);
}
$categories
{
$category
} = [];
}
push
(@{
$categories
{
$category
}},
$sub_command_class
);
}
return
(\
@order
, \
%categories
);
}
sub
help_sub_commands {
my
(
$self
,
%params
) =
@_
;
my
(
$order
,
$categories
) =
$self
->_categorize_sub_commands(
@_
);
my
$command_name_method
=
'command_name_brief'
;
no
warnings;
local
$Text::Wrap::columns
= 60;
my
@full_data
;
for
my
$category
(
@$order
) {
my
$sub_commands_within_this_category
=
$categories
->{
$category
};
my
@data
=
map
{
my
@rows
=
split
(
"\n"
,Text::Wrap::wrap(
''
,
' '
,
$_
->help_brief));
chomp
@rows
;
(
[
$_
->
$command_name_method
,
(
$_
->isa(
'Command::Tree'
) ?
'...'
:
''
),
$rows
[0],
],
map
{
[
''
,
' '
,
$rows
[
$_
],
]
} (1..
$#rows
)
);
}
@$sub_commands_within_this_category
;
if
(
$category
) {
push
@full_data
, [
''
,
''
,
''
]
if
@full_data
;
if
(
$category
=~ /\D/) {
$category
.=
':'
if
$category
=~ /\S/;
push
@full_data
,
[
Term::ANSIColor::colored(
uc
(
$category
),
'blue'
),
''
,
''
];
}
else
{
}
}
push
@full_data
,
@data
;
}
my
@max_width_found
= (0,0,0);
for
(
@full_data
) {
for
my
$c
(0..2) {
$max_width_found
[
$c
] =
length
(
$_
->[
$c
])
if
$max_width_found
[
$c
] <
length
(
$_
->[
$c
]);
}
}
my
@colors
= (
qw/ red bold /
);
my
$text
=
''
;
for
my
$row
(
@full_data
) {
for
my
$c
(0..2) {
$text
.=
' '
;
$text
.=
$colors
[
$c
] ? Term::ANSIColor::colored(
$row
->[
$c
],
$colors
[
$c
]) :
$row
->[
$c
];
$text
.=
' '
;
$text
.=
' '
x (
$max_width_found
[
$c
]-
length
(
$row
->[
$c
]));
}
$text
.=
"\n"
;
}
return
$text
;
}
sub
doc_sub_commands {
my
$self
=
shift
;
my
(
$order
,
$categories
) =
$self
->_categorize_sub_commands(
@_
);
my
$text
=
""
;
my
$indent_lvl
= 4;
for
my
$category
(
@$order
) {
my
$category_name
= (
$category
?
uc
$category
:
"GENERAL"
);
$text
.=
"=head2 $category_name\n\n"
;
for
my
$cmd
(@{
$categories
->{
$category
}}) {
$text
.=
"=over $indent_lvl\n\n"
;
my
$name
=
$cmd
->command_name_brief;
my
$link
=
$cmd
->command_name;
$link
=~ s/ /-/g;
my
$description
=
$cmd
->help_brief;
$text
.=
"=item B<L<$name|$link>>\n\n=over 2\n\n=item $description\n\n=back\n\n"
;
$text
.=
"=back\n\nE<10>\n\n"
;
}
}
return
$text
;
}
sub
sub_command_dirs {
my
$class
=
shift
;
my
$subdir
=
ref
(
$class
) ||
$class
;
$subdir
=~ s|::|\/|g;
my
@dirs
=
grep
{ -d
$_
}
map
{
$_
.
'/'
.
$subdir
}
@INC
;
return
@dirs
;
}
sub
sub_command_classes {
my
$class
=
shift
;
my
$mapping
=
$class
->_build_sub_command_mapping;
return
values
%$mapping
;
}
sub
is_sub_command_delegator {
return
scalar
(
shift
->sub_command_classes);
}
sub
command_tree_source_classes {
my
$class
=
shift
;
return
$class
;
}
sub
_build_sub_command_mapping {
my
$class
=
shift
;
$class
=
ref
(
$class
) ||
$class
;
my
@source_classes
=
$class
->command_tree_source_classes;
my
$mapping
;
do
{
no
strict
'refs'
;
$mapping
= ${
$class
.
'::SUB_COMMAND_MAPPING'
};
if
(
ref
(
$mapping
) eq
'HASH'
) {
return
$mapping
;
}
};
for
my
$source_class
(
@source_classes
) {
eval
{
$source_class
->class; };
if
( $@ ) {
warn
$@;
}
my
$subdir
=
$source_class
;
$subdir
=~ s|::|\/|g;
my
$source_class_above
=
$source_class
;
$source_class_above
=~ s/::Command//;
my
$subdir2
=
$source_class_above
;
$subdir2
=~ s|::|/|g;
for
my
$lib
(
@INC
) {
my
$subdir_full_path
=
$lib
.
'/'
.
$subdir
;
if
(-d
$subdir_full_path
) {
my
@files
=
glob
(
"\Q${subdir_full_path}/*"
);
for
my
$file
(
@files
) {
my
$basename
= basename(
$file
);
$basename
=~ s/.pm$// or
next
;
my
$sub_command_class_name
=
$source_class
.
'::'
.
$basename
;
my
$sub_command_class_meta
= UR::Object::Type->get(
$sub_command_class_name
);
unless
(
$sub_command_class_meta
) {
local
$SIG
{__DIE__};
local
$SIG
{__WARN__};
print
">> $sub_command_class_name\n"
;
eval
"use $sub_command_class_name"
;
}
$sub_command_class_meta
= UR::Object::Type->get(
$sub_command_class_name
);
next
unless
$sub_command_class_name
->isa(
"Command"
);
next
if
$sub_command_class_meta
->is_abstract;
next
if
$sub_command_class_name
eq
$class
;
my
$name
=
$source_class
->_command_name_for_class_word(
$basename
);
$mapping
->{
$name
} =
$sub_command_class_name
;
}
}
$subdir_full_path
=
$lib
.
'/'
.
$subdir2
;
my
$pattern
=
$subdir_full_path
.
'/*/Command.pm'
;
my
@paths
=
glob
(
"\Q$pattern\E"
);
for
my
$file
(
@paths
) {
next
unless
defined
$file
;
next
unless
length
$file
;
next
unless
-f
$file
;
my
$last_word
= File::Basename::basename(
$file
);
$last_word
=~ s/.pm$// or
next
;
my
$dir
= File::Basename::dirname(
$file
);
my
$second_to_last_word
= File::Basename::basename(
$dir
);
my
$sub_command_class_name
=
$source_class_above
.
'::'
.
$second_to_last_word
.
'::'
.
$last_word
;
next
unless
$sub_command_class_name
->isa(
'Command'
);
next
if
$sub_command_class_name
->__meta__->is_abstract;
next
if
$sub_command_class_name
eq
$class
;
my
$basename
=
$second_to_last_word
;
$basename
=~ s/.pm$//;
my
$name
=
$source_class
->_command_name_for_class_word(
$basename
);
$mapping
->{
$name
} =
$sub_command_class_name
;
}
}
}
return
$mapping
;
}
sub
sub_command_names {
my
$class
=
shift
;
my
$mapping
=
$class
->_build_sub_command_mapping;
return
keys
%$mapping
;
}
sub
_try_command_class_named {
my
$self
=
shift
;
my
$sub_class
=
join
(
'::'
,
@_
);
my
$meta
= UR::Object::Type->get(
$sub_class
);
unless
(
$meta
) {
eval
"use $sub_class;"
;
if
($@) {
if
($@ =~ /^Can't locate .*\.pm in \
@INC
/) {
return
;
}
else
{
my
@msg
=
split
(
"\n"
,$@);
pop
@msg
;
pop
@msg
;
$self
->error_message(
"$sub_class failed to compile!:\n@msg\n\n"
);
return
;
}
}
}
elsif
(
my
$isa
=
$sub_class
->isa(
"Command"
)) {
if
(
ref
(
$isa
)) {
if
(
$sub_class
->SUPER::isa(
"Command"
)) {
return
$sub_class
;
}
else
{
return
;
}
}
return
$sub_class
;
}
else
{
return
;
}
}
sub
class_for_sub_command {
my
$self
=
shift
;
my
$class
=
ref
(
$self
) ||
$self
;
my
$sub_command
=
shift
;
return
if
$sub_command
=~ /^\-/;
my
$name_for_sub_command
=
join
(
""
,
map
{
ucfirst
(
$_
) }
split
(/-/,
$sub_command
));
my
@class_name_parts
= (
split
(/::/,
$class
),
$name_for_sub_command
);
my
$sub_command_class
=
$self
->_try_command_class_named(
@class_name_parts
);
return
$sub_command_class
if
$sub_command_class
;
@class_name_parts
= ( (
map
{
$_
eq
'Command'
? () :
$_
}
@class_name_parts
) ,
'Command'
);
for
(
my
$i
=
$#class_name_parts
;
$i
> 0;
$i
--) {
$sub_command_class
=
$self
->_try_command_class_named(
@class_name_parts
);
return
$sub_command_class
if
$sub_command_class
;
$class_name_parts
[
$i
] =
$class_name_parts
[
$i
-1];
$class_name_parts
[
$i
-1] =
'Command'
;
}
my
$mapping
=
$class
->_build_sub_command_mapping;
if
(
my
$sub_command_class
=
$mapping
->{
$sub_command
}) {
return
$sub_command_class
;
}
else
{
return
;
}
}
my
$depth
= 0;
sub
__extend_namespace__ {
my
(
$self
,
$ext
) =
@_
;
my
$meta
=
$self
->SUPER::__extend_namespace__(
$ext
);
return
$meta
if
$meta
;
$depth
++;
if
(
$depth
>1) {
$depth
--;
return
;
}
my
$class
= Command::Tree::class_for_sub_command((
ref
$self
||
$self
),
$self
->_command_name_for_class_word(
$ext
));
return
$class
->__meta__
if
$class
;
return
;
}
1;