File Coverage

blib/lib/Astro/App/Satpass2.pm
Criterion Covered Total %
statement 1679 2483 67.6
branch 630 1346 46.8
condition 168 423 39.7
subroutine 277 353 78.4
pod 59 63 93.6
total 2813 4668 60.2


line stmt bran cond sub pod time code
1             package Astro::App::Satpass2;
2              
3 20     20   254584 use 5.008;
  20         83  
4              
5 20     20   115 use strict;
  20         38  
  20         525  
6 20     20   106 use warnings;
  20         38  
  20         1361  
7              
8 20     20   6808 use Astro::App::Satpass2::Locale qw{ __localize };
  20         93  
  20         1752  
9 20     20   11619 use Astro::App::Satpass2::Macro::Command;
  20         75  
  20         912  
10 20     20   11340 use Astro::App::Satpass2::Macro::Code;
  20         68  
  20         863  
11 20     20   8009 use Astro::App::Satpass2::ParseTime;
  20         190  
  20         1034  
12 20         4579 use Astro::App::Satpass2::Utils qw{
13             :os
14             :ref
15             __arguments __legal_options
16             expand_tilde find_package_pod
17             has_method instance load_package
18             my_dist_config quoter
19             __parse_class_and_args
20 20     20   140 };
  20         41  
21              
22 20     20   18268 use Astro::Coord::ECI 0.077; # This needs at least 0.049.
  20         327321  
  20         1000  
23 20     20   11293 use Astro::Coord::ECI::Moon 0.077;
  20         130513  
  20         978  
24 20     20   11773 use Astro::Coord::ECI::Star 0.077;
  20         37175  
  20         850  
25 20     20   10947 use Astro::Coord::ECI::Sun 0.077;
  20         92881  
  20         1012  
26 20     20   28485 use Astro::Coord::ECI::TLE 0.077 qw{:constants}; # This needs at least 0.059.
  20         1618539  
  20         5905  
27 20     20   16236 use Astro::Coord::ECI::TLE::Set 0.077;
  20         61340  
  20         878  
28             # The following includes @CARP_NOT.
29 20     20   142 use Astro::Coord::ECI::Utils 0.112 qw{ :all }; # This needs at least 0.112.
  20         294  
  20         9892  
30              
31             {
32             local $@ = undef;
33 20   50     68 use constant HAVE_TLE_IRIDIUM => eval {
34             require Astro::Coord::ECI::TLE::Iridium;
35             Astro::Coord::ECI::TLE::Iridium->VERSION( 0.077 );
36             1;
37 20     20   193 } || 0;
  20         51  
38             # Unfortunately the alias code creates the alias even if the version
39             # is unacceptable; so we may have to just delete the Iridium aliases
40             unless( HAVE_TLE_IRIDIUM ) {
41             my %type_map = Astro::Coord::ECI::TLE->alias();
42             foreach my $name ( keys %type_map ) {
43             $type_map{$name} eq 'Astro::Coord::ECI::TLE::Iridium'
44             and Astro::Coord::ECI::TLE->alias( $name, undef );
45             }
46             }
47             }
48              
49 20     20   11847 use Attribute::Handlers;
  20         144896  
  20         131  
50 20     20   837 use Clone ();
  20         42  
  20         380  
51 20     20   117 use Cwd ();
  20         41  
  20         633  
52 20     20   108 use File::Glob qw{ :glob };
  20         53  
  20         4607  
53 20     20   182 use File::HomeDir;
  20         44  
  20         1200  
54 20     20   158 use File::Spec;
  20         47  
  20         616  
55 20     20   18791 use File::Temp;
  20         298971  
  20         1971  
56 20     20   161 use Getopt::Long 2.33;
  20         204  
  20         585  
57 20     20   2970 use IO::File 1.14;
  20         452  
  20         3722  
58 20     20   143 use IO::Handle;
  20         41  
  20         941  
59 20     20   110 use POSIX qw{ floor };
  20         43  
  20         168  
60 20     20   1692 use Scalar::Util 1.26 qw{ blessed isdual openhandle };
  20         391  
  20         1084  
61 20     20   15099 use Text::Abbrev;
  20         986  
  20         1311  
62 20     20   133 use Text::ParseWords (); # Used only for {level1} stuff.
  20         42  
  20         488  
63              
64 20     20   97 use constant ASTRO_SPACETRACK_VERSION => 0.105;
  20         44  
  20         1467  
65 20     20   146 use constant DEFAULT_STDOUT_LAYERS => ':encoding(utf-8)';
  20         66  
  20         2136  
66              
67             BEGIN {
68             eval {
69 20 50       137 load_package( 'Time::y2038' )
70             and Time::y2038->import();
71 20         1615 1;
72             }
73 20 50   20   108 or do {
74 0         0 require Time::Local;
75 0         0 Time::Local->import();
76             };
77             }
78              
79             # The following is returned by method _attribute_value() when a
80             # non-existent attribute is specified. We can't use undef for this,
81             # because the attribute might really be undef.
82             # NOTE that this used to be just bless \( $x = undef ) ..., but blead
83             # Perl 6a011f13d7690dbe2e03ad7500756c983bcb1834 did not like this
84             # (modificatoin of read-only variable).
85 20         48 use constant NULL => do {
86 20         51 my $x = undef;
87 20         1677 bless \$x, 'Null';
88 20     20   163 };
  20         55  
89             # The canonical way to see if $rslt actually contains the above is
90             # NULL_REF eq ref $rslt
91 20     20   167 use constant NULL_REF => ref NULL;
  20         37  
  20         1162  
92              
93 20     20   110 use constant SUN_CLASS_DEFAULT => 'Astro::Coord::ECI::Sun';
  20         35  
  20         14742  
94              
95             our $VERSION = '0.057_01';
96              
97             # The following 'cute' code is so that we do not determine whether we
98             # actually have optional modules until we really need them, and yet do
99             # not repeat the process once it is done.
100              
101             my $have_time_hires;
102             $have_time_hires = sub {
103             my $value = load_package( 'Time::HiRes' );
104             $have_time_hires = sub { return $value };
105             return $value;
106             };
107              
108             my $have_astro_spacetrack;
109             $have_astro_spacetrack = sub {
110             my $value = load_package( { lib => undef }, 'Astro::SpaceTrack' ) && eval {
111             Astro::SpaceTrack->VERSION( ASTRO_SPACETRACK_VERSION );
112             1;
113             };
114             $have_astro_spacetrack = sub { $value };
115             return $value;
116             };
117              
118             my $default_geocoder;
119             $default_geocoder = sub {
120             my $value =
121             _can_use_geocoder( 'Astro::App::Satpass2::Geocode::OSM'
122             );
123             $default_geocoder = sub { return $value };
124             return $value;
125             };
126              
127             sub _can_use_geocoder {
128 0     0   0 my ( $geocoder ) = @_;
129 0 0       0 my $pkg = load_package( $geocoder )
130             or return;
131 0 0       0 load_package( $pkg->GEOCODER_CLASS() )
132             or return;
133 0         0 return $pkg;
134             }
135              
136             my $interrupted = 'Interrupted by user.';
137              
138             my %twilight_def = (
139             civil => deg2rad (-6),
140             nautical => deg2rad (-12),
141             astronomical => deg2rad (-18),
142             );
143             my %twilight_abbr = abbrev (keys %twilight_def);
144              
145             # Individual commands are defined by subroutines of the same name,
146             # and having the Verb attribute. You can specify additional
147             # attributes if you need to. Following are descriptions of the
148             # attributes used by this script.
149             #
150             # Configure(configurations)
151             #
152             # The 'Configure' attribute specifies options to be passed to
153             # Getopt::Long::Configure before the options are parsed. For
154             # example, if a command wants to keep unrecognized options on the
155             # command you would specify:
156             # sub foo : Configure(pass_through) Verb
157             #
158             # Tokenize(options)
159             #
160             # The 'Tokenize' attribute specifies tokenizatino options. These
161             # can not take effect until fairly late in the parse when the
162             # tokens are known. These options are parsed by Getopt::Long, and
163             # the value of the attribute is a reference to the options hash
164             # thus generated. Possible options are:
165             # -expand_tilde - Expand tildes in the tokens. For historical
166             # reasons this is the default, but it can be negated by
167             # specifying -noexpand_tilde. Tildes in redirect
168             # specifications are always expanded.
169             #
170             # Tweak(options)
171             #
172             # The 'Tweak' attribute specifies miscellaneous tweaks to
173             # subroutine usage. Possible options are:
174             # -unsatisfied - Execute even inside an unsatisfied if().
175             # Subroutines with this attribute may have to be aware
176             # that they are being called within the scope of an
177             # unsatisfied if(). All interactive methods that must be
178             # called even inside an unsatisfied if() MUST have this
179             # attribute. These are begin() and end(), and anything
180             # that might dispatch either of these. At the moment this
181             # means if() and time().
182             # -completion - Requires as argument the name of the command
183             # completion method. This can not be checked at compile
184             # time. It will be called with the following arguments:
185             # $code - the relevant code reference
186             # $text - the text being completed
187             # $line - the line being completed
188             # $start - the current position in the line.
189             # It should return either a reference to an array
190             # containing possible completions, or nothing to fall
191             # through to standard completion
192             #
193             # Verb(options)
194             #
195             # The 'Verb' attribute identifies the subroutine as representing a
196             # cvsx command. If it has options, they should be specified inside
197             # parentheses as a whitespace-separated list of option
198             # specifications appropriate for Getopt::Long. For example:
199             # sub foo : Verb(bar baz=s)
200             # specifies that 'foo' is a command, taking options -bar, and
201             # -baz; the latter takes a string value.
202              
203             {
204             my %attr;
205              
206             sub Configure : ATTR(CODE,RAWDATA) {
207 0     0 0 0 my ( undef, undef, $code, $name, $data ) = @_;
208 0         0 $attr{$code}{$name} = _attr_list( $data );
209 0         0 return;
210 20     20   198 }
  20         38  
  20         185  
211              
212             sub Tokenize : ATTR(CODE,RAWDATA) {
213 20     20 0 37136 my ( undef, undef, $code, $name, $data ) = @_;
214 20         91 my $opt = _attr_hash( $name, $data, qw{ expand_tilde|expand-tilde! } );
215             exists $opt->{expand_tilde}
216 20 50       121 or $opt->{expand_tilde} = 1;
217 20         127 $attr{$code}{$name} = $opt;
218 20         87 return;
219 20     20   23839 }
  20         47  
  20         105  
220              
221             sub Tweak : ATTR(CODE,RAWDATA) {
222 300     300 0 10173 my ( undef, undef, $code, $name, $data ) = @_;
223 300         960 $attr{$code}{$name} = _attr_hash( $name, $data,
224             qw{ completion=s unsatisfied! } );
225 300         1095 return;
226 20     20   21375 }
  20         42  
  20         126  
227              
228             sub Verb : ATTR(CODE,RAWDATA) {
229 1304     1304 0 2914894 my ( undef, undef, $code, $name, $data ) = @_;
230 1304         3774 $attr{$code}{$name} = _attr_list( $data );
231 1304         4273 return;
232 20     20   22434 }
  20         40  
  20         110  
233              
234             sub _attr_hash {
235 320     320   1031 my ( $name, $arg, @legal ) = @_;
236 320         1737 my $gol = Getopt::Long::Parser->new();
237 320         31201 my %opt;
238             $gol->getoptionsfromarray(
239             _attr_list( $arg ),
240             \%opt,
241             @legal,
242 320 50       847 ) or do {
243 0         0 require Carp;
244 0         0 Carp::croak( "Bad $name option" );
245             };
246 320         144362 return \%opt;
247             }
248              
249             sub _attr_list {
250 1624 50   1624   5985 defined( local $_ = $_[0] )
251             or return [];
252 1624         7159 s/ \A \s+ //smx;
253 1624         20816 return [ split qr< \s+ >smx ];
254             }
255              
256             sub __get_attr {
257 1235     1235   3139 my ( undef, $code, $name, $dflt ) = @_; # $pkg unused
258 1235 50       3055 defined $code
259             or return \%attr;
260             defined $name
261 1235 50       2271 or return $attr{$code};
262             exists $attr{$code}{$name}
263 1235 100       6411 and return $attr{$code}{$name};
264 624         2578 return $dflt;
265             }
266             }
267              
268             my %mutator = (
269             almanac_horizon => \&_set_almanac_horizon,
270             appulse => \&_set_angle,
271             autoheight => \&_set_unmodified,
272             backdate => \&_set_unmodified,
273             background => \&_set_unmodified,
274             continuation_prompt => \&_set_unmodified,
275             country => \&_set_unmodified,
276             date_format => \&_set_formatter_attribute,
277             desired_equinox_dynamical => \&_set_formatter_attribute,
278             debug => \&_set_unmodified,
279             echo => \&_set_unmodified,
280             edge_of_earths_shadow => \&_set_unmodified,
281             ellipsoid => \&_set_ellipsoid,
282             error_out => \&_set_unmodified,
283             events => \&_set_unmodified,
284             exact_event => \&_set_unmodified,
285             execute_filter => \&_set_code_ref, # Undocumented and unsupported
286             explicit_macro_delete => \&_set_unmodified,
287             extinction => \&_set_unmodified,
288             filter => \&_set_unmodified,
289             flare_mag_day => \&_set_unmodified,
290             flare_mag_night => \&_set_unmodified,
291             formatter => \&_set_formatter,
292             geocoder => \&_set_geocoder,
293             geometric => \&_set_unmodified,
294             gmt => \&_set_formatter_attribute,
295             height => \&_set_distance_meters,
296             horizon => \&_set_angle,
297             illum => \&_set_illum_class,
298             latitude => \&_set_angle_or_undef,
299             local_coord => \&_set_formatter_attribute,
300             location => \&_set_unmodified,
301             longitude => \&_set_angle_or_undef,
302             model => \&_set_model,
303             max_mirror_angle => \&_set_angle,
304             output_layers => \&_set_output_layers,
305             pass_threshold => \&_set_angle_or_undef,
306             pass_variant => \&_set_pass_variant,
307             perltime => \&_set_time_parser_attribute,
308             prompt => \&_set_unmodified,
309             refraction => \&_set_unmodified,
310             simbad_url => \&_set_unmodified,
311             singleton => \&_set_unmodified,
312             spacetrack => \&_set_spacetrack,
313             stdout => \&_set_stdout,
314             sun => \&_set_sun_class, # Only in {level1}
315             time_format => \&_set_formatter_attribute,
316             time_formatter => \&_set_formatter_attribute,
317             time_parser => \&_set_time_parser,
318             ## timing => \&_set_unmodified,
319             twilight => \&_set_twilight, # 'civil', 'nautical', 'astronomical'
320             # (or a unique abbreviation thereof),
321             # or degrees above (positive) or below
322             # (negative) the geometric horizon.
323             tz => \&_set_tz,
324             verbose => \&_set_unmodified, # 0 = events only
325             # 1 = whenever above horizon
326             # 2 = anytime
327             visible => \&_set_unmodified, # 1 = only if sun down & sat illuminated
328             warning => \&_set_warner_attribute, # True to warn/die; false to carp/croak.
329             warn_on_empty => \&_set_unmodified,
330             # True to have list commands warn on
331             # an empty list.
332             webcmd => \&_set_webcmd, # Command to spawn for web pages
333             );
334              
335             my %accessor = (
336             date_format => \&_get_formatter_attribute,
337             desired_equinox_dynamical => \&_get_formatter_attribute,
338             geocoder => \&_get_geocoder,
339             gmt => \&_get_formatter_attribute,
340             local_coord => \&_get_formatter_attribute,
341             perltime => \&_get_time_parser_attribute,
342             spacetrack => \&_get_spacetrack,
343             time_format => \&_get_formatter_attribute,
344             time_formatter => \&_get_formatter_attribute,
345             tz => \&_get_time_parser_attribute,
346             warning => \&_get_warner_attribute,
347             );
348              
349             foreach ( keys %mutator, qw{ initfile } ) {
350             $accessor{$_} ||= sub { return $_[0]->{$_[1]} };
351             }
352              
353             my %shower = (
354             date_format => \&_show_formatter_attribute,
355             desired_equinox_dynamical => \&_show_formatter_attribute,
356             formatter => \&_show_copyable,
357             geocoder => \&_show_copyable,
358             gmt => \&_show_formatter_attribute,
359             local_coord => \&_show_formatter_attribute,
360             pass_variant => \&_show_pass_variant,
361             sun => \&_show_sun_class, # only in {level1}
362             time_parser => \&_show_time_parser,
363             time_format => \&_show_formatter_attribute,
364             time_formatter => \&_show_formatter_attribute,
365             );
366             foreach ( keys %accessor ) { $shower{$_} ||= \&_show_unmodified }
367              
368             # Attributes which must be set programmatically (i.e. not
369             # interactively or in the initialization file).
370              
371             my %nointeractive = map {$_ => 1} qw{
372             execute_filter
373             spacetrack
374             stdout
375             };
376              
377             # Initial object contents
378              
379             my %static = (
380             almanac_horizon => 0,
381             appulse => 0,
382             autoheight => 1,
383             background => 1,
384             backdate => 0,
385             continuation_prompt => '> ',
386             date_format => '%a %d-%b-%Y',
387             debug => 0,
388             echo => 0,
389             edge_of_earths_shadow => 1,
390             ellipsoid => Astro::Coord::ECI->get ('ellipsoid'),
391             error_out => 0,
392             events => 0,
393             exact_event => 1,
394             execute_filter => sub { return 1 }, # Undocumented and unsupported
395             ## explicit_macro_delete => 1, # Deprecated
396             extinction => 1,
397             filter => 0,
398             flare_mag_day => -6,
399             flare_mag_night => 0,
400             formatter => 'Astro::App::Satpass2::Format::Template', # Formatter class.
401             ## geocoder => $default_geocoder->(), # Geocoder class set when accessed
402             geometric => 1,
403             height => undef, # meters
404             # initfile => undef, # Set by init()
405             horizon => 20, # degrees
406             illum => SUN_CLASS_DEFAULT,
407             latitude => undef, # degrees
408             longitude => undef, # degrees
409             max_mirror_angle => HAVE_TLE_IRIDIUM ? rad2deg(
410             Astro::Coord::ECI::TLE::Iridium->DEFAULT_MAX_MIRROR_ANGLE ) :
411             undef,
412             model => 'model',
413             # pending => undef, # Continued input line if it exists.
414             pass_variant => PASS_VARIANT_NONE,
415             perltime => 0,
416             prompt => 'satpass2> ',
417             refraction => 1,
418             simbad_url => 'simbad.u-strasbg.fr',
419             singleton => 0,
420             # spacetrack => undef, # Astro::SpaceTrack object set when accessed
421             # stdout => undef, # Set to stdout in new().
422             output_layers => DEFAULT_STDOUT_LAYERS,
423             time_parser => 'Astro::App::Satpass2::ParseTime', # Time parser class.
424             twilight => 'civil',
425             tz => $ENV{TZ},
426             verbose => 0,
427             visible => 1,
428             warning => 0,
429             warn_on_empty => 1,
430             webcmd => ''
431             );
432              
433             my %sky_class = (
434             fold_case( 'Sun' ) => [ SUN_CLASS_DEFAULT, name => 'Sun' ],
435             fold_case( 'Moon' ) => [ 'Astro::Coord::ECI::Moon', name => 'Moon' ],
436             # # The shape of things to come -- maybe
437             # # but commented out because Astro-App-Satpass2 does not depend on
438             # # these
439             # ( map { fold_case( $_ ) =>
440             # "Astro::Coord::ECI::VSOP87D::$_" } qw{ Mercury Venus
441             # Mars Jupiter Saturn Uranus Neptune } ),
442             );
443              
444             sub new {
445 7     7   1462090 my ( $class, %args ) = @_;
446 7 50       37 ref $class and $class = ref $class;
447 7         21 my $self = {};
448 7         28 $self->{bodies} = [];
449 7         1686 $self->{macro} = {};
450             $self->{sky} = [
451 7         101 SUN_CLASS_DEFAULT->new (),
452             Astro::Coord::ECI::Moon->new (),
453             ];
454 7         3308 $self->{sky_class} = { %sky_class };
455             $self->{_help_module} = {
456 7         115 '' => __PACKAGE__,
457             eci => 'Astro::Coord::ECI',
458             moon => 'Astro::Coord::ECI::Moon',
459             set => 'Astro::Coord::ECI::TLE::Set',
460             sun => SUN_CLASS_DEFAULT,
461             spacetrack => 'Astro::SpaceTrack',
462             star => 'Astro::Coord::ECI::Star',
463             tle => 'Astro::Coord::ECI::TLE',
464             utils => 'Astro::Coord::ECI::Utils',
465             };
466             HAVE_TLE_IRIDIUM
467 7         15 and $self->{_help_module}{iridium} = 'Astro::Coord::ECI::TLE::Iridium';
468 7         23 bless $self, $class;
469 7         72 $self->_frame_push(initial => []);
470 7         54 $self->set(stdout => select());
471              
472 7         146 foreach my $name ( keys %static ) {
473 301 50       812 exists $args{$name} or $args{$name} = $static{$name};
474             }
475              
476             $self->{_warner} = Astro::App::Satpass2::Warner->new(
477             warning => delete $args{warning}
478 7         108 );
479              
480 7         24 foreach my $name ( qw{ formatter time_parser } ) {
481 14         75 $self->set( $name => delete $args{$name} );
482             }
483              
484 7         149 $self->set( %args );
485              
486 7         92 return $self;
487             }
488              
489             sub add {
490 1     1 1 13 my ( $self, @bodies ) = @_;
491 1         13 foreach my $body ( @bodies ) {
492 1 50       16 embodies( $body, 'Astro::Coord::ECI::TLE' )
493             or $self->wail(
494             'Arguments must represent Astro::Coord::ECI::TLE objects' );
495             }
496 1         50 push @{ $self->{bodies} }, @bodies;
  1         5  
497 1         16 return $self;
498             }
499              
500             sub alias : Verb() {
501 5     5 1 17 my ( undef, undef, @args ) = __arguments( @_ ); # Invocant, $opt unused
502              
503 5 100       12 if ( @args ) {
504 2         9 Astro::Coord::ECI::TLE->alias( @args );
505 2         38 return;
506             } else {
507 3         4 my $output;
508 3         12 my %alias = Astro::Coord::ECI::TLE->alias();
509 3         38 foreach my $key ( sort keys %alias ) {
510 10         22 $output .= join( ' ', 'alias', $key, $alias{$key} ) . "\n";
511             }
512 3         10 return $output;
513             }
514 20     20   58867 }
  20         53  
  20         124  
515              
516             # Attributes must all be on one line to process correctly under Perl
517             # 5.8.8.
518             sub almanac : Verb( choose=s@ dump! horizon|rise|set! transit! twilight! quarter! ) {
519 3     3 1 17 my ( $self, $opt, @args ) = __arguments( @_ );
520 3         23 $self->_apply_boolean_default(
521             $opt, 0, qw{ horizon transit twilight quarter } );
522              
523 3         49 my $almanac_start = $self->__parse_time(
524             shift @args, $self->_get_day_midnight());
525 3   50     18 my $almanac_end = $self->__parse_time (shift @args || '+1');
526              
527 3 50       40 $almanac_start >= $almanac_end
528             and $self->wail( 'End time must be after start time' );
529              
530             # Build an object representing our ground location.
531              
532 3         16 my $sta = $self->station();
533              
534 3         1255 my @almanac;
535              
536             # Iterate through the background bodies, accumulating data or
537             # complaining about the lack of an almanac() method as
538             # appropriate.
539              
540             my @sky = $self->__choose( $opt->{choose}, $self->{sky} )
541 3 50       26 or return $self->__wail( 'No bodies selected' );
542              
543 3         9 foreach my $body ( @sky ) {
544 6 50       627153 $body->can ('almanac') or do {
545 0         0 $self->whinge(
546             ref $body, ' does not support the almanac method');
547 0         0 next;
548             };
549             $body->set (
550             station => $sta,
551             twilight => $self->{_twilight},
552 6         50 );
553 6         798 push @almanac, $body->almanac_hash(
554             $almanac_start, $almanac_end);
555             }
556              
557             # Record number of events found
558              
559 3         1214546 @almanac = grep { $opt->{$_->{almanac}{event}} } @almanac;
  27         110  
560 3         15 $self->{events} += @almanac;
561              
562             # Localize the event descriptions if appropriate.
563              
564 3         21 _almanac_localize( @almanac );
565              
566             # Sort the almanac data by date, and display the results.
567              
568             return $self->__format_data(
569             almanac => [
570 3         24 sort { $a->{time} <=> $b->{time} }
  41         103  
571             @almanac
572             ], $opt );
573              
574 20     20   12100 }
  20         52  
  20         144  
575             sub _almanac_localize {
576 9     9   36 my @almanac = @_;
577 9         27 foreach my $event ( @almanac ) {
578             $event->{almanac}{description} = __localize(
579             text => [ almanac => $event->{body}->get( 'name' ),
580             $event->{almanac}{event}, $event->{almanac}{detail} ],
581             default => $event->{almanac}{description},
582             argument => $event->{body},
583 46         176 );
584             }
585 9         28 return;
586             }
587              
588             sub begin : Verb() Tweak( -unsatisfied ) {
589 5     5 1 31 my ( $self, $opt, @args ) = __arguments( @_ );
590             $self->_frame_push(
591 5 50       46 begin => @args ? \@args : $self->{frame}[-1]{args});
592 5         17 $self->{frame}[-1]{level1} = $opt->{level1};
593 5         16 return;
594 20     20   9336 }
  20         44  
  20         117  
595              
596             # -level1 is UNSUPPORTED and may be removed without warning. It is only
597             # there for me to screw around with.
598             BEGIN {
599             $ENV{SATPASS2_LEVEL1}
600 20 50   20   9300 and __PACKAGE__->MODIFY_CODE_ATTRIBUTES(
601             \&begin,
602             'Verb( level1! )',
603             );
604             }
605              
606             sub cd : Verb() {
607 2     2 1 36 my ( $self, undef, $dir ) = __arguments( @_ ); # $opt unused
608 2 100       19 if (defined($dir)) {
609 1 50       25 chdir $dir or $self->wail("Can not cd to $dir: $!");
610             } else {
611 1 50       9 chdir File::HomeDir->my_home()
612             or $self->wail("Can not cd to home: $!");
613             }
614 2         56 return;
615 20     20   150 }
  20         45  
  20         140  
616              
617             sub choose : Verb( epoch=s ) {
618 2     2 1 11 my ( $self, $opt, @args ) = __arguments( @_ );
619              
620 2 50       10 if ($opt->{epoch}) {
621 0         0 my $epoch = $self->__parse_time($opt->{epoch});
622             $self->{bodies} = [
623             map {
624 0         0 $_->select($epoch);
625             }
626             $self->_aggregate( $self->{bodies} )
627 0         0 ];
628             }
629 2 50       7 if ( @args ) {
630 2 50       3 my @bodies = @{ $self->__choose( \@args, $self->{bodies} ) }
  2         13  
631             or return $self->__wail( 'No bodies chosen' );
632 2         7 @{ $self->{bodies} } = @bodies;
  2         19  
633             }
634 2         7 return;
635 20     20   11397 }
  20         72  
  20         150  
636              
637             sub clear : Verb() {
638 5     5 1 51 my ( $self ) = __arguments( @_ ); # $opt, @args unused
639 5         14 @{$self->{bodies}} = ();
  5         145  
640 5         17 return;
641 20     20   19648 }
  20         58  
  20         115  
