File Coverage

blib/lib/Sys/OsRelease.pm
Criterion Covered Total %
statement 152 169 89.9
branch 52 74 70.2
condition 8 12 66.6
subroutine 30 33 90.9
pod 13 17 76.4
total 255 305 83.6


line stmt bran cond sub pod time code
1             # Sys::OsRelease
2             # ABSTRACT: read operating system details from standard /etc/os-release file
3             # Copyright (c) 2022 by Ian Kluft
4             # Open Source license Perl's Artistic License 2.0:
5             # SPDX-License-Identifier: Artistic-2.0
6              
7             # This module must be maintained for minimal dependencies so it can be used to build systems and containers.
8              
9             ## no critic (Modules::RequireExplicitPackage)
10             # This resolves conflicting Perl::Critic rules which want package and strictures each before the other
11 4     4   11178 use strict;
  4         24  
  4         98  
12 4     4   17 use warnings;
  4         7  
  4         84  
13 4     4   1987 use utf8;
  4         48  
  4         17  
14             ## use critic (Modules::RequireExplicitPackage)
15              
16             package Sys::OsRelease;
17             $Sys::OsRelease::VERSION = '0.3.0';
18 4     4   169 use Config;
  4         8  
  4         163  
19 4     4   20 use Carp qw(carp croak);
  4         7  
  4         1574  
