2 # Desc: Compresses, encrypts, and writes stdin every 5 seconds 
   4 #==BEGIN Define script parameters== 
   5 #===BEGIN Initialize variables=== 
   7 # Logging Behavior parameters 
   8 bufferTTL
="300"; # Time-to-live (seconds) for each buffer round 
   9 scriptTTL_TE
="day"; # Time element at the end of which script terminates 
  10 dirTmpDefault
="/dev/shm"; # Default parent of working directory 
  13 scriptName
="bklog";             # Define basename of script file. 
  14 scriptVersion
="0.1.21";          # Define version of script. 
  15 scriptURL
="https://gitlab.com/baltakatei/ninfacyzga-01"; # Define wesite hosting this script. 
  16 scriptTimeStart
="$(date +%Y%m%dT%H%M%S.%N)"; # YYYYmmddTHHMMSS.NNNNNNNNN 
  17 scriptHostname
=$
(hostname
);     # Save hostname of system running this script. 
  18 PATH
="$HOME/.local/bin:$PATH";  # Add "$(systemd-path user-binaries)" path in case user apps saved there 
  19 ageVersion
="1.0.0-beta2";       # Define version of age (encryption program) 
  20 ageURL
="https://github.com/FiloSottile/age/releases/tag/v1.0.0-beta2"; # Define website hosting age. 
  23 declare -a buffer          
# array for storing while read buffer 
  24 declare -a argRecPubKeys   
# array for processArguments function 
  25 declare -a recPubKeysValid 
# array for storing both '-r' and '-R' recipient pubkeys 
  26 declare -a argProcStrings argProcFileExts 
# for storing buffer processing strings (ex: "gpsbabel -i nmea -f - -o gpx -F - ") 
  27 declare -Ag appRollCall    
# Associative array for storing app status 
  28 declare -Ag fileRollCall   
# Associative array for storing file status 
  29 declare -Ag dirRollCall    
# Associative array for storing dir status 
  30 declare -a procStrings procFileExts 
# Arrays for storing processing commands and resulting output file extensions 
  33 optionVerbose
=""; optionEncrypt
=""; dirOut
=""; optionEncrypt
=""; dir_tmp
=""; 
  34 cmd_compress
="";cmd_compress_suffix
=""; cmd_encrypt
=""; cmd_encrypt_suffix
=""; 
  36 #===END Initialize variables=== 
  38 #===BEGIN Declare local script functions=== 
  39 yell
() { echo "$0: $*" >&2; }      #o Yell, Die, Try Three-Fingered Claw technique 
  40 die
() { yell 
"$*"; exit 111; }     #o Ref/Attrib: https://stackoverflow.com/a/25515370 
  41 try
() { "$@" || die 
"cannot $*"; } #o 
  43     while [ ! $# -eq 0 ]; do   # While number of arguments ($#) is not (!) equal to (-eq) zero (0). 
  45             -v | 
--verbose) optionVerbose
="true"; vbm 
"DEBUG :Verbose mode enabled.";; # Enable verbose mode. 
  46             -h | 
--help) showUsage
; exit 1;; # Display usage. 
  47             --version) showVersion
; exit 1;; # Show version 
  48             -o | 
--output) if [ -d "$2" ]; then dirOut
="$2"; vbm 
"DEBUG :dirOut:$dirOut"; shift; fi ;; # Define output directory. 
  49             -e | 
--encrypt) optionEncrypt
="true"; vbm 
"DEBUG :Encrypted output mode enabled.";; # Enable encryption 
  50             -r | 
--recipient) optionRecArg
="true"; argRecPubKeys
+=("$2"); vbm 
"STATUS:pubkey added:""$2"; shift;; # Add recipients 
  51             -R | 
--recipient-dir) optionRecDir
="true" && argRecDir
="$2"; shift;; # Add recipient watch dir 
  52             -c | 
--compress) optionCompress
="true"; vbm 
"DEBUG :Compressed output mode enabled.";; # Enable compression 
  53             -z | 
--time-zone) try setTimeZoneEV 
"$2"; shift;; # Set timestamp timezone 
  54             -t | 
--temp-dir) optionTmpDir
="true" && argTempDirPriority
="$2"; shift;; # Set time zone 
  55             -b | 
--buffer-ttl) optionCustomBufferTTL
="true" && argCustomBufferTTL
="$2"; shift;; # Set custom buffer period (default: 300 seconds) 
  56             -B | 
--script-ttl) optionCustomScriptTTL_TE
="true" && argCustomScriptTTL_TE
="$2"; shift;; # Set custom script TTL (default: "day") 
  57             -p | 
--process-string) optionProcString
="true" && argProcStrings
+=("$2") && argProcFileExts
+=("$3") && vbm 
"STATUS:file extension \"$2\" for output of processing string added:\"$3\""; shift; shift;; 
  58             -l | 
--label) optionLabel
="true" && argLabel
="$2"; vbm 
"DEBUG :Custom label received:$argLabel"; shift;; 
  59             -w | 
--store-raw) optionStoreRaw
="true" && argRawFileExt
="$2"; vbm 
"DEBUG :Raw stdin file extension received:$argRawFileExt"; shift;; 
  60             -W | 
--no-store-raw) optionNoStoreRaw
="true"; vbm 
"DEBUG :Option selected to not store raw stdin data."; shift;; 
  61             *) yell 
"ERROR: Unrecognized argument: $1"; yell 
"STATUS:All arguments:$*"; exit 1;; # Handle unrecognized options. 
  65 } # Argument Processing 
  67     # Description: Prints verbose message ("vbm") to stderr if optionVerbose is set to "true". 
  68     # Usage: vbm "DEBUG :verbose message here" 
  73     # Depends: bash 5.0.3, echo 8.30, date 8.30 
  75     if [ "$optionVerbose" = "true" ]; then 
  76         functionTime
=$
(date --iso-8601=ns
); # Save current time in nano seconds. 
  77         echo "[$functionTime] ""$*" 1>&2; # Display argument text. 
  81     return 0; # Function finished. 
  82 } # Displays message if optionVerbose true 
  84     # Desc: If arg is a command, save result in assoc array 'appRollCall' 
  85     # Usage: checkapp arg1 arg2 arg3 ... 
  87     # Input: global assoc. array 'appRollCall' 
  88     # Output: adds/updates key(value) to global assoc array 'appRollCall' 
  94         if command -v "$arg" 1>/dev
/null 
2>&1; then # Check if arg is a valid command 
  95             appRollCall
[$arg]="true"; 
  96             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi; 
  98             appRollCall
[$arg]="false"; returnState
="false"; 
 102     #===Determine function return code=== 
 103     if [ "$returnState" = "true" ]; then 
 108 } # Check that app exists 
 110     # Desc: If arg is a file path, save result in assoc array 'fileRollCall' 
 111     # Usage: checkfile arg1 arg2 arg3 ... 
 113     # Input: global assoc. array 'fileRollCall' 
 114     # Output: adds/updates key(value) to global assoc array 'fileRollCall'; 
 115     # Output: returns 0 if app found, 1 otherwise 
 116     # Depends: bash 5.0.3 
 121         if [ -f "$arg" ]; then 
 122             fileRollCall
["$arg"]="true"; 
 123             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi; 
 125             fileRollCall
["$arg"]="false"; returnState
="false"; 
 129     #===Determine function return code=== 
 130     if [ "$returnState" = "true" ]; then 
 135 } # Check that file exists 
 137     # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' 
 138     # Usage: checkdir arg1 arg2 arg3 ... 
 140     # Input: global assoc. array 'dirRollCall' 
 141     # Output: adds/updates key(value) to global assoc array 'dirRollCall'; 
 142     # Output: returns 0 if app found, 1 otherwise 
 143     # Depends: Bash 5.0.3 
 148         if [ -d "$arg" ]; then 
 149             dirRollCall
["$arg"]="true"; 
 150             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi 
 152             dirRollCall
["$arg"]="false"; returnState
="false"; 
 156     #===Determine function return code=== 
 157     if [ "$returnState" = "true" ]; then 
 162 } # Check that dir exists 
 164     # Desc: Displays missing apps, files, and dirs 
 165     # Usage: displayMissing 
 167     # Input: associative arrays: appRollCall, fileRollCall, dirRollCall 
 168     # Output: stderr: messages indicating missing apps, file, or dirs 
 169     # Depends: bash 5, checkAppFileDir() 
 170     local missingApps value appMissing missingFiles fileMissing
 
 171     local missingDirs dirMissing
 
 173     #==BEGIN Display errors== 
 174     #===BEGIN Display Missing Apps=== 
 175     missingApps
="Missing apps  :"; 
 176     #for key in "${!appRollCall[@]}"; do echo "DEBUG :$key => ${appRollCall[$key]}"; done 
 177     for key 
in "${!appRollCall[@]}"; do 
 178         value
="${appRollCall[$key]}"; 
 179         if [ "$value" = "false" ]; then 
 180             #echo "DEBUG :Missing apps: $key => $value"; 
 181             missingApps
="$missingApps""$key "; 
 185     if [ "$appMissing" = "true" ]; then  # Only indicate if an app is missing. 
 186         echo "$missingApps" 1>&2; 
 189     #===END Display Missing Apps=== 
 191     #===BEGIN Display Missing Files=== 
 192     missingFiles