642              
643             sub dispatch {
644 289     289 1 1693 my ($self, $verb, @args) = @_;
645              
646 289 50       1159 defined $verb or return;
647              
648 289         1665 my $unsatisfied = $self->_in_unsatisfied_if();
649              
650 289 100       1083 if ( $self->{macro}{$verb} ) {
651 19 50       61 $unsatisfied
652             and return;
653 19         118 return $self->_macro( $verb, @args );
654             }
655              
656 270         437 my $code;
657 270         508 $verb =~ s/ \A core [.] //smx;
658 270 100 66     1574 $code = $self->can($verb)
659             and $self->__get_attr($code, 'Verb')
660             or $self->wail("Unknown interactive method '$verb'");
661              
662 269         478 my $rslt;
663             $unsatisfied
664             and not $self->__get_attr( $code, Tweak => {} )->{unsatisfied}
665 269 100 100     1292 or $rslt = $code->( $self, @args );
666              
667 261 100       2640391 defined $rslt
668             and $rslt =~ s/ (?
669              
670 261         527 foreach my $code (
671 261 100       1751 reverse @{ delete( $self->{frame}[-1]{post_dispatch} ) || [] }
672             ) {
673 23         32 my $append;
674 23 100       53 defined( $append = $code->( $self ) )
675             and $rslt .= $append;
676             }
677 261         4388 return $rslt;
678             }
679              
680             {
681             my %special = (
682             begin => sub {
683             my ( $self, $verb ) = @_;
684             $self->_is_interactive()
685             or $self->wail(
686             "'begin' forbidden in non-interactive $verb()" );
687             return;
688             },
689             end => sub {
690             my ( $self, $verb ) = @_;
691             $self->wail( "'end' forbidden in $verb()" );
692             },
693             );
694              
695             sub _dispatch_check {
696 23     23   45 my ( $self, $verb, $disp ) = @_;
697 23 100       64 my $code = $special{$disp}
698             or return;
699 4         16 return $code->( $self, $verb, $disp );
700             }
701             }
702              
703             sub drop : Verb() {
704 1     1 1 5 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
705              
706             @args
707 1 50       5 or return;
708              
709             my @bodies = @{
710 1 50       6 $self->__choose( { invert => 1 }, \@args, $self->{bodies} ) }
  1         99  
711             or return $self->__wail( 'No bodies left' );
712              
713 1         4 @{ $self->{bodies} } = @bodies;
  1         4  
714              
715 1         4 return;
716 20     20   16953 }
  20         85  
  20         118  
717              
718             sub dump : method Verb() Tweak( -unsatisfied ) { ## no critic (ProhibitBuiltInHomonyms)
719 0     0 1 0 my ( $self, undef, @arg ) = __arguments( @_ ); # $opt unused
720              
721 0         0 local $self->{time_parser} = ref $self->{time_parser};
722              
723 0         0 my $dumper = $self->_get_dumper();
724              
725             @arg
726 0 0       0 or return $dumper->( $self );
727              
728 0         0 local $_ = shift @arg;
729              
730             ref
731 0 0       0 and return $dumper->( $_ );
732              
733             m/ \A frames? \z /smxi
734 0 0       0 and return $dumper->( $self->{frame} );
735              
736 0 0       0 m/ \A tokens? \z /smxi
737             and return $dumper->( $self->__tokenize( @arg ) );
738              
739             m/ \A twilight \z /smxi
740             and return $dumper->(
741 0 0       0 { map { $_ => $self->{$_} } qw{ twilight _twilight } } );
  0         0  
742              
743 0         0 my @stuff = $self->__choose( [ $_ ], $self->{bodies} );
744 0 0       0 if ( defined( my $inx = $self->_find_in_sky( $_ ) ) ) {
745 0         0 push @stuff, $self->{sky}[$inx];
746             }
747             @stuff
748 0 0       0 and return $dumper->( @stuff );
749              
750 0         0 $self->whinge( "Dump argument '$_' not recognized" );
751            
752 0         0 return;
753 20     20   12649 }
  20         44  
  20         148  
754              
755             sub echo : Verb( n! ) {
756 44     44 1 270 my ( undef, $opt, @args ) = __arguments( @_ ); # Invocant unused
757 44         183 my $output = join( ' ', @args );
758 44 50       142 $opt->{n} or $output .= "\n";
759 44         143 return $output;
760 20     20   7345 }
  20         44  
  20         119  
761              
762             sub else : method Verb() Tweak( -unsatisfied ) { ## no critic (ProhibitBuiltInHomonyms)
763 2     2 1 10 my ( $self ) = __arguments( @_ ); # $opt, @args unused
764              
765 2         27 @{ $self->{frame} } > 1
766             and 'begin' eq $self->{frame}[-1]{type}
767             and 'if' eq $self->{frame}[-2]{type}
768 2 50 33     4 or $self->wail( 'Else without if ... then begin' );
      33        
769              
770 2 50       8 $self->{frame}[-1]{in_else}++
771             and $self->wail( 'Only one else may follow an if' );
772              
773 2         6 return $self->_twiddle_condition( ! $self->{frame}[-2]{condition} );
774 20     20   7415 }
  20         45  
  20         251  
775              
776             sub _twiddle_condition {
777 4     4   8 my ( $self, $cond ) = @_;
778              
779             # Here is where I pay for the convenience of the if()
780             # implementation. The if() itself is a frame because I do not yet
781             # know if it will entail a begin(). But I can't do an else() unless
782             # there is in fact a begin(), which creates another frame. So I end
783             # up twiddling values in both frames.
784              
785             $self->{frame}[-1]{unsatisfied_if} =
786             $self->{frame}[-2]{unsatisfied_if} =
787             ! $cond || (
788             @{ $self->{frame} } > 2 ?
789             $self->{frame}[-3]{unsatisfied_if} :
790 4   66     13 0
791             );
792              
793             $self->{frame}[-1]{condition} =
794 4         10 $self->{frame}[-2]{condition} = $cond;
795              
796 4         11 return;
797             }
798              
799             sub end : Verb() Tweak( -unsatisfied ) {
800 5     5 1 47 my ( $self ) = __arguments( @_ ); # $opt, @args unused
801              
802 5 50       50 $self->{frame}[-1]{type} eq 'begin'
803             or $self->wail( 'End without begin' );
804 5         66 $self->_frame_pop();
805 5         16 return;
806 20     20   9575 }
  20         47  
  20         117  
807              
808             sub error : Verb() {
809 1     1 1 5 my ( $self, undef, @arg ) = __arguments( @_ );
810             @arg
811 1 50       4 or push @arg, 'An error has occurred';
812 1         5 $self->wail( @arg );
813 0         0 return;
814 20     20   6486 }
  20         44  
  20         176  
815              
816             # Tokenize and execute one or more commands. Optionally (and
817             # unsupportedly) you can pass a code reference as the first argument.
818             # This code reference will be used to fetch commands when the arguments
819             # are exhausted. IF you pass your own code reference, we return after
820             # the first command, since the code reference is presumed to manage the
821             # input stream itself.
822             sub execute {
823 264     264 1 839 my ($self, @args) = @_;
824 264         785 my $accum;
825             my $in;
826 264         0 my $extern;
827 264 100       844 if ( CODE_REF eq ref $args[0] ) {
828 13         72 $extern = shift @args;
829             $in = sub {
830 21     21   93 my ( $prompt ) = @_;
831 21 100       108 @args and return shift @args;
832 8         26 return $extern->( $prompt );
833 13         94 };
834             } else {
835 251     502   1217 $in = sub { return shift @args };
  502         1804  
836             }
837 264         734 @args = map { split qr{ (?<= \n ) }smx, $_ } @args;
  265         4156  
838 264         1104 while ( defined ( local $_ = $in->( $self->get( 'prompt' ) ) ) ) {
839 280 50       989 $self->{echo} and $self->whinge($self->get( 'prompt' ), $_);
840 280 100       1061 m/ \A \s* [#] /smx and next;
841 277         728 my $stdout = $self->{frame}[-1]{stdout};
842             my ($args, $redirect) = $self->__tokenize(
843 277         1772 { in => $in }, $_, $self->{frame}[-1]{args});
844             # NOTICE
845             #
846             # The execute_filter attribute is undocumented and unsupported.
847             # It exists only so I can scavenge the user's initialization
848             # file for the (possible) Space Track username and password, to
849             # be used in testing, without being subject to any other
850             # undesired side effects, such as running a prediction and
851             # exiting. If I change my mind on how or whether to do this,
852             # execute_filter will be altered or retracted without warning,
853             # much less a deprecation cycle. If you have a legitimate need
854             # for this functionality, contact me.
855             #
856             # YOU HAVE BEEN WARNED.
857 273 100       1412 $self->{execute_filter}->( $self, $args ) or next;
858 267 100       5278 @{ $args } or next;
  267         644  
859 266 100       712 if ($redirect->{'>'}) {
860 1         2 my ( $mode, $name ) = map { $redirect->{'>'}{$_} } qw{ mode name };
  2         5  
861 1         1 my $fh;
862             $stdout = sub {
863 1     1   2 my ( $output ) = @_;
864 1   33     6 $fh ||= $self->_file_opener( $name, $mode );
865 1         8 $fh->print( $output );
866 1         7 return;
867 1         4 };
868             }
869              
870             # {localout} is the output to be used for this command. It goes
871             # in the frame stack because our command may start a new frame,
872             # and _frame_push() needs to have a place to get the correct
873             # output handle.
874              
875 266         383 my $frame_depth = $#{$self->{frame}};
  266         729  
876 266         772 $self->{frame}[-1]{localout} = $stdout;
877              
878 266         864 my $output = $self->dispatch( @$args );
879              
880 256         1211 $#{$self->{frame}} >= $frame_depth
881 256 100       438 and delete $self->{frame}[ $frame_depth ]{localout};
882              
883 256 100       1424 $self->_execute_output( $output,
884             defined $stdout ? $stdout : \$accum );
885              
886 256 100       1891 $extern and last;
887             }
888 250         1974 return $accum;
889             }
890              
891             # $satpass2->_execute(...);
892             #
893             # This subroutine calls $satpass2->execute() once for each
894             # argument. The call is wrapped in an eval{}; if an exception
895             # occurs the user is notified via warn.
896              
897             sub _execute {
898 0     0   0 my ($self, @args) = @_;
899             my $in = CODE_REF eq ref $args[0] ? shift @args : sub { return shift
900 0 0   0   0 @args };
  0         0  
901 0         0 while ( @args ) {
902 0     0   0 local $SIG{INT} = sub {die "\n$interrupted\n"};
  0         0  
903 0 0       0 eval {
904 0         0 $self->execute( $in, shift @args );
905 0         0 1;
906             } or warn $@; # Not whinge, since presumably we already did.
907             }
908 0         0 return;
909             }
910              
911             # $satpass2->_execute_output( $output, $stdout );
912             #
913             # If $output is defined, sends it to $stdout.
914              
915             sub _execute_output {
916 256     256   689 my ( undef, $output, $stdout ) = @_; # Invocant unused
917 256 100       615 defined $output or return;
918 152         377 my $ref = ref $stdout;
919 152 50       606 if ( !defined $stdout ) {
    100          
    100          
    50          
920 0         0 return $output;
921             } elsif ( SCALAR_REF eq $ref ) {
922 149         449 $$stdout .= $output;
923             } elsif ( CODE_REF eq $ref ) {
924 2         5 $stdout->( $output );
925             } elsif ( ARRAY_REF eq $ref ) {
926 1         9 push @$stdout, split qr{ (?<=\n) }smx, $output;
927             } else {
928 0         0 $stdout->print( $output );
929             }
930 152         285 return;
931             }
932              
933             sub exit : method Verb() { ## no critic (ProhibitBuiltInHomonyms)
934 1     1 1 5 my ( $self ) = __arguments( @_ ); # $opt, @args unused
935              
936 1         5 $self->_frame_pop(1); # Leave only the inital frame.
937              
938 1         1 eval { ## no critic (RequireCheckingReturnValueOfEval)
939 20     20   27873 no warnings qw{exiting};
  20         62  
  20         2509  
940 1         10 last SATPASS2_EXECUTE;
941             };
942 0         0 $self->whinge("$@Exiting Perl");
943 0         0 exit;
944              
945 20     20   136 }
  20         51  
  20         129  
946              
947             sub export : Verb() {
948 4     4 1 33 my ( $self, undef, $name, @args ) = __arguments( @_ ); # $opt unused
949 4 100       26 if ($mutator{$name}) {
950 1 50       9 @args and $self->set ($name, shift @args);
951 1         7 $self->{exported}{$name} = 1;
952             } else {
953 3 100       46 @args or return $self->wail( 'You must specify a value' );
954 2         19 $self->{exported}{$name} = shift @args;
955             }
956 3         10 return;
957 20     20   7445 }
  20         52  
  20         137  
958              
959             # Attributes must all be on one line to process correctly under Perl
960             # 5.8.8.
961             sub flare : Verb( algorithm=s am! choose=s@ day! dump! pm! questionable|spare! quiet! tz|zone=s )
962             {
963 0     0 1 0 my ( $self, $opt, @args ) = __arguments( @_ );
964 0         0 HAVE_TLE_IRIDIUM
965             or $self->wail( 'Astro::Coord::ECI::TLE::Iridium not available' );
966 0         0 my $pass_start = $self->__parse_time (
967             shift @args, $self->_get_day_noon());
968 0   0     0 my $pass_end = $self->__parse_time (shift @args || '+7');
969 0 0       0 $pass_start >= $pass_end
970             and $self->wail( 'End time must be after start time' );
971 0         0 my $sta = $self->station();
972              
973 0         0 my $max_mirror_angle = deg2rad( $self->{max_mirror_angle} );
974 0         0 my $horizon = deg2rad ($self->{horizon});
975 0         0 my $twilight = $self->{_twilight};
976 0         0 my @flare_mag = ($self->{flare_mag_night}, $self->{flare_mag_day});
977             my $zone = exists $opt->{tz} ? $opt->{tz} :
978             $self->{formatter}->gmt() ? 0 :
979 0 0 0     0 $self->{formatter}->tz() || undef;
    0          
980              
981 0         0 $self->_apply_boolean_default(
982             $opt, 0, qw{ am day pm } );
983              
984             # Decide which model to use.
985              
986 0         0 my $model = $self->{model};
987              
988             # Select only the bodies capable of flaring.
989              
990 0         0 my @active;
991 0         0 foreach my $tle ( $self->_aggregate(
992             scalar $self->__choose( $opt->{choose}, $self->{bodies} )
993             ) )
994             {
995 0 0       0 $tle->can_flare( $opt->{questionable} ) or next;
996             $tle->set (
997             algorithm => $opt->{algorithm} || 'fixed',
998             backdate => $self->{backdate},
999             edge_of_earths_shadow => $self->{edge_of_earths_shadow},
1000             horizon => $horizon,
1001             twilight => $twilight,
1002             model => $model,
1003             am => $opt->{am},
1004             max_mirror_angle => $max_mirror_angle,
1005             day => $opt->{day},
1006             pm => $opt->{pm},
1007             extinction => $self->{extinction},
1008 0   0     0 station => $sta,
1009             zone => $zone,
1010             );
1011 0         0 push @active, $tle;
1012             }
1013 0 0       0 @active or return $self->__wail( 'No bodies capable of flaring' );
1014              
1015 0         0 my @flares;
1016 0         0 foreach my $tle (@active) {
1017             eval {
1018 0         0 push @flares, $tle->flare( $pass_start, $pass_end );
1019 0         0 1;
1020 0 0       0 } or do {
1021 0 0       0 $@ =~ m/ \Q$interrupted\E /smxo and $self->wail($@);
1022 0 0       0 $opt->{quiet} or $self->whinge($@);
1023             };
1024             }
1025              
1026             # Record number of events found
1027              
1028 0         0 @flares = sort { $a->{time} <=> $b->{time} }
1029 0         0 grep { $_->{magnitude} <= $flare_mag[
1030 0 0       0 ( $_->{type} eq 'day' ? 1 : 0 ) ] }
1031             @flares;
1032 0         0 $self->{events} += @flares;
1033              
1034 0         0 return $self->__format_data( flare => \@flares, $opt );
1035 20     20   16416 }
  20         63  
  20         260  
1036              
1037             sub formatter : Verb() Tweak( -completion _readline_complete_subcommand ) {
1038 9 50   9 1 33 splice @_, ( HASH_REF eq ref $_[1] ? 2 : 1 ), 0, 'formatter';
1039 9         34 goto &_helper_handler;
1040 20     20   6168 }
  20         59  
  20         131  
1041              
1042             # Calls to the following _formatter_sub method are generated dynamically
1043             # above, so there is no way Perl::Critic can find them.
1044              
1045             sub _formatter_sub { ## no critic (ProhibitUnusedPrivateSubroutines)
1046 0     0   0 my ( $app, $text, $line, $start, @arg ) = @_;
1047 0         0 my $fmtr = $app->get( 'formatter' );
1048 0 0       0 if ( @arg == 2 ) {
1049 0         0 my @list = qw{
1050             date_format
1051             desired_equinox_dynamical
1052             gmt
1053             local_coord
1054             time_format
1055             tz
1056             };
1057 0 0       0 $fmtr->can( '__list_templates' )
1058             and push @list, 'template';
1059 0         0 my $re = qr/ \A \Q$arg[1]\E /smx;
1060 0         0 return [ grep { $_ =~ $re } sort @list ];
  0         0  
1061             }
1062 0 0       0 my $code = $app->can( "_formatter_complete_$arg[1]" )
1063             or return;
1064              
1065 0         0 my $r;
1066 0 0       0 $r = $app->_readline_complete_options( $code, $text, $line,
1067             $start )
1068             and return $r;
1069              
1070 0         0 return $code->( $app, @arg );
1071             }
1072              
1073             # Calls to the following _formatter_complete_... methods are generated
1074             # dynamically above, so there is no way Perl::Critic can find them.
1075             # The Verb attribute must aggree with _helper_handler().
1076              
1077             sub _formatter_complete_template : Verb( changes! raw! ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1078 0     0   0 my ( $app, undef, @arg ) = __arguments( @_ );
1079 0         0 my $fmtr = $app->get( 'formatter' );
1080 0         0 my $re = qr/ \A \Q$arg[2]\E /smx;
1081             return [
1082 0         0 grep { $_ =~ $re }
  0         0  
1083             sort( $fmtr->__list_templates() )
1084             ];
1085 20     20   12563 }
  20         160  
  20         106  
1086              
1087             sub geocode : Verb( debug! ) {
1088 0     0 1 0 my ( $self, $opt, $loc ) = __arguments( @_ );
1089              
1090 0         0 my $set_loc;
1091 0 0       0 if ( defined $loc ) {
1092 0         0 $set_loc = 1;
1093             } else {
1094 0         0 $loc = $self->get( 'location' );
1095             }
1096              
1097 0         0 my $geocoder = $self->_helper_get_object( 'geocoder' );
1098              
1099 0         0 my @rslt = $geocoder->geocode( $loc );
1100              
1101 0         0 my $output;
1102 0 0       0 if ( @rslt == 1 ) {
1103             $set_loc
1104 0 0       0 and $self->set( location => $rslt[0]{description} );
1105 0         0 $self->set( map { $_ => $rslt[0]{$_} } qw{ latitude
  0         0  
1106             longitude } );
1107 0 0       0 $output .= $self->show(
1108             ( $set_loc ? 'location' : () ), qw{latitude longitude} );
1109 0 0       0 if ( $self->get( 'autoheight' ) ) {
1110 0         0 $opt->{geocoding} = 1;
1111 0         0 $output .= $self->_height_us($opt);
1112             }
1113             } else {
1114 0         0 foreach my $poi ( @rslt ) {
1115 0         0 $output .= join ' ', map { $poi->{$_} } qw{ latitude
  0         0  
1116             longitude description };
1117 0         0 $output =~ s/ (?: \A | (?
1118             }
1119             }
1120 0         0 return $output;
1121 20     20   11267 }
  20         44  
  20         360  
1122              
1123             sub geodetic : Verb() {
1124 0     0 1 0 my ( $self, undef, $name, $lat, $lon, $alt ) = __arguments( @_ ); # $opt unused
1125 0 0       0 @_ == 5 or $self->wail( 'Want exactly four arguments' );
1126 0         0 my $body = Astro::Coord::ECI::TLE->new(
1127             name => $name,
1128             id => '',
1129             model => 'null',
1130             )->geodetic(
1131             deg2rad( $self->__parse_angle( $lat ) ),
1132             deg2rad( $self->__parse_angle( $lon ) ),
1133             $self->__parse_distance( $alt ),
1134             );
1135 0         0 push @{ $self->{bodies} }, $body;
  0         0  
1136 0         0 return;
1137 20     20   8258 }
  20         43  
  20         173  
1138              
1139             sub get {
1140 927     927   2658 my ($self, $name) = @_;
1141 927         3297 $self->_attribute_exists( $name );
1142 927         3294 $self->_deprecation_notice( attribute => $name );
1143 927         4194 return $accessor{$name}->($self, $name);
1144             }
1145              
1146             sub height : Verb( debug! ) {
1147 0     0 1 0 return _height_us( __arguments( @_ ) );
1148 20     20   6815 }
  20         54  
  20         115  
1149              
1150             sub _height_us {
1151 0     0   0 my ($self, $opt, @args) = @_;
1152 0         0 $self->_load_module ('Geo::WebService::Elevation::USGS');
1153 0         0 my $eq = Geo::WebService::Elevation::USGS->new(
1154             places => 2, # Service returns unreasonable precision
1155             units => 'METERS', # default for service is 'FEET'
1156             croak => 0, # Handle our own errors
1157             );
1158 0 0       0 @args or push @args, $self->get('latitude'), $self->get('longitude');
1159 0         0 my $output;
1160 0         0 my ( $rslt ) = $eq->elevation(@args);
1161 0 0       0 if ( $eq->is_valid( $rslt ) ) {
1162 0         0 $self->set( height => $rslt->{Elevation} );
1163             } else {
1164             $opt->{geocoding}
1165 0 0 0     0 or $self->wail( $eq->get( 'error' ) || 'No valid result found' );
1166 0         0 $self->set( height => 0 );
1167 0         0 $output .= "# Unable to obtain height. Setting to 0\n";
1168             }
1169 0         0 $output .= $self->show( 'height' );
1170 0         0 return $output;
1171             }
1172              
1173             sub help : Verb() {
1174 0     0 1 0 my ( $self, undef, $arg ) = __arguments( @_ ); # $opt unused
1175 0 0       0 defined $arg
1176             or $arg = '';
1177             defined $self->{_help_module}{$arg}
1178 0 0       0 and $arg = $self->{_help_module}{$arg};
1179 0 0       0 if ( my $cmd = $self->_get_browser_command() ) {
1180 0 0       0 my $kind = $arg =~ m/ - /smx ? 'release' : 'pod';
1181 0         0 $self->system( $cmd,
1182             "https://metacpan.org/$kind/$arg" );
1183             } else {
1184              
1185 0         0 my $os_specific = "_help_$^O";
1186 0 0       0 if (__PACKAGE__->can ($os_specific)) {
    0          
1187 0         0 return __PACKAGE__->$os_specific ();
1188             } elsif ( load_package( 'Pod::Usage' ) ) {
1189 0         0 my @ha;
1190 0 0       0 if ( defined( my $path = find_package_pod( $arg ) ) ) {
1191 0         0 push @ha, '-input' => $path;
1192             }
1193 0         0 my $stdout = $self->{frame}[-1]{localout};
1194 0 0 0     0 if (openhandle $stdout && !-t $stdout) {
1195 0         0 push @ha, -output => $stdout;
1196             }
1197             Pod::Usage::pod2usage (
1198 0         0 -verbose => 2, -exitval => 'NOEXIT', @ha);
1199             } else {
1200             # This should never happen, since Pod::Usage is core
1201             # since 5.6. On the other hand we have not declared it
1202             # as a dependency, and some downstream packagers seem to
1203             # think they know more than the author what should be in
1204             # a package.
1205             return <<'EOD'
1206             No help available; Pod::Usage can not be loaded.
1207             EOD
1208 0         0 }
1209             }
1210 0         0 return;
1211 20     20   16310 }
  20         45  
  20         191  
1212              
1213             # The call to this is generated dynamically above, and there is no way
1214             # Perl::Critic can find it.
1215             sub _help_MacOS { ## no critic (ProhibitUnusedPrivateSubroutines)
1216 0     0   0 return <<'EOD';
1217              
1218             Normally, we would display the documentation for the satpass2
1219             script here. But unfortunately this depends on the ability to
1220             spawn the perldoc command, and we do not have this ability under
1221             Mac OS 9 and earlier. You can find the same thing online at
1222             https://metacpan.org/release/Astro-App-Satpass2
1223              
1224             EOD
1225             }
1226              
1227             {
1228             # This hash specifies the specific grammar passed to
1229             # __infix_engine(). The keys are:
1230             # {done} optional; called when parse is complete.
1231             # {oper} defines operators. Values are hash refs with:
1232             # {handler} code that handles operator;
1233             # {validation} name of validation style (see {vld} below).
1234             # {vld} defines operator validation. There must be a key for each
1235             # distinct value of {oper}{$name}{validation}.
1236             # NOTE WELL
1237             # Because if() has the Tweak( -unsatisfied ) attribute, any
1238             # operators that have side effects will need to be aware of whether
1239             # they are running inside an unsatisfied if().
1240             my %define = (
1241             done => sub {
1242             # my ( $self, $def, $ctx, $tokens ) = @_;
1243             my ( $self, undef, $ctx ) = @_;
1244             @{ $ctx }
1245             and $self->wail( q );;
1246             return;
1247             },
1248             oper => {
1249             '(' => {
1250             handler => sub {
1251             my ( $self, $def, $ctx, $tokens ) = @_;
1252             my $want = delete $ctx->[-1]{want};
1253             defined $want
1254             or $want = 1;
1255             push @{ $ctx }, {
1256             want => $want,
1257             value => [],
1258             };
1259             $ctx->[-2]{shortcut}
1260             and $ctx->[1]{shortcut} = $ctx->[-2]{shortcut};
1261             my $depth = @{ $ctx };
1262             while ( $depth <= @{ $ctx } ) {
1263             $self->_infix_engine_dispatch( $def, $ctx, $tokens );
1264             }
1265             return;
1266             },
1267             },
1268             ')' => {
1269             handler => sub {
1270             # my ( $self, $def, $ctx, $tokens ) = @_;
1271             my ( $self, undef, $ctx ) = @_;
1272             @{ $ctx }
1273             or $self->wail( 'Unpaired right parentheses' );
1274             $ctx->[-1]{want} == @{ $ctx->[-1]{value} }
1275             or $self->wail(
1276             "Expected $ctx->[-1]{want} value(s), got " .
1277             scalar @{ $ctx->[-1]{value} } );
1278             push @{ $ctx->[-2]{value} }, @{ $ctx->[-1]{value} };
1279             pop @{ $ctx };
1280             return;
1281             },
1282             },
1283             '-n' => {
1284             handler => sub {
1285             # my ( $self, $def, $ctx, $tokens ) = @_;
1286             my ( undef, undef, $ctx, $tokens ) = @_;
1287             my $v = shift @{ $tokens };
1288             defined $v
1289             or $v = '';
1290             push @{ $ctx->[-1]{value} }, '' ne $v;
1291             },
1292             validation => 'prefix',
1293             },
1294             '-z' => {
1295             handler => sub {
1296             # my ( $self, $def, $ctx, $tokens ) = @_;
1297             my ( undef, undef, $ctx, $tokens ) = @_;
1298             my $v = shift @{ $tokens };
1299             defined $v
1300             or $v = '';
1301             push @{ $ctx->[-1]{value} }, '' eq $v;
1302             },
1303             validation => 'prefix',
1304             },
1305             and => {
1306             handler => sub {
1307             my ( $self, $def, $ctx, $tokens ) = @_;
1308             $ctx->[-1]{value}[-1]
1309             or $ctx->[-1]{shortcut} = 1;
1310             $self->_infix_engine_dispatch( $def, $ctx, $tokens );
1311             # For some reason the following has to be done in
1312             # two statements, or both operands remain on the
1313             # stack.
1314             my $ro = pop @{ $ctx->[-1]{value} };
1315             $ctx->[-1]{value}[-1] &&= $ro
1316             unless delete $ctx->[-1]{shortcut};
1317             return;
1318             },
1319             validation => 'infix',
1320             },
1321             attr => {
1322             handler => sub {
1323             # my ( $self, $def, $ctx, $tokens ) = @_;
1324             my ( $self, undef, $ctx, $tokens ) = @_;
1325             my $attr = shift @{ $tokens };
1326             my $val;
1327             $ctx->[-1]{shortcut}
1328             or $val = $self->_attribute_value( $attr );
1329             NULL_REF eq ref $val
1330             and $self->wail( "No such attribute as '$attr'" );
1331             push @{ $ctx->[-1]{value} }, $val;
1332             return;
1333             },
1334             validation => 'prefix',
1335             },
1336             env => {
1337             handler => sub {
1338             # my ( $self, $def, $ctx, $tokens ) = @_;
1339             my ( undef, undef, $ctx, $tokens ) = @_;
1340             my $name = shift @{ $tokens };
1341             my $val;
1342             $ctx->[-1]{shortcut}
1343             or $val = $ENV{$name};
1344             push @{ $ctx->[-1]{value} }, $val;
1345             return;
1346             },
1347             validation => 'prefix',
1348             },
1349             loaded => {
1350             handler => sub {
1351             # my ( $self, $def, $ctx, $tokens ) = @_;
1352             my ( $self, undef, $ctx, $tokens ) = @_;
1353             my $name = shift @{ $tokens };
1354             my @loaded;
1355             $ctx->[-1]{shortcut}
1356             or @loaded = $self->__choose(
1357             { bodies => 1 },
1358             [ $name ],
1359             );
1360             push @{ $ctx->[-1]{value} }, scalar @loaded;
1361             return;
1362             },
1363             validation => 'prefix',
1364             },
1365             not => {
1366             handler => sub {
1367             my ( $self, $def, $ctx, $tokens ) = @_;
1368             $self->_infix_engine_dispatch( $def, $ctx, $tokens );
1369             $ctx->[-1]{value}[-1] = ! $ctx->[-1]{value}[-1];
1370             return;
1371             },
1372             validation => 'prefix',
1373             },
1374             or => {
1375             handler => sub {
1376             my ( $self, $def, $ctx, $tokens ) = @_;
1377             $ctx->[-1]{value}[-1]
1378             and $ctx->[-1]{shortcut} = 1;
1379             $self->_infix_engine_dispatch( $def, $ctx, $tokens );
1380             # For some reason the following has to be done in
1381             # two statements, or both operands remain on the
1382             # stack.
1383             my $ro = pop @{ $ctx->[-1]{value} };
1384             $ctx->[-1]{value}[-1] ||= $ro
1385             unless delete $ctx->[-1]{shortcut};
1386             return;
1387             },
1388             validation => 'infix',
1389             },
1390             os => {
1391             handler => sub {
1392             # my ( $self, $def, $ctx, $tokens ) = @_;
1393             my ( undef, undef, $ctx, $tokens ) = @_;
1394             my $re = qr< \A \Q$^O\E \z >smxi;
1395             my $rslt = 0;
1396             my $name = shift @{ $tokens };
1397             unless ( $ctx->[-1]{shortcut} ) {
1398             foreach my $os ( split qr< [|] >smx, $name ) {
1399             $os =~ $re
1400             or next;
1401             $rslt = 1;
1402             last;
1403             }
1404             }
1405             push @{ $ctx->[-1]{value} }, $rslt;
1406             return;
1407             },
1408             validation => 'prefix',
1409             },
1410             then => {
1411             handler => sub {
1412             # my ( $self, $def, $ctx, $tokens ) = @_;
1413             my ( $self, undef, $ctx, $tokens ) = @_;
1414             1 == @{ $ctx }
1415             or $self->wail( 'Unclosed left parentheses' );
1416             my $last = pop @{ $ctx };
1417             my @arg = splice @{ $tokens };
1418             if ( $last->{dispatch} ) {
1419             $self->_dispatch_check( if => $arg[0] );
1420             $self->_frame_push( if => [], {
1421             condition => $last->{value}[-1],
1422             },
1423             );
1424             $self->_add_post_dispatch( sub {
1425             $self->_frame_pop( if => undef );
1426             },
1427             );
1428             return $self->dispatch( @arg );
1429             } else {
1430             $self->_twiddle_condition( $last->{value}[-1] );
1431             }
1432             },
1433             validation => 'terminal',
1434             },
1435             },
1436             val => sub {
1437             # my ( $self, $def, $ctx, $tkn, $tokens ) = @_;
1438             my ( undef, undef, $ctx, $tkn ) = @_;
1439             push @{ $ctx->[-1]{value} }, $tkn;
1440             return;
1441             },
1442             vld => {
1443             infix => sub {
1444             # my ( $self, $def, $ctx, $tkn, $tokens ) = @_;
1445             my ( $self, undef, $ctx, $tkn, $tokens ) = @_;
1446             @{ $ctx->[-1]{value} }
1447             or $self->wail( "'$tkn' requires a left argument" );
1448             @{ $tokens }
1449             or $self->wail( "'$tkn' requires a right argument" );
1450             return;
1451             },
1452             prefix => sub {
1453             # my ( $self, $def, $ctx, $tkn, $tokens ) = @_;
1454             my ( $self, undef, undef, $tkn, $tokens ) = @_;
1455             @{ $tokens }
1456             or $self->wail( "'$tkn' requires an argument" );
1457             return;
1458             },
1459             terminal => sub {
1460             # my ( $self, $def, $ctx, $tkn, $tokens ) = @_;
1461             my ( $self, undef, $ctx, $tkn, $tokens ) = @_;
1462             @{ $ctx->[-1]{value} }
1463             or $self->wail( "'$tkn' requires a left argument" );
1464             if ( $ctx->[-1]{dispatch} ) {
1465             @{ $tokens }
1466             or $self->wail( "Command required after '$tkn'" );
1467             } else {
1468             @{ $tokens }
1469             and $self->wail( "Command not allowed after '$tkn'" );
1470             }
1471             return;
1472             }
1473             },
1474             );
1475              
1476             sub elsif : method Verb() Tweak( -unsatisfied ) { ## no critic (ProhibitBuiltInHomonyms)
1477 2     2 1 5 my ( $self, @args ) = @_;
1478             @args
1479 2 50       5 or $self->wail( 'Arguments required' );
1480              
1481 2         33 @{ $self->{frame} } > 1
1482             and 'begin' eq $self->{frame}[-1]{type}
1483             and 'if' eq $self->{frame}[-2]{type}
1484 2 50 33     2 or $self->wail( 'Elsif without if ... then begin' );
      33        
1485              
1486 2         11 my @ctx = ( {
1487             dispatch => 0,
1488             value => [],
1489             } );
1490              
1491             # If any previous if() or elsif() evaluates true, we do not
1492             # evaluate subsequent elsif() calls.
1493             $self->{frame}[-2]{condition}
1494 2 50       36 and return;
1495 2         8 return $self->__infix_engine( \%define, \@ctx, @args );
1496 20     20   50726 }
  20         82  
  20         149  
1497              
1498             sub if : method Verb() Tweak( -unsatisfied ) { ## no critic (ProhibitBuiltInHomonyms)
1499 22     22 1 73 my ( $self, @args ) = @_;
1500             @args
1501 22 50       39 or $self->wail( 'Arguments required' );
1502 22         78 my @ctx = ( {
1503             dispatch => 1,
1504             value => [],
1505             } );
1506 22         58 return $self->__infix_engine( \%define, \@ctx, @args );
1507 20     20   7546 }
  20         48  
  20         125  
1508             }
1509              
1510             sub init {
1511 0     0 1 0 my ( $self, @args ) = @_;
1512              
1513 0 0       0 my $opt = HASH_REF eq ref $args[0] ? shift @args : {};
1514 0         0 my $init_file = shift @args;
1515              
1516 0         0 $self->{initfile} = undef;
1517              
1518 0 0       0 foreach (
1519             defined $init_file ? (
1520             sub {
1521             # A missing init file is only an error if it was
1522             # specified explicitly.
1523 0 0 0 0   0 -e $init_file
1524             and not -d _
1525             or $self->wail(
1526             "Initialization file $init_file not found, or is a directory"
1527             );
1528             return ( $init_file, $opt->{level1} )
1529 0         0 },
1530             ) : (
1531 0     0   0 sub { return $ENV{SATPASS2INI} },
1532 0     0   0 sub { $self->initfile( { quiet => 1 } ) },
1533 0     0   0 sub { return ( $ENV{SATPASSINI}, 1 ) },
1534             \&_init_file_01,
1535             )
1536             ) {
1537              
1538 0         0 my ( $fn, $level1 ) = $_->($self);
1539 0 0       0 my $reader = $self->_file_reader( $fn, { optional => 1 } )
1540             or next;
1541 0         0 $self->{initfile} = $fn;
1542 0         0 return $self->source( { level1 => $level1 }, $reader );
1543              
1544             }
1545              
1546 0         0 return;
1547             }
1548              
1549             sub initfile : Verb( create-directory! quiet! ) {
1550 0     0 1 0 my ( $self, $opt ) = __arguments( @_ ); # @args unused
1551              
1552             my $init_dir = my_dist_config(
1553 0         0 { create => $opt->{'create-directory'} } );
1554              
1555             defined $init_dir
1556 0 0       0 or do {
1557 0 0       0 $opt->{quiet} and return;
1558 0         0 $self->wail(
1559             'Init file directory not found' );
1560             };
1561              
1562 0         0 return File::Spec->catfile( $init_dir, 'satpass2rc' );
1563 20     20   14351 }
  20         49  
  20         104  
1564              
1565             sub _in_unsatisfied_if {
1566 290     290   605 my ( $self ) = @_;
1567 290 50       478 return @{ $self->{frame} } ? $self->{frame}[-1]{unsatisfied_if} : 0;
  290         1120  
1568             }
1569              
1570             # This is a generalized infix expression engine. It does not implement
1571             # operator precedence and is therefore very small. The arguments are:
1572             # - $self is the invocant, which must be an
1573             # Astro::App::Satpass2::Copier.
1574             # - $def is the hash that defines the grammar. This provides the
1575             # following keys:
1576             # {done} is an optional code reference. If present, the code
1577             # reference is called once the parse is complete, and passed
1578             # ( $self, $def, $ctx, \@tokens ). It returns nothing. The intent
1579             # is to throw an exception if the parse is incomplete.
1580             # {oper} defines the operators. This is a hash keyed by the literal
1581             # operator (i.e. '+' to implement a '+' operator), and having the
1582             # following values:
1583             # {handler} is a required code reference, which implements the
1584             # operator. It is passed ( $self, $def, $ctx, \@tokens ). The
1585             # @tokens do not include the operator itself.
1586             # {validation} is an optional validation specification. If
1587             # present it is a key in the {vld} (see below).
1588             # {val} is an optional code reference. If present, it is called if a
1589             # token is not recognized as an operator, and passed ( $self,
1590             # $def, $ctx, \@tokens ). The @tokens include the unrecognized
1591             # token, which is presumed to be a value, and must be removed
1592             # from @tokens.
1593             # {vld} is a hash of validators. The keys are values in the
1594             # {validation} key documented under {oper} (above), and the
1595             # values are code references which are called with ( $self, $ctx,
1596             # $tkn, \@tokens ) where $tkn is the token being validated, and
1597             # @tokens is the rest of the tokens. This hash must exist if the
1598             # {validation} key is used in {oper}; otherwise it is optional.
1599             # - $ctx is context for the operations. It is not used by the engine
1600             # itself, but the individual operator code will need to use it as
1601             # context for the parse. See if() for an example.
1602             # - @tokens are the tokens to be evaluated by the engine.
1603             sub __infix_engine {
1604 24     24   78 my ( $self, $def, $ctx, @tokens ) = @_;
1605             @tokens
1606 24 50       48 or $self->wail( 'Nothing to compute' );
1607 24         35 my $rslt;
1608 24         45 while ( @tokens ) {
1609 50         105 $rslt = $self->_infix_engine_dispatch( $def, $ctx, \@tokens );
1610             }
1611             $def->{done}
1612 24 50       89 and $def->{done}->( $self, $def, $ctx, \@tokens );
1613 24         74 return $rslt;
1614             }
1615              
1616             sub _infix_engine_dispatch {
1617 61     61   98 my ( $self, $def, $ctx, $tokens ) = @_;
1618 61 50       72 @{ $tokens }
  61         110  
1619             or return;
1620 61         69 my $tkn = shift @{ $tokens };
  61         123  
1621 61 100       161 if ( my $info = $def->{oper}{$tkn} ) {
    50          
1622             $info->{validation}
1623 57 100       202 and $def->{vld}{ $info->{validation} }->(
1624             $self, $def, $ctx, $tkn, $tokens );
1625 57         121 return $info->{handler}->( $self, $def, $ctx, $tokens );
1626             } elsif ( $def->{val} ) {
1627 4         12 return $def->{val}->( $self, $def, $ctx, $tkn, $tokens );
1628             } else {
1629 0         0 $self->wail( "Unrecognized token '$tkn'" );
1630             }
1631 0         0 return; # We can't get here, but Perl::Critic does not know this.
1632             }
1633              
1634             # $file_name = _init_file_01()
1635             #
1636             # This subroutine returns the first alternate init file name,
1637             # which is the standard name for the Astro-satpass 'satpass'
1638             # script. If called in list context it returns not only the name,
1639             # but a 1 to tell the caller this is a 'level1' file.
1640              
1641             sub _init_file_01 {
1642 0 0 0 0   0 my $inifn = $^O eq 'MSWin32' || $^O eq 'VMS' || $^O eq 'MacOS' ?
1643             'satpass.ini' : '.satpass';
1644             my $inifile = $^O eq 'VMS' ? "SYS\$LOGIN:$inifn" :
1645             $^O eq 'MacOS' ? $inifn :
1646             $ENV{HOME} ? "$ENV{HOME}/$inifn" :
1647             $ENV{LOGDIR} ? "$ENV{LOGDIR}/$inifn" :
1648 0 0       0 $ENV{USERPROFILE} ? "$ENV{USERPROFILE}" : undef;
    0          
    0          
    0          
    0          
1649 0 0       0 return wantarray ? ( $inifile, 1 ) : $inifile;
1650             }
1651              
1652             sub list : Verb( choose=s@ ) {
1653 7     7 1 45 my ( $self, $opt, @args ) = __arguments( @_ );
1654              
1655             @args
1656             and not $opt->{choose}
1657 7 50 33     51 and $opt->{choose} = \@args;
1658 7         77 my @bodies = $self->__choose( $opt->{choose}, $self->{bodies} );
1659              
1660             @bodies
1661 7 100       85 and return $self->__format_data(
1662             list => \@bodies, $opt );
1663              
1664             $self->{warn_on_empty}
1665 2 50       16 and $self->whinge( 'The observing list is empty' );
1666              
1667 2         8 return;
1668 20     20   18522 }
  20         42  
  20         99  
1669              
1670             sub load : Verb( verbose! ) {
1671 6     6 1 40 my ( $self, $opt, @names ) = __arguments( @_ );
1672 6 50       30 @names or $self->wail( 'No file names specified' );
1673              
1674 6         30 my $attrs = {
1675             illum => $self->get( 'illum' ),
1676             model => $self->get( 'model' ),
1677             sun => $self->_sky_object( 'sun' ),
1678             };
1679              
1680 6         612 foreach my $fn ( @names ) {
1681 6 50       20 $opt->{verbose} and warn "Loading $fn\n";
1682 6         109 my $data = $self->_file_reader( $fn, { glob => 1 } );
1683 5         305 $self->__add_to_observing_list(
1684             Astro::Coord::ECI::TLE->parse( $attrs, $data ) );
1685             }
1686 5         24 return;
1687 20     20   8102 }
  20         49  
  20         112  
1688              
1689             sub localize : Verb( all|except! ) {
1690 1     1 1 10 my ( $self, $opt, @args ) = __arguments( @_ );
1691              
1692 1         4 foreach my $name ( @args ) {
1693 2         5 $self->_attribute_exists( $name );
1694             }
1695              
1696 1 50       4 if ( $opt->{all} ) {
1697 0         0 my %except = map { $_ => 1 } @args;
  0         0  
1698 0         0 @args = grep { ! $except{$_} } sort keys %mutator;
  0         0  
1699             }
1700              
1701 1         3 foreach my $name ( @args ) {
1702 2         6 $self->_localize( $name );
1703             }
1704              
1705 1         3 return;
1706 20     20   8426 }
  20         51  
  20         110  
1707              
1708             sub _localize {
1709 2     2   6 my ( $self, $key ) = @_;
1710              
1711             my $val = exists $self->{$key} ?
1712 2 50       6 $self->{$key} :
1713             $self->get( $key );
1714 2 50 33     9 my $clone = ( blessed( $val ) && $val->can( 'clone' ) ) ?
    50          
1715             $val->clone() :
1716             ref $val ? Clone::clone( $val ) : $val;
1717              
1718 2         6 $self->{frame}[-1]{local}{$key} = $val;
1719 2 50       6 if ( exists $self->{$key} ) {
1720 2         4 $self->{$key} = $clone;
1721             } else {
1722 0         0 $self->set( $key => $clone );
1723             }
1724              
1725 2         4 return;
1726             }
1727              
1728             sub location : Verb( dump! ) {
1729 3     3 1 14 my ( $self, $opt ) = __arguments( @_ );
1730 3         19 return $self->__format_data(
1731             location => $self->station(), $opt );
1732 20     20   9266 }
  20         63  
  20         122  
1733              
1734             {
1735              
1736             # TODO the %mac_cmd hash is only needed for level1 compatibility.
1737             # Once that goes away, it can too PROVIDED we also drop the
1738             # subcommand defaulting functionality.
1739             # Subcommand defaulting dropped 2021-09-20 unless explicitly level1,
1740             # after I discovered that my init file defined an unwanted macro
1741             # when I mistyped 'define' as 'defined'.
1742             my %mac_cmd;
1743             {
1744             my $stb = __PACKAGE__ . '::';
1745             my @cmdnam;
1746             {
1747 20     20   4851 no strict qw{ refs };
  20         49  
  20         9537  
1748             foreach my $entry ( keys %{ $stb } ) {
1749             $entry =~ m/ \A _macro_sub_ ( \w+ ) /smx
1750             or next;
1751             # Strictly speaking I should make sure the {CODE} slot
1752             # is occupied here.
1753             push @cmdnam, $1;
1754             }
1755             }
1756             my %abbr = abbrev(@cmdnam);
1757             foreach (keys %abbr) {
1758             $mac_cmd{'-' . $_} = $abbr{$_};
1759             }
1760             foreach (@cmdnam) {
1761             $mac_cmd{$_} = $_;
1762             }
1763             }
1764              
1765             # NOTE that we must not define command options here, but on the
1766             # individual _macro_sub_* methods. Or at least we must not define
1767             # any command options here that get passed to the _macro_sub_*
1768             # methods.
1769             sub macro : Verb() Tweak( -completion _readline_complete_subcommand ) {
1770 29     29 1 181 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
1771 29         91 my $cmd;
1772 29 50       197 if (!@args) {
    100          
1773 0         0 $cmd = 'brief';
1774             } elsif ( $self->{frame}[-1]{level1} ) {
1775 8 50       36 if ($mac_cmd{$args[0]}) {
    50          
1776 0         0 $cmd = $mac_cmd{shift @args};
1777             } elsif (@args > 1) {
1778 8         17 $cmd = 'define';
1779             } else {
1780 0         0 $cmd = 'list';
1781             }
1782             } else {
1783 21 50       128 defined( $cmd = $mac_cmd{ $args[0] } )
1784             or $cmd = $args[0];
1785 21         49 shift @args;
1786             }
1787              
1788 29 50       236 my $code = $self->can( "_macro_sub_$cmd" )
1789             or $self->wail( "Subcommand '$cmd' unknown" );
1790 29         125 return $code->( $self, @args );
1791 20     20   161 }
  20         41  
  20         161  
1792             }
1793              
1794             # Calls to the following _macro_sub_... methods are generated dynamically
1795             # above, so there is no way Perl::Critic can find them.
1796             sub _macro_sub_brief : Verb() Tweak( -completion _macro_list_complete ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1797 3     3   16 my ( $self, undef, @args ) = __arguments( @_ );
1798 3         11 my $output;
1799 3 50       14 foreach my $name (sort @args ? @args : keys %{$self->{macro}}) {
  3         21  
1800 1 50       10 $self->{macro}{$name} and $output .= $name . "\n";
1801             }
1802 3         13 return $output;
1803 20     20   7499 }
  20         47  
  20         115  
1804              
1805             sub _macro_sub_define : Verb( completion=s@ ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1806 16     16   63 my ( $self, $opt, $name, @args ) = __arguments( @_ );
1807 16         59 my $output;
1808 16 50       60 defined $name
1809             or return $self->__wail( 'You must provide a name for the macro' );
1810             @args
1811 16 50       44 or return $self->__wail( 'You must provide a definition for the macro' );
1812              
1813 16 50 33     136 $name !~ m/ \W /smx
1814             and $name !~ m/ \A _ /smx
1815             or return $self->__wail("Invalid macro name '$name'");
1816              
1817             # NOTE the value of {def} used to be unescaped, but I do not now
1818             # know why, and the implementation of \U and friends is more natural
1819             # with this stripped out.
1820             $self->{macro}{$name} =
1821             Astro::App::Satpass2::Macro::Command->new(
1822             name => $name,
1823             parent => $self,
1824             completion => $opt->{completion},
1825             def => \@args,
1826             generate => \&_macro_define_generator,
1827             level1 => $self->{frame}[-1]{level1},
1828             warner => $self->{_warner},
1829 16         244 );
1830 16         130 return $output;
1831 20     20   10613 }
  20         391  
  20         345  
1832              
1833             sub _macro_define_generator {
1834 9     9   22 my ( $self, @args ) = @_; # $self if Macro object
1835 9         19 my $output;
1836 9         20 foreach my $macro ( @args ) {
1837 9 50       40 if ( my $comp = $self->completion() ) {
1838 0         0 $output .= "macro define \\\n " .
1839             "--completion '@$comp' \\\n " .
1840             "$macro \\\n ";
1841             } else {
1842 9         40 $output .= "macro define $macro \\\n ";
1843             }
1844 9         35 $output .= join( " \\\n ", map { quoter( $_ ) } $self->def() ) .
  17         57  
1845             "\n";
1846             }
1847 9         64 return $output;
1848             }
1849              
1850             sub _macro_sub_delete : Verb() Tweak( -completion _macro_list_complete ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1851 1     1   7 my ( $self, undef, @args ) = __arguments( @_ );
1852 1         4 my $output;
1853 1 50       6 foreach my $name (@args ? @args : keys %{$self->{macro}}) {
  0         0  
1854 1         24 delete $self->{macro}{$name};
1855             }
1856 1         5 return $output;
1857 20     20   10595 }
  20         164  
  20         724  
1858              
1859             sub _macro_sub_list : Verb() Tweak( -completion _macro_list_complete ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1860 9     9   34 my ( $self, undef, @args ) = __arguments( @_ );
1861 9         25 my $output;
1862 9 100       45 foreach my $name (sort @args ? @args : keys %{$self->{macro}}) {
  1         16  
1863 9 50       43 $self->{macro}{$name}
1864             or next;
1865 9         58 $output .= $self->{macro}{$name}->generator( $name );
1866             }
1867 9         40 return $output;
1868 20     20   8162 }
  20         122  
  20         195  
1869              
1870             sub _macro_sub_load : Verb( lib=s verbose! ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1871 0     0   0 my ( $self, $opt, $name, @args ) = __arguments( @_ );
1872 0         0 my $output;
1873 0 0       0 defined $name
1874             or $self->wail( 'Must provide name of macro to load' );
1875             my %marg = (
1876             name => $name,
1877             parent => $self,
1878             generate => \&_macro_load_generator,
1879             warner => $self->{_warner},
1880 0         0 );
1881             exists $opt->{lib}
1882 0 0       0 and $marg{lib} = $opt->{lib};
1883 0   0     0 my $obj = $self->{_macro_load}{$name} ||=
1884             Astro::App::Satpass2::Macro::Code->new( %marg );
1885 0 0       0 foreach my $mn ( @args ? @args : $obj->implements() ) {
1886             $obj->implements( $mn, required => 1 )
1887 0 0       0 and $self->{macro}{$mn} = $obj;
1888             }
1889 0 0       0 if ( $opt->{verbose} ) {
1890 0         0 ( my $fn = "$name.pm" ) =~ s| :: |/|smxg;
1891 0         0 $output .= "Macro $name\n loaded from $INC{$fn}\n";
1892 0         0 $output .= " implements:\n";
1893 0         0 $output .= " $_\n" for sort $obj->implements();
1894             }
1895 0 0       0 $obj->implements( 'after_load', required => 0 )
1896             and $output .= $self->dispatch( after_load => $opt, $name, @args );
1897 0         0 return $output;
1898 20     20   12961 }
  20         144  
  20         123  
1899              
1900             sub _macro_load_generator {
1901 0     0   0 my ( $self, @args ) = @_;
1902 0         0 my @preamble = qw{ macro load };
1903 0 0       0 if ( $self->has_lib() ) {
1904 0         0 push @preamble, '-lib', $self->lib();
1905 0 0       0 $self->relative()
1906             and push @preamble, '-relative';
1907             }
1908 0         0 push @preamble, $self->name();
1909 0         0 my $output;
1910 0         0 foreach my $macro ( @args ) {
1911 0         0 $output .= quoter( @preamble, $macro ) . "\n";
1912             }
1913 0         0 return $output;
1914             }
1915              
1916             sub magnitude_table : Verb( name! reload! ) {
1917 0     0 1 0 my ( undef, undef, @args ) = __arguments( @_ ); # Invocant, $opt unused
1918              
1919 0 0       0 @args or @args = qw{show};
1920              
1921 0   0     0 my $verb = lc (shift (@args) || 'show');
1922              
1923 0         0 my $output;
1924              
1925 0 0 0     0 if ( $verb eq 'show' || $verb eq 'list' ) {
1926              
1927 0         0 my %data = Astro::Coord::ECI::TLE->magnitude_table( 'show', @args );
1928              
1929 0         0 foreach my $oid ( sort keys %data ) {
1930 0         0 $output .= quoter( 'status', 'add', $oid, $data{$oid} )
1931             . "\n";
1932             }
1933              
1934             } else {
1935 0         0 Astro::Coord::ECI::TLE->magnitude_table( $verb, @args );
1936             }
1937              
1938 0         0 return $output;
1939              
1940 20     20   11705 }
  20         79  
  20         879  
1941              
1942             # Attributes must all be on one line to process correctly under Perl
1943             # 5.8.8.
1944             sub pass : Verb( :compute __pass_options ) {
1945 20     20 1 120 my ( $self, $opt, @args ) = __arguments( @_ );
1946              
1947             $opt->{ephemeris}
1948 20 100       110 and $opt->{almanac} = 1;
1949             $opt->{almanac}
1950             and not defined $opt->{ephemeris}
1951             and $opt->{ephemeris} = {
1952             pass_ics => 1,
1953 20 100 100     109 }->{$opt->{_template}};
1954              
1955 20         138 $self->_apply_boolean_default(
1956             $opt, 0, qw{ horizon illumination transit appulse } );
1957 20         86 $self->_apply_boolean_default( $opt, 0, qw{ am pm } );
1958 20 50 66     103 $opt->{am} or $opt->{pm} or $opt->{am} = $opt->{pm} = 1;
1959 20         122 my $pass_start = $self->__parse_time (
1960             shift @args, $self->_get_day_noon());
1961 20   100     106 my $pass_end = $self->__parse_time (shift @args || '+7');
1962 20 50       71 $pass_start >= $pass_end
1963             and $self->wail( 'End time must be after start time' );
1964              
1965 20         113 my $sta = $self->station();
1966             my @bodies = $self->__choose( $opt->{choose}, $self->{bodies} )
1967 20 50       7688 or $self->wail( 'No bodies selected' );
1968 20   50     133 my $pass_step = shift @args || 60;
1969              
1970             # Decide which model to use.
1971              
1972 20         67 my $model = $self->{model};
1973              
1974             # Set the station for the objects in the sky.
1975              
1976 20         44 foreach my $body ( @{ $self->{sky} } ) {
  20         96  
1977 41         2246 $body->set( station => $sta );
1978             }
1979              
1980             # Pick up horizon and appulse distance.
1981              
1982 20         1116 my $horizon = deg2rad ($self->{horizon});
1983 20         122 my $appulse = deg2rad ($self->{appulse});
1984 20         226 my $pass_threshold = deg2rad( $self->{pass_threshold} );
1985              
1986             # In order that the interface not be completely rude, the interface
1987             # allows -brightest to specify that you want the 'brightest' event.
1988             # But this is controlled by the pass_variant attribute. So if
1989             # -brightest appears, the pass_variant from it; otherwise we default
1990             # -brightest from the pass_variant attribute. We localize the
1991             # pass_variant attribute before modifying it, since the -brightest
1992             # option is to hold for this call only. We modify it (rather than
1993             # just passing a local copy to the bodies) because
1994             # Formatter::Template needs to know what it is, and modifying this
1995             # object is the obvious way to pass the information.
1996 20         180 local $self->{pass_variant} = $self->{pass_variant};
1997 20 50       78 if ( $opt->{brightest} ) {
    50          
1998 0         0 $self->{pass_variant} |= PASS_VARIANT_BRIGHTEST;
1999             } elsif ( exists $opt->{brightest} ) {
2000 0         0 $self->{pass_variant} &= ~ PASS_VARIANT_BRIGHTEST;
2001             } else {
2002 20         78 $opt->{brightest} = $self->{pass_variant} & PASS_VARIANT_BRIGHTEST;
2003             }
2004 20         40 my $pass_variant = $self->{pass_variant};
2005              
2006             # Foreach body to be modelled
2007              
2008 20         40 my @accumulate; # For chronological output.
2009 20         91 foreach my $tle ( $self->_aggregate( \@bodies ) ) {
2010              
2011             {
2012 39 50       2500 my $mdl = $tle->get('inertial') ? $model :
  39         149  
2013             $tle->get('model');
2014             $tle->set (
2015             appulse => $appulse,
2016             backdate => $self->{backdate},
2017             debug => $self->{debug},
2018             edge_of_earths_shadow => $self->{edge_of_earths_shadow},
2019             geometric => $self->{geometric},
2020             horizon => $horizon,
2021             interval => ( $self->{verbose} ? $pass_step : 0 ),
2022             model => $mdl,
2023             pass_threshold => $pass_threshold,
2024             pass_variant => $pass_variant,
2025             station => $sta,
2026             twilight => $self->{_twilight},
2027             visible => $self->{visible},
2028 39 50       1303 );
2029             }
2030              
2031             eval {
2032             push @accumulate, $self->_pass_select_event( $opt, $tle->pass (
2033 39         276 $pass_start, $pass_end, $self->{sky} ) );
2034 39         242 1;
2035 39 50       12430 } or do {
2036 0 0       0 $@ =~ m/ \Q$interrupted\E /smxo and $self->wail($@);
2037 0 0       0 $opt->{quiet} or $self->whinge($@);
2038             };
2039             }
2040              
2041 20         130 @accumulate = $self->__pass_filter_am_pm( $opt, @accumulate );
2042              
2043             $opt->{chronological}
2044 20 100       106 and @accumulate = sort { $a->{time} <=> $b->{time} }
  0         0  
2045             @accumulate;
2046              
2047             # Record number of events found.
2048             # NOTE that in this case an event is an entire pass.
2049              
2050 20         95 $self->{events} += @accumulate;
2051              
2052 20 100       102 if ( $opt->{almanac} ) {
2053 4         12 my %almanac;
2054 4         13 foreach my $pass ( @accumulate ) {
2055 6         49 my $illum = $pass->{body}->get( 'illum' );
2056 6         108 my $noon = $self->_get_day_noon( $pass->{time} );
2057 6   33     366 $almanac{$noon}{$illum} ||= do {
2058 6         12 my @day;
2059              
2060             my @events = grep { {
2061             horizon => 1,
2062             twilight => 1,
2063             }->{$_->{almanac}{event}}
2064 36         1065888 } $illum->almanac_hash(
2065 6         35 $self->_get_day_midnight( $pass->{time} ) );
2066              
2067 6         92 _almanac_localize( @events );
2068              
2069 6         15 foreach my $evt ( @events ) {
2070 24         44 $evt->{event} = 'almanac';
2071 24 100       55 my $pm = $evt->{time} >= $noon ? 1 : 0;
2072 24         38 push @{ $day[$pm] }, $evt;
  24         52  
2073             }
2074              
2075 6         33 \@day;
2076             };
2077              
2078 6 50       41 $pass->{_pm} = my $pm = $pass->{time} >= $noon ? 1 : 0;
2079             # TODO this way ALL passes get the almanac events. Is this
2080             # what I want? It varies. For --ics it is. For --events it
2081             # is not. For neither it's probably not.
2082 6 100       27 if ( $opt->{ephemeris} ) {
2083 3         18 @{ $pass->{events} } = sort { $a->{time} <=> $b->{time}
2084 3         6 } @{ $pass->{events} }, @{ $almanac{$noon}{$illum}[$pm] };
  26         46  
  3         8  
  3         25  
2085             }
2086             }
2087              
2088 4 100       40 unless( $opt->{ephemeris} ) {
2089 2         7 foreach my $pass ( @accumulate ) {
2090             $pass->{_pm}
2091 3 50       12 or next;
2092 0         0 my $illum = $pass->{body}->get( 'illum' );
2093 0         0 my $noon = $self->_get_day_noon( $pass->{time} );
2094 0 0       0 $almanac{$noon}{$illum}[1]
2095             or next;
2096 0         0 @{ $pass->{events} } = sort { $a->{time} <=> $b->{time} }
  0         0  
2097 0         0 @{ $pass->{events} },
2098 0         0 @{ $almanac{$noon}{$illum}[1] };
  0         0  
2099 0         0 $almanac{$noon}{$illum}[1] = undef;
2100             }
2101 2         6 foreach my $pass ( reverse @accumulate ) {
2102             $pass->{_pm}
2103 3 50       12 and next;
2104 3         23 my $illum = $pass->{body}->get( 'illum' );
2105 3         66 my $noon = $self->_get_day_noon( $pass->{time} );
2106 3 50       213 $almanac{$noon}{$illum}[0]
2107             or next;
2108 3         18 @{ $pass->{events} } = sort { $a->{time} <=> $b->{time} }
  26         58  
2109 3         33 @{ $pass->{events} },
2110 3         11 @{ $almanac{$noon}{$illum}[0] };
  3         22  
2111 3         36 $almanac{$noon}{$illum}[0] = undef;
2112             }
2113             }
2114             }
2115              
2116             return $self->__format_data(
2117 20         143 $opt->{_template} => \@accumulate, $opt );
2118              
2119 20     20   31254 }
  20         52  
  20         147  
2120              
2121             sub __pass_filter_am_pm {
2122 20     20   78 my ( $self, $opt, @accumulate ) = @_;
2123 20   50     69 $opt ||= {};
2124             $opt->{am} xor $opt->{pm}
2125 20 100 75     199 or return @accumulate;
2126             return (
2127 6         17 map { $_->[0] }
2128 12   50     174 grep { $opt->{am} xor $_->[1] }
2129 2         8 map { [
2130             $_,
2131             $_->{time} >= $self->_get_day_noon( $_->{time} )
2132 12         444 ] } @accumulate
2133             );
2134             }
2135              
2136             sub __pass_options {
2137 20     20   64 my ( $self, $opt ) = @_;
2138             return [
2139 20         118 qw{
2140             almanac! am! appulse! brightest|magnitude! choose=s@
2141             chronological! ephemeris! dump! horizon|rise|set!
2142             illumination! pm!
2143             quiet! transit|maximum|culmination!
2144             },
2145             $self->_templates_to_options( pass => $opt ),
2146             ];
2147             }
2148              
2149             {
2150             my @selector;
2151             $selector[ PASS_EVENT_SHADOWED ] = 'illumination';
2152             $selector[ PASS_EVENT_LIT ] = 'illumination';
2153             $selector[ PASS_EVENT_DAY ] = 'illumination';
2154             $selector[ PASS_EVENT_RISE ] = 'horizon';
2155             $selector[ PASS_EVENT_MAX ] = 'transit';
2156             $selector[ PASS_EVENT_SET ] = 'horizon';
2157             $selector[ PASS_EVENT_APPULSE ] = 'appulse';
2158             $selector[ PASS_EVENT_START ] = 'horizon';
2159             $selector[ PASS_EVENT_END ] = 'horizon';
2160             $selector[ PASS_EVENT_BRIGHTEST ] = 'brightest';
2161              
2162             # Remove from the pass data any events that are not wanted. The
2163             # arguments are $self, the $opt hash reference that (among other
2164             # things) specifies the desired events, and the passes, each pass
2165             # being an argument. The modified passes are returned.
2166             sub _pass_select_event {
2167 39     39   60811364 my ( undef, $opt, @passes ) = @_; # Invocant unused
2168 39         114 my @rslt;
2169 39         142 foreach my $pass ( @passes ) {
2170 38         188 @{ $pass->{events} } = grep {
2171             _pass_select_event_code( $opt, $_->{event} )
2172 38 50       119 } @{ $pass->{events} }
  136         418  
  38         153  
2173             and push @rslt, $pass;
2174             }
2175             return @rslt
2176 39         150 }
2177              
2178             # Determine whether an event is to be reported for the pass. The
2179             # arguments are the $opt hash reference and the event code or name.
2180             # Anything that is not a dualvar and not an integer is accepted, on
2181             # the presumption that it is an ad-hoc event provided by some
2182             # subclass. The null event is always accepted on the presumption
2183             # that if the user did not want it he or she would not have asked
2184             # for it. Anything that is left is accepted or rejected based on the
2185             # option hash and the @selector array (defined above).
2186             sub _pass_select_event_code {
2187 136     136   353 my ( $opt, $event ) = @_;
2188 136 50 33     410 isdual( $event )
2189             or $event !~ m/ \D /smx
2190             or return 1;
2191 136 50       346 $event == PASS_EVENT_NONE
2192             and return 1;
2193 136   66     693 return defined $selector[ $event ] && $opt->{ $selector[ $event ] };
2194             }
2195             }
2196              
2197             sub perl : Tokenize( -noexpand_tilde ) : Verb( eval! setup! ) {
2198 2     2 1 22 my ( $self, $opt, $file, @args ) = __arguments( @_ );
2199 2 50       24 defined $file
2200             or $self->wail( 'At least one argument is required' );
2201             $opt->{setup}
2202 2 50 0     18 and push @{ $self->{_perl} ||= [] }, [ $opt, $file, @args ];
  0         0  
2203 2         16 local @ARGV = ( $self, map { $self->expand_tilde( $_ ) } @args );
  0         0  
2204             $opt->{eval}
2205 2 100       17 or local $0 = $self->expand_tilde( $file );
2206              
2207             my $data = $opt->{eval} ?
2208 2 100       23 $file :
2209             $self->_file_reader( $file, { glob => 1 } );
2210 2         65 my $rslt;
2211             {
2212             # "random" package to prevent whoopsies in our own name space
2213 2         10 package qq_eval_namespace; ## no critic (Modules::ProhibitMultiplePackages)
2214 2         314 $rslt = eval $data; ## no critic (BuiltinFunctions::ProhibitStringyEval)
2215 2 100       44 $@
2216             and $self->wail( "Failed to eval '$file': $@" );
2217             }
2218 1 50       18 instance( $rslt, 'Astro::App::Satpass2' )
2219             or return $rslt;
2220 0         0 return;
2221 20     20   21205 }
  20         50  
  20         125  
2222              
2223             sub phase : Verb( choose=s@ ) {
2224 1     1 1 8 my ( $self, $opt, @args ) = __arguments( @_ );
2225              
2226 1         9 my $time = $self->__parse_time (shift @args, time );
2227              
2228             my @sky = $self->__choose( $opt->{choose}, $self->{sky} )
2229 1 50       10 or $self->wail( 'No bodies selected' );
2230             return $self->__format_data(
2231             phase => [
2232 1         9 map { { body => $_->universal( $time ), time => $time } }
2233 1         5 grep { $_->can( 'phase' ) }
  2         38  
2234             @sky
2235             ], $opt );
2236 20     20   8728 }
  20         53  
  20         110  
2237              
2238             sub position : Verb( choose=s@ questionable|spare! quiet! ) {
2239 4     4 1 35325 my ( $self, $opt, $time ) = __arguments( @_ );
2240              
2241 4 50       21 if ( defined $time ) {
2242 4         26 $time = $self->__parse_time($time);
2243             } else {
2244 0         0 $time = time;
2245             }
2246              
2247             # Define the observing station.
2248              
2249 4         32 my $sta = $self->station();
2250 4         2057 $sta->universal( $time );
2251              
2252             my @list = $self->__choose( { bodies => 1, sky => 1 },
2253 4         298 $opt->{choose} );
2254              
2255 4         18 my @good;
2256 4         21 my $horizon = deg2rad ($self->{horizon});
2257 4         50 foreach my $body (@list) {
2258 13 100       71 if ( $body->represents( 'Astro::Coord::ECI::TLE' ) ) {
2259             $body->set (
2260             backdate => $self->{backdate},
2261             debug => $self->{debug},
2262             edge_of_earths_shadow => $self->{edge_of_earths_shadow},
2263             geometric => $self->{geometric},
2264             horizon => $horizon,
2265             station => $sta,
2266             twilight => $self->{_twilight},
2267 4         158 );
2268             $body->get('inertial')
2269 4 50       1287 and $body->set( model => $self->{model} );
2270             }
2271             eval {
2272 13         51 $body->universal ($time);
2273 10         4574 push @good, $body;
2274 10         45 1;
2275 13 100       556 } or do {
2276 3 50       3244 $@ =~ m/ \Q$interrupted\E /smxo and $self->wail($@);
2277 3 50       28 $opt->{quiet} or $self->whinge($@);
2278             };
2279             }
2280              
2281             return $self->__format_data(
2282             position => {
2283             bodies => \@good,
2284             questionable => $opt->{questionable},
2285             station => $self->station()->universal(
2286             $time ),
2287             time => $time,
2288             twilight => $self->{_twilight},
2289 4         25 }, $opt );
2290 20     20   11932 }
  20         43  
  20         103  
2291              
2292             sub pwd : Verb() {
2293 1     1 1 7112 return Cwd::cwd() . "\n";
2294 20     20   5304 }
  20         54  
  20         98  
2295              
2296             {
2297             my @quarter_name = map { "q$_" } 0 .. 3;
2298              
2299             sub quarters : Verb( choose=s@ dump! q0|new|spring! q1|first|summer! q2|full|fall q3|last|winter ) {
2300 1     1 1 50 my ( $self, $opt, @args ) = __arguments( @_ );
2301              
2302 1         34 my $start = $self->__parse_time (
2303             $args[0], $self->_get_day_midnight() );
2304 1   50     33 my $end = $self->__parse_time ($args[1] || '+30');
2305              
2306 1         12 $self->_apply_boolean_default( $opt, 0, map { "q$_" } 0 .. 3 );
  4         29  
2307              
2308             my @sky = $self->__choose( $opt->{choose}, $self->{sky} )
2309 1 50       22 or $self->wail( 'No bodies selected' );
2310              
2311 1         7 my @almanac;
2312              
2313             # Iterate over any background objects, accumulating all
2314             # quarter-phases of each until we get one after the end time. We
2315             # silently ignore bodies that do not support the next_quarter()
2316             # method.
2317              
2318 1         13 foreach my $body ( @sky ) {
2319 2 50       55 next unless $body->can ('next_quarter_hash');
2320 2         55 $body->universal ($start);
2321              
2322 2         2791 while (1) {
2323 7         89 my $hash = $body->next_quarter_hash();
2324 7 100       275464 $hash->{time} > $end and last;
2325 5 50       49 $opt->{$quarter_name[$hash->{almanac}{detail}]}
2326             or next;
2327 5         20 push @almanac, $hash;
2328             }
2329             }
2330              
2331             # Localize the event descriptions if appropriate.
2332              
2333 1         21 foreach my $event ( @almanac ) {
2334             $event->{almanac}{description} = __localize(
2335             text => [ almanac => $event->{body}->get( 'name' ),
2336             $event->{almanac}{event}, $event->{almanac}{detail}
2337             ],
2338             default => $event->{almanac}{description},
2339             argument => $event->{body},
2340 5         42 );
2341             }
2342              
2343             # Record number of events found
2344              
2345 1         19 $self->{events} += @almanac;
2346              
2347             # Sort and display the quarter-phase information.
2348              
2349             return $self->__format_data(
2350             almanac => [
2351 1         21 sort { $a->{time} <=> $b->{time} }
  9         74  
2352             @almanac
2353             ], $opt );
2354              
2355 20     20   14553 }
  20         45  
  20         96  
2356             }
2357              
2358             {
2359             my $go;
2360              
2361             sub run {
2362 0     0 1 0 my ( $self, @args ) = @_;
2363              
2364             # We can be called statically. If we are, instantiate.
2365 0 0       0 ref $self or $self = $self->new(warning => 1);
2366              
2367             # Put all the I/O into UTF-8 mode.
2368 0         0 binmode STDIN, ':encoding(UTF-8)';
2369 0         0 binmode STDOUT, DEFAULT_STDOUT_LAYERS;
2370 0         0 binmode STDERR, ':encoding(UTF-8)';
2371              
2372             # If the undocumented first option is a code reference, use it to
2373             # get input.
2374 0         0 my $in;
2375 0 0       0 CODE_REF eq ref $args[0]
2376             and $in = shift @args;
2377              
2378             # Parse the command options. -level1 is undocumented.
2379 0         0 my %opt;
2380 0   0     0 $go ||= Getopt::Long::Parser->new();
2381 0 0       0 $go->getoptionsfromarray(
2382             \@args,
2383             \%opt,
2384             qw{
2385             echo! filter! gmt! help initialization_file|initfile=s
2386             level1! version
2387             },
2388             )
2389             or $self->wail( 'See the help method for valid options' );
2390              
2391             # If -version, do it and return.
2392 0 0       0 if ( $opt{version} ) {
2393 0         0 print $self->version();
2394 0         0 return;
2395             }
2396              
2397             # If -help, do it and return.
2398 0 0       0 if ( $opt{help} ) {
2399 0         0 $self->help();
2400 0         0 return;
2401             }
2402              
2403             # Get an input routine if we do not already have one.
2404 0   0     0 $in ||= $self->_get_readline();
2405              
2406             # Some options get processed before we initialize.
2407 0         0 foreach my $name ( qw{ echo filter } ) {
2408             exists $opt{$name}
2409 0 0       0 and $self->set( $name => delete( $opt{$name} ) );
2410             }
2411              
2412             # Display the front matter if desired.
2413 0 0 0     0 (!$self->get('filter') && $self->_get_interactive())
2414             and print $self->version();
2415              
2416             # Execute the initialization file.
2417 0 0       0 eval {
2418             $self->_execute_output( $self->init(
2419             { level1 => delete $opt{level1} },
2420             delete $opt{initialization_file},
2421 0         0 ), $self->get( 'stdout' ) );
2422 0         0 1;
2423             } or warn $@; # Not whinge, since presumably we already did.
2424              
2425             # The remaining options set the corresponding attributes.
2426 0 0       0 %opt and $self->set(%opt);
2427              
2428             # Execution loop. What exit() really does is a last on this.
2429             SATPASS2_EXECUTE:
2430             {
2431 0         0 $self->_execute( @args );
  0         0  
2432 0         0 while ( defined ( my $buffer = $in->( $self->get( 'prompt' ) ) ) ) {
2433 0         0 $self->_execute( $in, $buffer );
2434             }
2435             }
2436 0         0 $self->_execute( q{echo ''} ); # The lazy way to be sure we
2437             # have a newline before exit.
2438 0         0 return;
2439             }
2440             }
2441              
2442             sub save : Verb( changes! overwrite! ) {
2443 0     0 1 0 my ( $self, $opt, $fn ) = __arguments( @_ );
2444              
2445 0 0       0 defined $fn or $fn = $self->initfile( { 'create-directory' => 1 } );
2446 0         0 chomp $fn; # because initfile() adds a newline for printing
2447 0 0 0     0 if ($fn ne '-' && -e $fn) {
2448 0 0       0 -f $fn or $self->wail(
2449             "Can not overwrite $fn: not an ordinary file");
2450 0 0       0 $opt->{overwrite} or do {
2451 0         0 my $rslt = $self->_get_readline()->(
2452             "File $fn exists. Overwrite [y/N]? ");
2453 0 0       0 'y' eq lc substr($rslt, 0, 1)
2454             or return;
2455             };
2456             }
2457 0         0 my @show_opt;
2458 0         0 my $title = 'settings';
2459 0 0       0 if ($opt->{changes}) {
2460 0         0 push @show_opt, '-changes';
2461 0         0 $title = 'setting changes';
2462             }
2463              
2464 0         0 my $output = <<"EOD" .
2465              
2466             # Astro::App::Satpass2 $title
2467              
2468             EOD
2469             $self->show( @show_opt, qw{ -nodeprecated -noreadonly } ) .
2470             <<"EOD" . $self->macro('list');
2471              
2472             # Astro::App::Satpass2 macros
2473              
2474             EOD
2475              
2476 0 0       0 if ( $self->{_perl} ) {
2477 0         0 $output .= <<'EOD';
2478              
2479             # Astro::App::Satpass2 setup
2480              
2481             EOD
2482 0         0 foreach my $item ( @{ $self->{_perl} } ) {
  0         0  
2483 0         0 my ( $opt, @arg ) = @{ $item };
  0         0  
2484 0         0 my @cmd = ( 'perl' );
2485 0         0 push @cmd, map { "-$_" } grep { $opt->{$_} } sort keys %{ $opt };
  0         0  
  0         0  
  0         0  
2486 0         0 $output .= join ' ', quoter( @cmd, @arg );
2487 0         0 $output .= "\n";
2488             }
2489             }
2490              
2491 0         0 foreach my $attribute ( qw{ formatter spacetrack time_parser } ) {
2492 0 0       0 my $obj = $self->get( $attribute ) or next;
2493 0 0 0     0 my $class = $obj->can( 'class_name_of_record' ) ?
2494             $obj->class_name_of_record() :
2495             ref $obj || $obj;
2496 0   0     0 $output .= <<"EOD" .
2497              
2498             # $class $title
2499              
2500             EOD
2501             ( $self->$attribute( $opt, 'config' ) || "# none\n" );
2502             }
2503              
2504 0         0 $output .= $self->_save_sky( $opt );
2505              
2506 0 0       0 if ($fn ne '-') {
2507 0 0       0 my $fh = IO::File->new( $fn, '>:encoding(utf-8)')
2508             or $self->wail("Unable to open $fn: $!");
2509 0         0 print { $fh } $output;
  0         0  
2510 0         0 $output = "$fn\n";
2511             }
2512 0         0 return $output;
2513 20     20   27026 }
  20         50  
  20         104  
2514              
2515             # Formats the commands to reconstitute the sky. This is only called from
2516             # save(), but it is a subroutine for organizational reasons.
2517             sub _save_sky {
2518 0     0   0 my ( $self, $opt ) = @_;
2519              
2520 0         0 my $output = <<'EOD';
2521              
2522             # Astro::App::Satpass2 sky
2523              
2524             EOD
2525              
2526 0         0 foreach my $body ( sort keys %{ $self->{sky_class} } ) {
  0         0  
2527             $opt->{changes}
2528             and $sky_class{$body}
2529 0 0 0     0 and $sky_class{$body} eq $self->{sky_class}{$body}
      0        
2530             and next;
2531 0         0 $output .= $self->_sky_class_components( $body ) . "\n";
2532             }
2533 0         0 foreach my $body ( sort keys ( %sky_class ) ) {
2534 0 0       0 $self->{sky_class}{$body}
2535             or $output .= $self->_sky_class_components( $body ) . "\n";
2536             }
2537              
2538 0         0 my %exclude;
2539 0 0       0 if ( $opt->{changes} ) {
2540 0         0 %exclude = map { $_ => 1 }
  0         0  
2541             SUN_CLASS_DEFAULT, 'Astro::Coord::ECI::Moon';
2542 0         0 foreach my $name ( qw{ sun moon } ) {
2543 0 0       0 defined $self->_find_in_sky( $name )
2544             or $output .= "sky drop $name\n";
2545             }
2546             } else {
2547 0         0 $output .= "sky clear\n";
2548             }
2549 0         0 foreach my $body ( @{ $self->{sky} } ) {
  0         0  
2550 0 0       0 $exclude{ ref $body }
2551             and next;
2552 0         0 $output .= _sky_list_body( $body );
2553             }
2554              
2555 0         0 return $output;
2556             }
2557              
2558             sub set : Verb() {
2559 72     72   432 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
2560              
2561 72         261 while (@args) {
2562 351         925 my ( $name, $value ) = splice @args, 0, 2;
2563 351         1020 $self->_attribute_exists( $name );
2564 351 100       676 if ( _is_interactive() ) {
2565 28 100       131 $nointeractive{$name}
2566             and $self->wail(
2567             "Attribute '$name' may not be set interactively");
2568 27 50 66     192 defined $value and $value eq 'undef'
2569             and $value = undef;
2570             }
2571 350 50       891 if ( $mutator{$name} ) {
2572 350         922 $self->_deprecation_notice( attribute => $name );
2573 350         975 $mutator{$name}->($self, $name, $value);
2574             } else {
2575 0         0 $self->wail("Read-only attribute '$name'");
2576             }
2577             }
2578 71         770 return;
2579 20     20   17659 }
  20         54  
  20         124  
2580              
2581             sub _set_almanac_horizon {
2582 7     7   24 my ( $self, $name, $value ) = @_;
2583 7         38 my $parsed = $self->__parse_angle( { accept => 1 }, $value );
2584 7 50       78 my $internal = looks_like_number( $parsed ) ? deg2rad( $parsed ) :
2585             $parsed;
2586 7         85 my $eci = Astro::Coord::ECI->new();
2587 7         605 $eci->set( $name => $internal ); # To validate.
2588 7         224 $self->{"_$name"} = $internal;
2589 7         88 return( $self->{$name} = $parsed );
2590             }
2591              
2592             {
2593             my $plus_or_minus_90 = sub { $_[0] >= -90 && $_[0] <= 90 };
2594             my %validate = (
2595             horizon => $plus_or_minus_90,
2596             latitude => $plus_or_minus_90,
2597             longitude => sub {
2598             $_[0] > 360
2599             and return 0;
2600             $_[0] > 180
2601             and $_[0] -= 360;
2602             $_[0] >= -180 && $_[0] <= 180;
2603             },
2604             );
2605             sub _set_angle {
2606 31     31   83 my ( $self, $name, $value ) = @_;
2607 31         148 my $angle = $self->__parse_angle( $value );
2608 31 100       135 if ( my $code = $validate{$name} ) {
2609 15 0       64 defined $angle or $self->weep(
    50          
2610             "$name angle is undef for value ", defined $value ? $value : 'undef' );
2611 15 50       124 $code->( $angle )
2612             or $self->wail( "Value $value is invalid for $name" );
2613             }
2614 31         134 $self->{"_$name"} = deg2rad( $angle );
2615 31         334 return ( $self->{$name} = $angle );
2616             }
2617             }
2618              
2619             sub _set_angle_or_undef {
2620 21     21   56 my ( $self, $name, $value ) = @_;
2621 21 100 66     122 defined $value and 'undef' ne $value and goto &_set_angle;
2622 15         62 return ( $self->{$name} = undef );
2623             }
2624              
2625             sub _set_code_ref {
2626 11 50   11   45 CODE_REF eq ref $_[2]
2627             or $_[0]->wail( "Attribute $_[1] must be a code reference" );
2628 11         45 return( $_[0]{$_[1]} = $_[2] );
2629             }
2630              
2631             # Set an attribute whose value is an Astro::App::Satpass2::Copier object
2632             # %arg is a hash of argument name/value pairs:
2633             # {name} is the required name of the attribute to set;
2634             # {value} is the required value of the attribute to set;
2635             # {class} is the optional class that the object must be;
2636             # {default} is the optional default value if the required value is
2637             # undef or '';
2638             # {undefined} is an optional value which, if true, permits the
2639             # attribute to be set to undef;
2640             # {nocopy} is an optional value which, if true, causes the old
2641             # object's attributes not to be copied to the new object;
2642             # {message} is an optional message to emit if the object can not be
2643             # instantiated;
2644             # {prefix} is an optional reference to an array of name prefixes to
2645             # try if the named module does not load.
2646              
2647             sub _set_copyable {
2648 14     14   119 my ( $self, %arg ) = @_;
2649 14         46 my $old = $self->{$arg{name}};
2650 14         30 my $obj;
2651 14 50       76 if ( ref $arg{value} ) {
2652             blessed( $arg{value} )
2653 0 0       0 or $self->wail( "$arg{name} may not be unblessed reference" );
2654 0         0 $obj = $arg{value};
2655             $obj->can( 'warner' )
2656 0 0       0 and $obj->warner( $self->{_warner} );
2657             } else {
2658 14 50       49 if ( defined $arg{default} ) {
2659             defined $arg{value}
2660             and '' ne $arg{value}
2661 14 50 33     95 or $arg{value} = $arg{default};
2662             }
2663 14 50 33     84 if ( ! defined $arg{value} || $arg{value} eq '' ) {
2664             $arg{undefined}
2665 0 0       0 or $self->wail(
2666             "$arg{name} must be defined and not empty",
2667             );
2668 0         0 return ( $self->{$arg{name}} = $arg{value} = undef );
2669             }
2670 14         87 my ( $pkg, @args ) = $self->__parse_class_and_args( $arg{value} );
2671             my $cls = $self->load_package(
2672 14 50       62 { fatal => 'wail' }, $pkg, @{ $arg{prefix} || [] } );
  14         112  
2673 14 50 33     231 not $cls->can( 'init' )
2674             and _is_case_tolerant()
2675             and $self->wail(
2676             "$cls is missing methods. This can happen on a ",
2677             'case-tolerant system if you specify the class ',
2678             'name in the wrong case.' );
2679 14 100       113 $cls->can( 'parent' )
2680             and push @args, parent => $self;
2681             $obj = $cls->new(
2682             warner => $self->{_warner},
2683             @args,
2684             )
2685             or $self->wail( $arg{message} ||
2686 14 50 0     90 "Can not instantiate object from '$arg{value}'" );
2687             }
2688             defined $arg{class}
2689             and not $obj->isa( $arg{class} )
2690 14 50 66     123 and $self->wail( "$arg{name} must be of class $arg{class}" );
2691             blessed( $old )
2692             and not $arg{nocopy}
2693 14 0 33     53 and $old->can( 'copy' )
      33        
2694             and $old->copy( $obj );
2695 14         57 $self->{$arg{name}} = $obj;
2696 14         173 return $arg{value};
2697             }
2698              
2699             sub _set_distance_meters {
2700 9 100   9   60 return ( $_[0]{$_[1]} = defined $_[2] ?
2701             ( $_[0]->__parse_distance( $_[2], '0m' ) * 1000 ) : $_[2] );
2702             }
2703              
2704             sub _set_ellipsoid {
2705 7     7   36 my ($self, $name, $val) = @_;
2706 7         78 Astro::Coord::ECI->set (ellipsoid => $val);
2707 7         331 return ($self->{$name} = $val);
2708             }
2709              
2710             sub _set_formatter {
2711 7     7   26 my ( $self, $name, $val ) = @_;
2712 7         48 return $self->_set_copyable(
2713             name => $name,
2714             value => $val,
2715             message => 'Unknown formatter',
2716             default => 'Astro::App::Satpass2::Format::Template',
2717             prefix => [ 'Astro::App::Satpass2::Format' ]
2718             );
2719             }
2720              
2721             sub _set_formatter_attribute {
2722 24     24   61 my ( $self, $name, $val ) = @_;
2723 24         83 $self->get( 'formatter' )->$name( $val );
2724 24         69 return $val;
2725             }
2726              
2727             sub _set_geocoder {
2728 0     0   0 my ( $self, $name, $val ) = @_;
2729 0         0 return $self->_set_copyable(
2730             name => $name,
2731             value => $val,
2732             class => 'Astro::App::Satpass2::Geocode',
2733             message => 'Unknown formatter',
2734             default => $default_geocoder->(),
2735             undefined => 1,
2736             nocopy => 1,
2737             prefix => [ 'Astro::App::Satpass2::Geocode' ]
2738             );
2739             }
2740              
2741             sub _set_illum_class {
2742 7     7   20 my ( $self, $name, $class ) = @_;
2743 7         16 my $want_class = 'Astro::Coord::ECI';
2744 7 50       24 ref $class and $self->wail( "$name must not be a reference" );
2745 7 50       43 if ( defined $class ) {
2746 7         69 $self->load_package( { fatal => 'wail' }, $class );
2747 7 50       117 $class->isa( $want_class )
2748             or $self->wail( "$name must be an $want_class" );
2749             } else {
2750 0         0 $class = $want_class;
2751             }
2752 7         24 $self->{$name} = $class;
2753 7         26 $self->{_help_module}{$name} = $class;
2754 7         13 foreach my $body ( @{ $self->{bodies} } ) {
  7         38  
2755 0         0 $body->set( $name => $class );
2756             }
2757 7         40 return;
2758             }
2759              
2760             sub _set_model {
2761 7     7   24 my ( $self, $name, $val ) = @_;
2762 7 50       118 Astro::Coord::ECI::TLE->is_valid_model( $val )
2763             or $self->wail(
2764             "'$val' is not a valid Astro::Coord::ECI::TLE model" );
2765 7         49 foreach my $body ( @{ $self->{bodies} } ) {
  7         25  
2766 0         0 $body->set( model => $val );
2767             }
2768 7         30 return ( $self->{$name} = $val );
2769             }
2770              
2771             sub _set_output_layers {
2772 7     7   22 my ( $self, $name, $val ) = @_;
2773              
2774 7 50 33     45 if ( defined $val && '' ne $val ) {
2775 7 50   7   25807 open my $fh, ">$val", File::Spec->devnull()
  7         119  
  7         36  
  7         526  
2776             or $self->wail( "Invalid $name value '$val'" );
2777 7         9680 close $fh;
2778             }
2779 7         117 return ( $self->{$name} = $val );
2780             }
2781              
2782             {
2783             my %variant_def = (
2784             visible_events => PASS_VARIANT_VISIBLE_EVENTS,
2785             fake_max => PASS_VARIANT_FAKE_MAX,
2786             start_end => PASS_VARIANT_START_END,
2787             no_illumination => PASS_VARIANT_NO_ILLUMINATION,
2788             brightest => PASS_VARIANT_BRIGHTEST,
2789             );
2790              
2791             my @option_names;
2792             foreach my $key ( keys %variant_def ) {
2793             if ( $key =~ m/ _ /smx ) {
2794             ( my $dashed = $key ) =~ s/ _ /-/smxg;
2795             $key = "$key|$dashed";
2796             }
2797             push @option_names, "$key!";
2798             }
2799              
2800             my $go;
2801              
2802             sub _set_pass_variant {
2803 8     8   51 my ( $self, $name, $val ) = @_;
2804 8 100       75 if ( $val =~ m/ \A (?: 0 x? ) [0-9]* \z /smx ) {
    50          
2805 7         23 $val = oct $val;
2806             } elsif ( $val !~ m/ \A [0-9]+ \z /smx ) {
2807 1         42 my @args = split qr{ [^\w-] }smx, $val;
2808 1         6 foreach ( @args ) {
2809 1         7 s/ \A (?! - ) /-/smx;
2810             }
2811 1   33     14 $go ||= Getopt::Long::Parser->new();
2812 1         36 $val = $self->get( $name );
2813             $go->getoptionsfromarray( \@args,
2814 0     0   0 none => sub { $val = PASS_VARIANT_NONE },
2815 1 50       16 map { $_ => sub {
2816 1     1   1423 my ( $name, $value ) = @_;
2817 1         7 my $mask = $variant_def{$name};
2818 1 50       13 if ( $value ) {
2819 0         0 $val |= $mask;
2820             } else {
2821 1         4 $val &= ~ $mask;
2822             }
2823 1         7 return;
2824             }
2825 5         34 } @option_names )
2826             or $self->wail( "Invalid $name value '$val'" );
2827             }
2828 8         137 return ( $self->{$name} = $val );
2829             }
2830              
2831             sub _show_pass_variant {
2832 1     1   4 my ( $self, $name ) = @_;
2833 1         6 my $val = $self->get( $name );
2834 1         4 my @options;
2835 1         14 foreach my $key ( keys %variant_def ) {
2836 5 50       17 $val & $variant_def{$key}
2837             and push @options, "$key";
2838             }
2839             @options
2840 1 50       8 or push @options, 'none';
2841 1         10 return ( set => $name, join ',', @options );
2842             }
2843              
2844             sub want_pass_variant {
2845 138     138 1 3254 my ( $self, $variant ) = @_;
2846 138 50       757 $variant_def{$variant}
2847             or $self->wail( "Invalid pass_variant name '$variant'" );
2848 138         656 my $val = $self->get( 'pass_variant' ) & $variant_def{$variant};
2849 138         605 return $val;
2850             }
2851              
2852             }
2853              
2854             sub _set_spacetrack {
2855 0     0   0 my ($self, $name, $val) = @_;
2856 0 0       0 if (defined $val) {
2857 0 0       0 instance($val, 'Astro::SpaceTrack')
2858             or $self->wail("$name must be an Astro::SpaceTrack instance");
2859 0         0 my $version = $val->VERSION();
2860 0         0 $version =~ s/ _ //smxg;
2861 0 0       0 $version >= ASTRO_SPACETRACK_VERSION
2862             or $self->wail("$name must be Astro::SpaceTrack version ",
2863             ASTRO_SPACETRACK_VERSION, ' or greater' );
2864             }
2865 0         0 return ($self->{$name} = $val);
2866             }
2867              
2868             sub _set_stdout {
2869 15     15   73 my ($self, $name, $val) = @_;
2870             $self->{frame}
2871 15 50       78 and $self->{frame}[-1]{$name} = $val;
2872 15         62 return ($self->{$name} = $val);
2873             }
2874              
2875             sub _set_sun_class {
2876 0     0   0 my ( $self, $name, $val ) = @_;
2877 0         0 $self->_attribute_exists( $name );
2878 0         0 return $self->sky( class => $name, $val );
2879             }
2880              
2881             sub _set_time_parser {
2882 7     7   26 my ( $self, $name, $val ) = @_;
2883              
2884 7 50 33     77 if ( CODE_REF eq ref $val ) {
    50          
2885 0         0 $val = _set_time_parser_code( $val );
2886             } elsif ( defined $val and my $macro = $self->{macro}{$val} ) {
2887 0         0 $val = _set_time_parser_code(
2888             $macro->implements( $val, required => 1 ),
2889             $val,
2890             );
2891             }
2892              
2893 7         73 return $self->_set_copyable(
2894             name => $name,
2895             value => $val,
2896             class => 'Astro::App::Satpass2::ParseTime',
2897             message => 'Unknown time parser',
2898             default => 'Astro::App::Satpass2::ParseTime',
2899             nocopy => 1,
2900             prefix => [ 'Astro::App::Satpass2::ParseTime' ],
2901             );
2902             }
2903              
2904             sub _set_time_parser_attribute {
2905 14     14   35 my ( $self, $name, $val ) = @_;
2906 14 50 66     65 defined $val and $val eq 'undef' and $val = undef;
2907 14         119 $self->{time_parser}->$name( $val );
2908 14         27 return $val;
2909             }
2910              
2911             sub _set_time_parser_code {
2912 0     0   0 my ( $code, $name ) = @_;
2913 0         0 require Astro::App::Satpass2::ParseTime::Code;
2914 0         0 my $obj = Astro::App::Satpass2::ParseTime::Code->new();
2915 0         0 return $obj->code( $code, $name );
2916             }
2917              
2918             _frame_pop_force_set ( 'twilight' ); # Force use of the set() method
2919             # in _frame_pop(), because we
2920             # need to set {_twilight} as
2921             # well.
2922             sub _set_twilight {
2923 9     9   51 my ($self, $name, $val) = @_;
2924 9 50       55 if (my $key = $twilight_abbr{lc $val}) {
2925 9         33 $self->{$name} = $key;
2926 9         33 $self->{_twilight} = $twilight_def{$key};
2927             } else {
2928 0         0 my $angle = $self->__parse_angle( { accept => 1 }, $val );
2929 0 0       0 looks_like_number( $angle )
2930             or $self->wail( 'Twilight must be number or known keyword' );
2931 0         0 $self->{$name} = $val;
2932 0         0 $self->{_twilight} = deg2rad ($angle);
2933             }
2934 9         43 return $val;
2935             }
2936              
2937             sub _set_tz {
2938 7     7   31 my ( $self, $name, $val ) = @_;
2939 7         35 $self->_set_formatter_attribute( $name, $val );
2940 7         53 $self->_set_time_parser_attribute( $name, $val );
2941 7         32 return $val;
2942             }
2943              
2944             sub _set_unmodified {
2945 165     165   567 return ($_[0]{$_[1]} = $_[2]);
2946             }
2947              
2948             sub _set_warner_attribute {
2949 0     0   0 my ( $self, $name, $val ) = @_;
2950 0 0 0     0 defined $val and $val eq 'undef' and $val = undef;
2951 0         0 $self->{_warner}->$name( $val );
2952 0         0 return $val;
2953             }
2954              
2955             sub _set_webcmd {
2956 7     7   21 my ($self, $name, $val) = @_;
2957             # TODO warn if $val is true but not '1'.
2958 7 50       29 if ( my $st = $self->get( 'spacetrack' ) ) {
2959             # TODO once spacetrack supports '1', just pass $val.
2960 0         0 $st->set( webcmd => $self->_get_browser_command( $val ) );
2961             }
2962 7         32 return ($self->{$name} = $val);
2963             }
2964              
2965             sub show : Verb( changes! deprecated! readonly! ) Tweak( -completion _readline_complete_subcommand ) {
2966 23     23 1 136 my ( $self, $opt, @args ) = __arguments( @_ );
2967              
2968 23         117 foreach my $name ( qw{ deprecated readonly } ) {
2969 46 50       200 exists $opt->{$name} or $opt->{$name} = 1;
2970             }
2971 23         45 my $output;
2972              
2973 23 50       92 unless ( @args ) {
2974 0         0 foreach my $name ( sort keys %accessor ) {
2975 0 0       0 $self->_attribute_exists( $name, query => 1 )
2976             or next;
2977 0 0       0 $nointeractive{$name}
2978             and next;
2979             exists $mutator{$name}
2980             or $opt->{readonly}
2981 0 0 0     0 or next;
2982 0         0 my $depr;
2983             ( $depr = $self->_deprecation_in_progress( attribute =>
2984             $name ) )
2985 0 0 0     0 and ( not $opt->{deprecated} or $depr >= 3 )
      0        
2986             and next;
2987 0         0 push @args, $name;
2988             }
2989             }
2990              
2991 23         55 foreach my $name (@args) {
2992 23 50       85 exists $shower{$name}
2993             or $self->wail("No such attribute as '$name'");
2994              
2995 23         123 my @val = $shower{$name}->( $self, $name );
2996 23 50       78 if ( $opt->{changes} ) {
2997 20     20   81609 no warnings qw{ uninitialized };
  20         45  
  20         3952  
2998 0 0       0 $static{$name} eq $val[-1] and next;
2999             }
3000              
3001 23 50       71 exists $mutator{$name} or unshift @val, '#';
3002 23         139 $output .= quoter( @val ) . "\n";
3003             }
3004 23         99 return $output;
3005 20     20   177 }
  20         41  
  20         108  
3006              
3007             sub _show_copyable {
3008 0     0   0 my ( $self, $name ) = @_;
3009 0         0 my $obj = $self->get( $name );
3010 0         0 my $val = $obj->class_name_of_record();
3011 0         0 return ( 'set', $name, $val );
3012             }
3013              
3014             sub _show_formatter_attribute {
3015 2     2   7 my ( $self, $name ) = @_;
3016 2         10 my $val = $self->{formatter}->decode( $name );
3017 2         6 return ( qw{ formatter }, $name, $val );
3018             }
3019              
3020             # Calls to the following _show_sub method are generated dynamically
3021             # above, so there is no way Perl::Critic can find them.
3022              
3023             sub _show_sub { ## no critic (ProhibitUnusedPrivateSubroutines)
3024             # my ( $app, $text, $line, $start, @arg ) = @_;
3025 0     0   0 my ( undef, undef, undef, undef, @arg ) = @_;
3026 0 0       0 @arg > 1
3027             or return;
3028 0         0 my $re = qr/ \A \Q$arg[-1]\E /smx;
3029 0         0 return [ grep { $_ =~ $re } sort keys %accessor ];
  0         0  
3030             }
3031              
3032             sub _show_sun_class {
3033 0     0   0 my ( $self, $name ) = @_;
3034 0         0 $self->_attribute_exists( $name );
3035 0         0 return $self->_sky_class_components( $name );
3036             }
3037              
3038             sub _show_time_parser {
3039 0     0   0 my ( $self, $name ) = @_;
3040 0         0 my $obj = $self->get( $name );
3041 0         0 my $val = $obj->class_name_of_record();
3042 0 0       0 if ( my $back_end = $obj->back_end() ) {
3043 0         0 $val = "$val,back_end=$back_end";
3044             }
3045 0         0 return ( set => $name, $val );
3046             }
3047              
3048             sub _show_unmodified {
3049 20     20   50 my ($self, $name) = @_;
3050 20         818 my $val = $self->get( $name );
3051 20         106 return ( 'set', $name, $val );
3052             }
3053              
3054             # For proper motion, we need to convert arc seconds per year to degrees
3055             # per second. Perl::Critic does not like 'use constant' because they do
3056             # not interpolate, but they really do: "@{[SPY2DPS]}".
3057              
3058 20     20   15121 use constant SPY2DPS => 3600 * 365.24219 * SECSPERDAY;
  20         51  
  20         9442  
3059              
3060             # Given a body in the sky, encodes it in 'sky add' format
3061             sub _sky_list_body {
3062 8     8   21 my ( $body ) = @_;
3063 8 50       38 if ( embodies( $body, 'Astro::Coord::ECI::TLE' ) ) {
    100          
3064 0         0 return sprintf "sky tle %s\n", quoter(
3065             $body->get( 'tle' ) );
3066             } elsif ( $body->isa( 'Astro::Coord::ECI::Star' ) ) {
3067 1         96 my ( $ra, $dec, $rng, $pmra, $pmdec, $vr ) = $body->position();
3068 1         16 $rng /= PARSEC;
3069 1         8 $pmra = rad2deg( $pmra / 24 * 360 * cos( $ra ) ) * SPY2DPS;
3070 1         16 $pmdec = rad2deg( $pmdec ) * SPY2DPS;
3071 1         9 return sprintf
3072             "sky add %s %s %7.3f %.2f %.4f %.5f %s\n",
3073             quoter( $body->get( 'name' ) ), _rad2hms( $ra ),
3074             rad2deg( $dec ), $rng, $pmra, $pmdec, $vr;
3075             } else {
3076 7         323 return sprintf "sky add %s\n", quoter( $body->get( 'name' ) );
3077             }
3078             }
3079              
3080             sub sky : Verb() Tweak( -completion _readline_complete_subcommand ) {
3081 12     12 1 89 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3082              
3083 12   50     71 my $verb = lc ( shift @args || 'list' );
3084              
3085 12 50       131 if ( my $code = $self->can( "_sky_sub_$verb") ) {
3086 12         57 return $code->( $self, @args );
3087             } else {
3088 0         0 $self->wail("'sky' subcommand '$verb' not known");
3089             }
3090 0         0 return; # We can't get here, but Perl::Critic does not know this.
3091 20     20   151 }
  20         138  
  20         142  
3092              
3093             # Given the name of a potential background object, return its
3094             # definition. This is an array in list context, or a quoted string in
3095             # scalar context.
3096             sub _sky_class_components {
3097 0     0   0 my ( $self, $name ) = @_;
3098 0 0       0 my $info = $self->{sky_class}{ fold_case( $name ) }
3099             or $self->weep( "No class defined for $name" );
3100 0         0 my ( $class, @attr ) = @{ $info };
  0         0  
3101             # We rely on sky( class => $name, $class, ... ) keeping the name
3102             # last.
3103 0         0 $name = pop @attr;
3104 0         0 pop @attr; # 'name';
3105 0         0 my @parts = ( qw{ sky class }, $name, $class, @attr );
3106             wantarray
3107 0 0       0 and return @parts;
3108 0         0 return join ' ', map { quoter( $_ ) } @parts;
  0         0  
3109             }
3110              
3111             # Given the name of a potential sky object, instantiate it. Named
3112             # arguments are optional; the following are supported:
3113             # fatal = Whether failure to find the name is fatal. Default is true.
3114             sub _sky_object {
3115 12     12   54 my ( $self, $name, %opt ) = @_;
3116             defined $opt{fatal}
3117 12 100       63 or $opt{fatal} = 1;
3118 12 100       96 if ( my $info = $self->{sky_class}{ fold_case( $name ) } ) {
    50          
3119 10         26 my ( $class, @attr ) = @{ $info };
  10         58  
3120 10         111 return $class->new( @attr );
3121             } elsif ( $opt{fatal} ) {
3122 0         0 $self->weep( "No class defined for $name" );
3123             }
3124 2         23 return;
3125             }
3126              
3127             # Calls to the following _sky_sub_... methods are generated dynamically
3128             # above, so there is no way Perl::Critic can find them.
3129             #
3130             sub _sky_sub_add : Verb() { ## no critic (ProhibitUnusedPrivateSubroutines)
3131 5     5   20 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3132 5 50       36 my $name = shift @args
3133             or $self->wail( 'You did not specify what to add' );
3134 5 50       30 defined $self->_find_in_sky( $name )
3135             and return;
3136 5 100       26 if ( my $obj = $self->_sky_object( $name, fatal => 0 ) ) {
3137 3         268 push @{ $self->{sky} }, $obj;
  3         9  
3138             } else {
3139 2 100       40 @args >= 2
3140             or $self->wail(
3141             'You must give at least right ascension and declination' );
3142 1         22 my $ra = deg2rad( $self->__parse_angle( shift @args ) );
3143 1         12 my $dec = deg2rad( $self->__parse_angle( shift @args ) );
3144 1 50       28 my $rng = @args ?
3145             $self->__parse_distance( shift @args, '1pc' ) :
3146             10000 * PARSEC;
3147 1 50       5 my $pmra = @args ? do {
3148 1         4 my $angle = shift @args;
3149 1 50       19 $angle =~ s/ s \z //smxi
3150             or $angle *= 24 / 360 / cos( $ra );
3151 1         6 deg2rad( $angle / SPY2DPS );
3152             } : 0;
3153 1 50       12 my $pmdec = @args ? deg2rad( shift( @args ) / SPY2DPS ) : 0;
3154 1 50       7 my $pmrec = @args ? shift @args : 0;
3155 1         8 push @{ $self->{sky} }, Astro::Coord::ECI::Star->new(
3156             debug => $self->{debug},
3157 1         3 name => $name,
3158             sun => $self->_sky_object( 'sun' ),
3159             )->position( $ra, $dec, $rng, $pmra, $pmdec, $pmrec );
3160             }
3161 4         2468 return;
3162 20     20   20692 }
  20         97  
  20         167  
3163              
3164             sub _sky_sub_class : Verb( add! delete! ) { ## no critic (ProhibitUnusedPrivateSubroutines)
3165 0     0   0 my ( $self, $opt, @arg ) = __arguments( @_ );
3166              
3167             $opt->{add}
3168             and $opt->{delete}
3169 0 0 0     0 and $self->wail( 'May not specify both add and delete' );
3170              
3171 0 0       0 if ( $opt->{delete} ) {
    0          
3172 0         0 foreach my $name ( @arg ) {
3173 0 0       0 $name =~ m/ \A sun \z /smxi
3174             and $self->wail( 'Can not remove Sun class' );
3175 0 0       0 defined $self->_find_in_sky( $name )
3176             and $self->wail( 'Can not remove in-use class' );
3177 0         0 delete $self->{sky_class}{ fold_case( $name ) };
3178             }
3179             } elsif ( @arg < 2 ) {
3180             @arg
3181 0 0       0 or @arg = sort keys %{ $self->{sky_class} };
  0         0  
3182             return join '', map {
3183 0         0 $self->_sky_class_components( $_ ) . "\n" }
  0         0  
3184             @arg;
3185             } else {
3186 0         0 my ( $name, $class, @attr ) = @arg;
3187 0         0 $self->load_package( { fatal => 'wail' }, $class );
3188 0 0       0 my $want_class = $name =~ m/ \A sun \z /smxi ?
3189             SUN_CLASS_DEFAULT :
3190             'Astro::Coord::ECI';
3191 0 0       0 embodies( $class, $want_class )
3192             or $self->wail(
3193             "Must be a subclass of $want_class" );
3194             +{ @attr }->{name}
3195 0 0       0 and $self->wail( 'May not specify name explicitly' );
3196             # name must be last, because _sky_class_components()
3197             # needs to recover it.
3198 0         0 push @attr, name => $name;
3199 0         0 my $obj = $class->new( @attr );
3200 0         0 my $folded_name = fold_case( $name );
3201 0         0 $self->{sky_class}{$folded_name} = [ $class, @attr ];
3202             $self->_replace_in_sky( $folded_name, $obj )
3203             or $opt->{add}
3204 0 0 0     0 and push @{ $self->{sky} }, $obj;
  0         0  
3205 0         0 $self->{_help_module}{$folded_name} = $class;
3206 0 0       0 if ( $obj->isa( 'Astro::Coord::ECI::Sun' ) ) {
3207 0         0 foreach my $body (
3208 0         0 @{ $self->{bodies} }, @{ $self->{sky} }
  0         0  
3209             ) {
3210 0         0 $body->set(
3211             sun => $self->_sky_object( 'sun' ),
3212             );
3213             }
3214             }
3215             }
3216              
3217 0         0 return;
3218 20     20   16177 }
  20         68  
  20         138  
3219              
3220             sub _sky_sub_clear : Verb() { ## no critic (ProhibitUnusedPrivateSubroutines)
3221 1     1   7 my ( $self ) = __arguments( @_ ); # $opt and args unused
3222 1         15 @{ $self->{sky} } = ();
  1         6  
3223 1         5 return;
3224 20     20   5759 }
  20         63  
  20         109  
3225              
3226             sub _sky_sub_drop : Verb() Tweak( -completion _sky_body_complete ) { ## no critic (ProhibitUnusedPrivateSubroutines)
3227 1     1   5 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3228 1 50       19 @args or $self->wail(
3229             'You must specify at least one name to drop' );
3230 1         4 foreach my $name ( @args ) {
3231 1         6 $self->_drop_from_sky( $name );
3232             }
3233 1         23 return;
3234 20     20   6076 }
  20         39  
  20         119  
3235              
3236             sub _sky_sub_list : Verb( verbose! ) { ## no critic (ProhibitUnusedPrivateSubroutines)
3237 5     5   26 my ( $self, $opt ) = __arguments( @_ ); # args unused
3238 5         14 my $output;
3239 5         13 foreach my $body (
3240 8         83 map { $_->[1] }
3241 4         109 sort { $a->[0] cmp $b->[0] }
3242 8   33     221 map { [ lc( $_->get( 'name' ) || $_->get( 'id' ) ), $_ ] }
3243 5         25 @{$self->{sky}}
3244             ) {
3245 8         35 $output .= _sky_list_body( $body );
3246 8 50       58 if ( $opt->{verbose} ) {
3247 0         0 $output .= "# Class: @{[ ref $body ]}\n";
  0         0  
3248             }
3249             }
3250 5 100       27 unless (@{$self->{sky}}) {
  5         29  
3251             $self->{warn_on_empty}
3252 1 50       8 and $self->whinge( 'The sky is empty' );
3253             }
3254 5         50 return $output;
3255 20     20   11600 }
  20         61  
  20         167  
3256              
3257             # Undocumented. That means I can revoke at any time, without notice. If
3258             # you need this functionality, please contact me.
3259             sub _sky_sub_load : Verb() { ## no critic (ProhibitUnusedPrivateSubroutines)
3260 0     0   0 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3261 0         0 my $tle;
3262 0         0 foreach my $fn ( @args ) {
3263 0         0 local $/ = undef;
3264 0 0       0 open my $fh, '<', $fn
3265             or $self->wail( "Failed to open $fn: $!" );
3266 0         0 $tle .= <$fh>;
3267 0         0 close $fh;
3268             }
3269 0         0 return $self->_sky_sub_tle( $tle );
3270 20     20   7840 }
  20         70  
  20         113  
3271              
3272             sub _sky_sub_lookup : Verb() { ## no critic (ProhibitUnusedPrivateSubroutines)
3273 0     0   0 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3274 0         0 my $output;
3275 0         0 my $name = shift @args;
3276 0 0       0 defined $self->_find_in_sky( $name )
3277             and $self->wail( "Duplicate sky entry '$name'" );
3278 0         0 my ($ra, $dec, $rng, $pmra, $pmdec, $pmrec) =
3279             $self->_simbad4 ($name);
3280 0         0 $rng = sprintf '%.2f', $rng;
3281 0         0 $output .= 'sky add ' . quoter ($name) .
3282             " $ra $dec $rng $pmra $pmdec $pmrec\n";
3283 0         0 $ra = deg2rad ($self->__parse_angle ($ra));
3284 0         0 my $body = Astro::Coord::ECI::Star->new(
3285             name => $name,
3286             sun => $self->_sky_object( 'sun' ),
3287             );
3288 0         0 $body->position ($ra, deg2rad ($self->__parse_angle ($dec)),
3289             $rng * PARSEC, deg2rad ($pmra * 24 / 360 / cos ($ra) / SPY2DPS),
3290             deg2rad ($pmdec / SPY2DPS), $pmrec);
3291 0         0 push @{$self->{sky}}, $body;
  0         0  
3292 0         0 return $output;
3293 20     20   10472 }
  20         47  
  20         136  
3294              
3295             # Undocumented. That means I can revoke at any time, without notice. If
3296             # you need this functionality, please contact me.
3297             sub _sky_sub_tle : Verb() {
3298 0     0   0 my ( $self, undef, $tle ) = __arguments( @_ ); # $opt unused
3299 0         0 my @bodies = Astro::Coord::ECI::TLE::Set->aggregate(
3300             Astro::Coord::ECI::TLE->parse( $tle ) );
3301 0         0 my %extant = map { $_->get( 'id' ) => 1 }
3302 0         0 grep { embodies( $_, 'Astro::Coord::ECI::TLE' ) }
3303 0         0 @{ $self->{sky} };
  0         0  
3304 0         0 foreach my $body ( @bodies ) {
3305 0         0 my $id = $body->get( 'id' );
3306 0 0       0 $extant{$id}
3307             and $self->wail( "Duplicate sky entry $id" );
3308             }
3309 0         0 push @{ $self->{sky} }, @bodies;
  0         0  
3310 0         0 return sprintf "sky tle %s\n", quoter( $tle );
3311 20     20   9664 }
  20         60  
  20         142  
3312              
3313             sub source : Verb( optional! ) {
3314 8     8 1 40 my ( $self, $opt, $src, @args ) = __arguments( @_ );
3315              
3316 8         28 my $output;
3317 8 100       66 my $reader = $self->_file_reader( $src, $opt )
3318             or return;
3319              
3320 6         26 my @level1_cache;
3321 6         15 my $level1_context = {};
3322             my $fetcher = $opt->{level1} ? sub {
3323             @level1_cache
3324 21 100   21   60 and return shift @level1_cache;
3325 19         39 my $buffer = $reader->();
3326 19         78 @level1_cache = $self->_rewrite_level1_command(
3327             $buffer, $level1_context );
3328 19         71 return shift @level1_cache;
3329 6 100       32 } : $reader;
3330              
3331 6         59 my $frames = $self->_frame_push( source => \@args );
3332             # Note that level1 is unsupported, and works only when the
3333             # options are passed as a hash. It will go away when support for
3334             # the original satpass script is dropped.
3335 6         50 $self->{frame}[-1]{level1} = $opt->{level1};
3336 6         13 my $err;
3337 6 50       15 my $ok = eval { while ( defined( my $input = $fetcher->() ) ) {
  6         32  
3338 13 100       87 if ( defined ( my $buffer = $self->execute( $fetcher,
3339             $input ) ) ) {
3340 2         11 $output .= $buffer;
3341             }
3342             }
3343 6         25 1;
3344             } or $err = $@;
3345              
3346 6         36 $self->_frame_pop( $frames );
3347 6 50       19 $ok or $self->whinge( $err );
3348              
3349 6 100       35 $opt->{level1} and $self->_rewrite_level1_macros();
3350 6         131 return $output;
3351 20     20   11003 }
  20         38  
  20         101  
3352              
3353             {
3354              
3355 20     20   4326 use constant _FORMAT_NONE => undef; # Must be undef
  20         45  
  20         1519  
3356 20     20   137 use constant _FORMAT_DEFAULT => 'json'; # Must be defined
  20         40  
  20         6001  
3357              
3358             my %default_format = (
3359             new => _FORMAT_NONE,
3360             amsat => _FORMAT_NONE,
3361             attribute_names => _FORMAT_NONE,
3362             banner => _FORMAT_NONE,
3363             box_score => _FORMAT_DEFAULT,
3364             celestrak => _FORMAT_DEFAULT,
3365             celestrak_supplemental => _FORMAT_DEFAULT,
3366             cache_hit => _FORMAT_NONE,
3367             content_source => _FORMAT_NONE,
3368             content_type => _FORMAT_NONE,
3369             content_interface => _FORMAT_NONE,
3370             country_names => _FORMAT_DEFAULT,
3371             favorite => _FORMAT_DEFAULT,
3372             file => _FORMAT_DEFAULT,
3373             get => _FORMAT_NONE,
3374             getv => _FORMAT_NONE,
3375             help => _FORMAT_NONE,
3376             iridium_status => _FORMAT_NONE,
3377             launch_sites => _FORMAT_DEFAULT,
3378             login => _FORMAT_NONE,
3379             logout => _FORMAT_NONE,
3380             mccants => _FORMAT_NONE,
3381             names => _FORMAT_NONE, # ??
3382             retrieve => _FORMAT_DEFAULT,
3383             search_date => _FORMAT_DEFAULT,
3384             search_decay => _FORMAT_DEFAULT,
3385             search_id => _FORMAT_DEFAULT,
3386             search_name => _FORMAT_DEFAULT,
3387             search_oid => _FORMAT_DEFAULT,
3388             set => _FORMAT_NONE,
3389             shell => _FORMAT_NONE,
3390             source => _FORMAT_NONE,
3391             spacetrack => _FORMAT_DEFAULT,
3392             spacetrack_query_v2 => _FORMAT_DEFAULT,
3393             update => _FORMAT_DEFAULT,
3394             flush_identity_cache => _FORMAT_NONE,
3395             );
3396              
3397             my %handler = (
3398             config => sub {
3399             my ( $self, $obj, undef, $opt, @args ) = @_; # $method unused
3400             @args or @args = $obj->attribute_names();
3401             my ( $rslt, @values, $virgin );
3402             $opt->{changes}
3403             and $virgin = $self->_get_spacetrack_default();
3404             foreach my $name ( @args ) {
3405             $rslt = $obj->get( $name );
3406             $rslt->is_success()
3407             or return $rslt;
3408             my $value = $rslt->content();
3409 20     20   168 no warnings qw{ uninitialized };
  20         46  
  20         20830  
3410             $opt->{changes}
3411             and $value eq $virgin->getv( $name )
3412             and next;
3413             push @values, [ $name, $value ];
3414             }
3415             if ( $opt->{raw} ) {
3416             $rslt->content( \@values );
3417             } else {
3418             $opt->{raw} and return \@values;
3419             my $output = '';
3420             foreach ( @values ) {
3421             $output .= quoter( qw{ spacetrack set }, @{ $_ } ) . "\n";
3422             }
3423             $rslt->content( $output );
3424             }
3425             return $rslt;
3426             },
3427             get => sub {
3428             my ( undef, $obj, undef, $opt, @args ) = @_; # Invocant, $method unused
3429             my $rslt = $obj->get( @args );
3430             $rslt->is_success
3431             and not $opt->{raw}
3432             and $rslt->content( scalar quoter(
3433             qw{ spacetrack set }, $args[0], $rslt->content() ) );
3434             return $rslt;
3435             },
3436             mccants => sub {
3437             my ( undef, $obj, undef, $opt, @args ) = @_; # Invocant, $method unused
3438             delete $opt->{format}; # You get what he gives you
3439             return $obj->mccants( @args );
3440             },
3441             set => sub {
3442             my ( undef, $obj, $method, undef, @args ) = @_; # Invocant, $opt unused
3443             return $obj->$method( @args );
3444             },
3445             );
3446             $handler{getv} = $handler{get};
3447             $handler{show} = $handler{config};
3448             $handler{spacetrack_query_v2} = $handler{set};
3449              
3450             my %suppress_output = map { $_ => 1 } '', 'set';
3451              
3452             # Attributes must all be on one line to process correctly under
3453             # 5.8.8.
3454             sub spacetrack : Verb( all! changes! descending! effective! end_epoch=s exclude=s format=s last5! raw! rcs! status=s sort=s start_epoch=s tle! verbose! ) {
3455 0     0 1 0 my ( $self, $opt, $method, @args ) = __arguments( @_ );
3456              
3457             exists $opt->{raw}
3458 0 0       0 or $opt->{raw} = ( ! _is_interactive() );
3459              
3460 0         0 my $verbose = delete $opt->{verbose};
3461              
3462 0         0 my $object = $self->_helper_get_object( 'spacetrack' );
3463             $method !~ m/ \A _ /smx and $object->can( $method )
3464 0 0 0     0 or $handler{$method}
      0        
3465             or $self->wail("No such spacetrack method as '$method'");
3466              
3467             $opt->{start_epoch}
3468             and $opt->{start_epoch} = $self->__parse_time(
3469 0 0       0 $opt->{start_epoch} );
3470             $opt->{end_epoch}
3471             and $opt->{end_epoch} = $self->__parse_time(
3472 0 0       0 $opt->{end_epoch} );
3473              
3474 0 0       0 if ( defined( my $df = $default_format{$method} ) ) {
3475             defined $opt->{format}
3476 0 0       0 or $opt->{format} = $df;
3477             } else {
3478             # FIXME I would prefer to error out here, but not sure I
3479             # can.
3480 0         0 delete $opt->{format};
3481             }
3482              
3483 0         0 my ( $rslt, @rest );
3484 0 0       0 if ( $handler{$method} ) {
3485 0         0 ( $rslt, @rest ) = $handler{$method}->(
3486             $self, $object, $method, $opt, @args );
3487             } else {
3488 0         0 delete $opt->{raw};
3489 0         0 ( $rslt, @rest ) = $object->$method( $opt, @args );
3490             }
3491              
3492 0 0       0 $rslt->is_success()
3493             or $self->wail( $rslt->status_line() );
3494              
3495 0         0 my $output;
3496 0   0     0 my $content_type = $object->content_type || '';
3497              
3498 0 0 0     0 if ($content_type eq 'orbit') {
    0          
    0          
3499              
3500 0         0 push @{$self->{bodies}},
  0         0  
3501             Astro::Coord::ECI::TLE->parse ($rslt->content);
3502 0 0       0 $verbose
3503             and $output .= $rslt->content;
3504              
3505             } elsif ($content_type eq 'iridium-status') {
3506              
3507 0         0 $self->_iridium_status( @rest );
3508 0 0       0 $verbose
3509             and $output .= $rslt->content;
3510              
3511             } elsif ( ! $suppress_output{$content_type} || $verbose ) {
3512              
3513 0         0 $output .= $rslt->content;
3514              
3515             }
3516              
3517 0 0       0 defined $output
3518             and $output =~ s/ (?
3519 0         0 return $output;
3520 20     20   168 }
  20         42  
  20         152  
3521              
3522             }
3523              
3524             sub st : Verb() {
3525 0     0 1 0 my ( $self, undef, $func, @args ) = __arguments( @_ ); # $opt unused
3526              
3527 0         0 $self->_deprecation_notice( method => 'st' );
3528 0 0       0 if ( 'localize' eq $func ) {
3529 0         0 my $st = $self->_helper_get_object( 'spacetrack' );
3530 0         0 foreach my $key (@args) {
3531             exists $self->{frame}[-1]{spacetrack}{$key}
3532 0 0       0 or $self->{frame}[-1]{spacetrack}{$key} =
3533             $st->get ($key)->content
3534             }
3535             } else {
3536 0         0 goto &spacetrack;
3537             }
3538 0         0 return;
3539 20     20   8444 }
  20         65  
  20         98  
3540              
3541             sub station {
3542 34     34 1 138 my ( $self ) = @_;
3543              
3544             defined $self->{height}
3545             and defined $self->{latitude}
3546             and defined $self->{longitude}
3547 34 50 33     431 or $self->wail( 'You must set height, latitude, and longitude' );
      33        
3548              
3549             return Astro::Coord::ECI->new (
3550             almanac_horizon => $self->{_almanac_horizon},
3551             horizon => deg2rad( $self->get( 'horizon' ) ),
3552             id => 'station',
3553             name => $self->{location} || '',
3554             refraction => $self->{refraction} || 0,
3555             )->geodetic (
3556             deg2rad( $self->{latitude} ),
3557             deg2rad( $self->{longitude} ),
3558 34   100     510 $self->{height} / 1000
      50        
3559             );
3560             }
3561              
3562             # TODO I must have thought -reload would be good for something, but it
3563             # appears I never implemented it.
3564              
3565             sub status : Verb( name! reload! ) {
3566 3     3 1 12 my ( $self, $opt, @args ) = __arguments( @_ );
3567              
3568 3 100       15 @args or @args = qw{show};
3569              
3570 3   50     25 my $verb = lc (shift (@args) || 'show');
3571              
3572 3 50       10 if ( $verb eq 'iridium' ) {
3573 0         0 $self->_deprecation_notice( status => 'iridium', 'show' );
3574 0         0 $verb = 'show';
3575             }
3576              
3577 3         7 my $output;
3578              
3579 3 100 66     38 if ($verb eq 'add' || $verb eq 'drop') {
    100 33        
    50          
3580              
3581 1         17 Astro::Coord::ECI::TLE->status ($verb, @args);
3582 1         22 foreach my $tle (@{$self->{bodies}}) {
  1         10  
3583 1 50       8 $tle->get ('id') == $args[0] and $tle->rebless ();
3584             }
3585              
3586             } elsif ($verb eq 'clear') {
3587              
3588 1         13 Astro::Coord::ECI::TLE->status ($verb, @args);
3589 1         12 foreach my $tle (@{$self->{bodies}}) {
  1         5  
3590 2         142 $tle->rebless ();
3591             }
3592              
3593             } elsif ($verb eq 'show' || $verb eq 'list') {
3594              
3595 1         7 my @data = Astro::Coord::ECI::TLE->status( 'show', @args );
3596 1 50       14 @data = sort {$a->[3] cmp $b->[3]} @data if $opt->{name};
  0         0  
3597 1         2 $output .= ''; # Don't want it to be undef.
3598              
3599             my $encoder = ( HAVE_TLE_IRIDIUM &&
3600             Astro::Coord::ECI::TLE::Iridium->can(
3601 1     0   8 '__encode_operational_status' ) ) || sub { return $_[2] };
  0         0  
3602              
3603 1         5 foreach my $tle (@data) {
3604 0         0 my $status = $encoder->( undef, status => $tle->[2] );
3605 0         0 $output .= quoter( 'status', 'add',
3606             $tle->[0], $tle->[1], $status,
3607             $tle->[3], $tle->[4] ) . "\n";
3608             }
3609              
3610             } else {
3611 0         0 $output .= ''; # Don't want it to be undef.
3612 0         0 $output .= Astro::Coord::ECI::TLE->status ($verb, @args);
3613             }
3614              
3615 3         225 return $output;
3616              
3617 20     20   17739 }
  20         47  
  20         120  
3618              
3619             sub system : method Verb() { ## no critic (ProhibitBuiltInHomonyms)
3620 4     4 1 25 my ( $self, undef, $verb, @args ) = __arguments( @_ ); # $opt unused
3621              
3622             @args = map {
3623 4         15 bsd_glob( $_, GLOB_NOCHECK | GLOB_BRACE | GLOB_QUOTE )
  8         453  
3624             } @args;
3625 4         20 my $stdout = $self->{frame}[-1]{localout};
3626 4         9 my @exported = keys %{ $self->{exported} };
  4         33  
3627 4         13 local @ENV{@exported} = map { $mutator{$_} ? $self->get( $_ ) :
3628 5 100       74 $self->{exported}{$_} } @exported;
3629 4 50 33     26 if ( defined $stdout && -t $stdout ) {
3630 0         0 CORE::system {$verb} $verb, @args;
  0         0  
3631 0         0 return;
3632             } else {
3633 4         172 $self->load_package( { fatal => 'wail' }, 'IPC::System::Simple' );
3634 4         34 return IPC::System::Simple::capturex( $verb, @args );
3635             }
3636 20     20   10373 }
  20         65  
  20         130  
3637              
3638             sub time : method Verb() Tweak( -unsatisfied ) { ## no critic (ProhibitBuiltInHomonyms,RequireArgUnpacking)
3639 1 50   1 1 3 my ($self, @args) = map { ARRAY_REF eq ref $_ ? @{ $_ } : $_ } @_;
  2         13  
  0         0  
3640 1 50       12 $have_time_hires->() or $self->wail( 'Time::HiRes not available' );
3641 1         8 $self->_dispatch_check( time => $args[0] );
3642 1         6 my $start = Time::HiRes::time();
3643             # If we're inside an unsatisfied if() we do not do the timing,
3644             # because dispatch() is probably a no-op.
3645             $self->_in_unsatisfied_if()
3646             or $self->_add_post_dispatch(
3647             sub {
3648 1     1   7 return sprintf "%.3f seconds\n", Time::HiRes::time() - $start;
3649             },
3650 1 50       8 );
3651 1         5 return $self->dispatch( @args );
3652 20     20   8979 }
  20         57  
  20         164  
3653              
3654             sub time_parser : Verb() {
3655 0 0   0 1 0 splice @_, ( HASH_REF eq ref $_[1] ? 2 : 1 ), 0, 'time_parser';
3656 0         0 goto &_helper_handler;
3657 20     20   7621 }
  20         69  
  20         115  
3658              
3659             sub tle : Verb( :compute __tle_options ) {
3660 4     4 1 23 my ( $self, $opt, @args ) = __arguments( @_ );
3661             @args
3662             and not $opt->{choose}
3663 4 50 33     17 and $opt->{choose} = \@args;
3664              
3665 4         27 my $bodies = $self->__choose( $opt->{choose}, $self->{bodies} );
3666 4         9 @{ $bodies } = map { $_->[0] }
  5         152  
3667 1 50       59 sort { $a->[1] <=> $b->[1] || $a->[2] <=> $b->[2] }
3668 5         57 map { [ $_, $_->get( 'id' ), $_->get( 'epoch' ) ] }
3669 4         14 @{ $bodies };
  4         10  
3670 4         12 my $tplt_name = delete $opt->{_template};
3671 4         23 return $self->__format_data( $tplt_name => $bodies, $opt );
3672 20     20   9103 }
  20         52  
  20         121  
3673              
3674             sub __tle_options {
3675 4     4   9 my ( $self, $opt ) = @_;
3676             return [
3677 4         23 qw{ choose=s@ },
3678             $self->_templates_to_options( tle => $opt ),
3679             ];
3680             }
3681              
3682             sub unexport : Verb() {
3683 1     1 1 7 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3684              
3685 1         6 foreach my $name ( @args ) {
3686 1         5 delete $self->{exported}{$name};
3687             }
3688 1         3 return;
3689 20     20   7381 }
  20         58  
  20         107  
3690              
3691             sub validate : Verb( quiet! ) {
3692 1     1 1 9 my ( $self, $opt, @args ) = __arguments( @_ );
3693              
3694 1         10 my $pass_start = $self->__parse_time (
3695             shift @args, $self->_get_day_noon());
3696 1   50     21 my $pass_end = $self->__parse_time (shift @args || '+7');
3697 1 50       5 $pass_start >= $pass_end
3698             and $self->wail( 'End time must be after start time' );
3699              
3700 1 50       3 @{ $self->{bodies} }
  1         6  
3701             or $self->wail( 'No bodies selected' );
3702              
3703             # Validate each body.
3704              
3705 1         2 my @valid;
3706 1         6 foreach my $tle ( $self->_aggregate( $self->{bodies} ) ) {
3707 2 100       2007 $tle->validate( $opt, $pass_start, $pass_end )
3708             and push @valid, $tle->members();
3709             }
3710              
3711 1         991 $self->{bodies} = \@valid;
3712              
3713 1         5 return;
3714 20     20   10118 }
  20         85  
  20         131  
3715              
3716             sub version : Verb() {
3717 0     0 1 0 return <<"EOD";
3718              
3719 0         0 @{[__PACKAGE__]} $VERSION - Satellite pass predictor
3720 0         0 based on Astro::Coord::ECI @{[Astro::Coord::ECI->VERSION]}
3721             Copyright (C) 2009-2026 by Thomas R. Wyant, III
3722              
3723             EOD
3724 20     20   6316 }
  20         44  
  20         100  
3725              
3726             ########################################################################
3727              
3728             # $self->_add_post_dispatch( $code_ref );
3729              
3730             # Add a reference to code to be executed after the current interactive
3731             # method is dispatched. All such code is executed, in the reverse of
3732             # the order it was added. The only argument will be the invocant.
3733             # Because it is added to the current execution frame, if the
3734             # interactive method being dispatched is begin(), the code will be
3735             # executed after the corresponding end(). Code to make the execution
3736             # happen is, of course, in dispatch().
3737             sub _add_post_dispatch {
3738 23     23   41 my ( $self, $code ) = @_;
3739 23   50     30 push @{ $self->{frame}[-1]{post_dispatch} ||= [] }, $code;
  23         103  
3740 23         53 return;
3741             }
3742              
3743             # $self->_aggregate( $list_ref );
3744              
3745             sub __add_to_observing_list {
3746 5     5   12753 my ( $self, @args ) = @_;
3747 5         33 foreach my $body ( @args ) {
3748 10 50       144 embodies( $body, 'Astro::Coord::ECI::TLE' )
3749             and next;
3750 0         0 my $id = $body->get( 'id' );
3751 0 0       0 defined $id
3752             or $id = $body->get( 'name' );
3753 0         0 $self->wail( "Body $id is not a TLE" );
3754             }
3755 5         77 push @{ $self->{bodies} }, @args;
  5         47  
3756 5         14 return $self;
3757             }
3758              
3759             # This is just a wrapper for
3760             # Astro::Coord::ECI::TLE::Set->aggregate.
3761              
3762             sub _aggregate {
3763 27     27   90 my ( $self, $bodies ) = @_;
3764 27         108 local $Astro::Coord::ECI::TLE::Set::Singleton = $self->{singleton};
3765 27         65 return Astro::Coord::ECI::TLE::Set->aggregate ( @{ $bodies } );
  27         245  
3766             }
3767              
3768             # _apply_boolean_default( \%opt, $invert, @keys );
3769             #
3770             # This subroutine defaults a set of boolean options. The keys in
3771             # the set are specified in @keys, and the defined values are
3772             # inverted before the defaults are applied if $invert is true.
3773             # Nothing is returned.
3774              
3775             sub _apply_boolean_default {
3776 44     44   157 my ( $self, $opt, $invert, @keys ) = @_;
3777 44         101 my $state = my $found = 0;
3778 44         118 foreach my $key ( @keys ) {
3779 136 100       336 if ( exists $opt->{$key} ) {
3780 8         18 $found++;
3781             $invert
3782 8 50       26 and $opt->{$key} = ( ! $opt->{$key} );
3783 8 100       38 $state |= ( $opt->{$key} ? 2 : 1 );
3784             }
3785             }
3786             1 == $state # Only negated options found
3787             and @keys == $found # All options in group were specified
3788             and $self->wail( 'May not negate all of ' . join ', ', map {
3789 44 50 66     182 "-$_" } @keys );
  0         0  
3790 44         92 my $default = $state < 2;
3791 44         87 foreach my $key ( @keys ) {
3792             exists $opt->{$key}
3793 136 100       461 or $opt->{$key} = $default;
3794             }
3795 44         115 return;
3796             }
3797              
3798             # $self->_attribute_exists( $name, %arg );
3799             #
3800             # This method returns true if an accessor for the given attribute
3801             # exists, and croaks otherwise.
3802             # Attributes in the %level1_attr hash fail unless in level1 mode
3803             # Named arguments:
3804             # query: if true, returns false if attribute does not exist
3805              
3806             {
3807             my %level1_attr = map { $_ => 1 } qw{ sun };
3808              
3809             sub _attribute_exists {
3810 1280     1280   14247 my ( $self, $name, %arg ) = @_;
3811             exists $accessor{$name}
3812             and ( ! $level1_attr{$name} || $self->{frame}[-1]{level1} )
3813 1280 50 33     8455 and return $accessor{$name};
      33        
3814             $arg{query}
3815 0 0       0 or $self->wail("No such attribute as '$name'");
3816 0         0 return;
3817             }
3818             }
3819              
3820             {
3821              
3822             my %spacetrack_attributes;
3823             $have_astro_spacetrack->()
3824             and %spacetrack_attributes = map { $_ => 1 }
3825             Astro::SpaceTrack->attribute_names();
3826              
3827             my %special = (
3828             formatter => sub {
3829             my ( $obj, $attr ) = @_;
3830             $obj->can( $attr )
3831             or return NULL;
3832             return $obj->$attr();
3833             },
3834             spacetrack => sub {
3835             my ( $obj, $attr ) = @_;
3836             $spacetrack_attributes{$attr}
3837             or return NULL;
3838             return $obj->getv( $attr );
3839             },
3840             time_parser => sub {
3841             my ( $obj, $attr ) = @_;
3842             $obj->can( $attr )
3843             or return NULL;
3844             return $obj->$attr();
3845             },
3846             );
3847              
3848             # my $value = $self->_attribute_value( $name );
3849             #
3850             # Return an attribute value. If the attribute is 'formatter',
3851             # 'spacetrack' or 'time_parser' you can specify a dot and the name
3852             # of an attribute of the relevant object, e.g. spacetrack.username.
3853             # If the attribute does not exist you get back manifest constant
3854             # NULL, which is a reference to undef blessed into class 'Null'.
3855             sub _attribute_value {
3856 43     43   91 my ( $self, $name ) = @_;
3857 43         401 my ( $attr, $sub ) = split qr{ [.] }smx, $name, 2;
3858 43 100       217 $accessor{$attr}
3859             or return NULL;
3860 9         25 my $rslt = $self->get( $attr );
3861 9 100       22 if ( defined $sub ) {
3862             $rslt
3863 2 50 33     14 and my $code = $special{$attr}
3864             or return NULL;
3865 2         5 $rslt = $code->( $rslt, $sub );
3866             }
3867 9         19 return $rslt;
3868             }
3869             }
3870              
3871             # Documented in POD
3872              
3873             {
3874             my %chooser = (
3875             '' => sub {
3876             my ( $sel ) = @_;
3877             my @rslt;
3878             foreach my $s ( split qr{ \s* , \s* }smx, $sel ) {
3879             if ( $s =~ m/ \D /smx || $s < 1000 ) {
3880             my $re = qr{\Q$s\E}i;
3881             push @rslt, sub {
3882             my ( $tle, $context ) = @_;
3883             $context->{name} ||= $tle->get( 'name' );
3884             defined $context->{name}
3885             or return;
3886             return $context->{name} =~ $re;
3887             };
3888             } else {
3889             push @rslt, sub {
3890             my ( $tle, $context ) = @_;
3891             $context->{id} ||= $tle->get( 'id' );
3892             return $context->{id} == $s;
3893             };
3894             }
3895             }
3896             return @rslt;
3897             },
3898             CODE_REF() => sub {
3899             my ( $sel ) = @_;
3900             return $sel;
3901             },
3902             REGEXP_REF() => sub {
3903             my ( $sel ) = @_;
3904             return sub {
3905             my ( $tle, $context ) = @_;
3906             $context->{name} ||= $tle->get( 'name' );
3907             return $context->{name} =~ $sel;
3908             };
3909             },
3910             );
3911              
3912             sub __choose {
3913 45     45   222 my ( $self, @args ) = @_;
3914 45 100       213 my $opt = HASH_REF eq ref $args[0] ? shift @args : {};
3915 45         105 my $choice = shift @args;
3916 45 100       212 defined $choice
3917             or $choice = [];
3918 45 50       164 ARRAY_REF eq ref $choice
3919             or $self->weep( 'Choice must be an ARRAY ref' );
3920 45         125 my @rslt;
3921             my @selector;
3922 45         89 foreach my $sel ( @{ $choice } ) {
  45         132  
3923 5         10 my $ref = ref $sel;
3924 5 50       21 my $code = $chooser{$ref}
3925             or $self->weep( "$ref not supported as chooser" );
3926 5         32 push @selector, $code->( $sel );
3927             }
3928              
3929             $opt->{bodies}
3930             and push @args,
3931 45 100       168 $self->_aggregate( $self->{bodies} );
3932             $opt->{sky}
3933 45 100       741 and push @args, $self->{sky};
3934              
3935 45 100       115 @args = map { ARRAY_REF eq ref $_ ? @{ $_ } : $_ } @args;
  51         163  
  43         211  
3936              
3937             not @selector
3938 45 100       305 and return wantarray ? @args : \@args;
    100          
3939              
3940 5         8 foreach my $tle ( @args ) {
3941 10 50       23 ARRAY_REF eq ref $tle
3942             and $self->weep( 'Schwartzian-transform objects not supported' );
3943              
3944 10         73 my $match = $opt->{invert};
3945 10         31 my $context = {};
3946 10         15 foreach my $sel ( @selector ) {
3947 10 100       21 $sel->( $tle, $context )
3948             or next;
3949 4         9 $match = !$match;
3950 4         10 last;
3951             }
3952              
3953 10 100       31 $match and push @rslt, $tle;
3954             }
3955              
3956 5 100       51 return wantarray ? @rslt : \@rslt;
3957             }
3958              
3959             }
3960              
3961             # $self->_deprecation_notice( $type, $name );
3962             #
3963             # This method centralizes deprecation. Type is 'attribute' or
3964             # 'method'. Deprecation is driven of the %deprecate hash. Values
3965             # are:
3966             # false - no warning
3967             # 1 - warn on first use
3968             # 2 - warn on each use
3969             # 3 - die on each use.
3970             #
3971             # $self->_deprecation_in_progress( $type, $name )
3972             #
3973             # This method returns true if the deprecation is in progress. In
3974             # fact it returns the deprecation level.
3975              
3976             {
3977              
3978             my %deprecate = (
3979             attribute => {
3980             country => 0,
3981             date_format => 0,
3982             desired_equinox_dynamical => 0,
3983             explicit_macro_delete => 0,
3984             gmt => 0,
3985             local_coord => 0,
3986             perltime => 0,
3987             time_format => 0,
3988             tz => 0,
3989             },
3990             method => {
3991             st => 0,
3992             },
3993             status => {
3994             iridium => 3,
3995             },
3996             );
3997              
3998             sub _deprecation_notice {
3999 1277     1277   3209 my ( $self, $type, $name, $repl ) = @_;
4000 1277 50       4047 $deprecate{$type} or return;
4001 1277 50       3529 $deprecate{$type}{$name} or return;
4002             my $msg = sprintf 'The %s %s is %s', $name, $type,
4003 0 0       0 $deprecate{$type}{$name} > 2 ? 'removed' : 'deprecated';
4004 0 0       0 defined $repl
4005             and $msg .= "; use $repl instead";
4006 0 0       0 $deprecate{$type}{$name} >= 3
4007             and $self->wail( $msg );
4008 0 0       0 warnings::enabled( 'deprecated' )
4009             and $self->whinge( $msg );
4010             $deprecate{$type}{$name} == 1
4011 0 0       0 and $deprecate{$type}{$name} = 0;
4012 0         0 return;
4013             }
4014              
4015             sub _deprecation_in_progress {
4016 0     0   0 my ( undef, $type, $name ) = @_; # Invocant unused
4017 0 0       0 $deprecate{$type} or return;
4018 0         0 return $deprecate{$type}{$name};
4019             }
4020              
4021             }
4022              
4023             # my ( $obj ) = $self->_drop_from_sky( $name );
4024             # The return is an array containing the dropped body, or nothing if the
4025             # body was not found.
4026             sub _drop_from_sky {
4027 1     1   4 my ( $self, $name ) = @_;
4028 1 50       16 defined( my $inx = $self->_find_in_sky( $name ) )
4029             or return;
4030 1         31 return splice @{ $self->{sky} }, $inx, 1;
  1         13  
4031             }
4032              
4033             # $fh = $self->_file_opener( $name, $mode );
4034             #
4035             # This method opens the given file, returning the handle. If the
4036             # mode is output, the current value of output_layers is appended.
4037             # An exception is thrown if the file can not be opened.
4038              
4039             sub _file_opener {
4040 1     1   3 my ( $self, $name, $mode ) = @_;
4041              
4042             # NOTE special case for &1 (stdout) and &2 (stderr).
4043             my $fh = ( $name =~ m/ \A & ( [12] ) \z /smx ) ?
4044             [
4045             undef,
4046 1 50 0     10 $self->{frame}[-1]{localout} || \*STDOUT,
    50          
4047             \*STDERR,
4048             ]->[ $1 ] :
4049             IO::File->new( $name, $mode )
4050             or $self->wail( "Unable to open $name: $!" );
4051              
4052 1 50       156 if ( $mode =~ m/ \A (?: [+>] | [|] - ) /smx ) {
4053              
4054 1         3 my $layers = $self->get( 'output_layers' );
4055 1 50 33     6 if ( defined $layers && '' ne $layers ) {
4056 1 50       12 binmode $fh, $layers
4057             or $self->wail(
4058             "Unable to set '$layers' on $name: $!" );
4059             }
4060             }
4061              
4062 1         52 return $fh;
4063             }
4064              
4065             # $code = $self->_file_reader( $file, \%opt );
4066             #
4067             # This method returns a code snippet that returns the contents of
4068             # the file one line at a time. The $file can be any of:
4069             #
4070             # * An open handle
4071             # * A URL (if LWP::UserAgent can be loaded)
4072             # * A file name
4073             # * A scalar reference
4074             # * An array reference
4075             # * A code reference, which is returned unmodified
4076             #
4077             # The code snippet will return undef at end-of-file.
4078             #
4079             # The following keys in %opt are recognized:
4080             # {encoding} specifies the encoding of the file. How this is used
4081             # on the $file argument as follows:
4082             # * An open handle -- unused
4083             # * A URL ----------- unused (encoding taken from HTTP::Response)
4084             # * A file name ----- used (default is utf-8)
4085             # * A scalar ref ---- used (default is un-encoded)
4086             # * An array ref ---- unused
4087             # * A code ref ------ unused
4088             # {glob} causes the contents of the file to be returned, rather
4089             # than a reader.
4090             # {optional} causes the code to simply return on an error, rather
4091             # than failing.
4092              
4093             sub _file_reader {
4094 30     30   12069 my ( $self, $file, $opt ) = @_;
4095              
4096 30 100       160 if ( openhandle( $file ) ) {
4097             $opt->{glob}
4098 2 100   1   17 or return sub { return scalar <$file> };
  1         21  
4099 1         6 local $/ = undef;
4100 1         31 return scalar <$file>;
4101             }
4102              
4103 28         76 my $ref = ref $file;
4104 28 50       207 my $code = $self->can( "_file_reader_$ref" )
4105             or $self->wail( sprintf "Opening a $ref ref is unsupported" );
4106              
4107 28         705 goto &$code;
4108             }
4109              
4110             # Most of the following are called using '$self->can(
4111             # "_file_reader_$ref" )', and there is no way a static analysis tool can
4112             # find such calls. So we just have to exempt them from Perl::Critic
4113              
4114             sub _file_reader_ { ## no critic (ProhibitUnusedPrivateSubroutines)
4115 17     17   77 my ( $self, $file, $opt ) = @_;
4116 17   100     75 $opt ||= {};
4117              
4118 17 50       64 defined $file
4119             and chomp $file;
4120              
4121 17 50 33     136 if ( ! defined $file || ! ref $file && '' eq $file ) {
      33        
4122 0 0       0 $opt->{optional} and return;
4123 0         0 $self->wail( 'Defined file required' );
4124             }
4125              
4126 17 100       77 if ( $self->_file_reader__validate_url( $file ) ) {
4127 2         19 my $ua = LWP::UserAgent->new();
4128 2         3582 my $resp = $ua->get( $file );
4129             $resp->is_success()
4130 2 50       12708 or do {
4131 0 0       0 $opt->{optional} and return;
4132 0         0 $self->wail( "Failed to retrieve $file: ",
4133             $resp->status_line() );
4134             };
4135 2         25 $opt = { %{ $opt }, encoding => $resp->content_charset() };
  2         18  
4136 2         3220 return $self->_file_reader(
4137             \( scalar $resp->content() ),
4138             $opt,
4139             );
4140             } else {
4141 15         832 my $encoding = $self->_file_reader__encoding( $opt );
4142             open my $fh, "<$encoding", $self->expand_tilde( $file ) ## no critic (RequireBriefOpen)
4143 15 100       185 or do {
4144 4 100       45 $opt->{optional} and return;
4145 3         52 $self->wail( "Failed to open $file: $!" );
4146             };
4147             $opt->{glob}
4148 11 100   16   1451 or return sub { return scalar <$fh> };
  16         337  
4149 7         575 local $/ = undef;
4150 7         361 return scalar <$fh>;
4151             }
4152             }
4153              
4154             sub _file_reader__encoding {
4155 19     19   58 my ( undef, $opt ) = @_;
4156 19   100     78 $opt ||= {};
4157 19   100     157 my $encoding = $opt->{encoding} || 'utf-8';
4158 19         65 $encoding = ":encoding($encoding)";
4159 19         42 OS_IS_WINDOWS
4160             and substr $encoding, 0, 0, ':crlf';
4161 19         62 return $encoding;
4162             }
4163              
4164              
4165             sub _file_reader__validate_url {
4166 17     17   47 my ( undef, $url ) = @_; # Invocant unused
4167              
4168 17 50       90 load_package( 'LWP::UserAgent' )
4169             or return;
4170              
4171 17 50       71 load_package( 'URI' )
4172             or return;
4173              
4174 17 50       76 load_package( 'LWP::Protocol' )
4175             or return;
4176              
4177 17 50       237 my $obj = URI->new( $url )
4178             or return;
4179 17 50       25694 $obj->can( 'authority' )
4180             or return 1;
4181              
4182 17 100       95 defined( my $scheme = $obj->scheme() )
4183             or return;
4184 3 100       84 LWP::Protocol::implementor( $scheme )
4185             or return;
4186              
4187 2         22958 return 1;
4188             }
4189              
4190             sub _file_reader_ARRAY { ## no critic (ProhibitUnusedPrivateSubroutines)
4191 5     5   16 my ( undef, $file, $opt ) = @_; # Invocant unused
4192              
4193 5         9 my $inx = 0;
4194             $opt->{glob}
4195 5 100   11   38 or return sub { return $file->[$inx++] };
  11         32  
4196 1         3 my $buffer;
4197 1         3 foreach ( @{ $file } ) {
  1         5  
4198 5         41 $buffer .= $_;
4199 5 50       29 $buffer =~ m/ \n \z /smx
4200             or $buffer .= "\n";
4201             }
4202 1         9 return $buffer;
4203             }
4204              
4205             sub _file_reader_CODE { ## no critic (ProhibitUnusedPrivateSubroutines)
4206 2     2   7 my ( undef, $file, $opt ) = @_; # Invocant unused
4207             $opt->{glob}
4208 2 100       11 or return $file;
4209 1         3 my $buffer;
4210 1         3 local $_;
4211 1         5 while ( defined( $_ = $file->() ) ) {
4212 5         27 $buffer .= $_;
4213 5 50       29 $buffer =~ m/ \n \z /smx
4214             or $buffer .= "\n";
4215             }
4216 1         11 return $buffer;
4217             }
4218              
4219             sub _file_reader_SCALAR { ## no critic (ProhibitUnusedPrivateSubroutines)
4220 4     4   12 my ( $self, $file, $opt ) = @_;
4221              
4222 4         14 my $encoding = $self->_file_reader__encoding( $opt );
4223             open my $fh, "<$encoding", $file ## no critic (RequireBriefOpen)
4224 4 50       92 or do {
4225 0 0       0 $opt->{optional} and return;
4226 0         0 $self->wail( "Failed to open SCALAR reference: $!" );
4227             };
4228             $opt->{glob}
4229 4 100   2   1030 or return sub { return scalar <$fh> };
  2         41  
4230 2         14 local $/ = undef;
4231 2         61 return scalar <$fh>;
4232             }
4233              
4234             # $inx = $self->_find_in_sky( $name )
4235             # The return is the index of the named body in @{ $self->{sky} }, or
4236             # undef if it is not present. 'Sun' and 'Moon' are special cases;
4237             # everything else is presumed to be found by name.
4238             sub _find_in_sky {
4239 6     6   20 my ( $self, $name ) = @_;
4240              
4241 6         117 my $re = qr/ \A \Q$name\E \z /smxi;
4242 6         20 foreach my $inx ( 0 .. $#{ $self->{sky} } ) {
  6         33  
4243 8 100       180 $self->{sky}[$inx]->get( 'name' ) =~ $re
4244             and return $inx;
4245             }
4246 5         134 return;
4247             }
4248              
4249             # Documented in POD
4250              
4251             sub __format_data {
4252 41     41   5081 my ( $self, $action, $data, $opt ) = @_;
4253 41         236 return $self->_get_formatter_object( $opt )->format(
4254             sp => $self,
4255             template => $action,
4256             data => $data,
4257             opt => $opt,
4258             );
4259             }
4260              
4261             # $frames = $satpass2->_frame_push($type, \@args);
4262             #
4263             # This method pushes a context frame on the stack. The $type
4264             # describes the frame, and goes in the frame's {type} entry, but
4265             # is currently unused. The \@args entry goes in the {args} key,
4266             # and is the basis of argument expansion. The return is the number
4267             # of frames that were on the stack _BEFORE_ the now-current frame
4268             # was added to the stack. This gets passed to _frame_pop() to
4269             # restore the context stack to its status before the current frame
4270             # was added.
4271              
4272             sub _frame_push {
4273 59     59   185 my ( $self, $type, $args, $opt ) = @_;
4274 59   50     202 $args ||= [];
4275 59   100     322 $opt ||= {};
4276 59   100     93 my $frames = scalar @{$self->{frame} ||= []};
  59         271  
4277 59 100       240 my $prior = $frames ? $self->{frame}[-1] : {
4278             condition => 1,
4279             stdout => select(),
4280             };
4281             my $condition = exists $opt->{condition} ?
4282             $opt->{condition} :
4283 59 100       219 $prior->{condition};
4284             #### defined $stdout or $stdout = select();
4285 59         243 my ( undef, $filename, $line ) = caller;
4286 59         1252 push @{$self->{frame}}, {
4287             type => $type,
4288             args => $args,
4289             condition => $condition,
4290             define => {}, # Macro defaults done with :=
4291             local => {},
4292             localout => undef, # Output for statement.
4293             macro => {},
4294             pushed_by => "$filename line $line",
4295             spacetrack => {},
4296             stdout => $prior->{localout} || $prior->{stdout},
4297 59   66     117 unsatisfied_if => $prior->{unsatisfied_if} || ! $condition,
      100        
4298             };
4299 59         256 return $frames;
4300             }
4301              
4302             # $satpass2->_frame_pop($frames);
4303             # $satpass2->_frame_pop($type => $frames);
4304             # $satpass2->_frame_pop();
4305             #
4306             # This method pops context frames off the stack until there are
4307             # $frames frames left. The optional $type argument is currently
4308             # unused, but was intended for type checking should that become
4309             # necessary. The zero-argument call pops one frame off the stack.
4310             # An exception is thrown if there are no frames left to pop. After
4311             # all required frames are popped, an exception is thrown if the
4312             # pop was done with a continued input line pending.
4313              
4314             {
4315              
4316             my %force_set; # If true, the named attribute is set with the
4317             # set() method even if a hash key of the same
4318             # name exists. This is set with
4319             # _frame_pop_force_set(), typically where the
4320             # mutator is defined.
4321              
4322             sub _frame_pop {
4323 53     53   150 my ($self, @args) = @_;
4324             ## my $type = @args > 1 ? shift @args : undef;
4325 53 100       149 @args > 1 and shift @args; # Currently unused
4326             my $frames = ( @args && defined $args[0] ) ?
4327             shift @args :
4328 53 100 100     798 @{$self->{frame}} - 1;
  27         73  
4329 53         91 while (@{$self->{frame}} > $frames) {
  105         290  
4330 52 50       115 my $frame = pop @{$self->{frame}}
  52         214  
4331             or $self->weep( 'No frame to pop' );
4332 52   50     249 my $local = $frame->{local} || {};
4333 52         85 foreach my $name ( keys %{ $local } ) {
  52         196  
4334 2         14 my $value = $local->{$name};
4335 2 100 66     29 if ( exists $self->{$name} && !$force_set{$name} ) {
4336 1         33 $self->{$name} = $value;
4337             } else {
4338 1         9 $self->set( $name, $value );
4339             }
4340             }
4341 52         125 foreach my $key (qw{macro}) {
4342 52   50     192 my $info = $frame->{$key} || {};
4343 52         81 foreach my $name ( keys %{ $info } ) {
  52         168  
4344 19         88 $self->{$key}{$name} = $info->{ $name };
4345             }
4346             }
4347 52         434 ($frame->{spacetrack} && %{$frame->{spacetrack}})
4348 52 50 33     157 and $self->_get_spacetrack()->set(%{$frame->{spacetrack}});
  0         0  
4349             }
4350 53 50       138 if (delete $self->{pending}) {
4351 0         0 $self->wail('Input ended on continued line');
4352             }
4353 53         153 return;
4354             }
4355              
4356             # Force use of the set() method even if there is an attribute of the
4357             # same name.
4358             sub _frame_pop_force_set {
4359 20     20   56 foreach my $name ( @_ ) {
4360 20         125 $force_set{$name} = 1;
4361             }
4362 20         43 return;
4363             }
4364             }
4365              
4366             sub _get_browser_command {
4367 0     0   0 my ( $self, $val ) = @_;
4368             defined $val
4369 0 0       0 or $val = $self->{webcmd};
4370 0 0 0     0 defined $val
4371             and '' ne $val
4372             or return $val;
4373 0 0       0 '1' eq $val
4374             or return $val;
4375 0         0 require Browser::Open;
4376 0         0 return Browser::Open::open_browser_cmd();
4377             }
4378              
4379             # $dumper = $self->_get_dumper();
4380             #
4381             # This method returns a reference to code that can be used to dump
4382             # data. The first time it is called it goes through a list of
4383             # possible classes, and uses the first one it can load, dying if
4384             # it can not load any of them. After the first successful call, it
4385             # simply returns the cached dumper.
4386              
4387             {
4388             my $dumper;
4389             my %kode = (
4390             'Data::Dumper' => sub {
4391             local $Data::Dumper::Terse = 1;
4392             Data::Dumper::Dumper(@_);
4393             },
4394             );
4395             sub _get_dumper {
4396 0     0   0 my ($self) = @_;
4397 0         0 my %dmpr;
4398             my @mod;
4399 0   0     0 return $dumper ||= do {
4400 0         0 foreach (qw{YAML::Dump Data::Dumper::Dumper}) {
4401 0         0 my ($module, $routine) = m/ (.*) :: (.*) /smx;
4402 0         0 push @mod, $module;
4403 0         0 $dmpr{$module} = $routine;
4404             }
4405 0         0 my $mod = $self->_load_module(@mod);
4406 0 0       0 $kode{$mod} || $mod->can($dmpr{$mod});
4407             };
4408             }
4409             }
4410              
4411             # $fmt = $satpass2->_get_dumper_object();
4412             #
4413             # Gets a dumper object. This object must conform to the
4414             # Astro::App::Satpass2::Format interface.
4415              
4416             {
4417              
4418             my $dumper;
4419              
4420             sub _get_dumper_object {
4421 0   0 0   0 return ( $dumper ||= do {
4422 0         0 require Astro::App::Satpass2::Format::Dump;
4423 0         0 Astro::App::Satpass2::Format::Dump->new();
4424             }
4425             );
4426             }
4427              
4428             }
4429              
4430             # $fmt = $satpass2->_get_formatter_object( $opt );
4431             #
4432             # Gets the Astro::App::Satpass2::Format object. If $opt->{dump} is true,
4433             # returns a dumper object; otherwise returns the currently-set
4434             # formatter object.
4435              
4436             sub _get_formatter_object {
4437 41     41   111 my ( $self, $opt ) = @_;
4438 41   50     154 $opt ||= {};
4439 41 50 33     395 return ( $opt && $opt->{dump} ) ? $self->_get_dumper_object() :
4440             $self->get( 'formatter' );
4441             }
4442              
4443             sub _get_formatter_attribute {
4444 0     0   0 my ( $self, $name ) = @_;
4445 0         0 return $self->get( 'formatter' )->$name();
4446             }
4447              
4448             # $st = $satpass2->_get_geocoder()
4449              
4450             # Gets the geocoder object, instantiating it if
4451             # necesary.
4452              
4453             sub _get_geocoder {
4454 0     0   0 my ( $self ) = @_;
4455 0 0       0 if ( ! exists $self->{geocoder} ) {
4456 0         0 my ( $class, $obj );
4457 0 0       0 $class = $default_geocoder->()
4458             and $obj = $class->new();
4459 0         0 $self->{geocoder} = $obj;
4460             }
4461 0         0 return $self->{geocoder};
4462             }
4463              
4464             # $boolean = $satpass2->_get_interactive();
4465             #
4466             # This method returns true if the script is running interactively,
4467             # and false otherwise. Currently, it returns the results of -t
4468             # STDIN.
4469              
4470             sub _get_interactive {
4471 0     0   0 return -t STDIN;
4472             }
4473              
4474             # $code = $satpass2->_get_readline();
4475             #
4476             # Returns code to read input. The code takes an argument which
4477             # will be used as a prompt if one is needed. What is actually
4478             # returned is:
4479             #
4480             # If $satpass2->_get_interactive() is false, the returned code
4481             # just reads standard in. Otherwise,
4482             #
4483             # if Term::ReadLine can be loaded, a Term::ReadLine object is
4484             # instantiated if need be, and the returned code calls
4485             # Term::ReadLine->readline($_[0]) and returns whatever that gives
4486             # you. Otherwise,
4487             #
4488             # Otherwise the returned code writes its argument to STDERR and
4489             # reads STDIN.
4490             #
4491             # Note that the return from this subroutine may or may not be
4492             # chomped.
4493              
4494             my $readline_word_break_re;
4495              
4496             {
4497             my $rl;
4498              
4499             sub _get_readline {
4500 1     1   472 my ($self) = @_;
4501             # The Perl::Critic recommendation is IO::Interactive, but that
4502             # fiddles with STDOUT. We want STDIN, because we want to behave
4503             # differently if STDIN is a pipe, but not if STDOUT is a pipe.
4504             # We're still missing the *ARGV logic, but that's OK too, since
4505             # we use the contents of @ARGV as commands, not as file names.
4506 1         3 return do {
4507 1         4 my $buffer = '';
4508 1 50       4 if ($self->_get_interactive()) {
4509             eval {
4510 1 50       6 load_package( 'Term::ReadLine' )
4511             or return;
4512 1 50       4 unless ( $rl ) {
4513 1         11 $rl = Term::ReadLine->new( 'satpass2' );
4514 1 50       32296 if ( 'Term::ReadLine::Perl' eq $rl->ReadLine() ) {
4515              
4516 0   0     0 $readline_word_break_re ||= qr<
4517             [\Q$readline::rl_completer_word_break_characters\E]+
4518             >smx;
4519              
4520 20     20   123051 no warnings qw{ once };
  20         46  
  20         17406  
4521             $readline::rl_completion_function = sub {
4522 0     0   0 my ( $text, $line, $start ) = @_;
4523 0         0 return $self->__readline_completer(
4524             $text, $line, $start );
4525 0         0 };
4526             }
4527             }
4528             sub {
4529 0 0   0   0 defined $buffer or return $buffer;
4530 0         0 return ( $buffer = $rl->readline($_[0]) );
4531             }
4532 1         102 } || sub {
4533 0 0   0   0 defined $buffer or return $buffer;
4534 0         0 print STDERR $_[0];
4535             return (
4536 0         0 $buffer = ## no critic (ProhibitExplicitStdin)
4537             );
4538 1 50       6 };
4539             } else {
4540             sub {
4541 0 0   0   0 defined $buffer or return $buffer;
4542             return (
4543 0         0 $buffer = ## no critic (ProhibitExplicitStdin)
4544             );
4545 0         0 };
4546             }
4547             };
4548             }
4549             }
4550              
4551             sub __readline_completer {
4552 0     0   0 my ( $app, $text, $line, $start ) = @_;
4553              
4554 0 0       0 $start
4555             or return $app->_readline_complete_command( $text );
4556              
4557 0         0 my ( $cmd ) = split $readline_word_break_re, $line, 2;
4558 0         0 my $code;
4559             not $cmd =~ s/ \A core [.] //smx
4560             and ref $app
4561             and $app->{macro}{$cmd}
4562 0 0 0     0 and $code = $app->{macro}{$cmd}->implements( $cmd );
      0        
4563 0   0     0 $code ||= $app->can( $cmd );
4564              
4565 0 0       0 if ( CODE_REF eq ref $code ) {
    0          
4566             # builtins and code macros go here
4567              
4568 0         0 my $rslt;
4569              
4570 0 0       0 if ( my $method = $app->__get_attr( $code, Tweak => {}
4571             )->{completion} ) {
4572             $rslt = $app->$method( $code, $text, $line, $start )
4573 0 0       0 and return @{ $rslt };
  0         0  
4574             }
4575              
4576             $rslt = $app->_readline_complete_options( $code, $text,
4577             $line, $start )
4578 0         0 and @{ $rslt }
4579 0 0 0     0 and return @{ $rslt };
  0         0  
4580              
4581             } elsif ( my $macro = $app->{macro}{$cmd} ) {
4582             # command macros go here
4583              
4584 0         0 my $rslt;
4585             $rslt = $macro->completion( $text )
4586 0 0       0 and return @{ $rslt };
  0         0  
4587             }
4588              
4589 0         0 my @files = bsd_glob( "$text*" );
4590 0 0       0 if ( 1 == @files ) {
    0          
4591 0 0       0 $files[0] .= -d $files[0] ? '/' : ' ';
4592             } elsif ( $readline::var_CompleteAddsuffix ) {
4593 0         0 foreach ( @files ) {
4594 0 0 0     0 if ( -l $_ ) {
    0          
    0          
    0          
4595 0         0 $_ .= '@';
4596             } elsif ( -d $_ ) {
4597 0         0 $_ .= '/';
4598             } elsif ( -x _) {
4599 0         0 $_ .= '*';
4600             } elsif ( -S _ || -p _ ) {
4601 0         0 $_ .= '=';
4602             }
4603             }
4604             }
4605 0         0 $readline::rl_completer_terminator_character = '';
4606 0         0 return @files;
4607             }
4608              
4609             {
4610             my @builtins;
4611             sub _readline_complete_command {
4612 0     0   0 my ( $app, $text ) = @_;
4613 0 0       0 unless ( @builtins ) {
4614 0   0     0 my $stash = ( ref $app || $app ) . '::';
4615 20     20   188 no strict qw{ refs };
  20         40  
  20         19266  
4616 0         0 foreach my $sym ( keys %$stash ) {
4617 0 0       0 $sym =~ m/ \A _ /smx
4618             and next;
4619 0 0       0 my $code = $app->can( $sym )
4620             or next;
4621 0 0       0 $app->__get_attr( $code, 'Verb' )
4622             or next;
4623 0         0 push @builtins, $sym;
4624             }
4625 0         0 @builtins = sort @builtins;
4626             }
4627 0         0 my @rslt;
4628 0 0       0 if ( $text =~ s/ \A core [.] //smx ) {
4629 0         0 my $match = qr< \A \Q$text\E >smx;
4630 0         0 @rslt = map { "core.$_" } grep { $_ =~ $match } @builtins;
  0         0  
  0         0  
4631             } else {
4632 0         0 my $match = qr< \A \Q$text\E >smx;
4633 0         0 @rslt = grep { $_ =~ $match } @builtins, 'core.',
4634 0 0       0 ref $app ? keys %{ $app->{macro} } : ();
  0         0  
4635             }
4636 0 0 0     0 1 == @rslt
4637             and $rslt[0] =~ m/ \W \z /smx
4638             and $readline::rl_completer_terminator_character = '';
4639 0         0 return ( sort @rslt );
4640             }
4641             }
4642              
4643             sub _readline_complete_options {
4644             # my ( $app, $code, $text, $line, $start ) = @_;
4645 0     0   0 my ( $app, $code, $text ) = @_;
4646 0 0       0 $text =~ m/ \A ( --? ) ( .* ) /smx
4647             or return;
4648 0         0 my ( $prefix, $match ) = ( $1, $2 );
4649 0         0 my $lgl = $app->__legal_options( $code );
4650 0         0 my $re = qr< \A \Q$match\E >smx;
4651 0         0 my @rslt;
4652 0         0 foreach ( @{ $lgl } ) {
  0         0  
4653 0 0       0 next if ref;
4654             # De-alias before modifying
4655 0         0 ( my $o = $_ ) =~ s/ [!=?] .* //smx;
4656 0         0 push @rslt, grep { m/$re/ } split qr< \| >smx, $o;
  0         0  
4657             }
4658             @rslt
4659 0 0       0 and return [ map { "$prefix$_" } sort @rslt ];
  0         0  
4660 0         0 return;
4661             }
4662              
4663             # The following subroutine is called dynamically
4664             sub _readline_complete_subcommand { ## no critic (ProhibitUnusedPrivateSubroutines)
4665             # my ( $app, $code, $text, $line, $start ) = @_;
4666 0     0   0 my ( $app, undef, $text, $line, $start ) = @_;
4667 0         0 my @part = _readline_line_to_parts( $line );
4668 0 0       0 if ( my $code = $app->can( "_$part[0]_sub" ) ) {
4669 0         0 return $code->( $app, $text, $line, $start, @part );
4670             }
4671 0         0 my @rslt;
4672 0 0       0 if ( 2 == @part ) {
4673 0         0 my $re = qr< \A _$part[0]_sub_ ( \Q$part[1]\E \w* ) >smx;
4674 0   0     0 my $stash = ( ref $app || $app ) . '::';
4675 20     20   168 no strict qw{ refs };
  20         44  
  20         38701  
4676 0         0 foreach my $key ( keys %$stash ) {
4677 0 0       0 $key =~ m/$re/smx
4678             and push @rslt, "$1";
4679             }
4680 0         0 return [ sort @rslt ];
4681             }
4682              
4683 0 0       0 my $code = $app->can( "_$part[0]_sub_$part[1]" )
4684             or return;
4685              
4686 0         0 my $r;
4687 0 0       0 $r = $app->_readline_complete_options( $code, $text, $line,
4688             $start )
4689             and return $r;
4690              
4691             my $complete = $app->__get_attr( $code, Tweak => {} )->{completion}
4692 0 0       0 or return;
4693              
4694 0 0       0 $r = $app->$complete( $code, $text, $line, $start )
4695             and return $r;
4696              
4697 0         0 return;
4698             }
4699              
4700             sub _macro_list_complete { ## no critic (ProhibitUnusedPrivateSubroutines)
4701             # my ( $app, $code, $text, $line, $start ) = @_;
4702 0     0   0 my ( $app, undef, undef, $line, undef ) = @_;
4703 0 0       0 ref $app
4704             or return;
4705 0         0 my @part = _readline_line_to_parts( $line );
4706 0 0       0 3 == @part
4707             or return;
4708 0         0 my $re = qr< \A \Q$part[2]\E >smx;
4709 0         0 my @rslt;
4710 0         0 foreach ( sort keys %{ $app->{macro} } ) {
  0         0  
4711 0 0       0 m/$re/smx
4712             and push @rslt, $_;
4713             }
4714 0         0 return \@rslt;
4715             }
4716              
4717             sub _sky_body_complete { ## no critic (ProhibitUnusedPrivateSubroutines)
4718             # my ( $app, $code, $text, $line, $start ) = @_;
4719 0     0   0 my ( $app, undef, undef, $line, undef ) = @_;
4720 0 0       0 ref $app
4721             or return;
4722 0         0 my @part = _readline_line_to_parts( $line );
4723 0 0       0 3 == @part
4724             or return;
4725 0         0 my $re = qr< \A \Q$part[2]\E >smxi;
4726 0         0 my @rslt;
4727 0         0 foreach my $body ( @{ $app->{sky} } ) {
  0         0  
4728 0 0       0 if ( ( my $name = $body->get( 'name' ) ) =~ $re ) {
    0          
4729 0         0 push @rslt, $name;
4730             } elsif ( ( my $id = $body->get( 'id' ) ) =~ $re ) {
4731 0         0 push @rslt, $id;
4732             }
4733             }
4734 0         0 return [ sort @rslt ];
4735             }
4736              
4737             sub _readline_line_to_parts {
4738 0     0   0 my ( $line ) = @_;
4739             # NOTE that the field count of -1 causes a trailing separator to
4740             # result in a trailing empty field.
4741 0         0 my @parts = split $readline_word_break_re, $line, -1;
4742             # NOTE that we strip the leading 'core.' if any, so the return from
4743             # this method does not distinguish between a core command and the
4744             # same-named macro if any.
4745             @parts
4746 0 0       0 and $parts[0] =~ s/ \A core [.] //smx;
4747 0         0 return @parts;
4748             }
4749              
4750             sub _get_time_parser_attribute {
4751 0     0   0 my ( $self, $name ) = @_;
4752 0         0 return $self->{time_parser}->$name();
4753             }
4754              
4755             # $st = $satpass2->_get_spacetrack()
4756              
4757             # Gets the Astro::SpaceTrack object, instantiating it if
4758             # necesary.
4759              
4760             sub _get_spacetrack {
4761 7     7   19 my ( $self ) = @_;
4762             exists $self->{spacetrack}
4763 7 50       77 or $self->{spacetrack} = $self->_get_spacetrack_default();
4764 7         32 return $self->{spacetrack};
4765             }
4766              
4767             # $st = $satpass2->_get_spacetrack_default();
4768             #
4769             # Returns a new Astro::SpaceTrack object, initialized with this
4770             # object's webcmd, and with its filter attribute set to 1.
4771              
4772             sub _get_spacetrack_default {
4773 7     7   20 my ( $self ) = @_;
4774 7 50       34 $have_astro_spacetrack->()
4775             or return;
4776             return Astro::SpaceTrack->new (
4777             webcmd => $self->{webcmd},
4778 0         0 filter => 1,
4779             );
4780             }
4781              
4782             sub _get_day_midnight {
4783 10     10   40 my ( $self, $day ) = @_;
4784 10 100       48 defined $day
4785             or $day = time;
4786 10         64 my $gmt = $self->get( 'formatter' )->gmt();
4787 10 50       76 my @time = $gmt ? gmtime( $day ) : localtime( $day );
4788 10         27 $time[0] = $time[1] = $time[2] = 0;
4789 10         24 $time[5] += 1900;
4790 10 50       67 return $gmt ? greg_time_gm(@time) : greg_time_local(@time);
4791             }
4792              
4793             sub _get_day_noon {
4794 42     42   130 my ( $self, $day ) = @_;
4795 42 100       133 defined $day
4796             or $day = time;
4797 42         175 my $gmt = $self->get( 'formatter' )->gmt();
4798 42 50       256 my @time = $gmt ? gmtime( $day ) : localtime( $day );
4799 42         133 $time[0] = $time[1] = 0;
4800 42         74 $time[2] = 12;
4801 42         109 $time[5] += 1900;
4802 42 50       239 return $gmt ? greg_time_gm(@time) : greg_time_local(@time);
4803             }
4804              
4805             sub _get_warner_attribute {
4806 0     0   0 my ( $self, $name ) = @_;
4807 0         0 return $self->{_warner}->$name();
4808             }
4809              
4810             sub _helper_get_object {
4811 9     9   18 my ( $self, $attribute ) = @_;
4812 9 50       23 my $object = $self->get( $attribute )
4813             or $self->wail( "No $attribute object available" );
4814 9         17 return $object;
4815             }
4816              
4817             {
4818              
4819             my %parse_input = (
4820             formatter => {
4821             desired_equinox_dynamical => sub {
4822             my ( $self, undef, @args ) = @_; # $opt unused
4823             if ( $args[0] ) {
4824             $args[0] = $self->__parse_time( $args[0], 0 );
4825             }
4826             return @args;
4827             },
4828             format => sub {
4829             my ( $self, $opt, $template, @args ) = @_;
4830             $opt->{raw} = 1;
4831             return (
4832             arg => \@args,
4833             sp => $self,
4834             template => $template,
4835             );
4836             },
4837             },
4838             time_parser => {
4839             base => sub {
4840             my ( $self, undef, @args ) = @_; # $opt unused
4841             if ( @args && defined $args[0] ) {
4842             $args[0] = $self->__parse_time( $args[0], time );
4843             }
4844             return @args;
4845             }
4846             },
4847             );
4848              
4849             sub _helper_handler : Verb( changes! raw! ) {
4850 9     9   32 my ( $self, $opt, $name, $method, @args ) = __arguments( @_ );
4851              
4852             exists $opt->{raw}
4853 9 50       39 or $opt->{raw} = ( ! _is_interactive() );
4854              
4855 9 50       26 defined $method
4856             or $self->wail( 'No method name specified' );
4857              
4858 9 50       18 'config' eq $method
4859             and return $self->_helper_config_handler( $name => $opt );
4860              
4861 9         27 my $object = $self->_helper_get_object( $name );
4862 9 50 33     57 $method !~ m/ \A _ /smx and $object->can( $method )
4863             or $self->wail("No such $name method as '$method'");
4864              
4865             @args
4866             and $parse_input{$name}
4867             and $parse_input{$name}{$method}
4868 9 100 66     87 and @args = $parse_input{$name}{$method}->( $self, $opt, @args );
      66        
4869             delete $opt->{raw}
4870 9 100       38 and return $object->$method( @args );
4871 5         18 my @rslt = $object->decode( $method, @args );
4872              
4873 5 100       17 instance( $rslt[0], ref $object ) and return;
4874 2 50       7 ref $rslt[0] and return $rslt[0];
4875 2         7 return quoter( $name, $method, @rslt ) . "\n";
4876 20     20   193 }
  20         43  
  20         162  
4877             }
4878              
4879             sub _helper_config_handler {
4880 0     0   0 my ( $self, $name, $opt ) = @_;
4881 0         0 my $object = $self->_helper_get_object( $name );
4882             my $rslt = $object->config(
4883             changes => $opt->{changes},
4884             decode => ! $opt->{raw},
4885 0         0 );
4886 0 0       0 $opt->{raw} and return $rslt;
4887 0         0 my $output = '';
4888 0         0 foreach my $item ( @{ $rslt } ) {
  0         0  
4889 0         0 $output .= quoter( $name, @{ $item } ) . "\n";
  0         0  
4890             }
4891 0         0 return $output;
4892             }
4893              
4894             # $satpass2->_iridium_status(\@status)
4895              
4896             # Updates the status of all Iridium satellites from the given
4897             # array, which is compatible with the second item returned by
4898             # Astro::SpaceTrack->iridium_status(). If no argument is passed,
4899             # the status is retrieved using Astro::SpaceTrack->iridium_status()
4900              
4901             sub _iridium_status {
4902 0     0   0 my ($self, $status) = @_;
4903 0 0       0 unless ($status) {
4904 0         0 my $st = $self->_get_spacetrack();
4905 0         0 (my $rslt, $status) = $st->iridium_status;
4906 0 0       0 $rslt->is_success or $self->wail($rslt->status_line);
4907             }
4908              
4909 0 0       0 if ( ARRAY_REF eq ref $status ) {
4910 0         0 Astro::Coord::ECI::TLE->status (clear => 'iridium');
4911 0         0 foreach (@$status) {
4912 0         0 Astro::Coord::ECI::TLE->status (add => $_->[0], iridium =>
4913             $_->[4], $_->[1], $_->[3]);
4914             }
4915             } else {
4916 0         0 $self->weep(
4917             'Portable status not passed, and unavailable from Astro::SpaceTrack'
4918             );
4919             }
4920              
4921 0         0 foreach my $tle (@{$self->{bodies}}) {
  0         0  
4922 0         0 $tle->rebless ();
4923             }
4924              
4925 0         0 return;
4926              
4927             }
4928              
4929             # _is_case_tolerant()
4930             # Returns true if the OS supports case-tolerant file names. Yes, I know
4931             # it's the file system that is important, but I don't have access to
4932             # that level of detail.
4933             {
4934             my %os = map { $_ => 1 } qw{ darwin };
4935              
4936             sub _is_case_tolerant {
4937             exists $os{$^O}
4938 0 0   0   0 and return $os{$^O};
4939 0         0 return File::Spec->case_tolerant();
4940             }
4941             }
4942              
4943             # _is_interactive()
4944             #
4945             # Returns true if the dispatch() method is above us on the call
4946             # stack, otherwise returns false.
4947              
4948 20     20   15478 use constant INTERACTIVE_CALLER => __PACKAGE__ . '::dispatch';
  20         48  
  20         4248  
4949             sub _is_interactive {
4950 364     364   493 my $level = 0;
4951 364         1830 while ( my @info = caller( $level ) ) {
4952 1734 100       3010 INTERACTIVE_CALLER eq $info[3]
4953             and return $level;
4954 1696         5980 $level++;
4955             }
4956 326         666 return;
4957             }
4958              
4959             # $self->_load_module ($module_name)
4960              
4961             # Loads the module if it has not yet been loaded. Dies if it
4962             # can not be loaded.
4963              
4964             { # Begin local symbol block
4965              
4966             my %version;
4967             BEGIN {
4968 20     20   158377 %version = (
4969             'Astro::SpaceTrack' => ASTRO_SPACETRACK_VERSION,
4970             );
4971             }
4972              
4973             sub _load_module {
4974 0     0   0 my ($self, @module) = @_;
4975             ARRAY_REF eq ref $module[0]
4976 0 0       0 and @module = @{$module[0]};
  0         0  
4977 0 0       0 @module or $self->weep( 'No module specified' );
4978 0         0 my @probs;
4979 0         0 foreach my $module (@module) {
4980 0 0       0 load_package ($module) or do {
4981 0         0 push @probs, "$module needed";
4982 0         0 next;
4983             };
4984 0         0 my $modver;
4985 0 0 0     0 ($version{$module} && ($modver = $module->VERSION)) and do {
4986 0         0 $modver =~ s/_//g;
4987 0 0       0 $modver < $version{$module} and do {
4988 0         0 push @probs,
4989             "$module version $version{$module} needed";
4990 0         0 next;
4991             };
4992             };
4993 0         0 return $module;
4994             }
4995             {
4996 0         0 my $inx = 1;
  0         0  
4997 0         0 while (my @clr = caller($inx++)) {
4998 0 0       0 $clr[3] eq '(eval)' and next;
4999 0         0 my @raw = split '::', $clr[3];
5000 0 0       0 substr ($raw[-1], 0, 1) eq '_' and next;
5001 0         0 push @probs, "for method $raw[-1]";
5002 0         0 last;
5003             }
5004             }
5005 0         0 my $pfx = 'Error -';
5006 0         0 $self->wail(map {my $x = "$pfx $_\n"; $pfx = ' ' x 7; $x} @probs);
  0         0  
  0         0  
  0         0  
5007 0         0 return; # Can't get here, but Perl::Critic does not know this.
5008             }
5009              
5010             } # end local symbol block.
5011              
5012             # $output = $self->_macro($name,@args)
5013             #
5014             # Execute the named macro. The @args are of course optional.
5015              
5016             sub _macro {
5017 19     19   56 my ($self, $name, @args) = @_;
5018 19 50       114 $self->{macro}{$name} or $self->wail("No such macro as '$name'");
5019 19         102 my $frames = $self->_frame_push(macro => [@args]);
5020             my $macro = $self->{frame}[-1]{macro}{$name} =
5021 19         125 delete $self->{macro}{$name};
5022 19         37 my $output;
5023             my $err;
5024 19 100       40 my $ok = eval {
5025 19         132 $output = $macro->execute( $name, @args );
5026 18         74 1;
5027             } or $err = $@;
5028 19         106 $self->_frame_pop($frames);
5029 19 100       86 $ok or $self->wail($err);
5030 18         87 return $output;
5031             }
5032              
5033             # $angle = _parse_angle_parts ( @parts );
5034             #
5035             # Joins parts of angles into an angle.
5036             # The @parts array is array references describing the parts in
5037             # decreasing significance, with [0] being the value, and [1] being
5038             # the number in the next larger part. For the first piece, [1]
5039             # should be the number in an entire circle.
5040              
5041             sub _parse_angle_parts {
5042 3     3   10 my @parts = @_;
5043 3         5 my $angle = 0;
5044 3         4 my $circle = 1;
5045 3         6 my $places;
5046 3         7 foreach ( @parts ) {
5047 9         26 my ( $part, $size ) = @{ $_ };
  9         18  
5048 9 50       18 defined $part or last;
5049 9         13 $circle *= $size;
5050 9         14 $angle = $angle * $size + $part;
5051 9 50       27 $places = $part =~ m/ [.] ( [0-9]+ ) /smx ? length $1 : 0;
5052             }
5053 3         8 $angle *= 360 / $circle;
5054 3 50       16 if ( my $mag = sprintf '%d', $circle / 360 ) {
5055 3         6 $places += length $mag;
5056             }
5057 3         46 return sprintf( '%.*f', $places, $angle ) + 0;
5058             }
5059              
5060             # Documented in POD
5061              
5062             sub __parse_angle {
5063 40     40   104 my ( $self, @args ) = @_;
5064 40 100       134 my $opt = HASH_REF eq ref $args[0] ? shift @args : {};
5065 40         92 my ( $angle ) = @args;
5066 40 100       101 defined $angle or return;
5067              
5068 33 100       307 if ( $angle =~ m/ : /smx ) {
    100          
5069              
5070 2         10 my ($h, $m, $s) = split ':', $angle;
5071 2         15 return _parse_angle_parts(
5072             [ $h => 24 ],
5073             [ $m => 60 ],
5074             [ $s => 60 ],
5075             );
5076              
5077             } elsif ( $angle =~
5078             m{ \A ( [-+] )? ( [0-9]* ) d
5079             ( [0-9]* (?: [.] [0-9]* )? ) (?: m
5080             ( [0-9]* (?: [.] [0-9]* )? ) s? )? \z
5081             }smxi ) {
5082 1         5 my ( $sgn, $deg, $min, $sec ) = ( $1, $2, $3, $4 );
5083 1         7 $angle = _parse_angle_parts(
5084             [ $deg => 360 ],
5085             [ $min => 60 ],
5086             [ $sec => 60 ],
5087             );
5088 1 50 33     4 $sgn and '-' eq $sgn and return -$angle;
5089 1         3 return $angle;
5090             }
5091              
5092             $opt->{accept}
5093 30 50 66     248 or looks_like_number( $angle )
5094             or $self->wail( "Invalid angle '$angle'" );
5095              
5096 30         110 return $angle;
5097             }
5098              
5099             # Documented in POD
5100             {
5101             my %units = (
5102             au => AU,
5103             ft => 0.0003048,
5104             km => 1,
5105             ly => LIGHTYEAR,
5106             m => .001,
5107             mi => 1.609344,
5108             pc => PARSEC,
5109             );
5110              
5111             sub __parse_distance {
5112 3     3   9 my ($self, $string, $dfdist) = @_;
5113 3 50       10 defined $dfdist or $dfdist = 'km';
5114 3 50       37 my $dfunits = $dfdist =~ s/ ( [[:alpha:]]+ ) \z //smx ? $1 : 'km';
5115 3 50       25 my $units = lc (
5116             $string =~ s/ \s* ( [[:alpha:]]+ ) \z //smx ? $1 : $dfunits );
5117 3 50       13 $units{$units}
5118             or $self->wail( "Units of '$units' are unknown" );
5119 3 50       12 $string ne '' or $string = $dfdist;
5120 3 50       12 looks_like_number ($string)
5121             or $self->wail( "'$string' is not a number" );
5122 3         19 return $string * $units{$units};
5123             }
5124             }
5125              
5126             # Documented in POD
5127              
5128             sub __parse_time {
5129 55     55   1574 my ($self, $time, $default) = @_;
5130             my $pt = $self->{time_parser}
5131 55 50       205 or $self->wail( 'No time parser available' );
5132 55 50       291 $self->{time_parser}->can( 'station' )
5133             and $self->_set_time_parser_attribute(
5134             station => $self->station() );
5135 55 50       232 if ( defined( my $time = $pt->parse( $time, $default ) ) ) {
5136 55         142 return $time;
5137             }
5138 0         0 $self->wail( "Invalid time '$time'" );
5139 0         0 return;
5140             }
5141              
5142             # Reset the last time set. This is called from __arguments() in
5143             # ::Utils if the invocant is an Astro::App::Satpass2.
5144              
5145             sub __parse_time_reset {
5146 332     332   716 my ( $self ) = @_;
5147             defined ( my $pt = $self->{time_parser} )
5148 332 100       1191 or return;
5149 311         1790 $pt->reset();
5150 311         635 return;
5151             }
5152              
5153             # $string = _rad2hms ($angle)
5154              
5155             # Converts the given angle in radians to hours, minutes, and
5156             # seconds (of right ascension, presumably)
5157              
5158             sub _rad2hms {
5159 1     1   3 my $sec = shift;
5160 1         3 $sec *= 12 / PI;
5161 1         11 my $hr = floor( $sec );
5162 1         4 $sec = ( $sec - $hr ) * 60;
5163 1         4 my $min = floor( $sec );
5164 1         4 $sec = ( $sec - $min ) * 60;
5165 1         10 my $rslt = sprintf '%2d:%02d:%02d', $hr, $min, floor( $sec + .5 );
5166 1         18 return $rslt;
5167             }
5168              
5169             # $line = $self->_read_continuation( $in, $error_message );
5170             #
5171             # Acquire a line from $in, which must be a code reference taking
5172             # the prompt as an argument. If $in is not a code reference, or if
5173             # it returns undef, we wail() with the error message. Otherwise
5174             # we return the line read. I expect this to be used only by
5175             # __tokenize().
5176              
5177             sub _read_continuation {
5178 15     15   51 my ( $self, $in, $error ) = @_;
5179             $in and defined( my $more = $in->(
5180             my $prompt = $self->get( 'continuation_prompt' ) ) )
5181 15 100 66     87 or do {
5182 1 50       3 $error or return;
5183 1 50       3 ref $error eq CODE_REF
5184             and return $error->();
5185 1         4 $self->wail( $error );
5186             };
5187 14 50       189 $self->{echo} and $self->whinge( $prompt, $more );
5188 14 100       93 $more =~ m/ \n \z /smx or $more .= "\n";
5189 14         51 return $more;
5190             }
5191              
5192             # my ( $old_obj ) = $self->_replace_in_sky( $name, $new_obj );
5193             # This is restricted to objects constructed via {sky_class}.
5194             # The return is an array containing the replaced body, or nothing if
5195             # the body was not found. The $new_obj is optional; if not provided a
5196             # new object is created.
5197             sub _replace_in_sky {
5198 0     0   0 my ( $self, $name, $new_obj ) = @_;
5199             $new_obj
5200 0 0 0     0 or $self->{sky_class}{ fold_case( $name ) }
5201             or $self->weep( "Can not replace $name; no class defined" );
5202 0 0       0 defined( my $inx = $self->_find_in_sky( $name ) )
5203             or return;
5204 0   0     0 return splice @{ $self->{sky} }, $inx, $inx + 1,
  0         0  
5205             $new_obj || $self->_sky_object( $name );
5206             }
5207              
5208             # $self->_rewrite_level1_command( $buffer, $context );
5209             #
5210             # This method rewrites a level1 command to its current form. The
5211             # arguments are the buffer containing the command, and an
5212             # initially-empty hash reference, which the method will use to
5213             # preserve context across lines of command. NOTE that more than
5214             # one rewritten command may be returned (e.g. 'almanac' into
5215             # ( 'location', 'almanac' ).
5216              
5217             {
5218              
5219             my %level1_map = (
5220             almanac => sub {
5221             return ( 'location', $_[0] );
5222             },
5223             flare => sub {
5224             local $_ = $_[0];
5225             s/ (?<= \s ) - ( am|pm|day ) \b /-no$1/sxmg;
5226             return $_;
5227             },
5228             pass => sub {
5229             return ( 'location', $_[0] );
5230             },
5231             );
5232              
5233             my %level1_requote = (
5234             # In a macro definition:
5235             macro => {
5236             # In single-quoted strings,
5237             q{'} => sub {
5238             # escaped interpolations and double quotes may be
5239             # unescaped,
5240             s{ (?: \A | (?
5241             }{$1$2}sxmg;
5242             # and the string remains single-quoted.
5243             $_ = qq{'$_'};
5244             return;
5245             },
5246             # In double-quoted strings,
5247             q{"} => sub {
5248             # escaped interpolations and double quotes may be
5249             # unescaped,
5250             s{ (?: \A | (?
5251             }{$1$2}sxmg;
5252             # unescaped single quotes become double quotes,
5253             s/ (?: \A | (?
5254             # and the string becomes single-quoted.
5255             $_ = qq{'$_'};
5256             return;
5257             },
5258             },
5259             # Anywhere else
5260             '' => {
5261             # In single-quoted strings,
5262             q{'} => sub {
5263             # unescaped double quotes must be escaped,
5264             s/ (?: \A | (?
5265             # escaped single quotes may be unescaped,
5266             s/ (?: \A | (?
5267             # and the string becomes double-quoted.
5268             $_ = qq{"$_"};
5269             return;
5270             },
5271             # In double-quoted strings,
5272             q{"} => sub {
5273             # no changes need to be made.
5274             $_ = qq{"$_"};
5275             return;
5276             },
5277             },
5278             );
5279              
5280             sub _rewrite_level1_command {
5281 19     19   48 my ( undef, $buffer, $context ) = @_; # Invocant unused
5282              
5283 19         38 my $command = delete $context->{command};
5284              
5285 19 100       49 defined $buffer
5286             or return $buffer;
5287 12 50       75 $buffer =~ m/ \A \s* \z /sxm
5288             and return $buffer;
5289 12 50       43 $buffer =~ s/ \A \s* [#] 2 [#] \s* //sxm
5290             and return $buffer;
5291 12 50       38 $buffer =~ m/ \A \s* [#] /sxm
5292             and return $buffer;
5293              
5294 12 50       26 if ( ! defined $command ) {
5295 12 100       60 $buffer =~ m/ \A \s* ( \w+ ) /sxm
5296             or return $buffer;
5297 11         37 $command = $1;
5298             }
5299 11         22 my $append = '';
5300 11 100       133 $buffer =~ s/ ( \s* \\? \n ) //sxm
5301             and $append = $1;
5302             $append =~ m/ \\ /sxm
5303 11 50       34 and $context->{command} = $command;
5304              
5305 11   66     47 my $handler = $level1_requote{$command} || $level1_requote{''};
5306 11         22 my ( $this_quote, $start_pos );
5307 11         137 while ( $buffer =~ m/ (?: \A | (?
5308             ) {
5309 22 100       117 if ( ! defined $start_pos ) {
    100          
5310 9         35 $start_pos = $+[0] - 1;
5311 9         125 $this_quote = $1;
5312             } elsif ( $1 eq $this_quote ) {
5313 9         27 my $length = $+[0] - $start_pos;
5314 9         44 local $_ = substr $buffer, $start_pos + 1, $length - 2;
5315 9         35 $handler->{$this_quote}->();
5316 9         29 substr $buffer, $start_pos, $length, $_;
5317 9         32 pos( $buffer ) = $start_pos + length $_;
5318 9         46 $start_pos = undef;
5319             }
5320             }
5321              
5322 11 100       67 my $code = $level1_map{$command}
5323             or return $buffer . $append;
5324              
5325 3         7 my @rslt = $code->( $buffer );
5326 3         7 $rslt[-1] .= $append;
5327 3         21 return @rslt;
5328              
5329             }
5330             }
5331              
5332             # $self->_rewrite_level1_macros();
5333             #
5334             # This method rewrites all macros defined by a satpass
5335             # initialization file (as opposed to a satpass2 initialization
5336             # file) to be satpass2-compatible. It also clears the level1 flag
5337             # so that the satpass-compatible functionality is not invoked.
5338             #
5339             # Specifically it:
5340             # * Inserts a 'location' command before 'almanac' and 'pass';
5341             # * Changes the senses of the -am, -day, and -pm options in
5342             # 'flare';
5343             # * Removes delegated attributes from 'localize', replacing them
5344             # with a localization of the helper object.
5345             #
5346             # This method goes away when the satpass functionality does.
5347              
5348             {
5349             my %helper_map = (
5350             date_format => {
5351             helper => 'formatter', # Helper obj attr. Req'd.
5352             },
5353             desired_equinox_dynamical => {
5354             helper => 'formatter',
5355             },
5356             gmt => {
5357             helper => 'formatter',
5358             },
5359             local_coord => {
5360             helper => 'formatter',
5361             },
5362             time_format => {
5363             helper => 'formatter',
5364             },
5365             );
5366              
5367             my %filter = (
5368             almanac => sub {
5369             my ( undef, $line ) = @_; # $verb unused
5370             return ( 'location', $line );
5371             },
5372             flare => sub {
5373             my ( undef, $line ) = @_; # $verb unused
5374             $line =~ s/ (?<= \s ) - (am|day|pm) \b /-no$1/smx;
5375             return $line;
5376             },
5377             localize => sub {
5378             my ( undef, $line ) = @_; # $verb unused
5379             my @things = split qr{ \s+ }smx, $line;
5380             my @output;
5381             my %duplicate;
5382             foreach my $token ( @things ) {
5383             $helper_map{$token}
5384             and $token = $helper_map{$token}{helper};
5385             $duplicate{$token}++ or push @output, $token;
5386             }
5387             return join ' ', @output;
5388             },
5389             pass => sub {
5390             my ( undef, $line ) = @_; # $verb unused
5391             return ( 'location', $line );
5392             },
5393             set => sub {
5394             my ( undef, $line ) = @_; # $verb unused
5395             my @output = [ 'fubar' ]; # Prime the pump.
5396             my @input = Text::ParseWords::quotewords( qr{ \s+ }smx, 1,
5397             $line );
5398             shift @input;
5399             while ( @input ) {
5400             my ( $attr, $val ) = splice @input, 0, 2;
5401             if ( my $helper = $helper_map{$attr} ) {
5402             push @output, [ $helper->{helper},
5403             # not quoter( $val ) here, because presumably it
5404             # is already quoted if it needs to be.
5405             $helper->{attribute} || $attr, $val ];
5406             } else {
5407             'set' eq $output[-1][0]
5408             or push @output, [ 'set' ];
5409             # not quoter( $val ) here, because presumably it is
5410             # already quoted if it needs to be.
5411             push @{ $output[-1] }, $attr, $val;
5412             }
5413             }
5414             shift @output; # Get rid of the pump priming.
5415             return ( map { join ' ', @{ $_ } } @output );
5416             },
5417             st => sub {
5418             my ( undef, $line ) = @_; # $verb unused
5419             m/ \A \s* st \s+ localize \b /smx
5420             and return $line;
5421             $line =~ s/ \b st \b /spacetrack/smx;
5422             return $line;
5423             },
5424             show => sub {
5425             my ( undef, $line ) = @_; # $verb unused
5426             my @output = [ 'fubar' ];
5427             my @input = split qr{ \s+ }smx, $line;
5428             shift @input;
5429             foreach my $attr ( @input ) {
5430             if ( my $helper = $helper_map{$attr} ) {
5431             push @output, [ $helper->{helper},
5432             $helper->{attribute} || $attr ];
5433             } else {
5434             'show' eq $output[-1][0]
5435             or push @output, [ 'show' ];
5436             push @{ $output[-1] }, $attr;
5437             }
5438             }
5439             shift @output;
5440             return ( map { join ' ', @{ $_ } } @output );
5441             },
5442             );
5443              
5444             # Called by macro object's __level1_rewrite().
5445             sub __rewrite_level1_macro_def {
5446 8     8   21 my ( $self, $name, $args ) = @_;
5447              
5448 8         16 my ( $rewrote, @rslt );
5449 8         14 foreach ( @{ $args } ) {
  8         18  
5450 8 100 100     99 if ( m/ ( \S+ ) /smx
      66        
      66        
5451             and ( not $self->{macro}{$1}
5452             or $1 eq $name )
5453             and my $code = $filter{$1} ) {
5454 7         39 push @rslt, $code->( $1, $_ );
5455 7         24 $rewrote++;
5456             } else {
5457 1         4 push @rslt, $_;
5458             }
5459             }
5460              
5461 8 100       40 return $rewrote ? \@rslt : $args;
5462             }
5463              
5464             sub _rewrite_level1_macros {
5465 4     4   22 my ( $self ) = @_;
5466              
5467 4         8 foreach my $macro ( values %{ $self->{macro} } ) {
  4         14  
5468 8         28 $macro->__level1_rewrite();
5469             }
5470              
5471 4         27 return;
5472             }
5473             }
5474              
5475             # @coordinates = $self->_simbad4 ($query)
5476              
5477             # Look up the given star in the SIMBAD catalog. This assumes
5478             # SIMBAD 4.
5479              
5480             # We die on any error.
5481              
5482             sub _simbad4 {
5483 0     0   0 my $self = shift;
5484 0         0 $self->_load_module ('Astro::SIMBAD::Client');
5485 0         0 my $query = shift;
5486             my $simbad = Astro::SIMBAD::Client->new (
5487             format => {txt => 'FORMAT_TXT_SIMPLE_BASIC'},
5488             parser => {
5489             script => 'Parse_TXT_Simple',
5490             txt => 'Parse_TXT_Simple',
5491             },
5492             server => $self->{simbad_url},
5493 0         0 type => 'txt',
5494             );
5495             # I prefer script() to query() these days because the former does
5496             # not require SOAP::Lite, which seems to be getting flakier as time
5497             # goes on.
5498             # TODO get rid of $fmt =~ s/// once I massage
5499             # FORMAT_TXT_SIMPLE_BASIC in Astro::SIMBAD::Client
5500             # my @rslt = $simbad->query (id => $query)
5501 0         0 my $fmt = Astro::SIMBAD::Client->FORMAT_TXT_SIMPLE_BASIC();
5502 0         0 $fmt =~ s/ \n //smxg;
5503 0 0       0 my @rslt = $simbad->script( <<"EOD" )
5504             format obj "$fmt"
5505             query id $query
5506             EOD
5507             or $self->wail("No entry found for $query");
5508 0 0       0 @rslt > 1
5509             and $self->wail("More than one entry found for $query");
5510 0 0 0     0 @rslt = map {$rslt[0]{$_} eq '~' ? 0 : $rslt[0]{$_} || 0} qw{
  0         0  
5511             ra dec plx pmra pmdec radial};
5512 0 0 0     0 ($rslt[0] && $rslt[1])
5513             or $self->wail("No position returned by $query");
5514 0 0       0 $rslt[2] = $rslt[2] ? 1000 / $rslt[2] : 10000;
5515 0 0       0 $rslt[3] and $rslt[3] /= 1000;
5516 0 0       0 $rslt[4] and $rslt[4] /= 1000;
5517 0 0       0 return wantarray ? @rslt : join ' ', @rslt;
5518             }
5519              
5520             sub _templates_to_options {
5521 24     24   82 my ( $self, $name, $opt ) = @_;
5522              
5523 24         104 $opt->{_template} = $name;
5524             my $code = sub {
5525 5     5   4651 my ( $opt_name, $opt_value ) = @_;
5526 5 50       68 $opt->{_template} = $opt_value ? "${name}_$opt_name" : $name;
5527 5         75 return;
5528 24         151 };
5529 24         858 my $re = qr< \A \Q$name\E _ ( \w+ ) \z >smx;
5530 24         60 my @rslt;
5531             my %valid_format;
5532 24         82 my $fmtr = $self->get( 'formatter' );
5533 24 50       176 if ( $fmtr->can( '__list_templates' ) ) {
5534 24         102 foreach ( $fmtr->__list_templates() ) {
5535 720 100       2133 $_ =~ $re
5536             or next;
5537 52         174 $valid_format{$1} = 1;
5538 52         171 push @rslt, "$1!", $code;
5539             }
5540             }
5541             @rslt
5542             and push @rslt, 'format=s', sub {
5543 0     0   0 my ( undef, $opt_value ) = @_;
5544 0 0       0 $valid_format{$opt_value}
5545             or $self->wail( "Invalid format '$opt_value'" );
5546 0         0 $opt->{_template} = "${name}_$opt_value";
5547 0         0 return;
5548 24 50       318 };
5549 24         338 return @rslt;
5550             }
5551              
5552             # ($tokens, $redirect) = $self->__tokenize(
5553             # {option => $value}, $buffer, [$arg0 ...]);
5554             #
5555             # This method tokenizes the buffer. The options hash may be
5556             # omitted, in which case the $buffer to be tokenized is the first
5557             # argument. After the buffer is an optional reference to an array
5558             # of arguments to be substituted in.
5559             #
5560             # This method attempts to parse and tokenize the buffer in a way
5561             # similar to the bash shell. That is, parameters are interpolated
5562             # inside double quotes but not single quotes, tilde expansion
5563             # takes place unless quoted, and spaces delimit tokens only when
5564             # occurring outside quotes.
5565             #
5566             # The back slash character ('\') is an escape character. Inside
5567             # single quotes only the back slash itself and a single quote may
5568             # be escaped. Otherwise, anything can be escaped.
5569             #
5570             # The returns are a reference to an array of tokens found, and a
5571             # reference to a hash of redirections found. This hash will have
5572             # zero or more of the keys '>' (standard output redirection) and
5573             # '<' (standard input redirection. The value of each key will be a
5574             # reference to a hash containing keys 'mode' ('>' or '>>' for
5575             # output, '<' or '<<' for input) and 'name' (normally the file
5576             # name).
5577             #
5578             # The recognized options are:
5579             #
5580             # single => 1
5581             # causes the buffer to be interpreted as a single token.
5582             #
5583             # noredirect => 1
5584             # causes redirects to be illegal.
5585             #
5586             # If noredirect is specified, only the $tokens reference is
5587             # returned. If noredirect and single are both specified, the
5588             # parsed and interpolated token is returned.
5589             #
5590             # If interpolation is being done, an unescaped dollar sign
5591             # introduces the interpolation. This works pretty much the same
5592             # way as under bash: if the first character after the dollar sign
5593             # is a left curly bracket, everything to the corresponding right
5594             # curly bracked specifies the interpolation; if not, the rule is
5595             # that word characters specify the interpolation.
5596             #
5597             # A number (i.e. $1) specifies interpolation of an argument.
5598             # Arguments are numbered starting at 1.
5599             #
5600             # Otherwise, if the interpolation names an attribute, the value of
5601             # that attribute is interpolated in, otherwise the named
5602             # environment variable is interpolated in.
5603             #
5604             # Most of the fancier forms of interpolation are suported. In the
5605             # following, word is expanded by recursively calling __tokenize
5606             # with options {single => 1, noredirect => 1}. But unlike bash, we
5607             # make no distinction between unset or null. The ':' can be
5608             # omitted before the '-', '=', '?' or '+', but it does not change
5609             # the functionality.
5610             #
5611             # ${parameter:-word} causes the given word to be substituted if
5612             # the parameter is undefined.
5613             #
5614             # ${parameter:=word} is the same as above, but also causes the
5615             # word to be assigned to the parameter if it is unassigned. Unlike
5616             # bash, this assignment takes place on positional parameters. If
5617             # done on an attribute or environment variable, it causes that
5618             # attribute or environment variable to be set to the given value.
5619             #
5620             # ${parameter:?word} causes the parse to fail with the error
5621             # 'word' if the parameter is undefined.
5622             #
5623             # ${parameter:+word} causes the value of the given word to be used
5624             # if the parameter is defined, otherwise '' is used.
5625             #
5626             # ${parameter:offset} and ${parameter:offset:length} take
5627             # substrings of the parameter value. The offset and length must be
5628             # numeric.
5629              
5630             {
5631              
5632             # Special variables.
5633             # Calling sequence: $special{$name}->(\@args, $relquote)
5634             my %special = (
5635             '0' => sub { return $0 },
5636             '#' => sub { return scalar @{ $_[0] } },
5637             ## '*' => sub { return join ' ', @{ $_[0] } },
5638             ## '@' => sub { return $_[1] ? join( ' ', @{ $_[0] } ) : $_[0] },
5639             '*' => sub { return $_[1] ? join( ' ', @{ $_[0] } ) : $_[0] },
5640             '@' => sub { return $_[0] },
5641             '$' => sub { return $$ },
5642             '_' => sub { return $^X },
5643             );
5644              
5645             my %case_ctl = (
5646             E => sub { delete $_[0]->{_case_mod} },
5647             F => sub { $_[0]->{_case_mod}{case} = sub { fold_case( $_[1] ) } },
5648             L => sub { $_[0]->{_case_mod}{case} = sub { lc $_[1] } },
5649             U => sub { $_[0]->{_case_mod}{case} = sub { uc $_[1] } },
5650             l => sub { $_[0]->{_case_mod}{single} = sub { lcfirst $_[1] } },
5651             u => sub { $_[0]->{_case_mod}{single} = sub { ucfirst $_[1] } },
5652             );
5653              
5654             # Leading punctuation that is equivalent to a method.
5655             my %command_equivalent = (
5656             '.' => 'source',
5657             '!' => 'system',
5658             );
5659             my $command_equiv_re = do {
5660             my $keys = join '', sort keys %command_equivalent;
5661             qr{ [$keys] }smx;
5662             };
5663              
5664             my %escape = (
5665             t => "\t",
5666             n => "\n",
5667             r => "\r",
5668             f => "\f",
5669             b => "\b",
5670             a => "\a",
5671             e => "\e",
5672             );
5673              
5674             sub __tokenize {
5675 381     381   149115 my ($self, @parms) = @_;
5676 381         1408 local $self->{_case_mod} = undef;
5677 381 100       1403 my $opt = HASH_REF eq ref $parms[0] ? shift @parms : {};
5678 381         927 my $in = $opt->{in};
5679 381         766 my $buffer = shift @parms;
5680 381 100       1903 $buffer =~ m/ \n \z /smx or $buffer .= "\n";
5681 381   100     1056 my $args = shift @parms || [];
5682 381         940 my @rslt = ( {} );
5683 381         652 my $absquote; # True if inside ''
5684             my $relquote; # True if inside "" (and not in '')
5685 381         743 my $len = length $buffer;
5686 381         651 my $inx = 0;
5687              
5688             # Because I'm not smart enough to do all this with a regular
5689             # expression, I take the brute force approach and iterate
5690             # through the buffer to be tokenized. It's a 'while' rather than
5691             # a 'for' or 'foreach' because that way I get to muck around
5692             # with the current position inside the loop.
5693              
5694 381         1135 while ($inx < $len) {
5695 6313         18138 my $char = substr $buffer, $inx++, 1;
5696              
5697             # If we're inside single quotes, the only escapable
5698             # characters are single quote and back slash, and all
5699             # characters until the next unescaped single quote go into
5700             # the current token
5701              
5702 6313 100 66     41110 if ( $absquote ) {
    100 100        
    100 100        
    100 66        
    100 66        
    100          
    100          
    100          
    100          
    100          
5703 621 50       1308 if ( $char eq '\\' ) {
    100          
5704 0 0       0 if ( (my $next = substr $buffer, $inx, 1) =~
5705             m/ ['\\] /smx ) {
5706 0         0 $inx++;
5707 0         0 $rslt[-1]{token} .= $next;
5708             } else {
5709 0         0 $rslt[-1]{token} .= $char;
5710             }
5711             } elsif ( $char eq q{'} ) {
5712 34         81 $absquote = undef;
5713             } else {
5714 587         1041 $rslt[-1]{token} .= $char;
5715 587 100       1184 if ( $inx >= $len ) {
5716 2         13 $buffer .= $self->_read_continuation( $in,
5717             'Unclosed single quote' );
5718 1         5 $len = length $buffer;
5719             }
5720             }
5721              
5722             # If we have a backslash, it escapes the next character,
5723             # which goes on the current token no matter what it is.
5724              
5725             } elsif ( $char eq '\\' ) {
5726 10         33 my $next = substr $buffer, $inx++, 1;
5727 10 100       39 if ( $inx >= $len ) { # At end of line
    100          
5728 2 50       35 if ( $relquote ) { # Inside ""
5729 0         0 $buffer .= $self->_read_continuation( $in,
5730             'Unclosed double quote' );
5731             } else { # Between tokens
5732 2         19 $buffer .= $self->_read_continuation( $in,
5733             'Dangling continuation' );
5734 2 50       17 $opt->{single} or push @rslt, {}; # New token
5735             }
5736 2         10 $len = length $buffer;
5737             } elsif ( $relquote ) {
5738 7 100       29 if ( my $code = $case_ctl{$next} ) {
5739 6         19 $code->( $self );
5740             } else {
5741 1   33     9 $rslt[-1]{token} .= $escape{$next} || $next;
5742             }
5743             } else {
5744 1         3 $rslt[-1]{token} .= $next;
5745             }
5746              
5747             # If we have a single quote and we're not inside double
5748             # quotes, we go into absolute quote mode. We also append an
5749             # empty string to the current token to force its value to be
5750             # defined; otherwise empty quotes do not generate tokens.
5751              
5752             } elsif ($char eq q{'} && !$relquote) {
5753 35         135 $rslt[-1]{token} .= ''; # Empty string, to force defined.
5754 35         87 $absquote++;
5755              
5756             # If we have a double quote, we toggle relative quote mode.
5757             # We also append an empty string to the current tokens for
5758             # the reasons discussed above.
5759              
5760             } elsif ($char eq '"') {
5761 44         153 $rslt[-1]{token} .= ''; # Empty string, to force defined.
5762             ( $relquote = !$relquote )
5763 44 100       151 or delete $self->{_case_mod};
5764              
5765             # If we have a whitespace character and we're not inside
5766             # quotes and not in single-token mode, we start a new token.
5767             # It is possible that we generate redundant tokens this way,
5768             # but the unused ones are eliminated later.
5769              
5770             } elsif ($char =~ m/ \s /smx && !$relquote && !$opt->{single}) {
5771 937         1942 push @rslt, {};
5772              
5773             # If we have a dollar sign, it introduces parameter
5774             # substitution, a non trivial endeavor.
5775              
5776             } elsif ( $char eq '$' && $inx < $len ) {
5777 72         191 my $name = substr $buffer, $inx++, 1;
5778 72         133 my $brkt;
5779              
5780             # Names beginning with brackets are special. We note the
5781             # fact and scan for the matching close bracket, throwing
5782             # an exception if we do not have one.
5783              
5784 72 100 66     492 if ($name eq '{' && $inx < $len) {
    100          
5785 34         72 $brkt = 1;
5786 34         60 $name = '';
5787 34         69 my $nest = 1;
5788 34         118 while ($inx < $len) {
5789 369         587 $char = substr $buffer, $inx++, 1;
5790 369 50       959 if ($char eq '{') {
    100          
5791 0         0 $nest++;
5792             } elsif ($char eq '}') {
5793 33 50       142 --$nest or last;
5794             }
5795 336         647 $name .= $char;
5796             }
5797 34 100       100 $char eq '}'
5798             or $self->wail('Missing right curly bracket');
5799              
5800             # If the name begins with an alpha or an underscore, we
5801             # simply append any word ('\w') characters to it. If it
5802             # the word characters are immediately followed by a dot
5803             # and more word characters we grab them too, and advance
5804             # the current location past whatever we grabbed. The dot
5805             # syntax is in aid of accessing attributes of
5806             # attributes (e.g. $formatter.time_format)
5807              
5808             } elsif ( $name =~ m/ \A [[:alpha:]_] \z /smx ) {
5809 21         87 pos( $buffer ) = $inx;
5810 21 50       155 if ( $buffer =~ m/ \G ( \w* (?: [.] \w+ )? ) /smxgc ) {
5811 21         81 $name .= $1;
5812 21         55 $inx += length $1;
5813             }
5814             }
5815              
5816             # Only bracketed names can be indirected, and then only
5817             # if the first character is a bang.
5818              
5819 71         173 my ($indirect, $value);
5820 71 100       269 $brkt and $indirect = $name =~ s/ \A ! //smx;
5821              
5822             # If we find a colon and/or one of the other cabbalistic
5823             # characters, we need to do some default processing.
5824              
5825 71 100       516 if ($name =~ m/ (.*?) ( [:]? [\-\+\=\?] | [:] ) (.*) /smx) {
5826 28         216 my ($name, $flag, $rest) = ($1, $2, $3);
5827              
5828             # First we do indirection if that was required.
5829              
5830 28 50       90 $indirect
5831             and $name = $self->_tokenize_var(
5832             $name, $args, $relquote, $indirect);
5833              
5834             # Next we find out whether we have an honest-to-God
5835             # colon, since that might specify substring
5836             # processing.
5837              
5838             ## my $colon = $flag =~ s/ \A : //smx ? ':' : '';
5839 28         132 $flag =~ s/ \A : //smx;
5840              
5841             # We run the stuff after the first cabbalistic
5842             # character through the tokenizer, since further
5843             # expansion is possible here.
5844              
5845 28         202 my $mod = __tokenize(
5846             $self,
5847             { single => 1, noredirect => 1, in => $in },
5848             $rest, $args);
5849 28         102 chomp $mod; # Don't want trailing \n here.
5850              
5851             # At long last we get the actual value of the
5852             # variable. This will be either undef, a scalar, or
5853             # a list reference.
5854              
5855 28         119 $value = $self->_tokenize_var(
5856             $name, $args, $relquote);
5857              
5858             # The value is logically defined if it is a scalar
5859             # and not undef, or if it is an array reference and
5860             # the array is not empty.
5861              
5862 28 100       89 my $defined = ref $value ? @$value : defined $value;
5863              
5864             # The '+' cabbalistic sign replaces the value of the
5865             # variable if it is logically defined.
5866              
5867 28 100       142 if ($flag eq '+') {
    100          
    100          
    100          
    100          
    50          
5868 4 100       37 $value = $defined ? $mod : '';
5869              
5870             # If the variable is defined, only substring
5871             # processing is possible. This actually is
5872             # implemented as slice processing if the value is an
5873             # array reference.
5874              
5875             } elsif ($defined) {
5876 16 100       82 if ($flag eq '') {
5877 10         37 my @pos = split ':', $mod, 2;
5878 10         25 foreach ( @pos ) {
5879 18         74 s/ \A \s+ //smx;
5880             }
5881 10 50       33 @pos > 2
5882             and $self->wail(
5883             'Substring expansion has extra arguments' );
5884 10         22 foreach ( @pos ) {
5885 18 50       83 m/ \A [-+]? [0-9]+ \z /smx
5886             or $self->wail(
5887             'Substring expansion argument non-numeric'
5888             );
5889             }
5890 10 100       26 if (ref $value) {
5891 4 50       12 if (@pos > 1) {
5892 4         35 $pos[1] += $pos[0] - 1;
5893             } else {
5894 0         0 $pos[1] = $#$args;
5895             }
5896 4 100       15 $pos[1] > $#$value and $pos[1] = $#$value;
5897 4         21 $value = [@$value[$pos[0] .. $pos[1]]];
5898             } else {
5899             # We want to disable warnings if we slop
5900             # outside the string.
5901 20     20   193 no warnings qw{substr};
  20         45  
  20         56130  
5902 6 100       43 $value = @pos == 1 ? substr $value, $pos[0] :
5903             substr $value, $pos[0], $pos[1];
5904             }
5905             }
5906              
5907             # If the cabbalistic sign is '-', we supply the
5908             # remainder of the specification as the default.
5909              
5910             } elsif ($flag eq '-') {
5911 2         8 $value = $mod;
5912              
5913             # If the cabbalistic sign is '=', we supply the
5914             # remainder of the specification as the default. We
5915             # also set the variable to the value, for future
5916             # use. Note that special variables may not be set,
5917             # and result in an exception.
5918              
5919             } elsif ($flag eq '=') {
5920 3         12 $value = $mod;
5921 3 50 33     56 if ( $special{$name} || $name !~ m/ \D /smx ) {
    50          
5922 0         0 $self->wail("Cannot assign to \$$name");
5923             ## } elsif ($name !~ m/\D/) {
5924             ## $args->[$name - 1] = $value;
5925             } elsif (exists $mutator{$name}) {
5926 0         0 $self->set($name => $value);
5927             } else {
5928 3         27 $self->{frame}[-1]{define}{$name} = $value;
5929             }
5930              
5931             # If the cabbalistic sign is '?', we throw an
5932             # exception with the remainder of the specification
5933             # as the text.
5934              
5935             } elsif ($flag eq '?') {
5936 2         13 $self->wail($mod);
5937              
5938             # If there is no cabbalistic sign at all, we fell
5939             # through here trying to do substring expansion on
5940             # an undefined variable. Since Bash allows this, we
5941             # will to, though with misgivings.
5942              
5943             } elsif ( $flag eq '' ) {
5944 1         5 $value = '';
5945              
5946             # Given the way the parser works, the above should
5947             # have exhausted all possibilities. But being a
5948             # cautious programmer ...
5949              
5950             } else {
5951 0         0 $self->weep(
5952             "\$flag = '$flag'. This should not happen"
5953             );
5954             }
5955              
5956             # Without any cabbalistic signs, variable expansion is
5957             # easy. We perform the indirection if needed, and then
5958             # grab the value of the variable, which still can be
5959             # undef, a scalar, or an array reference.
5960              
5961             } else {
5962 43 100       137 $indirect
5963             and $name = $self->_tokenize_var(
5964             $name, $args, $relquote, $indirect);
5965 43         182 $value = $self->_tokenize_var(
5966             $name, $args, $relquote);
5967             }
5968              
5969             # For simplicity in what follows, make the value into an
5970             # array reference.
5971 69 100       405 ref $value
    100          
5972             or $value = defined $value ? [ $value ] : [];
5973              
5974             # If we are inside quotes
5975 69 100       151 if ( $relquote ) {
5976             # do case modification
5977             # NOTE that the argument list is modified in-place.
5978 12         21 $self->_case_mod( @{ $value } );
  12         42  
5979             } else {
5980             # otherwise do word splitting
5981 57         91 $value = [ map { split qr{ \s+ }smx } @{ $value } ];
  71         542  
  57         156  
5982             }
5983              
5984             # If we have a value, append each element to the current
5985             # token, and then create a new token for the next
5986             # element. The last element's empty token gets
5987             # discarded, since we may need to append more data to
5988             # the last element (e.g. "$@ foo").
5989 69 100       173 if ( @{ $value } ) {
  69         191  
5990 58         130 foreach ( @$value ) {
5991 86         243 $rslt[-1]{token} .= $_;
5992 86         237 push @rslt, {};
5993             }
5994 58         143 pop @rslt;
5995             }
5996              
5997             # Here ends the variable expansion code.
5998              
5999             # If the character is an angle bracket or a pipe, we have a
6000             # redirect specification. This always starts a new token. We
6001             # flag the token as a redirect, stuff all matching
6002             # characters into the mode (throwing an exception if there
6003             # are too many), consume any trailing spaces, and set the
6004             # token value to the empty string to prevent executing this
6005             # code again when we hit the first character of the file
6006             # name. Note that redirect tokens always get tilde
6007             # expansion.
6008              
6009             } elsif ( $char =~ m/ [<>|] /smx ) {
6010 6 100       78 push @rslt, {
    50          
6011             redirect => 1,
6012             type => ($char eq '<' ? '<' : '>'),
6013             mode => ($char eq '|' ? '|-' : $char),
6014             expand => ($char ne '|')
6015             };
6016 6         24 while ($inx < $len) {
6017 11         28 my $next = substr $buffer, $inx++, 1;
6018 11 50       32 $next =~ m/ \s /smx and next;
6019 11 100       25 if ($next eq $char) {
6020 6         19 $rslt[-1]{mode} .= $next;
6021 6 100       26 length $rslt[-1]{mode} > 2
6022             and $self->wail(
6023             "Syntax error near $rslt[-1]{mode}");
6024             } else {
6025 5         10 --$inx;
6026 5         15 $rslt[-1]{token} = '';
6027 5         15 last;
6028             }
6029             }
6030 5 100       19 if ( '<<' eq $rslt[-1]{mode} ) { # Heredoc
6031 4         11 delete $rslt[-1]{redirect};
6032 4         10 delete $rslt[-1]{type};
6033 4         9 delete $rslt[-1]{mode};
6034 4         27 my $quote = '';
6035 4         62 while ( $inx < $len ) {
6036 62         119 my $next = substr $buffer, $inx++, 1;
6037 62 100       141 if ( $next =~ m/ \s /smx ) {
6038 2 50       9 $quote or last;
6039 0         0 $rslt[-1]{token} .= $next;
6040             } else {
6041             '' eq $rslt[-1]{token}
6042             and $next =~ m/ ['"] /smx
6043             and $quote = $next
6044 60 100 100     229 or $rslt[-1]{token} .= $next;
      66        
6045             $quote
6046             and $next eq $quote
6047 60 100 100     231 and $rslt[-1]{token} ne ''
      100        
6048             and last;
6049             }
6050             }
6051 4 100       19 $quote and $rslt[-1]{token} =~ s/ . \z //sxm;
6052 4         14 my $terminator = $rslt[-1]{token};
6053 4         12 my $look_for = $terminator . "\n";
6054 4         9 $rslt[-1]{token} = '';
6055 4         14 $rslt[-1]{expand} = $quote ne q<'>;
6056 4         8 while ( 1 ) {
6057 9         48 my $buffer = $self->_read_continuation( $in,
6058             "Here doc terminator $terminator not found" );
6059 9 100       32 $buffer eq $look_for and last;
6060 5         15 $rslt[-1]{token} .= $buffer;
6061             }
6062 4 100       14 if ( $quote ne q<'> ) {
6063             $rslt[-1]{token} = __tokenize(
6064             $self,
6065             { single => 1, noredirect => 1, in => $in },
6066 3         72 $rslt[-1]{token}, $args
6067             );
6068             }
6069 4         19 push @rslt, {}; # New token
6070             }
6071              
6072             # If the token already exists at this point, the current
6073             # character, whatever it is, is simply appended to it.
6074              
6075             } elsif (exists $rslt[-1]{token} || $relquote) {
6076             # do case modification
6077             # NOTE that the argument list is modified in-place.
6078 3745         8537 $self->_case_mod( $char );
6079 3745         6075 $rslt[-1]{token} .= $char;
6080              
6081             # If the character is a tilde, we flag the token for tilde
6082             # expansion.
6083              
6084             } elsif ($char eq '~') {
6085 12         59 $rslt[-1]{tilde}++;
6086 12         46 $rslt[-1]{token} .= $char;
6087              
6088             # If the character is a hash mark, it means a comment. Bail
6089             # out of the loop.
6090             } elsif ( $char eq '#' ) {
6091 2         7 last;
6092              
6093             # Else we just put it in the token.
6094             } else {
6095 829         2184 $rslt[-1]{token} .= $char;
6096             }
6097              
6098             # If we're at the end of the buffer but we're inside quotes,
6099             # we need to read another line.
6100 6306 100 66     16036 if ( $inx >= $len && ( $absquote || $relquote ) ) {
      100        
6101 2 50       12 $buffer .= $self->_read_continuation( $in,
6102             $absquote ? 'Unclosed single quote' :
6103             'Unclosed double quote'
6104             );
6105 2         8 $len = length $buffer;
6106             }
6107              
6108             }
6109              
6110             # We have run through the entire string to be tokenized. If
6111             # there are unclosed quotes of either sort, we declare an error
6112             # here. This should actually not happen, since we allow
6113             # multi-line quotes, and if we have run out of input we catch it
6114             # above.
6115              
6116 376 50       790 $absquote and $self->wail( 'Unclosed terminal single quote' );
6117 376 50       834 $relquote and $self->wail( 'Unclosed terminal double quote' );
6118              
6119             # Replace leading punctuation with the corresponding method.
6120              
6121             shift @rslt
6122 376   100     2244 while @rslt && ! defined $rslt[0]{token};
6123 376 50 66     4344 if ( defined $rslt[0]{token} and
6124             $rslt[0]{token} =~ s/ \A ( $command_equiv_re ) //smx ) {
6125 0 0       0 if ( $rslt[0]{token} eq '' ) {
    0          
6126 0         0 $rslt[0]{token} = $command_equivalent{$1};
6127             } elsif ( $opt->{single} ) {
6128             $rslt[0]{token} = join ' ', $command_equivalent{$1},
6129 0         0 $rslt[0]{token};
6130             } else {
6131             unshift @rslt, {
6132 0         0 token => $command_equivalent{$1},
6133             };
6134             }
6135             }
6136              
6137             # Go through our prospective tokens, keeping only those that
6138             # were actually defined, and shuffling the redirects off into
6139             # the redirect hash.
6140              
6141 376         1334 my (@tokens, %redir);
6142 376         635 my $expand_tildes = 1;
6143 376 100 100     3810 if ( defined $rslt[0]{token}
6144             and my $kode = $self->can( $rslt[0]{token} ) ) {
6145 252 100       1086 if ( my $hash = $self->__get_attr( $kode, 'Tokenize' ) ) {
6146 2         16 $expand_tildes = $hash->{expand_tilde};
6147             }
6148             }
6149 376         933 foreach (@rslt) {
6150 1318 100       2816 exists $_->{token} or next;
6151 966 100 66     3011 if ($_->{redirect}) {
    100          
6152 1 50       3 if ( $_->{mode} eq '<' ) {
6153             push @tokens, $self->_file_reader(
6154 0         0 $_->{token}, { glob => 1 } );
6155             } else {
6156 1         2 my $type = $_->{type};
6157             $redir{$type} = {
6158             mode => $_->{mode},
6159             name => ($_->{expand} ?
6160             $self->expand_tilde($_->{token}) :
6161 1 50       8 $_->{token}),
6162             };
6163             }
6164             } elsif ( $expand_tildes && $_->{tilde} ) {
6165 12         149 push @tokens, $self->expand_tilde( $_->{token} );
6166             } else {
6167 953         2123 push @tokens, $_->{token};
6168             }
6169             }
6170              
6171             # With the {single} and {noredirect} options both asserted,
6172             # there is only one token, so we return it directly.
6173              
6174 372 50 66     1178 ($opt->{single} && $opt->{noredirect}) and return $tokens[0];
6175              
6176             # With the {noredirect} option asserted, we just return a
6177             # reference to the tokens found.
6178              
6179 341 50       804 $opt->{noredirect} and return \@tokens;
6180              
6181             # Otherwise we return a list, with a reference to the token list
6182             # as the first element, and a reference to the redirect hash as
6183             # the second element.
6184              
6185 341         3747 return (\@tokens, \%redir);
6186             }
6187              
6188             # Retrieve the value of a variable.
6189             sub _tokenize_var {
6190 74     74   263 my ($self, $name, $args, $relquote, $indirect) = @_;
6191              
6192 74 0 33     348 defined $name and $name ne ''
    50          
6193             or return $indirect ? '' : undef;
6194              
6195 74 100       251 $special{$name} and do {
6196 19         119 my $val = $special{$name}->($args, $relquote);
6197 19 50 33     97 return ($indirect && ref $val) ? '' : $val;
6198             };
6199              
6200 55 100       301 $name !~ m/ \D /smx
6201             and return $args->[$name - 1];
6202              
6203 40         143 my $value = $self->_attribute_value( $name );
6204 40 100       155 NULL_REF eq ref $value
6205             or return $value;
6206              
6207             exists $self->{exported}{$name}
6208 34 100       151 and return $self->{exported}{$name};
6209              
6210             defined $ENV{$name}
6211 32 100       148 and return $ENV{$name};
6212              
6213 14         27 foreach my $frame ( reverse @{ $self->{frame} } ) {
  14         58  
6214             defined $frame->{define}{$name}
6215 17 100       69 and return $frame->{define}{$name};
6216             }
6217              
6218 11         36 return;
6219             }
6220              
6221             }
6222              
6223             # Apply case modification to the arguments
6224             # NOTE that the argument list is modified in-place. I'm a little
6225             # surprised that this didn't tickle Perl::Critic.
6226             sub _case_mod {
6227 3757     3757   8333 my $self = shift;
6228 3757         6909 foreach ( @_ ) {
6229             $self->{_case_mod}{case}
6230 3760 100       8117 and $_ = $self->{_case_mod}{case}->( $self, $_ );
6231 3760         4833 my $code;
6232             $code = delete $self->{_case_mod}{single}
6233 3760 100       9764 and $_ = $code->( $self, $_ );
6234             }
6235 3757         5680 return;
6236             }
6237              
6238             # $self->wail(...)
6239             #
6240             # Either die or croak with the arguments, depending on the value
6241             # of the 'warning' attribute. If we die, a trailing period and
6242             # newline are provided if necessary. If we croak, any trailing
6243             # punctuation and newline are stripped.
6244              
6245             sub wail {
6246 19     19 1 66 my ($self, @args) = @_;
6247 19         196 $self->{_warner}->wail( @args );
6248 0         0 return; # We can't hit this, but Perl::Critic does not know that.
6249             }
6250              
6251             # $self->__wail(...)
6252             #
6253             # either wail() or whinge() depending on error_out.
6254             sub __wail {
6255 1     1   4 my ($self, @args) = @_;
6256 1 50       5 if ( $self->get( 'error_out' ) ) {
6257 1         4 $self->{_warner}->wail( @args );
6258             } else {
6259 0         0 $self->{_warner}->whinge( @args );
6260             }
6261 0         0 return;
6262             }
6263              
6264             # $self->weep(...)
6265             #
6266             # Die with a stack dump (Carp::confess).
6267              
6268             sub weep {
6269 0     0 1 0 my ($self, @args) = @_;
6270 0         0 $self->{_warner}->weep( @args );
6271 0         0 return; # We can't hit this, but Perl::Critic does not know that.
6272             }
6273              
6274             # $self->whinge(...)
6275             #
6276             # Either warn or carp with the arguments, depending on the value
6277             # of the 'warn' attribute. If we warn, a trailing period and
6278             # newline are provided if necessary. If we carp, any trailing
6279             # punctuation and newline are stripped.
6280              
6281             sub whinge {
6282 3     3 1 17 my ($self, @args) = @_;
6283 3         31 $self->{_warner}->whinge( @args );
6284 3         14 return;
6285             }
6286              
6287             1;
6288              
6289             __END__