App-Cme

 view release on metacpan or  search on metacpan

lib/App/Cme/Command/run.pm  view on Meta::CPAN

use base qw/App::Cme::Common/;
use feature qw/postderef signatures/;
no warnings qw/experimental::postderef experimental::signatures/;

my $__test_home = '';
# used only by tests
## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
sub _set_test_home { $__test_home = shift; return;}

my $home = $__test_home || File::HomeDir->my_home;

my @script_paths = map {path($_)} (
    "$home/.cme/scripts",
    "/etc/cme/scripts/",
);

push @script_paths, path($INC{"Config/Model.pm"})->parent->child("Model/scripts") ;

sub opt_spec {
    my ( $class, $app ) = @_;
    return ( 
        [ "arg=s@"  => "script argument. run 'cme run <script> -doc' for possible arguments" ],
        [ "backup:s"  => "Create a backup of configuration files before saving." ],
        [ "foreach=s" => "Run script in several directories. The list of directories "
          . "must be passed as a single argument, i.e. --foreach 'foo bar'. If this "
          . "argument is '-', the list is taken from STDIN."],
        [ "commit|c:s" => "commit change with passed message" ],
        [ "cat" => "Show the script file" ],
        [ "no-commit|nc!" => "skip commit to git" ],
        [ "doc!"    => "show documention of script" ],
        [ "list!"   => "list available scripts" ],
        $class->cme_global_options,
    );
}

sub validate_args {
    my ($self, $opt, $args) = @_;

    $self->check_unknown_args($args);
    return;
}

sub usage_desc {
  my ($self) = @_;
  my $desc = $self->SUPER::usage_desc; # "%c COMMAND %o"
  return "$desc [ script ] [ -args foo=12 [ -args bar=13 ]";
}

sub description {
    my ($self) = @_;
    return $self->get_documentation;
}

sub check_script_arguments ($self, $opt, $script_name) {
    if ($opt->{list} or not $script_name) {
        my @scripts;
        foreach my $path ( @script_paths ) {
            next unless $path->is_dir;
            push @scripts, map {$_->basename} grep { ! /~$/ } $path->children();
        }
        say $opt->{list} ? "Available scripts:" : "Missing script argument. Choose one of:";
        foreach my $script_path (sort @scripts) {
            my ($file, $data) = $self->get_script_data($script_path);
            my $app_info = $data->{app} ? sprintf(" (app %s)", $data->{app}) : "";
            printf("- %s%s\n",$script_path, $app_info );
        }
        say "";
        say "Run 'cme run <script> -doc' to get more details on a script.";
        return 0;
    }
    return 1;
}

sub find_script_file ($self, $script_name) {
    my $script;
    if ($script_name =~ m!/!) {
        $script = path($script_name);
    }
    else {
        # check script in known locations
        foreach my $path ( @script_paths ) {
            next unless $path->is_dir;
            $script = $path->child($script_name);
            last if $script->is_file;
        }
    }

    die "Error: cannot find script $script_name\n" unless $script->is_file;

    return $script;
}

## no critic (Subroutines::ProhibitManyArgs)
sub replace_var_in_value ($user_args, $script_var, $data, $value) {
    state $var_pattern = qr~(?<!\\) \$([a-zA-Z]\w+) (?!\s*{)~x;

    # change $var but not \$var, not $var{} and not $1
    $value =~ s~ $var_pattern
               ~ $user_args->{$1} // $script_var->{$1} // $ENV{$1} // $data->{default}{$1} // '$'.$1 ~xeg;

    # register vars without replacements
    foreach my $var ($value =~ m~ $var_pattern ~xg) {
        $data->{missing}{$var} = 1 ;
    }

    # now change \$var in $var
    $value =~ s!\\\$!\$!g;

    return $value;
}

# replace variables with command arguments or eval'ed variables or env variables
## no critic (Subroutines::ProhibitManyArgs)
sub replace_vars ($user_args, $script_var, $data, @items) {
    foreach my $item (@items) {
        if (ref $data->{$item}  eq 'ARRAY') {
            my @new;
            foreach my $value ($data->{$item}->@*) {
                push @new, replace_var_in_value ($user_args, $script_var, $data, $value);
            }
            $data->{$item} = \@new;
        }
        elsif ($data->{$item}) {
            $data->{$item} = replace_var_in_value ($user_args, $script_var, $data, $data->{$item});
        }
    }
    return;
}

lib/App/Cme/Command/run.pm  view on Meta::CPAN

    # replace strings like "{{ config_load_path }}" in $msg
    $msg =~ s/\{\{(.*?)\}\}/$root->grab_value($1)/e;

    # replace strings like "$value" in $msg
    $msg =~ s/\$(\w+)/$values->{$1}/eg;

    return $msg;
}

