File Coverage

blib/lib/IO/Prompt/Simple.pm
Criterion Covered Total %
statement 143 147 97.2
branch 98 112 87.5
condition 23 41 56.1
subroutine 13 14 92.8
pod 1 1 100.0
total 278 315 88.2


line stmt bran cond sub pod time code
1             package IO::Prompt::Simple;
2              
3 12     12   361703 use strict;
  12         31  
  12         436  
4 12     12   66 use warnings;
  12         25  
  12         347  
5 12     12   323 use 5.006001;
  12         51  
  12         963  
6 12     12   67 use base 'Exporter';
  12         24  
  12         1236  
7              
8             BEGIN {
9 12 50   12   55362 $ENV{ANSI_COLORS_DISABLED} = 1 if $^O eq 'MSWin32';
10             }
11              
12             our $VERSION = '0.06';
13              
14             our @EXPORT = 'prompt';
15              
16             sub prompt {
17 43     43 1 52757 my ($message, $opts) = @_;
18 43 100       146 _croak('Usage: prompt($message, [$default_or_opts])') unless defined $message;
19              
20 42         60 my $default;
21 42 100       132 if (ref $opts eq 'HASH') {
22 32         75 $default = $opts->{default};
23             }
24             else {
25 10         22 ($default, $opts) = ($opts, {});
26             }
27 42 100       112 my $display_default = defined $default ? "[$default]" : '';
28 42 100       101 $default = defined $default ? $default : '';
29              
30 42         111 my $stash = { message => $message };
31 42         104 _parse_option($opts, $stash);
32              
33 42         42933 $stash->{message} .= " $display_default";
34 42 100       117 if (my $color = $opts->{color}) {
35 3         17 require Term::ANSIColor;
36 3 100       10 $color = [$color] unless ref $color eq 'ARRAY';
37 3         12 $stash->{message} = Term::ANSIColor::colored($color, $stash->{message});
38             }
39              
40 42         222 my ($in, $out) = @$stash{qw/in out/};
41              
42             # autoflush and reset format for output
43 42         614 my $org_out = select $out;
44 42         152 local $| = 1;
45 42         85 local $\;
46 42         105 select $org_out;
47              
48 42 100       108 my $ignore_case = $opts->{ignore_case} ? 1 : 0;
49 42         107 my $isa_tty = _isa_tty($in, $out);
50 42         124 my $answer;
51             my @answers; # for multi
52 42         47 while (1) {
53 52 100       137 print {$out} $stash->{choices}, "\n" if defined $stash->{choices};
  8         19  
54 52         62 print {$out} $stash->{message}, ': ';
  52         172  
55 52 100 100     389 if ($ENV{PERL_IOPS_USE_DEFAULT} || $opts->{use_default} || (!$isa_tty && eof $in)) {
      100        
      66        
56 4         25 print {$out} "$default\n";
  4         12  
57 4         9 $answer = $default;
58 4         8 last;
59             }
60 48         728 $answer = <$in>;
61 48 100       238 if (defined $answer) {
62 45         82 chomp $answer;
63 45 100       110 print {$out} "$answer\n" unless $isa_tty;
  1         3  
64             }
65             else {
66 3         4 print {$out} "\n";
  3         8  
67             }
68              
69 48 100 100     218 $answer = $default if !defined $answer || $answer eq '';
70 48 100       1478 $answer = $stash->{encoder}->decode($answer) if defined $stash->{encoder};
71 48 100       371 if (my $exclusive_map = $stash->{exclusive_map}) {
    100          
72 31 100       56 if ($stash->{want_multi}) {
73 3 50       7 $answer = $ignore_case ? lc $answer : $answer;
74 3         3 my $has_error;
75 3         9 for my $ans (split /\s+/, $answer) {
76 5 100       12 unless (exists $exclusive_map->{$ans}) {
77 1         1 $has_error = 1;
78 1         2 last;
79             }
80 4         10 push @answers, $exclusive_map->{$ans};
81             }
82 3 100       10 $has_error = 1 unless @answers;
83 3 100       9 last unless $has_error;
84             }
85             else {
86 28 100       85 if (exists $exclusive_map->{$ignore_case ? lc $answer : $answer}) {
    100          
87 20 100       42 $answer = $exclusive_map->{$ignore_case ? lc $answer : $answer};
88 20         28 last;
89             }
90             }
91 9         17 @answers = ();
92 9         12 $answer = undef;
93 9         12 print {$out} $stash->{hint};
  9         26  
94 9         18 next;
95             }
96             elsif (my $regexp = $stash->{regexp}) {
97 5 100       37 last if $answer =~ $regexp;
98 1         1 $answer = undef;
99 1         3 print {$out} $stash->{hint};
  1         3  
100 1         2 next;
101             }
102 12         21 last;
103             }
104              
105 42 100       891 return $stash->{want_multi} ? @answers : $answer;
106             }
107              
108             sub _parse_option {
109 42     42   66 my ($opts, $stash) = @_;
110              
111 42 100       134 $stash->{in} = _is_fh($opts->{input}) ? $opts->{input} : *STDIN;
112 42 100       129 $stash->{out} = _is_fh($opts->{output}) ? $opts->{output} : *STDOUT;
113              
114 42 100       117 if ($opts->{yn}) {
115 5         16 $opts->{anyone} = \[y => 1, n => 0];
116 5 100       17 $opts->{ignore_case} = 1 unless exists $opts->{ignore_case};
117             }
118              
119 42   100     165 $opts->{anyone} ||= $opts->{choices};
120 42 100       114 if ($opts->{anyone}) {
    100          
121 24         63 $stash->{exclusive_map} = _make_exclusive_map($opts, $stash);
122 24 100       91 $stash->{want_multi} = $opts->{multi} ? 1 : 0;
123             }
124             elsif ($opts->{regexp}) {
125 5         14 $stash->{regexp} = _make_regexp($opts, $stash);
126             }
127              
128 42 100       122 if ($opts->{encode}) {
129 1         9 require Encode;
130 1         12 $stash->{encoder} = Encode::find_encoding($opts->{encode});
131             }
132             }
133              
134             sub _make_exclusive_map {
135 24     24   32 my ($opts, $stash) = @_;
136 24         39 my $anyone = $opts->{anyone};
137 24         37 my $exclusive_map = {};
138              
139 24 100       63 my $ignore_case = $opts->{ignore_case} ? 1 : 0;
140 24         62 my ($message, $hint, $choices) = @$stash{qw/message hint choices/};
141 24   50     52 my $type = _anyone_type($anyone) || return;
142 24 100 66     139 if ($type eq 'ARRAY') {
    50 33        
143 6         16 my @stuffs = _uniq(@$anyone);
144 6         12 for my $stuff (@stuffs) {
145 12 100       585 $exclusive_map->{$ignore_case ? lc $stuff : $stuff} = $stuff;
146             }
147 6         42 $hint = sprintf "# Please answer %s\n", join ' or ', map qq{`$_`}, @stuffs;
148 6         56 $message .= sprintf ' (%s)', join '/', @stuffs;
149             }
150             elsif ($type eq 'HASH' || $type eq 'REFARRAY' || $type eq 'Hash::MultiValue') {
151 9         26 my @keys =
152             $type eq 'HASH' ? sort { $a cmp $b } keys %$anyone :
153 18 0       590 $type eq 'REFARRAY' ? do { my $i = 0; grep { ++$i % 2 == 1 } @{$$anyone} } :
  8 50       11  
  8 100       11  
  32         79  
  8         15  
154             $type eq 'Hash::MultiValue' ? $anyone->keys : ();
155 18         30 my $max = 0;
156 18         23 my $idx = 1;
157 18         35 for my $key (@keys) {
158 35 100       77 $max = length $key > $max ? length $key : $max;
159 35 100       125 $exclusive_map->{$ignore_case ? lc $key : $key} =
    100          
160             $type eq 'REFARRAY' ? $$anyone->[$idx] : $anyone->{$key};
161 35         78 $idx += 2;
162             }
163 18         115 $hint = sprintf "# Please answer %s\n", join ' or ',map qq{`$_`}, @keys;
164 18 100       63 if ($opts->{verbose}) {
165 6         7 my $idx = -1;
166 11         12 $choices = join "\n", map {
167 6         12 $idx += 2;
168 11 100       54 sprintf "# %-*s => %s", $max, $_,
169             $type eq 'REFARRAY' ? $$anyone->[$idx] : $anyone->{$_};
170             } @keys;
171             }
172             else {
173 12         45 $message .= sprintf ' (%s)', join '/', @keys;
174             }
175             }
176              
177 24         75 @$stash{qw/message hint choices/} = ($message, $hint, $choices);
178 24         75 return $exclusive_map;
179             }
180              
181             sub _anyone_type {
182 24     24   35 my $anyone = shift;
183             my $type =
184             ref $anyone eq 'ARRAY' && @$anyone ? 'ARRAY' :
185             ref $anyone eq 'HASH' && %$anyone ? 'HASH' :
186             ref $anyone eq 'REF' && ref $$anyone eq 'ARRAY' && @{$$anyone}
187             ? 'REFARRAY' :
188 24 0 66     750 do {
    50 66        
    100 33        
    100 0        
189             require Scalar::Util;
190             Scalar::Util::blessed($anyone) || ''
191             } eq 'Hash::MultiValue' && %$anyone
192             ? 'Hash::MultiValue' : '';
193 24         575 return $type;
194             }
195              
196             sub _make_regexp {
197 5     5   6 my ($opts, $stash) = @_;
198 5 100       58 my $regexp = ref $opts->{regexp} eq 'Regexp' ? $opts->{regexp}
    100          
199             : $opts->{ignore_case} ? qr/$opts->{regexp}/i : qr/$opts->{regexp}/;
200 5         24 $stash->{hint} = sprintf "# Please answer pattern %s\n", $regexp;
201 5         71 $regexp = qr/\A $regexp \Z/x;
202 5         23 return $regexp;
203             }
204              
205             # using IO::Interactive::is_interactive() ?
206             sub _isa_tty {
207 0     0   0 my ($in, $out) = @_;
208 0 0 0     0 return -t $in && (-t $out || !(-f $out || -c $out)) ? 1 : 0; ## no critic
209             }
210              
211             # taken from Test::Builder
212             sub _is_fh {
213 84     84   108 my $maybe_fh = shift;
214 84 100       248 return 0 unless defined $maybe_fh;
215              
216 64 50       338 return 1 if ref $maybe_fh eq 'GLOB'; # its a glob ref
217 0 0       0 return 1 if ref \$maybe_fh eq 'GLOB'; # its a glob
218              
219             return eval { $maybe_fh->isa('IO::Handle') }
220 0   0     0 || eval { tied($maybe_fh)->can('TIEHANDLE') };
221             }
222              
223             sub _uniq {
224 6     6   7 my %h;
225 6         37 grep !$h{$_}++, @_;
226             }
227              
228             sub _croak {
229 1     1   12 require Carp;
230 1         405 Carp::croak(@_);
231             }
232              
233             1;
234             __END__