File Coverage

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