File Coverage

bin/glob
Criterion Covered Total %
statement 66 102 64.7
branch 26 56 46.4
condition 0 21 0.0
subroutine 14 18 77.7
pod n/a
total 106 197 53.8


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             =begin metadata
4              
5             Name: glob
6             Description: find pathnames matching a pattern
7             Author: Marc Mengel, mengel@fnal.gov
8             Author: brian d foy, briandfoy@pobox.com
9             License: perl
10              
11             =end metadata
12              
13             =cut
14              
15             package PerlPowerTools::glob;
16              
17 1     1   295314 use strict;
  1         2  
  1         32  
18 1     1   3 use warnings;
  1         0  
  1         71  
19              
20             our $VERSION = '2.1';
21              
22 1     1   5 use constant EX_SUCCESS => 0;
  1         1  
  1         50  
23 1     1   6 use constant EX_NO_MATCHES => 1;
  1         2  
  1         97  
24 1     1   4 use constant EX_FAILURE => 1;
  1         1  
  1         39  
25 1     1   8 use constant EX_ERROR => 2;
  1         1  
  1         37  
26 1     1   3 use constant IS_WINDOWS => ( $^O eq 'MSWin32' );
  1         0  
  1         36  
27              
28 1     1   3 use File::Basename;
  1         7  
  1         74  
29 1     1   5 use File::Glob qw(csh_glob GLOB_CSH);
  1         1  
  1         135  
