File Coverage

bin/date
Criterion Covered Total %
statement 63 99 63.6
branch 11 38 28.9
condition 3 9 33.3
subroutine 11 15 73.3
pod n/a
total 88 161 54.6


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2 1     1   4765 use strict;
  1         2  
  1         52  
3 1     1   428 use subs qw(core_time);
  1         226  
  1         9  
4              
5             =begin metadata
6              
7             Name: date
8             Description: display or set date and time
9             Author: brian d foy, brian.d.foy@gmail.com
10             Author: Joshua Gross
11             License: artistic2
12              
13             =end metadata
14              
15             =cut
16              
17 1     1   525 use POSIX;
  1         7396  
  1         6  
18              
19 1         182748 my $VERSION = '1.0.5';
20              
21             # set this if we do anything to select a timezone abbreviation, then
22             # prefer this if it is defined.
23 1         3 my $TZ;
24              
25             # run if called directly, indirectly, directly par-packed, undirectly par-packed
26 1 0 33     10 run(\@ARGV) if !caller() || caller(0) =~ /^(PerlPowerTools::Packed|PAR)$/ || caller(1) eq 'PAR';
      33        
27              
28             sub get_formats {
29             {
30 2     2   13 "--rfc-3339" => '%Y-%m-%d %H:%M:%S%z', # iso-8601 without T
31             "--rfc-5322" => '%a, %d %b %Y %H:%M:%S %z',
32             "-I" => '%Y-%m-%dT%H:%M:%S%:z', # iso-8601
33             "-R" => '%a, %d %b %Y %T %z', # rfc-2822
34             "default" => '%a %b %e %T %Z %Y',
35             };
36             }
37              
38             sub munge_tz {
39 1 50   1   9 return $TZ if defined $TZ; # something else we did set this, so use it
40 0         0 my $from_posix = posix_tz();
41 0 0       0 return $from_posix if $from_posix =~ m/\A[A-Z]{3,4}\z/;
42 0         0 my $from_windows = windows_time_zones();
43 0 0       0 my $i_dst = (core_time())[-1] ? -1 : 0; # some only have one entry, so two would be the same
44              
45 0 0       0 return $from_windows->{$from_posix}[$i_dst] if exists $from_windows->{$from_posix};
46             }
47              
48 0     0   0 sub posix_tz { POSIX::strftime( '%Z', core_time() ) }
49              
50 1     1   3 sub quarter { int((core_time())[4] / 3) + 1 }
51              
52             sub run {
53 1     1   3 my $args = shift;
54 1         4 *core_time = do {
55 1         67 my @times = CORE::localtime;
56 0     0   0 sub { @times }
57 1         16 };
58              
59 1 50       3 if( grep { $_ eq '-h' } @$args ) { usage(0) }
  1 50       16  
  0         0  
60 1         5 elsif( grep { $_ eq '-v' } @$args ) { print "$0 $VERSION\n"; exit 0; }
  0         0  
  0         0  
61              
62 1         2 my $formats = eval { get_formats() };
  1         4  
63 1         2 my $at = $@;
64 1 50       3 if( length $at ) {
65 0         0 print $at;
66 0         0 exit 1;
67             }
68              
69 1         3 my %allowed = map { ("-$_" => 1) } split( //, 'u' );
  1         4  
70 1         7 @allowed{ keys %$formats } = (1) x (keys %$formats);
71              
72 1         3 foreach (@$args) {
73 1 50 33     8 if( /^-/ and ! exists $allowed{$_} ) {
74 0         0 print STDERR "Unrecognized option $_\n";
75 0         0 usage(2);
76             }
77              
78 1 50   39   8 if( /^\-u/ ) { $TZ = $ENV{'TZ'} = 'UTC'; my @times = CORE::gmtime; *core_time = sub { @times } }
  1         10  
  1         3  
  1         9  
  39         262  
79             }
80              
81 1         3 my $format = select_format(@$args);
82 1         3 my $specifiers = setup_specifiers();
83              
84 1 50       6 $format =~ s/%(:?.)/ exists $specifiers->{$1} ? $specifiers->{$1} : "%$1" /eg;
  6         15  
85 1         0 print "$format\n";
86             }
87              
88             sub select_format {
89 1     1   2 my @format_args = grep { /\A\+/ } @_;
  1         3  
90 1 50       3 die "Extra operands: " . join( ' ', @format_args[1..$#format_args] ) . "\n"
91             if @format_args > 1;
92              
93 1 50       3 return $1 if $format_args[0] =~ /\A\+(.+)/;
94              
95 1         2 my $formats = get_formats();
96              
97             (
98 1 50       5 map { $formats->{$_} || () }
99 1         2 grep { exists $formats->{$_} }
  2         3  
100             ( @ARGV, 'default' )
101             )[0]
102             }
103              
104             sub setup_specifiers {
105             my %specifiers = (
106             'e' => sprintf( '%2d', (core_time)[3] ),
107             'P' => lc(POSIX::strftime('%p', core_time())),
108             'q' => quarter(),
109             'T' => sprintf( '%02d:%02d:%02d', (core_time)[2,1,0] ),
110             'z' => tz_offset(),
111 1     1   3 ':z' => do { ( my $z = tz_offset() ) =~ s/(\d\d)/$1:/; $z },
  1         2  
  1         5  
112             'Z' => munge_tz(),
113             );
114              
115             # We cheat by letting POSIX figure these out because it can handle the
116             # locale. If we later find out that some system doesn't handle one of these,
117             # we'll define our own. But, only add a POSIX cheat if we haven't already
118             # defined that format.
119 1         13 my @POSIX = qw(
120             a A b B c C d D F g G h H I j k l m M
121             n p r R s S t u U V w W x X y Y
122             );
123             @specifiers{ @POSIX } =
124 35         50 map { POSIX::strftime( "%$_", core_time() ) }
125 1         2 grep { ! exists $specifiers{$_} }
  35         28  
126             @POSIX;
127              
128 1         5 \%specifiers;
129             }
130              
131             sub tz_offset {
132             # https://stackoverflow.com/a/6428732/2766176
133 2     2   5 my @l = CORE::localtime;
134 2         4 my @g = CORE::gmtime;
135              
136 2         8 my $minutes = (
137             $l[2] - $g[2] +
138             ( # is this the same year or day? No? Add or subtract 24 hours
139             (($l[5]<<9)|$l[7]) <=> (($g[5]<<9)|$g[7])) * 24
140             ) * 60
141             + $l[1] - $g[1];
142              
143 2 50       9 my $sign = $minutes < 0 ? '-' : "+";
144 2         3 $minutes = abs($minutes);
145              
146 2         17 sprintf "%s%02d%02d", $sign, int($minutes / 60), $minutes % 60;
147             }
148              
149             sub usage {
150 0 0   0     my $exit_code = defined $_[0] ? $_[0] : 0;
151              
152 0           my $output = <<"HEADER";
153             usage: $0 [-hIRuv] [+format]
154              
155             Formats:
156             HEADER
157              
158 0           open my $fh, '<:encoding(UTF-8)', __FILE__;
159 0           while( <$fh> ) {
160 0 0         next unless s/\A=item \s+ \* \s+(?=%)//x;
161 0           $output .= "\t$_";
162             }
163              
164 0           print "$output\n";
165 0           exit( $exit_code );
166             }
167              
168             sub windows_time_zones {
169 0     0     my %hash;
170              
171 0           open my $fh, '<:encoding(UTF-8)', __FILE__;
172 0           while( <$fh> ) {
173 0 0         next unless /\A__(?:END|DATA)__/;
174 0           last;
175             }
176              
177 0           while( <$fh> ) {
178 0           chomp;
179 0           s/\s*#.*//;
180 0 0         next unless /\S/;
181 0           my( $windows_name, $tz, @names ) = split /\s*,\s*/;
182 0           @names = sort @names; # capital letters sort before + - digits!
183 0           $hash{$windows_name} = \@names;
184             }
185              
186 0           close $fh;
187              
188 0           return \%hash;
189             }
190              
191             =encoding utf8
192              
193             =head1 NAME
194              
195             date - display date and time
196              
197             =head1 SYNOPSIS
198              
199             # show the local date in the default format
200             % date
201              
202             # show the UTC date in the default format
203             % date -u
204              
205             # display version, help
206             % date -v
207             % date -h
208              
209             # show the local date in the specified format
210             % date +FORMAT
211              
212             # show the GMT date in the specified format
213             % date -u +FORMAT
214              
215             # show local date in ISO 8601 format
216             % date -I
217              
218             # show local date in RFC 2822 format
219             % date -R
220              
221              
222             =head1 DESCRIPTION
223              
224             =head2 Options
225              
226             =over 4
227              
228             =item * +FORMAT
229              
230             Specify the date format
231              
232             =item * -h
233              
234             Show the help message and exit
235              
236             =item * -I
237              
238             Use the ISO 8601 date format: C<%Y-%m-%dT%H:%M:%S%:z>
239              
240             =item * -R
241              
242             Use RFC 2822 format: C<%a, %d %b %Y %T %z>
243              
244             =item * -u
245              
246             Use UTC time instead of local time
247              
248             =item * -v
249              
250             Show the version and exit
251              
252             =back
253              
254             =head2 Formats
255              
256             =over 4
257              
258             =item * %% - The character %
259              
260             =item * %a - Three-letter weekday name
261              
262             =item * %A - Full weekday name
263              
264             =item * %b - Three-letter month name
265              
266             =item * %B - Full month name
267              
268             =item * %c - locale version of the date-time string
269              
270             =item * %C - Century (00-99)
271              
272             =item * %d - Day of month (padded w/ zero)
273              
274             =item * %D - Date in MM/DD/YY format
275              
276             =item * %e - Day of month (padded w/ space)
277              
278             =item * %F - %Y-%m-%d
279              
280             =item * %g - ISO 8601 year
281              
282             =item * %G - ISO 8601 year
283              
284             =item * %h - Three-letter month name
285              
286             =item * %H - Hour HH
287              
288             =item * %I - Hour HH (12 hour)
289              
290             =item * %j - Three-digit Julian day
291              
292             =item * %k - Hour - space padded
293              
294             =item * %l - Hour - space padded (12 hour)
295              
296             =item * %m - Month number 01-12
297              
298             =item * %M - Minute MM
299              
300             =item * %n - Newline
301              
302             =item * %p - AM or PM
303              
304             =item * %P - like %p, but lowercase
305              
306             =item * %q - quarter of the year (1-4)
307              
308             =item * %r - Time in HH(12 hour):MM:SS (AM|PM) format
309              
310             =item * %R - Time in HH:MM format
311              
312             =item * %s - Absolute seconds (since epoch)
313              
314             =item * %S - Seconds SS
315              
316             =item * %t - Tab
317              
318             =item * %T - Time in HH:MM:SS format.
319              
320             =item * %u - Day of week, 1=Monday, 7=Sunday.
321              
322             =item * %U - Two digit week number, starting on Sunday.
323              
324             =item * %V - ISO week number, with Monday as the first day of week
325              
326             =item * %w - Day of week, 0=Sunday, 6=Saturday.
327              
328             =item * %W - Two digit week number, start Monday.
329              
330             =item * %x - locale's date representation
331              
332             =item * %X - locale's time representation
333              
334             =item * %y - Two-digit year.
335              
336             =item * %Y - Four-digit year.
337              
338             =item * %z - Time zone offset in [+-]HHMM.
339              
340             =item * %:z - Time zone offset in [+-]HH:MM.
341              
342             =item * %Z - Time zone abbrevation, such as UTC or EST.
343              
344             Note: Windows does not use the POSIX time stuff, so we try to fake it.
345             If you don't get what you expect for the time zone abbreviation
346             please open an issue: L.
347             If you look in the C file, you'll see a simple text list at the end
348             that we use for Windows.
349              
350             =back
351              
352             =cut
353              
354             # If any of these are wrong, just send the patch or raise an issue
355              
356             =begin comment
357              
358             Windows is a bit of a pain here. We want to support %Z, but Windows
359             doesn't track the time zone abbreviations. You get the full name,
360             such as "Alaskan Standard Time". So, I want a way to convert that.
361              
362             These data are ad hoc, and guessing sometimes.
363              
364             First, I got a list of all the Windows timezone names with `tzutil /l`.
365             However, those are just the names you can use with `/s` rather than
366             all of the names windows will report. For example, you can set
367             "Alaskan Standard Time" but not "Alaskan Daylight Time", although
368             Windows, through the POSIX module, will report "Alaskan Daylight Time"
369             when appropriate.
370              
371             I get all those names by setting every Windows time zone, then checking
372             what POSIX's %Z returns. Note that you need to do this in a separate
373             process because most of the time tools only respect the first setting
374             of the time zone.
375              
376             Second, I want to get time zone abbreviations. This is a bit tricky because
377             the same offset can have many different abbreviations. By hand, I
378             associated the Olson name, such as America/Anchorage, with the Windows
379             name. Once I have all of those, I can use DateTime::TimeZone to get the
380             Olson abbreviations. In some cases, I added additional time zone names.
381              
382             Note that some of the abbreviations may be unexpected or unwanted. For
383             example, in the Russian Time Zones, they have special names, but you
384             may want MSK+n. You can edit this file to put whatever you like.
385              
386             =end comment
387              
388             __DATA__