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