File Coverage

blib/lib/Slackware/SBoKeeper.pm
Criterion Covered Total %
statement 309 433 71.3
branch 73 180 40.5
condition 22 41 53.6
subroutine 40 46 86.9
pod 23 31 74.1
total 467 731 63.8


line stmt bran cond sub pod time code
1             package Slackware::SBoKeeper;
2 2     2   339282 use 5.016;
  2         8  
3             our $VERSION = '2.06';
4 2     2   11 use strict;
  2         3  
  2         84  
5 2     2   10 use warnings;
  2         11  
  2         174  
6              
7 2     2   14 use File::Basename;
  2         10  
  2         248  
8 2     2   1193 use File::Copy;
  2         9432  
  2         231  
9 2     2   17 use File::Path qw(make_path);
  2         4  
  2         135  
10 2     2   11 use File::Spec;
  2         4  
  2         38  
11 2     2   1742 use Getopt::Long;
  2         27531  
  2         15  
12 2     2   406 use List::Util qw(uniq);
  2         4  
  2         172  
13              
14 2     2   1310 use Slackware::SBoKeeper::Config qw(read_config);
  2         6  
  2         162  
15 2     2   1211 use Slackware::SBoKeeper::Database;
  2         22  
  2         104  
16 2     2   1267 use Slackware::SBoKeeper::Home;
  2         7  
  2         260  
17 2     2   1070 use Slackware::SBoKeeper::System;
  2         6  
  2         12929  
