| 1 | #!/usr/bin/perl |
|---|
| 2 | |
|---|
| 3 | use strict; |
|---|
| 4 | use warnings FATAL => 'all'; |
|---|
| 5 | |
|---|
| 6 | package Shell::Complete; |
|---|
| 7 | |
|---|
| 8 | sub new |
|---|
| 9 | { |
|---|
| 10 | my $proto = shift; |
|---|
| 11 | my $class = ref($proto)? ref $proto: $proto; |
|---|
| 12 | my $self = bless {}, $proto; |
|---|
| 13 | $self->rebless; |
|---|
| 14 | return $self->init(@_); |
|---|
| 15 | } |
|---|
| 16 | |
|---|
| 17 | sub rebless |
|---|
| 18 | { |
|---|
| 19 | my $self = shift; |
|---|
| 20 | my $shell = $ENV{'SHELL'} || 'bash'; |
|---|
| 21 | $shell =~ s/^.*[\\\/]//; |
|---|
| 22 | $shell = ucfirst $shell; |
|---|
| 23 | my $class = 'Shell::Complete::'. $shell; |
|---|
| 24 | # eval "require $class"; |
|---|
| 25 | return warn("couldn't find $class") unless UNIVERSAL::can( $class, 'init' ); |
|---|
| 26 | if( ref( $self ) eq 'Shell::Complete' ) { |
|---|
| 27 | bless $self, $class; |
|---|
| 28 | } else { |
|---|
| 29 | my $cur = ref $self; |
|---|
| 30 | my @tmp = eval "map s/^Shell::Complete\$/\Q$class\E/, \@$cur\:\:ISA"; |
|---|
| 31 | warn $@ if( $@ ); |
|---|
| 32 | unless( grep $_, @tmp ) { |
|---|
| 33 | warn "$cur class doesn't inherit interface from Shell::Complete"; |
|---|
| 34 | } |
|---|
| 35 | } |
|---|
| 36 | return; |
|---|
| 37 | } |
|---|
| 38 | |
|---|
| 39 | sub init { return $_[0] } |
|---|
| 40 | |
|---|
| 41 | sub command { return $_[0]->{'command'} } |
|---|
| 42 | sub cur { return $_[0]->{'cur'} } |
|---|
| 43 | sub prev { return $_[0]->{'prev'} } |
|---|
| 44 | sub line { return $_[0]->{'line'} } |
|---|
| 45 | sub pos { return $_[0]->{'pos'} } |
|---|
| 46 | |
|---|
| 47 | sub _set_cur |
|---|
| 48 | { |
|---|
| 49 | my $self = shift; |
|---|
| 50 | my $new_cur = shift; |
|---|
| 51 | my $old_cur = $self->cur; |
|---|
| 52 | substr( $self->{'line'}, $self->pos - length($old_cur), length($old_cur) ) = |
|---|
| 53 | $new_cur; |
|---|
| 54 | $self->{'cur'} = $new_cur; |
|---|
| 55 | $self->{'pos'} += length($new_cur) - length($old_cur); |
|---|
| 56 | return $old_cur; |
|---|
| 57 | } |
|---|
| 58 | |
|---|
| 59 | sub args { return @{ $_[0]->{'args'} } } |
|---|
| 60 | |
|---|
| 61 | sub at_last_pos { return length($_[0]->line) == $_[0]->pos } |
|---|
| 62 | |
|---|
| 63 | sub is_option { return $_[0]->cur =~ /^-/ } |
|---|
| 64 | sub is_long_option { return $_[0]->cur =~ /^--/ } |
|---|
| 65 | sub options_strings { return (); } |
|---|
| 66 | |
|---|
| 67 | sub options |
|---|
| 68 | { |
|---|
| 69 | my $self = shift; |
|---|
| 70 | my $long = $self->is_long_option; |
|---|
| 71 | my @res = map { s/[^\w|].*$//; |
|---|
| 72 | split /\|/ |
|---|
| 73 | } $self->options_strings; |
|---|
| 74 | |
|---|
| 75 | @res = grep length() == 1, @res unless $long; |
|---|
| 76 | return map { length() == 1? "-$_": "--$_" } @res; |
|---|
| 77 | } |
|---|
| 78 | |
|---|
| 79 | sub filter_list |
|---|
| 80 | { |
|---|
| 81 | my ($self, @list) = @_; |
|---|
| 82 | my $cur = $self->cur; |
|---|
| 83 | return grep /^\Q$cur/, @list; |
|---|
| 84 | } |
|---|
| 85 | |
|---|
| 86 | sub output_list |
|---|
| 87 | { |
|---|
| 88 | my $self = shift; |
|---|
| 89 | print "$_\n" foreach $self->filter_list( @_ ); |
|---|
| 90 | return; |
|---|
| 91 | } |
|---|
| 92 | |
|---|
| 93 | |
|---|
| 94 | 1; |
|---|
| 95 | |
|---|
| 96 | package Shell::Complete::Bash; |
|---|
| 97 | |
|---|
| 98 | our @ISA = qw(Shell::Complete); |
|---|
| 99 | |
|---|
| 100 | sub init |
|---|
| 101 | { |
|---|
| 102 | my $self = shift; |
|---|
| 103 | $self->{'command'} = $ARGV[0] || die "empty \@ARGV"; |
|---|
| 104 | $self->{'cur'} = $ARGV[1] || ''; |
|---|
| 105 | $self->{'prev'} = $ARGV[2] || ''; |
|---|
| 106 | $self->{'line'} = $ENV{'COMP_LINE'} || die "empty \$ENV{COMP_LINE}"; |
|---|
| 107 | $self->{'pos'} = $ENV{'COMP_POINT'} || die "empty \$ENV{COMP_POINT}"; |
|---|
| 108 | my( $cmd, @args ) = split /\s+/, $self->{'line'}; |
|---|
| 109 | push @args, '' if $self->{'line'} =~ /\s+$/; |
|---|
| 110 | $self->{'args'} = [ @args ]; |
|---|
| 111 | return $self; |
|---|
| 112 | } |
|---|
| 113 | |
|---|
| 114 | 1; |
|---|
| 115 | |
|---|
| 116 | package Shell::Complete::Zsh; |
|---|
| 117 | |
|---|
| 118 | our @ISA = qw(Shell::Complete); |
|---|
| 119 | |
|---|
| 120 | sub init |
|---|
| 121 | { |
|---|
| 122 | my $self = shift; |
|---|
| 123 | $self->{'command'} = $ARGV[0] || die "empty \@ARGV"; |
|---|
| 124 | $self->{'cur'} = $ARGV[1] || ''; |
|---|
| 125 | $self->{'prev'} = $ARGV[2] || ''; |
|---|
| 126 | $self->{'line'} = $ENV{'BUFFER'} || die "empty \$ENV{BUFFER}"; |
|---|
| 127 | $self->{'pos'} = $ENV{'CURSOR'} || die "empty \$ENV{CURSOR}"; |
|---|
| 128 | my( $cmd, @args ) = split /\s+/, $self->{'line'}; |
|---|
| 129 | push @args, '' if $self->{'line'} =~ /\s+$/; |
|---|
| 130 | $self->{'args'} = [ @args ]; |
|---|
| 131 | return $self; |
|---|
| 132 | } |
|---|
| 133 | |
|---|
| 134 | 1; |
|---|
| 135 | |
|---|
| 136 | package SVK::Complete; |
|---|
| 137 | |
|---|
| 138 | our @ISA = qw(Shell::Complete); |
|---|
| 139 | |
|---|
| 140 | |
|---|
| 141 | sub svk_commands |
|---|
| 142 | { |
|---|
| 143 | my $self = shift; |
|---|
| 144 | unless( $self->{'svk_commands'} ) { |
|---|
| 145 | require SVK::Command; |
|---|
| 146 | my @cmds = $self->svk_command_aliases; |
|---|
| 147 | |
|---|
| 148 | require File::Find; |
|---|
| 149 | my $dir = $INC{"SVK/Command.pm"}; |
|---|
| 150 | $dir =~ s/\.pm$//; |
|---|
| 151 | File::Find::find(sub { push @cmds, lc() if s/\.pm$// }, $dir ); |
|---|
| 152 | |
|---|
| 153 | my %seen = (); |
|---|
| 154 | @cmds = grep !$seen{$_}++, @cmds; |
|---|
| 155 | $self->{'svk_commands'} = [ sort @cmds ]; |
|---|
| 156 | } |
|---|
| 157 | return @{ $self->{'svk_commands'} }; |
|---|
| 158 | } |
|---|
| 159 | |
|---|
| 160 | # only works for svk >1.04 |
|---|
| 161 | sub svk_command_aliases |
|---|
| 162 | { |
|---|
| 163 | my $self = shift; |
|---|
| 164 | unless( exists $self->{'svk_command_aliases'} ) { |
|---|
| 165 | require SVK::Command; |
|---|
| 166 | local $@; |
|---|
| 167 | my %cmds = (eval { SVK::Command->alias }); |
|---|
| 168 | $self->{'svk_command_aliases'} = \%cmds; |
|---|
| 169 | } |
|---|
| 170 | return %{ $self->{'svk_command_aliases'} }; |
|---|
| 171 | } |
|---|
| 172 | |
|---|
| 173 | sub svk_command |
|---|
| 174 | { |
|---|
| 175 | my( $self ) = @_; |
|---|
| 176 | unless( exists $self->{'svk_command'} ) { |
|---|
| 177 | my @args = $self->args; |
|---|
| 178 | my $cur = $self->cur; |
|---|
| 179 | if( scalar(@args) <= 1 ) { |
|---|
| 180 | $self->{'svk_command'} = undef; |
|---|
| 181 | } else { |
|---|
| 182 | my $cmd = $args[0] || ''; |
|---|
| 183 | $self->{'svk_command'} = (grep $_ eq $cmd, |
|---|
| 184 | $self->svk_commands |
|---|
| 185 | )? $cmd: ''; |
|---|
| 186 | my %alias = $self->svk_command_aliases; |
|---|
| 187 | $self->{'svk_command'} = $alias{ $cmd } if $alias{ $cmd }; |
|---|
| 188 | # old svk versions <= 1.04 doesn't allow to get aliases map |
|---|
| 189 | # but have util SVK::Command::_cmd_map |
|---|
| 190 | $self->{'svk_command'} ||= eval { SVK::Command::_cmd_map($cmd) } || ''; |
|---|
| 191 | } |
|---|
| 192 | } |
|---|
| 193 | return $self->{'svk_command'}; |
|---|
| 194 | } |
|---|
| 195 | |
|---|
| 196 | sub svk_command_obj |
|---|
| 197 | { |
|---|
| 198 | my $self = shift; |
|---|
| 199 | unless( exists $self->{'svk_command_obj'} ) { |
|---|
| 200 | my $cmd = $self->svk_command; |
|---|
| 201 | $cmd = "SVK::Command::". ucfirst($cmd); |
|---|
| 202 | eval "require $cmd"; |
|---|
| 203 | die $@ if $@; |
|---|
| 204 | $self->{'svk_command_obj'} = $cmd->new(); |
|---|
| 205 | } |
|---|
| 206 | return $self->{'svk_command_obj'}; |
|---|
| 207 | } |
|---|
| 208 | |
|---|
| 209 | sub options_strings |
|---|
| 210 | { |
|---|
| 211 | my $obj = $_[0]->svk_command_obj; |
|---|
| 212 | my %opt = eval{ $obj->command_options }; |
|---|
| 213 | %opt = $obj->options if $@; |
|---|
| 214 | return sort keys %opt; |
|---|
| 215 | } |
|---|
| 216 | |
|---|
| 217 | sub __svk_sys_path |
|---|
| 218 | { |
|---|
| 219 | require File::Spec; |
|---|
| 220 | return $ENV{SVKROOT} || File::Spec->catfile($ENV{HOME}, ".svk"); |
|---|
| 221 | } |
|---|
| 222 | |
|---|
| 223 | sub __svk_xd |
|---|
| 224 | { |
|---|
| 225 | my $self = shift; |
|---|
| 226 | my $svkpath = $self->__svk_sys_path; |
|---|
| 227 | require SVK::XD; require Data::Hierarchy; |
|---|
| 228 | my $xd = SVK::XD->new ( giantlock => File::Spec->catfile($svkpath, 'lock'), |
|---|
| 229 | statefile => File::Spec->catfile($svkpath, 'config'), |
|---|
| 230 | svkpath => $svkpath, |
|---|
| 231 | ); |
|---|
| 232 | $xd->load; |
|---|
| 233 | return $xd; |
|---|
| 234 | } |
|---|
| 235 | |
|---|
| 236 | sub is_svk_depotname |
|---|
| 237 | { |
|---|
| 238 | return $_[0]->cur =~ /^\/[^\/]*$/; |
|---|
| 239 | } |
|---|
| 240 | |
|---|
| 241 | sub svk_depotnames |
|---|
| 242 | { |
|---|
| 243 | return map "/$_/", sort keys %{$_[0]->__svk_xd->{depotmap}}; |
|---|
| 244 | } |
|---|
| 245 | |
|---|
| 246 | sub is_svk_depotpath |
|---|
| 247 | { |
|---|
| 248 | my $self = shift; |
|---|
| 249 | return unless $self->cur =~ /^(\/[^\/]*\/)/; |
|---|
| 250 | my $dn = $1; |
|---|
| 251 | return unless grep $dn eq $_, $self->svk_depotnames; |
|---|
| 252 | return 1; |
|---|
| 253 | } |
|---|
| 254 | |
|---|
| 255 | sub svk_depotpaths |
|---|
| 256 | { |
|---|
| 257 | my $self = shift; |
|---|
| 258 | my $cur = $self->cur; |
|---|
| 259 | $cur =~ s/[^\/]+$//; |
|---|
| 260 | my $xd = $self->__svk_xd; |
|---|
| 261 | |
|---|
| 262 | my $output = ''; |
|---|
| 263 | open my $sfh, ">:scalar", \$output or die; |
|---|
| 264 | require SVK::Command; |
|---|
| 265 | eval { require SVK::Path::Checkout }; |
|---|
| 266 | eval { require SVK::Target }; |
|---|
| 267 | SVK::Command->invoke($xd, 'list', $sfh, "-f", $cur ); |
|---|
| 268 | close( $sfh ); |
|---|
| 269 | |
|---|
| 270 | return split /\r*\n/, $output; |
|---|
| 271 | } |
|---|
| 272 | |
|---|
| 273 | 1; |
|---|
| 274 | |
|---|
| 275 | package main; |
|---|
| 276 | |
|---|
| 277 | my $complete = new SVK::Complete; |
|---|
| 278 | |
|---|
| 279 | # if we aren't at last pos, get out of here |
|---|
| 280 | exit unless $complete->at_last_pos; |
|---|
| 281 | |
|---|
| 282 | my @args = $complete->args; |
|---|
| 283 | |
|---|
| 284 | my $svk_command = $complete->svk_command; |
|---|
| 285 | |
|---|
| 286 | unless( defined $svk_command ) { |
|---|
| 287 | $complete->output_list( $complete->svk_commands ); |
|---|
| 288 | exit 0; |
|---|
| 289 | } |
|---|
| 290 | |
|---|
| 291 | unless( $svk_command ) { |
|---|
| 292 | print STDERR "\nwarning: unknown svk command\n"; |
|---|
| 293 | exit 0; |
|---|
| 294 | } |
|---|
| 295 | |
|---|
| 296 | if( $complete->is_option ) { |
|---|
| 297 | $complete->output_list( $complete->options ); |
|---|
| 298 | exit 0; |
|---|
| 299 | } |
|---|
| 300 | |
|---|
| 301 | if( $complete->is_svk_depotname ) { |
|---|
| 302 | my @list = $complete->filter_list( $complete->svk_depotnames ); |
|---|
| 303 | unless( @list == 1 ) { |
|---|
| 304 | $complete->output_list( @list ); |
|---|
| 305 | exit 0; |
|---|
| 306 | } |
|---|
| 307 | $complete->_set_cur( $list[0] ); |
|---|
| 308 | } |
|---|
| 309 | |
|---|
| 310 | if( $complete->is_svk_depotpath ) { while(1) { |
|---|
| 311 | my @list = $complete->filter_list( $complete->svk_depotpaths ); |
|---|
| 312 | unless( @list == 1 ) { |
|---|
| 313 | $complete->output_list( @list ); |
|---|
| 314 | exit 0; |
|---|
| 315 | } |
|---|
| 316 | $complete->_set_cur( $list[0] ); |
|---|
| 317 | } } |
|---|
| 318 | |
|---|
| 319 | |
|---|
| 320 | exit 0; |
|---|
| 321 | |
|---|