File Coverage

blib/lib/Circle/Commandable.pm
Criterion Covered Total %
statement 131 207 63.2
branch 34 96 35.4
condition 10 24 41.6
subroutine 22 23 95.6
pod 0 9 0.0
total 197 359 54.8


line stmt bran cond sub pod time code
1             # You may distribute under the terms of the GNU General Public License
2             #
3             # (C) Paul Evans, 2008-2012 -- leonerd@leonerd.org.uk
4              
5             package Circle::Commandable;
6              
7 4     4   26 use strict;
  4         7  
  4         98  
8 4     4   20 use warnings;
  4         7  
  4         143  
9              
10             our $VERSION = '0.173320';
11              
12 4     4   18 use Carp;
  4         13  
  4         230  
13              
14 4     4   1777 use Attribute::Storage 0.06 qw( get_subattr get_subattrs );
  4         7397  
  4         18  
15              
16 4     4   1748 use Circle::Command;
  4         8  
  4         301  
17 4     4   1394 use Circle::CommandInvocation;
  4         8  
  4         116  
18              
19 4     4   1753 use Circle::Widget::Entry;
  4         10  
  4         285  
20              
21             #############################################
22             ### Attribute handlers for command_* subs ###
23             #############################################
24              
25             sub Command_description :ATTR(CODE)
26             {
27 334     334 0 42371 my $class = shift;
28 334         616 my ( $text ) = @_;
29              
30 334         954 my ( $brief, $detail ) = split( m/\n/, $text, 2 );
31              
32 334         969 return [ $brief, $detail ];
33 4     4   1790 }
  4         3409  
  4         21  
34              
35             sub Command_arg :ATTR(CODE,MULTI)
36             {
37 280     280 0 26203 my $class = shift;
38 280         581 my ( $args, $name, %spec ) = @_;
39              
40             # Some things are only allowed on the last argument. Check none of these
41             # apply to the previous one
42 280 100       544 my $prev = $args ? $args->[-1] : undef;
43              
44 280 100       667 if( $prev ) {
45 64 50       118 $prev->{eatall} and croak "Cannot have another argument after an eatall";
46 64 50       110 $prev->{collect} and croak "Cannot have another argument after a collect";
47 64 50       108 $prev->{trail} and croak "Cannot have another argument after a trail";
48             }
49              
50 280         616 my $optional = $name =~ s/\?$//; # No error if this is missing
51              
52             my %arg = (
53             name => uc $name,
54             optional => $optional,
55             eatall => delete $spec{eatall}, # This argument consumes all the remaining text in one string
56             collect => delete $spec{collect}, # This argument collects all the non-option tokens in an ARRAY ref
57 280         1163 );
58              
59 280 50 66     829 $arg{eatall} and $arg{collect} and croak "Cannot eatall and collect";
60              
61 280 50       582 keys %spec and croak "Unrecognised argument specification keys: ".join( ", ", keys %spec );
62              
63 280         379 my $trail = 0;
64 280 100       545 if( $name eq "..." ) {
65 4         9 $arg{trail} = 1;
66             }
67             else {
68 276 50       616 $name =~ m/\W/ and croak "Cannot use $name as an argument name";
69             }
70              
71 280         707 push @$args, \%arg;
72              
73 280         793 return $args;
74 4     4   1529 }
  4         8  
  4         13  
75              
76             sub Command_opt :ATTR(CODE,MULTI)
77             {
78 138     138 0 17863 my $class = shift;
79 138         379 my ( $opts, $name, %spec ) = @_;
80              
81             my %opt = (
82             desc => delete $spec{desc},
83 138         316 );
84              
85 138 50       304 keys %spec and croak "Unrecognised option specification keys: ".join( ", ", keys %spec );
86              
87 138 50       622 $name =~ s/=(.*)$// or croak "Cannot recognise $name as an option spec";
88 138         343 $opt{type} = $1;
89              
90 138 50       351 $opt{type} =~ m/^[\$\+]$/ or croak "Cannot recognise $opt{type} as an option type";
91              
92 138         295 $opts->{$name} = \%opt;
93              
94 138         409 return $opts;
95 4     4   1122 }
  4         7  
  4         14  
96              
97             sub Command_subof :ATTR(CODE)
98             {
99 122     122 0 14818 my $class = shift;
100 122         218 my ( $parent ) = @_;
101              
102 122         257 return $parent;
103 4     4   1194 }
  4         7  
  4         14  
104              
105             sub Command_default :ATTR(CODE)
106             {
107 32     32 0 2911 return 1; # Just a boolean
108 4     4   654 }
  4         9  
  4         19  