# returns: script file name, script data if script is *not* Perl code
sub get_script_data ($self, $script_name, $opt = {}) {
    my $script_file = $self->find_script_file($script_name);

    my $content = $script_file->slurp_utf8;

    if ($content =~ m/^#!/ and -x $script_file) {
        my ($app) = $content =~ /# app:\s?(.*?)\n/;
        my @doc = grep {s/^##\s?//;} split /\n/, $content;
        return $script_file, {doc => \@doc, app => $app};
    }

    # parse variables passed on command line
    my %user_args = map { split '=',$_,2; } @{ $opt->{arg} };

    return ($script_file, parse_script($script_file, $content, \%user_args));
}

sub run_script_as_code ($self, $script_name, $script_file) {
    splice @ARGV, 0,2;          # remove 'run script' arguments
    my $done = eval $script_file->slurp_utf8."\n1;\n"; ## no critic (BuiltinFunctions::ProhibitStringyEval)
    if (ref $done eq 'HASH') {
        warn "script $script_name returns a hash but it's processed as a plain script.",
            " This may not be what you want\n";
    }
    die "Error in script $script_name: $@\n" unless $done;
    return;
}

sub execute {
    my ($self, $opt, $app_args) = @_;

    # cannot use logger until Config::Model is initialised

    # see Debian #839593 and perlunicook(1) section X 13
    @$app_args = map { decode_utf8($_, 1) } @$app_args;

    my $script_name = shift @$app_args;

    return unless $self->check_script_arguments($opt, $script_name);

    my ($script_file, $script_data) = $self->get_script_data($script_name, $opt);

    if ($opt->{cat}) {
        print $script_file->slurp_utf8;
        return;
    }

    my $commit_msg = $script_data->{commit_msg};

    if ($opt->doc) {
        say join "\n", $script_data->{doc}->@*;
        say "will commit with message: '$commit_msg'" if $commit_msg;
        return;
    }

    if (not defined $script_data->{app}) {
        $self->run_script_as_code ($script_name, $script_file);
        return;
    }

    if (my @missing = sort keys $script_data->{missing}->%*) {
        die "Error: Missing variables '". join("', '",@missing)
            ."' in command arguments for script $script_file\n"
            ."Please use option '".join(' ', map { "-arg $_=xxx"} @missing)."'\n";
    }

    $self->process_args($opt, [ $script_data->{app}, $app_args->@* ]);

    # override commit message. may also trigger a commit even if none
    # is specified in script
    if ($opt->{commit}) {
        $commit_msg = $opt->{commit};
    }

    $opt->{_verbose} = 'Loader' if $opt->{verbose};

    my ( $categories, $appli_info, $appli_map ) = Config::Model::Lister::available_models;
    my $app = $script_data->{app};
    if ($opt->{foreach} and $appli_info->{$app}{_category} ne 'application') {
        die "Cannot use --foreach option with $app. This option can only be used with ".
            join(' ', $categories->{application}->@*). ". Check your cme script.\n";
    }

    # parse variables passed on command line

    if ($opt->{foreach}) {
        $self->run_foreach_loop($opt,$app_args, $script_data);
    }
    else {
        my %user_args = map { split '=',$_,2; } @{ $opt->{arg} };
        $self->run_script ($opt, $app_args, $script_data, \%user_args);
    }

    return;
}

sub run_foreach_loop($self, $opt,$app_args, $script_data ) {
    my %user_args = map { split '=',$_,2; } @{ $opt->{arg} };

    ## no critic (BuiltinFunctions::ProhibitComplexMappings)
    my @dirs = map { chomp; split /\s+/; }
        ($opt->{foreach} eq '-' ? <STDIN> : ($opt->{foreach}));

    my $start = path('.')->absolute;

    foreach my $d (@dirs) {
        my $t_dir = $start->child($d);
        if (not $t_dir->is_dir) {
            die "Cannot run script in $d: not a directory\n";
        }
        say "Running script in $t_dir ...";
        # instance is stored in Config::Model, so the name must be changed
        $opt->{instance_name} = $d;
        # instance is persisted in $self, so its ref must be removed
        delete $self->{_instance};
        chdir $t_dir->stringify;
        $self->run_script ($opt, $app_args, $script_data, {%user_args});
        # once we're done, remove instance from Model to avoid memory leaks
        $self->{_model}->delete_instance($d);
    }

    chdir $start->stringify;
    return;
}

sub run_script ($self, $opt, $app_args, $script_data, $user_args){
    my $commit_msg = $script_data->{commit_msg};
    my $stashed;

    # stash pending work
    if ($commit_msg and not $opt->{no_commit}) {
        $stashed = $self->autostash;
    }

    # call loads
    my ($model, $inst, $root) = $self->init_cme($opt,$app_args);
    foreach my $load_str ($script_data->{load}->@*) {
        $root->load($load_str);
    }

    if ($script_data->{code}) {
        my $to_run = '';
        while (my ($name, $value) = each $script_data->{values}->%*) {
            $to_run .= "my \$$name = '$value';\n";
        }
        $to_run .= join("\n",$script_data->{code}->@*);
        my $res = eval($to_run); ## no critic (ProhibitStringyEval)
        die "Error in code specification: $@\ncode is: \n$to_run\n" if $@;
    }

    if ($script_data->{sub}) {
        $script_data->{sub}->($root, $user_args);
    }

    if ($inst->needs_save) {
        $self->save($inst,$opt) ;

        # commit if needed
        if ($commit_msg and not $opt->{no_commit}) {
            my $processed_msg = $self->process_commit_message(
                $root, $script_data->{values}, $commit_msg
            );
            $self->commit($processed_msg);
        }
    } else {
        say "No change were applied";
    }


    if ($stashed) {
        $self->pop_stash;
    }

    return;
}

package App::Cme::Run::Var; ## no critic (Modules::ProhibitMultiplePackages)
$App::Cme::Run::Var::VERSION = '1.049';
require Tie::Hash;

## no critic (ClassHierarchies::ProhibitExplicitISA)
our @ISA = qw(Tie::ExtraHash);

sub FETCH {
    my ($self, $key) = @_ ;
    my ($h, $missing, $default) = @$self;
    my $res = $h->{$key} // $default->{$key} ;
    $missing->{$key} = 1 unless defined $res;
    return $res // '';
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

App::Cme::Command::run - Run a cme script

=head1 VERSION

version 1.049

=head1 SYNOPSIS

 $ cat ~/.cme/scripts/remove-mia
 doc: remove mia from Uploaders. Require mia parameter
 # declare app to configure
 app: dpkg
 # specify one or more instructions
 load: ! control source Uploaders:-~/$mia$/
 # commit the modifications with a message (git only)
 commit: remove MIA dev $mia

 $ cme run remove-mia -arg mia=longgone@d3bian.org

 # cme run can also use environment variables
 $ cat ~/.cme/scripts/add-me-to-uploaders
 app: dpkg-control
 load: source Uploaders:.push("$DEBFULLNAME <$DEBEMAIL>")

 $ cme run add-me-to-uploaders



( run in 2.016 seconds using v1.01-cache-2.11-cpan-302cb4679cc )