File Coverage

bin/remperl
Criterion Covered Total %
statement 138 154 89.6
branch 65 82 79.2
condition 22 37 59.4
subroutine 10 12 83.3
pod n/a
total 235 285 82.4


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2 49     49   260189 use v5.36;
  49         163  
3 49     49   27180 use FindBin qw($Bin);
  49         87280  
  49         7435  
4 49     49   32739 use lib "$Bin/../lib";
  49         41134  
  49         372  
5              
6 49     49   44522 use Getopt::Long qw(GetOptions);
  49         779923  
  49         379  
7 49     49   34339 use Remote::Perl;
  49         162  
  49         199905  
8              
9             # Only load Pod::Usage if required because its dependencies load slowly.
10 3     3   4302 sub pod2usage { require Pod::Usage; Pod::Usage::pod2usage(@_) }
  3         203595  
11              
12             # -- Option variables ----------------------------------------------------------
13              
14 49         8745405 my $rsh = 'ssh';
15 49         124 my $pipe_cmd = 0;
16 49         122 my $window = 65_536;
17 49         106 my $stdin_file = undef;
18 49         87 my $stdin_str = undef;
19 49         119 my $serve_modules = 0;
20 49         140 my @inc_local;
21 49         88 my $tmpfile = 0;
22 49         79 my $tmpfile_mode = undef;
23 49         91 my $no_data_warn = 0;
24 49         95 my $no_system_inc = 0;
25 49         88 my $serve_restrict = 0;
26 49         108 my @serve_allow;
27 49         108 my $help = 0;
28 49         97 my $version = 0;
29              
30             # -- GetOptions spec (shared between parse_perl_opts and GetOptions) -----------
31              
32 49         642 my @getopt_spec = (
33             'rsh=s' => \$rsh,
34             'pipe-cmd' => \$pipe_cmd,
35             'window-size=i' => \$window,
36             'stdin-file=s' => \$stdin_file,
37             'stdin-str=s' => \$stdin_str,
38             'serve-modules!' => \$serve_modules,
39             'inc-local=s@' => \@inc_local,
40             'no-system-inc' => \$no_system_inc,
41             'serve-restrict-paths' => \$serve_restrict,
42             'serve-allow=s@' => \@serve_allow,
43             'tmpfile!' => \$tmpfile,
44             'tmpfile-mode=s' => \$tmpfile_mode,
45             'no-data-warn' => \$no_data_warn,
46             'help' => \$help,
47             'version' => \$version,
48             );
49              
50             # -- Perl-compatible short option parsing --------------------------------------
51              
52             # Mirrors Perl's own option parsing rules for -e, -m, -M, -w, -h, -V.
53             # Processes @ARGV in-place before GetOptions runs.
54              
55 9     9   16 sub use_spec_to_line($flag, $spec) {
  9         21  
  9         16  
  9         16  
56             # '=' form: -m and -M are identical; use split like Perl does
57 9 100       57 if ($spec =~ /^([^=]+)=(.+)$/) {
58 4         38 return "use $1 split(/,/,q{$2});\n";
59             }
60 5 100       18 if ($flag eq 'm') { return "use $spec ();\n" }
  1         3  
61 4 100       18 if ($spec =~ /^-(.+)/) { return "no $1;\n" }
  2         14  
62 2         8 return "use $spec;\n";
63             }
64              
65 49     49   156 sub parse_perl_opts() {
  49         77  
66 49         109 my @eval_code;
67             my @use_specs;
68 49         124 my $warnings = 0;
69 49         133 my @new_argv;
70             my @trailing; # args after stop point (not seen by GetOptions)
71 49         115 my $positionals = 0;
72              
73             # Long options that consume the next argv element as a value.
74             # Derived from @getopt_spec: any key containing = or : takes an argument.
75 49         83 my %long_val;
76 49         294 for (my $i = 0; $i < @getopt_spec; $i += 2) {
77 735 100       2054 if ($getopt_spec[$i] =~ /[=:]/) {
78 343         931 (my $name = $getopt_spec[$i]) =~ s/[=:!+].*//;
79 343         1859 $long_val{"--$name"} = 1;
80             }
81             }
82              
83 49         336 while (@ARGV) {
84 160         712 my $arg = shift @ARGV;
85              
86             # -- stops all processing
87 160 100       406 if ($arg eq '--') {
88 2         4 push @trailing, @ARGV;
89 2         4 last;
90             }
91              
92             # Long options: pass through to GetOptions (with value if applicable)
93 158 100       547 if ($arg =~ /^--/) {
94 50         177 push @new_argv, $arg;
95             # If --foo=bar, value is already embedded; otherwise consume next
96 50 100 66     650 if ($arg !~ /=/ && $long_val{$arg} && @ARGV) {
      66        
97 4         8 push @new_argv, shift @ARGV;
98             }
99 50         149 next;
100             }
101              
102             # Not an option: positional argument
103 108 100       290 if ($arg !~ /^-/) {
104 71         123 push @new_argv, $arg;
105 71         117 $positionals++;
106 71 100       214 if ($positionals >= 2) {
107 27         50 push @trailing, @ARGV;
108 27         45 last;
109             }
110 44         156 next;
111             }
112              
113             # Short option bundle: strip leading '-' and process char by char
114 37         89 my $bundle = substr($arg, 1);
115 37         113 while (length $bundle) {
116 39         85 my $c = substr($bundle, 0, 1, '');
117              
118 39 100 100     476 if ($c eq 'w') {
    100 100        
    100          
    100          
119 6         16 $warnings = 1;
120             }
121             elsif ($c eq 'h') {
122 1         5 pod2usage(-verbose => 1, -exitstatus => 0);
123             }
124             elsif ($c eq 'V') {
125 1         4 $version = 1;
126             }
127             elsif ($c eq 'e' || $c eq 'm' || $c eq 'M') {
128             # Value-consuming: rest of bundle, or next argv element
129 30         80 my $val;
130 30 100       122 if (length $bundle) {
131 9         18 $val = $bundle;
132 9         18 $bundle = '';
133             }
134             else {
135 21         60 $val = shift @ARGV;
136 21 100       64 die "remperl: missing argument for -$c\n"
137             unless defined $val;
138             }
139 29 100       100 if ($c eq 'e') {
140 20         78 push @eval_code, $val;
141             }
142             else {
143 9         54 push @use_specs, [$c, $val];
144             }
145             }
146             else {
147 1         0 die "remperl: unrecognized switch: -$c\n";
148             }
149             }
150             }
151              
152 46         154 @ARGV = @new_argv;
153 46         309 return (\@eval_code, \@use_specs, $warnings, \@trailing);
154             }
155              
156 49         315 my ($eval_codes, $use_specs, $warnings, $trailing) = parse_perl_opts();
157              
158             # -- Long option parsing -------------------------------------------------------
159              
160 46 100       392 GetOptions(@getopt_spec) or pod2usage(2);
161              
162             # Re-append args that appeared after the stop point (2nd positional or --).
163             # These are script arguments and must not be processed by GetOptions.
164 45         112092 push @ARGV, @$trailing;
165              
166 45 100       171 pod2usage(-verbose => 1, -exitstatus => 0) if $help;
167              
168 44 100       128 if ($version) {
169 1         27 print "remperl $Remote::Perl::VERSION\n";
170 1         0 exit 0;
171             }
172              
173             # No arguments at all: show usage instead of a cryptic "missing host" error.
174 43 50 33     179 pod2usage(-verbose => 99, -sections => 'SYNOPSIS', -exitstatus => 0) unless @ARGV || @$eval_codes;
175              
176             # -- Build the remote command --------------------------------------------------
177              
178 43         81 my @cmd;
179 43 50       136 if ($pipe_cmd) {
180 43   50     147 my $spec = shift @ARGV
181             // die "remperl: --pipe-cmd requires a command argument\n";
182 43         153 @cmd = ('sh', '-c', $spec);
183             }
184             else {
185 0   0     0 my $host = shift @ARGV
186             // die "remperl: missing host argument\n";
187 0         0 @cmd = ($rsh, $host, 'perl');
188             }
189              
190             # -- Source: file or -e --------------------------------------------------------
191              
192 43 100       235 my $eval_code = @$eval_codes ? join("\n", @$eval_codes) : undef;
193              
194             # Build preamble from -m/-M specs
195 43         133 my $preamble = '';
196 43         156 for my $spec (@$use_specs) {
197 9         52 $preamble .= use_spec_to_line($spec->[0], $spec->[1]);
198             }
199              
200 43         136 my $script;
201 43 100       136 if (!defined $eval_code) {
202 25 100       78 die "remperl: -m/-M requires -e\n" if length $preamble;
203 24   50     84 $script = shift @ARGV
204             // die "remperl: missing script argument (or use -e CODE)\n";
205             }
206             else {
207 18 100       70 $eval_code = $preamble . $eval_code if length $preamble;
208             }
209              
210             # -- stdin ---------------------------------------------------------------------
211              
212 42 50 66     182 die "remperl: --stdin-file and --stdin-str are mutually exclusive\n"
213             if defined $stdin_file && defined $stdin_str;
214              
215 42         128 my $stdin;
216 42 100       200 if (defined $stdin_str) {
    100          
217 2         32 $stdin = $stdin_str;
218             }
219             elsif (defined $stdin_file) {
220 2 50       164 open(my $fh, '<', $stdin_file)
221             or die "remperl: cannot open '$stdin_file': $!\n";
222 2         8 binmode($fh);
223 2         6 $stdin = $fh;
224             }
225             else {
226 38         92 $stdin = \*STDIN;
227             }
228              
229             # -- Connect and run -----------------------------------------------------------
230              
231 42         3578 STDOUT->autoflush(1);
232              
233 42 50       455856 my @inc = $no_system_inc ? @inc_local : (@inc_local, @INC);
234              
235             # -- Module serving path restriction -------------------------------------------
236              
237             # --serve-allow implies --serve-restrict-paths
238 42 50       190 $serve_restrict = 1 if @serve_allow;
239              
240 42         78 my $serve_filter;
241 42 50       178 if ($serve_restrict) {
242 0         0 require Cwd;
243             # Allowed dirs: explicit --serve-allow + implicit --inc-local
244 0         0 my @allowed = grep { defined } map { Cwd::realpath($_) } (@serve_allow, @inc_local);
  0         0  
  0         0  
245 0     0     $serve_filter = sub($path) {
  0            
  0            
246 0   0     0 my $real = Cwd::realpath($path) // return 0;
247 0         0 for my $dir (@allowed) {
248 0 0       0 return 1 if index($real, "$dir/") == 0;
249             }
250 0         0 return 0;
251 0         0 };
252             }
253              
254 42 50 33     186 die "remperl: unknown --tmpfile-mode value '$tmpfile_mode'\n"
255             if defined $tmpfile_mode && $tmpfile_mode !~ /^(auto|linux|perl|named|off)$/;
256              
257             # --tmpfile-mode implies --tmpfile; --tmpfile alone means auto.
258 42 50       124 $tmpfile = 1 if defined $tmpfile_mode;
259 42 0       162 my $tmpfile_val = !$tmpfile ? 0
    0          
    50          