109              
110             sub do_command
111             {
112 5     5 0 9 my $self = shift;
113 5         11 my ( $cinv ) = @_;
114              
115 5         29 my $cmd = $cinv->pull_token;
116              
117 5         10 my $command = undef;
118 5         39 my %commands = Circle::Command->root_commands( $cinv );
119              
120 5   66     46 while( keys %commands and $cmd ||= $cinv->pull_token ) {
      66        
121 7 50       29 unless( exists $commands{$cmd} ) {
122 0 0       0 $cinv->responderr( $command ? $command->name . " has no sub command $cmd"
123             : "No such command $cmd" );
124 0         0 return;
125             }
126              
127 7         16 $command = $commands{$cmd};
128 7         21 %commands = $command->sub_commands( $cinv );
129              
130 7         31 undef $cmd;
131             }
132              
133 5         17 while( keys %commands ) {
134 0         0 my $subcmd = $command->default_sub( $cinv );
135              
136 0 0       0 if( !$subcmd ) {
137             # No default subcommand - issue help on $command instead
138 0         0 my $helpinv = $cinv->nest( "help " . $command->name );
139 0         0 return $self->do_command( $helpinv );
140             }
141              
142 0         0 $command = $subcmd;
143 0         0 %commands = $command->sub_commands( $cinv );
144             }
145              
146 5         16 my $cname = $command->name;
147              
148 5         11 my @args;
149             my %opts;
150              
151 5         16 my @argspec = $command->args;
152 5         17 my $optspec = $command->opts;
153              
154 5         16 my $argindex = 0;
155              
156 5         7 my $no_more_opts;
157 5         19 while( length $cinv->peek_remaining ) {
158 8 50       22 if( $cinv->peek_token eq "--" ) {
159 0         0 $cinv->pull_token;
160 0         0 $no_more_opts++;
161 0         0 next;
162             }
163              
164 8 100 66     41 if( !$no_more_opts and $cinv->peek_remaining =~ m/^-/ ) {
165             # An option
166 2         6 my $optname = $cinv->pull_token;
167 2         9 $optname =~ s/^-//;
168              
169 2 50 33     14 $optspec and exists $optspec->{$optname} or
170             return $cinv->responderr( "$cname: unrecognised option $optname" );
171              
172 2         10 my $optvalue;
173              
174 2 50       10 if( $optspec->{$optname}{type} eq '$' ) {
175 2         7 $optvalue = $cinv->pull_token;
176 2 50       7 defined $optvalue or
177             return $cinv->responderr( "$cname: option $optname require a value" );
178             }
179             else {
180 0         0 $optvalue = 1;
181             }
182              
183 2         8 $opts{$optname} = $optvalue;
184             }
185             else {
186 6 50 33     27 return $cinv->responderr( "$cname: Too many arguments" ) if !@argspec or $argindex >= @argspec;
187              
188 6         12 my $a = $argspec[$argindex];
189              
190 6 100       30 if( $a->{eatall} ) {
    50          
    50          
191 2         5 push @args, $cinv->peek_remaining;
192 2         3 $argindex++;
193 2         4 last;
194             }
195             elsif( $a->{collect} ) {
196             # If this is the first one, $args[-1] won't be an ARRAY ref
197 0 0       0 push @args, [] unless ref $args[-1];
198 0         0 push @{ $args[-1] }, $cinv->pull_token;
  0         0  
199             }
200             elsif( $a->{trail} ) {
201 0         0 last;
202             }
203             else {
204 4         14 push @args, $cinv->pull_token;
205 4         12 $argindex++;
206             }
207             }
208             }
209              
210 5         19 while( $argindex < @argspec ) {
211 0         0 my $a = $argspec[$argindex++];
212              
213 0 0       0 if( $a->{collect} ) {
    0          
214 0 0       0 push @args, [] unless ref $args[-1];
215 0         0 last;
216             }
217             elsif( $a->{trail} ) {
218 0         0 last;
219             }
220              
221             $a->{optional} or
222 0 0       0 return $cinv->responderr( "$cname: expected $a->{name}" );
223              
224 0         0 push @args, undef;
225             }
226              
227 5 100       14 push @args, \%opts if $optspec;
228              
229 5         8 push @args, $cinv;
230              
231 5         11 my @response = eval { $command->invoke( @args ) };
  5         16  
232 5 50       28 if( $@ ) {
233 0         0 my $text = $@; chomp $text;
  0         0  
234 0         0 $cinv->responderr( $text );
235             }
236             else {
237 5         162 $cinv->respond( $_ ) foreach @response;
238             }
239             }
240              
241             sub command_help
242             : Command_description("Display help on a command")
243             : Command_arg('command?')
244             : Command_arg('...')
245             {
246 0     0 0 0 my $self = shift;
247 0         0 my ( $cmd, $cinv ) = @_;
248              
249 0         0 my $command = undef;
250 0         0 my %commands = Circle::Command->root_commands( $cinv );
251              
252 0 0       0 if( !defined $cmd ) {
253 0   0     0 my $class = ref $self || $self;
254 0         0 $cinv->respond( "Available commands for $class:" );
255             }
256              
257 0   0     0 while( ( $cmd ||= $cinv->pull_token ) ) {
258 0 0       0 unless( exists $commands{$cmd} ) {
259 0 0       0 $cinv->responderr( $command ? $command->name . " has no sub command $cmd"
260             : "No such command $cmd" );
261 0         0 return;
262             }
263              
264 0         0 $command = $commands{$cmd};
265 0         0 %commands = $command->sub_commands( $cinv );
266              
267 0         0 undef $cmd;
268             }
269              
270 0 0       0 if( $command ) {
271 0         0 $cinv->respond( "/" . $command->name . " - " . $command->desc );
272             }
273              
274 0 0       0 if( keys %commands ) {
275 0 0       0 $cinv->respond( "Usage: " . $command->name . " SUBCMD ..." ) if $command;
276              
277 0         0 my @table;
278 0         0 foreach my $sub ( map { $commands{$_} } sort keys %commands ) {
  0         0  
279 0         0 my $subname;
280             # bold function name if it's default
281 0 0       0 if( $sub->is_default ) {
282 0         0 $subname = Circle::TaggedString->new( " /" . $sub->name );
283 0         0 $subname->apply_tag( 0, $subname->length, b => 1 );
284             }
285             else {
286 0         0 $subname = " /" . $sub->name;
287             }
288              
289 0         0 push @table, [ $subname, $sub->desc ];
290             }
291              
292 0         0 $cinv->respond_table( \@table, colsep => " - ", headings => [ "Command", "Description" ] );
293              
294 0         0 return;
295             }
296              
297 0         0 my @argdesc;
298 0         0 foreach my $a ( $command->args ) {
299 0         0 my $name = $a->{name};
300 0 0       0 $name .= "..." if $a->{eatall};
301 0 0       0 $name .= "+" if $a->{collect};
302 0 0       0 $name = "[$name]" if $a->{optional};
303 0         0 push @argdesc, $name;
304             }
305              
306 0         0 $cinv->respond( "Usage: " . join( " ", $command->name, @argdesc ) );
307              
308 0 0       0 if( my $opts = $command->opts ) {
309 0         0 $cinv->respond( "Options:" );
310              
311 0         0 my @table;
312              
313 0         0 foreach my $opt ( sort keys %$opts ) {
314 0         0 my $opttype = $opts->{$opt}{type};
315 0 0       0 my $desc = defined $opts->{$opt}{desc} ? $opts->{$opt}{desc} : "";
316              
317 0 0       0 push @table, [ " -$opt" . ( $opttype eq '$' ? " VALUE" : "" ), $desc ];
318             }
319              
320 0         0 $cinv->respond_table( \@table, headings => [ "Option", "Description" ] );
321             }
322              
323 0 0       0 if( my $detail = $command->detail ) {
324 0         0 $cinv->respond( "" );
325 0         0 $cinv->respond( $_ ) for split( m/\n/, $detail );
326             }
327              
328 0         0 return;
329 4     4   4885 }
  4         13  
  4         18  