18              
19             my $PRGNAM = 'sbokeeper';
20             my $PRGVER = $VERSION;
21              
22             my $HELP_MSG = <
23             $PRGNAM $PRGVER
24             Usage: $0 [options] command [args]
25              
26             Commands:
27             add Add pkgs + dependencies.
28             tack Add pkgs (no dependencies).
29             addish Add pkgs + dependencies, do not mark as manually added.
30             tackish Add pkgs, do not mark as manually added.
31             rm Remove pkg(s).
32             clean Remove unnecessary pkgs.
33             deps Print dependencies for pkg.
34             rdeps Print reverse dependencies for pkg.
35             depadd Add deps to pkg's dependencies.
36             deprm Remove deps from pkg's dependencies.
37             pull Find and add installed SlackBuilds.org pkgs.
38             diff Show discrepancies between installed pkgs and database.
39             depwant Show missing dependencies for pkgs.
40             depextra Show extraneous dependencies for pkgs.
41             unmanual Unset pkg(s) as manually added.
42             print Print all pkgs in specified categories.
43             tree Print dependency tree.
44             rtree Print reverse dependency tree.
45             dump Dump database.
46             help Print cmd help message.
47              
48             Options:
49             -B --blacklist= Blacklist string/file of packages
50             -c --config= Specify config file location.
51             -d --datafile= Specify data file location.
52             -s --sbodir= Specify SBo directory.
53             -t --tag= Specify SlackBuild package tag.
54             -y --yes Automatically agree to all prompts.
55             -h --help Print help message and exit.
56             -v --version Print version + copyright info and exit.
57             END
58              
59             my $VERSION_MSG = <
60             $PRGNAM $PRGVER
61              
62             Copyright (C) 2024-2025 Samuel Young
63              
64             This program is free software; you can redistribute it and/or modify it under
65             the terms of either: the GNU General Public License as published by the Free
66             Software Foundation; or the Artistic License.
67              
68             See for more information.
69             END
70              
71             # TODO: Is there a way I can have all of these command help blurbs without this
72             # long list of HERE docs?
73             my %COMMAND_HELP = (
74             'add' => <
75             Usage: add ...
76              
77             Add one or more packages to package database, marking them as manually added.
78             Dependencies will automatically be added as well. If a specified package is
79             already present in the database but is not marked as manually added, sbokeeper
80             will mark it as manually added.
81             END
82             'tack' => <
83             Usage: tack ...
84              
85             Add one or more packages to package database. Does not pull in dependencies.
86             Besides that, same behavior as add.
87             END
88             'addish' => <
89             Usage: addish ...
90              
91             Same thing as add, but added packages are not marked as manually added.
92             END
93             'tackish' => <
94             Usage: tackish ...
95              
96             Same thing as tack, but added packages are not marked as manually added.
97             END
98             'rm' => <
99             Usage: rm ...
100              
101             Removes one or more packages from package database. Dependencies are not
102             automatically removed.
103             END
104             'clean' => <
105             Usage: clean
106              
107             Removes unnecessary packages from package database. This command is the same
108             as running 'sbokeeper rm \@unnecessary'.
109             END
110             'deps' => <
111             Usage: deps
112              
113             Prints list of dependencies for specified package, according to the database.
114             Does not print complete dependency tree, for that one should use the tree
115             command.
116             END
117             'rdeps' => <
118             Usage: rdeps
119              
120             Prints list of reverse dependencies for specified package (packages that depend
121             on pkg), according to the database. Does not print complete reverse dependency
122             tree, for that one should use the rtree command.
123             END
124             'depadd' => <
125             Usage: depadd ...
126              
127             Add one or more deps to pkg's dependencies. Dependencies that are not present in
128             the database will automatically be added.
129              
130             ** IMPORTANT**
131             Be cautious when using this command. This command provides an easy way for you
132             to screw up your package database by introducing circular dependencies which
133             sbokeeper cannot handle. When using this command, be sure you are not accidently
134             introducing circular dependencies!
135             END
136             'deprm' => <
137             Usage: deprm ...
138              
139             Remove one or more deps from pkg's dependencies.
140             END
141             'pull' => <
142             Usage: pull
143              
144             Find any SlackBuilds.org packages that are installed on your system but not
145             present in your package database and attempt to add them to it. All packages
146             added are marked as manually added. Packages that are already present are
147             skipped.
148             END
149             'diff' => <
150             Usage: diff
151              
152             Prints a list of SlackBuild packages that are present on your system but not in
153             your database and vice versa.
154             END
155             'depwant' => <
156             Usage: depwant
157              
158             Prints a list of packages that, according to the SlackBuild repo, are missing
159             dependencies and prints a list of their dependencies.
160             END
161             'depextra' => <
162             Usage: depextra
163              
164             Prints a list of packages with extra dependencies and said extra dependencies.
165             Extra dependencies are dependencies listed in the package database that are not
166             present in the SlackBuild repo.
167             END
168             'unmanual' => <
169             Usage: unmanual ...
170              
171             Unset one or more packages as being manually installed, but do not remove them
172             from database.
173             END
174             'print' => <
175             Usage: print [ ...]
176              
177             Prints a unique list of packages in the specified categories. The following are
178             valid categories:
179              
180             all All added packages
181             manual Packages added manually
182             nonmanual Packages added not manually
183             necessary Packages added manually, or dependency of a manual package
184             unnecessary Packages not manually added and not depended on by another
185             missing Missing dependencies
186             untracked Installed SlackBuild packages not present in database
187             phony Packages in database that are not installed on system
188              
189             If no category is specified, defaults to 'all'.
190             END
191             'tree' => <
192             Usage: tree [] ...
193              
194             Prints a dependency tree. If pkgs is not specified, prints a dependency tree
195             for each manually added package. If pkgs are given, prints a dependency tree of
196             each package specified.
197             END
198             'rtree' => <
199             Usage: rtree ...
200              
201             Prints a reverse dependency tree for each package specified.
202             END
203             'dump' => <
204             Usage: dump
205              
206             Dumps contents of data file to stdout.
207             END
208             'help' => <
209             Usage: help
210              
211             Print help message for cmd.
212             END
213             );
214              
215             my @CONFIG_PATHS = (
216             "$HOME/.config/sbokeeper.conf",
217             "$HOME/.sbokeeper.conf",
218             "/etc/sbokeeper.conf",
219             "/etc/sbokeeper/sbokeeper.conf",
220             );
221              
222             my $SLACKWARE_VERSION = Slackware::SBoKeeper::System->version();
223              
224             my $DEFAULT_DATADIR = $> == 0
225             ? "/var/lib/$PRGNAM"
226             : "$HOME/.local/share/$PRGNAM";
227              
228             my $OLD_ROOT_DATA = "/root/.local/share/$PRGNAM/data.$PRGNAM";
229              
230             # Hash of commands and some info about them
231             # Method: Reference to the method to call.
232             # NeedDatabase: Does a database need to already be present?
233             # NeedSlack: Does the command only work on Slackware systems?
234             # NeedWrite: Does the command require write permissions to the data file?
235             # Args: Minimum number of args needed.
236             my %COMMANDS = (
237             'add' => {
238             Method => \&add,
239             NeedDatabase => 0,
240             NeedSlack => 0,
241             NeedWrite => 1,
242             Args => 1,
243             },
244             'tack' => {
245             Method => \&tack,
246             NeedDatabase => 0,
247             NeedSlack => 0,
248             NeedWrite => 1,
249             Args => 1,
250             },
251             'addish' => {
252             Method => \&addish,
253             NeedDatabase => 0,
254             NeedSlack => 0,
255             NeedWrite => 1,
256             Args => 1,
257             },
258             'tackish' => {
259             Method => \&tackish,
260             NeedDatabase => 0,
261             NeedSlack => 0,
262             NeedWrite => 1,
263             Args => 1,
264             },
265             'rm' => {
266             Method => \&rm,
267             NeedDatabase => 1,
268             NeedSlack => 0,
269             NeedWrite => 1,
270             Args => 1,
271             },
272             'clean' => {
273             Method => \&clean,
274             NeedDatabase => 1,
275             NeedSlack => 0,
276             NeedWrite => 1,
277             Args => 0,
278             },
279             'rdeps' => {
280             Method => \&rdeps,
281             NeedDatabase => 1,
282             NeedSlack => 0,
283             NeedWrite => 0,
284             Args => 1,
285             },
286             'deps' => {
287             Method => \&deps,
288             NeedDatabase => 1,
289             NeedSlack => 0,
290             NeedWrite => 0,
291             Args => 1,
292             },
293             'depadd' => {
294             Method => \&depadd,
295             NeedDatabase => 1,
296             NeedSlack => 0,
297             NeedWrite => 1,
298             Args => 2,
299             },
300             'deprm' => {
301             Method => \&deprm,
302             NeedDatabase => 1,
303             NeedSlack => 0,
304             NeedWrite => 1,
305             Args => 2,
306             },
307             'pull' => {
308             Method => \&pull,
309             NeedDatabase => 0,
310             NeedSlack => 1,
311             NeedWrite => 1,
312             Args => 0,
313             },
314             'diff' => {
315             Method => \&diff,
316             NeedDatabase => 1,
317             NeedSlack => 1,
318             NeedWrite => 0,
319             Args => 0,
320             },
321             'depwant' => {
322             Method => \&depwant,
323             NeedDatabase => 1,
324             NeedSlack => 0,
325             NeedWrite => 0,
326             Args => 0,
327             },
328             'depextra' => {
329             Method => \&depextra,
330             NeedDatabase => 1,
331             NeedSlack => 0,
332             NeedWrite => 0,
333             Args => 0,
334             },
335             'unmanual' => {
336             Method => \&unmanual,
337             NeedDatabase => 1,
338             NeedSlack => 0,
339             NeedWrite => 1,
340             Args => 1,
341             },
342             'print' => {
343             Method => \&sbokeeper_print,
344             NeedDatabase => 1,
345             NeedSlack => 0,
346             NeedWrite => 0,
347             Args => 0,
348             },
349             'tree' => {
350             Method => \&tree,
351             NeedDatabase => 1,
352             NeedSlack => 0,
353             NeedWrite => 0,
354             Args => 0,
355             },
356             'rtree' => {
357             Method => \&rtree,
358             NeedDatabase => 1,
359             NeedSlack => 0,
360             NeedWrite => 0,
361             Args => 1,
362             },
363             'dump' => {
364             Method => \&dump,
365             NeedDatabase => 1,
366             NeedSlack => 0,
367             NeedWrite => 0,
368             Args => 0,
369             },
370             'help' => {
371             Method => \&help,
372             NeedDatabase => 0,
373             NeedSlack => 0,
374             NeedWrite => 0,
375             Args => 0,
376             },
377             );
378              
379             my $CONFIG_READERS = {
380             'DataFile' => sub {
381              
382             my $val = shift;
383             my $param = shift;
384              
385             $val =~ s/^~/$HOME/;
386              
387             unless (File::Spec->file_name_is_absolute($val)) {
388             $val = File::Spec->catfile(dirname($param->{File}), $val);
389             }
390              
391             return $val;
392              
393             },
394             'SBoPath' => sub {
395              
396             my $val = shift;
397             my $param = shift;
398              
399             $val =~ s/^~/$HOME/;
400              
401             unless (File::Spec->file_name_is_absolute($val)) {
402             $val = File::Spec->catfile(dirname($param->{File}), $val);
403             }
404              
405             unless (-d $val) {
406             die "$val is not a directory or does not exist\n";
407             }
408              
409             return $val;
410              
411             },
412             'Tag' => sub {
413              
414             return shift;
415              
416             },
417             'Blacklist' => sub {
418              
419             my $val = shift;
420             my $param = shift;
421              
422             $val =~ s/^~/$HOME/;
423              
424             my %blacklist;
425              
426             my $blfile = File::Spec->file_name_is_absolute($val)
427             ? $val
428             : File::Spec->catfile(dirname($param->{File}), $val);
429              
430             if (-f $blfile) {
431             %blacklist = read_blacklist($blfile);
432             # SlackBuild packages cannot contain a slash character, so the user
433             # probably means for $val to be a blacklist file, but the blacklist file
434             # does not exist.
435             } elsif ($val =~ /\//) {
436             die "$val does not look like a blacklist file or list\n";
437             } else {
438             %blacklist = map { $_ => 1 } split /\s/, $val;
439             }
440              
441             return \%blacklist;
442              
443             },
444             };
445              
446             my %PKG_CATEGORIES = (
447             'all' => sub {
448              
449             my $sbok = shift;
450              
451             return $sbok->packages;
452              
453             },
454             'manual' => sub {
455              
456             my $sbok = shift;
457              
458             return grep { $sbok->is_manual($_) } $sbok->packages;
459              
460             },
461             'nonmanual' => sub {
462              
463             my $sbok = shift;
464              
465             return grep { !$sbok->is_manual($_) } $sbok->packages;
466              
467             },
468             'necessary' => sub {
469              
470             my $sbok = shift;
471              
472             return grep { $sbok->is_necessary($_) } $sbok->packages;
473              
474             },
475             'unnecessary' => sub {
476              
477             my $sbok = shift;
478              
479             return grep { !$sbok->is_necessary($_) } $sbok->packages;
480              
481             },
482             'missing' => sub {
483              
484             my $sbok = shift;
485              
486             my %missing = $sbok->missing;
487              
488             return uniq sort map { @{$missing{$_}} } keys %missing;
489              
490             },
491             'untracked' => sub {
492              
493             my $sbok = shift;
494              
495             return
496             grep { !$sbok->has($_) }
497             Slackware::SBoKeeper::System->packages_by_tag('_SBo')
498             ;
499              
500             },
501             'phony' => sub {
502              
503             my $sbok = shift;
504              
505             return
506             grep { !Slackware::SBoKeeper::System->installed($_) }
507             $sbok->packages
508             ;
509              
510             },
511             );
512              
513             sub read_blacklist {
514              
515 3     3 0 9 my $file = shift;
516              
517 3 50       160 open my $fh, '<', $file
518             or die "Failed to open $file for reading: $!\n";
519              
520 3         12 my %blacklist;
521              
522 3         105 while (my $l = readline $fh) {
523              
524 30         59 chomp $l;
525              
526 30 100 100     156 if ($l =~ /^#/ or $l =~ /^\s*$/) {
527 6         24 next;
528             }
529              
530 24         250 $l =~ s/^\s*|\s*$//g;
531              
532 24 50       68 if ($l =~ /\s/) {
533 0         0 die "Blacklist entry cannot contain whitespace\n";
534             }
535              
536 24         181 $blacklist{$l} = 1;
537              
538             }
539              
540 3         65 close $fh;
541              
542 3         146 return %blacklist;
543              
544             }
545              
546             sub get_default_sbopath {
547              
548 0 0   0 0 0 unless (Slackware::SBoKeeper::System->is_slackware()) {
549 0         0 return undef;
550             }
551              
552             # Default repo locations for popular SlackBuild package managers. This sub
553             # finds a list of default repos that are present on the system and then
554             # returns the one that was last modified (based on the repo's ChangeLog).
555 0         0 my %sbopaths = (
556             'sbopkg' => "/var/lib/sbopkg/SBo/$SLACKWARE_VERSION",
557             'sbotools' => "/usr/sbo/repo",
558             'sbotools2' => "/usr/sbo/repo",
559             'sbpkg' => "/var/lib/sbpkg/SBo/$SLACKWARE_VERSION",
560             'slpkg' => "/var/lib/slpkg/repos/sbo",
561             'slackrepo' => "/var/lib/slackrepo/SBo/slackbuilds",
562             'sboui' => "/var/lib/sboui/repo",
563             );
564              
565 0         0 my @potential;
566              
567 0         0 foreach my $m (sort keys %sbopaths) {
568              
569 0 0       0 unless (Slackware::SBoKeeper::System->installed($m)) {
570 0         0 next;
571             }
572              
573 0 0       0 next unless -d $sbopaths{$m};
574              
575 0         0 push @potential, $sbopaths{$m};
576              
577             }
578              
579             # Pick the directory with the ChangeLog with the latest mod time.
580             @potential =
581 0         0 map { $_->[0] }
582 0         0 sort { $b->[1] <=> $a->[1] }
583 0 0       0 map { [ $_, -f "$_/ChangeLog.txt" ? (stat "$_/ChangeLog.txt")[9] : 0 ] }
  0         0  
584             @potential;
585              
586 0 0       0 return @potential ? $potential[0] : undef;
587              
588             }
589              
590             sub yesno {
591              
592 0     0 0 0 my $prompt = shift;
593              
594 0         0 while (1) {
595              
596 0         0 print "$prompt [y/N] ";
597              
598 0         0 my $l = readline(STDIN);
599 0         0 chomp $l;
600              
601 0 0 0     0 if (fc $l eq fc 'y') {
    0          
602 0         0 return 1;
603             # If no input is given, assume 'no'.
604             } elsif (fc $l eq fc 'n' or $l eq '') {
605 0         0 return 0;
606             } else {
607 0         0 print "Invalid input '$l'\n"
608             }
609              
610             }
611              
612             }
613              
614             # Expand aliases to package lists. Also gets rid of redundant packages and sorts
615             # returned list.
616             sub alias_expand {
617              
618 16     16 0 33 my $sbokeeper = shift;
619 16         53 my $args = shift;
620              
621 16         33 my @rtrn;
622             my @alias;
623              
624 16         30 foreach my $a (@{$args}) {
  16         41  
625 47 100       121 if ($a =~ /^@/) {
626 2         8 push @alias, $a;
627             } else {
628 45         102 push @rtrn, $a;
629             }
630             }
631              
632 16         36 foreach my $a (@alias) {
633             # Get rid of '@'
634 2         7 $a = substr $a, 1;
635              
636 2 50       10 unless (defined $PKG_CATEGORIES{$a}) {
637 0         0 die "'$a' is not a valid package category\n";
638             }
639              
640 2         9 push @rtrn, $PKG_CATEGORIES{$a}($sbokeeper);
641             }
642              
643 16         212 return uniq sort @rtrn;
644              
645             }
646              
647             sub backup {
648              
649 14     14 0 33 my $file = shift;
650              
651 14 100       287 if (-r $file) {
652 8 50       65 copy($file, "$file.bak")
653             or die "Failed to copy $file to $file.bak: $!\n";
654             }
655              
656             }
657              
658             sub print_package_list {
659              
660 35     35 0 89 my $pref = shift;
661 35         169 my @list = @_;
662              
663 35 50       130 @list = ('(none)') unless @list;
664              
665 35         93 foreach my $p (@list) {
666 215         1642 print "$pref$p\n";
667             }
668              
669             }
670              
671             sub package_branch {
672              
673 29     29 0 46 my $sbokeeper = shift;
674 29         51 my $pkg = shift;
675 29   100     69 my $level = shift // 0;
676              
677 29         69 my $has = $sbokeeper->has($pkg);
678              
679             # Add '(missing)' if package is not present in database but depended on by
680             # another package.
681 29 50       222 printf "%s%s%s\n", ' ' x $level, $pkg, $has ? '' : ' (missing?)';
682              
683 29 50       85 return unless $has;
684              
685 29         72 foreach my $d ($sbokeeper->immediate_dependencies($pkg)) {
686 25         91 package_branch($sbokeeper, $d, $level + 1);
687             }
688              
689             }
690              
691             sub rpackage_branch {
692              
693 5     5 0 8 my $sbokeeper = shift;
694 5         10 my $pkg = shift;
695 5   100     25 my $level = shift // 0;
696              
697 5         88 printf "%s%s\n", ' ' x $level, $pkg;
698              
699 5         23 foreach my $rd ($sbokeeper->reverse_dependencies($pkg)) {
700 1         10 rpackage_branch($sbokeeper, $rd, $level + 1);
701             }
702              
703             }
704              
705             sub add {
706              
707 3     3 1 10 my $self = shift;
708              
709             my $sbokeeper = Slackware::SBoKeeper::Database->new(
710             -s $self->{DataFile} ? $self->{DataFile} : '',
711             $self->{SBoPath},
712             $self->{Blacklist}
713 3 50       89 );
714              
715 3         21 my @pkgs = alias_expand($sbokeeper, $self->{Args});
716              
717 3         18 my @add = $sbokeeper->add(\@pkgs, 1);
718              
719 3         106 printf "The following packages will be added:\n";
720 3         20 print_package_list(' ', @add);
721 3         20 printf "The following packages will be marked as manually added:\n";
722 3         11 print_package_list(' ', grep { $sbokeeper->is_manual($_) } @pkgs);
  6         25  
723 3 50       15 my $ok = $self->{YesAll} ? 1 : yesno("Is this okay?");
724              
725 3 50       12 unless ($ok) {
726 0         0 print "No packages added\n";
727 0         0 return;
728             }
729              
730 3         16 backup($self->{DataFile});
731 3         19 $sbokeeper->write($self->{DataFile});
732              
733 3         88 printf "%d packages added\n", scalar @add;
734              
735             }
736              
737             sub tack {
738              
739 3     3 1 7 my $self = shift;
740              
741             my $sbokeeper = Slackware::SBoKeeper::Database->new(
742             -s $self->{DataFile} ? $self->{DataFile} : '',
743             $self->{SBoPath},
744             $self->{Blacklist}
745 3 50       56 );
746              
747 3         13 my @pkgs = alias_expand($sbokeeper, $self->{Args});
748              
749 3         16 my @add = $sbokeeper->tack(\@pkgs, 1);
750              
751 3         105 printf "The following packages will be tacked:\n";
752 3         18 print_package_list(' ', @add);
753 3 50       13 my $ok = $self->{YesAll} ? 1 : yesno("Is this okay?");
754              
755 3 50       25 unless ($ok) {
756 0         0 print "No packages added\n";
757 0         0 return;
758             }
759              
760 3         13 backup($self->{DataFile});
761 3         356 $sbokeeper->write($self->{DataFile});
762              
763 3         56 printf "%d packages tacked\n", scalar @add;
764              
765             }
766              
767             sub addish {
768              
769 1     1 1 5 my $self = shift;
770              
771             my $sbokeeper = Slackware::SBoKeeper::Database->new(
772             -s $self->{DataFile} ? $self->{DataFile} : '',
773             $self->{SBoPath},
774             $self->{Blacklist}
775 1 50       21 );
776              
777 1         4 my @pkgs = alias_expand($sbokeeper, $self->{Args});
778              
779 1         7 my @add = $sbokeeper->add(\@pkgs, 0);
780              
781 1 50       7 unless (@add) {
782 0         0 die "No packages could be added\n";
783             }
784              
785 1         32 printf "The following packages will be added:\n";
786 1         10 print_package_list(' ', @add);
787 1 50       6 my $ok = $self->{YesAll} ? 1 : yesno("Is this okay?");
788              
789 1 50       4 unless ($ok) {
790 0         0 print "No packages added\n";
791 0         0 return;
792             }
793              
794 1         5 backup($self->{DataFile});
795 1         481 $sbokeeper->write($self->{DataFile});
796              
797 1         33 printf "%d packages added\n", scalar @add;
798              
799             }
800              
801             sub tackish {
802              
803 1     1 1 3 my $self = shift;
804              
805             my $sbokeeper = Slackware::SBoKeeper::Database->new(
806             -s $self->{DataFile} ? $self->{DataFile} : '',
807             $self->{SBoPath},
808             $self->{Blacklist}
809 1 50       21 );
810              
811 1         5 my @pkgs = alias_expand($sbokeeper, $self->{Args});
812              
813 1         6 my @add = $sbokeeper->tack(\@pkgs, 0);
814              
815 1 50       35 unless (@add) {
816 0         0 die "No packages could be added\n";
817             }
818              
819 1         27 printf "The following packages will be tacked:\n";
820 1         6 print_package_list(' ', @add);
821 1 50       9 my $ok = $self->{YesAll} ? 1 : yesno("Is this okay?");
822              
823 1 50       4 unless ($ok) {
824 0         0 print "No packages added\n";
825 0         0 return;
826             }
827              
828 1         4 backup($self->{DataFile});
829 1         6 $sbokeeper->write($self->{DataFile});
830              
831 1         18 printf "%d packages tacked\n", scalar @add;
832              
833             }
834              
835             sub rm {
836              
837 3     3 1 8 my $self = shift;
838              
839             my $sbokeeper = Slackware::SBoKeeper::Database->new(
840             $self->{DataFile},
841             $self->{SBoPath},
842             $self->{Blacklist}
843 3         69 );
844              
845 3         15 my @pkgs = alias_expand($sbokeeper, $self->{Args});
846              
847 3         14 my @rm = $sbokeeper->remove(\@pkgs);
848              
849 3 50       11 unless (@rm) {
850 0         0 die "No packages could be removed\n";
851             }
852              
853 3         94 printf "The following packages will be removed:\n";
854 3         17 print_package_list(' ', @rm);
855 3 50       12 my $ok = $self->{YesAll} ? 1 : yesno("Is this okay?");
856              
857 3 50       10 unless ($ok) {
858 0         0 print "No packages removed\n";
859 0         0 return;
860             }
861              
862 3         11 backup($self->{DataFile});
863 3         1407 $sbokeeper->write($self->{DataFile});
864              
865 3         83 printf "%d packages removed\n", scalar @rm;
866              
867             }
868              
869             sub clean {
870              
871 1     1 1 3 my $self = shift;
872              
873 1         4 $self->{Args} = ['@unnecessary'];
874              
875 1         5 $self->rm;
876              
877             }
878              
879             sub deps {
880              
881 1     1 1 4 my $self = shift;
882              
883             my $sbokeeper = Slackware::SBoKeeper::Database->new(
884             $self->{DataFile},
885             $self->{SBoPath},
886             $self->{Blacklist}
887 1         12 );
888              
889 1         2 my $pkg = shift @{$self->{Args}};
  1         4  
890              
891 1 50       6 unless ($sbokeeper->has($pkg)) {
892 0         0 die "$pkg not present in database\n";
893             }
894              
895 1         6 my @deps = $sbokeeper->immediate_dependencies($pkg);
896              
897 1 50       46 print @deps ? "@deps\n" : "No dependencies found\n";
898              
899             }
900              
901             sub rdeps {
902              
903 1     1 1 4 my $self = shift;
904              
905             my $sbokeeper = Slackware::SBoKeeper::Database->new(
906             $self->{DataFile},
907             $self->{SBoPath},
908             $self->{Blacklist}
909 1         7 );
910              
911 1         4 my $pkg = shift @{$self->{Args}};
  1         4  
912              
913 1 50       5 unless ($sbokeeper->has($pkg)) {
914 0         0 die "$pkg not present in database\n";
915             }
916              
917 1         5 my @rdeps = $sbokeeper->reverse_dependencies($pkg);
918              
919 1 50       49 print @rdeps ? "@rdeps\n" : "No reverse dependencies found\n";
920              
921             }
922              
923             sub depadd {
924              
925 1     1 1 4 my $self = shift;
926              
927             my $sbokeeper = Slackware::SBoKeeper::Database->new(
928             $self->{DataFile},
929             $self->{SBoPath},
930             $self->{Blacklist}
931 1         11 );
932              
933 1         4 my $pkg = shift @{$self->{Args}};
  1         3  
934 1         5 my @deps = alias_expand($sbokeeper, $self->{Args});
935              
936 1         8 my @add = $sbokeeper->add(\@deps, 0);
937 1         9 my @depadd = $sbokeeper->depadd($pkg, \@deps);
938              
939 1 0 33     21 if (!@add and !@depadd) {
940 0         0 die "No dependencies could be added to $pkg\n";
941             }
942              
943 1         29 printf "The following packages will be added to your database:\n";
944 1         7 print_package_list(' ', @add);
945 1         8 printf "The following dependencies will be added to %s:\n", $pkg;
946 1         5 print_package_list(' ', @depadd);
947 1 50       6 my $ok = $self->{YesAll} ? 1 : yesno("Is this okay?");
948              
949 1 50       3 unless ($ok) {
950 0         0 print "No packages changed\n";
951 0         0 return;
952             }
953              
954 1         19 backup($self->{DataFile});
955 1         527 $sbokeeper->write($self->{DataFile});
956              
957 1         15 printf "%d packages added\n", scalar @add;
958 1         26 printf "%d dependencies added to %s\n", scalar @depadd, $pkg;
959              
960             }
961              
962             sub deprm {
963              
964 1     1 1 4 my $self = shift;
965              
966             my $sbokeeper = Slackware::SBoKeeper::Database->new(
967             $self->{DataFile},
968             $self->{SBoPath},
969             $self->{Blacklist}
970 1         174 );
971              
972 1         3 my $pkg = shift @{$self->{Args}};
  1         6  
973 1         5 my @deps = alias_expand($sbokeeper, $self->{Args});
974              
975 1         8 my @rm = $sbokeeper->depremove($pkg, \@deps);
976              
977 1 50       6 unless (@rm) {
978 0         0 die "No dependencies could be removed from $pkg\n";
979             }
980              
981 1         24 printf "The following dependencies will be removed from %s\n", $pkg;
982 1         6 print_package_list(' ', @rm);
983 1 50       32 my $ok = $self->{YesAll} ? 1 : yesno("Is this okay?");
984              
985 1 50       4 unless ($ok) {
986 0         0 print "No packages changed\n";
987 0         0 return;
988             }
989              
990 1         5 backup($self->{DataFile});
991 1         397 $sbokeeper->write($self->{DataFile});
992              
993 1         65 printf "%d dependencies removed from %s\n", scalar @rm, $pkg;
994              
995             }
996              
997             sub pull {
998              
999 0     0 1 0 my $self = shift;
1000              
1001             my $sbokeeper = Slackware::SBoKeeper::Database->new(
1002             -s $self->{DataFile} ? $self->{DataFile} : '',
1003             $self->{SBoPath},
1004             $self->{Blacklist}
1005 0 0       0 );
1006              
1007 0         0 my @installed = Slackware::SBoKeeper::System->packages_by_tag($self->{Tag});
1008              
1009 0         0 my @pull;
1010              
1011 0         0 foreach my $i (@installed) {
1012              
1013 0 0       0 unless ($sbokeeper->exists($i)) {
1014 0         0 warn "Could not find $i in SlackBuild repo, skipping\n";
1015 0         0 next;
1016             }
1017              
1018 0 0       0 next if $sbokeeper->has($i);
1019              
1020 0         0 push @pull, $i;
1021              
1022             }
1023              
1024 0         0 my @add = $sbokeeper->add(\@pull, 1);
1025              
1026 0 0       0 unless (@add) {
1027 0         0 print "No packages need to be added, doing nothing\n";
1028 0         0 return;
1029             }
1030              
1031 0         0 printf "The following packages will be added:\n";
1032 0         0 print_package_list(' ', @add);
1033 0 0       0 my $ok = $self->{YesAll} ? 1 : yesno("Is this okay?");
1034              
1035 0 0       0 unless ($ok) {
1036 0         0 print "No packages added\n";
1037 0         0 return;
1038             }
1039              
1040 0         0 backup($self->{DataFile});
1041 0         0 $sbokeeper->write($self->{DataFile});
1042              
1043 0         0 printf "%d packages added\n", scalar @add;
1044              
1045             }
1046              
1047             sub diff {
1048              
1049 0     0 1 0 my $self = shift;
1050              
1051             my $sbokeeper = Slackware::SBoKeeper::Database->new(
1052             $self->{DataFile},
1053             $self->{SBoPath},
1054             $self->{Blacklist}
1055 0         0 );
1056              
1057             my %installed =
1058 0         0 map { $_ => 1 }
1059             Slackware::SBoKeeper::System->packages_by_tag($self->{Tag})
1060 0         0 ;
1061              
1062 0         0 my %added = map { $_ => 1 } $sbokeeper->packages;
  0         0  
1063              
1064 0         0 my (@idiff, @adiff);
1065              
1066 0         0 foreach my $i (keys %installed) {
1067 0 0       0 push @idiff, $i unless defined $added{$i};
1068             }
1069              
1070 0         0 foreach my $a (keys %added) {
1071 0 0       0 push @adiff, $a unless defined $installed{$a};
1072             }
1073              
1074 0 0 0     0 if (!@idiff && !@adiff) {
1075 0         0 print "No package differences found\n";
1076 0         0 return;
1077             }
1078              
1079             # Tell the user if the packages that differ are actually in the repo or
1080             # not.
1081             @idiff =
1082 0 0       0 map { $sbokeeper->exists($_) ? $_ : "$_ (does not exist in repo)" }
  0         0  
1083             sort @idiff
1084             ;
1085             # This shouldn't happen, but we'll check for consistency's sake.
1086             @adiff =
1087 0 0       0 map { $sbokeeper->exists($_) ? $_ : "$_ (does not exist in repo)" }
  0         0  
1088             sort @adiff
1089             ;
1090              
1091 0 0       0 if (@idiff) {
1092 0         0 printf "Packages found installed on system that are not present in database:\n";
1093 0         0 print_package_list(' ', @idiff);
1094 0 0       0 printf "\n" if @adiff;
1095             }
1096              
1097 0 0       0 if (@adiff) {
1098 0         0 printf "Packages found in database that are not installed on system:\n";
1099 0         0 print_package_list(' ', @adiff);
1100             }
1101              
1102             }
1103              
1104             sub depwant {
1105              
1106 1     1 1 3 my $self = shift;
1107              
1108             my $sbokeeper = Slackware::SBoKeeper::Database->new(
1109             $self->{DataFile},
1110             $self->{SBoPath},
1111             $self->{Blacklist}
1112 1         10 );
1113              
1114 1         7 my %missing = $sbokeeper->missing();
1115              
1116 1 50       6 unless (%missing) {
1117 0         0 print "There no dependencies missing from your database\n";
1118 0         0 return;
1119             }
1120              
1121 1         4 foreach my $p (sort keys %missing) {
1122 1         24 printf "%s:\n", $p;
1123 1         5 print_package_list(' ', @{$missing{$p}});
  1         5  
1124 1         17 print "\n";
1125             }
1126              
1127             }
1128              
1129             sub depextra {
1130              
1131 1     1 1 4 my $self = shift;
1132              
1133             my $sbokeeper = Slackware::SBoKeeper::Database->new(
1134             $self->{DataFile},
1135             $self->{SBoPath},
1136             $self->{Blacklist}
1137 1         11 );
1138              
1139 1         7 my %extra = $sbokeeper->extradeps();
1140              
1141 1 50       6 unless (%extra) {
1142 0         0 print "No packages have extraneous dependencies in your database\n";
1143 0         0 return;
1144             }
1145              
1146 1         4 foreach my $p (sort keys %extra) {
1147 1         25 printf "%s:\n", $p;
1148 1         3 print_package_list(' ', @{$extra{$p}});
  1         6  
1149 1         18 print "\n";
1150             }
1151              
1152             }
1153              
1154             sub unmanual {
1155              
1156 1     1 1 5 my $self = shift;
1157              
1158             my $sbokeeper = Slackware::SBoKeeper::Database->new(
1159             $self->{DataFile},
1160             $self->{SBoPath},
1161             $self->{Blacklist}
1162 1         11 );
1163              
1164 1         6 my @pkgs = alias_expand($sbokeeper, $self->{Args});
1165              
1166 1         3 foreach my $p (@pkgs) {
1167 4 50       13 die "$p is not present in database\n" unless $sbokeeper->has($p);
1168             }
1169              
1170 1         31 printf "The following packages will have their manually added flag unset\n";
1171 1         6 print_package_list(' ', @pkgs);
1172 1 50       22 my $ok = $self->{YesAll} ? 1 : yesno("Is this okay?");
1173              
1174 1 50       6 unless ($ok) {
1175 0         0 print "No packages changed\n";
1176 0         0 return;
1177             }
1178              
1179 1         2 my $n = 0;
1180 1         3 foreach my $p (@pkgs) {
1181 4 50       12 next unless $sbokeeper->is_manual($p);
1182 4         13 $sbokeeper->unmanual($p);
1183 4         8 $n++;
1184             }
1185              
1186 1         5 backup($self->{DataFile});
1187 1         422 $sbokeeper->write($self->{DataFile});
1188              
1189 1         18 print "$n packages updated\n";
1190              
1191             }
1192              
1193             sub sbokeeper_print {
1194              
1195 17     17 1 36 my $self = shift;
1196              
1197 17         36 my @cat = @{$self->{Args}};
  17         71  
1198              
1199 17 50       54 @cat = ('all') unless @cat;
1200              
1201             my $sbokeeper = Slackware::SBoKeeper::Database->new(
1202             $self->{DataFile},
1203             $self->{SBoPath},
1204             $self->{Blacklist}
1205 17         172 );
1206              
1207 17         52 my @pkgs;
1208              
1209 17         48 foreach my $c (@cat) {
1210              
1211             # Get rid of alias '@' if present
1212 24         82 $c =~ s/^@//;
1213              
1214 24 50       106 unless (defined $PKG_CATEGORIES{$c}) {
1215 0         0 die "'$c' is not a valid package category\n";
1216             }
1217              
1218 24         140 push @pkgs, $PKG_CATEGORIES{$c}($sbokeeper);
1219              
1220             }
1221              
1222 17         309 @pkgs = uniq sort @pkgs;
1223              
1224 17 100       134 print_package_list('', @pkgs) if @pkgs;
1225              
1226             }
1227              
1228             sub tree {
1229              
1230 1     1 1 4 my $self = shift;
1231              
1232             my $sbokeeper = Slackware::SBoKeeper::Database->new(
1233             $self->{DataFile},
1234             $self->{SBoPath},
1235             $self->{Blacklist}
1236 1         12 );
1237              
1238 1         8 my @pkgs = @{$self->{Args}}
1239             ? alias_expand($sbokeeper, $self->{Args})
1240 1 50       4 : grep { $sbokeeper->is_manual($_) } $sbokeeper->packages;
  0         0  
1241              
1242 1         3 foreach my $p (@pkgs) {
1243 4 50       12 unless ($sbokeeper->has($p)) {
1244 0         0 die "$p is not present in package database\n";
1245             }
1246             }
1247              
1248 1         3 foreach my $p (@pkgs) {
1249 4         13 package_branch($sbokeeper, $p);
1250 4         32 print "\n";
1251             }
1252              
1253             }
1254              
1255             sub rtree {
1256              
1257 1     1 1 3 my $self = shift;
1258              
1259             my $sbokeeper = Slackware::SBoKeeper::Database->new(
1260             $self->{DataFile},
1261             $self->{SBoPath},
1262             $self->{Blacklist}
1263 1         8 );
1264              
1265 1         6 my @pkgs = alias_expand($sbokeeper, $self->{Args});
1266              
1267 1         3 foreach my $p (@pkgs) {
1268 4 50       12 die "$p is not present in package database\n"
1269             unless $sbokeeper->has($p);
1270             }
1271              
1272 1         5 foreach my $p (@pkgs) {
1273 4         16 rpackage_branch($sbokeeper, $p);
1274 4         57 print "\n";
1275             }
1276              
1277             }
1278              
1279             sub dump {
1280              
1281 1     1 1 3 my $self = shift;
1282              
1283             my $sbokeeper = Slackware::SBoKeeper::Database->new(
1284             $self->{DataFile},
1285             $self->{SBoPath},
1286             $self->{Blacklist}
1287 1         8 );
1288              
1289 1         34 print $sbokeeper->write;
1290              
1291             }
1292              
1293             sub help {
1294              
1295 40     40 1 74 my $self = shift;
1296              
1297             # If no argument was given, just print help message and exit.
1298 40 50       92 unless (@{$self->{Args}}) {
  40         110  
1299 0         0 print $HELP_MSG;
1300 0         0 exit 0;
1301             }
1302              
1303 40         66 my $help = lc shift @{$self->{Args}};
  40         119  
1304              
1305 40 50       137 unless (defined $COMMAND_HELP{$help}) {
1306 0         0 die "$help is not a command\n";
1307             }
1308              
1309 40         3722 print $COMMAND_HELP{$help};
1310              
1311             }
1312              
1313             sub init {
1314              
1315 81     81 1 197517 my $class = shift;
1316              
1317 81         772 my $self = {
1318             Blacklist => 0,
1319             ConfigFile => '',
1320             DataFile => '',
1321             SBoPath => '',
1322             Tag => '',
1323             YesAll => 0,
1324             Command => '',
1325             Args => [],
1326             };
1327              
1328 81         207 my $blacklist = undef;
1329              
1330 81         465 Getopt::Long::config('bundling');
1331             GetOptions(
1332             'blacklist|B=s' => \$blacklist,
1333             'config|c=s' => \$self->{ConfigFile},
1334             'datafile|d=s' => \$self->{DataFile},
1335             'sbodir|s=s' => \$self->{SBoPath},
1336             'tag|t=s' => \$self->{Tag},
1337             'yes|y' => \$self->{YesAll},
1338 0     0   0 'help|h' => sub { print $HELP_MSG; exit 0 },
  0         0  
1339 0     0   0 'version|v' => sub { print $VERSION_MSG; exit 0 },
  0         0  
1340 81 50       4438 ) or die "Error in command line arguments\n";
1341              
1342 81 50       116186 unless (@ARGV) {
1343 0         0 die $HELP_MSG;
1344             }
1345              
1346 81 50 66     676 if (!$self->{ConfigFile} and defined $ENV{SBOKEEPER_CONFIG}) {
1347 0         0 $self->{ConfigFile} = $ENV{SBOKEEPER_CONFIG};
1348             }
1349              
1350 81 100       295 unless ($self->{ConfigFile}) {
1351 78         277 ($self->{ConfigFile}) = grep { -r } @CONFIG_PATHS;
  312         4343  
1352             }
1353              
1354 81 100       339 if ($self->{ConfigFile}) {
1355 3         60 my $config = read_config($self->{ConfigFile}, $CONFIG_READERS);
1356 3         9 foreach my $cf (keys %{$config}) {
  3         13  
1357 10   33     64 $self->{$cf} ||= $config->{$cf};
1358             }
1359             }
1360              
1361 81         302 $self->{Command} = lc shift @ARGV;
1362              
1363 81         324 $self->{Args} = [@ARGV];
1364              
1365 81 100       296 if (defined $blacklist) {
1366              
1367 2 100       116 if (-f $blacklist) {
1368 1         8 $self->{Blacklist} = { read_blacklist($blacklist) };
1369             } else {
1370 1         24 $self->{Blacklist} = { map { $_ => 1 } split /\s/, $blacklist };
  25         62  
1371             }
1372              
1373             }
1374              
1375 81   100     607 $self->{Blacklist} ||= {};
1376              
1377 81 50       251 unless ($self->{DataFile}) {
1378             # If the old default root data file exists, use it but warn the user
1379             # that they should consider moving it to the new default location.
1380 0 0 0     0 if ($> == 0 and -f $OLD_ROOT_DATA) {
1381 0         0 warn "Using $OLD_ROOT_DATA data file, which was the default " .
1382             "root data file path prior to $PRGNAM 2.05. You should " .
1383             "consider moving the data file to the new default path " .
1384             "$DEFAULT_DATADIR/data.$PRGNAM and deleting the old one.\n";
1385 0         0 $self->{DataFile} = $OLD_ROOT_DATA;
1386             } else {
1387 0 0       0 make_path($DEFAULT_DATADIR) unless -d $DEFAULT_DATADIR;
1388 0         0 $self->{DataFile} = "$DEFAULT_DATADIR/data.$PRGNAM";
1389             }
1390             }
1391              
1392 81 50       229 unless ($self->{SBoPath}) {
1393             $self->{SBoPath} = get_default_sbopath($self->{PkgtoolLogs})
1394 0 0       0 or die "Cannot determine default path for SBo repo, please use " .
1395             "the 'SBoPath' config option or '-s' CLI option\n";
1396             }
1397              
1398 81 50       1986 unless (-d $self->{SBoPath}) {
1399 0         0 die "SlackBuild repo directory $self->{SBoPath} does not exit or " .
1400             "is not a directory\n";
1401             }
1402              
1403 81   100     328 $self->{Tag} ||= '_SBo';
1404              
1405 81         544 return bless $self, $class;
1406              
1407             }
1408              
1409             sub run {
1410              
1411 78     78 1 1297 my $self = shift;
1412              
1413 78 50       338 unless (defined $COMMANDS{$self->{Command}}) {
1414 0         0 die "$self->{Command} is not a valid command\n";
1415             }
1416              
1417 78 50 66     772 if (
1418             $COMMANDS{$self->{Command}}->{NeedDatabase} and
1419             not -s $self->{DataFile}
1420             ) {
1421 0         0 die "'$self->{Command}' requires an already-existing database\n";
1422             }
1423              
1424 78 50 33     286 if (
1425             $COMMANDS{$self->{Command}}->{NeedSlack} and
1426             not Slackware::SBoKeeper::System->is_slackware()
1427             ) {
1428 0         0 die "'$self->{Command}' can only be used in Slackware systems\n";
1429             }
1430              
1431 78 50 66     753 if (
      66        
1432             $COMMANDS{$self->{Command}}->{NeedWrite} and
1433             (-e $self->{DataFile} and ! -w $self->{DataFile})
1434             ) {
1435 0         0 die "'$self->{Command}' requires a writable database, $self->{DataFile} is not writable\n";
1436             }
1437              
1438 78 50       156 if (+@{$self->{Args}} < $COMMANDS{$self->{Command}}->{Args}) {
  78         297  
1439 0         0 die $COMMAND_HELP{$self->{Command}};
1440             }
1441              
1442 78         371 $COMMANDS{$self->{Command}}->{Method}($self);
1443              
1444 78         610 1;
1445              
1446             }
1447              
1448             sub get {
1449              
1450 15     15 1 706 my $self = shift;
1451 15         44 my $get = shift;
1452              
1453 15         22779 return $self->{$get};
1454              
1455             }
1456              
1457             1;
1458              
1459             =head1 NAME
1460              
1461             Slackware::SBoKeeper - SlackBuild package manager helper
1462              
1463             =head1 SYNOPSIS
1464              
1465             use Slackware::SBoKeeper;
1466              
1467             my $sbokeeper = Slackware::SBoKeeper->init();
1468             $sbokeeper->run();
1469              
1470             =head1 DESCRIPTION
1471              
1472             Slackware::SBoKeeper is the workhorse module behind L. It should not
1473             be used by any other script/program other than L. If you are looking
1474             for L user documentation, please consult its manual.
1475              
1476             =head1 SUBROUTINES/METHODS
1477              
1478             =over 4
1479              
1480             =item init()
1481              
1482             Reads C<@ARGV> and returns a blessed Slackware::SBoKeeper object. For the list
1483             of options that are available to C, please consult the L
1484             manual.
1485              
1486             =item run()
1487              
1488             Runs L.
1489              
1490             =item get($get)
1491              
1492             Get the value of attribute C<$get>. The following are valid attributes:
1493              
1494             =over 4
1495              
1496             =item Blacklist
1497              
1498             Hash ref of blacklisted packages.
1499              
1500             =item ConfigFile
1501              
1502             Path to config file.
1503              
1504             =item DataFile
1505              
1506             Path to database file.
1507              
1508             =item SBoPath
1509              
1510             Path to local SlackBuild repo.
1511              
1512             =item Tag
1513              
1514             Package tag that the SlackBuild repo uses.
1515              
1516             =item YesAll
1517              
1518             Boolean determining whether to automatically accept any given prompts or not.
1519              
1520             =item Command
1521              
1522             The command that was supplied to L.
1523              
1524             =item Args
1525              
1526             Array ref of arguments given to command.
1527              
1528             =back
1529              
1530             =back
1531              
1532             The following methods correspond to L commands. Consult its manual
1533             for information on their functionality.
1534              
1535             =over 4
1536              
1537             =item add()
1538              
1539             =item tack()
1540              
1541             =item addish()
1542              
1543             =item tackish()
1544              
1545             =item rm()
1546              
1547             =item clean()
1548              
1549             =item deps()
1550              
1551             =item rdeps()
1552              
1553             =item depadd()
1554              
1555             =item deprm()
1556              
1557             =item pull()
1558              
1559             =item diff()
1560              
1561             =item depwant()
1562              
1563             =item depextra()
1564              
1565             =item unmanual()
1566              
1567             =item sbokeeper_print()
1568              
1569             =item tree()
1570              
1571             =item rtree()
1572              
1573             =item dump()
1574              
1575             =item help()
1576              
1577             =back
1578              
1579             =head1 AUTHOR
1580              
1581             Written by Samuel Young, L.
1582              
1583             =head1 BUGS
1584              
1585             Report bugs on my Codeberg, Ehttps://codeberg.org/1-1samE.
1586              
1587             =head1 COPYRIGHT
1588              
1589             Copyright (C) 2024-2025 Samuel Young
1590              
1591             This program is free software; you can redistribute it and/or modify it under
1592             the terms of either: the GNU General Public License as published by the Free
1593             Software Foundation; or the Artistic License.
1594              
1595             =head1 SEE ALSO
1596              
1597             L
1598              
1599             =cut