260             : !defined($tmpfile_mode) ? 'auto'
261             : $tmpfile_mode eq 'off' ? 0
262             : $tmpfile_mode;
263              
264 42 50       624 my $r = Remote::Perl->new(
265             cmd => \@cmd,
266             window => $window,
267             serve => $serve_modules,
268             inc => \@inc,
269             serve_filter => $serve_filter,
270             tmpfile => $tmpfile_val,
271             data_warn => ($no_data_warn ? 0 : 1),
272             );
273              
274             # Forward signals through the protocol so the remote executor receives them
275             # regardless of transport (SSH, docker, etc.).
276 21         225 for my $sig (qw(INT TERM QUIT HUP)) {
277 84     0   1310 $SIG{$sig} = sub { eval { $r->send_signal($sig) } };
  0         0  
  0         0  
278             }
279              
280 21         89 my @script_args = @ARGV;
281              
282             my %run_opts = (
283 22     22   783 on_stdout => sub { print STDOUT $_[0] },
284 3     3   132 on_stderr => sub { print STDERR $_[0] },
285 21         537 stdin => $stdin,
286             args => \@script_args,
287             warnings => $warnings,
288             );
289              
290 21 100       482 my ($rc, $msg) = defined($eval_code)
291             ? $r->run_code($eval_code, %run_opts)
292             : $r->run_file($script, %run_opts);
293              
294 21 100 66     348 print STDERR "remperl: $msg\n" if $rc && defined $msg && length $msg;
      100        
295              
296 21         167 $r->disconnect;
297 21   50     0 exit($rc // 1);
298              
299             __END__