File Coverage

blib/lib/App/Rakubrew/Shell.pm
Criterion Covered Total %
statement 37 180 20.5
branch 3 84 3.5
condition 1 63 1.5
subroutine 11 20 55.0
pod 0 8 0.0
total 52 355 14.6


line stmt bran cond sub pod time code
1             package App::Rakubrew::Shell;
2 3     3   65071 use strict;
  3         15  
  3         91  
3 3     3   16 use warnings;
  3         6  
  3         70  
4 3     3   64 use 5.010;
  3         9  
5 3     3   464 use File::Spec::Functions qw(catdir catfile updir splitpath);
  3         824  
  3         214  
6 3     3   484 use Try::Tiny;
  3         1976  
  3         162  
7 3     3   436 use App::Rakubrew::Tools;
  3         6  
  3         250  
8 3     3   444 use App::Rakubrew::Variables;
  3         7  
  3         441  
9 3     3   504 use App::Rakubrew::VersionHandling;
  3         7  
  3         421  
10              
11             # Turn on substring-based command line completion where possible contrary to the
12             # "start of the line completion". I.e., to visualize the difference, 'ver'
13             # string would result in the following command candidates:
14             # SUBSTRING_COMPLETION==1 -> version versions rakubrew-version
15             # SUBSTRING_COMPLETION==0 -> version versions
16 3     3   20 use constant SUBSTRING_COMPLETION => 1;
  3         7  
  3         5718  
