File Coverage

blib/lib/Commandable/Invocation.pm
Criterion Covered Total %
statement 40 40 100.0
branch 11 12 91.6
condition 5 6 83.3
subroutine 11 11 100.0
pod 6 6 100.0
total 73 75 97.3


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2018 -- leonerd@leonerd.org.uk
5              
6             package Commandable::Invocation 0.11;
7              
8 6     6   622266 use v5.14;
  6         37  
9 6     6   28 use warnings;
  6         9  
  6         3412  
10              
11             =head1 NAME
12              
13             C<Commandable::Invocation> - represents one invocation of a CLI command
14              
15             =head1 SYNOPSIS
16              
17             my %commands = (
18             exit => sub { exit },
19             print => sub { print $_[0]->peek_remaining },
20             ...
21             );
22              
23             while(1) {
24             my $inv = Commmandable::Invocation->new( scalar <STDIN> );
25              
26             $commands{ $inv->pull_token }->( $inv );
27             }
28              
29             =head1 DESCRIPTION
30              
31             Instances of this class represent the text of a single invocation of a CLI
32             command, allowing it to be incrementally parsed and broken into individual
33             tokens during dispatch and invocation.
34              
35             =head2 Tokens
36              
37             When parsing for the next token, strings quoted using quote marks (C<"">) will
38             be retained as a single token. Otherwise, tokens are split on (non-preserved)
39             whitespace.
40              
41             Quote marks and backslashes may be escaped using C<\> characters.
42              
43             =cut
44              
45             =head1 CONSTRUCTOR
46              
47             =cut
48              
49             =head2 new
50              
51             $inv = Commandable::Invocation->new( $text )
52              
53             Constructs a new instance, initialised to contain the given text string.
54              
55             =cut
56              
57             sub new
58             {
59 37     37 1 9368 my $class = shift;
60 37         62 my ( $text ) = @_;
61              
62 37         92 $text =~ s/^\s+//;
63              
64 37         135 return bless {
65             text => $text,
66             }, $class;
67             }
68              
69             =head2 new_from_tokens
70              
71             $inv = Commandable::Invocation->new_from_tokens( @tokens )
72              
73             I<Since version 0.03.>
74              
75             Constructs a new instance, initialised to contain text from the given tokens,
76             such that subsequent calls to L</pull_token> will yield the given list of
77             tokens. This may be handy for constructing instances from C<@ARGV> or similar
78             cases where text has already been parsed and split into tokens.
79              
80             =cut
81              
82             sub new_from_tokens
83             {
84 9     9 1 363 my $class = shift;
85 9         23 my ( @tokens ) = @_;
86              
87 9         20 my $self = $class->new( "" );
88 9         28 $self->putback_tokens( @tokens );
89              
90 9         42 return $self;
91             }
92              
93             =head1 METHODS
94              
95             =cut
96              
97             sub _next_token
98             {
99 109     109   161 my $self = shift;
100              
101 109 100       217 if( $self->{text} =~ m/^"/ ) {
102             $self->{text} =~ m/^"((?:\\.|[^"])*)"\s*/ and
103 4 50       57 $self->{trim_pos} = $+[0], return $self->_unescape( $1 );
104             }
105             else {
106             $self->{text} =~ m/^(\S+)\s*/ and
107 105 100       470 $self->{trim_pos} = $+[0], return $self->_unescape( $1 );
108             }
109              
110 31         84 return undef;
111             }
112              
113             sub _escape
114             {
115 46     46   53 my $self = shift;
116 46         67 my ( $s ) = @_;
117              
118 46         115 $s =~ s/(["\\])/\\$1/g;
119              
120 46         71 return $s;
121             }
122              
123             sub _unescape
124             {
125 78     78   128 my $self = shift;
126 78         156 my ( $s ) = @_;
127              
128 78         121 $s =~ s/\\(["\\])/$1/g;
129              
130 78         266 return $s;
131             }
132              
133             =head2 peek_token
134              
135             $token = $inv->peek_token
136              
137             Looks at, but does not remove, the next token in the text string. Subsequent
138             calls to this method will yield the same string, as will the next call to
139             L</pull_token>.
140              
141             =cut
142              
143             sub peek_token
144             {
145 3     3 1 10 my $self = shift;
146              
147 3   66     14 return $self->{next_token} //= $self->_next_token;
148             }
149              
150             =head2 pull_token
151              
152             $token = $inv->pull_token
153              
154             Removes the next token from the text string and returns it.
155              
156             =cut
157              
158             sub pull_token
159             {
160 108     108 1 159 my $self = shift;
161              
162 108   100     275 my $token = $self->{next_token} //= $self->_next_token;
163              
164 108 100       266 substr $self->{text}, 0, $self->{trim_pos}, "" if defined $token;
165 108         146 undef $self->{next_token};
166              
167 108         307 return $token;
168             }
169              
170             =head2 peek_remaining
171              
172             $text = $inv->peek_remaining
173              
174             I<Since version 0.04.>
175              
176             Returns the entire unparsed content of the rest of the text string.
177              
178             =cut
179              
180             sub peek_remaining
181             {
182 30     30 1 54 my $self = shift;
183              
184 30         134 return $self->{text};
185             }
186              
187             =head2 putback_tokens
188              
189             $inv->putback_tokens( @tokens )
190              
191             I<Since version 0.02.>
192              
193             Prepends text back onto the stored text string such that subsequent calls to
194             L</pull_token> will yield the given list of tokens once more. This takes care
195             to quote tokens with spaces inside, and escape any embedded backslashes or
196             quote marks.
197              
198             This method is intended to be used, for example, around a commandline option
199             parser which handles mixed options and arguments, to put back the non-option
200             positional arguments after the options have been parsed and removed from it.
201              
202             =cut
203              
204             sub putback_tokens
205             {
206 31     31 1 41 my $self = shift;
207              
208             $self->{text} = join " ",
209             ( map {
210 46         67 my $s = $self->_escape( $_ );
211 46 100       169 $s =~ m/ / ? qq("$s") : $s
212             } @_ ),
213 31 100       77 ( length $self->{text} ? $self->{text} : () );
214             }
215              
216             =head1 AUTHOR
217              
218             Paul Evans <leonerd@leonerd.org.uk>
219              
220             =cut
221              
222             0x55AA;