File Coverage

blib/lib/Circle/Commandable.pm
Criterion Covered Total %
statement 129 202 63.8
branch 33 94 35.1
condition 8 21 38.1
subroutine 22 23 95.6
pod 0 9 0.0
total 192 349 55.0


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