="Missing files:"; 
 193     #for key in "${!fileRollCall[@]}"; do echo "DEBUG :$key => ${fileRollCall[$key]}"; done 
 194     for key 
in "${!fileRollCall[@]}"; do 
 195         value
="${fileRollCall[$key]}"; 
 196         if [ "$value" = "false" ]; then 
 197             #echo "DEBUG :Missing files: $key => $value"; 
 198             missingFiles
="$missingFiles""$key "; 
 202     if [ "$fileMissing" = "true" ]; then  # Only indicate if an app is missing. 
 203         echo "$missingFiles" 1>&2; 
 206     #===END Display Missing Files=== 
 208     #===BEGIN Display Missing Directories=== 
 209     missingDirs
="Missing dirs:"; 
 210     #for key in "${!dirRollCall[@]}"; do echo "DEBUG :$key => ${dirRollCall[$key]}"; done 
 211     for key 
in "${!dirRollCall[@]}"; do 
 212         value
="${dirRollCall[$key]}"; 
 213         if [ "$value" = "false" ]; then 
 214             #echo "DEBUG :Missing dirs: $key => $value"; 
 215             missingDirs
="$missingDirs""$key "; 
 219     if [ "$dirMissing" = "true" ]; then  # Only indicate if an dir is missing. 
 220         echo "$missingDirs" 1>&2; 
 223     #===END Display Missing Directories=== 
 225     #==END Display errors== 
 226 } # Display missing apps, files, dirs 
 229     # Desc: Writes first argument to temporary file with arguments as options, then appends file to tar 
 230     # Usage: appendArgTar "$(echo "Data to be written.")" [name of file to be inserted] [tar path] [temp dir] ([cmd1] [cmd2] [cmd3] [cmd4]...) 
 232     # Input: arg1: data to be written 
 233     #        arg2: file name of file to be inserted into tar 
 234     #        arg3: tar archive path (must exist first) 
 235     #        arg4: temporary working dir 
 236     #        arg5+: command strings (ex: "gpsbabel -i nmea -f - -o kml -F - ") 
 237     # Output: file written to disk 
 238     # Example: decrypt multiple large files in parallel 
 239     #          appendArgTar "$(cat /tmp/largefile1.gpg)" "largefile1" $HOME/archive.tar /tmp "gpg --decrypt" & 
 240     #          appendArgTar "$(cat /tmp/largefile2.gpg)" "largefile2" $HOME/archive.tar /tmp "gpg --decrypt" & 
 241     #          appendArgTar "$(cat /tmp/largefile3.gpg)" "largefile3" $HOME/archive.tar /tmp "gpg --decrypt" & 
 242     # Depends: bash 5, tar 1, yell() 
 243     # Ref/Attrib: Using 'eval' to construct command strings https://askubuntu.com/a/476533 
 245     local fn fileName tarPath tmpDir cmd0 cmd1 cmd2 cmd3 cmd4
 
 249     #yell "DEBUG:STATUS:$fn:Finished appendArgTar()." 
 252     if ! [ -z "$2" ]; then fileName
="$2"; else yell 
"ERROR:$fn:Not enough arguments."; exit 1; fi 
 254     # Check tar path is a file 
 255     if [ -f "$3" ]; then tarPath
="$3"; else yell 
"ERROR:$fn:Tar archive arg not a file."; exit 1; fi 
 258     if ! [ -z "$4" ]; then tmpDir
="$4"; else yell 
"ERROR:$fn:No temporary working dir set."; exit 1; fi 
 260     # Set command strings 
 261     if ! [ -z "$5" ]; then cmd1
="$5"; else cmd1
="cat "; fi # command string 1 
 262     if ! [ -z "$6" ]; then cmd2
="$6"; else cmd2
="cat "; fi # command string 2 
 263     if ! [ -z "$7" ]; then cmd3
="$7"; else cmd3
="cat "; fi # command string 3 
 264     if ! [ -z "$8" ]; then cmd4
="$8"; else cmd4
="cat "; fi # command string 4 
 270     # yell "DEBUG:STATUS:$fn:cmd0:$cmd0" 
 271     # yell "DEBUG:STATUS:$fn:cmd1:$cmd1" 
 272     # yell "DEBUG:STATUS:$fn:cmd2:$cmd2" 
 273     # yell "DEBUG:STATUS:$fn:cmd3:$cmd3" 
 274     # yell "DEBUG:STATUS:$fn:cmd4:$cmd4" 
 275     # yell "DEBUG:STATUS:$fn:fileName:$fileName" 
 276     # yell "DEBUG:STATUS:$fn:tarPath:$tarPath" 
 277     # yell "DEBUG:STATUS:$fn:tmpDir:$tmpDir" 
 279     # Write to temporary working dir 
 280     eval "$cmd0 | $cmd1 | $cmd2 | $cmd3 | $cmd4" > "$tmpDir"/"$fileName"; 
 283     try 
tar --append --directory="$tmpDir" --file="$tarPath" "$fileName"; 
 284     #yell "DEBUG:STATUS:$fn:Finished appendArgTar()." 
 285 } # Append Bash var to file appended to Tar archive 
 287     # Desc: Appends [processed] file to tar 
 288     # Usage: appendFileTar [file path] [name of file to be inserted] [tar path] [temp dir] ([process cmd]) 
 290     # Input: arg1: path of file to be (processed and) written 
 291     #        arg2: name to use for file inserted into tar 
 292     #        arg3: tar archive path (must exist first) 
 293     #        arg4: temporary working dir 
 294     #        arg5: (optional) command string to process file (ex: "gpsbabel -i nmea -f - -o kml -F - ") 
 295     # Output: file written to disk 
 296     # Example: decrypt multiple large files in parallel 
 297     #          appendFileTar /tmp/largefile1.gpg "largefile1" $HOME/archive.tar /tmp "gpg --decrypt" & 
 298     #          appendFileTar /tmp/largefile2.gpg "largefile2" $HOME/archive.tar /tmp "gpg --decrypt" & 
 299     #          appendFileTar /tmp/largefile3.gpg "largefile3" $HOME/archive.tar /tmp "gpg --decrypt" & 
 300     # Depends: bash 5.0.3, tar 1.30, cat 8.30, yell() 
 301     local fn fileName tarPath tmpDir
 
 305     #yell "DEBUG :STATUS:$fn:Started appendFileTar()." 
 308     if ! [ -z "$2" ]; then fileName
="$2"; else yell 
"ERROR:$fn:Not enough arguments."; exit 1; fi 
 309     # Check tar path is a file 
 310     if [ -f "$3" ]; then tarPath
="$3"; else yell 
"ERROR:$fn:Tar archive arg not a file:$3"; exit 1; fi 
 312     if ! [ -z "$4" ]; then tmpDir
="$4"; else yell 
"ERROR:$fn:No temporary working dir set."; exit 1; fi 
 313     # Set command strings 
 314     if ! [ -z "$5" ]; then cmd1
="$5"; else cmd1
="cat "; fi # command string 
 316     # Input command string 
 319     # Write to temporary working dir 
 320     eval "$cmd0 | $cmd1" > "$tmpDir"/"$fileName"; 
 323     try 
tar --append --directory="$tmpDir" --file="$tarPath" "$fileName"; 
 324     #yell "DEBUG :STATUS:$fn:Finished appendFileTar()." 
 325 } # Append [processed] file to Tar archive 
 327     # Desc: Checks if string is an age-compatible pubkey 
 328     # Usage: checkAgePubkey [str pubkey] 
 330     # Input: arg1: string 
 331     # Output: return code 0: string is age-compatible pubkey 
 332     #         return code 1: string is NOT an age-compatible pubkey 
 333     #         age stderr (ex: there is stderr if invalid string provided) 
 334     # Depends: age (v0.1.0-beta2; https://github.com/FiloSottile/age/releases/tag/v1.0.0-beta2 ) 
 338     if echo "test" | age 
-a -r "$argPubkey" 1>/dev
/null
; then 
 345     # Desc: Checks that a valid tar archive exists, creates one otherwise 
 346     # Usage: checkMakeTar [ path ] 
 348     # Input: arg1: path of tar archive 
 349     # Output: exit code 0 : tar readable 
 350     #         exit code 1 : tar missing; created 
 351     #         exit code 2 : tar not readable; moved; replaced 
 352     # Depends: bash 5, date 8, tar 1, try() 
 353     local pathTar returnFlag0 returnFlag1 returnFlag2
 
 356     # Check if file is a valid tar archive 
 357     if tar --list --file="$pathTar" 1>/dev
/null 
2>&1; then 
 358         ## T1: return success 
 359         returnFlag0
="tar valid"; 
 361         ## F1: Check if file exists 
 362         if [[ -f "$pathTar" ]]; then 
 364             try 
mv "$pathTar" "$pathTar""--broken--""$(date +%Y%m%dT%H%M%S)" && \
 
 365                 returnFlag1
="tar moved"; 
 370         ## F2: Create tar archive, return 0 
 371         try 
tar --create --file="$pathTar" --files-from=/dev
/null 
&& \
 
 372             returnFlag2