17              
18             my $shell_hook;
19              
20             sub initialize {
21 7     7 0 3491 my $class = shift;
22 7         15 my $shell = shift;
23              
24 7 50 33     24 if (!shell_exists('Dummy self', $shell) || $shell eq 'auto') {
25 0         0 $shell = detect_shell();
26             }
27              
28 7         368 eval "require App::Rakubrew::Shell::$shell";
29 7 50       28 if ($@) {
30 0         0 die "Loading shell hook failed: " . $@;
31             }
32 7         55 $shell_hook = bless {}, "App::Rakubrew::Shell::$shell";
33 7         24 return $shell_hook;
34             }
35              
36             sub detect_shell {
37 0 0   0 0 0 if ($^O =~ /win32/i) {
38             # https://stackoverflow.com/a/8547234
39 0         0 my $psmodpath = $ENV{PSMODULEPATH};
40 0         0 my $userprofile = $ENV{USERPROFILE};
41 0 0       0 if (index($psmodpath, $userprofile) == 0) {
42 0         0 return 'PowerShell';
43             }
44             else {
45 0         0 return 'Cmd';
46             }
47             }
48             else {
49 0   0     0 my $shell = $ENV{'SHELL'} || '/bin/bash';
50 0         0 $shell = (splitpath( $shell))[2];
51 0         0 $shell =~ s/[^a-z]+$//; # remove version numbers
52              
53             # tcsh claims it's csh on FreeBSD. Try to detect that.
54 0 0 0     0 if ($shell eq 'csh' && $ENV{'tcsh'}) {
55 0         0 $shell = 'tcsh';
56             }
57              
58 0         0 $shell = ucfirst $shell;
59              
60 0 0       0 if (!shell_exists('Dummy self', $shell)) {
61 0         0 $shell = 'Sh';
62             }
63              
64 0         0 return $shell;
65             }
66             }
67              
68             sub get {
69 0     0 0 0 my $self = shift;
70 0         0 return $shell_hook;
71             }
72              
73             sub shell_exists {
74 7     7 0 14 my $self = shift;
75 7         11 my $shell = shift;
76              
77 7         395 eval "require App::Rakubrew::Shell::$shell";
78 7 50       75 return $@ ? 0 : 1;
79             }
80              
81             sub print_shellmod_code {
82 0     0 0   my $self = shift;
83 0           my @params = @_;
84 0   0       my $command = shift(@params) // '';
85 0           my $mode = get_brew_mode(1);
86 0           my $version;
87              
88 0 0         my $sep = $^O =~ /win32/i ? ';' : ':';
89              
90 0 0 0       if ($command eq 'shell' && @params) {
    0 0        
    0          
91 0           $version = $params[0];
92 0 0         if ($params[0] eq '--unset') {
    0          
93 0           say $self->get_shell_unsetter_code();
94             }
95             elsif (! is_version_broken($params[0])) {
96 0           say $self->get_shell_setter_code($params[0]);
97             }
98             }
99             elsif ($command eq 'mode' && $mode eq 'shim') { # just switched to shim mode
100 0           my $path = $ENV{PATH};
101 0           $path = $self->clean_path($path);
102 0           $path = $shim_dir . $sep . $path;
103 0           say $self->get_path_setter_code($path);
104             }
105             elsif ($mode eq 'env') {
106 0           $version = get_version();
107             }
108              
109 0 0         if ($mode eq 'env') {
110 0           my $path = $ENV{PATH};
111 0           $path = $self->clean_path($path);
112              
113 0 0         if ($version ne 'system') {
114 0 0         if ($version eq '--unset') {
115             # Get version ignoring the still set shell version.
116 0           $version = get_version('shell');
117             }
118 0 0         return if is_version_broken($version);
119 0           $path = join($sep, get_bin_paths($version), $path);
120             }
121              
122             # In env mode several commands require changing PATH, so we just always
123             # construct a new PATH and see if it's different.
124 0 0         if ($path ne $ENV{PATH}) {
125 0           say $self->get_path_setter_code($path);
126             }
127             }
128             }
129              
130             sub clean_path {
131 0     0 0   my $self = shift;
132 0           my $path = shift;
133 0           my $also_clean_path = shift;
134              
135 0 0         my $sep = $^O =~ /win32/i ? ';' : ':';
136              
137 0           my @paths;
138 0           for my $version (get_versions()) {
139 0 0         next if $version eq 'system';
140 0 0         next if is_version_broken($version);
141             try {
142 0     0     push @paths, get_bin_paths($version);
143             }
144       0     catch {
145             # Version is broken. So it's likely not in path anyways.
146             # -> ignore it
147 0           };
148             }
149 0           push @paths, $versions_dir;
150 0           push @paths, $shim_dir;
151 0 0         push @paths, $also_clean_path if $also_clean_path;
152 0           @paths = map { "\Q$_\E" } @paths;
  0            
153 0           my $paths_regex = join "|", @paths;
154              
155 0           my $old_path;
156 0           do {
157 0           $old_path = $path;
158 0           $path =~ s/^($paths_regex)[^$sep]*$//g;
159 0           $path =~ s/^($paths_regex)[^$sep]*$sep//g;
160 0           $path =~ s/$sep($paths_regex)[^$sep]*$//g;
161 0           $path =~ s/$sep($paths_regex)[^$sep]*$sep/$sep/g;
162             } until $path eq $old_path;
163 0           return $path;
164             }
165              
166             # Strips out all elements in arguments array up to and including $bre_name
167             # command. The first argument is index where the completion should look for the
168             # word to be completed.
169             sub strip_executable {
170 0     0 0   my $self = shift;
171 0           my $index = shift;
172              
173 0           my $cmd_pos = 0;
174 0           foreach my $word (@_) {
175 0           ++$cmd_pos;
176 0           --$index;
177 0 0         last if $word =~ /(^|\W)$brew_name$/;
178             }
179 0           return ($index, @_[$cmd_pos..$#_])
180             }
181              
182             =pod
183              
184             Returns a list of completion candidates.
185             This function takes two parameters:
186              
187             =over 4
188              
189             =item * Index of the word to complete, 0-based. If C<-1> is passed then list of all commands is returned.
190              
191             =item * A list of words already entered
192              
193             =back
194              
195             =cut
196              
197             sub _filter_candidates {
198 0     0     my $self = shift;
199 0           my $seed = shift;
200             return
201             # If a shell preserves ordering then put the prefix-mathing candidates first. I.e. for 'ver' 'version' would
202             # precede 'rakudo-version'
203 0           sort { index($a, $seed) cmp index($b, $seed) }
204             grep {
205 0           my $pos = index($_, $seed);
  0            
206 0           SUBSTRING_COMPLETION ? $pos >= 0 : $pos == 0
207             } @_
208             }
209              
210             sub get_completions {
211 0     0 0   my $self = shift;
212 0           my ($index, @words) = @_;
213              
214 0           my @commands = qw(version current versions list global switch shell local nuke unregister rehash available list-available build register build-zef download exec which whence mode self-upgrade triple test home rakubrew-version);
215              
216 0 0 0       if ($index <= 0) { # if @words is empty then $index == -1
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
217 0 0 0       my $candidate = $index < 0 || !$words[0] ? '' : $words[0];
218 0           my @c = $self->_filter_candidates($candidate, @commands, 'help');
219 0           return @c;
220             }
221             elsif($index == 1 && ($words[0] eq 'global' || $words[0] eq 'switch' || $words[0] eq 'shell' || $words[0] eq 'local' || $words[0] eq 'nuke' || $words[0] eq 'test')) {
222 0           my @versions = get_versions();
223 0 0         push @versions, 'all' if $words[0] eq 'test';
224 0 0         push @versions, '--unset' if $words[0] eq 'shell';
225 0   0       my $candidate = $words[1] // '';
226 0           return $self->_filter_candidates($candidate, @versions);
227             }
228             elsif($index == 1 && $words[0] eq 'build') {
229 0   0       my $candidate = $words[1] // '';
230 0           return $self->_filter_candidates($candidate, (App::Rakubrew::Variables::available_backends(), 'all'));
231             }
232             elsif($index == 2 && $words[0] eq 'build') {
233 0 0         my @installed = map { if ($_ =~ /^\Q$words[1]\E-(.*)$/) {$1} else { () } } get_versions();
  0            
  0            
  0            
234 0           my @installables = grep({ my $able = $_; !grep({ $able eq $_ } @installed) } App::Rakubrew::Build::available_rakudos());
  0            
  0            
  0            
235 0   0       my $candidate = $words[2] // '';
236 0           return $self->_filter_candidates($candidate, @installables);
237             }
238             elsif($index == 1 && $words[0] eq 'download') {
239 0   0       my $candidate = $words[1] // '';
240 0           return $self->_filter_candidates($candidate, ('moar'));
241             }
242             elsif($index == 2 && $words[0] eq 'download') {
243 0 0         my @installed = map { if ($_ =~ /^\Q$words[1]\E-(.*)$/) {$1} else { () } } get_versions();
  0            
  0            
  0            
244 0           my @installables = map { $_->{ver} } App::Rakubrew::Download::available_precomp_archives();
  0            
245 0           @installables = grep { my $able = $_; !grep({ $able eq $_ } @installed) } @installables;
  0            
  0            
  0            
246 0   0       my $candidate = $words[2] // '';
247 0           return $self->_filter_candidates($candidate, @installables);
248             }
249             elsif($index == 1 && $words[0] eq 'mode') {
250 0           my @modes = qw(env shim);
251 0   0       my $candidate = $words[2] // '';
252 0           return $self->_filter_candidates($candidate, @modes);
253             }
254             elsif($index == 2 && $words[0] eq 'register') {
255 0           my @completions;
256              
257 0           my $path = $words[2];
258 0           my ($volume, $directories, $file) = splitpath($path);
259 0           $path = catdir($volume, $directories, $file); # Normalize the path
260 0           my $basepath = catdir($volume, $directories);
261 0 0         opendir(my $dh, $basepath) or return '';
262 0           while (my $entry = readdir $dh) {
263 0           my $candidate = catdir($basepath, $entry);
264 0 0         next if $entry =~ /^\./;
265 0 0         next if substr($candidate, 0, length($path)) ne $path;
266 0 0         next if !-d $candidate;
267 0 0 0       $candidate .= '/' if length($candidate) > 0 && substr($candidate, -1) ne '/';
268 0           push @completions, $candidate;
269             }
270 0           closedir $dh;
271 0           return @completions;
272             }
273             elsif($index == 1 && $words[0] eq 'help') {
274 0   0       my $candidate = $words[1] // '';
275 0           my @topics = @commands;
276 0           push @topics, '--verbose';
277 0           return $self->_filter_candidates($candidate, @topics);
278             }
279             }
280              
281             1;