]> Dogcows Code - chaz/git-codeowners/blob - lib/App/Codeowners/Options.pm
add projects command and filtering to show command
[chaz/git-codeowners] / lib / App / Codeowners / Options.pm
1 package App::Codeowners::Options;
2 # ABSTRACT: Getopt and shell completion for App::Codeowners
3
4 use warnings;
5 use strict;
6
7 use Getopt::Long 2.39 ();
8 use Path::Tiny;
9 use Pod::Usage;
10
11 our $VERSION = '9999.999'; # VERSION
12
13 sub early_options {
14 return {
15 'color|colour!' => (-t STDOUT ? 1 : 0), ## no critic (InputOutput::ProhibitInteractiveTest)
16 'format|f=s' => undef,
17 'help|h|?' => 0,
18 'manual|man' => 0,
19 'shell-completion:s' => undef,
20 'version|v' => 0,
21 };
22 }
23
24 sub command_options {
25 return {
26 'create' => {},
27 'owners' => {
28 'pattern=s' => '',
29 },
30 'patterns' => {
31 'owner=s' => '',
32 },
33 'projects' => {},
34 'show' => {
35 'owner=s@' => [],
36 'pattern=s@' => [],
37 'project=s@' => [],
38 'patterns!' => 0,
39 'projects!' => undef,
40 },
41 'update' => {},
42 };
43 }
44
45 sub commands {
46 my $self = shift;
47 my @commands = sort keys %{$self->command_options};
48 return @commands;
49 }
50
51 sub options {
52 my $self = shift;
53 my @command_options;
54 if (my $command = $self->{command}) {
55 @command_options = keys %{$self->command_options->{$command} || {}};
56 }
57 return (keys %{$self->early_options}, @command_options);
58 }
59
60 sub new {
61 my $class = shift;
62 my @args = @_;
63
64 my $self = bless {}, $class;
65
66 my @args_copy = @args;
67
68 my $opts = $self->get_options(
69 args => \@args,
70 spec => $self->early_options,
71 config => 'pass_through',
72 ) or pod2usage(2);
73
74 if ($ENV{CODEOWNERS_COMPLETIONS}) {
75 $self->{command} = $args[0] || '';
76 my $cword = $ENV{CWORD};
77 my $cur = $ENV{CUR} || '';
78 # Adjust cword to remove progname
79 while (0 < --$cword) {
80 last if $cur eq ($args_copy[$cword] || '');
81 }
82 $self->completions($cword, @args_copy);
83 exit 0;
84 }
85
86 if ($opts->{version}) {
87 my $progname = path($0)->basename;
88 print "${progname} ${VERSION}\n";
89 exit 0;
90 }
91 if ($opts->{help}) {
92 pod2usage(-exitval => 0, -verbose => 99, -sections => [qw(NAME SYNOPSIS OPTIONS COMMANDS)]);
93 }
94 if ($opts->{manual}) {
95 pod2usage(-exitval => 0, -verbose => 2);
96 }
97 if (defined $opts->{shell_completion}) {
98 $self->shell_completion($opts->{shell_completion});
99 exit 0;
100 }
101
102 # figure out the command (or default to "show")
103 my $command = shift @args;
104 my $command_options = $self->command_options->{$command || ''};
105 if (!$command_options) {
106 unshift @args, $command if defined $command;
107 $command = 'show';
108 $command_options = $self->command_options->{$command};
109 }
110
111 my $more_opts = $self->get_options(
112 args => \@args,
113 spec => $command_options,
114 ) or pod2usage(2);
115
116 %$self = (%$opts, %$more_opts, command => $command, args => \@args);
117 return $self;
118 }
119
120 sub command {
121 my $self = shift;
122 my $command = $self->{command};
123 my @commands = sort keys %{$self->command_options};
124 return if not grep { $_ eq $command } @commands;
125 $command =~ s/[^a-z]/_/g;
126 return $command;
127 }
128
129 sub args {
130 my $self = shift;
131 return @{$self->{args} || []};
132 }
133
134 =method get_options
135
136 $options = $options->get_options(
137 args => \@ARGV,
138 spec => \@expected_options,
139 callback => sub { my ($arg, $results) = @_; ... },
140 );
141
142 Convert command-line arguments to options, based on specified rules.
143
144 Returns a hashref of options or C<undef> if an error occurred.
145
146 =for :list
147 * C<args> - Arguments from the caller (e.g. C<@ARGV>).
148 * C<spec> - List of L<Getopt::Long> compatible option strings.
149 * C<callback> - Optional coderef to call for non-option arguments.
150 * C<config> - Optional L<Getopt::Long> configuration string.
151
152 =cut
153
154 sub get_options {
155 my $self = shift;
156 my $args = {@_ == 1 && ref $_[0] eq 'HASH' ? %{$_[0]} : @_};
157
158 my %options;
159 my %results;
160 while (my ($opt, $default_value) = each %{$args->{spec}}) {
161 my ($name) = $opt =~ /^([^=:!|]+)/;
162 $name =~ s/-/_/g;
163 $results{$name} = $default_value;
164 $options{$opt} = \$results{$name};
165 }
166
167 if (my $fn = $args->{callback}) {
168 $options{'<>'} = sub {
169 my $arg = shift;
170 $fn->($arg, \%results);
171 };
172 }
173
174 my $p = Getopt::Long::Parser->new;
175 $p->configure($args->{config} || 'default');
176 return if !$p->getoptionsfromarray($args->{args}, %options);
177
178 return \%results;
179 }
180
181 =method shell_completion
182
183 $options->shell_completion($shell_type);
184
185 Print shell code to C<STDOUT> for the given type of shell. When eval'd, the shell code enables
186 completion for the F<git-codeowners> command.
187
188 =cut
189
190 sub shell_completion {
191 my $self = shift;
192 my $type = lc(shift || 'bash');
193
194 if ($type eq 'bash') {
195 print <<'END';
196 # git-codeowners - Bash completion
197 # To use, eval this code:
198 # eval "$(git-codeowners --shell-completion)"
199 # This will work without the bash-completion package, but handling of colons
200 # in the completion word will work better with bash-completion installed and
201 # enabled.
202 _git_codeowners() {
203 local cur words cword
204 if declare -f _get_comp_words_by_ref >/dev/null
205 then
206 _get_comp_words_by_ref -n : cur cword words
207 else
208 words=("${COMP_WORDS[@]}")
209 cword=${COMP_CWORD}
210 cur=${words[cword]}
211 fi
212 local IFS=$'\n'
213 COMPREPLY=($(CODEOWNERS_COMPLETIONS=1 CWORD="$cword" CUR="$cur" ${words[@]}))
214 # COMPREPLY=($(${words[0]} --completions "$cword" "${words[@]}"))
215 if [[ "$?" -eq 9 ]]
216 then
217 COMPREPLY=($(compgen -A "${COMPREPLY[0]}" -- "$cur"))
218 fi
219 declare -f __ltrim_colon_completions >/dev/null && \
220 __ltrim_colon_completions "$cur"
221 return 0
222 }
223 complete -F _git_codeowners git-codeowners
224 END
225 }
226 else {
227 # TODO - Would be nice to support Zsh
228 warn "No such shell completion: $type\n";
229 }
230 }
231
232 =method completions
233
234 $options->completions($current_arg_index, @args);
235
236 Print completions to C<STDOUT> for the given argument list and cursor position, and exit.
237
238 May also exit with status 9 and a compgen action printed to C<STDOUT> to indicate that the shell
239 should generate its own completions.
240
241 Doesn't return.
242
243 =cut
244
245 sub completions {
246 my $self = shift;
247 my $cword = shift;
248 my @words = @_;
249
250 my $current = $words[$cword] || '';
251 my $prev = $words[$cword - 1] || '';
252
253 my $reply;
254
255 if ($prev eq '--format' || $prev eq '-f') {
256 $reply = $self->_completion_formats;
257 }
258 elsif ($current =~ /^-/) {
259 $reply = $self->_completion_options;
260 }
261 else {
262 if (!$self->command) {
263 $reply = [$self->commands, @{$self->_completion_options([keys %{$self->early_options}])}];
264 }
265 else {
266 print 'file';
267 exit 9;
268 }
269 }
270
271 local $, = "\n";
272 print grep { /^\Q$current\E/ } @$reply;
273 exit 0;
274 }
275
276 sub _completion_options {
277 my $self = shift;
278 my $opts = shift || [$self->options];
279
280 my @options;
281
282 for my $option (@$opts) {
283 my ($names, $op, $vtype) = $option =~ /^([^=:!]+)([=:!]?)(.*)$/;
284 my @names = split(/\|/, $names);
285
286 for my $name (@names) {
287 if ($op eq '!') {
288 push @options, "--$name", "--no-$name";
289 }
290 else {
291 if (length($name) > 1) {
292 push @options, "--$name";
293 }
294 else {
295 push @options, "-$name";
296 }
297 }
298 }
299 }
300
301 return [sort @options];
302 }
303
304 sub _completion_formats { [qw(csv json json:pretty tsv yaml)] }
305
306 1;
This page took 0.046183 seconds and 4 git commands to generate.