="tar created"; 
 375     # Determine function return code 
 376     if [[ "$returnFlag0" = "tar valid" ]]; then 
 378     elif [[ "$returnFlag2" = "tar created" ]] && ! [[ "$returnFlag1" = "tar moved" ]]; then 
 379         return 1; # tar missing so created 
 380     elif [[ "$returnFlag2" = "tar created" ]] && [[ "$returnFlag1" = "tar moved" ]]; then 
 381         return 2; # tar not readable so moved; replaced 
 383 } # checks if arg1 is tar; creates one otherwise 
 385     # Desc: Date without separators (YYYYmmdd) 
 386     # Usage: dateShort ([str date]) 
 388     # Input: arg1: 'date'-parsable timestamp string (optional) 
 389     # Output: stdout: date (ISO-8601, no separators) 
 390     # Depends: bash 5.0.3, date 8.30, yell() 
 391     local argTime timeCurrent timeInput dateCurrentShort
 
 395     timeCurrent
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second. 
 396     # Decide to parse current or supplied date 
 397     ## Check if time argument empty 
 398     if [[ -z "$argTime" ]]; then 
 399         ## T: Time argument empty, use current time 
 400         timeInput
="$timeCurrent"; 
 402         ## F: Time argument exists, validate time 
 403         if date --date="$argTime" 1>/dev
/null 
2>&1; then 
 404             ### T: Time argument is valid; use it 
 405             timeInput
="$argTime"; 
 407             ### F: Time argument not valid; exit 
 408             yell 
"ERROR:Invalid time argument supplied. Exiting."; exit 1; 
 411     # Construct and deliver separator-les date string     
 412     dateCurrentShort
="$(date -d "$timeInput" +%Y%m%d)"; # Produce separator-less current date with resolution 1 day. 
 413     echo "$dateCurrentShort"; 
 416     # Desc: Timestamp without separators (YYYYmmddTHHMMSS+zzzz) 
 417     # Usage: dateTimeShort ([str date]) 
 419     # Input: arg1: 'date'-parsable timestamp string (optional) 
 420     # Output: stdout: timestamp (ISO-8601, no separators) 
 422     local argTime timeCurrent timeInput timeCurrentShort
 
 426     timeCurrent
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second. 
 427     # Decide to parse current or supplied date 
 428     ## Check if time argument empty 
 429     if [[ -z "$argTime" ]]; then 
 430         ## T: Time argument empty, use current time 
 431         timeInput
="$timeCurrent"; 
 433         ## F: Time argument exists, validate time 
 434         if date --date="$argTime" 1>/dev
/null 
2>&1; then 
 435             ### T: Time argument is valid; use it 
 436             timeInput
="$argTime"; 
 438             ### F: Time argument not valid; exit 
 439             yell 
"ERROR:Invalid time argument supplied. Exiting."; exit 1; 
 442     # Construct and deliver separator-les date string 
 443     timeCurrentShort
="$(date -d "$timeInput" +%Y%m%dT%H%M%S%z)"; 
 444     echo "$timeCurrentShort"; 
 445 } # Get YYYYmmddTHHMMSS±zzzz 
 447     # Desc: Set time zone environment variable TZ 
 448     # Usage: setTimeZoneEV arg1 
 450     # Input: arg1: 'date'-compatible timezone string (ex: "America/New_York") 
 451     #        TZDIR env var (optional; default: "/usr/share/zoneinfo") 
 453     #         exit code 0 on success 
 454     #         exit code 1 on incorrect number of arguments 
 455     #         exit code 2 if unable to validate arg1 
 456     # Depends: yell(), printenv 8.30, bash 5.0.3 
 457     # Tested on: Debian 10 
 458     local tzDir returnState argTimeZone
 
 461     if ! [[ $# -eq 1 ]]; then 
 462         yell 
"ERROR:Invalid argument count."; 
 466     # Read TZDIR env var if available 
 467     if printenv TZDIR 
1>/dev
/null 
2>&1; then 
 468         tzDir
="$(printenv TZDIR)"; 
 470         tzDir
="/usr/share/zoneinfo"; 
 474     if ! [[ -f "$tzDir"/"$argTimeZone" ]]; then 
 475         yell 
"ERROR:Invalid time zone argument."; 
 478     # Export ARG1 as TZ environment variable 
 479         TZ
="$argTimeZone" && export TZ 
&& returnState
="true"; 
 482     # Determine function return code 
 483     if [ "$returnState" = "true" ]; then 
 486 } # Exports TZ environment variable 
 488     yell 
"$scriptVersion" 
 489 } # Display script version. 
 491     # Desc: Given seconds, output ISO-8601 duration string 
 492     # Ref/Attrib: ISO-8601:2004(E), §4.4.4.2 Representations of time intervals by duration and context information 
 493     # Note: "1 month" ("P1M") is assumed to be "30 days" (see ISO-8601:2004(E), §2.2.1.2) 
 494     # Usage: timeDuration [1:seconds] ([2:precision]) 
 496     # Input: arg1: seconds as base 10 integer >= 0  (ex: 3601) 
 497     #        arg2: precision level (optional; default=2) 
 498     # Output: stdout: ISO-8601 duration string (ex: "P1H1S", "P2Y10M15DT10H30M20S") 
 499     #         exit code 0: success 
 500     #         exit code 1: error_input 
 501     #         exit code 2: error_unknown 
 502     # Example: 'timeDuration 111111 3' yields 'P1DT6H51M' 
 503     # Depends: date 8, bash 5, yell, 
 504     local argSeconds argPrecision precision returnState remainder
 
 505     local fullYears fullMonths fullDays fullHours fullMinutes fullSeconds
 
 506     local hasYears hasMonths hasDays hasHours hasMinutes hasSeconds
 
 507     local witherPrecision output
 
 508     local displayYears displayMonths displayDays displayHours displayMinutes displaySeconds
 
 510     argSeconds
="$1"; # read arg1 (seconds) 
 511     argPrecision
="$2"; # read arg2 (precision) 
 512     precision
