sub
execute_with_shell_params_and_exit {
my
$class
=
shift
;
if
(
@_
) {
die
"No params expected for execute_with_shell_params_and_exit()!"
;
}
my
@argv
=
@ARGV
;
@ARGV
= ();
my
$exit_code
=
$class
->_cmdline_run(
@argv
);
exit
$exit_code
;
}
sub
_cmdline_run {
my
$class
=
shift
;
my
@argv
=
@_
;
$Command::entry_point_class
||=
$class
;
$Command::entry_point_bin
||= File::Basename::basename($0);
if
(
$ENV
{COMP_CWORD}) {
my
@spec
=
$class
->resolve_option_completion_spec();
my
$options
= Getopt::Complete::Options->new(
@spec
);
$options
->handle_shell_completion;
die
"error: failed to exit after handling shell completion!"
;
}
my
$exit_code
;
eval
{
$exit_code
=
$class
->_execute_with_shell_params_and_return_exit_code(
@argv
);
UR::Context->commit or
die
"Failed to commit!: "
. UR::Context->error_message();
};
if
($@) {
$class
->error_message($@);
UR::Context->rollback or
die
"Failed to rollback changes after failed commit!!!\n"
;
$exit_code
= 255
unless
(
$exit_code
);
}
return
$exit_code
;
}
sub
_execute_with_shell_params_and_return_exit_code {
my
$class
=
shift
;
my
@argv
=
@_
;
my
$original_cmdline
=
join
(
"\0"
,$0,
@argv
);
@argv
=
map
{ (
$_
=~ /^(--\w+?)\=(.*)/) ? ($1,$2) : (
$_
) }
@argv
;
my
(
$delegate_class
,
$params
,
$errors
) =
$class
->resolve_class_and_params_for_argv(
@argv
);
my
$exit_code
;
if
(
$errors
and
@$errors
) {
$delegate_class
->dump_status_messages(1);
$delegate_class
->dump_warning_messages(1);
$delegate_class
->dump_error_messages(1);
for
my
$error
(
@$errors
) {
$delegate_class
->error_message(
join
(
' '
,
$error
->property_names) .
": "
.
$error
->desc);
}
$exit_code
= 1;
}
else
{
my
$rv
=
$class
->_execute_delegate_class_with_params(
$delegate_class
,
$params
,
$original_cmdline
);
$exit_code
=
$delegate_class
->exit_code_for_return_value(
$rv
);
}
return
$exit_code
;
}
sub
_execute_delegate_class_with_params {
my
(
$class
,
$delegate_class
,
$params
,
$original_cmdline
) =
@_
;
unless
(
$delegate_class
) {
$class
->dump_status_messages(1);
$class
->dump_warning_messages(1);
$class
->dump_error_messages(1);
$class
->dump_usage_messages(1);
$class
->dump_debug_messages(0);
$class
->usage_message(
$class
->help_usage_complete_text);
return
;
}
$delegate_class
->dump_status_messages(1);
$delegate_class
->dump_warning_messages(1);
$delegate_class
->dump_error_messages(1);
$delegate_class
->dump_usage_messages(1);
$delegate_class
->dump_debug_messages(0);
if
( !
defined
(
$params
) ) {
my
$command_name
=
$delegate_class
->command_name;
$delegate_class
->status_message(
$delegate_class
->help_usage_complete_text);
$delegate_class
->error_message(
"Please specify valid params for '$command_name'."
);
return
;
}
if
(
$params
->{help} ) {
$delegate_class
->usage_message(
$delegate_class
->help_usage_complete_text);
return
1;
}
$params
->{
'original_command_line'
} =
$original_cmdline
if
(
defined
$original_cmdline
);
my
$command_object
=
$delegate_class
->create(
%$params
);
unless
(
$command_object
) {
print
STDERR
"Exiting.\n"
;
return
;
}
$command_object
->dump_status_messages(1);
$command_object
->dump_warning_messages(1);
$command_object
->dump_error_messages(1);
$command_object
->dump_debug_messages(
$command_object
->debug);
if
(
$command_object
->debug) {
UR::ModuleBase->dump_debug_messages(
$command_object
->debug);
}
my
$rv
=
$command_object
->execute(
$params
);
if
(
$command_object
->__errors__) {
$command_object
->
delete
;
}
return
$rv
;
}
sub
resolve_class_and_params_for_argv {
my
$self
=
shift
;
my
@argv
=
@_
;
my
(
$params_hash
,
@spec
) =
$self
->_shell_args_getopt_specification;
unless
(
grep
{ /^help\W/ }
@spec
) {
push
@spec
,
"help!"
;
}
my
@error_tags
;
local
@ARGV
;
@ARGV
=
@argv
;
do
{
my
@errors
;
my
$rv
;
{
local
$SIG
{__WARN__} =
sub
{
push
@errors
,
@_
};
$rv
= GetOptions(
$params_hash
,
@spec
);
}
unless
(
$rv
) {
for
my
$error
(
@errors
) {
$self
->error_message(
$error
);
}
return
(
$self
,
undef
);
}
};
if
(
my
@names
=
$self
->_bare_shell_argument_names) {
for
(
my
$n
=0;
$n
<
@ARGV
;
$n
++) {
my
$name
=
$names
[
$n
];
unless
(
$name
) {
$self
->error_message(
"Unexpected bare arguments: @ARGV[$n..$#ARGV]!"
);
return
(
$self
,
undef
);
}
my
$value
=
$ARGV
[
$n
];
my
$meta
=
$self
->__meta__->property_meta_for_name(
$name
);
if
(
$meta
->is_many and
$n
==
$#names
) {
$params_hash
->{
$name
} = [
@ARGV
[
$n
..
$#ARGV
]];
last
;
}
else
{
$params_hash
->{
$name
} =
$value
;
}
}
}
if
(
@ARGV
and not
$self
->_bare_shell_argument_names) {
$self
->error_message(
"Unexpected bare arguments: @ARGV!"
);
return
(
$self
,
undef
);
}
for
my
$key
(
keys
%$params_hash
) {
my
$value
=
$params_hash
->{
$key
};
if
(
ref
(
$value
)) {
my
@new_value
;
for
my
$v
(
@$value
) {
my
@parts
=
split
(/,\s*/,
$v
);
push
@new_value
,
@parts
;
}
@$value
=
@new_value
;
}
elsif
(
$value
eq
q('')
or
$value
eq
q("")
) {
$params_hash
->{
$key
} =
''
;
}
my
$new_key
=
$key
;
next
unless
(
$new_key
=~
tr
/-/_/);
if
(
exists
$params_hash
->{
$new_key
} &&
exists
$params_hash
->{
$key
}) {
delete
$params_hash
->{
$key
};
next
;
}
$params_hash
->{
$new_key
} =
delete
$params_hash
->{
$key
};
}
if
(
$params_hash
->{help}) {
return
(
$self
,
$params_hash
);
}
my
$params
=
$params_hash
;
my
$class
=
$self
->class;
if
(
my
@errors
=
$self
->_errors_from_missing_parameters(
$params
)) {
return
(
$class
,
$params
, \
@errors
);
}
unless
(
@_
) {
return
(
$class
,
$params
);
}
local
$ENV
{UR_COMMAND_DUMP_STATUS_MESSAGES} = (!
exists
(
$ENV
{UR_COMMAND_DUMP_STATUS_MESSAGES})
or
$ENV
{UR_COMMAND_DUMP_STATUS_MESSAGES});
my
@params_to_resolve
=
$self
->_params_to_resolve(
$params
);
for
my
$p
(
@params_to_resolve
) {
my
$param_arg_str
=
join
(
','
, @{
$p
->{value}});
my
$pmeta
=
$self
->__meta__->property(
$p
->{name});
my
@params
;
eval
{
@params
=
$self
->resolve_param_value_from_cmdline_text(
$p
);
};
if
($@) {
push
@error_tags
, UR::Object::Tag->create(
type
=>
'invalid'
,
properties
=> [
$p
->{name}],
desc
=>
"Errors while resolving from $param_arg_str: $@"
,
);
}
if
(
@params
and
$params
[0]) {
if
(
$pmeta
->{
'is_many'
}) {
$params
->{
$p
->{name}} = \
@params
;
}
else
{
$params
->{
$p
->{name}} =
$params
[0];
}
}
else
{
push
@error_tags
, UR::Object::Tag->create(
type
=>
'invalid'
,
properties
=> [
$p
->{name}],
desc
=>
"Problem resolving from $param_arg_str."
,
);
}
}
if
(
@error_tags
) {
return
(
$class
,
undef
, \
@error_tags
);
}
else
{
return
(
$class
,
$params
);
}
}
sub
resolve_option_completion_spec {
my
$class
=
shift
;
my
@completion_spec
=
$class
->_shell_args_getopt_complete_specification;
no
warnings;
unless
(
grep
{ /^help\W/ }
@completion_spec
) {
push
@completion_spec
,
"help!"
=>
undef
;
}
return
\
@completion_spec
}
sub
_errors_from_missing_parameters {
my
(
$self
,
$params
) =
@_
;
my
$class_meta
=
$self
->__meta__;
my
@all_property_metas
=
$class_meta
->properties();
my
@specified_property_metas
=
grep
{
exists
$params
->{
$_
->property_name} }
@all_property_metas
;
my
%specified_property_metas
=
map
{
$_
->
property_name
=>
$_
}
@specified_property_metas
;
my
%set_indirectly
;
my
@todo
=
@specified_property_metas
;
while
(
my
$property_meta
=
shift
@todo
) {
if
(
my
$via
=
$property_meta
->via) {
if
(not
$property_meta
->is_mutable) {
my
$list
=
$set_indirectly
{
$via
} ||= [];
push
@$list
,
$property_meta
;
}
unless
(
$specified_property_metas
{
$via
}) {
my
$via_meta
=
$specified_property_metas
{
$via
} =
$class_meta
->property(
$via
);
push
@specified_property_metas
,
$via_meta
;
push
@todo
,
$via_meta
;
}
}
elsif
(
my
$id_by
=
$property_meta
) {
my
$list
=
$set_indirectly
{
$id_by
} ||= [];
push
@$list
,
$property_meta
;
unless
(
$specified_property_metas
{
$id_by
}) {
my
$id_by_meta
=
$specified_property_metas
{
$id_by
} =
$class_meta
->property(
$id_by
);
push
@specified_property_metas
,
$id_by_meta
;
push
@todo
,
$id_by_meta
;
}
}
}
my
@property_names
;
if
(
my
$has
=
$class_meta
->{
has
}) {
@property_names
= List::MoreUtils::uniq(
keys
%$has
);
}
my
@property_metas
=
map
{
$class_meta
->property_meta_for_name(
$_
); }
@property_names
;
my
@error_tags
;
for
my
$property_meta
(
@property_metas
) {
my
$pn
=
$property_meta
->property_name;
next
if
$property_meta
->is_optional;
next
if
$property_meta
->implied_by;
next
if
defined
$property_meta
->default_value;
next
if
defined
$params
->{
$pn
};
next
if
$set_indirectly
{
$pn
};
if
(
my
$via
=
$property_meta
->via) {
if
(
$params
->{
$via
} or
$set_indirectly
{
$via
}) {
next
;
}
}
my
$arg
=
$pn
;
$arg
=~ s/_/-/g;
$arg
=
"--$arg"
;
if
(
$property_meta
->is_output and not
$property_meta
->is_input and not
$property_meta
->is_param) {
if
(
$property_meta
->_data_type_as_class_name->__meta__->data_source
and not
$property_meta
->_data_type_as_class_name->isa(
"UR::Value"
)
) {
next
;
}
elsif
(
$property_meta
->is_calculated) {
next
;
}
else
{
push
@error_tags
, UR::Object::Tag->create(
type
=>
'invalid'
,
properties
=> [
$pn
],
desc
=>
"Output requires specified destination: "
.
$arg
.
"."
);
}
}
else
{
$DB::single
= 1;
push
@error_tags
, UR::Object::Tag->create(
type
=>
'invalid'
,
properties
=> [
$pn
],
desc
=>
"Missing required parameter: "
.
$arg
.
"."
);
}
}
return
@error_tags
;
}
sub
_params_to_resolve {
my
(
$self
,
$params
) =
@_
;
my
@params_to_resolve
;
if
(
$params
) {
my
$cmeta
=
$self
->__meta__;
my
@params_will_require_verification
;
my
@params_may_require_verification
;
for
my
$param_name
(
keys
%$params
) {
my
$pmeta
=
$cmeta
->property(
$param_name
);
unless
(
$pmeta
) {
$self
->warning_message(
"No metadata for property '$param_name'"
);
next
;
}
my
$param_type
=
$pmeta
->data_type;
next
unless
(
$self
->_can_resolve_type(
$param_type
));
my
$param_arg
=
$params
->{
$param_name
};
if
(
my
$arg_type
=
ref
(
$param_arg
)) {
next
if
$arg_type
eq
$param_type
;
if
(
$arg_type
ne
'ARRAY'
) {
$self
->error_message(
"no handler for property '$param_name' with argument type "
.
ref
(
$param_arg
));
next
;
}
}
else
{
$param_arg
= [
$param_arg
];
}
next
unless
(
@$param_arg
);
my
$resolve_info
= {
name
=>
$param_name
,
class
=>
$param_type
,
value
=>
$param_arg
,
};
push
(
@params_to_resolve
,
$resolve_info
);
my
$require_user_verify
=
$pmeta
->{
'require_user_verify'
};
if
(
defined
(
$require_user_verify
) ) {
push
@params_will_require_verification
,
"'$param_name'"
if
(
$require_user_verify
);
}
else
{
push
@params_may_require_verification
,
"'$param_name'"
;
}
}
my
@adverbs
= (
'will'
,
'may'
);
my
@params_adverb_require_verification
= (
\
@params_will_require_verification
,
\
@params_may_require_verification
,
);
for
(
my
$i
= 0;
$i
<
@adverbs
;
$i
++) {
my
$adverb
=
$adverbs
[
$i
];
my
@param_adverb_require_verification
= @{
$params_adverb_require_verification
[
$i
]};
next
unless
(
@param_adverb_require_verification
);
if
(
@param_adverb_require_verification
> 1) {
$param_adverb_require_verification
[-1] =
'and '
.
$param_adverb_require_verification
[-1];
}
my
$param_str
=
join
(
', '
,
@param_adverb_require_verification
);
$self
->status_message(
$param_str
.
" $adverb require verification..."
);
}
}
return
@params_to_resolve
;
}
sub
_can_resolve_type {
my
(
$self
,
$type
) =
@_
;
return
0
unless
(
$type
);
my
$non_classes
= 0;
if
(
ref
(
$type
) ne
'ARRAY'
) {
$non_classes
=
$type
!~ m/::/;
}
else
{
$non_classes
=
scalar
grep
{ ! m/::/ }
@$type
;
}
return
$non_classes
== 0;
}
sub
_shell_args_property_meta {
my
$self
=
shift
;
my
$class_meta
=
$self
->__meta__;
my
(
$rule
,
%extra
) = UR::Object::Property->define_boolexpr(
@_
);
my
%seen
;
my
(
@positional
,
@required_input
,
@required_param
,
@optional_input
,
@optional_param
,
@output
);
my
@property_meta
=
$class_meta
->properties();
PROP:
foreach
my
$property_meta
(
@property_meta
) {
my
$property_name
=
$property_meta
->property_name;
next
if
$seen
{
$property_name
}++;
next
unless
$rule
->evaluate(
$property_meta
);
next
unless
$property_meta
->can(
"is_param"
) and (
$property_meta
->is_param or
$property_meta
->is_input or
$property_meta
->is_output);
if
(
%extra
) {
no
warnings;
for
my
$key
(
keys
%extra
) {
if
(
$property_meta
->
$key
ne
$extra
{
$key
}) {
next
PROP;
}
}
}
next
if
$property_name
eq
'id'
;
next
if
$property_name
eq
'result'
;
next
if
$property_name
eq
'is_executed'
;
next
if
$property_name
eq
'original_command_line'
;
next
if
$property_name
=~ /^_/;
next
if
$property_meta
->implied_by;
next
if
$property_meta
->is_calculated;
next
if
$property_meta
->is_transient;
next
if
$property_meta
->is_constant;
if
((
$property_meta
->is_delegated) || (
defined
(
$property_meta
->data_type) and
$property_meta
->data_type =~ /::/)) {
next
unless
(
$self
->can(
'resolve_param_value_from_cmdline_text'
));
}
else
{
next
unless
(
$property_meta
->is_mutable);
}
if
(
$property_meta
->{shell_args_position}) {
push
@positional
,
$property_meta
;
}
elsif
(
$property_meta
->is_optional) {
if
(
$property_meta
->is_input or
$property_meta
->is_output) {
push
@optional_input
,
$property_meta
;
}
elsif
(
$property_meta
->is_param) {
push
@optional_param
,
$property_meta
;
}
}
else
{
if
(
$property_meta
->is_input or
$property_meta
->is_output) {
push
@required_input
,
$property_meta
;
}
elsif
(
$property_meta
->is_param) {
push
@required_param
,
$property_meta
;
}
}
}
my
@result
;
@result
= (
(
sort
{
$a
->position_in_module_header cmp
$b
->position_in_module_header }
@required_param
),
(
sort
{
$a
->position_in_module_header cmp
$b
->position_in_module_header }
@optional_param
),
(
sort
{
$a
->position_in_module_header cmp
$b
->position_in_module_header }
@required_input
),
(
sort
{
$a
->position_in_module_header cmp
$b
->position_in_module_header }
@optional_input
),
(
sort
{
$a
->shell_args_position <=>
$b
->shell_args_position }
@positional
),
);
return
@result
;
}
sub
_shell_arg_name_from_property_meta {
my
(
$self
,
$property_meta
,
$singularize
) =
@_
;
my
$property_name
= (
$singularize
?
$property_meta
->singular_name :
$property_meta
->property_name);
my
$param_name
=
$property_name
;
$param_name
=~ s/_/-/g;
return
$param_name
;
}
sub
_shell_arg_getopt_qualifier_from_property_meta {
my
(
$self
,
$property_meta
) =
@_
;
my
$many
= (
$property_meta
->is_many ?
'@'
:
''
);
if
(
defined
(
$property_meta
->data_type) and
$property_meta
->data_type =~ /Boolean/) {
return
'!'
.
$many
;
}
else
{
return
'=s'
.
$many
;
}
}
sub
_shell_arg_usage_string_from_property_meta {
my
(
$self
,
$property_meta
) =
@_
;
my
$string
=
$self
->_shell_arg_name_from_property_meta(
$property_meta
);
if
(
$property_meta
->{shell_args_position}) {
$string
=
uc
(
$string
);
}
if
(
$property_meta
->{shell_args_position}) {
if
(
$property_meta
->is_optional) {
$string
=
"[$string]"
;
}
}
else
{
$string
=
"--$string"
;
if
(
defined
(
$property_meta
->data_type) and
$property_meta
->data_type =~ /Boolean/) {
$string
=
"[$string]"
;
}
else
{
if
(
$property_meta
->is_many) {
$string
.=
"=?[,?]"
;
}
else
{
$string
.=
'=?'
;
}
if
(
$property_meta
->is_optional) {
$string
=
"[$string]"
;
}
}
}
return
$string
;
}
sub
_shell_arg_getopt_specification_from_property_meta {
my
(
$self
,
$property_meta
) =
@_
;
my
$arg_name
=
$self
->_shell_arg_name_from_property_meta(
$property_meta
);
return
(
$arg_name
.
$self
->_shell_arg_getopt_qualifier_from_property_meta(
$property_meta
),
);
}
sub
_shell_arg_getopt_complete_specification_from_property_meta {
my
(
$self
,
$property_meta
) =
@_
;
my
$arg_name
=
$self
->_shell_arg_name_from_property_meta(
$property_meta
);
my
$completions
=
$property_meta
->valid_values;
if
(
$completions
) {
if
(
ref
(
$completions
) eq
'ARRAY'
) {
$completions
= [
@$completions
];
}
}
else
{
my
$type
=
$property_meta
->data_type;
my
@complete_as_files
= (
'File'
,
'FilePath'
,
'Filesystem'
,
'FileSystem'
,
'FilesystemPath'
,
'FileSystemPath'
,
'Text'
,
'String'
,
);
my
@complete_as_directories
= (
'Directory'
,
'DirectoryPath'
,
'Dir'
,
'DirPath'
,
);
if
(!
defined
(
$type
)) {
$completions
=
'files'
;
}
else
{
for
my
$pattern
(
@complete_as_files
) {
if
(!
$type
||
$type
eq
$pattern
) {
$completions
=
'files'
;
last
;
}
}
for
my
$pattern
(
@complete_as_directories
) {
if
(
$type
&&
$type
eq
$pattern
) {
$completions
=
'directories'
;
last
;
}
}
}
}
return
(
$arg_name
.
$self
->_shell_arg_getopt_qualifier_from_property_meta(
$property_meta
),
$completions
,
);
}
sub
_shell_args_getopt_specification {
my
$self
=
shift
;
my
@getopt
;
my
@params
;
for
my
$meta
(
$self
->_shell_args_property_meta) {
my
(
$spec
,
@params_addition
) =
$self
->_shell_arg_getopt_specification_from_property_meta(
$meta
);
push
@getopt
,
$spec
;
push
@params
,
@params_addition
;
}
@getopt
=
sort
@getopt
;
return
{
@params
},
@getopt
;
}
sub
_shell_args_getopt_complete_specification {
my
$self
=
shift
;
my
@getopt
;
for
my
$meta
(
$self
->_shell_args_property_meta) {
my
(
$spec
,
$completions
) =
$self
->_shell_arg_getopt_complete_specification_from_property_meta(
$meta
);
push
@getopt
,
$spec
,
$completions
;
}
return
@getopt
;
}
sub
_bare_shell_argument_names {
my
$self
=
shift
;
my
$meta
=
$self
->__meta__;
my
@ordered_names
=
map
{
$_
->property_name }
sort
{
$a
->{shell_args_position} <=>
$b
->{shell_args_position} }
grep
{
$_
->{shell_args_position} }
$self
->_shell_args_property_meta();
return
@ordered_names
;
}
our
%ALTERNATE_FROM_CLASS
= ();
our
%SEEN_FROM_CLASS
= ();
our
$MESSAGE
;
sub
resolve_param_value_from_cmdline_text {
my
(
$self
,
$param_info
) =
@_
;
my
$param_name
=
$param_info
->{name};
my
$param_class
=
$param_info
->{class};
my
@param_args
= @{
$param_info
->{value}};
my
$param_str
=
join
(
','
,
@param_args
);
if
(
ref
(
$param_class
) eq
'ARRAY'
) {
my
@param_class
=
@$param_class
;
if
(
@param_class
> 1) {
die
'Multiple data types on command arguments are not supported.'
;
}
else
{
$param_class
=
$param_class
[0];
}
}
my
$param_resolve_message
=
"Resolving parameter '$param_name' from command argument '$param_str'..."
;
my
$pmeta
=
$self
->__meta__->property(
$param_name
);
my
$require_user_verify
=
$pmeta
->{
'require_user_verify'
};
my
@results
;
my
$bx
=
eval
{ UR::BoolExpr->resolve_for_string(
$param_class
,
$param_str
) };
my
$bx_error
= $@;
if
(
$bx
) {
@results
=
$param_class
->get(
$bx
);
if
(
@results
> 1 && !
defined
(
$require_user_verify
)) {
$require_user_verify
= 1;
}
}
else
{
for
my
$arg
(
@param_args
) {
%SEEN_FROM_CLASS
= ();
my
@arg_results
=
$self
->resolve_param_value_from_text(
$arg
,
$param_class
);
if
(
@arg_results
!= 1 && !
defined
(
$require_user_verify
)) {
$require_user_verify
= 1;
}
push
@results
,
@arg_results
;
}
}
if
(
@results
) {
@results
= List::MoreUtils::uniq(
@results
);
@results
=
grep
{
$_
->isa(
$param_class
) }
@results
;
$self
->status_message(
$param_resolve_message
.
" found "
.
@results
);
}
else
{
if
(
$bx_error
) {
$self
->status_message(
$bx_error
);
}
$self
->status_message(
$param_resolve_message
.
" none found."
);
}
return
unless
(
@results
);
my
$limit_results_method
=
"_limit_results_for_$param_name"
;
if
(
$self
->can(
$limit_results_method
) ) {
@results
=
$self
->
$limit_results_method
(
@results
);
return
unless
(
@results
);
}
@results
= List::MoreUtils::uniq(
@results
);
if
(
$require_user_verify
) {
if
(!
$pmeta
->{
'is_many'
} &&
@results
> 1) {
$MESSAGE
.=
"\n"
if
(
$MESSAGE
);
$MESSAGE
.=
"'$param_name' expects only one result."
;
if
(
$ENV
{UR_NO_REQUIRE_USER_VERIFY}) {
die
"$MESSAGE\n"
;
}
}
@results
=
$self
->_get_user_verification_for_param_value(
$param_name
,
@results
);
}
while
(!
$pmeta
->{
'is_many'
} &&
@results
> 1) {
$MESSAGE
.=
"\n"
if
(
$MESSAGE
);
$MESSAGE
.=
"'$param_name' expects only one result, not many!"
;
@results
=
$self
->_get_user_verification_for_param_value(
$param_name
,
@results
);
}
if
(
wantarray
) {
return
@results
;
}
elsif
(not
defined
wantarray
) {
return
;
}
elsif
(
@results
> 1) {
Carp::confess(
"Multiple matches found!"
);
}
else
{
return
$results
[0];
}
}
sub
resolve_param_value_from_text {
my
(
$self
,
$param_arg
,
$param_class
,
$via_method
) =
@_
;
unless
(
$param_class
) {
$param_class
=
$self
->class;
}
$SEEN_FROM_CLASS
{
$param_class
} = 1;
my
@results
;
eval
{
@results
=
$self
->_resolve_param_value_from_text_by_bool_expr(
$param_class
,
$param_arg
); };
Carp::croak($@)
if
($@ and $@ !~ m/Not a valid BoolExpr/);
if
(!
@results
&& !$@) {
return
;
}
if
(!
@results
&&
$param_arg
!~ /,/) {
my
@results_by_string
;
if
(
$param_class
->can(
'_resolve_param_value_from_text_by_name_or_id'
)) {
@results_by_string
=
$param_class
->_resolve_param_value_from_text_by_name_or_id(
$param_arg
);
}
else
{
@results_by_string
=
$self
->_resolve_param_value_from_text_by_name_or_id(
$param_class
,
$param_arg
);
}
push
@results
,
@results_by_string
;
}
if
(!
@results
&&
$param_arg
!~ /,/) {
@results
=
$self
->_resolve_param_value_via_related_class_method(
$param_class
,
$param_arg
,
$via_method
);
}
if
(
$via_method
) {
@results
=
map
{
$_
->
$via_method
}
@results
;
}
if
(
wantarray
) {
return
@results
;
}
elsif
(not
defined
wantarray
) {
return
;
}
elsif
(
@results
> 1) {
Carp::confess(
"Multiple matches found!"
);
}
else
{
return
$results
[0];
}
}
sub
_resolve_param_value_via_related_class_method {
my
(
$self
,
$param_class
,
$param_arg
,
$via_method
) =
@_
;
my
@results
;
my
$via_class
;
if
(
exists
(
$ALTERNATE_FROM_CLASS
{
$param_class
})) {
$via_class
=
$param_class
;
}
else
{
for
my
$class
(
keys
%ALTERNATE_FROM_CLASS
) {
if
(
$param_class
->isa(
$class
)) {
if
(
$via_class
) {
$self
->error_message(
"Found additional via_class $class but already found $via_class!"
);
}
$via_class
=
$class
;
}
}
}
if
(
$via_class
) {
my
@from_classes
=
sort
keys
%{
$ALTERNATE_FROM_CLASS
{
$via_class
}};
while
(
@from_classes
&& !
@results
) {
my
$from_class
=
shift
@from_classes
;
my
@methods
= @{
$ALTERNATE_FROM_CLASS
{
$via_class
}{
$from_class
}};
my
$method
;
if
(
@methods
> 1 && !
$via_method
&& !
$ENV
{UR_NO_REQUIRE_USER_VERIFY}) {
$self
->status_message(
"Trying to find $via_class via $from_class...\n"
);
my
$method_choices
;
for
(
my
$i
= 0;
$i
<
@methods
;
$i
++) {
$method_choices
.= (
$i
+ 1) .
": "
.
$methods
[
$i
];
$method_choices
.=
" [default]"
if
(
$i
== 0);
$method_choices
.=
"\n"
;
}
$method_choices
.= (
scalar
(
@methods
) + 1) .
": none\n"
;
$method_choices
.=
"Which method would you like to use?"
;
my
$response
=
$self
->_ask_user_question(
$method_choices
, 0,
'\d+'
, 1,
'#'
);
if
(
$response
=~ /^\d+$/) {
$response
--;
if
(
$response
==
@methods
) {
$method
=
undef
;
}
elsif
(
$response
>= 0 &&
$response
<=
$#methods
) {
$method
=
$methods
[
$response
];
}
else
{
$self
->error_message(
"Response was out of bounds, exiting..."
);
exit
;
}
$ALTERNATE_FROM_CLASS
{
$via_class
}{
$from_class
} = [
$method
];
}
elsif
(!
$response
) {
$self
->status_message(
"Exiting..."
);
}
}
else
{
$method
=
$methods
[0];
}
unless
(
$SEEN_FROM_CLASS
{
$from_class
}) {
@results
=
eval
{
$self
->resolve_param_value_from_text(
$param_arg
,
$from_class
,
$method
)};
}
}
}
return
@results
;
}
sub
_resolve_param_value_from_text_by_bool_expr {
my
(
$self
,
$param_class
,
$arg
) =
@_
;
my
@results
;
my
$bx
=
eval
{
UR::BoolExpr->resolve_for_string(
$param_class
,
$arg
);
};
if
(
$bx
) {
@results
=
$param_class
->get(
$bx
);
}
else
{
die
"Not a valid BoolExpr"
;
}
return
@results
;
}
sub
_try_get_by_id {
my
(
$self
,
$param_class
,
$str
) =
@_
;
my
$class_meta
=
$param_class
->__meta__;
my
@id_property_names
=
$class_meta
->id_property_names;
if
(
@id_property_names
== 0) {
die
"Failed to determine ID property names for class ($param_class)."
;
}
elsif
(
@id_property_names
== 1) {
my
$id_data_type
=
$class_meta
->property_meta_for_name(
$id_property_names
[0])->_data_type_as_class_name ||
''
;
if
(
$id_data_type
->isa(
'UR::Value::Number'
)) {
return
(
$str
=~ /^[+-]?\d+$/);
}
}
return
1;
}
sub
_resolve_param_value_from_text_by_name_or_id {
my
(
$self
,
$param_class
,
$str
) =
@_
;
my
(
@results
);
if
(
$self
->_try_get_by_id(
$param_class
,
$str
)) {
@results
=
eval
{
$param_class
->get(
$str
) };
}
if
(!
@results
&&
$param_class
->can(
'name'
)) {
@results
=
$param_class
->get(
name
=>
$str
);
unless
(
@results
) {
@results
=
$param_class
->get(
"name like"
=>
"$str"
);
}
}
return
@results
;
}
sub
_get_user_verification_for_param_value {
my
(
$self
,
$param_name
,
@list
) =
@_
;
my
$n_list
=
scalar
(
@list
);
if
(
$n_list
> 200 && !
$ENV
{UR_NO_REQUIRE_USER_VERIFY}) {
my
$response
=
$self
->_ask_user_question(
"Would you [v]iew all $n_list item(s) for '$param_name', (p)roceed, or e(x)it?"
, 0,
'[v]|p|x'
,
'v'
);
if
(!
$response
||
$response
eq
'x'
) {
$self
->status_message(
"Exiting..."
);
exit
;
}
return
@list
if
(
$response
eq
'p'
);
}
my
@new_list
;
while
(!
@new_list
) {
@new_list
=
$self
->_get_user_verification_for_param_value_drilldown(
$param_name
,
@list
);
}
my
@ids
=
map
{
$_
->id }
@new_list
;
$self
->status_message(
"The IDs for your selection are:\n"
.
join
(
','
,
@ids
) .
"\n\n"
);
return
@new_list
;
}
sub
_get_user_verification_for_param_value_drilldown {
my
(
$self
,
$param_name
,
@results
) =
@_
;
my
$n_results
=
scalar
(
@results
);
my
$pad
=
length
(
$n_results
);
return
@results
if
(
$ENV
{UR_NO_REQUIRE_USER_VERIFY});
return
if
(
@results
== 0);
my
@dnames
=
map
{
$_
->__display_name__}
grep
{
$_
->can(
'__display_name__'
) }
@results
;
my
$max_dname_length
=
@dnames
?
length
((
sort
{
length
(
$b
) <=>
length
(
$a
) }
@dnames
)[0]) : 0;
my
@statuses
=
map
{
$_
->status ||
'missing_status'
}
grep
{
$_
->can(
'status'
) }
@results
;
my
$max_status_length
=
@statuses
?
length
((
sort
{
length
(
$b
) <=>
length
(
$a
) }
@statuses
)[0]) : 0;
my
@results_with_display_name_and_class
=
map
{ [
$_
->__display_name__,
$_
->class,
$_
] }
@results
;
@results
=
map
{
$_
->[2] }
sort
{
$a
->[1] cmp
$b
->[1] }
sort
{
$a
->[0] cmp
$b
->[0] }
@results_with_display_name_and_class
;
my
@classes
= List::MoreUtils::uniq(
map
{
$_
->class}
@results
);
my
$response
;
my
@caller
=
caller
(1);
while
(!
$response
) {
$self
->status_message(
"\n"
);
for
(
my
$i
= 1;
$i
<=
$n_results
;
$i
++) {
my
$param
=
$results
[
$i
- 1];
my
$num
=
$self
->_pad_string(
$i
,
$pad
);
my
$msg
=
"$num:"
;
$msg
.=
' '
.
$self
->_pad_string(
$param
->__display_name__,
$max_dname_length
,
'suffix'
);
my
$status
=
' '
;
if
(
$param
->can(
'status'
)) {
$status
=
$param
->status ||
'missing_status'
;
}
$msg
.=
"\t"
.
$self
->_pad_string(
$status
,
$max_status_length
,
'suffix'
);
$msg
.=
"\t"
.
$param
->class
if
(
@classes
> 1);
$self
->status_message(
$msg
);
}
if
(
$MESSAGE
) {
$MESSAGE
=
"\n"
.
'*'
x80 .
"\n"
.
$MESSAGE
.
"\n"
.
'*'
x80 .
"\n"
;
$self
->status_message(
$MESSAGE
);
$MESSAGE
=
''
;
}
my
$pretty_values
=
'(c)ontinue, (h)elp, e(x)it'
;
my
$valid_values
=
'\*|c|h|x|[-+]?[\d\-\., ]+'
;
if
(
$caller
[3] =~ /_trim_list_from_response/) {
$pretty_values
.=
', (b)ack'
;
$valid_values
.=
'|b'
;
}
$response
=
$self
->_ask_user_question(
"Please confirm the above items for '$param_name' or modify your selection."
, 0,
$valid_values
,
'h'
,
$pretty_values
.
', or specify item numbers to use'
);
if
(
lc
(
$response
) eq
'h'
|| !
$self
->_validate_user_response_for_param_value_verification(
$response
)) {
$MESSAGE
.=
"\n"
if
(
$MESSAGE
);
$MESSAGE
.=
"Help:\n"
.
"* Specify which elements to keep by listing them, e.g. '1,3,12' would keep\n"
.
" items 1, 3, and 12.\n"
.
"* Begin list with a minus to remove elements, e.g. '-1,3,9' would remove\n"
.
" items 1, 3, and 9.\n"
.
"* Ranges can be used, e.g. '-11-17, 5' would remove items 11 through 17 and\n"
.
" remove item 5."
;
$response
=
''
;
}
}
if
(
lc
(
$response
) eq
'x'
) {
$self
->status_message(
"Exiting..."
);
exit
;
}
elsif
(
lc
(
$response
) eq
'b'
) {
return
;
}
elsif
(
lc
(
$response
) eq
'c'
|
$response
eq
'*'
) {
return
@results
;
}
elsif
(
$response
=~ /^[-+]?[\d\-\., ]+$/) {
@results
=
$self
->_trim_list_from_response(
$response
,
$param_name
,
@results
);
return
@results
;
}
else
{
die
$self
->error_message(
"Conditional exception, should not have been reached!"
);
}
}
sub
terminal_input_filehandle {
my
$self
=
shift
;
my
$fh
= IO::File->new(
'/dev/tty'
,
'r'
);
unless
(
$fh
) {
Carp::carp(
"Couldn't open /dev/tty for terminal input: $!\n Using STDIN..."
);
$fh
=
*STDIN
;
}
return
$fh
;
}
sub
_ask_user_question {
my
$self
=
shift
;
my
$question
=
shift
;
my
$timeout
=
shift
;
my
$valid_values
=
shift
||
"yes|no"
;
my
$default_value
=
shift
||
undef
;
my
$pretty_valid_values
=
shift
||
$valid_values
;
$valid_values
=
lc
(
$valid_values
);
my
$input
;
$timeout
= 60
unless
(
defined
(
$timeout
));
local
$SIG
{ALRM} =
sub
{
print
STDERR
"Exiting, failed to reply to question '$question' within '$timeout' seconds.\n"
;
exit
; };
print
STDERR
"\n$question\n"
;
print
STDERR
"Reply with $pretty_valid_values: "
;
unless
(
$self
->_can_interact_with_user) {
print
STDERR
"\n"
;
die
$self
->error_message(
"Attempting to ask user question but cannot interact with user!"
);
}
my
$terminal
=
$self
->terminal_input_filehandle();
alarm
(
$timeout
)
if
(
$timeout
);
chomp
(
$input
=
$terminal
->getline());
alarm
(0)
if
(
$timeout
);
print
STDERR
"\n"
;
if
(
lc
(
$input
) =~ /^
$valid_values
$/) {
return
lc
(
$input
);
}
elsif
(
$default_value
) {
return
$default_value
;
}
else
{
$self
->error_message(
"'$input' is an invalid answer to question '$question'\n\n"
);
return
;
}
}
sub
_validate_user_response_for_param_value_verification {
my
(
$self
,
$response_text
) =
@_
;
$response_text
=
substr
(
$response_text
, 1)
if
(
$response_text
=~ /^[+-]/);
my
@response
=
split
(/[\s\,]/,
$response_text
);
for
my
$response
(
@response
) {
if
(
$response
=~ /^[xbc*]$/) {
return
1;
}
if
(
$response
!~ /^(\d+)([-\.]+(\d+))?$/) {
$MESSAGE
.=
"\n"
if
(
$MESSAGE
);
$MESSAGE
.=
"ERROR: Invalid list provided ($response)"
;
return
0;
}
if
($3 && $1 && $3 < $1) {
$MESSAGE
.=
"\n"
if
(
$MESSAGE
);
$MESSAGE
.=
"ERROR: Inverted range provided ($1-$3)"
;
return
0;
}
}
return
1;
}
sub
_trim_list_from_response {
my
(
$self
,
$response_text
,
$param_name
,
@list
) =
@_
;
my
$method
;
if
(
$response_text
=~ /^[+-]/) {
$method
=
substr
(
$response_text
, 0, 1);
$response_text
=
substr
(
$response_text
, 1);
}
else
{
$method
=
'+'
;
}
my
@response
=
split
(/[\s\,]/,
$response_text
);
my
%indices
;
@indices
{0..
$#list
} = 0..
$#list
if
(
$method
eq
'-'
);
for
my
$response
(
@response
) {
$response
=~ /^(\d+)([-\.]+(\d+))?$/;
my
$low
= $1;
$low
--;
my
$high
= $3 || $1;
$high
--;
die
if
(
$high
<
$low
);
if
(
$method
eq
'+'
) {
@indices
{
$low
..
$high
} =
$low
..
$high
;
}
else
{
delete
@indices
{
$low
..
$high
};
}
}
my
@new_list
=
$self
->_get_user_verification_for_param_value_drilldown(
$param_name
,
@list
[
sort
keys
%indices
]);
unless
(
@new_list
) {
@new_list
=
$self
->_get_user_verification_for_param_value_drilldown(
$param_name
,
@list
);
}
return
@new_list
;
}
sub
_pad_string {
my
(
$self
,
$str
,
$width
,
$pos
) =
@_
;
$str
=
''
if
!
defined
$str
;
my
$padding
=
$width
-
length
(
$str
);
$padding
= 0
if
(
$padding
< 0);
if
(
$pos
&&
$pos
eq
'suffix'
) {
return
$str
.
' '
x
$padding
;
}
else
{
return
' '
x
$padding
.
$str
;
}
}
sub
_can_interact_with_user {
my
$self
=
shift
;
if
( -t STDERR ) {
return
1;
}
else
{
return
0;
}
}
1;