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