File Coverage

blib/lib/App/Cleo/Patra.pm
Criterion Covered Total %
statement 27 105 25.7
branch 0 50 0.0
condition 0 15 0.0
subroutine 9 14 64.2
pod 1 3 33.3
total 37 187 19.7


line stmt bran cond sub pod time code
1             package App::Cleo::Patra;
2              
3 1     1   692 use strict;
  1         3  
  1         28  
4 1     1   5 use warnings;
  1         1  
  1         24  
5              
6 1     1   499 use Term::ReadKey;
  1         2005  
  1         74  
7 1     1   607 use Term::ANSIColor qw(colored);
  1         8215  
  1         729  
8 1     1   560 use File::Slurp qw(read_file);
  1         13079  
  1         64  
9 1     1   597 use Time::HiRes qw(usleep);
  1         1290  
  1         4  
10              
11 1     1   180 use constant PS1 => 'ps1';
  1         3  
  1         79  
12 1     1   7 use constant PS2 => 'ps2';
  1         2  
  1         488  
13             our $VERSION = 0.001;
14              
15             #-----------------------------------------------------------------------------
16              
17             sub new {
18 0     0 0   my $class = shift;
19              
20             my $self = {
21 0   0       shell => $ENV{SHELL} || '/bin/bash',
22             ps1 => colored( ['green'], '(%d)$ '),
23             ps2 => colored( ['green'], '> '),
24             delay => 25_000,
25             state => PS1,
26             @_,
27             };
28              
29 0           return bless $self, $class;
30             }
31              
32             #-----------------------------------------------------------------------------
33              
34             sub run {
35 0     0 1   my ($self, $input, $multiline) = @_;
36              
37 0           my $type = ref $input;
38             # my @commands_raw = !$type ? read_file($commands_raw)
39             # : $type eq 'SCALAR' ? split "\n", ${$commands_raw}
40             ##: $type eq 'SCALAR' and $multiline ? split /^\$\s/m,${$commands_raw}
41             # : $type eq 'ARRAY' ? @{$commands_raw}
42             # : die "Unsupported type: $type";
43              
44 0           my @commands = ();
45 0 0         if (!$type) {
46 0 0         if ($multiline) {
47 0           my $data = read_file($input);
48 0           @commands = split /^\$\s/m, $data;
49             }
50             else {
51 0           @commands = read_file($input);
52             }
53             }
54              
55 0 0         open my $fh, '|-', $self->{shell} or die $!;
56 0           $self->{fh} = $fh;
57 0           ReadMode('raw');
58 0           local $| = 1;
59              
60             local $SIG{CHLD} = sub {
61 0     0     print "Child shell exited!\n";
62 0           ReadMode('restore');
63 0           exit;
64 0           };
65              
66 0           chomp @commands;
67 0           @commands = grep { /^\s*[^\#;]\S+/ } @commands;
  0            
68 0 0         @commands = grep { /.+/ } @commands if $multiline;
  0            
69              
70             # # squeeze multi line commands into one array slot (indicated by ~~~)
71             # my @commands = ();
72             # for (my $i=0; $i<@commands_raw; $i++) {
73             # if ($commands_raw[$i] =~ /[~]{3}(.*)/ and $i != 0) {
74             # $commands[@commands - 1] .= "\n$1";
75             # }
76             # else {
77             # push @commands, $commands_raw[$i];
78             # }
79             # }
80              
81 0           my $continue_to_end = 0;
82              
83             CMD:
84 0           for (my $i = 0; $i < @commands; $i++) {
85              
86 0 0         my $cmd = defined $commands[$i] ? $commands[$i] : die "no command $i";
87 0           chomp $cmd;
88              
89 0           my $keep_going = $cmd =~ s/^\.\.\.//;
90 0           my $run_in_background = $cmd =~ s/^!!!//;
91              
92 0 0 0       $self->do_cmd($cmd) and next CMD
93             if $run_in_background;
94              
95 1     1   8 no warnings 'redundant';
  1         2  
  1         810  
96 0           my $prompt_state = $self->{state};
97 0           print sprintf $self->{$prompt_state}, $i;
98              
99 0           my @steps = split /%%%/, $cmd;
100 0           while (my $step = shift @steps) {
101              
102 0   0       my $should_pause = !($keep_going || $continue_to_end);
103 0 0         my $key = $should_pause ? ReadKey(0) : '';
104 0 0         if ($key =~ /^\d$/) {
105 0           $key .= $1 while (ReadKey(0) =~ /^(\d)/);
106             }
107 0 0         print "\n" if $key =~ m/^[srp]|[0-9]+/;
108              
109 0 0         last CMD if $key eq 'q';
110 0 0         next CMD if $key eq 's';
111 0 0         redo CMD if $key eq 'r';
112 0 0         $i--, redo CMD if $key eq 'p';
113 0 0         $i = $key, redo CMD if $key =~ /^\d+$/;
114 0 0         $continue_to_end = 1 if $key eq 'c';
115              
116 0 0         $step .= ' ' if not @steps;
117 0           my @chars = split '', $step;
118 0   0       print and usleep $self->{delay} for @chars;
119             }
120              
121 0   0       my $should_pause = !($keep_going || $continue_to_end);
122 0 0         my $key = $should_pause ? ReadKey(0) : '';
123 0 0         if ($key =~ /^\d$/) {
124 0           $key .= $1 while (ReadKey(0) =~ /^(\d)/);
125             }
126 0           print "\n";
127              
128 0 0         last CMD if $key eq 'q';
129 0 0         next CMD if $key eq 's';
130 0 0         redo CMD if $key eq 'r';
131 0 0         $i--, redo CMD if $key eq 'p';
132 0 0         $i = $key, redo CMD if $key =~ /^\d+$/;
133 0 0         $continue_to_end = 1 if $key eq 'c';
134              
135 0           $self->do_cmd($cmd);
136             }
137              
138 0           ReadMode('restore');
139 0           print "\n";
140              
141 0           return $self;
142             }
143              
144             #-----------------------------------------------------------------------------
145              
146             sub do_cmd {
147 0     0 0   my ($self, $cmd) = @_;
148              
149 0           my $cmd_is_finished;
150 0     0     local $SIG{ALRM} = sub {$cmd_is_finished = 1};
  0            
151              
152 0           $cmd =~ s/%%%//g;
153 0           my $fh = $self->{fh};
154              
155 0           print $fh "$cmd\n";
156              
157 0 0 0       ($self->{state} = PS2) and return 1
158             if $cmd =~ m{\s+\\$};
159              
160 0           print $fh "kill -14 $$\n";
161 0           $fh->flush;
162              
163             # Wait for signal that command has ended
164 0           until ($cmd_is_finished) {}
165 0           $cmd_is_finished = 0;
166              
167 0           $self->{state} = PS1;
168              
169 0           return 1;
170             }
171              
172             #-----------------------------------------------------------------------------
173             1;
174              
175             =pod
176              
177             =encoding utf8
178              
179             =head1 NAME
180              
181             App::Cleo - Play back shell commands for live demonstrations
182              
183             =head1 SYNOPSIS
184              
185             use App::Cleo::Patra
186             my $patra = App::Cleo::Patra->new(%options);
187             $patra->run($commands);
188              
189             =head1 DESCRIPTION
190              
191             B
192             C is an experimental fork from C.
193             You should check the current differences from C and decide, which one you want to use.
194             It may be, that in your current time, C is merged back into C or obsolete for other reasons.
195              
196             App::Cleo::Patra is the back-end for the L utility. Please see the L
197             documentation for details on how to use this.
198              
199             =head1 CONSTRUCTOR
200              
201             The constructor accepts arguments as key-value pairs. The following keys are
202             supported:
203              
204             =over 4
205              
206             =item delay
207              
208             Number of microseconds to wait before displaying each character of the command.
209             The default is C<25_000>.
210              
211             =item ps1
212              
213             String to use for the artificial prompt. The token C<%d> will be substituted
214             with the number of the current command. The default is C<(%d)$>.
215              
216             =item ps2
217              
218             String to use for the artificial prompt that appears for multiline commands. The
219             token C<%d> will be substituted with the number of the current command. The
220             default is C<< > >>.
221              
222             =item shell
223              
224             Path to the shell command that will be used to run the commands. Defaults to
225             either the C environment variable or C.
226              
227             =back
228              
229             =head1 METHODS
230              
231             =over 4
232              
233             =item run( $commands )
234              
235             Starts playback of commands. If the argument is a string, it will be treated
236             as a file name and commands will be read from the file. If the argument is a
237             scalar reference, it will be treated as a string of commands separated by
238             newlines. If the argument is an array reference, then each element of the
239             array will be treated as a command.
240              
241             =back
242              
243             =head1 AUTHOR
244              
245             Jeffrey Ryan Thalhammer
246              
247             Boris Däppeb (BORISD)
248              
249             =head1 COPYRIGHT
250              
251             cleo - Copyright (c) 2014, Imaginative Software Systems
252              
253             patra - Boris Däppen (BORISD) 2018
254              
255             =cut