From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

package Command::V2; # additional methods to dispatch from a command-line
use strict;
# instead of tacking these methods onto general Command::V2 objects
# they could be put on the Command::Shell class, which is a wrapper/adaptor Command for translating from
# command-line shell to purely functional commands.
# old entry point
# new cmds will call Command::Shell->run("MyClass",@ARGV)
# which goes straight into _cmdline_run for now...
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 {
# This automatically parses command-line options and "does the right thing":
# TODO: abstract out all dispatchers for commands into a given API
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);
# make --foo=bar equivalent to --foo bar
@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 {
# this is called by both the shell dispatcher and http dispatcher for now
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);
# FIXME There should be a better check for params that are there because they came from the
# command line, and params that exist for infrastructural purposes. 'original_command_line'
# won't ever be given on the command line and shouldn't count toward the next test.
# maybe check the is_input properties...
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) {
# The delegate class should have emitted an error message.
# This is just in case the developer is sloppy, and the user will think the task did not fail.
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 {
# This is used by execute_with_shell_params_and_exit, but might be used within an application.
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;
# Thes nasty GetOptions modules insist on working on
# the real @ARGV, while we like a little more flexibility.
# Not a problem in Perl. :) (which is probably why it was never fixed)
local @ARGV;
@ARGV = @argv;
do {
# GetOptions also likes to emit warnings instead of return a list of errors :(
my @errors;
my $rv;
{
local $SIG{__WARN__} = sub { push @errors, @_ };
## Change the pattern to be '--', '-' followed by a non-digit, or '+'.
## This s the effect of treating a negative number as a value of an option.
## This means that we won't be allowed to have an option named, say, -1.
## But since command modules' properties have to be allowable function names,
## and "1" is not a valid function name, it's not really a problem
#Getopt::Long::Configure('prefix_pattern=--|-(?!\D)|\+');
$rv = GetOptions($params_hash,@spec);
}
unless ($rv) {
for my $error (@errors) {
$self->error_message($error);
}
return($self, undef);
}
};
# Q: Is there a standard getopt spec for capturing non-option paramters?
# Perhaps that's not getting "options" :)
# A: Yes. Use '<>'. But we need to process this anyway, so it won't help us.
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) {
# slurp the rest
$params_hash->{$name} = [@ARGV[$n..$#ARGV]];
last;
}
else {
$params_hash->{$name} = $value;
}
}
}
if (@ARGV and not $self->_bare_shell_argument_names) {
## argv but no names
$self->error_message("Unexpected bare arguments: @ARGV!");
return($self, undef);
}
for my $key (keys %$params_hash) {
# handle any has-many comma-sep values
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("")) {
# Handle the special values '' and "" to mean undef/NULL
$params_hash->{$key} = '';
}
# turn dashes into underscores
my $new_key = $key;
next unless ($new_key =~ tr/-/_/);
if (exists $params_hash->{$new_key} && exists $params_hash->{$key}) {
# this corrects a problem where is_many properties badly interact
# with bare args leaving two entries in the hash like:
# a-bare-opt => [], a_bare_opt => ['with','vals']
delete $params_hash->{$key};
next;
}
$params_hash->{$new_key} = delete $params_hash->{$key};
}
# futher work is looking for errors, and may display them
# if help is set, return now
# we might have returned sooner, but having full info available
# allows for dynamic help
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);
}
# should this be moved up into the methods which are only called
# directly from the shell, or is it okay everywhere in this module to
# presume we're a direct cmdline call? -ssmith
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;
}
}
}
# TODO: this should use @all_property_metas, and filter down to is_param and is_input
# This old code just ignores things inherited from a base class.
# We will need to be careful fixing this because it could add checks to tools which
# work currently and lead to unexpected failures.
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")
) {
# outputs with a data source do not need a specification
# on the cmdline to "store" them after execution
next;
}
elsif ($property_meta->is_calculated) {
# outputs that are calculated don't need to be specified on
# the command line
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) {
# This message was a die after a next, so I guess it isn't supposed to be fatal?
$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; # param is already the right 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__;
# Find which property metas match the rules. We have to do it this way
# because just calling 'get_all_property_metas()' will product multiple matches
# if a property is overridden in a child class
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;
# Kept commented out from UR's Command.pm, I believe is_output is a workflow property
# and not something we need to exclude (counter to the old comment below).
#next if $property_meta->{is_output}; # TODO: This was breaking the G::M::T::Annotate::TranscriptVariants annotator. This should probably still be here but temporarily roll back
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;
}
#elsif ($property_meta->is_optional) {
# return ':s' . $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),
#this prevents defaults from being used for is_many properties
#($property_meta->is_many ? ($arg_name => []) : ())
);
}
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,
# ($property_meta->is_many ? ($arg_name => []) : ())
);
}
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;
}
#
# Logic to turn command-line text into objects for parameter/input values
#
our %ALTERNATE_FROM_CLASS = ();
# This will prevent infinite loops during recursion.
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 = ();
# call resolve_param_value_from_text without a via_method to "bootstrap" recursion
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) {
# the ALTERNATE_FROM_CLASS stuff leads to non $param_class objects in 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;
# try getting BoolExpr, otherwise fallback on '_resolve_param_value_from_text_by_name_or_id' parser
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 && !$@) {
# no result and was valid BoolExpr then we don't want to break it apart because we
# could query enormous amounts of info
return;
}
# the first param_arg is all param_args to try BoolExpr so skip if it has commas
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 we still don't have any values then try via alternate class
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}) {
#$self->debug_message("Trying to find $via_class via $from_class->$method...");
@results = eval {$self->resolve_param_value_from_text($param_arg, $from_class, $method)};
}
} # END for my $from_class (@from_classes)
} # END if ($via_class)
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";
}
#$self->debug_message("B: $param_class '$arg' " . scalar(@results));
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 || '';
# Validate $str, if possible, to prevent warnings from database if $str does not fit column type.
if ($id_data_type->isa('UR::Value::Number')) { # Oracle's Number data type includes floats but we just use integers for numeric IDs
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);
# Allow an environment variable to be set to disable the require_user_verify attribute
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");
# TODO: Replace this with lister?
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};
}
}
#$self->debug_message("Indices: " . join(',', sort(keys %indices)));
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;