diff --git a/.gitignore b/.gitignore
index 0520c8a..2211702 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,2 @@
-TODO.md
-OLD
+TODO
+*.new
diff --git a/Makefile b/Makefile
index a8dcc4c..5fb7dd5 100644
--- a/Makefile
+++ b/Makefile
@@ -1,12 +1,14 @@
-SHELL := /bin/bash
-PATH := $(PATH):UTIL
PACKAGE := subrelease
-VERSION := 0.11
+VERSION := 0.12
PROJECT := makeutils
AUTHORS := R.Jaksa 2001,2021,2023 GPLv3
-CAPTION := minimal subrelease snapshotting
-SUBVERS :=
-BUILT := $(shell echo `date +%Y-%m-%d`)
+CAPTION := directory to package snapshotting
+SUBVERS := b
+
+SHELL := /bin/bash
+PATH := usr/bin:$(PATH)
+PKGNAME := $(PACKAGE)-$(VERSION)$(SUBVERSION)
+DATE := $(shell date '+%Y-%m-%d')
BIN := getversion subrelease
SRC := $(BIN:%=%.pl)
@@ -17,8 +19,9 @@ LIB := $(filter-out $(SRC),$(LIB))
all: $(BIN) $(DOC)
%: %.pl $(LIB) .version.pl .built.%.pl Makefile
- @echo -e '#!/usr/bin/perl\n' > $@
- perlpp-simple $< >> $@
+ @echo -e '#!/usr/bin/perl' > $@
+ @echo "# $@ generated from $(PKGNAME)/$< $(DATE)" >> $@
+ pcpp $< >> $@
@chmod 755 $@
.version.pl: Makefile
@@ -31,20 +34,23 @@ all: $(BIN) $(DOC)
.PRECIOUS: .built.%.pl
.built.%.pl: %.pl $(LIB) .version.pl Makefile
- @echo 'our $$BUILT = "$(BUILT)";' > $@
+ @echo 'our $$BUILT = "$(DATE)";' > $@
@echo "update $@"
-# install symlinks to private ~/bin
-install: $(BIN) subls
- @echo install symlinks to ~/bin:
- @mkdir -p ~/bin
- @for i in $+; do ln -sf `pwd`/$$i ~/bin/`basename $$i`; done
- @for i in $+; do ls -l --color ~/bin/`basename $$i`; done
- @if test ! `echo $$PATH | grep ~/bin`; then echo "~/bin missing in your PATH: $$PATH"; fi
-
$(DOC): doc/%.md: % | doc
$< -h | man2md > $@
+# /map install (also subls)
+ifneq ($(wildcard /map),)
+install: $(BIN) subls
+ mapinstall -v /box/$(PROJECT)/$(PKGNAME) /map/$(PACKAGE) bin $^
+
+# /usr/local install
+else
+install: $(BIN)
+ install $^ /usr/local/bin
+endif
+
clean:
rm -f .version.pl
rm -f .built.*.pl
@@ -53,4 +59,4 @@ mrproper: clean
rm -f $(BIN)
rm -f $(DOC)
-include ~/.gitlab/Makefile.git
+-include ~/.github/Makefile.git
diff --git a/NEWS b/NEWS
index e269b5e..838da85 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,5 @@
+0.12 - fixed Makefile, install to /usr/local
+ - added EXCLUDE
0.11 - version up to the mkdist 0.8...
0.2 - code cleanup
- getversion uses next.pl
diff --git a/README.md b/README.md
index 173b712..5d4d8f4 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,16 @@
-
# subrelease
-Simple tool to snapshot current directory to the package file or into
+Simple tool to snapshot the current directory to the package file or into
subrelease archive.
#### Story
-I have used a packing script since around 2000. First it was a few lines shell
-script, later a big one with html generator, now small again. I still use it
+I have used a packing script since around 2000. First, it was a few lines of shell
+script, later a big one with html generator, and now small again. I still use it
today, even alongside git.
The inspiration came when somebody told me that compressing a file once or
-ten-times is the same.
+ten times is the same.
![](doc/size.png)
@@ -21,16 +20,16 @@ The hit-enter usage is: `subrelease`, `subrelease`, `subrelease`, increment vers
`subrelease`, `subrelease`...
This is a sequence of several 'small' subreleases which are handled
-automatically, then a 'big' release, then next round of subreleases etc. If it
-is git, it would be a sequence of commits, then tag (plus release), and next
+automatically, then a 'big' release, then the next round of subreleases, etc. If it
+is git, it would be a sequence of commits, then tag (plus release), and the next
round of commits. When subrelease is used over the git repository, subreleases
-are completely independent from the git.
+are completely independent of the git.
Subrelease itself does:
-1. identifies package name, version, subversion and location of archive files (automatically),
+1. identifies package name, version, subversion, and location of archive files (automatically),
2. identifies whether now it should be a subrelease or release (automatically),
-3. saves package snapshot into archive or a release file (automatically),
+3. saves package snapshot into an archive or a release file (automatically),
4. increments subrelease variable value if needed (automatically).
@@ -45,7 +44,7 @@ Subrelease saves two types of files:
The `.tlz` and `.xlz` are shortcuts for `.tar.lz` and can be extracted with
standard tar. Optionally, subrelease can save `.tar.gz`, `.tbz2`, `.tar.zst`,
-etc. All these files are saved into parent directory, or into `../tgz` directory
+etc. All these files are saved into the parent directory, or the `../tgz` directory
if it exists.
Typical subrelease history and saved files would be:
@@ -55,9 +54,9 @@ Typical subrelease history and saved files would be:
#### Variants
-Subrelease can archive variants of a package, for instance the "beta" variant,
-or David's variant "david" etc. To create a beta variant of package, just set
-a flag `VARIANT` to "beta" (when you increment a version). Later when you
+Subrelease can archive variants of a package, for instance, the "beta" variant,
+or David's variant "david" etc. To create a beta variant of the package, just set
+the flag `VARIANT` to "beta" (when you increment a version). Later when you
decide to turn off beta status, just remove the flag and call the subrelease.
You will have beta release and main release packages, and a common archive for
beta and main subreleases:
@@ -65,25 +64,25 @@ beta and main subreleases:
-Variants flags can be used to create temporary experimental variants of package
-to be eventually merged-back later. In following example we copy the
-subversion b of package to different directory and set a variant flag on it.
-Then on the next subrelease call we got local initial release of variant
-"david", and on the second call a subversions archive is created and initiated
+Variant flags can be used to create temporary experimental variants of the package
+to be eventually merged back later. In the following example, we copy the
+subversion b of the package to a different directory and set a variant flag on it.
+Then on the next subrelease call, we got the local initial release of the variant
+"david", and on the second call, a subversion archive is created and initiated
with a new subrelease c. This subrelease c and the main package subrelease c are
different. Archives are different as well, although having the same name.
-Archive names do not contain variant string, to allow to freely change variant
+Archive names do not contain a variant string, to allow to freely change the variant
string from subversion to subversion.
#### Reserved names
-In order to use the same version name by the subrelease and by the application,
+To use the same version name by the subrelease and by the application,
subrelease has to understand variable definition in particular programming
-language, and we have to reserve few variable names, like this for C:
+language, and we have to reserve a few variable names, like this for C:
``` c
# define PACKAGE "package-name"
@@ -103,14 +102,14 @@ $COPYLEFT="(c) The.Author 1999, GPL";
Accompanying script `getversion` can inspect these reserved variables. It
checks `VERSION.h` or `CONFIG.h` files, or `VERSION.pl`, `VERSION.py`, etc. for
-other languages. See [`getversion -h`](doc/getversion.md) for description of
-syntax for other languages.
+other languages. See [`getversion -h`](doc/getversion.md) for a description of
+the syntax for other languages.
For multi-language applications, the `Makefile` can be used to hold variables,
which when changed will write them to temporary files for every used language,
like `.version.pl`, `.version.py`, etc.
-Reserved variables names are (plus few more for config files):
+Reserved variable names are (plus a few more for config files):
`PACKAGE` package name,
`VERSION` its version,
@@ -120,22 +119,23 @@ Reserved variables names are (plus few more for config files):
`CAPTION` any package notes, message, definition,
`VARIANT` `BRANCH` variant/branch name.
-The `SUBVERSION` variable is automatically incremented always when subversion
+The `SUBVERSION` variable is automatically incremented always when the subversion
creates a new subrelease, thus the application can know which subversion it is.
#### Config files
-Stealth operation without introducing any new variables definitions to the
+Stealth operation without introducing any new variable definitions to the
source tree is possible by keeping them instead in the config file
-`.subrelease` in current or any parent directory (than you will not add
-anything into source tree).
+`.subrelease` in the current or any parent directory (than you will not add
+anything into the source tree).
-Config files also recognize few more variables. To preserve the namespace of
+Config files also recognize a few more variables. To preserve the namespace of
application code these are config-files only, visible only in `.subrelease` or
`VERSION` files:
`SUFFIX` the archive suffix, see [`subrelease -h`](doc/subrelease.md) for the list of suffixes,
`TGZDIR` target directory, instead of the default `../tgz` or `..`,
+ `EXCLUDE` exclude specified files from being archived,
`ALWAYS` the script/command to be fired at the and of every `subrelease` run,
`ONREL` `ONRELEASE` script only fired at full release,
`ONSUB` `ONSUBRELEASE` only fired at the subrelease, not at release.
@@ -143,20 +143,27 @@ application code these are config-files only, visible only in `.subrelease` or
Scripts can be used to automatically:
* to sync data to mirror,
- * to submit summary to webpage,
+ * to submit a summary to a webpage,
* to run code metrics scripts,
* etc.
-The `.subrelease` can be found in home directory, current directory or any
+The `EXCLUDE` can be used to avoid `.git` subdirs in all project directories by
+placing the `.subrelease` file in the top-level projects directory with a line:
+
+```
+EXCLUDE: .git
+```
+
+The `.subrelease` can be placed in the home directory, current directory, or any
parent directory. The order of variables overriding is:
- 1. `~/.subrelease` from home directory,
- 2. `./.subrelease` or `../.subrelease` or higher, varibles here override these from 1.,
- 3. CLI arguments, like `-v` option overrides config values, step 4. depends on its value,
+ 1. `~/.subrelease` from the home directory,
+ 2. `./.subrelease` or `../.subrelease` or higher, variables here override these from 1.,
+ 3. CLI arguments, like `-v` option override config values, step 4. depends on its value,
4. `./VERSION.c` or `./VERSION.pl` etc., these are main variables definitions, override 1. to 3.,
- 5. CLI arguments, finaly CLI again overrides everything.
+ 5. CLI arguments, finally CLI again overrides everything.
-Some variables can be defined is `.subrelease` file and some in `VERSION.c`
+Some variables can be defined in the `.subrelease` file and some in the `VERSION.c`
file etc. at the same time, to control their visibility.
#### See also
@@ -172,24 +179,24 @@ installation the `make install` routine is provided.
#### Under the hood
-Subrelease is written in perl, but is compiled from source files to the final
-executable script by preprocessor. The reason for such approach is to have
-singlefile script as a result, and at the same time allow source code to be
-structured to several files. Perl provides best expressive power and great
+Subrelease is written in perl but is compiled from source files to the final
+executable script by preprocessor. The reason for such an approach is to have
+a singlefile script as a result, and at the same time allow source code to be
+structured to several files. Perl provides the best expressive power and great
stability, which is (most) important for the maintenance of script in the long
-term. And for compactness of executed application script.
+term. And for compactness of the executed application script.
-Over the time I had different requirements for the script. Sometimes conflicting.
-Today my subrelease design summary would be:
+Over time I had different requirements for the script. Sometimes conflicting.
+Today my subrelease design summary will be:
* content-only approach without script's private metadata/database/indexes:
- primary knowledge source - filenames in the archive,
- secondary knowledge source - variables in the application source code (and configs),
* data access with standard tools - `subrelease` is not needed to extract or inspect the archive,
- * autodetection first - autodetect everything what is possible to autodetect,
+ * autodetection first - autodetect everything possible to autodetect,
* any-functionality = single-command:
- `subrelease` to archive,
- `getversion` to inspect,
- * can be used either as standalone release tool, or alongside git/github etc.
+ * can be used either as a standalone release tool or alongside git/github etc.
R.Jaksa 2023
diff --git a/UTIL/perlpp-simple b/UTIL/perlpp-simple
deleted file mode 100755
index 30d0aae..0000000
--- a/UTIL/perlpp-simple
+++ /dev/null
@@ -1,120 +0,0 @@
-#!/usr/bin/perl
-# simple perl preprocessor - just incorporates include files
-# * looks into all subdirectories to find included file
-# - in the depth order
-# - relative to CWD from where perlpp is run, not where file is located
-# * avoids double include
-# * -e DIR to exclude directories from search
-# * -d for debug messaging
-# syntax:
-# #include "abc.pl" # comments, any name if quoted
-# # include abc.pl comments, only .pl suffix if not quoted, # not needed
-# # include abc.pl def.pl # multiple files in one include allowed
-# TODO: actually, double include is useful when including inside blocks!
-
-for(@ARGV) { $DEBUG=1 and $_="" if $_ eq "-d" }
-our $CR_="\033[31m"; # color red
-our $CG_="\033[32m"; # color green
-our $CK_="\033[90m"; # color black
-our $CD_="\033[0m"; # color default
-sub pr { print STDERR @_ }
-sub pf { printf STDERR @_ }
-
-# --------------------------------------------------------------- EXCLUDED DIRS
-our @EXCL; # list of dirs to be excluded
-
-for(my $i=0;$i<$#ARGV;$i++) {
- next if $ARGV[$i] ne "-e" or not -d $ARGV[$i+1];
- push @EXCL,$ARGV[$i+1];
- $ARGV[$i] = $ARGV[$i+1] = "" }
-
-for(@EXCL) { $_="./$_" if not /^\.\// } # prepend "./" if needed
-
-# pr "excl: $_\n" for @EXCL;
-# ------------------------------------------------ LIST OF DIRS WITH PERL FILES
-our @DIRS; # recursive list of local dirs
-our %NF; # number of files in each dir
-our %FF; # list of perl files in each dir
-
-# inar \@a,$s; checks whether the string $s is in an array @a
-sub inar { my ($a,$s)=@_; for(@{$a}) { return 1 if $_ eq $s } return 0 }
-
-# pldirs "."; looks for all dirs with .pl files in ".", fills-up @DIRS and %NF
-sub pldirs {
- my $dir = $_[0];
- my @all; opendir(DIR,$dir); @all=readdir(DIR); closedir(DIR);
- my @ff; for(@all) { push @ff,$_ if $_=~/\.pl$/ } # save .pl files names
- if(@ff) { push @DIRS,$dir; $NF{$dir}=@ff; $FF{$dir}=\@ff } # save nonempty
- for(@all) {
- next if /^\./; # skip hidden dirs
- my $path = "$dir/$_";
- next if inar \@EXCL,$path; # skip excluded dirs
- pldirs($path) if -d $path; }}
-
-# do look
-pldirs ".";
-
-# pf "%4d: %s\n",$NF{$_},$_ for @DIRS;
-# for my $d (@DIRS) { pf "%22s:",$d; pr " $_" for @{$FF{$d}}; pr "\n" }
-# ------------------------------------------------------------ SORT SEARCH DIRS
-
-# 1st compare by number of slashes - to look in current directory first
-# 2nd compare by number of .pl files in dir - just speculative speedup
-sub compare {
- my $ca = $a=~tr/\///; # count of / in $a
- my $cb = $b=~tr/\///; # in $b
- if ($ca<$cb) { return -1 }
- elsif($ca>$cb) { return 1 }
- else {
- if ($NF{$a}>$NF{$b}) { return -1 }
- elsif($NF{$a}<$NF{$b}) { return 1 }
- else { return 0 }}}
-
-# resort DIRS
-@DIRS=(); push @DIRS,$_ for sort compare keys %NF;
-
-# pf "%4d: %s\n",$NF{$_},$_ for @DIRS;
-# ------------------------------------------------------------ PROCESS INCLUDES
-our @INCLUDED; # list of already included files (to disable double include)
-
-# line by line add a file to the output, parse # macros
-sub addfile {
- my $file=$_[0];
- local $in=$_[1];
-
- # look for file recursively
- my ($path,$ok); # full path and whether found
- for my $dir (@DIRS) { $path = "$dir/$file";
- $ok=2 and last if inar \@INCLUDED,$path; # already included
- $ok=1 and last if inar $FF{$dir},$file } # found => proceed
-
- # debug
- sub prd { pr "$in$_[0]$_[1]$CD_\n" }
- if($DEBUG) {
- if ($ok==1) { (my $p=$path)=~s/^\.\///; prd $CG_,$p } # OK
- elsif($ok==2) { prd $CK_,$file } # double include
- else { prd $CR_,$file }} # not found
-
- return if $ok!=1; # file not found
- push @INCLUDED,$path; # register file
-
- # parse the file
- my $IN1 = qr/^\h*\"([^\"]+)\"\h*/; # quoted include
- my $IN2 = qr/^\h*([a-zA-Z0-9\._-]+\.pl)\h*/; # .pl unquoted include
- for(split /\n/,`cat $path`) {
-
- # include lines
- if(/^\h*\#\h*include\h+(.*?)$/) {
- my $s=$1; my $ok;
- while($s=~s/$IN1// or $s=~s/$IN2//) { addfile($1,"$in "); $ok=1 }
- next if $ok }
-
- # regular lines
- print "$_\n" }}
-
-# add each requested file (argument) to the output
-for(@ARGV) {
- next if $_ eq "";
- addfile $_ }
-
-# ----------------------------------------------------- R.Jaksa 2000,2023 GPLv3
diff --git a/doc/getversion.md b/doc/getversion.md
index d72e8cf..43a0d06 100644
--- a/doc/getversion.md
+++ b/doc/getversion.md
@@ -88,5 +88,5 @@ examination of present configuration files, or directory names.
subrelease -h
### VERSION
-subrelease-0.11bt R.Jaksa 2001,2021,2023 GPLv3 (built 2023-08-13)
+subrelease-0.12 R.Jaksa 2001,2021,2023 GPLv3 (built 2024-05-10)
diff --git a/doc/subrelease.md b/doc/subrelease.md
index b355dda..c424546 100644
--- a/doc/subrelease.md
+++ b/doc/subrelease.md
@@ -44,9 +44,9 @@ autodetected by getversion.
The .subrelease file from the home directory and from the first
parent directory is used as a config file for the subrelease run.
Syntax is the same as for the VERSION file (see getversion -h).
- Config-specific keywords are SUFFIX, TGZDIR and keywords ALWAYS,
- ONRELEASE and ONSUBRELEASE to define scripts to run. Possible
- suffixes for the archive files are:
+ Config-specific keywords are SUFFIX, TGZDIR, EXCLUDE and keywords
+ ALWAYS, ONRELEASE and ONSUBRELEASE to define scripts to run.
+ Possible suffixes for the archive files are:
short full suffix
+-------------+-----------------------+
@@ -57,7 +57,7 @@ autodetected by getversion.
+-------------+-----------------------+
rel. arch. release archive
- Script keywords usage examples with automatic variables follows:
+ Examples of scripts defintion with automatic variables:
ALWAYS: echo `date -I` %P %f >> /var/log/subrelease.log
ONSUBRELEASE: ~/util/mknews ./Changelog
ONRELEASE: scp %f server:/archive/%x/
@@ -67,6 +67,11 @@ autodetected by getversion.
%p package name, %P w. version, %b variant/branch, %B w. branch
%a authors, %c caption, %x project, %l language
%% the % character
+
+ The EXCLUDE allows to exclude specified files from being archived.
+ Exclude patterns are space separated glob patterns, can be quoted
+ with double quotes:
+ EXCLUDE: .git *.png "copy of *"
### RECOVERY
In case of error (No space left on device, etc.) the re-packaging
@@ -79,5 +84,5 @@ autodetected by getversion.
getversion -h
### VERSION
-subrelease-0.11bt R.Jaksa 2001,2021,2023 GPLv3 built 2023-08-13
+subrelease-0.12 R.Jaksa 2001,2021,2023 GPLv3 built 2024-05-10
diff --git a/exclude.pl b/exclude.pl
new file mode 100644
index 0000000..e5db95d
--- /dev/null
+++ b/exclude.pl
@@ -0,0 +1,57 @@
+# HANDLING EXCLUDE PATTERNS
+
+# parse exclude lines: space-split, but keep quoted ones; keep the order
+sub parse_excluded {
+ my @new; # output: new per-pattern array
+ my $i=1; # index of quoted patterns
+ for my $line (@{$_[0]}) { # input: raw EXCLUDE lines of patterns
+
+ # save quoted patterns
+ my @quoted; @quoted[$i++]=$1 while $line=~s/"([^"]*)"/__QTD${i}__/;
+
+ # space-split every EXCLUDE line
+ for my $pat (split(/ /,$line)) {
+ $pat=$quoted[$1] if $pat=~/^__QTD([0-9]+)__$/;
+ next if $pat eq "";
+ push @new,$pat }}
+
+ return \@new }
+
+# return only patterns which match some file in current working directory, input=patterns
+# TODO: load cwd tree only once, and per-pattern parse using perl regex
+sub valid_excluded {
+ my %valid; # output hash: how many files/dirs are found per pattern
+ for my $pat (@{$_[0]}) {
+
+ # fixed pattern for the -path variant
+ my $fix=$pat;
+ $fix="$fix*" if not $pat=~/\*$/;
+ $fix="*$fix" if not $pat=~/^\*/;
+
+ my @found; # list of found files/paths
+ if($pat=~/\//) { @found = split /\n/,`find . -path '$fix'` }
+ else { @found = split /\n/,`find . -name '$pat'` }
+ # print "$#found $pat: @found\n";
+ $valid{$pat} = $#found+1; }
+
+ return %valid }
+
+# filter_excluded(\@patterns,\%valid) return only the valid patterns
+sub filter_excluded {
+ my @new;
+ for my $pat (@{$_[0]}) {
+ next if not $_[1]->{$pat};
+ push @new,$pat }
+ return \@new }
+
+# print_excluded(\@patterns,\%valid) return verbose patterns string
+sub print_excluded {
+ my $s;
+ for my $pat (@{$_[0]}) {
+ if ($_[1]->{$pat}==0) { $s .= "$CK_$pat$CD_ " } # black - none
+ elsif($_[1]->{$pat}==1) { $s .= "$CG_$pat$CD_ " } # green - one
+ else { $s .= "$CR_$pat$CD_ " }} # red - more
+ $s =~ s/ $//;
+ return $s }
+
+# R.Jaksa 2024 GPLv3
diff --git a/getversion b/getversion
index 5ba2bd4..97d858a 100755
--- a/getversion
+++ b/getversion
@@ -1,11 +1,19 @@
#!/usr/bin/perl
+# getversion generated from subrelease-0.12/getversion.pl 2024-05-10
-our $BUILT = "2023-08-13";
+# included ".built.getversion.pl"
+our $BUILT = "2024-05-10";
+# end ".built.getversion.pl"
+
+# included ".version.pl"
our $PACKAGE = "subrelease";
-our $VERSION = "0.11";
+our $VERSION = "0.12";
our $PROJECT = "makeutils";
our $AUTHOR = "R.Jaksa 2001,2021,2023 GPLv3";
-our $SUBVERSION = "bt";
+our $SUBVERSION = "";
+# end ".version.pl"
+
+# included "support.pl"
# remove NL
our sub nonl { chomp $_[0]; return $_[0]; }
@@ -115,6 +123,7 @@ sub homecfg { my $fnm = $_[0];
sub cfgval { return $2 if $_[0]=~/(^|\n)\h*$_[1]\h*:\h+(.+?)\h*(\n|$)/ }
# R.Jaksa 2009,2023
+# end "support.pl"
our $HELP=<> /var/log/subrelease.log)
CW(ONSUBRELEASE: ~/util/mknews ./Changelog)
CW(ONRELEASE: scp %f server:/archive/%x/)
@@ -189,6 +198,11 @@ CONFIG
CC(%a) authors, CC(%c) caption, CC(%x) project, CC(%l) language
CC(%%) the % character
+ The EXCLUDE allows to exclude specified files from being archived.
+ Exclude patterns are space separated glob patterns, can be quoted
+ with double quotes:
+ CW(EXCLUDE: .git *.png "copy of *")
+
RECOVERY
In case of error (No space left on device, etc.) the re-packaging
might be stopped in the middle. For recovery check CC(.bkp) files in
@@ -234,6 +248,7 @@ for($i=0;$i<$#ARGV;$i++) {
next if not $ARGV[$i]=~/^[a-zA-Z]/;
$REQNAME=$ARGV[$i]; $ARGV[$i]=""; last }
+# included "prnt.pl"
{ # CUSTOM PRINTING
my $HDRLEN=20;
@@ -257,6 +272,7 @@ my sub prnt_ { my ($c1,$c2,$k,$v,$c) = @_;
# prnt "key",$var,"comment";
our sub prnt { prnt_ "",$CD_,@_ } # default color
our sub prntg { prnt_ "",$CG_,@_ } # green
+our sub prntr { prnt_ $CR_,$CR_,@_ } # red
our sub prntfn { prnt_ "",$CK_,$_[0],defgrfn($_[1]),$_[2]; } # filename debug
# debug "key",$var,"comment";
@@ -277,12 +293,12 @@ our sub prnt_logo {
prnt; }
# ---------------------------------------------------------------------------------------- HASH DEBUG
-# TODO: explicit order of keys
+# for explicit order of keys
my @ORDER = ("file","language","package","project","version","subversion","next","variant",
"pkgname","fullname","major","minor","mtype","delimiter","dot","pwd","config2",
"config1","config","tmpdir","tgzdir","suffix","authors","caption","always",
- "onsubrelease","onrelease");
+ "onsubrelease","onrelease","exclude");
# prel $key,$val,$msg; prints single element for debughash
my sub prel {
@@ -318,6 +334,7 @@ our sub proceed {
prnt; } # print empty line
} # R.Jaksa 2001,2023 GPLv3
+# end "prnt.pl"
# wrong args check
my @wrong;
@@ -331,7 +348,12 @@ our %PKG; # package parameters key-value pairs
our %PKK; # exact original keywords for given key
our %PKM; # verbose message for given key
+# included "config.pl"
+
+# included "parser.pl"
# GETVERSION PARSERS
+
+# included "tore.pl"
# to-regex parser: regexes are hard to read, but simple task-specific language
# can be parsed to regex, and such language can be OK to read...
#
@@ -364,8 +386,9 @@ sub enre {
$s =~ s/^\(/((?:^|\\n)/; # first element starts with (?:^|\n) start of line
$s =~ s/\)$/(?:\\n|\$))/; # last element ends with (?:\n|$) end of line
return $s }
+# end "tore.pl"
-{ # ---------------------------------------------------------------------------- PER-LASGUAGE PARSERS
+{ # ---------------------------------------------------------------------------- PER-LANGUAGE PARSERS
# key/value parsers: value = xyzvar(body,key) # $_[0]=body $_[1]=key
# return: a value, or (value,used_keyname) pair
@@ -455,30 +478,33 @@ our sub getvar {
local $CMD = $_[0]; # local to be seen in checkvar
local $BODY = $_[1]; $BODY = `cat $PKG{file}` if not defined $_[1];
local $MSG = "<- ".beautify($PKG{file},$PKG{pwd}); $MSG.=" $_[2]" if defined $_[2];
- checkvar "PACKAGE","package";
- checkvar "VERSION","version";
- checkvar "SUBVERS", "subversion";
- checkvar "SUBVERSION","subversion";
- checkvar "PROJECT","project";
- checkvar "AUTHOR", "authors";
- checkvar "AUTHORS", "authors";
- checkvar "COPYRIGHT","authors";
- checkvar "COPYLEFT", "authors";
- checkvar "CAPTION","caption";
- checkvar "BRANCH", "variant";
- checkvar "VARIANT","variant";
- return if not $PKG{config}; # the rest are config specific keywords
- checkvar "SUFFIX","suffix";
- checkvar "TGZDIR","tgzdir";
- checkcfg "ONSUBRELEASE","onsubrelease"; # next are arrays = accept multiple lines
- checkcfg "ONSUB", "onsubrelease";
- checkcfg "ONRELEASE","onrelease";
- checkcfg "ONREL", "onrelease";
- checkcfg "ALWAYS","always" }
+ # ------- VARIABLE -------- KEY -----------
+ checkvar "PACKAGE", "package";
+ checkvar "VERSION", "version";
+ checkvar "SUBVERS", "subversion";
+ checkvar "SUBVERSION", "subversion";
+ checkvar "PROJECT", "project";
+ checkvar "AUTHOR", "authors";
+ checkvar "AUTHORS", "authors";
+ checkvar "COPYRIGHT", "authors";
+ checkvar "COPYLEFT", "authors";
+ checkvar "CAPTION", "caption";
+ checkvar "BRANCH", "variant";
+ checkvar "VARIANT", "variant";
+ return if not $PKG{config}; # the rest are config specific keywords
+ checkvar "SUFFIX", "suffix";
+ checkvar "TGZDIR", "tgzdir";
+ checkcfg "ONSUBRELEASE", "onsubrelease"; # next are checkcfg arrays = accept multiple lines
+ checkcfg "ONSUB", "onsubrelease";
+ checkcfg "ONRELEASE", "onrelease";
+ checkcfg "ONREL", "onrelease";
+ checkcfg "ALWAYS", "always";
+ checkcfg "EXCLUDE", "exclude" }
} # -------------------------------------------------------------------------------------------------
# R.Jaksa 2001,2009,2023 GPLv3
+# end "parser.pl"
{ # SUBRELEASE CONFIG
@@ -564,6 +590,8 @@ our sub subrelease_config {
} subrelease_config;
# R.Jaksa 2023 GPLv3
+# end "config.pl"
+
prntg "config",beautify($PKG{config1},$PKG{pwd}) if defined $PKG{config1};
prntg "config",beautify($PKG{config2},$PKG{pwd}) if defined $PKG{config2};
@@ -584,6 +612,10 @@ $PKG{suffix}="tlz" and $PKM{suffix}="<= default" if not $PKG{suffix};
fatal "target directory $PKG{tgzdir} does not exist" if not -d $PKG{tgzdir};
# ========================================================================================== IDENTIFY
+
+# included "identify.pl"
+
+# included "names.pl"
{ # SUBRELEASE NAMES/FILENAMES UTILITIES, required %PKG access
# parse the string to package, delimiter, version
@@ -673,15 +705,18 @@ my sub ziptype {
elsif($PKG{suffix} =~ /gz$/) { return "--gzip" }
return "--lzip" }
+# TODO here add tar exclude options
+
# tar from given dir: tarfrom(inputdir,inputfileordir,outputtar)
our sub tarfrom { my ($dir,$file,$tar,$cc) = @_; my $zip = ziptype;
- cmd "tar cf $tar $zip -C $dir $file",$cc; }
+ cmd "tar cf $tar $TAROPT$zip -C $dir $file",$cc; }
# directory tar: tardir(inputdir,outputtar)
our sub tardir { my ($dir,$tar) = @_; my $zip = ziptype;
- cmd "tar cf $tar $zip $dir"; }
+ cmd "tar cf $tar $TAROPT$zip $dir"; }
} # R.Jaksa 2001,2021,2023 GPLv3
+# end "names.pl"
{ # GETVERSION IDENTIFY
@@ -788,6 +823,7 @@ our sub getversion_identify {
} getversion_identify;
# R.Jaksa 2001,2021,2023 GPLv3
+# end "identify.pl"
# explicit values by CLI args (again, here to override autodetected)
$PKG{$_}=$ARG{$_} and $PKM{$_}="<- CLI arg" for keys %ARG;
@@ -828,6 +864,8 @@ debugfn "old sub. archive","$PKG{tgzdir}/$archive" if $archive;
debugfn "new sub. archive","$PKG{tgzdir}/$ARCHIVE" if $ARCHIVE and $ARCHIVE ne $archive;
# ============================================================================================== NEXT
+
+# included "next.pl"
# this works directly on global %PKG
# get_next_sub $release,$archive; inspects release/archive files (relative to tgzdir)
@@ -884,6 +922,7 @@ sub get_next_sub {
$PKM{next} = "<- $archive"; }
# R.Jaksa 2001,2023 GPLv3
+# end "next.pl"
# reset subversion if we do RELEASE and there is no ARCHIVE
if($RELEASE and not $archive and defined $PKG{subversion} and $PKG{subversion}) {
@@ -940,6 +979,85 @@ if(defined $PKG{always}) { prnt "script","$CC_$_$CD_" for @{$PKG{always}}}
if(defined $PKG{onsubrelease}) { prnt "subrelease script","$CC_$_$CD_" for @{$PKG{onsubrelease}}}
if(defined $PKG{onrelease}) { prnt "release script","$CC_$_$CD_" for @{$PKG{onrelease}}}
+# =========================================================================================== EXCLUDE
+
+# included "exclude.pl"
+# HANDLING EXCLUDE PATTERNS
+
+# parse exclude lines: space-split, but keep quoted ones; keep the order
+sub parse_excluded {
+ my @new; # output: new per-pattern array
+ my $i=1; # index of quoted patterns
+ for my $line (@{$_[0]}) { # input: raw EXCLUDE lines of patterns
+
+ # save quoted patterns
+ my @quoted; @quoted[$i++]=$1 while $line=~s/"([^"]*)"/__QTD${i}__/;
+
+ # space-split every EXCLUDE line
+ for my $pat (split(/ /,$line)) {
+ $pat=$quoted[$1] if $pat=~/^__QTD([0-9]+)__$/;
+ next if $pat eq "";
+ push @new,$pat }}
+
+ return \@new }
+
+# return only patterns which match some file in current working directory, input=patterns
+# TODO: load cwd tree only once, and per-pattern parse using perl regex
+sub valid_excluded {
+ my %valid; # output hash: how many files/dirs are found per pattern
+ for my $pat (@{$_[0]}) {
+
+ # fixed pattern for the -path variant
+ my $fix=$pat;
+ $fix="$fix*" if not $pat=~/\*$/;
+ $fix="*$fix" if not $pat=~/^\*/;
+
+ my @found; # list of found files/paths
+ if($pat=~/\//) { @found = split /\n/,`find . -path '$fix'` }
+ else { @found = split /\n/,`find . -name '$pat'` }
+ # print "$#found $pat: @found\n";
+ $valid{$pat} = $#found+1; }
+
+ return %valid }
+
+# filter_excluded(\@patterns,\%valid) return only the valid patterns
+sub filter_excluded {
+ my @new;
+ for my $pat (@{$_[0]}) {
+ next if not $_[1]->{$pat};
+ push @new,$pat }
+ return \@new }
+
+# print_excluded(\@patterns,\%valid) return verbose patterns string
+sub print_excluded {
+ my $s;
+ for my $pat (@{$_[0]}) {
+ if ($_[1]->{$pat}==0) { $s .= "$CK_$pat$CD_ " } # black - none
+ elsif($_[1]->{$pat}==1) { $s .= "$CG_$pat$CD_ " } # green - one
+ else { $s .= "$CR_$pat$CD_ " }} # red - more
+ $s =~ s/ $//;
+ return $s }
+
+# R.Jaksa 2024 GPLv3
+# end "exclude.pl"
+
+$PKG{exclude} = parse_excluded $PKG{exclude};
+
+# check applicability of exclude commands
+my %valid = valid_excluded $PKG{exclude};
+debugfn "exclude",print_excluded($PKG{exclude},\%valid) if @{$PKG{exclude}} and $DEBUG;
+
+$PKG{exclude} = filter_excluded $PKG{exclude},\%valid;
+prntr "exclude","@{$PKG{exclude}}" if @{$PKG{exclude}} and not $DEBUG;
+
+# additional tar options (to exclude by glob pattern)
+our $TAROPT;
+for my $s (@{$PKG{exclude}}) {
+ if($s=~/[\h\*\?\\]/) { $TAROPT .= "--exclude='$s' " }
+ else { $TAROPT .= "--exclude=$s " }}
+
+debugfn "tar options",$TAROPT if $TAROPT and $DEBUG;
+
# ==================================================================================== ASK TO PROCEED
proceed;
# ============================================================================ RE/WRITE PACKAGE FILES
@@ -1004,6 +1122,7 @@ print "${CK_}cd .$CD_\n";
chdir $PKG{pwd};
print "can't return back to $PKG{pwd} to run scripts\n" if $nscripts and $PKG{pwd} ne nonl(`pwd`);
+# included "scripts.pl"
# resolve automatic variables in the script command line
our sub resolve {
@@ -1031,12 +1150,16 @@ our sub resolve {
$cmd =~ s/__%__/%/g;
return $cmd }
+# end "scripts.pl"
+
if($nscripts) {
$PKG{filename} = $tar; # needed for %f
cmd resolve($_),$CC_,$CD_ for @{$PKG{always}},@{$PKG{onsubrelease}},@{$PKG{onrelease}}}
# ================================================================================ REWRITE SUBVERSION
+# included "rewrite.pl"
+
{ # SUBVERSION REWRITE
# the SUBVERS(ION) keyword plus whitespaces and begine of line
@@ -1097,6 +1220,7 @@ our sub rewrite_sub {
writefile $PKG{file},$s if -f $PKG{file}; }
} # R.Jaksa 2022 GPLv3
+# end "rewrite.pl"
rewrite_sub if not $NOTMP
and defined $PKG{file} and -f $PKG{file}
@@ -1112,4 +1236,4 @@ print "$CM_$cmd$CD_\n\n$CR_";
system $cmd;
print "$CD_\n";
-# ====================================================================== R.Jaksa 2001,2021,2023 GPLv3
+# ================================================================= R.Jaksa 2001,2021,2023,2024 GPLv3
diff --git a/subrelease.pl b/subrelease.pl
index 45bb61a..64d375f 100644
--- a/subrelease.pl
+++ b/subrelease.pl
@@ -49,9 +49,9 @@
The CG(.subrelease) file from the home directory and from the first
parent directory is used as a config file for the subrelease run.
Syntax is the same as for the CG(VERSION) file (see getversion -h).
- Config-specific keywords are SUFFIX, TGZDIR and keywords ALWAYS,
- ONRELEASE and ONSUBRELEASE to define scripts to run. Possible
- suffixes for the archive files are:
+ Config-specific keywords are SUFFIX, TGZDIR, EXCLUDE and keywords
+ ALWAYS, ONRELEASE and ONSUBRELEASE to define scripts to run.
+ Possible suffixes for the archive files are:
CK(short) CK(full suffix)
CK(+-------------+-----------------------+)
@@ -62,7 +62,7 @@
CK(+-------------+-----------------------+)
rel. CK(arch.) release CK(archive)
- Script keywords usage examples with automatic variables follows:
+ Examples of scripts defintion with automatic variables:
CW(ALWAYS: echo `date -I` %P %f >> /var/log/subrelease.log)
CW(ONSUBRELEASE: ~/util/mknews ./Changelog)
CW(ONRELEASE: scp %f server:/archive/%x/)
@@ -73,6 +73,11 @@
CC(%a) authors, CC(%c) caption, CC(%x) project, CC(%l) language
CC(%%) the % character
+ The EXCLUDE allows to exclude specified files from being archived.
+ Exclude patterns are space separated glob patterns, can be quoted
+ with double quotes:
+ CW(EXCLUDE: .git *.png "copy of *")
+
RECOVERY
In case of error (No space left on device, etc.) the re-packaging
might be stopped in the middle. For recovery check CC(.bkp) files in
@@ -251,6 +256,26 @@
if(defined $PKG{onsubrelease}) { prnt "subrelease script","$CC_$_$CD_" for @{$PKG{onsubrelease}}}
if(defined $PKG{onrelease}) { prnt "release script","$CC_$_$CD_" for @{$PKG{onrelease}}}
+# =========================================================================================== EXCLUDE
+# include exclude.pl
+
+$PKG{exclude} = parse_excluded $PKG{exclude};
+
+# check applicability of exclude commands
+my %valid = valid_excluded $PKG{exclude};
+debugfn "exclude",print_excluded($PKG{exclude},\%valid) if @{$PKG{exclude}} and $DEBUG;
+
+$PKG{exclude} = filter_excluded $PKG{exclude},\%valid;
+prntr "exclude","@{$PKG{exclude}}" if @{$PKG{exclude}} and not $DEBUG;
+
+# additional tar options (to exclude by glob pattern)
+our $TAROPT;
+for my $s (@{$PKG{exclude}}) {
+ if($s=~/[\h\*\?\\]/) { $TAROPT .= "--exclude='$s' " }
+ else { $TAROPT .= "--exclude=$s " }}
+
+debugfn "tar options",$TAROPT if $TAROPT and $DEBUG;
+
# ==================================================================================== ASK TO PROCEED
proceed;
# ============================================================================ RE/WRITE PACKAGE FILES
@@ -337,4 +362,4 @@
system $cmd;
print "$CD_\n";
-# ====================================================================== R.Jaksa 2001,2021,2023 GPLv3
+# ================================================================= R.Jaksa 2001,2021,2023,2024 GPLv3
diff --git a/usr/Makefile b/usr/Makefile
new file mode 100644
index 0000000..ad7b0d0
--- /dev/null
+++ b/usr/Makefile
@@ -0,0 +1,6 @@
+
+all: bin/pcpp
+
+bin/pcpp: /map/pcpp/bin/pcpp
+ cp $< $@
+
diff --git a/usr/bin/pcpp b/usr/bin/pcpp
new file mode 100755
index 0000000..90e7f68
--- /dev/null
+++ b/usr/bin/pcpp
@@ -0,0 +1,495 @@
+#!/usr/bin/perl
+
+$SIGN = "pcpp-0.4a R.Jaksa 2008,2024 GPLv3";
+
+$HELP=<"; # private left brace
+ my $R = "\<\#\#"; # private right brace
+ my %STR; # private substitutions content strings
+ my $id = 0; # last ID
+ sub SBS { return "$L$_[0]$R"; } # return complete private substitution identifier
+
+ # skip commented-out lines
+ $help =~ s/(\n\#.*)*\n/\n/g;
+
+ # add version/copyright section
+# my $built = " $CK_($PKGBUILT)$CD_" if $SUBVERSION ne "none";
+# $help .= "VERSION\n";
+# $help .= " $PKGMSG$built\n\n";
+
+ # escapes
+ $help =~ s/\\\)/SBS "brc2"/eg; # escaped bracket
+
+ # CC(text)
+ my $RE1 = qr/(\((([^()]|(?-3))*)\))/x; # () group, $1=withparens, $2=without
+ $STR{$id++}=$4 while $help =~ s/([^A-Z0-9])(C[$colors])$RE1/$1.SBS("c$2$id")/e;
+
+ # options lists
+ $STR{$id++}=$2 while $help =~ s/(\n[ ]*)(-[a-zA-Z0-9]+(\[?[ =][A-Z]{2,}(x[A-Z]{2,})?\]?)?)([ \t])/$1.SBS("op$id").$5/e;
+
+ # bracketed uppercase words
+ $STR{$id++}="$1$2" while $help =~ s/\[([+-])?([A-Z]+)\]/SBS "br$id"/e;
+
+ # plain uppercase words, like sections headers
+ $STR{$id++}=$2 while $help =~ s/(\n|[ \t])(([A-Z_\/-]+[ ]?){4,})/$1.SBS("pl$id")/e;
+
+ # re-substitute
+ $help =~ s/${L}pl([0-9]+)$R/$CC_$STR{$1}$CD_/g;
+ $help =~ s/${L}op([0-9]+)$R/$CC_$STR{$1}$CD_/g;
+ $help =~ s/${L}br([0-9]+)$R/\[$CC_$STR{$1}$CD_\]/g;
+
+ # CC(text)
+ my %cc; $cc{$_} = ${"C".$_."_"} for split //,$colors;
+ $help =~ s/${L}cC([$colors])([0-9]+)$R/$cc{$1}$STR{$2}$CD_/g;
+
+ # escapes
+ $help =~ s/${L}brc2$R/)/g;
+
+ # star bullets
+ $help =~ s/\n \* /\n $CC_\*$CD_ /g;
+
+ print $help; }
+
+}
+# end "helpman.pl"
+
+# included "dirname.pl"
+# return the dirname from the path
+# ccc/aaa/bbb -> ccc/aaa
+# ccc/aaa/bbb/ -> ccc/aaa
+sub dirname { my $p=$_[0]; $p=~s/\/*$//; $p=~s/\/[^\/]*$//; return $p }
+# end "dirname.pl"
+
+# included "beautify.pl"
+# beautify the path
+# for now just remove the leading "./"
+sub beautify { my $p=$_[0]; $p=~s/^\.\///; return $p }
+# end "beautify.pl"
+
+for(@ARGV) { if($_ eq "-h") { printhelp $HELP; exit 0 }}
+for(@ARGV) { if($_ eq "-v") { $VERBOSE=1; $_=""; last }}
+for(@ARGV) { if($_ eq "-vv") { $VERBOSE=2; $_=""; last }}
+for(@ARGV) { if($_ eq "-nt") { $NOTRIPLE=1; $_=""; last }}
+for(@ARGV) { if($_ eq "-nw") { $NOWATERMARK=1; $_=""; last }}
+for(@ARGV) { if($_ eq "-ni") { $NOIND=1; $_=""; last }}
+for(@ARGV) { if($_ eq "-l") { $LIST=1; $_=""; last }}
+for(@ARGV) { if($_ eq "-l1") { $LIST=2; $_=""; last }}
+for(@ARGV) { if($_ eq "-lp") { $LIST=3; $_=""; last }}
+
+# list of dirs to be excluded
+our @EXCL;
+for(my $i=0;$i<$#ARGV;$i++) {
+ next if $ARGV[$i] ne "-e" or not -d $ARGV[$i+1];
+ push @EXCL,$ARGV[$i+1]; $ARGV[$i]=$ARGV[$i+1]="" }
+
+# prepend "./" if needed
+for(@EXCL) { $_="./$_" if not /^\.\// and not /^\// }
+if($VERBOSE) { pr "${CK_}# exclude$CD_ $CR_$_$CD_\n" for @EXCL }
+
+# input files
+our @FILES; # list of files to be processed
+for(@ARGV) { push @FILES,$_ if $_ ne "" }
+
+# -------------------------------------------- GET LIST OF DIRS WITH INCLUDABLE PERL/C FILES
+our @DIRS; # recursive list of all local dirs
+our %FF; # per-directory list of all perl/c files
+our %NF; # number of files in each dir
+
+# included "mode.pl"
+
+# included "sx.pl"
+# return file suffix
+sub sx { my $s=$_[0]; $s=~s/^.*\.//; return $s }
+# end "sx.pl"
+
+{
+
+# return the file mode by the suffix of filename: pl, py or c
+my sub getbysx {
+ return "pl" if $_[0] eq "pl";
+ return "py" if $_[0] eq "py";
+ return "c" if $_[0] eq "c++";
+ return "c" if $_[0] eq "c"; }
+
+# return the mode by the filename, or return the default perl mode
+our sub getmode {
+ my $sx = sx $_[0];
+ my $mode = getbysx $sx;
+ $mode = "pl" if not $mode;
+ return $mode }
+
+# auto-identify the mode of a list of files by the first identifiable
+# file suffix, or return the default perl mode
+our sub firstmode {
+ my $mode;
+ for(@{$_[0]}) {
+ $mode = getbysx sx $_;
+ last if $mode }
+ $mode = "pl" if not $mode;
+ return $mode }
+
+# return the comment identifier string by the mode: # or //
+our sub getsy {
+ return "//" if $_[0] eq "c";
+ return "#" }
+
+} # R.Jaksa 2024 GPLv3
+# end "mode.pl"
+
+our $MODE = firstmode \@FILES; # input file mode (by 1st file): pl, py or c
+our $SY = getsy $MODE; # comment identifier
+our $SYQ = quotemeta $SY;
+
+# included "scandirs.pl"
+{ # FIND LIST OF DIRS WITH INCLUDABLE PERL/C FILES
+
+# check if filename is includable pl/py/c file
+# according to the MODE and suffix
+my sub isinc {
+ return 1 if not defined $MODE;
+ return 1 if $MODE eq "pl" and sx($_[0]) eq "pl";
+ return 1 if $MODE eq "py" and sx($_[0]) eq "py";
+ return 1 if $MODE eq "c" and sx($_[0]) eq "h";
+ return 0 }
+
+# getsubdirs "."; looks for all dirs with .pl/.h files in "."
+# fills-up @DIRS and %FF,%NF
+our sub getsubdirs {
+ my $dir = $_[0];
+ my @all; opendir(DIR,$dir); @all=readdir(DIR); closedir(DIR);
+ my @ff; for(@all) { push @ff,$_ if isinc $_ } # save .pl/.c filenames
+ if(@ff) { push @DIRS,$dir; $NF{$dir}=@ff; $FF{$dir}=\@ff } # save if nonempty
+ for(@all) {
+ next if /^\./; # skip hidden dirs
+ my $path = "$dir/$_";
+ next if inar \@EXCL,$path; # skip excluded dirs
+ getsubdirs($path) if -d $path; }}
+
+# SORT SEARCH DIRS TO PUT BETTER UP
+# 1st compare by number of slashes - to look in current directory first
+# 2nd compare by number of .pl/.c files in dir - just speculative speedup
+my sub subcompare {
+ my $ca = $a=~tr/\///; # count of / in $a
+ my $cb = $b=~tr/\///; # in $b
+ if ($ca<$cb) { return -1 }
+ elsif($ca>$cb) { return 1 }
+ else {
+ if ($NF{$a}>$NF{$b}) { return -1 }
+ elsif($NF{$a}<$NF{$b}) { return 1 }
+ else { return 0 }}}
+
+# re-sort DIRS
+our sub resort {
+ @DIRS=();
+ push @DIRS,$_ for sort subcompare keys %NF }
+
+} # R.Jaksa 2024 GPLv3
+# end "scandirs.pl"
+
+getsubdirs "."; # fill-up DIRS, FF and NF
+resort(); # re-sort DIRS
+
+if($VERBOSE>1) {
+ my $l = $L1+3;
+ for my $d (@DIRS) {
+ pf "$CK_# search $CG_%-${l}s$CD_",$d;
+ pr " $CK_$_$CD_" for @{$FF{$d}};
+ pr "\n" }}
+
+# ------------------------------------------------------------------------- PROCESS INCLUDES
+our @INCLUDED; # list of already included files (to disable double include)
+# TODO: actually, double include can be useful when including inside blocks!
+
+# included "include.pl"
+{ # ------------------------------------ RESOLVE INCLUDE DIRECTIVES AND ASSEMBLE FULL OUTPUT
+
+# verbose printout of includes, globals: $L1, $L2, $level, $how
+my sub report {
+ my $path=$_[1];
+ my $c1=$_[0];
+ $c1=$CG_ if $c1 eq $CC_ and defined $path and $path eq $file;
+ my $c2=$c1;
+ $c2=$CG_ if $c1 ne $CK_ and $c1 ne $CM_ and defined $path;
+ my $sp = " " x ($level-1);
+
+ # cell-lengths logic
+ my $sl = length($sp); # just the L1-space length
+ my $ll = length($file)+$sl; # whole L1 length
+ my $hl = length($how); # whole L2 length
+ my $l1 = $L1-$sl; # space-corrected L1 length
+ $l1-= $hl-$L2 if $hl>$L2; # make space for L2 is L1 if needed and possible
+ my $l2 = $L2; #
+ $l2-= $ll-$L1 if $ll>$L1; # move L2 left if possible (L1 space available)
+
+ if($VERBOSE) {
+ pf "$CK_$SY include $sp$c1%-*s$CD_ $CK_%*s$CD_",$l1,$file,$l2,"$how";
+ pr " $c2$path$CD_" if defined $path and $path ne $file;
+ pr "\n" }
+ if($LIST and $c1 ne $CK_ and $how ne "missing" and $level>=1) {
+ if ($LIST==3) { print "$sp$path\n" }
+ elsif($LIST==2) { print "$sp$file\n" if $level==1 }
+ else { print "$sp$file\n" }}}
+
+# ------------------------------------------------------------------------------------- MAIN
+
+# line by line add a file to the output, parse #include directives
+our sub addfile {
+ local $file=$_[0];
+ my $rdir=$_[1]; # current relative subdir
+ local $level=$_[2]; # recursion level
+ my $indent=$_[3]; # requested additional indentation space for includes
+ my $ok=0; # 1=alreadyincluded 2=speculativepath 3=filefound
+ my $path; # full path (to be found)
+ local $how; # verbose: how was the path found
+
+ $level=0 if not defined $level; # start level zero
+ $indent="" if not defined $indent;
+
+ # look for file in CWD using direct path if no recursion yet
+ if(not $level) {
+ $path = $file; # try direct explicit path
+ if(inar \@INCLUDED,$path) { $ok=1 } # skip already included
+ elsif(-f $path) { $ok=3 }
+ $how = "direct" if $ok }
+
+ # look for file using explicit path relative to parent-file dir
+ if(not $ok) {
+ $path = "$rdir/$file"; # try path relative to parent
+ if(inar \@INCLUDED,$path) { $ok=1 } # already included
+ elsif(-f $path) { $ok=3 }
+ $how = "relative" if $ok }
+
+ # look for file recursively (by filename)
+ if(not $ok) {
+ my $fn=$file; $fn=~s/^.*\/// if $fn=~/\//; # strip the explicit dir
+ my $dir; for(@DIRS) { # loop through dirs
+ $dir = $_;
+ $path = "$dir/$fn"; # try path relative to every dir
+ $ok=1 and last if inar \@INCLUDED,$path; # already included
+ $ok=3 and last if inar $FF{$dir},$fn } # found => proceed
+ $how = "found" if $ok;
+ if($ok==3 and $fn ne $file) { # if file contained dirname
+ my $fd = quotemeta "/".dirname($file); # directory part of the include name
+ $ok=2 if not $dir =~ /$fd$/ } # is speculative
+ $how = "guess" if $ok==2 }
+
+ # otherwise missing
+ if(not $ok) { $how = "missing" }
+
+ # verbose/list
+ if ($ok==1) { report $CK_,beautify($path) } # double include
+ elsif($ok==2) { report $CM_,beautify($path) } # speculative
+ elsif($ok==3) { report $CC_,beautify($path) } # OK
+ else { report $CR_,$file } # not found
+
+ return if $ok==0; # file not found
+ return if $ok==1; # file already included (TODO: accept if requested, but avoid recursion)
+ push @INCLUDED,$path; # register file
+ $rdir = dirname $path if $ok; # save for the explicit path lookup in next recursion
+
+ # filename regexes
+ my $IN1 = qr/^\h*\"([^\"]+)\"/; # quoted include
+ my $IN2; # unquoted include
+ $IN2 = qr/^\h*([a-zA-Z0-9\._-]+\.pl)/ if $MODE eq "pl";
+ $IN2 = qr/^\h*([a-zA-Z0-9\._-]+\.py)/ if $MODE eq "py";
+ $IN2 = qr/^\h*([a-zA-Z0-9\._-]+\.h)/ if $MODE eq "c";
+
+ my @output; # the output line-by-line
+
+ # watermark for new file, at the included-file indentation level
+ if(not $NOWATERMARK and $level) {
+ my $ind=$indent; $ind="" if $NOIND;
+ push @output,"$SY:SEP\n";
+ push @output,"$ind$SY included \"$file\"\n" } # TODO: also comment
+
+ # read cuurent file and recursively resolve include directives
+ for my $line (split /\n/,`cat $path`) { # <- we read files here!
+ if($line =~ /^(\h*)$SYQ\h*include\h+(.*?)$/) { # identify include line
+ my $ind=$1; my $s=$2; my $OK=0;
+ while($s=~s/$IN1// or $s=~s/$IN2//) { # parse it
+ push @output,addfile($1,$rdir,$level+1,$ind); $OK=1 } # recurse inside
+ next if $OK } # go for the next line
+ if($indent and not $NOIND and not $line=~/^\h*$/) { $line="$indent$line" } # indentation
+ push @output,"$line\n"; } # <- add regular lines here!
+
+ # watermark footer
+ if(not $NOWATERMARK and$level) {
+ my $ind=$indent; $ind="" if $NOIND;
+ push @output,"$ind$SY end \"$file\"\n";
+ push @output,"$SY:SEP\n" }
+
+ return @output; }
+
+} # ---------------------------------------------------------------- R.Jaksa 2023,2024 GPLv3
+# end "include.pl"
+
+# auxiliary output buffer, as the include recursion would break simple print to stdout,
+# we print to the @output buffer instead and only at the end to the stdout
+my @output;
+
+# TODO: header with timestamp and list of inputs
+# TODO: #! interpreter identifier
+
+# add each argv file to the output
+push @output,addfile($_) for @FILES;
+
+# skip the rest in the list mode
+exit if $LIST;
+
+if(1) {
+# remove tripled comments
+if(not $NOTRIPLE) {
+ for my $i (0..$#output) {
+ if(($MODE eq "c" and $output[$i]=~/^\h*\/\/\/[^\/]/) or $output[$i]=~/^\h*\#\#\#[^\#]/) {
+ $output[$i] = "$SY:DEL $output[$i]";
+ my $j=$i-1; while($j>=0 and $output[$j]=~/^\h*$/) {
+ $output[$j--] = "$SY:DEL\n" }}}}
+
+# multiple :SEP tags
+for my $i (0..$#output-1) {
+ $output[$i]="$SY:DEL\n" if $output[$i]=~/^$SYQ:SEP/ and $output[$i+1]=~/^$SYQ:SEP/ }
+
+# surviving :SEP tags, skip if previous/next line is empty, otherwise add new empty line
+for my $i (0..$#output) {
+ next if not $output[$i] =~ /^$SYQ:SEP/;
+ if($i>0 and $output[$i-1] =~ /^\h*$/) { $output[$i] = "$SY:DEL\n" }
+ elsif($i<$#output and $output[$i+1] =~ /^\h*$/) { $output[$i] = "$SY:DEL\n" }
+ else { $output[$i] = "\n" }}
+
+}
+# :DEL tags and assembly of the final output string
+my $out;
+for my $i (0..$#output) {
+ if($output[$i] =~ /^$SYQ:DEL/) { next }
+ $out .= $output[$i] }
+
+# emit the output
+print $out;
+
+# ------------------------------------------------------------------ R.Jaksa 2000,2024 GPLv3