30              
31             __PACKAGE__->run(@ARGV) unless caller();
32              
33             my %Unknown_users;
34             my $wildcards;
35 1     1   1122 BEGIN { $wildcards = qr/[*?\[]/ }
36              
37             sub run {
38 12     12   55219 my( $class, @args ) = @_;
39              
40 12         14 my( $code, $message ) = do {
41 12 100       40 if( @args == 0 ) {
    100          
42 1         2 ( EX_ERROR, undef );
43             }
44             elsif( $class->globbrace(@args) == 0 ) {
45 1         8 ( EX_FAILURE, $class->missing_brace_message );
46             }
47             else {
48 10         11 my $separator = "\n";
49 10 50       18 if( $args[0] eq '-0' ) {
50 0         0 shift @args;
51 0         0 $separator = "\0";
52             }
53              
54 10         11 my @ARGV_expanded = @args;
55 10         15 if( IS_WINDOWS ) {
56             @ARGV_expanded = map { expand_tilde($_) } @ARGV_expanded;
57             }
58              
59 10         2290 my @matches = csh_glob( "@ARGV_expanded", GLOB_CSH );
60              
61 10 100       44 if( @matches ) {
    50          
62 9         44 $class->output_list( \@matches, $separator );
63 9         76 ( EX_SUCCESS );
64             }
65             elsif( () = keys %Unknown_users ) {
66 0         0 ( EX_NO_MATCHES, $class->no_match_message );
67             }
68             else {
69 1         12 my( $message, $code ) = do {
70 1         2 my $pattern = "@args";
71 1         1 my $unknown = () = keys %Unknown_users;
72              
73 1 50       9 if( ! IS_WINDOWS && $pattern =~ /(?:\A|\s)~([\w-]+?)\b/ ) {
    50          
74 0 0       0 getpwnam($1) ? undef : "Unknown user $1.";
75             }
76             elsif( $pattern =~ $wildcards ) {
77 1         6 $class->no_match_message;
78             }
79 0         0 else { (undef, EX_FAILURE) }
80             };
81 1 50       3 $code = EX_NO_MATCHES unless defined $code;
82              
83 1         2 ( $code, $message );
84             }
85             }
86             };
87              
88 12         35 $class->exit( $code, $message );
89             }
90              
91             sub exit {
92 0     0   0 my( $class, $code, $message ) = @_;
93              
94 0 0       0 print STDERR $message if defined $message;
95 0 0       0 exit( defined $code ? $code : 0 );
96             }
97              
98             sub expand_tilde {
99 0     0   0 return $_[0] unless IS_WINDOWS;
100 0 0       0 return $_[0] unless $_[0] =~ $wildcards;
101 0         0 local $_ = $_[0];
102              
103 0         0 my $home = my_home();
104 0         0 my $dir = dirname($home);
105 0 0       0 $dir = '/Users' unless defined $dir;
106              
107 0 0       0 return $_ unless m/ \A ~ (\w+)? /x;
108 0         0 my $user = $1;
109              
110 0 0 0     0 if( $user && ! $Unknown_users{$user} ) {
111 0         0 my $net_user = `net user "$user" 2>&1`;
112 0 0       0 if( $net_user =~ /could not be found|The syntax of this command/ ) {
113 0         0 print STDERR "Unknown user $user.\n";
114 0         0 $Unknown_users{$user}++;
115 0         0 return;
116             }
117             s/ \A ~ (\w+) /$dir\\$1/x
118 0         0 }
119             else {
120 0         0 s/ \A ~ /$home/x;
121             }
122              
123 0         0 return $_;
124             }
125              
126             # https://github.com/aatrens-juniper/OpenBSD-src/blob/master/bin/csh/glob.c
127             sub globbrace {
128 11     11   28 my( $class, @args ) = @_;
129              
130 11         17 foreach my $s ( @args ) {
131 11         17 my $start = index $s, '{';
132 11 100       29 return 1 if $start == -1;
133              
134 5         6 my $counter = 0;
135 5         14 STRING: for( my $i = 0; $i < length($s); $i++ ) {
136 24 100       91 if( '[' eq substr($s, $i, 1) ) {
    100          
    100          
137 1         7 SQUARE: for( $i++; $i <= length($s); $i++ ) {
138 2 100       9 last SQUARE if ']' eq substr( $s, $i, 1 );
139 1 50       5 return 0 if $i == length $s;
140             }
141             }
142             elsif( '{' eq substr( $s, $i, 1 ) ) {
143 5         9 $counter++
144             }
145             elsif( '}' eq substr( $s, $i, 1 ) ) {
146 5 100       11 next if $counter == 0;
147 4         7 $counter--
148             }
149             }
150              
151 5 100       13 return 0 if $counter != 0;
152             }
153              
154 4         10 return 1;
155             }
156              
157              
158 1     1   3 sub missing_brace_message { q(Missing '}'.) }
159              
160             # Stolen from File::HomeDir::Windows;
161             sub my_home {
162             # A lot of unix people and unix-derived tools rely on
163             # the ability to overload HOME. We will support it too
164             # so that they can replace raw HOME calls with File::HomeDir.
165 0 0 0 0   0 if (exists $ENV{HOME} and defined $ENV{HOME} and length $ENV{HOME}) {
      0        
166 0         0 return $ENV{HOME};
167             }
168              
169             # Do we have a user profile?
170 0 0 0     0 if (exists $ENV{USERPROFILE} and $ENV{USERPROFILE}) {
171 0         0 return $ENV{USERPROFILE};
172             }
173              
174             # Some Windows use something like $ENV{HOME}
175 0 0 0     0 if (exists $ENV{HOMEDRIVE} and exists $ENV{HOMEPATH} and $ENV{HOMEDRIVE} and $ENV{HOMEPATH}) {
      0        
      0        
176 0         0 return File::Spec->catpath($ENV{HOMEDRIVE}, $ENV{HOMEPATH}, '',);
177             }
178              
179 0         0 return;
180             }
181              
182 1     1   4 sub no_match_message { 'glob: No match.' }
183              
184             sub output_list {
185 0     0     my( $class, $array, $separator ) = @_;
186 0 0         $separator = "\n" unless defined $separator;
187              
188 0           print STDOUT join $separator, @$array;
189 0           print "\n";
190             }
191              
192             =encoding utf8
193              
194             =head1 NAME
195              
196             glob - output pathnames matching a pattern
197              
198             =head1 SYNOPSIS
199              
200             On the command-line:
201              
202             glob 'eenie{meenie,mynie,moe}*.[ch]'
203              
204             =head1 DESCRIPTION
205              
206             When this program was originally created, *perl* did not have a builtin
207             C feature and would rely on the *csh* to do the work for it. With
208             Perl v5.6 in March 2000, the L module has done that work
209             without interacting with *csh*.
210              
211             =head2 Pattern Matching Syntax for Filename Expansion
212              
213             The expressions that are passed as arguments to B must adhere to
214             csh/tcsh pattern-matching syntax for wildcard filename expansion (also
215             known as I). Unquoted words containing an asterisk (C<*>),
216             question-mark (C), square-brackets (C<[...]>), or curly-braces (C<{...}>), or
217             beginning with a tilde (~), are expanded into an alphabetically sorted
218             list of filenames, as follows:
219              
220             =over 5
221              
222             =item C<*>
223              
224             Match any (zero or more) characters.
225              
226             =item C
227              
228             Match any single character.
229              
230             =item [...]
231              
232             Match any single character in the given character class. The character
233             class is the enclosed list(s) or range(s). A list is a string of
234             characters. A range is two characters separated by a dash (-), and
235             includes all the characters in between the two characters given
236             (inclusive). If a dash (C<->) is intended to be part of the character
237             class it must be the first character given.
238              
239             =item {str1,str2,...}
240              
241             Expand the given "word-set" to each string (or filename-matching
242             pattern) in the comma-separated list. Unlike the pattern-matching
243             expressions above, the expansion of this construct is not sorted. For
244             instance, C<{foo,bar}> expands to C (not C). As
245             special cases, unmatched C<{> and C<}>, and the "empty set" (the string
246             {}) are treated as ordinary characters instead of pattern-matching
247             meta-characters. A backslash (C<\)> may be used to escape an opening or
248             closing curly brace, or the backslash character itself. Note that
249             word-sets I be nested!
250              
251             =item C<~>
252              
253             The home directory of the invoking user as indicated by the value of
254             the variable C<$HOME>.
255              
256             =item ~username
257              
258             The home directory of the user whose login name is 'username',
259             as indicated by the password entry for the named user.
260              
261             =back
262              
263             Only the patterns *, ? and [...] imply pattern matching; an error
264             results if no filename matches a pattern that contains them. When
265             a period or "dot" (.) is the first character in a filename or
266             pathname component, it must be matched explicitly. The filename
267             component separator character (e.g., / or slash) must also
268             be matched explicitly.
269              
270             =head1 OPTIONS
271              
272             When the first argument is B<-0> (a minus sign followed by the number
273             zero), then a NUL character ("\0") is used to separate the expanded
274             words and/or filenames when printing them to standard output.
275             Otherwise a newline is used as the word/filename output separator.
276              
277             =head1 RETURNS
278              
279             When B is invoked as a script from the command-line, the exit-status
280             returned will be 0 if any files were matched or word-sets were expanded;
281             1 if no files/word-sets were matched/expanded; and 2 if some other kind of
282             error occurred.
283              
284             =head1 DIAGNOSTICS
285              
286             If no filenames are matched and pattern-matching characters were used
287             (C<*>, C, or C<[...]>), then an error message of "No Match" is issued. If a
288             user's home directory is specified using tilde-expansion (e.g., C<~username>)
289             but the corresponding username or their home directory cannot be found,
290             then the error message "Unknown user: username" is issued.
291              
292             =head1 COPYRIGHT
293              
294             Copyright (c) 1997-2025 Marc Mengel. All rights reserved.
295              
296             This library is free software; you can redistribute it and/or modify it under
297             the same terms as Perl itself.
298              
299             =head1 AUTHOR
300              
301             Marc Mengel EFE
302              
303             =head1 REVISIONS
304              
305             =over 4
306              
307             =item brian f foy EFE - v2.1 February 2025
308              
309             Reimplement this as a thin layer over L. This
310             program was written before that was a core module, but had several
311             edge cases where it would crash.
312              
313             =item Brad Appleton EFE - v1.2 March 1999
314              
315             Modified to use qr// (and some other minor speedups), to explode
316             subexpressions in curly braces (a la csh -- rather than using just
317             plain alternation), and made callable as a standalone script.
318              
319             =back
320              
321             =cut
322