App-plx/lib/App/plx.pm
#!perl
package App::plx;
our $VERSION = '0.902003'; # 0.902.3
$VERSION = eval $VERSION;
use strict;
use warnings;
use File::Spec;
use File::Basename ();
use Cwd ();
use lib ();
use Config;
use File::Which ();
use List::Util ();
use local::lib ();
BEGIN {
our %orig_env = %ENV;
{ local $0 = $0 eq '-' ? 'plx' : $0;
local::lib->import('--deactivate-all') }
delete @ENV{grep /^PERL/, keys %ENV}
}
no lib @Config{qw(sitearch sitelibexp)};
my $fs = 'File::Spec';
my $self = do {
package Perl::Layout::Executor::_self;
sub self { package DB; () = caller(2); $DB::args[0] }
use overload '%{}' => sub { self }, fallback => 1;
sub AUTOLOAD {
my ($meth) = (our $AUTOLOAD =~ /([^:]+)$/);
self->$meth(@_[1..$#_]);
}
sub DESTROY {}
bless([], __PACKAGE__);
};
sub barf { die "$_[0]\n" }
sub stderr { warn "$_[0]\n" }
sub say { print "$_[0]\n" }
sub new {
my $class = shift;
bless @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {}, ref $class || $class;
}
sub layout_base_dir {
$self->{layout_base_dir} //= $self->_build_layout_base_dir
}
sub layout_perl {
$self->{layout_perl} //= $self->_build_layout_perl
}
sub _build_layout_base_dir {
my @parts = $fs->splitdir(Cwd::realpath(Cwd::getcwd()));
my $cand;
my $reason = '';
while (@parts > 1) { # go back to one step before root at most
$cand = $fs->catdir(@parts);
return $cand if -d $fs->catdir($cand, '.plx');
if (-d $fs->catdir($cand, '.git')) { # don't escape current repository
$reason = ' due to .git directory';
last;
}
pop @parts;
}
barf "Couldn't find .plx directory (stopped searching at ${cand}${reason})";
}
sub _build_layout_perl {
my $perl_bin = $self->read_config_entry('perl');
unless ($perl_bin) {
my $perl_spec = $self->read_config_entry('perl.spec');
barf "No perl and no perl.spec in config" unless $perl_spec;
$self->run_config_perl_set($perl_spec);
$perl_bin = $self->read_config_entry('perl');
barf "Rehydration of perl from perl.spec failed" unless $perl_bin;
}
barf "perl binary ${perl_bin} not executable" unless -x $perl_bin;
return $perl_bin;
}
sub layout_libspec_config {
[ grep $_->[1],
map [ $_, $self->read_config_entry([ libspec => $_ ]) ],
$self->list_config_names('libspec') ];
}
sub layout_lib_specs {
my $base_dir = $self->layout_base_dir;
local *_ = sub { Cwd::realpath($fs->rel2abs(shift, $base_dir)) };
[ map [ ($_->[0] =~ /\.([^.]+)$/), _($_->[1]) ],
@{$self->layout_libspec_config} ];
}
sub layout_file {
my ($self, @path) = @_;
$fs->catfile($self->layout_base_dir, @path);
}
sub layout_dir {
my ($self, @path) = @_;
$fs->catdir($self->layout_base_dir, @path);
}
sub ensure_layout_config_dir {
barf ".plx directory does not exist"
unless -d $self->layout_dir('.plx');
my $format = $self->read_config_entry('format');
barf ".plx directory has no format specifier" unless $format;
barf ".plx format ${format} unknown" unless $format eq '1';
}
sub layout_config_file { shift->layout_file('.plx', @_) }
sub layout_config_dir { shift->layout_dir('.plx', @_) }
sub write_config_entry {
my ($self, $path, $value) = @_;
my $file = $self->layout_config_file(ref($path) ? @$path : $path);
open my $wfh, '>', $file or die "Couldn't open ${file}: $!";
print $wfh "${value}\n";
}
sub clear_config_entry {
my ($self, $path) = @_;
my $file = $self->layout_config_file(ref($path) ? @$path : $path);
unlink($file) or barf "Failed to unlink ${file}: $!" if -e $file;
}
sub read_config_entry {
my ($self, $path) = @_;
my $file = $self->layout_config_file(ref($path) ? @$path : $path);
return undef unless -f $file;
open my $rfh, '<', $file or die "Couldn't open ${file}: $!";
chomp(my $value = <$rfh>);
return $value;
}
sub list_config_names {
my ($self, $path) = @_;
my $dir = $self->layout_config_dir(ref($path) ? @$path : $path);
return () unless -d $dir;
opendir my($dh), $dir or die "Couldn't opendir ${dir}: $!";
return grep -f $fs->catfile($dir, $_), sort readdir($dh);
}
sub slurp_command {
my ($self, @cmd) = @_;
open my $slurp_fh, '-|', @cmd
or barf "Failed to start command (".join(' ', @cmd)."): $!";
chomp(my @slurp = <$slurp_fh>);
return @slurp;
}
sub prepend_env {
my ($self, $env, @parts) = @_;
$ENV{$env} = join(':', @parts, $ENV{$env}||());
}
sub setup_env_for_ll {
my ($self, $path) = @_;
local $0 = $0 eq '-' ? 'plx' : $0;
local::lib->import($path);
}
sub setup_env_for_dir {
my ($self, $path) = @_;
$self->prepend_env(PERL5LIB => $path);
}
sub setup_env {
$ENV{PERL_PLX_BASE} = $self->layout_base_dir;
my ($site_libs) = $self->slurp_command(
$self->layout_perl, '-MConfig', '-e',
'print join(",", @Config{qw(sitearch sitelibexp)})'
);
$ENV{PERL5OPT} = '-M-lib='.$site_libs;
$ENV{$_} = $self->read_config_entry([ env => $_ ])
for $self->list_config_names('env');
my $perl_dirname = File::Basename::dirname($self->layout_perl);
our %orig_env;
unless (grep $_ eq $perl_dirname, split ':', $orig_env{PATH}) {
$self->prepend_env(PATH => $perl_dirname);
}
foreach my $lib_spec (@{$self->layout_lib_specs}) {
my ($type, $path) = @$lib_spec;
next unless $path and -d $path;
$self->${\"setup_env_for_${type}"}($path);
}
return;
}
sub cmd_search_path { qw(.plx/cmd dev bin) }
sub run_action_commands {
my ($self, $filter) = @_;
$self->ensure_layout_config_dir;
my @commands;
my %seen;
foreach my $dirname ($self->cmd_search_path) {
next unless -d (my $dir = $self->layout_dir($dirname));
opendir my ($dh), $dir or barf "Couldn't open ${dir}: $!";
foreach my $entry (sort readdir($dh)) {
next if $entry =~ /^\.+$/;
my $file = $self->layout_file($dirname, $entry);
next unless -f $file;
unless ($seen{$entry}++) {
push @commands, [ $entry, "${dirname}/${entry}" ];
}
}
}
my $path = do { local $ENV{PATH} = ''; $self->setup_env; $ENV{PATH} };
foreach my $dir (split ':', $path) {
opendir my ($dh), $dir;
foreach my $entry (sort readdir($dh)) {
next if $entry =~ /^\.+$/;
my $file = $fs->catfile($dir, $entry);
next unless -x $file;
push @commands, [ $entry, $file ] unless $seen{$entry}++;
}
}
if ($filter) {
my $match = $filter =~ m{^/(.+)/$} ? $1 : qr/^\Q${filter}/;
@commands = grep { $_->[0] =~ $match } @commands;
}
my $max = List::Util::max(map length($_->[0]), @commands);
my $base = $self->layout_base_dir;
my $home = $ENV{HOME};
foreach my $command (@commands) {
my ($name, $path) = @$command;
$path =~ s/^\Q${base}\///;
$path =~ s/^\Q${home}/~/ if $home;
say sprintf("%-${max}s %s", $name, $path);
}
}
sub run_action_bareinit {
my ($self, $perl) = @_;
my $dir = $fs->catdir($self->{layout_base_dir}||Cwd::getcwd(), '.plx');
if (-d $dir) {
if ($perl) {
stderr <<END;
.plx already initialised - if you wanted to set the perl to ${perl} run:
plx --config perl set ${perl}
END
}
return;
}
mkdir($dir) or barf "Couldn't create ${dir}: $!";
$self->run_config_perl_set($perl||'perl');
$self->write_config_entry(format => 1);
}
sub run_action_userinit {
my ($self, @args) = @_;
my @perl = (
(@args and !ref($args[0]) and $args[0] ne '[')
? shift(@args)
: ()
);
barf "--userinit requires \$HOME to be set" unless $ENV{HOME};
$self->run_action_base(
$ENV{HOME},
'--multi' =>
[ '--bareinit', @perl ],
[ qw(--config libspec add 25.perl5.ll perl5) ],
(@args ? [ '--multi', @args ] : ()),
);
}
sub run_action_userstrap {
my ($self, @args) = @_;
my @perl = (
(@args and !ref($args[0]) and $args[0] ne '[')
? shift(@args)
: ()
);
$self->run_action_userinit(
@perl,
[ '--installself' ],
[ '--installenv' ],
@args
);
}
sub run_action_installself {
my $last_ll;
foreach my $lib_spec (@{$self->layout_lib_specs}) {
my ($type, $path) = @$lib_spec;
$last_ll = $path if $type eq 'll';
}
barf "No local::lib in libspec config" unless $last_ll;
$self->run_action_cpanm(
"-l${last_ll}", '-n',
qw(App::cpanminus App::plx)
);
}
sub run_action_installenv {
$self->ensure_layout_config_dir;
barf "--installenv action currently assumes bash"
unless $ENV{SHELL} =~ /bash/;
barf "Couldn't find .bashrc"
unless -f (my $bashrc = $fs->catfile($ENV{HOME}, ".bashrc"));
my $plx_bin = do {
local %ENV = our %orig_env;
File::Which::which('plx-packed');
} || do {
local %ENV = %ENV;
$self->setup_env;
File::Which::which('plx-packed');
};
barf "Couldn't find plx in PATH" unless $plx_bin;
{
open my $fh, '<', $bashrc or die "Couldn't open ${bashrc} to read: $!";
if (my ($line) = grep /plx-packed/, <$fh>) {
chomp($line);
stderr("Found line in .bashrc: $line");
return;
}
}
my $base = $self->layout_base_dir;
stderr("Appending to .bashrc");
open my $fh, '>>', $bashrc or die "Couldn't open ${bashrc} to append: $!";
print $fh "\neval \$(${plx_bin} --base ${base} --env)\n";
}
sub run_action_init {
my ($self, $perl) = @_;
$self->run_action_bareinit($perl);
my $libspec_dir = $self->layout_config_dir('libspec');
mkdir($libspec_dir) or barf "Couldn't create ${libspec_dir}: $!";
$self->run_config_libspec_add(@$_) for (
[ '25-local.ll' => 'local' ],
[ '50-devel.ll' => 'devel' ],
[ '75-lib.dir' => 'lib' ],
);
}
sub _which {
my ($self, @args) = @_;
$self->ensure_layout_config_dir;
my @env;
push @env, shift @args while $args[0] =~ /^\w+=/;
my $cmd = shift @args;
barf "--cmd <cmd> <args>" unless $cmd;
if ($fs->file_name_is_absolute($cmd)) {
return (exec => @env => $cmd => @args);
}
if ($cmd eq 'perl') {
return (perl => @env => @args);
}
if ($cmd =~ m{/}) {
return (perl => @env => $cmd, @args);
}
if ($cmd =~ /^-/) {
my @optargs = ($cmd, @args);
foreach my $optarg (@optargs) {
next if $optarg =~ /^-/;
foreach my $dirname ($self->cmd_search_path) {
if (-f (my $file = $self->layout_file($dirname => $optarg))) {
$optarg = $file;
last;
}
}
last;
}
return (perl => @env => @optargs);
}
foreach my $dirname ($self->cmd_search_path) {
if (-f (my $file = $self->layout_file($dirname => $cmd))) {
return (perl => @env => $file, @args);
}
}
return (exec => @env => $cmd, @args);
}
sub run_action_which {
my ($self, @args) = @_;
my ($action, @call) = $self->_which(@args);
say join(' ', 'plx', "--${action}", @call);
}
sub run_action_cmd {
my ($self, @args) = @_;
my ($action, @call) = $self->_which(@args);
$self->${\"run_action_${action}"}(@call);
}
sub run_action_perl {
my ($self, @call) = @_;
$self->ensure_layout_config_dir;
return $self->show_config_perl unless @call;
my @env;
push @env, shift @call while $call[0] =~ /^\w+=/;
$self->run_action_exec(@env, $self->layout_perl, @call);
}
sub run_action_exec {
my ($self, @exec) = @_;
$self->ensure_layout_config_dir;
$self->setup_env;
shift @exec and $ENV{$1} = $2 while $exec[0] =~ /^(\w+)=(.*)$/;
exec(@exec) or barf "exec of (".join(' ', @exec).") failed: $!";
}
sub find_cpanm {
local %ENV = our %orig_env;
barf "Couldn't find cpanm in \$PATH"
unless my $cpanm = File::Which::which('cpanm');
$cpanm;
}
sub run_action_cpanm {
my ($self, @args) = @_;
$self->ensure_layout_config_dir;
my @cpanm = $self->find_cpanm;
unless (@args and $args[0] =~ /^-[lL]/) {
barf "--cpanm args must start with -l or -L to specify target local::lib";
}
$self->setup_env;
system($self->layout_perl, @cpanm, @args);
}
sub run_action_config {
my ($self, $config, @args) = @_;
$self->ensure_layout_config_dir;
unless ($config) {
say "# perl";
$self->show_config_perl;
say "# libspec";
$self->show_config_libspec;
if ($self->list_config_names('env')) {
say "# env";
$self->show_config_env;
}
return;
}
barf "Unknown config key ${config}"
unless my $show = $self->can("show_config_${config}");
return $self->$show unless @args;
if (my $code = $self->can("run_config_${config}")) {
return $self->$code(@args);
}
my ($subcmd, @rest) = @args;
barf "Invalid subcommand ${subcmd} for config key ${config}"
unless my $code = $self->can("run_config_${config}_${subcmd}");
return $self->$code(@rest);
}
sub show_config_perl { say $self->layout_perl }
sub resolve_perl_via_perlbrew {
my ($self, $perl) = @_;
stderr "Resolving perl '${perl}' via perlbrew";
local %ENV = our %orig_env;
barf "Couldn't find perlbrew in \$PATH"
unless my $perlbrew = File::Which::which('perlbrew');
my @list = $self->slurp_command($perlbrew, 'list');
barf join(
"\n", "No such perlbrew perl '${perl}', choose from:\n", @list, ''
) unless grep $_ eq $perl, map /(\S+)/, @list;
my ($perl_path) = $self->slurp_command(
$perlbrew, qw(exec --with), $perl, qw(perl -e), 'print $^X'
);
return $perl_path;
}
sub run_config_perl_set {
my ($self, $new_perl) = @_;
barf "plx --config perl set <perl>" unless $new_perl;
my $perl_spec = $new_perl;
unless ($new_perl =~ m{/}) {
$new_perl = "perl${new_perl}" if $new_perl =~ /^5/;
$new_perl =~ s/perl-5/perl5/; # perlbrew name to perl binary
require File::Which;
stderr "Resolving perl '${new_perl}' via PATH";
if (my $resolved = File::Which::which($new_perl)) {
$new_perl = $resolved;
} else {
$new_perl =~ s/^perl5/perl-5/; # perl binary to perlbrew name
$new_perl = $self->resolve_perl_via_perlbrew($new_perl);
}
}
barf "Not executable: $new_perl" unless -x $new_perl;
$self->write_config_entry('perl.spec' => $perl_spec);
$self->write_config_entry(perl => $new_perl);
}
sub show_config_libspec {
my @ent = @{$self->layout_libspec_config};
my $max = List::Util::max(map length($_->[0]), @ent);
say sprintf("%-${max}s %s", @$_) for @ent;
}
sub run_named_config_add {
my ($self, $type, $name, $value) = @_;
barf "plx --config ${type} add <name> <value>"
unless $name and defined($value);
unless (-d (my $dir = $self->layout_config_dir($type))) {
mkdir($dir) or die "Couldn't make config dir ${dir}: $!";
}
$self->write_config_entry([ $type => $name ], $value);
}
sub run_named_config_del {
my ($self, $type, $name) = @_;
barf "plx --config ${type} dev <name>" unless $name;
$self->clear_config_entry([ $type => $name ]);
}
sub run_config_libspec_add { shift->run_named_config_add(libspec => @_) }
sub run_config_libspec_del { shift->run_named_config_del(libspec => @_) }
sub show_config_env {
my $max = List::Util::max(
map length, my @names = $self->list_config_names('env')
);
say sprintf("%-${max}s %s", $_, $self->read_config_entry([ env => $_ ]))
for @names;
}
sub run_config_env_add { shift->run_named_config_add(env => @_) }
sub run_config_env_del { shift->run_named_config_del(env => @_) }
sub show_env {
my ($self, $env) = @_;
$self->ensure_layout_config_dir;
local $ENV{$env} = '';
$self->setup_env;
say $_ for split ':', $ENV{$env};
}
sub run_action_libs { $self->show_env('PERL5LIB') }
sub run_action_paths { $self->show_env('PATH') }
sub run_action_env {
$self->ensure_layout_config_dir;
$self->setup_env;
my @env_change;
our %orig_env;
foreach my $key (sort(keys %{{ %orig_env, %ENV }})) {
my ($oval, $eval) = ($orig_env{$key}, $ENV{$key});
if (!defined($eval) or ($oval//'') ne $eval) {
push @env_change, [ $key, $eval ];
}
}
my $shelltype = local::lib->guess_shelltype;
my $shellbuild = "build_${shelltype}_env_declaration";
foreach my $change (@env_change) {
print +local::lib->$shellbuild(@$change);
}
}
sub run_action_help {
require Pod::Usage;
Pod::Usage::pod2usage();
}
sub run_action_version {
say sprintf "%f", $VERSION;
}
sub run_action_base {
my ($self, $base, @chain) = @_;
unless ($base) {
say $self->layout_base_dir;
return;
}
barf "--base <base> <action> <args>" unless @chain;
$self->new({ layout_base_dir => $base })->run(@chain);
}
sub _parse_multi {
my ($self, @args) = @_;
my @multi;
MULTI: while (@args) {
barf "Expected multi arg [, got: $args[0]" unless $args[0] eq '[';
shift @args;
my @action;
while (my $el = shift @args) {
push @multi, \@action and next MULTI if $el eq ']';
push @action, $el;
}
barf "Missing closing ] for multi";
}
return @multi;
}
sub run_action_multi {
my ($self, @args) = @_;
return $self->run_multi(@args) if @args and ref($args[0]);
my @multi = $self->_parse_multi(@args);
$self->run_multi(@multi);
}
sub run_multi {
my ($self, @multi) = @_;
foreach my $multi (@multi) {
my @debug_multi = map +(ref($_) ? ('[', @$_, ']') : $_), @$multi;
stderr '# '.join(' ', plx => @debug_multi);
$self->run(@$multi);
}
}
sub run_action_showmulti {
my ($self, @args) = @_;
my @multi = $self->_parse_multi(@args);
say join(' ', plx => @$_) for @multi;
}
sub run {
my ($self, $cmd, @args) = @_;
$cmd ||= '--help';
if ($cmd eq '[') {
return $self->run_action_multi($cmd, @args);
}
if ($cmd =~ s/^--//) {
if ($cmd) {
my $method = join('_', 'run_action', split '-', $cmd);
if (my $code = $self->can($method)) {
return $self->$code(@args);
}
barf "No such action --${cmd}, see 'perldoc plx' for the full list";
}
$cmd = shift @args;
}
$self->ensure_layout_config_dir;
return $self->run_action_cmd($cmd, @args);
}
caller() ? 1 : __PACKAGE__->new->run(@ARGV);
=head1 NAME
App::plx - Perl Layout Executor
=head1 SYNOPSIS
plx --help # This output
plx --init <perl> # Initialize layout config
plx --perl # Show layout perl binary
plx --libs # Show layout $PERL5LIB entries
plx --paths # Show layout additional $PATH entries
plx --env # Show layout env var changes
plx --cpanm -llocal --installdeps . # Run cpanm from outside $PATH
plx perl <args> # Run perl within layout
plx -E '...' # (ditto)
plx script-in-dev <args> # Run dev/ script within layout
plx script-in-bin <args> # Run bin/ script within layout
plx ./script <args> # Run script within layout
plx script/in/cwd <args> # (ditto)
plx program <args> # Run program from layout $PATH
=head1 WHY PLX
While perl has many tools for configuring per-project development
environments, using them can still be a little on the lumpy side. With
L<Carton>, you find yourself running one of
perl -Ilocal/lib/perl -Ilib bin/myapp
carton exec perl -Ilib bin/myapp
With L<App::perlbrew>,
perlbrew switch perl-5.28.0@libname
perl -Ilib bin/myapp
With L<https://github.com/tokuhirom/plenv>,
plenv exec perl -Ilib bin/myapp
and if you have more than one distinct layer of dependencies, while
L<local::lib> will happily handle that, integrating it with everything else
becomes a pain in the buttocks.
As a result of this, your not-so-humble author found himself regularly having
a miniature perl executor script at the root of git clones that looked
something like:
#!/bin/sh
eval $(perl -Mlocal::lib=--deactivate-all)
export PERL5LIB=$PWD/local/lib/perl5
bin=$1
shift
~/perl5/perlbrew/perls/perl-5.28.0/bin/$bin "$@"
and then running:
./pl perl -Ilib bin/myapp
However, much like back in 2007 frustration with explaining to other
developers how to set up L<CPAN> to install into C<~/perl5> and how to
set up one's environment variables to then find the modules so installed
led to the exercise in rage driven development that first created
L<local::lib>, walking newbies through the creation and subsequent use of
such a script was not the most enjoyable experience for anybody involved.
Thus, the creation of this module to reduce the setup process to:
cpanm App::plx
cd MyProject
plx --init 5.28.0
plx --cpanm -llocal --notest --installdeps .
Follwed by being able to immediately (and even more concisely) run:
plx myapp
which will execute C<perl -Ilib bin/myapp> with the correct C<perl> and the
relevant L<local::lib> already in scope.
If this seems of use to you, the L</QUICKSTART> is next and the L</ACTIONS>
section of this document lists the full capabilities of plx. Onwards!
=head1 QUICKSTART
Let's assume we're going to be working on Foo-Bar, so we start with:
git clone git@github.com:arthur-nonymous/Foo-Bar.git
cd Foo-Bar
Assuming the perl we'd get from running just C<perl> suffices, then we
next run:
plx --init
If we want a different perl - say, we have a C<perl5.30.1> in our path, or
a C<perl-5.30.1> built in perlbrew, we'd instead run:
plx --init 5.30.1
To quickly get our dependencies available, we then run:
plx --cpanm -llocal --notest --installdeps .
If the project is designed to use L<Carton> and has a C<cpanfile.snapshot>,
instead we would run:
plx --cpanm -ldevel --notest Carton
plx carton install
If the goal is to test this against our current development version of another
library, then we'd also want to run:
plx --config libspec add 40otherlib.dir ../Other-Lib/lib
If we want our ~/perl L<local::lib> available within the plx environment, we
can add that as the least significant libspec with:
plx --config libspec add 00tilde.ll $HOME/perl5
At which point, we're ready to go, and can run:
plx myapp # to run bin/myapp
plx t/foo.t # to run one test file
plx prove # to run all t/*.t test files
plx -E 'say for @INC' # to run a one liner within the layout
To learn everything else plx is capable of, read on to the L</ACTIONS> section
coming next.
Have fun!
=head1 BOOTSTRAP
Under normal circumstances, one would run something like:
cpanm App::plx
However, if you want a self-contained plx script without having a cpan
installer available, you can run:
mkdir bin
wget https://raw.githubusercontent.com/shadowcat-mst/plx/master/bin/plx-packed -O bin/plx
to get the current latest packed version.
The packed version bundles L<local::lib> and L<File::Which>, and also includes
a modified C<--cpanm> action that uses an inline C<App::cpanminus>.
=head1 ENVIRONMENT
C<plx> actions that execute external commands all clear any existing
environment variables that start with C<PERL> to keep an encapsulated setup
for commands being run within the layouts - and also set C<PERL5OPT> to
exclude C<site_perl> (but not C<vendor_perl>) to avoid locally installed
modules causing unexpected effects.
Having done so, C<plx> then loads each env config entry and sets those
variables - then prepends the C<plx> specific entries to both C<PATH> and
C<PERL5LIB>. You can add env config entries with L</--config>:
plx --config env add NAME VALUE
The changes that will be made to your environment can be output by calling
the L</--env> command.
Additionally, environment variable overrides may be provided to the
L</--cmd>, L</--exec> and L</--perl> commands by providing them in
C<NAME=VALUE> format:
# do not do this, it will be deleted
PERL_RL=Perl5 plx <something>
# do this instead, it will provide the environment variable to the command
plx PERL_RL=Perl5 <something>
=head1 ACTIONS
plx --help # Print synopsis
plx --version # Print plx version
plx --init <perl> # Initialize layout config for .
plx --bareinit <perl> # Initialize bare layout config for .
plx --base # Show layout base dir
plx --base <base> <action> <args> # Run action with specified base dir
plx --perl # Show layout perl binary
plx --libs # Show layout $PERL5LIB entries
plx --paths # Show layout additional $PATH entries
plx --env # Show layout env var changes
plx --cpanm -llocal --installdeps . # Run cpanm from outside $PATH
plx --config perl # Show perl binary
plx --config perl set /path/to/perl # Select exact perl binary
plx --config perl set perl-5.xx.y # Select perl via $PATH or perlbrew
plx --config libspec # Show lib specifications
plx --config libspec add <name> <path> # Add lib specification
plx --config libspec del <name> <path> # Delete lib specification
plx --config env # Show additional env vars
plx --config env add <name> <path> # Add env var
plx --config env del <name> <path> # Delete env var
plx --exec <cmd> <args> # exec()s with env vars set
plx --perl <args> # Run perl with args
plx --cmd <cmd> <args> # DWIM command:
cmd = perl -> --perl <args>
cmd = -<flag> -> --perl -<flag> <args>
cmd = some/file -> --perl some/file <args>
cmd = ./file -> --perl ./file <args>
cmd = name ->
exists .plx/cmd/<name> -> --perl .plx/cmd/<name> <args>
exists dev/<name> -> --perl dev/<name> <args>
exists bin/<name> -> --perl bin/<name> <args>
else -> --exec <name> <args>
plx --which <cmd> # Expands --cmd <cmd> without running
plx <something> <args> # Shorthand for plx --cmd
plx --commands <filter>? # List available commands
plx --multi [ <cmd1> <args1> ] [ ... ] # Run multiple actions
plx --showmulti [ ... ] [ ... ] # Show multiple action running
plx [ ... ] [ ... ] # Shorthand for plx --multi
plx --userinit <perl> # Init ~/.plx with ~/perl5 ll
plx --installself # Installs plx and cpanm into layout
plx --installenv # Appends plx --env call to .bashrc
plx --userstrap <perl> # userinit+installself+installenv
=head2 --help
Prints out the usage information (i.e. the L</SYNOPSIS>) for plx.
=head2 --init
plx --init # resolve 'perl' in $PATH
plx --init perl # (ditto)
plx --init 5.28.0 # looks for perl5.28.0 in $PATH
# or perl-5.28.0 in perlbrew
plx --init /path/to/some/perl # uses the absolute path directly
Initializes the layout.
If a perl name is passed, attempts to resolve it via C<$PATH> and C<perlbrew>
and sets the result as the layout perl; if not looks for just C<perl>.
Creates the following libspec config:
25-local.ll local
50-devel.ll devel
75-lib.dir lib
=head2 --bareinit
Identical to C<--init> but creates no default configs except for C<perl>.
=head2 --base
plx --base
plx --base <base> <action> <args>
Without arguments, shows the selected base dir - C<plx> finds this by
checking for a C<.plx> directory in the current directory, and if not tries
the parent directory, recursively. The search stops either when C<plx> finds
a C<.git> directory, to avoid accidentally escaping a project repository, or
at the last directory before the root - i.e. C<plx> will test C</home> but
not C</>.
With arguments, specifies a base dir to use, and then invokes the rest of the
arguments with that base dir selected - so for example one can make a default
configuration in C<$HOME> available as C<plh> by running:
plx --init $HOME
alias plh='plx --base $HOME'
=head2 --libs
Prints the directories that will be added to C<PERL5LIB>, one per line.
These will include the C<lib/perl5> subdirectory for each C<ll> entry in the
libspecs, and the directory for each C<dir> entry.
=head2 --paths
Prints the directories that will be added to C<PATH>, one per line.
These will include the containing directory of the environment's perl binary
if not already in C<PATH>, followed by the C<bin> directories of any C<ll>
entries in the libspecs.
=head2 --env
Prints the changes that will be made to your environment variables, in a
syntax that is (hopefully) correct for your current shell.
=head2 --cpanm
plx --cpanm -Llocal --installdeps .
plx --cpanm -ldevel App::Ack
Finds the C<cpanm> binary in the C<PATH> that C<plx> was executed I<from>,
and executes it using the layout's perl binary and environment variables.
Requires the user to specify a L<local::lib> to install into via C<-l> or
C<-L> in order to avoid installing modules into unexpected places.
Note that this action exists primarily for bootstrapping, and if you want
to use a different installer such as L<App::cpm>, you'd install it with:
plx --cpanm -ldevel App::cpm
and then subsequently run e.g.
plx cpm install App::Ack
to install modules.
=head2 --exec
plx --exec <command> <args>
Sets up the layout's environment variables and C<exec>s the command.
=head2 --perl
plx --perl
plx --perl <options> <script> <args>
Without arguments, sugar for C<--config perl>.
Otherwise, sets up the layout's environment variables and C<exec>s the
layout's perl with the given options and arguments.
=head2 --cmd
plx --cmd <cmd> <args>
cmd = perl -> --perl <args>
cmd = -<flag> -> --perl -<flag> <args>
cmd = some/file -> --perl some/file <args>
cmd = ./file -> --perl ./file <args>
cmd = name ->
exists .plx/cmd/<name> -> --perl .plx/cmd/<name> <args>
exists dev/<name> -> --perl dev/<name> <args>
exists bin/<name> -> --perl bin/<name> <args>
else -> --exec <name> <args>
B<Note>: Much like the C<devel> L<local::lib> is created to allow for the
installation of out-of-band dependencies that aren't going to be needed in
production, the C<dev> directory is supported to allow for the easy addition
of development time only sugar commands. Note that since C<perl> will re-exec
anything with a non-perl shebang, one can add wrappers here ala:
$ cat dev/prove
#!/bin/sh
exec prove -j8 "$@"
=head2 --which
plx --which <cmd>
Outputs the expanded form of a C<--cmd> invocation without running it.
=head2 --config
plx --config # Show current config
plx --config <name> # Show current <name> config
plx --config <name> <operation> # Invoke config operation
=head3 perl
plx --config perl
plx --config perl set <spec>
If the spec passed to C<set> contains a C</> character, plx assumes that it's
an absolute bath and records it as-is.
If not, we go a-hunting.
First, if the spec begins with a C<5>, we replace it with C<perl5>.
Second, we search C<$PATH> for a binary of that name, and record it if so.
Third, if the (current) spec begins C<perl5>, we replace it with C<perl-5>.
Fourth, we search C<$PATH> for a C<perlbrew> binary, and ask it if it has a
perl named after the spec, and record that if so.
Fifth, we shrug and hope the user can come up with an absolute path next time.
B<Note:> The original spec passed to C<set> is recorded in C<.plx/perl.spec>,
so if you intend to share the C<.plx> directory across multiple machines via
version control or otherwise, remove/exclude the C<.plx/perl> file and plx
will automatically attempt to re-locate the perl on first invocation.
=head3 libspec
plx --config libspec
plx --config libspec add <name> <spec>
plx --config libspec del <name> <spec>
A libspec config entry consists of a name and a spec, and the show output
prints them space separated one per line, with enough spaces to make the
specs align:
25-local.ll local
50-devel.ll devel
75-lib.dir lib
The part of the name before the last C<.> is not semantically significant to
plx, but is used for asciibetical sorting of the libspec entries to determine
in which order to apply them.
The part after must be either C<ll> for a L<local::lib>, or C<dir> for a bare
L<lib> directory.
When loaded, the spec is (if relative) resolved to an absolute path relative
to the layout root, then all C<..> entries and symlinks resolved to give a
final path used to set up the layout environment.
=head3 env
plx --config env
plx --config env add <name> <value>
plx --config env del <name> <value>
Manages additional environment variables, which are set immediately before
any environment changes required for the current L</libspec> and L</perl>
settings are processed.
=head2 --commands
plx --commands # all commands
plx --commands c # all commands starting with c
plx --commands /json/ # all commands matching /json/
Lists available commands, name first, then full path.
If a filter argument is given, treats it as a fixed prefix to filter the
command list, unless the filter is C</re/> in which case the slashes are
stripped and the filter is treated as a regexp.
=head2 --multi
plx --multi [ --init ] [ --config perl set 5.28.0 ]
Runs multiple plx commands from a single invocation delimited by C<[ ... ]>.
=head2 --showmulti
plx --showmulti [ --init ] [ --config perl set 5.28.0 ]
Outputs approximate plx invocations that would be run by C<--multi>.
=head2 --userinit
Same as C<--init> but assumes C<$HOME> as base and sets up only a single
libspec pointing at C<$HOME/perl5>.
=head2 --installself
Installs L<App::plx> and L<App::cpanminus> into the highest-numbered
L<local::lib> within the layout.
=head2 --installenv
(bash only currently)
Appends an eval line to set up the layout environment to the user's bashrc.
=head2 --userstrap
Convenience command for C<--userinit> plus C<--installself> plus
C<--installenv>.
=head1 AUTHOR
mst - Matt S. Trout (cpan:MSTROUT) <mst@shadowcat.co.uk>
=head1 CONTRIBUTORS
None yet - maybe this software is perfect! (ahahahahahahahahaha)
=head1 COPYRIGHT
Copyright (c) 2020 the App::plx L</AUTHOR> and L</CONTRIBUTORS>
as listed above.
=head1 LICENSE
This library is free software and may be distributed under the same terms
as perl itself.