———————————# -----------------------------------------------------------------------------
=encoding utf8
=head1 NAME
Quiq::DestinationTree - Verwalte Zielbaum eines Datei-Generators
=head1 BASE CLASS
L<Quiq::Object>
=head1 SYNOPSIS
use Quiq::DestinationTree;
$dt = Quiq::DestinationTree->(@dirs);
...
# Dateien und Verzeichnisse erzeugen
$dt->addFile($file,$content);
$dt->addDir($dir);
...
# Überzählige Dateien und Verzeichnisse entfernen
$dt->cleanup;
=head1 DESCRIPTION
Die Klasse verwaltet die Dateien und Unterverzeichnisse von
einem oder mehreren Zielverzeichnissen. Sie ist für Dateigeneratoren
gedacht, die den Inhalt ihrer Zielverzeichnisse komplett
kontrollieren.
=cut
# -----------------------------------------------------------------------------
package
Quiq::DestinationTree;
use
v5.10;
use
strict;
use
warnings;
use
utf8;
our
$VERSION
=
'1.225'
;
use
Quiq::Option;
use
Quiq::DirHandle;
use
Quiq::Path;
use
Quiq::Terminal;
# -----------------------------------------------------------------------------
=head1 METHODS
=head2 Konstruktor
=head3 new() - Konstruktor
=head4 Synopsis
$tree = $class->new(@dirs,@opt);
=head4 Options
=over 4
=item -cleanup => $bool (Default: 0)
Entferne alle Dateien und Verzeichnisse, die generiert wurden.
Diese Option ist für ein "make clean" nützlich. Im Unterschied
zu einem $tree->cleanup() direkt nach Objektinstantiierung,
werden hierbei Dateien, die mit -writeInitially=>1 erzeugt wurden,
nur dann gelöscht, wenn sie nicht modifiziert wurden. Wurden
sie modifiziert, bleiben sie erhalten.
=item -dryRun => $bool (Default: 0)
Ändere nichts, zeige die Operationen nur an.
=item -exclude => $regex (Default: keiner)
Schließe alle Dateien, die Regex $regex erfüllen von der Betrachtung
aus. D.h. diese werden vom Nutzer der Klasse nicht verwaltet (weder
erzeugt noch entfernt).
=item -files => \@files (Default: [])
Liste von Einzeldateien. Beispiel:
-files=>[glob '*_1.sql'],
=item -force => $bool (Default: 0)
Forciere das Schreiben aller Dateien, auch wenn ihr Inhalt nicht
differiert.
=item -include => $regex (Default: keiner)
Berücksichtige nur Dateien, die Regex $regex erfüllen, alle anderen
werden von der Klasse nicht verwaltet (weder erzeugt noch entfernt).
=item -inHandle => $fh (Default: \*STDIN)
Filehandle, von der Benutzereingaben gelesen werden.
=item -language => 'de'|'en' (Default: 'de')
Sprache für die Kommunikation mit dem Benutzer.
=item -prefix => $str (Default: '')
Setze Zeichenkette $str an den Anfang jeder Änderungsmeldung.
Beispiel: C<< -prefix=>'* ' >>
=item -quiet => $bool (Default: 0)
Schreibe keine Meldungen nach STDERR.
=item -outHandle => $fh (Default: \*STDOUT)
Filehandle, auf die Ausgaben geschrieben werden.
=back
=head4 Description
Instantiiere ein Dateibaumobjekt über den Verzeichnissen @dirs
und liefere eine Referenz auf dieses Objekt zurück. Die Verzeichnisse
in @dirs müssen nicht existieren. Hat ein Verzeichnis die Form
"DIR/*" wird nicht rekursiv in DIR abgestiegen, sondern nur
die Dateien (nicht die Verzeichnisse) in DIR werden verwaltet.
=cut
# -----------------------------------------------------------------------------
sub
new {
my
$class
=
shift
;
# @_: @dirs,@opt
my
$cleanup
= 0;
my
$dryRun
= 0;
my
$exclude
=
undef
;
my
$files
= [];
my
$force
= 0;
my
$include
=
undef
;
my
$inHandle
= \
*STDIN
;
my
$language
=
'de'
;
my
$outHandle
= \
*STDOUT
;
my
$prefix
=
''
;
my
$quiet
= 0;
Quiq::Option->extract(
-mode
=>
'sloppy'
,\
@_
,
-cleanup
=> \
$cleanup
,
-dryRun
=> \
$dryRun
,
-exclude
=> \
$exclude
,
-files
=> \
$files
,
-force
=> \
$force
,
-include
=> \
$include
,
-inHandle
=> \
$inHandle
,
-language
=> \
$language
,
-outHandle
=> \
$outHandle
,
-prefix
=> \
$prefix
,
-quiet
=> \
$quiet
,
);
my
%path
;
for
my
$dir
(
@_
) {
next
if
defined
$dir
&&
$dir
ne
''
&& !-e
$dir
;
if
((
my
$dir
) =
$dir
=~ m|(.*)/\*$|) {
# Füge nur die Dateien aus dem Verzeichnis hinzu, steige
# nicht in Subverzeichnisse ab
my
$dh
= Quiq::DirHandle->new(
$dir
);
while
(
my
$path
=
$dh
->
next
) {
$path
=
"$dir/$path"
;
next
if
!-f
$path
;
# wir verwalten nur die Dateien
next
if
$exclude
&&
$path
=~ /
$exclude
/;
next
if
$include
&&
$path
!~ /
$include
/;
$path
{
$path
} = 1;
}
$dh
->
close
;
next
;
}
# Rekursiver Abstieg
for
(Quiq::Path->find(
$dir
,
-exclude
=>
$exclude
,
)) {
s|^\./||;
# ./ am Pfadanfang entfernen
next
if
$_
eq
''
;
# next if $exclude && /$exclude/; # hier ignorieren wir Dateien
# füge Verzeichnisse immer hinzu, wenn sie nicht excluded wurden
if
(-d) {
$path
{
$_
} = 1;
next
;
}
next
if
$include
&& !/
$include
/;
$path
{
$_
} = 1;
}
}
# Einzeldateien
@path
{
@$files
} = (1) x
@$files
;
return
bless
[
\
%path
,
# [0]
$force
,
# [1]
$quiet
,
# [2]
$cleanup
,
# [3]
$inHandle
,
# [4]
$outHandle
,
# [5]
$language
,
# [6]
$prefix
,
# [7]
$dryRun
,
# [8]
],
$class
;
}
# -----------------------------------------------------------------------------
=head2 Objektmethoden
=head3 addDir() - Füge Verzeichnis zum Dateibaum hinzu
=head4 Synopsis
$bool = $tree->addDir($dir);
=head4 Description
Füge Verzeichnis $dir zum Dateibaum hinzu. Die Methode liefert 1, wenn
das Verzeichnis effektiv erzeugt wurde (s.u.) oder 0, wenn dies nicht
der Fall war.
Das Verzeichnis wird effektiv nur erzeugt, wenn es noch nicht
existiert. In dem Fall schreibt die Methode eine entsprechende Meldung
auf den Ausgabekanal und liefert als Returnwert 1.
Der Rückgabewert kann benutzt werden, um im Falle der
Verzeichniserzeugung weitere Aktionen auszuführen, wie z.B. das Setzen
von Verzeichnisrechten.
=cut
# -----------------------------------------------------------------------------
sub
addDir {
my
$self
=
shift
;
my
$dir
=
shift
;
# Nulloperation, wenn Konstruktor-Option -cleanup
return
0
if
$self
->[3];
# Nulloperation, wenn kein Pfad angegeben
return
if
!
$dir
;
# ./ am Pfadanfang entfernen
$dir
=~ s|^\./||;
# Eintrag entfernen
delete
$self
->[0]{
$dir
};
# Vaterverzeichnis hinzufügen, falls vorhanden
my
(
$parent
) =
$dir
=~ m|(.*)/|;
$self
->addDir(
$parent
)
if
$parent
;
# Verzeichnis erzeugen, wenn es nicht existiert
if
(!-e
$dir
) {
if
(!
$self
->[8]) {
Quiq::Path->
mkdir
(
$dir
);
}
if
(!
$self
->[2]) {
my
$out
=
$self
->[5];
my
$msg
=
$self
->getText(
'Verzeichnis erzeugt'
);
$out
"$self->[7]$dir -- $msg\n"
;
}
return
1;
}
return
0;
}
# -----------------------------------------------------------------------------
=head3 paths() - Liefere die Liste der Pfade
=head4 Synopsis
@paths | $pathA = $tree->paths;
=head4 Description
Liefere die Liste der Pfade, die zu den Zielverzeichnissen gehören.
=over 2
=item *
Die Reihenfolge ist undefiniert.
=item *
Die Liste enthält sowohl Verzeichnisse also auch Dateien.
=back
Sortierung herstellen:
@paths = sort $tree->paths;
Nur Dateien erhalten:
@paths = grep {!-d} $tree->paths;
=cut
# -----------------------------------------------------------------------------
sub
paths {
my
$self
=
shift
;
my
@paths
=
keys
%{
$self
->[0]};
return
wantarray
?
@paths
: \
@paths
;
}
# -----------------------------------------------------------------------------
=head3 addFile() - Füge Datei zum Dateibaum hinzu
=head4 Synopsis
$bool = $tree->addFile($file,$data,@opt);
$bool = $tree->addFile($file,\$data,@opt);
$bool = $tree->addFile($file,@opt);
$bool = $tree->addFile($file,
-generate => $bool,
-onGenerate => sub {
...
return $data;
},
@opt
);
=head4 Options
=over 4
=item -encoding => $encoding (Default: undef)
Encodiere/decodiere die Daten mit Encoding $encoding.
=item -force => $bool (Default: 0)
Forciere das Schreiben der Datei ohne den Dateiinhalt zu vergleichen.
Die Option ist nützlich, wenn der Aufrufer bereits festgestellt hat,
dass eine Differenz besteht.
=item -generate => $bool (Default: 0)
Rufe die onGenerate-Subroutine auf, um den Dateiinhalt zu generieren.
=item -onGenerate => sub { ... } (Default: undef)
Die Subroutine generiert und liefert den Inhalt der Datei. Sie wird
aufgerufen, wenn Argument $data nicht definiert ist und die Datei
nicht existiert oder Option -generate I<wahr> ist.
=item -onUpdate => sub { ... } (Default: undef)
Führe Subroutine aus, I<bevor> Datei $file geschrieben wird.
=item -quiet => $bool (Default: 0)
Unterdrücke Ausgabe der Meldung.
=item -skipEmptyFiles => $bool
Übergehe Dateien mit leerem Inhalt.
=item -writeInitially => $bool (Default: 0)
Schreibe Datei nur, wenn sie nicht existiert.
Diese Option ist nützlich, wenn von einer Datei ein anfängliches
Muster erzeugt werden soll, das anschließend manuell bearbeitet werden
kann. Die manuell bearbeitete Datei soll danach natürlich nicht mehr
vom Muster überschieben werden.
=back
=head4 Description
Füge Datei $file mit dem Inhalt $data zum Dateibaum hinzu. Die
Methode liefert 0, wenn die Datei nicht geschrieben wurde,
1, wenn die Datei existiert hat und geschrieben wurde, 2, wenn
die Datei neu erzeugt wurde.
Ist $data C<undef> wird die Datei nicht geschrieben, bleibt
aber weiter bestehen. Dies ist nützlich, wenn es teuer ist, den Inhalt
der Datei zu generieren, und bekannt ist, dass sich am Inhalt nichts
geändert hat.
Die Datei wird effektiv geschrieben, wenn sie nicht existiert oder der
Inhalt differiert. In dem Fall schreibt die Methode eine entsprechende
Meldung nach STDERR und liefert als Returnwert 1.
Der Rückgabewert kann benutzt werden, um im Falle des Schreibens
der Datei weitere Aktionen auszuführen, wie z.B. das Setzen von
Dateirechten.
=cut
# -----------------------------------------------------------------------------
sub
addFile {
my
$self
=
shift
;
my
$file
=
shift
;
my
$dataR
=
@_
%2? (
ref
$_
[0]?
shift
: \
shift
):
undef
;
my
$encoding
=
undef
;
my
$force
= 0;
my
$generate
= 0;
my
$onGenerate
=
undef
;
my
$onUpdate
=
undef
;
my
$quiet
= 0;
my
$skipEmptyFiles
= 0;
my
$writeInitially
= 0;
Quiq::Option->extract(\
@_
,
# ehedem: -mode=>'strict-dash'
-encoding
=> \
$encoding
,
-force
=> \
$force
,
-generate
=> \
$generate
,
-onGenerate
=> \
$onGenerate
,
-onUpdate
=> \
$onUpdate
,
-quiet
=> \
$quiet
,
-skipEmptyFiles
=> \
$skipEmptyFiles
,
-writeInitially
=> \
$writeInitially
,
);
# Wir ignorieren leere Dateien, wenn -skipEmptyFiles=>1
if
(
$skipEmptyFiles
&& (!
$dataR
|| !
defined
$$dataR
||
$$dataR
eq
''
)) {
return
0;
}
# ./ am Pfadanfang entfernen
$file
=~ s|^\./||;
# Verzeichnis ermitteln
my
(
$dir
) =
$file
=~ m|(.*)/|;
# Wir wissen, dass die Datei nicht modifiziert wurde und wollen
# sie nicht schreiben.
if
(!
defined
$dataR
|| !
defined
$$dataR
) {
if
((!-e
$file
||
$generate
) &&
$onGenerate
) {
# Wir generieren den Inhalt der Datei
$dataR
= \
$onGenerate
->(
$file
);
}
elsif
(!-e
$file
) {
# Fataler Fehler. Die Datei existiert nicht und wir können
# ihren Inhalt nicht erzeugen, da wir ihn nicht haben.
$self
->throw(
'DTREE-00001: Datei existiert nicht. Ohne Inhalt kann sie nicht angelegt werden.'
,
File
=>
$file
,
);
}
else
{
# Als Dateiinhalt wurde undef übergeben und die Datei existiert.
# Wir registrieren die Datei als existent und kehren zurück.
$self
->addDir(
$dir
);
delete
$self
->[0]{
$file
};
return
0;
}
}
# Nulloperation, bei Konstruktor-Option -cleanup=>1. Ist die
# Option -writeInitially=>1 angegeben, wird die Datei aus
# der Pfadliste entfernt, wenn sie existiert und nicht identisch ist,
# d.h. sie wird im Zuge des cleanup *nicht* gelöscht.
if
(
$self
->[3]) {
# cleanup
if
(
$writeInitially
&& -e
$file
&&
Quiq::Path->
read
(
$file
,
-decode
=>
$encoding
) ne
$$dataR
) {
# Eintrag entfernen, d.h. Datei am Ende *nicht* löschen
delete
$self
->[0]{
$file
};
}
return
0;
}
# Eintrag entfernen
delete
$self
->[0]{
$file
};
# Existenz der Datei ermitteln
my
$fileExists
= -e
$file
;
# Datei nicht schreiben, wenn sie bereits existiert
if
(
$writeInitially
&&
$fileExists
) {
$self
->addDir(
$dir
);
return
0;
}
# Inhalt vergleichen. Wenn identisch, Datei nicht schreiben.
if
(
$self
->[1] ||
$force
|| !
$fileExists
) {
# nichts tun, wir schreiben auf jeden Fall
}
else
{
# Datei existiert. Prüfen, ob die Daten differieren.
if
(Quiq::Path->
read
(
$file
,
-decode
=>
$encoding
) eq
$$dataR
) {
$self
->addDir(
$dir
);
return
0;
}
}
# Callbackmethode vor dem Schreiben
if
(
$onUpdate
) {
$self
->
$onUpdate
(
$file
,
$$dataR
);
}
# Verzeichnis hinzufügen
$self
->addDir(
$dir
);
# Datei schreiben
if
(!
$self
->[8]) {
Quiq::Path->
write
(
$file
,
$$dataR
,
-encode
=>
$encoding
);
}
if
(!
$self
->[2] && !
$quiet
) {
my
$out
=
$self
->[5];
my
$msg
=
$self
->getText(
$fileExists
?
'Datei aktualisiert'
:
'Datei erzeugt'
);
$out
"$self->[7]$file -- $msg\n"
;
}
return
$fileExists
? 1: 2;
}
# -----------------------------------------------------------------------------
=head3 obsoletePaths() - Liste der überzähligen Pfade
=head4 Synopsis
@arr | $arr = $tree->obsoletePaths;
=head4 Description
Die Pfade werden lexikalisch absteigend sortiert, so dass der
Inhalt eines Verzeichnisses typischerweise vor dem Verzeichnis
kommt. Dies ist aber nicht garantiert, da Punkt "." und Bindestrich
"-" vor dem Verzeichnistrenner "/" kommen.
=cut
# -----------------------------------------------------------------------------
sub
obsoletePaths {
my
$self
=
shift
;
my
@arr
=
sort
{
$b
cmp
$a
}
keys
%{
$self
->[0]};
return
wantarray
?
@arr
: \
@arr
;
}
# -----------------------------------------------------------------------------
=head3 removePath() - Lösche Datei oder Verzeichnis aus Baum
=head4 Synopsis
$bool = $tree->removePath($path);
=head4 Description
Lösche Datei oder Verzeichnis $path aus dem Zielbaum. Liefere 1, wenn
die Löschung durchgeführt wurde, andernfalls 0.
Im Falle eines Verzeichnisses wird die Löschung nur durchgeführt, wenn
das Verzeichnis leer ist. Dateien werden grundsätzlich gelöscht.
=cut
# -----------------------------------------------------------------------------
sub
removePath {
my
$self
=
shift
;
my
$path
=
shift
;
my
$removed
= 0;
if
(-d
$path
) {
if
(Quiq::Path->isEmpty(
$path
)) {
if
(!
$self
->[8]) {
Quiq::Path->
delete
(
$path
);
}
if
(!
$self
->[2]) {
my
$out
=
$self
->[5];
my
$msg
=
$self
->getText(
'Verzeichnis gelöscht'
);
$out
"$self->[7]$path -- $msg\n"
;
}
$removed
= 1;
}
}
else
{
if
(!
$self
->[8]) {
Quiq::Path->
delete
(
$path
);
}
if
(!
$self
->[2]) {
my
$out
=
$self
->[5];
my
$msg
=
$self
->getText(
'Datei gelöscht'
);
$out
"$self->[7]$path -- $msg\n"
;
}
$removed
= 1;
}
if
(
$removed
) {
# Eintrag entfernen
delete
$self
->[0]{
$path
};
}
return
$removed
;
}
# -----------------------------------------------------------------------------
=head3 cleanup() - Entferne überzählige Pfade aus dem Zielbaum
=head4 Synopsis
$n|@paths = $tree->cleanup;
$n|@paths = $tree->cleanup($ask);
$n|@paths = $tree->cleanup($ask,\$timer);
=head4 Returns
Anzahl oder Liste der gelöschten Pfade (Skalarkontext/Arraykontext)
=head4 Description
Entferne alle Dateien und Verzeichnisse aus dem Zielbaum, die obsolet
geworden sind, die seit Objekt-Instantiierung also nicht via
$tree->addFile() oder $tree->addDir() angelegt wurden.
Ist der Parameter $ask angegeben und wahr, wird vor der Löschung
eine Rückfrage auf STDERR gestellt, ob die Löschung wirklich erfolgen
soll. Wird diese nicht mit 'y' beantwortet (auf STDIN), findet
kein Löschen statt und die Methode liefert 0.
Ist ferner der Parameter $timer angegeben, wird die Antwortzeit
des Benutzers auf dessen Wert aufaddiert.
=cut
# -----------------------------------------------------------------------------
sub
cleanup {
my
(
$self
,
$ask
,
$timerR
) =
@_
;
my
@obsoletePaths
=
$self
->obsoletePaths;
if
(
@obsoletePaths
) {
if
(
$ask
) {
my
$msg
=
$self
->getText(
'Überzählige Dateien'
);
my
$prompt
=
"$msg:\n"
;
for
my
$path
(
@obsoletePaths
) {
# Verzeichnisse zeigen wir nicht an
if
(-f
$path
) {
$prompt
.=
"$self->[7]$path\n"
;
}
}
$prompt
.=
$self
->getText(
'Löschen?'
);
my
$answ
= Quiq::Terminal->askUser(
$prompt
,
-values
=>
'y/n'
,
-default
=>
'y'
,
-inHandle
=>
$self
->[4],
-outHandle
=>
$self
->[5],
$timerR
? (
-timer
=>
$timerR
): (),
);
if
(
$answ
ne
'y'
) {
return
wantarray
? (): 0;
}
}
else
{
# Doch (erstmal) keine Meldung
#if (!$self->[2]) {
# my $out = $self->[5];
# my $msg = $self->getText('Aufräumen');
# print $out "*$msg*\n";
#}
}
}
# Die Liste der überzähligen Pfade wird so lange durchlaufen
# bis keine Änderungen mehr eintritt. Auf diese Weise wird
# sichergestellt, dass Elternverzeichnisse gelöscht werden, wenn
# sie leer werden.
my
@paths
;
while
(1) {
my
$n
= 0;
for
my
$path
(
$self
->obsoletePaths) {
# Liste ändert sich!
if
(
$self
->removePath(
$path
)) {
$n
++;
push
@paths
,
$path
;
}
}
# TODO: Warnung, wenn ungelöschte Pfade übrig bleiben?
last
if
$n
== 0;
}
return
@paths
;
}
# -----------------------------------------------------------------------------
=head3 close() - Schließe Zielbaumvergleich ab
=head4 Synopsis
$dt->close;
=head4 Description
Schließe Zielbaumvergeleich ab. Nach Aufruf kann die Objektreferenz
nicht mehr verwendet werden. Die Methode liefert keinen Wert zurück.
=cut
# -----------------------------------------------------------------------------
sub
close
{
$_
[0] =
undef
;
return
;
}
# -----------------------------------------------------------------------------
=head3 getText() - Liefere Übersetzung
=head4 Synopsis
$text = $tree->getText($textDe);
=head4 Returns
Übersetzten Text (String)
=head4 Description
Liefere die Übersetzung zum deutschen Text $textDe.
=cut
# -----------------------------------------------------------------------------
my
%Text
= (
'Verzeichnis erzeugt'
=>
'directory created'
,
'Datei aktualisiert'
=>
'file updated'
,
'Datei erzeugt'
=>
'file created'
,
'Verzeichnis gelöscht'
=>
'directory deleted'
,
'Datei gelöscht'
=>
'file deleted'
,
'Überzählige Dateien'
=>
'Obsolete files'
,
'Löschen?'
=>
'Remove?'
,
'Aufräumen'
=>
'cleanup'
,
);
sub
getText {
my
(
$self
,
$textDe
) =
@_
;
return
$self
->[6] eq
'de'
?
$textDe
:
$Text
{
$textDe
} ||
$textDe
;
}
# -----------------------------------------------------------------------------
=head1 VERSION
1.225
=head1 AUTHOR
Frank Seitz, L<http://fseitz.de/>
=head1 COPYRIGHT
Copyright (C) 2025 Frank Seitz
=head1 LICENSE
This code is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
=cut
# -----------------------------------------------------------------------------
1;
# eof