20              
21             # the instance - use Sys::OsRelease->instance() to get it
22             my %_instances = ();
23              
24             # default search path and file name for os-release file
25             my @std_search_path = qw(/etc /usr/lib /run/host);
26             my $std_file_name = "os-release";
27              
28             # defined attributes from FreeDesktop's os-release standard - this needs to be kept up-to-date with the standard
29             my @std_attrs = qw(NAME ID ID_LIKE PRETTY_NAME CPE_NAME VARIANT VARIANT_ID VERSION VERSION_ID VERSION_CODENAME
30             BUILD_ID IMAGE_ID IMAGE_VERSION HOME_URL DOCUMENTATION_URL SUPPORT_URL BUG_REPORT_URL PRIVACY_POLICY_URL
31             LOGO ANSI_COLOR DEFAULT_HOSTNAME SYSEXT_LEVEL);
32              
33             # OS ID strings which are preferred as common if found in ID_LIKE
34             my %common_id = (
35             alpine => 1,
36             arch => 1,
37             fedora => 1,
38             debian => 1,
39             opensuse => 1,
40             );
41              
42             # call destructor when program ends
43             END {
44 4     4   5187 foreach my $class (keys %_instances) {
45 1         3 $class->clear_instance();
46             }
47 4         23 undef %_instances;
48             }
49              
50             #
51             # singleton management methods
52             # These can be imported by another class by using the import_singleton() method. That was done for Sys::OsPackage,
53             # to avoid copying those methods. But other classes with a similar need to minimize module dependencies which already
54             # use Sys::OsRelease can do this too.
55             #
56              
57             # alternative method to initiate initialization without returning a value
58             sub init
59             {
60 0     0 1 0 my ($class, @params) = @_;
61 0         0 $class->instance(@params);
62 0         0 return;
63             }
64              
65             # new method calls instance
66             sub new
67             {
68 0     0 1 0 my ($class, @params) = @_;
69 0         0 return $class->instance(@params);
70             }
71              
72             # singleton class instance
73             sub instance
74             {
75 29     29 1 56178 my ($class, @params) = @_;
76              
77             # initialize if not already done
78 29 100       59 if (not $class->defined_instance()) {
79 11         24 $_instances{$class} = $class->_new_instance(@params);
80             }
81              
82             # return singleton instance
83 29         66 return $_instances{$class};
84             }
85              
86             # test if instance is defined for testing
87             sub defined_instance
88             {
89 42     42 1 697 my $class = shift;
90 42 100 66     216 return ((exists $_instances{$class}) and $_instances{$class}->isa($class)) ? 1 : 0;
91             }
92              
93             # clear instance for exit-cleanup or for re-use in testing
94             sub clear_instance
95             {
96 11     11 1 25588 my $class = shift;
97 11 50       27 if ($class->defined_instance()) {
98             # clean up anything that the destructor will miss, such as auto-generated methods
99 11 50       47 if ($class->can("_cleanup_instance")) {
100 11         24 $class->_cleanup_instance();
101             }
102              
103             # dereferencing will destroy singleton instance
104 11         32 delete $_instances{$class};
105             }
106 11         65 return;
107             }
108              
109             # allow other classes which cooperate with Sys::OsRelease to import our singleton-management methods
110             # This helps maintain minimal prerequisites among modules working to set up Perl on containers or new systems.
111             sub import_singleton
112             {
113 1     1 1 2034 my $class = shift;
114 1         3 my $caller_class = caller;
115              
116             # export singleton-management methods to caller class
117 1         3 foreach my $method_name (qw(init new instance defined_instance clear_instance)) {
118             ## no critic (TestingAndDebugging::ProhibitNoStrict)
119 4     4   34 no strict 'refs';
  4         7  
  4         5619  
120 5         6 *{$caller_class."::".$method_name} = \&{$class."::".$method_name};
  5         46  
  5         13  
121             }
122 1         5 return;
123             }
124              
125             #
126             # os-release data access methods
127             #
128              
129             # access module constants
130 1     1 0 474 sub std_search_path { return @std_search_path; }
131 1     1 0 69 sub std_attrs { return @std_attrs; }
132              
133             # fold case for case-insensitive matching
134             my $can_fc = CORE->can("fc"); # test fc() once and save result
135             sub fold_case
136             {
137 553     553 0 670 my $str = shift;
138              
139             # use fc if available, otherwise lc to support older Perls
140 553 50       2671 return $can_fc ? $can_fc->($str) : lc($str);
141             }
142              
143             # initialize a new instance
144             sub _new_instance
145             {
146 11     11   34 my ($class, @params) = @_;
147              
148             # enforce class lineage - _new_instance() should be overloaded by other classes that import singleton methods
149 11 50       40 if (not $class->isa(__PACKAGE__)) {
150 0 0       0 croak "_new_instance() should be overloaded by calling class: "
151             .(ref $class ? ref $class : $class)." is not a ".__PACKAGE__;
152             }
153              
154             # obtain parameters from array or hashref
155 11         14 my %obj;
156 11 100       23 if (scalar @params > 0) {
157 9 50       22 if (ref $params[0] eq 'HASH') {
158 0         0 $obj{_config} = $params[0];
159             } else {
160 9         24 $obj{_config} = {@params};
161             }
162             }
163              
164             # locate os-release file in standard places
165 11         17 my $osrelease_path;
166 11 100       25 my @search_path = ((exists $obj{_config}{search_path}) ? @{$obj{_config}{search_path}} : @std_search_path);
  9         16  
167 11 100       26 my $file_name = ((exists $obj{_config}{file_name}) ? $obj{_config}{file_name} : $std_file_name);
168 11         20 foreach my $search_dir (@search_path) {
169 10 50       282 if (-r "$search_dir/$file_name") {
170 10         31 $osrelease_path = $search_dir."/".$file_name;
171 10         19 last;
172             }
173             }
174              
175             # If we found os-release on this system, read it
176             # otherwise leave everything empty and platform() method will use Perl's $Config{osname} as a summary value
177 11 100       18 if (defined $osrelease_path) {
178             # save os-release file path
179 10         21 $obj{_config}{osrelease_path} = $osrelease_path;
180              
181             # read os-release file
182             ## no critic (InputOutput::RequireBriefOpen)
183 10 50       349 if (open my $fh, "<", $osrelease_path) {
184 10         1188 while (my $line = <$fh>) {
185 119         179 chomp $line; # remove trailing nl
186 119 50       188 if (substr($line, -1, 1) eq "\r") {
187 0         0 $line = substr($line, 0, -1); # remove trailing cr
188             }
189              
190             # skip comments and blank lines
191 119 50 33     416 if ($line =~ /^ \s+ #/x or $line =~ /^ \s+ $/x) {
192 0         0 next;
193             }
194              
195             # read attribute assignment lines
196 119 100 66     415 if ($line =~ /^ ([A-Z0-9_]+) = "(.*)" $/x
      100        
197             or $line =~ /^ ([A-Z0-9_]+) = '(.*)' $/x
198             or $line =~ /^ ([A-Z0-9_]+) = (.*) $/x)
199             {
200 117 50       212 next if $1 eq "_config"; # don't overwrite _config
201 117         161 $obj{fold_case($1)} = $2;
202             }
203             }
204 10         117 close $fh;
205             }
206             }
207              
208             # bless instance and generate accessor methods
209 11         47 my $obj_ref = bless \%obj, $class;
210 11         36 $obj_ref->_gen_accessors();
211              
212             # instantiate object
213 11         28 return $obj_ref;
214             }
215              
216             # helper function to allow methods to get the instance ref when called via the class name
217             sub class_or_obj
218             {
219 735     735 0 831 my $coo = shift;
220              
221             # return the instance
222 735 100       1249 return ((ref $coo) ? $coo : $coo->instance());
223             }
224              
225             # clean up data in an instance before feeding it to the destructor
226             sub _cleanup_instance
227             {
228 11     11   15 my ($class_or_obj) = @_;
229 11         21 my $self = class_or_obj($class_or_obj);
230              
231             # enforce class lineage - _cleanup_instance() should be overloaded by other classes that import singleton methods
232 11 50       38 if (not $self->isa(__PACKAGE__)) {
233 0         0 croak "_new_instance() should be overloaded by calling class: "
234             .(ef $self)." is not a ".__PACKAGE__;
235             }
236              
237             # clear accessor functions
238 11         15 foreach my $acc (keys %{$self->{_config}{accessor}}) {
  11         72  
239 242         296 $self->_clear_accessor($acc);
240             }
241 11         23 return;
242             }
243              
244             # determine platform type
245             sub platform
246             {
247 4     4 1 1233 my ($class_or_obj) = @_;
248 4         7 my $self = class_or_obj($class_or_obj);
249            
250             # if we haven't already saved this result, compute and save it
251 4 100       7 if (not $self->has_config("platform")) {
252 2 100       6 if ($self->has_attr("id")) {
253 1         3 $self->config("platform", $self->id);
254             }
255 2 100       4 if ($self->has_attr("id_like")) {
256             # check if the configuration has additional common IDs which should be recognized if seen in ID_LIKE
257 1 50       3 if ($self->has_config("common_id")) {
258 0         0 my $cids = $self->config("common_id");
259 0 0       0 my @cids = (ref $cids eq "ARRAY") ? (@{$cids}) : (split /\s+/x, $cids);
  0         0  
260 0         0 foreach my $cid (@cids) {
261 0         0 $common_id{$cid} = 1;
262             }
263             }
264              
265             # check ID_LIKE for more common names which should be used instead of ID
266 1         3 foreach my $like (split /\s+/x, $self->id_like) {
267 1 50       5 if (exists $common_id{$like}) {
268 1         5 $self->config("platform", $like);
269 1         2 last;
270             }
271             }
272             }
273              
274             # if platform is still not set, use Perl's osname config as a summary value
275 2 100       5 if (not $self->has_config("platform")) {
276 1         20 $self->config("platform", $Config{osname});
277             }
278             }
279 4         8 return $self->config("platform");
280             }
281              
282             # get location of the os-release file found on this system
283             # return undef if the file was not found
284             sub osrelease_path
285             {
286 1     1 1 4 my ($class_or_obj) = @_;
287 1         3 my $self = class_or_obj($class_or_obj);
288 1 50       4 if (exists $self->{_config}{osrelease_path}) {
289 1         3 return $self->{_config}{osrelease_path};
290             }
291 0         0 return;
292             }
293              
294             # return list of attributes found in os-release file
295             sub found_attrs
296             {
297 12     12 1 3419 my ($class_or_obj) = @_;
298 12         23 my $self = class_or_obj($class_or_obj);
299 12         45 return grep { $_ ne "_config" } keys %$self;
  129         201  
300             }
301              
302             # attribute existence checker
303             sub has_attr
304             {
305 99     99 1 40131 my ($class_or_obj, $key) = @_;
306 99         180 my $self = class_or_obj($class_or_obj);
307 99 100       176 return ((exists $self->{fold_case($key)}) ? 1 : 0);
308             }
309              
310             # attribute read-only accessor
311             sub get
312             {
313 95     95 1 161 my ($class_or_obj, $key) = @_;
314 95         163 my $self = class_or_obj($class_or_obj);
315 95         163 return $self->{fold_case($key)};
316             }
317              
318             # attribute existence checker
319             sub has_config
320             {
321 11     11 1 10275 my ($class_or_obj, $key) = @_;
322 11         18 my $self = class_or_obj($class_or_obj);
323 11 100       37 return ((exists $self->{_config}{$key}) ? 1 : 0);
324             }
325              
326             # config read/write accessor
327             sub config
328             {
329 7     7 1 25 my ($class_or_obj, $key, $value) = @_;
330 7         10 my $self = class_or_obj($class_or_obj);
331 7 100       14 if (defined $value) {
332 3         6 $self->{_config}{$key} = $value;
333             }
334 7         29 return $self->{_config}{$key};
335             }
336              
337             # generate accessor methods for all defined and standardized attributes
338             # private internal method
339             sub _gen_accessors
340             {
341 11     11   18 my ($class_or_obj) = @_;
342 11         19 my $self = class_or_obj($class_or_obj);
343              
344             # generate accessors for standardized attributes whether or not they were not found in os-release
345             # for attributes which exist, it makes a read-only accessor
346             # for attributes which don't exist, it makes an undef accessor
347 11         22 foreach my $std_attr (@std_attrs) {
348 242 50       350 next if $std_attr eq "_config"; # protect special/reserved attribute
349 242         299 my $fc_attr = fold_case($std_attr);
350 242         334 $self->_gen_accessor($fc_attr);
351             }
352 11         16 return;
353             }
354              
355             # generate accessor
356             # private internal method
357             sub _gen_accessor
358             {
359 242     242   307 my ($class_or_obj, $name) = @_;
360 242         278 my $self = class_or_obj($class_or_obj);
361 242 50       350 my $class = (ref $self) ? (ref $self) : $self;
362 242         335 my $method_name = $class."::".$name;
363              
364             # mark accessor flag in configuration so it can be deleted for cleanup (mainly for testing)
365 242 100       620 if (not exists $self->{_config}{accessor}) {
366 11         20 $self->{_config}{accessor} = {};
367             }
368              
369             # generate accessor as read-only or undef depending whether it exists in the running system
370 242 100       347 if (exists $self->{$name}) {
371             # generate read-only accessor for attribute which was found in os-release
372 103     3   287 $self->{_config}{accessor}{$name} = sub { return $self->{$name} };
  3         10  
373             } else {
374             # generate undef accessor for standard attribute which was not found in os-release
375 139     0   336 $self->{_config}{accessor}{$name} = sub { return; };
  0         0  
376             }
377              
378             ## no critic (TestingAndDebugging::ProhibitNoStrict)
379 4     4   32 no strict 'refs';
  4         5  
  4         522  
380 242         333 *{$method_name} = $self->{_config}{accessor}{$name};
  242         503  
381 242         416 return;
382             }
383              
384             # clean up accessor
385             # private internal method
386             sub _clear_accessor
387             {
388 242     242   302 my ($class_or_obj, $name) = @_;
389 242         289 my $self = class_or_obj($class_or_obj);
390 242 50       344 my $class = (ref $self) ? (ref $self) : $self;
391 242 50       366 if (exists $self->{_config}{accessor}{$name}) {
392 242         320 my $method_name = $class."::".$name;
393             ## no critic (TestingAndDebugging::ProhibitNoStrict)
394 4     4   25 no strict 'refs';
  4         8  
  4         533  
395 242         236 undef *{$method_name};
  242         459  
396 242         698 delete $self->{_config}{accessor}{$name};
397             }
398 242         378 return;
399             }
400              
401             1;
402              
403             =pod
404              
405             =encoding UTF-8
406              
407             =head1 NAME
408              
409             Sys::OsRelease - read operating system details from standard /etc/os-release file
410              
411             =head1 VERSION
412              
413             version 0.3.0
414              
415             =head1 SYNOPSIS
416              
417             non-object-oriented:
418              
419             Sys::OsRelease->init();
420             my $id = Sys::OsRelease->id();
421             my $id_like = Sys::OsRelease->id_like();
422              
423             object-oriented:
424              
425             my $osrelease = Sys::OsRelease->instance();
426             my $id = $osrelease->id();
427             my $id_like = $osrelease->id_like();
428              
429             =head1 DESCRIPTION
430              
431             Sys::OsRelease is a helper library to read the /etc/os-release file, as defined by FreeDesktop.Org.
432             The os-release file is used to define an operating system environment.
433             It has been in widespread use among Linux distributions since 2017 and BSD variants since 2020.
434             It was started on Linux systems which use the systemd software, but then spread to other Linux, BSD and
435             Unix-based systems.
436             Its purpose is to identify the system to any software which needs to know.
437             It differentiates between Unix-based operating systems and even between Linux distributions.
438              
439             Sys::OsRelease is implemented with a singleton model, meaning there is only one instance of the class.
440             Instead of instantiating an object with new(), the instance() class method returns the one and only instance.
441             The first time it's called, it instantiates it.
442             On following calls, it returns a reference to the singleton instance.
443              
444             This module maintains minimal prerequisites, and only those which are usually included with Perl.
445             (Suggestions of new features and code will have to follow this rule.)
446             That is intended to be acceptable for establishing system or container environments which contain Perl programs.
447             It can also be used for installing or configuring software that needs to know about the system environment.
448              
449             =head2 The os-release Standard
450              
451             FreeDesktop.Org's os-release standard is at L.
452              
453             Current attributes recognized by Sys::OsRelease are:
454             NAME ID ID_LIKE PRETTY_NAME CPE_NAME VARIANT VARIANT_ID VERSION VERSION_ID VERSION_CODENAME BUILD_ID IMAGE_ID
455             IMAGE_VERSION HOME_URL DOCUMENTATION_URL SUPPORT_URL BUG_REPORT_URL PRIVACY_POLICY_URL LOGO ANSI_COLOR
456             DEFAULT_HOSTNAME SYSEXT_LEVEL
457              
458             If other attributes are found in the os-release file, they will be accepted.
459             Folded to lower case, the attribute names are used as keys in an internal hash structure.
460              
461             =head1 METHODS
462              
463             =head2 Class methods
464              
465             I uses a singleton model. So there is only one instance.
466             Class methods manage the singleton instance, or import those methods to another cooperating class' namespace.
467              
468             Class methods must be called using the class name, like Cinstance()> .
469              
470             =over 1
471              
472             =item init([key => value, ...])
473              
474             initializes the singleton instance without returning a value.
475             Parameters are passed to the instance() method.
476             This method is for cases where method calls will be via the class name, and the program
477             doesn't need a reference to the instance.
478              
479             Under normal circumstances no parameters are needed. See instance() for possible parameters.
480              
481             =item new([key => value, ...])
482              
483             initializes the singleton instance and returns a reference to it.
484             Parameters are passed to the instance() method.
485             This is equivalent to using the instance() method, made available if new() sounds more comfortable.
486              
487             Under normal circumstances no parameters are needed. See instance() for possible parameters.
488              
489             =item instance([key => value, ...])
490              
491             initializes the singleton instance and returns a reference to it.
492              
493             Under normal circumstances no parameters are needed. Possible optional parameters are as follows:
494              
495             =over 1
496              
497             =item common_id
498              
499             supplies an arrayref to use as a list of additional common strings which should be recognized by the platform()
500             method, if they occur in the ID_LIKE attribute in the os-release file. By default, "debian" and "fedora" are
501             regonized by platform() as common names and it will return them instead of the system's ID attribute.
502              
503             =item search_path
504              
505             supplies an arrayref of strings with directories to use as the search path for the os-release file.
506              
507             =item file_name
508              
509             supplies a string with the basename of the file to look for the os-release file.
510             Obviously the default file name is "os-release".
511             Under normal circumstances there is no need to set this.
512             Currently this is only used for testing, where suffixes are added for copies of various different systems'
513             os-release files, to indicate which system they came from.
514              
515             =back
516              
517             =item defined_instance()
518              
519             returns true if the singleton instance is defined, false if it is not yet defined or has been cleared.
520              
521             =item clear_instance()
522              
523             removes the singleton instance of the class if it was defined.
524             Under normal circumstances it is not necessary to call this since the class destructor will call it automatically.
525             It is currently only used for testing, where it is necessary to clear the instance before loading a new one with
526             different parameters.
527              
528             Since this class is based on the singleton model, there is only one instance.
529             The instance(), new() and init() methods will only initialize the instance if it is not already initialized.
530              
531             =item import_singleton()
532              
533             The singleton-management methods I, I, I, I and I
534             can be imported by another class by using the import_singleton() method.
535             That was done for L, to allow it to avoid copying those methods.
536             But other classes with a similar need to minimize module dependencies which already
537             use I can do this too.
538             This helps maintain minimal prerequisites among modules working to set up Perl on containers or new systems.
539              
540             =back
541              
542             =head2 Auto-generated Accessor Methods
543              
544             For convenience, I generates read-only accessor methods for each of the standard
545             attribute names, converted to lower case. For example, from the list above they are I, I,
546             I, etc. The auto-generated methods do not require any parameters, and ignore any if provided.
547              
548             Accessor methods are not generated for non-standard atttributes because it would be unreliable to try to
549             call methods named for transient data that may or may not exist on a given platform, and for the possibility
550             they could conflict with existing functions in the I namespace. Use the I,
551             I and I methods to detect and access non-standard attributes.
552              
553             =head2 Instance methods
554              
555             Object methods, including auto-generated accessors described above, access the data from the singleton instance,
556             either read from an os-release file or empty to indicate no os-release file was found on the system.
557              
558             Instance methods may be called either via the class name or a reference to the singleton instance.
559             Each of these functions can determine whether they were called as a class or object method, and obtain the
560             reference to the singleton instance if needed.
561              
562             =over 1
563              
564             =item platform()
565              
566             returns a string with the platform type. On systems with /etc/os-release (or os-release in any location
567             from the standard) this is usually from the ID field.
568             On systems that use the ID_LIKE field, systems that claim to be like "debian" or "fedora" (always in lower case)
569             will return those names for the platform.
570              
571             The list of recognized common platforms can be modified by passing a "common_id" parameter to instance()/new()
572             with an arrayref containing additional names to recognize as common. For example, "centos" is another possibility.
573             It was not included in the default because CentOS is discontinued. Both Rocky Linux and Alma Linux have
574             ID_LIKE fields of "rhel centos fedora", which will match "fedora" with the default setting, but could be configured
575             via "common_id" to recognize "centos" since it's listed first in ID_LIKE.
576              
577             On systems where an os-release file doesn't exist or isn't found, the platform string will fall back to Perl's
578             $Config{osname} setting for the system.
579              
580             =item osrelease_path()
581              
582             returns the path where os-release was found.
583              
584             The default search path is /etc, /usr/lib and /run/host as defined by the standard.
585             The search path can be replaced by providing a "search_path" parameter to instance()/new() with an arrayref
586             containing the directories to search. This feature is currently only used for testing purposes.
587              
588             =item found_attrs()
589              
590             returns a list of attribute names found in the os-release file, empty if os-release doesn't exist on the platform.
591              
592             =item has_attr(name)
593              
594             returns a boolean which is true if the attribute named by the string parameter exists in the os-release data for the
595             current system.
596             The attribute name is case insensitive.
597              
598             =item get(name)
599              
600             is a read-only accessor which returns the value of the os-release attribute named by the string parameter,
601             or undef if it doesn't exist.
602              
603             =item has_config(name)
604              
605             returns a boolean which is true if Sys::OsRelease contains a configuration setting named by the string parameter.
606              
607             =item config(name, [value])
608              
609             is a read/write accessor for the configuration setting named by the string parameter "name".
610             If no value parameter is provided, it returns the value of the parameter, or undef if it doesn't exist.
611             If a value parameter is provided, it assigns that to the configuration setting and returns the same value.
612              
613             =back
614              
615             =head1 SEE ALSO
616              
617             FreeDesktop.Org's os-release standard: L
618              
619             GitHub repository for Sys::OsRelease: L
620              
621             Related modules:
622              
623             =over 1
624              
625             =item L
626              
627             installs Perl modules, for example as dependencies of a script, via OS packages if available or otherwise via CPAN -
628             uses Sys::OsRelease to determine OS type
629              
630             =item L
631              
632             system information collected from multiple sources including system architecture, hardware, OS release data
633              
634             =back
635              
636             =head1 BUGS AND LIMITATIONS
637              
638             Please report bugs via GitHub at L
639              
640             Patches and enhancements may be submitted via a pull request at L
641              
642             =head1 LICENSE INFORMATION
643              
644             Copyright (c) 2022 by Ian Kluft
645              
646             This module is distributed in the hope that it will be useful, but it is provided “as is” and without any express or implied warranties. For details, see the full text of the license in the file LICENSE or at L.
647              
648             =head1 AUTHOR
649              
650             Ian Kluft
651              
652             =head1 COPYRIGHT AND LICENSE
653              
654             This software is Copyright (c) 2022 by Ian Kluft.
655              
656             This is free software, licensed under:
657              
658             The Artistic License 2.0 (GPL Compatible)
659              
660             =cut
661              
662             __END__