File Coverage

blib/lib/Zoidberg/Fish/Intel.pm
Criterion Covered Total %
statement 18 320 5.6
branch 0 168 0.0
condition 0 42 0.0
subroutine 6 31 19.3
pod 3 22 13.6
total 27 583 4.6


line stmt bran cond sub pod time code
1             package Zoidberg::Fish::Intel;
2              
3             our $VERSION = '0.981';
4              
5 1     1   1189 use strict;
  1         2  
  1         38  
6 1     1   6 use vars qw/$DEVNULL/;
  1         2  
  1         43  
7             #use AutoLoader 'AUTOLOAD';
8 1     1   5 use Zoidberg::Fish;
  1         2  
  1         23  
9 1     1   5 use Zoidberg::Utils qw/:default path list_path list_dir/;
  1         2  
  1         8  
10              
11             our @ISA = qw/Zoidberg::Fish/;
12              
13             sub init {
14 0     0 1   my $self = shift;
15             #if ($self->{config}{man_cmd}) {} # TODO split \s+
16             }
17              
18             =head1 NAME
19              
20             Zoidberg::Fish::Intel - Completion plugin for Zoidberg
21              
22             =head1 SYNOPSIS
23              
24             This module is a Zoidberg plugin, see Zoidberg::Fish for details.
25              
26             =head1 DESCRIPTION
27              
28             This class provides intelligence for tab-expansion
29             and similar functions. It is very dynamic structured.
30              
31             =head1 METHODS
32              
33             =over 4
34              
35             =item completion_function
36              
37              
38             =cut
39              
40             sub completion_function {
41 0     0 1   my ($self, $word, $buffer, $start) = @_;
42 0           debug "\ncomplete for predefined word '$word' starting at $start";
43 0           my ($m, $c) = $self->_complete($word, $buffer, $start);
44 0           my $diff = $start - $$m{start}; # their word isn't ours
45 0 0         return if $diff < 0; # you never know
46 0           $diff -= length substr $$m{prefix}, 0, $diff, '';
47 0 0         if ($diff) { # we can't be sure about the real length due to escapes
    0          
48 0 0         if (substr($$c[0], 0, $diff) =~ /^(.*\W)/) { $diff = length $1 }
  0            
49 0           substr $_, 0, $diff, '' for @$c;
50             }
51 0           elsif (length $$m{prefix}) { @$c = map {$$m{prefix}.$_} @$c }
  0            
52 0 0         if (@$c == 1) { # postfix only if we have a match
53 0           $$c[0] .= $$m{postfix};
54 0 0         $$c[0] .= $$m{quote}.' ' if $$c[0] =~ /\w$/;
55             }
56 0           output $c;
57             }
58              
59             =item complete
60              
61             =cut
62              
63 0     0 1   sub complete { output _complete(@_) }
64              
65             sub _complete {
66 0     0     my ($self, $word, $buffer, $start) = @_;
67             #debug "complete got word: $word, start $start, line: ".$buffer;
68 0           my $cursor = $start + length $word;
69 0   0       $buffer ||= $word;
70              
71             # fetch block
72 0           $buffer = substr $buffer, 0, $cursor;
73 0           my ($pref, $block) = $self->_get_last_block($buffer);
74             # $$block[0]{i_feel_lucky} = $i_feel_lucky; TODO, also T:RL:Zoid support for this
75 0 0         @{$$block[0]}{qw/quote/} = $1 if $$block[-1] =~ s/^(['"])//;
  0            
76              
77 0           debug "\ncompletion prefix: ", $pref, "\nand start block: ", $block;
78 0           $block = $self->do($block, $$block[0]{context});
79 0 0         $block = $self->join_blocks(@$block) if ref($$block[0]) eq 'ARRAY';
80 0           my %meta = (
81             start => length $pref,
82 0           ( map {($_ => $$block[0]{$_})} qw/message prefix postfix quoted quote/ )
83             );
84 0 0         if ($meta{quote}) { $meta{prefix} = $meta{quote} . $meta{prefix} }
  0            
85 0           else { $meta{quote} = \&_escape_completion } # escaping
86             #debug scalar(@{$$block[0]{poss}}) . ' completions, meta: ', \%meta;
87             #debug [$$block[0]{poss}];
88 0           return (\%meta, $$block[0]{poss});
89             }
90              
91             sub _escape_completion {
92 0 0   0     $_[0] =~ s#\\\\|(?<*?\[\]{}()\$\%\@'"`])#$1?"\\$1":'\\\\'#eg;
  0            
93 0           return $_[0];
94             }
95              
96             sub _get_last_block {
97 0     0     my ($self, $string) = @_;
98              
99             #debug 'string: '.$string;
100 0           my ($block) = reverse $$self{shell}{stringparser}->split('script_gram', $string);
101             #debug 'last block: '.$$block;
102 0 0         $block = '' unless ref $block;
103              
104             #debug "parsing block: $block";
105 0           $block = $$self{shell}->parse_block({pretend => 1}, $block);
106             #debug 'parsed last block: ', $block;
107 0 0         unless ($block) {
108 0   0       my $c = $$self{shell}{settings}{mode} || '_WORDS';
109 0 0         $c = uc $c unless $c =~ /::/;
110 0           return $string, [ {context => $c, string => '', poss => [], pref => ''}, ''];
111             }
112              
113 0           @{$$block[0]}{'poss', 'pref'} = ([], '');
  0            
114 0 0 0       if (exists $$block[0]{compl}) {
    0          
    0          
115 0           $$block[0]{context} = 'CMD';
116 0           @$block = ($$block[0], '_stub_', $$block[0]{compl});
117             }
118 0           elsif (@$block == 1) { push @$block, '' } # empty string
119 0           elsif ($$block[0]{string} =~ /\s$/ and $$block[-1] !~ /\s$/) { push @$block, '' }
120 0 0 0       $$block[0]{context} ||= (@$block == 2) ? '_WORDS' : 'CMD';
121              
122             # get pref right
123 0 0 0       if (length $$block[-1] and $string !~ s/\Q$$block[-1]\E$//) {
124 0           debug 'last word didn\'t match prefix';
125 0           my @words = $$self{shell}{stringparser}->split(
126             {was_regexp => 1, no_esc_rm => 1, tokens => [ [qr/\s/, '_CUT'] ]},
127             $string );
128 0           debug 'no_esc_rm resulted in: '.$words[-1];
129 0           $string =~ s/\Q$words[-1]\E$//;
130             }
131              
132 0           return ($string, $block);
133             }
134              
135             sub join_blocks {
136 0     0 0   my ($self, @blocks) = @_;
137 0           @blocks = grep {scalar @{$$_[0]{poss}}} @blocks;
  0            
  0            
138 0 0 0       return $blocks[0] || [{poss => []},''] if @blocks < 2;
139 0           my @poss = map {
140 0           my $b = $_;
141 0           ( map {$$b[0]{prefix}.$_} @{$$b[0]{poss}} )
  0            
  0            
142             } @blocks;
143 0           shift @{$blocks[0]};
  0            
144 0           return [{poss => \@poss}, @{$blocks[0]}];
  0            
145             }
146              
147             sub do { # FIXME with mode it is possible to have no words but context set to somethingdifferent from _WORDS => word_list should be checked
148 0     0 0   my ($self, $block, $try, @try) = @_;
149 0           debug "gonna try $try (".'i_'.lc($try).")";
150 0 0         return $block unless $try;
151 0           my @re;
152 0 0         if (ref($try) eq 'CODE') { @re = $try->($self, $block) }
  0 0          
    0          
    0          
153             elsif (exists $self->{shell}{parser}{$try}{intel}) {
154 0           @re = $self->{shell}{parser}{$try}{intel}->($block)
155             }
156             elsif (exists $self->{shell}{parser}{$try}{completion_function}) {
157 0           @re = $self->do_completion_function($try, $block);
158             }
159             elsif ($self->can('i_'.lc($try))) {
160 0           my $sub = 'i_'.lc($try);
161 0           @re = $self->$sub($block);
162             }
163             else {
164 0           debug $try.': no such expansion available';
165             }
166              
167 0 0         if (defined $re[0]) { ($block, @try) = (@re, @try) }
  0            
168 0 0         else { return @try ? $self->do($block, @try) : $block } # recurs
169              
170 0           my $succes = 0;
171 0 0         if (ref($$block[0]) eq 'ARRAY') {
172 0 0         $succes++ if grep {$$_[0]{poss} && @{$$_[0]{poss}}} @{$block}
  0 0          
  0            
  0            
173             }
174 0 0 0       else { $succes++ if $$block[0]{poss} && @{$$block[0]{poss}} }
  0            
175              
176 0 0         if ($succes) { return $block }
  0            
177 0 0         else { return scalar(@try) ? $self->do($block, @try) : $block } # recurs
178             }
179              
180             sub do_completion_function {
181 0     0 0   my ($self, $try, $block) = @_;
182 0 0         error "$try: no such context or no completion_function"
183             unless exists $$self{shell}{parser}{$try}{completion_function};
184 0           my ($line, $word) = ($$block[0]{zoidcmd}, $$block[-1]);
185 0           my $start = length($line) - length($word);
186 0           my $end = length($line);
187 0           debug qq#completion_function: $try line: "$line" word: "$word" from $start to $end#;
188 0           my @poss = $$self{shell}{parser}{$try}{completion_function}->($word, $line, $start);
189 0           $$block[0]{poss} = \@poss;
190 0           return $block;
191             }
192              
193             =back
194              
195             =head1 COMPLETIONS
196              
197             =over 4
198              
199             =cut
200              
201             sub i__words { # to expand the first word
202 0     0 0   my ($self, $block) = @_;
203              
204 0           my $arg = $block->[-1];
205 0           push @{$block->[0]{poss}}, grep /^\Q$arg\E/, keys %{$self->{shell}{aliases}};
  0            
  0            
206 0           push @{$block->[0]{poss}}, grep /^\Q$arg\E/, keys %{$self->{shell}{commands}};
  0            
  0            
207 0 0         push @{$block->[0]{poss}}, grep /^\Q$arg\E/, list_path() unless $arg =~ m#/#;
  0            
208              
209 0           my @blocks = ($self->i_dirs_n_files($block, 'x'));
210 0           my @alt;
211 0           for ($$self{shell}{parser}->stack('word_list')) {
212 0           my @re = $_->($block);
213 0 0         unless (@re) { next }
  0 0          
214             elsif (ref $re[0]) {
215 0           push @blocks, shift @re;
216 0           push @alt, @re;
217             }
218 0           else { push @{$block->[0]{poss}}, grep {defined $_} @re }
  0            
  0            
219             }
220 0           push @blocks, $block;
221              
222 0           return (\@blocks, @alt);
223             }
224              
225             sub i_cmd {
226 0     0 0   my ($self, $block) = @_;
227 0 0 0       if (! exists $$self{shell}{commands}{$$block[1]} and $$block[-1] =~ /^-/) {
    0          
    0          
228 0           return $block, 'man_opts';
229             }
230             elsif (exists $self->{config}{commands}{$$block[1]}) { # FIXME non compat with dispatch table
231 0           my $exp = $self->{config}{commands}{$$block[1]};
232 0 0         return $block, (ref($exp) ? (@$exp) : $exp), qw/env_vars dirs_n_files/;
233             }
234             elsif ($self->can('i_cmd_'.lc($$block[1]))) {
235 0           my $sub = 'i_cmd_'.lc($$block[1]);
236 0           return $self->$sub($block);
237             }
238 0           else { return $block, qw/env_vars dirs_n_files/ }
239             }
240              
241 0     0 0   sub i__end { i_dirs_n_files(@_) } # to expand after redirections
242              
243 0     0 0   sub i_dirs { i_dirs_n_files(@_, 'd') }
244 0     0 0   sub i_files { i_dirs_n_files(@_, 'f') }
245 0     0 0   sub i_exec { i_dirs_n_files(@_, 'x') }
246              
247             sub i_dirs_n_files { # types can be x, f, ans/or d # TODO globbing tab :)
248 0     0 0   my ($self, $block, $type) = @_;
249 0 0         $type = 'df' unless $type;
250              
251 0           my $arg = $block->[-1];
252 0 0         if ($arg =~ s/^(.*?(? or VAR=
253 0 0         $$block[0]{prefix} .= $1 unless $$block[0]{i_dirs_n_files};
254             }
255 0           $arg =~ s#\\##g;
256              
257 0           my $dir;
258 0 0 0       if ($arg =~ m#^~# && $arg !~ m#/#) { # expand home dirs
259 0 0         return unless $type =~ /d/;
260 0           push @{$$block[0]{poss}}, grep /^\Q$arg\E/, map "~$_/", list_users();
  0            
261 0           return $block;
262             }
263             else {
264 0 0         if ($arg =~ s!^(.*/)!!) {
265 0           $dir = path($1);
266 0           $block->[0]{prefix} .= $1;
267             }
268 0           else { $dir = '.' }
269 0 0         return undef unless -d $dir;
270             }
271 0           debug "Expanding files ($type) from dir: $dir with arg: $arg";
272              
273 0           my (@f, @d, @x);
274 0           for (grep /^\Q$arg\E/, list_dir($dir)) {
275 0 0         (-d "$dir/$_") ? (push @d, $_) :
    0          
276             (-x _) ? (push @x, $_) : (push @f, $_) ;
277             }
278            
279 0 0         my @poss = ($type =~ /f/) ? (sort @f, @x) : ($type =~ /x/) ? (@x) : ();
    0          
280 0           unshift @poss, map $_.'/', @d;
281              
282 0 0 0       @poss = grep {$_ !~ /^\./} @poss
  0            
283             if $$self{shell}{settings}{hide_hidden_files} && $arg !~ /^\./;
284              
285 0           push @{$$block[0]{poss}}, @poss;
  0            
286              
287 0           return $block;
288             }
289              
290 0     0 0   sub i_perl { return ($_[1], qw/_zoid env_vars dirs_n_files/) }
291             # FIXME how bout completing commands as subs ?
292              
293             #1;
294              
295             #__END__
296              
297             sub i__zoid {
298 0     0 0   my ($self, $block) = @_;
299              
300 0 0         return undef if $block->[0]{opts} =~ /z/; # FIXME will fail when default opts are used
301             return undef unless
302 0 0         $block->[-1] =~ /^( (?:\$shell)? ( (?:->|->|\xA3) (?:\S+->)* (?:[\[\{].*?[\]\}])* )) (\S*)$/x;
303 0           my ($pref, $code, $arg) = ($1, $2, qr/^\Q$3\E/);
304              
305 0           $code = '$self->{shell}' . $code;
306 0           $code =~ s/\xA3/->/;
307 0           $code =~ s/->$//;
308 0           my $ding = eval($code);
309 0           debug "$ding resulted from code: $code";
310 0           my $type = ref $ding;
311 0 0 0       if ($@ || ! $type) {
312 0 0         $$block[0]{message} = $@ if $@;
313 0           return $block;
314             }
315 0           else { $block->[0]{prefix} .= $pref }
316              
317 0           my @poss;
318 0 0         if ($type eq 'HASH') { push @poss, sort grep m/$arg/, map {'{'.$_.'}'} keys %$ding }
  0 0          
  0 0          
319 0           elsif ($type eq 'ARRAY') { push @poss, grep m/$arg/, map {'['.$_.']'} (0 .. $#$ding) }
  0            
320 0           elsif ($type eq 'CODE' ) { $block->[0]{message} = "\'$pref\' is a CODE reference" } # do nothing (?)
321             else { # $ding is object
322 0 0 0       if ( $type eq ref $$self{shell} and ! $$self{shell}{settings}{naked_zoid} ) {
323             # only display clothes
324 0           debug 'show zoid clothed';
325 0           push @poss, grep m/$arg/, @{ $$self{shell}->list_clothes };
  0            
326 0           push @poss, grep m/$arg/, sort keys %{ $$self{shell}{objects} };
  0            
327 0           $block->[0]{postf} = '->';
328             }
329             else {
330 0 0         if (UNIVERSAL::isa($ding, 'HASH')) {
    0          
331 0           push @poss, sort grep m/$arg/, map {'{'.$_.'}'} keys %$ding
  0            
332             }
333             elsif (UNIVERSAL::isa($ding, 'ARRAY')) {
334 0           push @poss, grep m/$arg/, map {'['.$_.']'} (0 .. $#$ding)
  0            
335             }
336              
337 0 0         unless ($arg =~ /[\[\{]/) {
338 1     1   4223 no strict 'refs';
  1         2  
  1         54  
339 0           my @isa = ($type);
340 0           my @m_poss;
341 0           while (my $c = shift @isa) {
342 1     1   5 no strict 'refs';
  1         2  
  1         1675  
343 0           push @m_poss, grep m/$arg/,
344 0           grep defined *{$c.'::'.$_}{CODE}, keys %{$c.'::'};
  0            
345 0           debug "class $c, ISA ", @{$c.'::ISA'};
  0            
346 0           push @isa, @{$c.'::ISA'};
  0            
347             }
348 0           push @poss, @m_poss;
349 0           $block->[0]{postf} = '(';
350             }
351             }
352             }
353              
354 0 0 0       @poss = grep {$_ !~ /^\{?_/} @poss
  0            
355             if $$self{shell}{settings}{hide_private_method} && $arg !~ /_/;
356 0           $$block[0]{poss} = \@poss;
357 0           $$block[0]{quoted}++;
358 0           return $block;
359             }
360              
361             sub i_env_vars {
362 0     0 0   my ($self, $block) = @_;
363 0 0         return undef unless $$block[-1] =~ /^(.*[\$\@])(\w*)$/;
364 0           $$block[0]{prefix} .= $1;
365 0 0         $$block[0]{poss} = $2 ? [ grep /^$2/, keys %ENV ] : [keys %ENV];
366 0           return $block;
367             }
368              
369 0     0 0   sub i_cdpath { # TODO
370             # for (@CDPATH) {
371             # }
372             }
373              
374             sub i_users { # TODO use this
375 0     0 0   my ($self, $block) = @_;
376 0           $block->[0]{poss} = [ grep /^\Q$block->[-1]\E/, list_users() ];
377 0           return $block;
378             }
379              
380             sub list_users {
381 0     0 0   my ($u, @users);
382 0           setpwent;
383 0           while ($u = getpwent) { push @users, $u }
  0            
384 0           return @users;
385             }
386              
387             sub i_man_opts { # TODO caching (tie classe die ook usefull is voor FileRoutines ?)
388 0     0 0   my ($self, $block) = @_;
389 0 0 0       return unless $$self{config}{man_cmd} && $$block[1];
390 0           debug "Going to open pipeline '-|', '$$self{config}{man_cmd}', '$$block[1]'";
391              
392             # re-route STDERR
393 0           open SAVERR, '>&STDERR';
394 0           open STDERR, '>', $Zoidberg::Utils::FileSystem::DEVNULL;
395              
396             # reset manpager
397 0           local $ENV{MANPAGER} = 'cat'; # FIXME is this portable ?
398              
399 0           open MAN, '-|', $$self{config}{man_cmd}, $$block[1];
400 0           my (%poss, @poss, $state, $desc);
401             # state 3 = new alinea
402             # 2 = still parsing options
403             # 1 = recoding description
404             # 0 = skipping
405 0           while () { # line based parsing ...
406 0 0         if ($state > 1) { # FIXME try to expand "zoid --help" & "zoid --usage"
    0          
407             # search for more options
408 0           s/\e.*?m//g; # de-ansi-fy
409 0           s/.\x08//g; # remove backspaces
410 0 0         unless (/^\s*-{1,2}\w/) { $state = ($state == 3) ? 0 : 1 }
  0 0          
411 0           else { $state = 2 }
412 0 0         $desc .= $_ if $state;
413 0 0         next unless $state > 1;
414 0 0         while (/(-{1,2}[\w-]+)/g) { push @poss, $1 unless exists $poss{$1} }
  0            
415             }
416             elsif ($state == 1) {
417 0 0         if (/\w/) { $desc .= $_ }
  0            
418             else {
419 0           $state = 3;
420             # backup description
421 0   0       my $copy = $desc || '';
422 0           for (@poss) { $poss{$_} = \$copy }
  0            
423 0           ($desc, @poss) = ('', ());
424             }
425             }
426 0 0         else { $state = 3 unless /\w/ }
427             }
428 0           close MAN;
429 0           open STDERR, '>&SAVERR';
430            
431 0           $block->[0]{poss} = [ grep /^\Q$$block[-1]\E/, sort keys %poss ];
432 0 0         if (@{$$block[0]{poss}} == 1) { $$block[0]{message} = ${$poss{$$block[0]{poss}[0]}} }
  0 0          
  0            
  0            
433 0           elsif (exists $poss{$$block[-1]}) { $$block[0]{message} = ${$poss{$$block[-1]}} }
  0            
434 0           $block->[0]{message} =~ s/[\s\n]+$//; #chomp it
435              
436 0           return $block;
437             }
438              
439             sub i_cmd_make { # TODO vars etc. -- see http://www.gnu.org/software/make/manual/make.html
440 0     0 0   my ($self, $block) = @_;
441 0           my ($mfile) = sort grep /makefile/i, list_dir();
442 0           $$block[0]{poss} = [];
443 0           debug "reading $mfile";
444 0           open MFILE, $mfile;
445 0           while () {
446 0 0         /^(\Q$$block[-1]\E\S*?)\s*:/ or next;
447 0           push @{$$block[0]{poss}}, $1;
  0            
448             }
449 0           close MFILE;
450 0           return $block;
451             }
452              
453             sub i_cmd_man {
454 0     0 0   my ($self, $block) = @_;
455 0 0         return $block, 'files' if $$block[-1] =~ m#/#;
456 0           my @manpath = split /:/, $ENV{MANPATH}; # in the environemnt we trust
457 0 0         my $section = $$block[-2] if @$block > 3;
458 0           my @pages;
459 0           for my $mdir (@manpath) {
460 0 0         next unless -d $mdir;
461 0           my @sect = list_dir($mdir);
462 0 0         @sect = grep /\Q$section\E/, @sect if $section;
463 0           for (@sect) {
464 0           my $dir = "$mdir/$_";
465 0 0         next unless -d $dir;
466 0           push @pages,
467 0           map { s/\..*$//; $_ }
  0            
468             grep /^\Q$$block[-1]\E/, list_dir($dir);
469             }
470             }
471 0           $$block[0]{poss} = [sort @pages];
472 0           return $block;
473             }
474              
475             =back
476              
477             =head1 CUSTOM COMPLETION
478              
479             FIXME
480              
481             =head1 AUTHOR
482              
483             Jaap Karssenberg || Pardus [Larus] Epardus@cpan.orgE
484              
485             Copyright (c) 2011 Jaap G Karssenberg and Joel Berger. All rights reserved.
486             This program is free software; you can redistribute it and/or
487             modify it under the same terms as Perl itself.
488              
489             =head1 SEE ALSO
490              
491             L, L,
492              
493             =cut
494              
495             1;
496