Proc::Feed
Introduction
Proc::Feed
provides a couple wrappers for Proc::Async
that are
more convenient to use than the run
and shell
subs. Specifically, these
wrappers let you easily feed data to them and also from them to other
callables using the feed operator (==>
or <==
).
sub proc
sub proc(
\command where Str:D|List:D,
$input? is raw,
Bool :$check = False,
:$bin where 'IN' | {! .so} = False, # i.e., only :bin<IN> or :!bin
:$stderr where Any|Callable:D|Str:D|IO::Handle:D,
:$shell where Bool:D|Str:D = False,
:$cwd = $*CWD,
Hash() :$env = %*ENV,
:$scheduler = $*SCHEDULER
--> Proc:D
) is export(:DEFAULT, :proc);
Use the proc
sub to run a command when you don't need to capture its
output(STDOUT
):
proc <<ls -la "READ ME.txt">>;
Here we passed a list, of which the first element is the executable, while the rest are arguments passed to the executable.
NOTE:
proc
blocks until the external command is completed and returns aProc
object, which when sunk throws an exception if the command exited with a non-zero status.
It's also possible to redirect STDERR
if desired:
# Redirect STDERR to /dev/null
proc <ls whatever>, :stderr('/dev/null/');
# Same thing; but with a file we opened
my $black-hole = open('/dev/null', :w);
proc <ls whatever>, :stderr($black-hole);
$black-hole.close;
# Same effect that ignores error messages; this time, with a callable
# that just ignores the lines from STDERR.
proc <ls whatever>, :stderr({;});
Sometimes it can be convenient to run a command via a shell (defaults to
/bin/bash
) if you need to use the features (e.g., globbing, I/O redirection,
... etc) it provides:
proc(['ls -la ~/.*'], :shell);
One can also simply pass a single string (recommended when using :shell
)
instead of a list if not passing any arguments to the command from Perl 6:
proc('ls -la ~/.*', :shell);
However, if a list is passed, any remaining elements of the list are passed to
the shell as positional arguments, which will be available as $1
, $2
, ...,
and so on, in the shell command/script:
proc(['md5sum "$1" | cut -d" " -f1 > "$1.md5"', "$*PROGRAM"], :shell);
# ~/.* is not expanded here because it will be passed as an argument to bash.
proc(<<ls -la ~/.*>>, :shell); # runs: bash -c 'ls' - bash '-la' '~/.*'
# prints an empty line
proc(<<echo hello world>>, :shell)
# prints hello world
proc(<<'echo "$@"' hello world>>, :shell)
# prints the passed args; also shows you can specify the shell to use.
proc([q:to/EOF/, 'arg1', 'arg2'], :shell</usr/local/bin/bash>);
echo "Using SHELL - $0"
echo "First argument is: $1"
echo "Second argument is: $2"
EOF
sub capture
sub capture(
\command where Str:D|List:D,
$input? is raw,
Bool :$check = True,
Bool :$chomp = True,
Bool :$merge,
:$bin where 'IN' | {! .so} = False, # i.e., only :bin<IN> or :!bin
:$stderr where Any|Callable:D|Str:D|IO::Handle:D,
:$shell where Bool:D|Str:D = False,
Str:D :$enc = 'UTF-8',
Bool :$translate-nl = True,
:$cwd = $*CWD,
Hash() :$env = %*ENV,
:$scheduler = $*SCHEDULER
--> Str:D
) is export(:DEFAULT, :capture);
To capture the STDOUT
of a command use the capture
sub, which returns a Str:D
instead of a Proc
:
capture(<<md5sum "$*PROGRAM">>).split(' ')[0] ==> spurt("$*PROGRAM.md5");
# You can still run command via a shell
my $ls = capture('ls -la ~/.*', :shell);
my $err = capture(<<ls -la "doesn't exist">>, :merge);
#
# :merge redirects STDERR to STDOUT, so error messages are also
# captured in this case.
NOTE: By default, similar to command substitution in Bash, any trailing newline of the captured output is removed. You can disable this behavior by specifying
:!chomp
.
NOTE: If the command run by
capture
fails with a non-zero exit status, an exception will be thrown. You can disable this behavior with:!check
, and in which case, an empty string will be returned if the command fails.
You can easily feed iterable inputs to a capture
: (you can do the same with
proc
or pipe
too):
my $data = slurp("$*HOME/.bashrc");
my $chksum = ($data ==> capture('md5sum')).split(' ')[0];
By default, both input and output are assumed to be lines of strings, but you
can specify both to be binary with :bin
, or only one of which to be binary
with either :bin<IN>
or :bin<OUT>
:
NOTE: For
capture
andproc
, only:bin<IN>
(default) and:!bin
are possible.
# Binary (Blob) in; string (Str) out.
my $blob = Blob.new(slurp("/bin/bash", :bin));
my $chksum = ($blob ==> capture('md5sum', :bin<IN>)).split(' ')[0];
# String (Str) in; binary (Blob) out.
# See below for more details on using 'run' and 'pipe'.
run {
my $f = "$*HOME/.bashrc";
my $gzipped = open("$f.gz", :w:bin);
LEAVE $gzipped.close;
slurp($f) \
==> pipe(<gzip -c>, :bin<OUT>)
==> each { spurt $gizpped, :bin }
}
NOTE: It makes no sense to feed from a
proc
(i.e.,proc(…) ==> …
) becauseproc(…)
returns aProc
, which is not iterable.
sub pipe
sub pipe(
\command where Str:D|List:D,
$input? is raw,
Bool :$chomp is copy,
:$bin where {
$_ ~~ Bool:D|'IN'|'OUT' and (
# if :bin or :bin<OUT> then :chomp shouldn't be specified.
! ($_ eq 'OUT' || ($_ ~~ Bool && $_))
|| !$chomp
)
} = False,
:$stderr where Any|Callable:D|Str:D|IO::Handle:D,
Bool :$merge,
Str:D :$enc = 'UTF-8',
Bool :$translate-nl = True,
:$cwd = $*CWD,
Hash() :$env = %*ENV,
:$shell where Bool:D|Str:D = False,
:$scheduler = $*SCHEDULER
--> List:D
) is export(:DEFAULT, :pipe);
Finally, pipe
returns an iterable of the output from the STDOUT
of the
external process, so you can feed it to another callable. By default, text
output is assumed and the output is an iterable of lines. If binary output
is specified, then the output will be an iterable of Blob
's.
pipe
should be used within a Block
passed to the run
sub, and it should
not be used at the end of a pipeline where nothing consumes its output. The
run { ... }
returns the result returned by the run block. Examples:
# Example
my $chksum = run {
slurp("/bin/bash", :bin) \
==> pipe('gzip', :bin)
==> pipe('md5sum', :bin<IN>)
==> split(' ')
==> *.[0]()
}
# Example
run {
my $tarball = open("./somedir.tgz", :w);
LEAVE $tarball.close;
pipe «ssh my.remote-host.com "tar -czf - ./somedir"», :bin \
==> each { spurt $tarball, $_ }
}
# Example
my $total = run {
[+] (pipe «find /var/log -type f» ==> map(*.IO.s))
}
You must extract the data you want in the run
block because run
terminates
all external processes started by pipe
's at the end of the block by sending
them the SIGCHLD
signal.
NOTE: Subs like
map
andgrep
are lazy by default, so they are not suitable to be used at the end of a pipeline, unless you also consume the entire pipeline with somethig like the[+]
operator in the above example. To consume a pipeline, you can also assign or feed the pipeline to an array, or listify the pipeline by using a==> @
at the end of the pipeline, or feed it to aproc(...)
that consumes STDIN.
By default, run
throws a BrokenPipeline
exception if any of the pipe
calls fails or exits with non-zero status, or if an exception is propagated
from the block. You can prevent an exception from being thrown with the
:!check
option of run
, in which case it returns a two-element list instead:
my ($result, $err) = run :!check, {
pipe(<<cat "$*HOME/.bashrc">>) \
==> pipe(<gunzip -c>) # <-- fails because input is not gzipped
==> join('')
}
if $err {
put 'Failed decompressing file...';
say $err; # $err.gist will show you the errors and stack traces.
} else {
put $result;
}
The first item is the return value of the block you passed to run
; the second
item is a BrokenPipeline
exception object if there's any.
NOTE: Many code snippets in this doc are made to demo how the various functions from this module can be used. They are usually not the most efficient way to do the same thing if data have to flow from an external process to Perl 6, without any processing, just to flow back to another external process. For example, with
run { pipe(<<ls -la>>) ==> proc('nl') }
this module currently is not smart enough to connect theSTDOUT
of the first process to theSTDIN
of the second process, and therefore the data have to go through Perl 6.
Other helpers
sub each(&code, $input? is raw) is export(:each);
multi sub map(Range \range, &code, $input? is raw) is export(:map);
each
, often used for side effects, makes feeding into a block that
just wants to be called for each item of the iterable fed to it easier:
# using each:
(1,2,3,4) ==> each { .say }
# without each:
(1,2,3,4) ==> { .say for $_ }()
The map
multi sub exported by this module takes a Range
parameter so that
you can limit the code to only a certain range of input a block:
# using ranged map:
1,1, * + * ... Inf ==> map ^10, { $_ ** 2 }
# without ranged map:
1,1, * + * ... Inf ==> { gather for $_[^10] { take $_ ** 2 } }()
sub quote(Str $s --> Str) is export(:quote);
quote
will single-quote a string so that it can be used in a shell script/command
literally:
my $file = q/someone's file is > than 1GB/;
put capture("echo {quote $file}", :shell);