330              
331             sub method_do_command
332             {
333 4     4 0 48862 my $self = shift;
334 4         12 my ( $ctx, $command ) = @_;
335              
336 4         17 my $cinv = Circle::CommandInvocation->new( $command, $ctx->stream, $self );
337 4         25 $self->do_command( $cinv );
338             }
339              
340             ###
341             # Widget
342             ###
343              
344             sub get_widget_commandentry
345             {
346 2     2 0 5 my $self = shift;
347              
348 2 50       7 return $self->{widget_commandentry} if defined $self->{widget_commandentry};
349              
350 2         4 my $registry = $self->{registry};
351              
352             my $widget = $registry->construct(
353             "Circle::Widget::Entry",
354             autoclear => 1,
355             focussed => 1,
356             history => 100, # TODO
357             on_enter => sub {
358 2     2   5 my ( $text, $ctx ) = @_;
359              
360 2 100       46 if( $text =~ m{^/} ) {
    50          
361 1         5 substr( $text, 0, 1 ) = "";
362              
363 1         5 my $cinv = Circle::CommandInvocation->new( $text, $ctx->stream, $self );
364 1         10 $self->do_command( $cinv );
365             }
366             elsif( $self->can( "enter_text" ) ) {
367 1         4 $self->enter_text( $text );
368             }
369             else {
370 0         0 $self->responderr( "Cannot enter raw text here" );
371             }
372             },
373 2         24 );
374              
375 2         104 return $self->{widget_commandentry} = $widget;
376             }
377              
378             0x55AA;