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