=2; # set default precision 
 514     # Check that between one and two arguments is supplied 
 515     if ! { [[ $# -ge 1 ]] && [[ $# -le 2 ]]; }; then 
 516         yell 
"ERROR:Invalid number of arguments:$# . Exiting."; 
 517         returnState
="error_input"; fi 
 519     # Check that argSeconds provided 
 520     if [[ $# -ge 1 ]]; then 
 521         ## Check that argSeconds is a positive integer 
 522         if [[ "$argSeconds" =~ ^
[[:digit
:]]+$ 
]]; then 
 525             yell 
"ERROR:argSeconds not a digit."; 
 526             returnState
="error_input"; 
 529         yell 
"ERROR:No argument provided. Exiting."; 
 533     # Consider whether argPrecision was provided 
 534     if  [[ $# -eq 2 ]]; then 
 535         # Check that argPrecision is a positive integer 
 536         if [[ "$argPrecision" =~ ^
[[:digit
:]]+$ 
]] && [[ "$argPrecision" -gt 0 ]]; then 
 537         precision
="$argPrecision"; 
 539             yell 
"ERROR:argPrecision not a positive integer. (is $argPrecision ). Leaving early."; 
 540             returnState
="error_input"; 
 546     remainder
="$argSeconds" ; # seconds 
 547     ## Calculate full years Y, update remainder 
 548     fullYears
=$
(( remainder 
/ (365*24*60*60) )); 
 549     remainder
=$
(( remainder 
- (fullYears
*365*24*60*60) )); 
 550     ## Calculate full months M, update remainder 
 551     fullMonths
=$
(( remainder 
/ (30*24*60*60) )); 
 552     remainder
=$
(( remainder 
- (fullMonths
*30*24*60*60) )); 
 553     ## Calculate full days D, update remainder 
 554     fullDays
=$
(( remainder 
/ (24*60*60) )); 
 555     remainder
=$
(( remainder 
- (fullDays
*24*60*60) )); 
 556     ## Calculate full hours H, update remainder 
 557     fullHours
=$
(( remainder 
/ (60*60) )); 
 558     remainder
=$
(( remainder 
- (fullHours
*60*60) )); 
 559     ## Calculate full minutes M, update remainder 
 560     fullMinutes
=$
(( remainder 
/ (60) )); 
 561     remainder
=$
(( remainder 
- (fullMinutes
*60) )); 
 562     ## Calculate full seconds S, update remainder 
 563     fullSeconds
=$
(( remainder 
/ (1) )); 
 564     remainder
=$
(( remainder 
- (remainder
*1) )); 
 565     ## Check which fields filled 
 566     if [[ $fullYears -gt 0 ]]; then hasYears
="true"; else hasYears
="false"; fi 
 567     if [[ $fullMonths -gt 0 ]]; then hasMonths
="true"; else hasMonths
="false"; fi 
 568     if [[ $fullDays -gt 0 ]]; then hasDays
="true"; else hasDays
="false"; fi 
 569     if [[ $fullHours -gt 0 ]]; then hasHours
="true"; else hasHours
="false"; fi 
 570     if [[ $fullMinutes -gt 0 ]]; then hasMinutes
="true"; else hasMinutes
="false"; fi 
 571     if [[ $fullSeconds -gt 0 ]]; then hasSeconds
="true"; else hasSeconds
="false"; fi 
 573     ## Determine which fields to display (see ISO-8601:2004 §4.4.3.2) 
 574     witherPrecision
="false" 
 577     if $hasYears && [[ $precision -gt 0 ]]; then 
 579         witherPrecision
="true"; 
 581         displayYears
="false"; 
 583     if $witherPrecision; then ((precision--
)); fi; 
 586     if $hasMonths && [[ $precision -gt 0 ]]; then 
 587         displayMonths
="true"; 
 588         witherPrecision
="true"; 
 590         displayMonths
="false"; 
 592     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 593         displayMonths
="true"; 
 595     if $witherPrecision; then ((precision--
)); fi; 
 598     if $hasDays && [[ $precision -gt 0 ]]; then 
 600         witherPrecision
="true"; 
 604     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 607     if $witherPrecision; then ((precision--
)); fi; 
 610     if $hasHours && [[ $precision -gt 0 ]]; then 
 612         witherPrecision
="true"; 
 614         displayHours
="false"; 
 616     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 619     if $witherPrecision; then ((precision--
)); fi; 
 622     if $hasMinutes && [[ $precision -gt 0 ]]; then 
 623         displayMinutes
="true"; 
 624         witherPrecision
="true"; 
 626         displayMinutes
="false"; 
 628     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 629         displayMinutes
="true"; 
 631     if $witherPrecision; then ((precision--
)); fi; 
 635     if $hasSeconds && [[ $precision -gt 0 ]]; then 
 636         displaySeconds
="true"; 
 637         witherPrecision
="true"; 
 639         displaySeconds
="false"; 
 641     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 642         displaySeconds
="true"; 
 644     if $witherPrecision; then ((precision--
)); fi; 
 646     ## Determine whether or not the "T" separator is needed to separate date and time elements 
 647     if ( $displayHours || 
$displayMinutes || 
$displaySeconds); then 
 648         displayDateTime
="true"; else displayDateTime
="false"; fi 
 650     ## Construct duration output string 
 652     if $displayYears; then 
 653         output
=$output$fullYears"Y"; fi 
 654     if $displayMonths; then 
 655         output
=$output$fullMonths"M"; fi 
 656     if $displayDays; then 
 657         output
=$output$fullDays"D"; fi 
 658     if $displayDateTime; then 
 659         output
=$output"T"; fi 
 660     if $displayHours; then 
 661         output
=$output$fullHours"H"; fi 
 662     if $displayMinutes; then 
 663         output
=$output$fullMinutes"M"; fi 
 664     if $displaySeconds; then 
 665         output
=$output$fullSeconds"S"; fi 
 667     ## Output duration string to stdout 
 668     echo "$output" && returnState
="true"; 
 670     #===Determine function return code=== 
 671     if [ "$returnState" = "true" ]; then 
 673     elif [ "$returnState" = "error_input" ]; then 
 677         yell 
"ERROR:Unknown"; 
 681 } # Get duration (ex: PT10M4S ) 
 683     # Desc: Report seconds until next day. 
 685     # Output: stdout: integer seconds until next day 
 686     # Output: exit code 0 if stdout > 0; 1 if stdout = 0; 2 if stdout < 0 
 687     # Usage: timeUntilNextDay 
 688     # Usage: if ! myTTL="$(timeUntilNextDay)"; then yell "ERROR in if statement"; exit 1; fi 
 689     # Depends: date 8, echo 8, yell, try 
 691     local returnState timeCurrent timeNextDay secondsUntilNextDay returnState
 
 692     timeCurrent
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second. 
 693     timeNextDay
="$(date -d "$timeCurrent next day
" --iso-8601=date)"; # Produce timestamp of beginning of tomorrow with resolution of 1 second. 
 694     secondsUntilNextDay
="$(( $(date +%s -d "$timeNextDay") - $(date +%s -d "$timeCurrent") ))" ; # Calculate seconds until closest future midnight (res. 1 second). 
 695     if [[ "$secondsUntilNextDay" -gt 0 ]]; then 
 697     elif [[ "$secondsUntilNextDay" -eq 0 ]]; then 
 698         returnState
="warning_zero"; 
 699         yell 
"WARNING:Reported time until next day exactly zero."; 
 700     elif [[ "$secondsUntilNextDay" -lt 0 ]]; then 
 701         returnState
="warning_negative"; 
 702         yell 
"WARNING:Reported time until next day is negative."; 
 705     try 
echo "$secondsUntilNextDay"; # Report 
 707     # Determine function return code 
 708     if [[ "$returnState" = "true" ]]; then 
 710     elif [[ "$returnState" = "warning_zero" ]]; then 
 712     elif [[ "$returnState" = "warning_negative" ]]; then 
 715 } # Report seconds until next day 
 717     # Desc: Report seconds until next hour 
 719     # Output: stdout: integer seconds until next hour 
 720     # Output: exit code 0 if stdout > 0; 1 if stdout = 0; 2 if stdout < 0 
 721     # Usage: timeUntilNextHour 
 722     # Usage: if ! myTTL="$(timeUntilNextHour)"; then yell "ERROR in if statement"; exit 1; fi 
 724     local returnState timeCurrent timeNextHour secondsUntilNextHour
 
 725     timeCurrent
="$(date --iso-8601=seconds)"; # Produce `date`-parsable current timestamp with resolution of 1 second. 
 726     timeNextHour
="$(date -d "$timeCurrent next hour
" --iso-8601=hours)"; # Produce `date`-parsable current time stamp with resolution of 1 second. 
 727     secondsUntilNextHour
="$(( $(date +%s -d "$timeNextHour") - $(date +%s -d "$timeCurrent") ))"; # Calculate seconds until next hour (res. 1 second). 
 728     if [[ "$secondsUntilNextHour" -gt 0 ]]; then 
 730     elif [[ "$secondsUntilNextHour" -eq 0 ]]; then 
 731         returnState
="warning_zero"; 
 732         yell 
"WARNING:Reported time until next hour exactly zero."; 
 733     elif [[ "$secondsUntilNextHour" -lt 0 ]]; then 
 734         returnState
="warning_negative"; 
 735         yell 
"WARNING:Reported time until next hour is negative."; 
 738     try 
echo "$secondsUntilNextHour"; # Report 
 740     # Determine function return code 
 741     if [[ "$returnState" = "true" ]]; then 
 743     elif [[ "$returnState" = "warning_zero" ]]; then 
 745     elif [[ "$returnState" = "warning_negative" ]]; then 
 748 } # Report seconds until next hour 
 750     # Desc: Validates Input 
 751     # Usage: validateInput [str input] [str input type] 
 753     # Input: arg1: string to validate 
 754     #        arg2: string specifying input type (ex:"ssh_pubkey") 
 755     # Output: return code 0: if input string matched specified string type 
 756     # Depends: bash 5, yell() 
 758     local fn argInput argType
 
 766     if [[ $# -gt 2 ]]; then yell 
"ERROR:$0:$fn:Too many arguments."; exit 1; fi; 
 769     if [[ -z "$argInput" ]]; then return 1; fi 
 773     ### Check for alnum/dash base64 (ex: "ssh-rsa AAAAB3NzaC1yc2EAAA") 
 774     if [[ "$argType" = "ssh_pubkey" ]]; then 
 775         if [[ "$argInput" =~ ^
[[:alnum
:]-]*[\ 
]*[[:alnum
:]+/=]*$ 
]]; then 
 779     ### Check for age1[:bech32:] 
 780     if [[ "$argType" = "age_pubkey" ]]; then 
 781         if [[ "$argInput" =~ ^age1
[qpzry9x8gf2tvdw0s3jn54khce6mua7l
]*$ 
]]; then 
 785     if [[ "$argType" = "integer" ]]; then 
 786         if [[ "$argInput" =~ ^
[[:digit
:]]*$ 
]]; then 
 789     ## time element (year, month, week, day, hour, minute, second) 
 790     if [[ "$argType" = "time_element" ]]; then 
 791         if [[ "$argInput" = "year" ]] || \
 
 792                [[ "$argInput" = "month" ]] || \
 
 793                [[ "$argInput" = "week" ]] || \
 
 794                [[ "$argInput" = "day" ]] || \
 
 795                [[ "$argInput" = "hour" ]] || \
 
 796                [[ "$argInput" = "minute" ]] || \
 
 797                [[ "$argInput" = "second" ]]; then 
 800     # Return error if no condition matched. 
 802 } # Validates strings 
 804 magicInitWorkingDir
() { 
 805     # Desc: Determine temporary working directory from defaults or user input 
 806     # Usage: magicInitWorkingDir 
 807     # Input:  vars: optionTmpDir, argTempDirPriority, dirTmpDefault 
 808     # Input:  vars: scriptTimeStart 
 809     # Output: vars: dir_tmp 
 810     # Depends: bash 5.0.3, processArguments(), vbm(), yell() 
 811     # Parse '-t' option (user-specified temporary working dir) 
 812     ## Set dir_tmp_parent to user-specified value if specified 
 813     local fn dir_tmp_parent
 
 818     vbm 
"STATUS:$fn:Starting magicInitWorkingDir() function."; 
 819     if [[ "$optionTmpDir" = "true" ]]; then 
 820         if [[ -d "$argTempDirPriority" ]]; then 
 821             dir_tmp_parent
="$argTempDirPriority";  
 823             yell 
"WARNING:$fn:Specified temporary working directory not valid:$argTempDirPriority"; 
 824             exit 1; # Exit since user requires a specific temp dir and it is not available. 
 827     ## Set dir_tmp_parent to default or fallback otherwise 
 828         if [[ -d "$dirTmpDefault" ]]; then 
 829             dir_tmp_parent
="$dirTmpDefault"; 
 830         elif [[ -d /tmp 
]]; then 
 831             yell 
"WARNING:$fn:$dirTmpDefault not available. Falling back to /tmp ."; 
 832             dir_tmp_parent
="/tmp"; 
 834             yell 
"ERROR:$fn:No valid working directory available. Exiting."; 
 838     ## Set dir_tmp using dir_tmp_parent and nonce (scriptTimeStart) 
 839     dir_tmp
="$dir_tmp_parent"/"$scriptTimeStart""..bkgpslog" && vbm 
"DEBUG :$fn:Set dir_tmp to:$dir_tmp"; # Note: removed at end of main(). 
 840     vbm 
"STATUS:$fn:Finished magicInitWorkingDir() function."; 
 842 magicInitCheckTar
() { 
 843     # Desc: Initializes or checks output tar 
 844     # input: vars: dirOut, bufferTTL, cmd_encrypt_suffix, cmd_compress_suffix 
 845     # input: vars: scriptHostname 
 846     # output: vars: pathout_tar 
 847     # depends: Bash 5.0.3, vbm(), dateShort(), checkMakeTar(), magicWriteVersion() 
 848     local fn checkMakeTarES
 
 853     vbm 
"STATUS:$fn:Starting magicInitCheckTar() function."; 
 855     pathout_tar
="$dirOut"/"$(dateShort "$
(date --date="$bufferTTL seconds ago" --iso-8601=seconds
)")"..
"$scriptHostname""$label""$cmd_compress_suffix""$cmd_encrypt_suffix".
tar && \
 
 856         vbm 
"STATUS:$fn:Set pathout_tar to:$pathout_tar"; 
 857     # Validate pathout_tar as tar. 
 858     checkMakeTar 
"$pathout_tar"; checkMakeTarES
="$?"; 
 859     ## Add VERSION file if checkMakeTar had to create a tar (exited 1) or replace one (exited 2) 
 860     vbm 
"STATUS:$fn:exit status before magicWriteVersion:$checkMakeTarES" 
 861     if [[ "$checkMakeTarES" -eq 1 ]] || 
[[ "$checkMakeTarES" -eq 2 ]]; then magicWriteVersion
; fi 
 862     vbm 
"STATUS:$fn:Finished magicInitCheckTar() function."; 
 863 } # Initialize tar, set pathout_tar 
 864 magicParseCompressionArg
() { 
 865     # Desc: Parses compression arguments specified by '-c' option 
 866     # Input:  vars: optionCompress 
 867     # Output: cmd_compress, cmd_compress_suffix 
 868     # Depends: processArguments(), vbm(), checkapp(), gzip 1.9 
 874     vbm 
"STATUS:$fn:Starting magicParseCompressionArg() function."; 
 875     if [[ "$optionCompress" = "true" ]]; then # Check if compression option active 
 876         if checkapp 
gzip; then # Check if gzip available 
 877             cmd_compress
="gzip " && vbm 
"STATUS:$fn:cmd_compress:$cmd_compress"; 
 878             cmd_compress_suffix
=".gz" && vbm 
"STATUS:$fn:cmd_compress_suffix:$cmd_compress_suffix"; 
 880             yell 
"ERROR:$fn:Compression enabled but \"gzip\" not found. Exiting."; exit 1; 
 883         cmd_compress
="tee /dev/null " && vbm 
"STATUS:$fn:cmd_compress:$cmd_compress"; 
 884         cmd_compress_suffix
="" && vbm 
"STATUS:$fn:cmd_compress_suffix:$cmd_compress_suffix"; 
 885         vbm 
"DEBUG :$fn:Compression not enabled."; 
 887     vbm 
"STATUS:$fn:Starting magicParseCompressionArg() function."; 
 888 } # Form compression cmd string and filename suffix 
 889 magicParseCustomTTL
() { 
 890     # Desc: Set user-specified TTLs for buffer and script 
 891     # Usage: magicParseCustomTTL 
 892     # Input: vars: argCustomBufferTTL (integer), argCustomScriptTTL_TE (string) 
 893     # Input: vars: optionCustomBufferTTL, optionCustomScriptTTL_TE 
 894     # Input: vars: bufferTTL (integer), scriptTTL_TE (string) 
 895     # Output: bufferTTL (integer), scriptTTL_TE (string) 
 896     # Depends: Bash 5.0.3, yell(), vbm(), validateInput(), showUsage() 
 902     vbm 
"STATUS:$fn:Starting magicParseCustomTTL() function."; 
 903     # React to '-b, --buffer-ttl' option 
 904     if [[ "$optionCustomBufferTTL" = "true" ]]; then 
 905         ## T: Check if argCustomBufferTTL is an integer 
 906         if validateInput 
"$argCustomBufferTTL" "integer"; then 
 907             ### T: argCustomBufferTTL is an integer 
 908             bufferTTL
="$argCustomBufferTTL" && vbm 
"STATUS:$fn:Custom bufferTTL from -b:$bufferTTL"; 
 910             ### F: argcustomBufferTTL is not an integer 
 911             yell 
"ERROR:$fn:Invalid integer argument for custom buffer time-to-live."; showUsage
; exit 1; 
 913         ## F: do not change bufferTTL 
 916     # React to '-B, --script-ttl' option 
 917     if [[ "$optionCustomScriptTTL_TE" = "true" ]]; then 
 918         ## T: Check if argCustomScriptTTL is a time element (ex: "day", "hour") 
 919         if validateInput 
"$argCustomScriptTTL_TE" "time_element"; then 
 920             ### T: argCustomScriptTTL is a time element 
 921             scriptTTL_TE
="$argCustomScriptTTL_TE" && vbm 
"STATUS:$fn:Custom scriptTTL_TE from -B:$scriptTTL_TE"; 
 923             ### F: argcustomScriptTTL is not a time element 
 924             yell 
"ERROR:$fn:Invalid time element argument for custom script time-to-live."; showUsage
; exit 1; 
 926         ## F: do not change scriptTTL_TE 
 928     vbm 
"STATUS:$fn:Starting magicParseCustomTTL() function."; 
 929 } # Sets custom script or buffer TTL if specified 
 931     # Desc: Parses -l option to set label 
 932     # In : optionLabel, argLabel 
 934     # Depends: Bash 5.0.3, vbm(), yell() 
 940     vbm 
"STATUS:$fn:Started magicParseLabel() function."; 
 941     # Do nothing if optionLabel not set to true. 
 942     if [[ ! "$optionLabel" = "true" ]]; then 
 943         vbm 
"STATUS:$fn:optionlabel not set to 'true'. Returning early."; 
 946     # Set label if optionLabel is true 
 947     if [[ "$optionLabel" = "true" ]]; then 
 948         label
="_""$argLabel"; 
 949         vbm 
"STATUS:$fn:Set label:$label"; 
 951     vbm 
"STATUS:$fn:Finished magicParseLabel() function."; 
 952 } # Set label used in output file name 
 953 magicParseProcessStrings
() { 
 954     # Desc: Processes user-supplied process strings into process commands for appendFileTar(). 
 955     # Usage: magicParseProcessStrings 
 956     # In : vars: optionProcString optionNoStoreRaw optionStoreRaw argRawFileExt 
 957     #      arry: argProcStrings, argProcFileExts 
 958     # Out: arry: procStrings, procFileExts 
 959     # Depends Bash 5.0.3, yell(), vbm() 
 965     vbm 
"STATUS:$fn:Starting magicParseProcessStrings() function."; 
 966     vbm 
"STATUS:$fn:var:optionProcString:$optionProcString"; 
 967     vbm 
"STATUS:$fn:var:optionNoStoreRaw:$optionNoStoreRaw"; 
 968     vbm 
"STATUS:$fn:var:optionStoreRaw:$optionStoreRaw"; 
 969     vbm 
"STATUS:$fn:var:argRawFileExt:$argRawFileExt"; 
 970     vbm 
"STATUS:$fn:ary:argProcStrings:${argProcStrings[*]}"; 
 971     vbm 
"STATUS:$fn:ary:argProcFileExts:${argProcFileExts[*]}" 
 973     ## Validate argRawFileExt 
 974     if [[ "$argRawFileExt" =~ ^
[.
][[:alnum
:]]*$ 
]]; then 
 975         rawFileExt
="$argRawFileExt" && \
 
 976             vbm 
"DEBUG :$fn:Set rawFileExt to \"$argRawFileExt\""; 
 978         vbm 
"DEBUG :$fn:Validation failure for $argRawFileExt . Not set to rawFileExt."; 
 981     # Add default stdin output file entries for procStrings, procFileExts 
 982     ## Check if user specified that no raw stdin be saved. 
 983     if [[ ! "$optionNoStoreRaw" = "true" ]]; then 
 984         ### T: --no-store-raw not set. Store raw. Append procStrings with cat. 
 985         vbm 
"DEBUG :$fn:--no-store-raw not set. Storing raw."; 
 986         #### Append procStrings array 
 987         procStrings
+=("cat ") && \
 
 988             vbm 
"DEBUG :$fn:Appended \"cat \" to procStrings"; 
 989         vbm 
"DEBUG :$fn:procStrings array:${procStrings[*]}"; 
 990         #### Check if --store-raw set. 
 991         if [[ "$optionStoreRaw" = "true" ]]; then 
 992             ##### T: --store-raw set. Append procFileExts with user-specified file ext 
 993             vbm 
"DEBUG :$fn:--store-raw set."; 
 994             procFileExts
+=("$rawFileExt") && \
 
 995                 vbm 
"DEBUG :$fn:Appended $rawFileExt to procFileExts"; 
 996             vbm 
"STATUS:$fn:procFileExts array:${procFileExts[*]}"; 
 998             ##### F: --store-raw not set. Append procFileExts with default ".stdin" file ext 
 999             ###### Append procFileExts array 
1000             procFileExts
+=(".stdin") && \
 
1001                 vbm 
"DEBUG :$fn:Appended \".stdin\" to procFileExts"; 
1002             vbm 
"STATUS:$fn:procFileExts array:${procFileExts[*]}"; 
1005         ### F: --no-store-raw set. Do not store raw. 
1006         #### Do not append procStrings or procFileExts arrays. 
1007         vbm 
"STATUS:$fn:--no-store-raw set. Not storing raw."; 
1008         vbm 
"STATUS:$fn:procFileExts array:${procFileExts[*]}"; 
1011     # Do nothing more if optionProcString not set to true. 
1012     if [[ ! "$optionProcString" = "true" ]]; then 
1013         vbm 
"STATUS:$fn:optionProcString not set to 'true'. Returning early."; 
1015     # Validate input array indices 
1016     ## Make sure that argProcStrings and argProcFileExts have same index counts 
1017     if ! [[ "${#argProcStrings[@]}" -eq "${#argProcFileExts[@]}" ]]; then 
1018         yell 
"ERROR:$fn:Mismatch in number of elements in arrays argProcStrings and argProcFileExts:${#argProcStrings[@]} DNE ${#argProcFileExts[@]}"; 
1019         yell 
"STATUS:$fn:argProcStrings:${argProcStrings[*]}"; yell 
"STATUS:$fn:argProcFileExts:${argProcFileExts[*]}"; exit 1; fi; 
1020     ## Make sure that no array elements are blank 
1021     for element 
in "${argProcStrings[@]}"; do 
1022         if [[ -z "$element" ]]; then yell 
"ERROR:$fn:Empty process string specified. Exiting."; exit 1; fi; done 
1023     for element 
in "${argProcFileExts[@]}"; do 
1024         if [[ -z "$element" ]]; then yell 
"ERROR:$fn:Empty output file extension specified. Exiting."; exit 1; fi; done 
1025     ## Make sure that no process string starts with '-' (ex: if only one arg supplied after '-p' option) 
1026     for element 
in "${argProcStrings[@]}"; do 
1027         if [[ "$element" =~ ^
[-][[:print
:]]*$ 
]] && [[ ! "$element" =~ ^
[[:print
:]]*$ 
]]; then 
1028             yell 
"ERROR:$fn:Illegal character '-' at start of process string element:\"$element\""; 
1030     vbm 
"STATUS:$fn:Quick check shows argProcStrings and argProcFileExts appear to have valid contents."; 
1031     vbm 
"STATUS:$fn:argProcStrings:${argProcStrings[*]}" 
1032     vbm 
"STATUS:$fn:argProcStrings:${argProcFileExts[*]}" 
1033     procStrings
+=("${argProcStrings[@]}"); # Export process command strings 
1034     procFileExts
+=("${argProcFileExts[@]}"); # Export process command strings 
1035     vbm 
"STATUS:$fn:Finished magicParseProcessStrings() function."; 
1036 } # Validate and save process strings and file extensions to arrays procStrings, procFileExts 
1037 magicParseRecipients
() { 
1038     # Desc: Parses recipient arguments specified by '-r' or '-R' options 
1039     # Usage: magicParseRecipients 
1040     # In : vars: optionEncrypt, optionRecArg, optionRecDir 
1041     #      arry: argRecPubKeys (-r), argRecDir (-R) 
1042     # Out: vars: cmd_encrypt, cmd_encrypt_suffix 
1043     # Depends: head 8.30, checkapp(), checkAgePubkey(), validateInput() 
1044     local fn recipients recipientDir recFileLine updateRecipients
 
1045     local -a recPubKeysValid candRecPubKeysValid
 
1047     # Save function name 
1048     fn
="${FUNCNAME[0]}"; 
1049     vbm 
"STATUS:$fn:Starting magicParseRecipients() function."; 
1051     # Catch illegal option combinations 
1052     ## Catch case if '-e' is set but neither '-r' nor '-R' is set 
1053     if [[ "$optionEncrypt" = "true" ]] && \
 
1054            ! { [[ "$optionRecArg" = "true" ]] || 
[[ "$optionRecDir" = "true" ]]; }; then 
1055         yell 
"ERROR:$fn:\\'-e\\' set but no \\'-r\\' or \\'-R\\' set."; exit 1; fi; 
1056     ## Catch case if '-r' or '-R' set but '-e' is not 
1057     if [[ ! "$optionEncrypt" = "true" ]] && \
 
1058            { [[ "$optionRecArg" = "true" ]] || 
[[ "$optionRecDir" = "true" ]]; }; then 
1059         yell 
"ERROR:$fn:\\'-r\\' or \\'-R\\' set but \\'-e\\' is not set."; exit 1; fi; 
1061     # Handle no encryption cases 
1062     if [[ ! "$optionEncrypt" = "true" ]]; then 
1063         cmd_encrypt
="cat " && vbm 
"STATUS:$fn:cmd_encrypt:$cmd_encrypt"; 
1064         cmd_encrypt_suffix
="" && vbm 
"STATUS:$fn:cmd_encrypt_suffix:$cmd_encrypt_suffix"; 
1065         vbm 
"DEBUG :$fn:Encryption not enabled."; 
1068     # Handle encryption cases 
1069     ## Check age availability 
1070     if ! checkapp age
; then yell 
"ERROR:$fn:age not available. Exiting."; exit 1; fi 
1071     ## Parse '-r' options: validate and append pubkeys from argRecPubKeys to recPubKeysValid 
1072     if [[ "$optionRecArg" = "true" ]]; then 
1073         for pubkey 
in "${argRecPubKeys[@]}"; do # Validate recipient pubkey strings by forming test message 
1074             vbm 
"DEBUG :$fn:Testing pubkey string:$pubkey"; 
1075             if checkAgePubkey 
"$pubkey" && \
 
1076                     ( validateInput 
"$pubkey" "ssh_pubkey" || validateInput 
"$pubkey" "age_pubkey"); then 
1077                 #### Add validated pubkey to recPubKeysValid array 
1078                 recPubKeysValid
+=("$pubkey") && \
 
1079                     vbm 
"DEBUG :$fn:recPubkeysValid:pubkey added:$pubkey"; 
1081                 yell 
"ERROR:$fn:Exit code ""$?"". Invalid recipient pubkey string. Exiting."; exit 1; 
1084         vbm 
"STATUS:$fn:Finished processing argRecPubKeys array"; 
1085         vbm 
"DEBUG :$fn:Array of validated pubkeys:${recPubKeysValid[*]}"; 
1087     ## Parse '-R' options: validate and append pubkeys in argRecDir to recPubKeysValid 
1088     if [[ "$optionRecDir" = "true" ]]; then 
1089         ### Check that argRecDir is a directory 
1090         if [[ -d "$argRecDir" ]]; then 
1091             recipientDir
="$argRecDir" && \
 
1092                 vbm 
"STATUS:$fn:Recipient watch directory detected:\"$recipientDir\""; 
1093             #### Initialize variable indicating outcome of pubkey review 
1094             unset updateRecipients
 
1095             #### Add existing recipients from '-r' option 
1096             candRecPubKeysValid
=("${recPubKeysValid[@]}"); 
1097             #### Parse files in recipientDir 
1098             for file in "$recipientDir"/*; do 
1099                 ##### Read first line of each file 
1100                 recFileLine
="$(head -n1 "$file")" && \
 
1101                     vbm 
"STATUS:$fn:Checking if pubkey:\"$recFileLine\""; 
1102                 ##### check if first line is a valid pubkey 
1103                 if checkAgePubkey 
"$recFileLine" && \
 
1104                         ( validateInput 
"$recFileLine" "ssh_pubkey" || validateInput 
"$recFileLine" "age_pubkey"); then 
1105                     ###### T: add candidate pubkey to candRecPubKeysValid 
1106                     candRecPubKeysValid
+=("$recFileLine") && \
 
1107                         vbm 
"STATUS:$fn:RecDir pubkey is valid pubkey:\"$recFileLine\""; 
1109                     ###### F: throw warning; 
1110                     yell 
"ERROR:$fn:Invalid recipient file detected. Not modifying recipient list:$recFileLine"; 
1111                     updateRecipients
="false"; 
1114             #### Write candRecPubKeysValid array to recPubKeysValid if no invalid key detected 
1115             if ! [[ "$updateRecipients" = "false" ]]; then 
1116                 recPubKeysValid
=("${candRecPubKeysValid[@]}") && \
 
1117                     vbm 
"STATUS:$fn:Wrote candRecPubkeysValid to recPubKeysValid:\"${recPubKeysValid[*]}\""; 
1122     ## Form age recipient string from recPubKeysValid 
1123     for pubkey 
in "${recPubKeysValid[@]}"; do 
1124         recipients
="$recipients""-r '$pubkey' "; 
1125         vbm 
"STATUS:$fn:Added pubkey for forming age recipient string:""$pubkey"; 
1126         vbm 
"DEBUG :$fn:recipients:""$recipients";       
1129     ## Output cmd_encrypt, cmd_encrypt_suffix from recipients 
1130     cmd_encrypt
="age ""$recipients " && vbm 
"STATUS:$fn:cmd_encrypt:$cmd_encrypt"; 
1131     cmd_encrypt_suffix
=".age" && vbm 
"STATUS:$fn:cmd_encrypt_suffix:$cmd_encrypt_suffix"; 
1133     vbm 
"STATUS:$fn:Finished magicParseRecipients() function."; 
1134 } # Sets cmd_encrypt, cmd_encrypt_suffix from -r, -R args 
1135 magicSetScriptTTL
() { 
1136     #Desc: Sets script_TTL seconds from provided time_element string argument 
1137     #Usage: magicSetScriptTTL [str time_element] 
1138     #Input: arg1: string (Ex: scriptTTL_TE; "day" or "hour") 
1139     #Output: var: scriptTTL (integer seconds) 
1140     #Depends: timeUntilNextHour, timeUntilNextDay 
1141     local fn argTimeElement
 
1143     # Save function name 
1144     fn
="${FUNCNAME[0]}"; 
1146     vbm 
"STATUS:$fn:Starting magicSetScriptTTL() function."; 
1147     argTimeElement
="$1"; 
1148     if [[ "$argTimeElement" = "day" ]]; then 
1149             # Set script lifespan to end at start of next day 
1150         if ! scriptTTL
="$(timeUntilNextDay)"; then # sets scriptTTL, then checks exit code 
1151             if [[ "$scriptTTL" -eq 0 ]]; then 
1152             ((scriptTTL
++)); # Add 1 because 0 would cause 'timeout' to never timeout. 
1154             yell 
"ERROR:$fn:timeUntilNextDay exit code $?"; exit 1; 
1157     elif [[ "$argTimeElement" = "hour" ]]; then 
1158         # Set script lifespan to end at start of next hour 
1159         if ! scriptTTL
="$(timeUntilNextHour)"; then # sets scriptTTL, then checks exit code 
1160             if [[ "$scriptTTL" -eq 0 ]]; then 
1161                 ((scriptTTL
++)); # Add 1 because 0 would cause 'timeout' to never timeout. 
1163                 yell 
"ERROR:$fn:timeUntilNextHour exit code $?"; exit 1; 
1167         yell 
"ERROR:$fn:Invalid argument for setScriptTTL function:$argTimeElement"; exit 1; 
1169     vbm 
"STATUS:$fn:Finished magicSetScriptTTL() function."; 
1170 } # Set scriptTTL in seconds until next (day|hour). 
1171 magicWriteVersion
() { 
1172     # Desc: Appends time-stamped VERSION to pathout_tar 
1173     # Usage: magicWriteVersion 
1174     # Input: vars: pathout_tar, dir_tmp 
1175     # Input: vars: scriptVersion, scriptURL, ageVersion, ageURL, scriptHostname 
1176     # Input: array: recPubKeysValid 
1177     # Output: appends tar (pathout_tar) 
1178     # Depends: bash 5.0.3, dateTimeShort(), appendArgTar() 
1179     local fn fileoutVersion contentVersion pubKeyIndex pubKeyIndex
 
1181     # Save function name 
1182     fn
="${FUNCNAME[0]}"; 
1184     vbm 
"STATUS:$fn:Starting magicWriteVersion() function."; 
1185     # Set VERSION file name 
1186     fileoutVersion
="$(dateTimeShort)..VERSION"; 
1188     # Gather VERSION data in contentVersion 
1189     contentVersion
="scriptVersion=$scriptVersion"; 
1190     #contentVersion="$contentVersion""\\n"; 
1191     contentVersion
="$contentVersion""\\n""scriptName=$scriptName"; 
1192     contentVersion
="$contentVersion""\\n""scriptURL=$scriptURL"; 
1193     contentVersion
="$contentVersion""\\n""ageVersion=$ageVersion"; 
1194     contentVersion
="$contentVersion""\\n""ageURL=$ageURL"; 
1195     contentVersion
="$contentVersion""\\n""date=$(date --iso-8601=seconds)"; 
1196     contentVersion
="$contentVersion""\\n""hostname=$scriptHostname"; 
1197     ## Add list of recipient pubkeys 
1198     for pubkey 
in "${recPubKeysValid[@]}"; do 
1200         contentVersion
="$contentVersion""\\n""PUBKEY_$pubKeyIndex=$pubkey"; 
1202     ## Process newline escapes 
1203     contentVersion
="$(echo -e "$contentVersion")" 
1205     # Write contentVersion as file fileoutVersion and write-append to pathout_tar 
1206     appendArgTar 
"$contentVersion" "$fileoutVersion" "$pathout_tar" "$dir_tmp" && \
 
1207         vbm 
"STATUS:$fn:Appended $fileoutVersion to $pathout_tar"; 
1208     vbm 
"STATUS:$fn:Finished magicWriteVersion() function."; 
1209 } # write version data to pathout_tar via appendArgTar() 
1210 magicProcessWriteBuffer
() { 
1211     # Desc: process and write buffer 
1212     # In : vars: bufferTTL bufferTTL_STR scriptHostname label dir_tmp SECONDS 
1214     # Out: file:(pathout_tar) 
1215     # Depends: Bash 5.0.3, date 8.30, yell(), vbm(), dateTimeShort(), 
1216     ### Note: These arrays should all have the same number of elements: 
1217     ###       pathouts, fileouts, procFileExts, procStrings 
1219     local fn timeBufferStartLong timeBufferStart fileoutBasename
 
1220     local -a fileouts pathouts
 
1221     local writeCmd1 writeCmd2 writeCmd3 writeCmd4
 
1223     # Debug:Get function name 
1224     fn
="${FUNCNAME[0]}"; 
1226     vbm 
"STATUS:$fn:Started magicProcessWriteBuffer()."; 
1227     vbm 
"DEBUG :$fn:buffer array element count:${#buffer[@]}"; 
1228     vbm 
"DEBUG :$fn:buffer array first element:${buffer[0]}"; 
1229     vbm 
"DEBUG :$fn:buffer array last element :${buffer[-1]}"; 
1231     # Determine file paths (time is start of buffer period) 
1232     ## Calculate start time 
1233     timeBufferStartLong
="$(date --date="$bufferTTL seconds ago
" --iso-8601=seconds)" && \
 
1234         vbm 
"DEBUG :$fn:timeBufferStartLong:$timeBufferStartLong"; 
1235     timeBufferStart
="$(dateTimeShort "$timeBufferStartLong" )" && \
 
1236         vbm 
"DEBUG :$fn:timeBufferStart:$timeBufferStart"; # Note start time YYYYmmddTHHMMSS+zzzz (no separators) 
1237     ## Set common basename 
1238     fileoutBasename
="$timeBufferStart""--""$bufferTTL_STR""..""$scriptHostname""$label" && \
 
1239         vbm 
"STATUS:$fn:Set fileoutBasename to:$fileoutBasename"; 
1240     ## Determine output file name array 
1241     ### in: fileOutBasename cmd_compress_suffix cmd_encrypt_suffix procFileExts 
1242     for fileExt 
in "${procFileExts[@]}"; do 
1243         fileouts
+=("$fileoutBasename""$fileExt""$cmd_compress_suffix""$cmd_encrypt_suffix") && \
 
1244             vbm 
"STATUS:$fn:Added $fileExt to fileouts:${fileouts[*]}"; 
1246     for fileName 
in "${fileouts[@]}"; do 
1247         pathouts
+=("$dir_tmp"/"$fileName") && \
 
1248             vbm 
"STATUS:$fn:Added $fileName to pathouts:${pathouts[*]}"; 
1250     ## Update pathout_tar 
1253     # Process and write buffers to dir_tmp 
1254     ## Prepare command strings 
1255     writeCmd1
="printf \"%s\\\\n\" \"\${buffer[@]}\""; # printf "%s\\n" "${buffer[@]}" 
1256     #writeCmd2="" # NOTE: Specified by parsing array procStrings 
1257     writeCmd3
="$cmd_compress"; 
1258     writeCmd4
="$cmd_encrypt"; 
1260     ## Process buffer and write to dir_tmp 
1261     vbm 
"DEBUG :$fn:fileouts    element count:${#fileouts[@]}"; 
1262     vbm 
"DEBUG :$fn:pathouts    element count:${#pathouts[@]}"; 
1263     vbm 
"DEBUG :$fn:procStrings element count:${#pathouts[@]}"; 
1264     vbm 
"DEBUG :$fn:fileouts    contents:${fileouts[*]}"; 
1265     vbm 
"DEBUG :$fn:pathouts    contents:${pathouts[*]}"; 
1266     vbm 
"DEBUG :$fn:procStrings contents:${pathouts[*]}"; 
1267     for index 
in "${!pathouts[@]}"; do 
1268         writeCmd2
="${procStrings[$index]}"; 
1269         writeCmdAll
="$writeCmd1 | $writeCmd2 | $writeCmd3 | $writeCmd4" && vbm 
"STATUS:$fn:Assembled command:\"$writeCmdAll\""; 
1270         eval "$writeCmd1 | $writeCmd2 | $writeCmd3 | $writeCmd4" > "${pathouts[$index]}" && vbm 
"STATUS:$fn:Wrote command output to ${pathouts[$index]}"; 
1273     # Append dir_tmp files to pathout_tar 
1274     wait; # Wait to avoid collision with older magicProcessWriteBuffer() instances (see https://www.tldp.org/LDP/abs/html/x9644.html ) 
1275     for index 
in "${!pathouts[@]}"; do 
1276         tar --apend --directory="$dir_tmp" --file="$pathout_tar" "${fileouts[$index]}" && \
 
1277             vbm 
"STATUS:$fn:Appended ${pathouts[$index]} to $pathout_tar"; 
1278         #appendFileTar "${pathouts[$index]}" "${fileouts[$index]}" "$pathout_tar" "$dir_tmp" && \ 
1281     # Remove secured chunks from dir_tmp 
1282     for path 
in "${pathouts[@]}"; do 
1283         rm "$path" && vbm 
"STATUS:$fn:Removed:$path"; 
1286     vbm 
"STATUS:$fn:Finished magicProcessWriteBuffer()."; 
1287 } # Process and Write buffer 
1291         cmd | bklog [ options ] 
1295                 Display help information. 
1297                 Display script version. 
1299                 Display debugging info. 
1302         -r, --recipient [ string pubkey ] 
1303                 Specify recipient. May be age or ssh pubkey. 
1304                 May be specified multiple times for multiple pubkeys. 
1305                 See https://github.com/FiloSottile/age 
1306         -o, --output [ path dir ] 
1307                 Specify output directory to save logs. This option is required 
1309         -p, --process-string [ filter command ] [ output file extension]  
1310                 Specify how to create and name a processed version of the stdin. 
1311                 For example, if stdin is 'nmea' location data: 
1313                 -p "gpsbabel -i nmea -f - -o gpx -F - " ".gpx" 
1315                 This option would cause the stdin to 'bklog' to be piped into 
1316                 the 'gpsbabel' command, interpreted as 'nmea' data, converted 
1317                 into 'gpx' format, and then appended to the output tar file 
1318                 as a file with a '.gpx' extension. 
1319                 This option may be specified multiple times in order to output 
1320                 results of multiple different processing methods. 
1321         -l, --label [ string ] 
1322                 Specify a label to be included in all output file names. 
1323                 Ex: 'location' if stdin is location data. 
1324         -w, --store-raw [ file extension ] 
1325                 Specify file extension of file within output tar that contains 
1326                 raw stdin data. The default behavior is to always save raw stdin 
1327                 data in a '.stdin' file. Example usage when 'bklog' receives 
1328                 'nmea' data from 'gpspipe -r': 
1332                 Stdin data is saved in a '.nmea' file within the output tar. 
1334                 Do not store raw stdin in output tar. 
1336                 Compress output with gzip (before encryption if enabled). 
1338                 Specify time zone. (ex: "America/New_York") 
1339         -t, --temp-dir [path dir] 
1340                 Specify parent directory for temporary working directory. 
1342         -R, --recipient-dir [path dir] 
1343                 Specify directory containing files whose first lines are 
1344                 to be interpreted as pubkey strings (see '-r' option). Only 
1345                 one directory may be specified. 
1346         -b, --buffer-ttl [integer] 
1347                 Specify custom buffer period in seconds (default: 300 seconds) 
1348         -B, --script-ttl [time element string] 
1349                 Specify custom script time-to-live in seconds (default: "day") 
1350                 Valid values: "day", "hour" 
1352     EXAMPLE: (bash script lines) 
1353     $ gpspipe -r | /bin/bash bklog -v -e -c -z "UTC" -t "/dev/shm" \ 
1354     -r age1mrmfnwhtlprn4jquex0ukmwcm7y2nxlphuzgsgv8ew2k9mewy3rs8u7su5 \ 
1355     -r age1ala848kqrvxc88rzaauc6vc5v0fqrvef9dxyk79m0vjea3hagclswu0lgq \ 
1356     -R ~/.config/bklog/recipients -w ".nmea" -b 300 -B "day" \ 
1357     -o ~/Sync/Logs -l "location" \ 
1358     -p "gpsbabel -i nmea -f - -o gpx -F - " ".gpx" \ 
1359     -p "gpsbabel -i nmea -f - -o kml -F - " ".kml" 
1361 } # Display information on how to use this script. 
1364     # Desc: Main function 
1367     # Outputs: file (pathout_tar) 
1371     # Debug:Get function name 
1372     fn
="${FUNCNAME[0]}"; 
1374     vbm 
"STATUS:$fn:Started function main()."; 
1376     processArguments 
"$@"; 
1377     ## Determine working directory 
1378     magicInitWorkingDir
; # Sets dir_tmp from argTempDirPriority 
1379     ## Set output encryption and compression option strings 
1380     ### React to "-e", "-r", and "-R" (encryption recipients) options 
1381     magicParseRecipients
; # Update cmd_encrypt, cmd_encrypt_suffix 
1382     ### React to "-c" ("compression") option 
1383     magicParseCompressionArg
; # Updates cmd_compress[_suffix] 
1384     ## React to "-b" and "-B" (custom buffer and script TTL) options 
1385     magicParseCustomTTL
; # Sets custom scriptTTL_TE and/or bufferTTL if specified 
1386     ## React to "-p" (user-supplied process command and file extension strings) options 
1387     magicParseProcessStrings
; # Sets arrays: procStrings, procFileExts 
1388     ## React to "-l" (output file label) option 
1389     magicParseLabel
; # sets label (ex: "_location") 
1391     # Perform secondary setup operations 
1392     ## Set script lifespan (scriptTTL from scriptTTL_TE) 
1393     magicSetScriptTTL 
"$scriptTTL_TE"; 
1394     ## File name substring (ISO-8601 duration from bufferTTL) 
1395     bufferTTL_STR
="$(timeDuration "$bufferTTL")" && vbm 
"DEBUG :$fn:bufferTTL_STR:$bufferTTL_STR"; 
1396     ## Init temp working dir 
1397     try mkdir 
"$dir_tmp" && vbm 
"DEBUG :$fn:Working dir created at dir_tmp:$dir_tmp"; 
1398     ## Initialize output tar (set pathout_tar) 
1400     ## Append VERSION file to tar 
1403     # Check vital apps, files, dirs 
1404     if ! checkapp 
tar && ! checkdir 
"$dirOut" "dir_tmp"; then 
1405         yell 
"ERROR:$fn:Critical components missing."; 
1406         displayMissing
; yell 
"Exiting."; exit 1; fi 
1408     # MAIN LOOP: Run until script TTL seconds pass 
1410     while [[ $SECONDS -lt "scriptTTL" ]]; do 
1411         vbm 
"STATUS:$fn:Starting buffer round:$bufferRound"; 
1412         bufferTOD
="$((SECONDS + bufferTTL))"; # Set buffer round time-of-death 
1413         # Consume stdin to fill buffer until buffer time-of-death (TOD) arrives 
1414         while read -r -t "$bufferTTL" line 
&& [[ $SECONDS -lt "$bufferTOD" ]]; do 
1415             # Append line to buffer array 
1418         # Create dir_tmp if missing 
1419         if ! [[ -d "$dir_tmp" ]]; then 
1420             yell 
"ERROR:$fn:dir_tmp existence failure:$dir_tmp"; 
1421             try mkdir 
"$dir_tmp" && vbm 
"DEBUG :$fn:Working dir recreated dir_tmp:$dir_tmp"; fi 
1422         # Update cmd_encrypt, cmd_encrypt_suffix 
1423         magicParseRecipients
; 
1424         # Export buffer to asynchronous processing. 
1425         magicProcessWriteBuffer 
& 
1426         unset buffer
; # Clear buffer array for next bufferRound 
1427         # Increment buffer round 
1433     try 
rm -r "$dir_tmp" && vbm 
"STATUS:$fn:Removed dir_tmp:$dir_tmp"; 
1435     vbm 
"STATUS:$fn:Finished function main()."; 
1438 #===END Declare local script functions=== 
1439 #==END Define script parameters== 
1441 #==BEGIN Perform work and exit== 
1442 main 
"$@" # Run main function. 
1444 #==END Perform work and exit== 
1446 # Author: Steven Baltakatei Sandoval;