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.23";          # 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 \"$3\" for output of processing string added:\"$2\""; 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]:$0:""$*" 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:Finished 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:Finished 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:argProcFileExts:${argProcFileExts[*]}" 
1033     procStrings
+=("${argProcStrings[@]}"); # Export process command strings 
1034     procFileExts
+=("${argProcFileExts[@]}"); # Export process command strings 
1035     vbm 
"STATUS:$fn:procStrings:${procStrings[*]}" 
1036     vbm 
"STATUS:$fn:procFileExts:${procFileExts[*]}" 
1037     vbm 
"STATUS:$fn:Finished magicParseProcessStrings() function."; 
1038 } # Validate and save process strings and file extensions to arrays procStrings, procFileExts 
1039 magicParseRecipients
() { 
1040     # Desc: Parses recipient arguments specified by '-r' or '-R' options 
1041     # Usage: magicParseRecipients 
1042     # In : vars: optionEncrypt, optionRecArg, optionRecDir 
1043     #      arry: argRecPubKeys (-r), argRecDir (-R) 
1044     # Out: vars: cmd_encrypt, cmd_encrypt_suffix 
1045     # Depends: head 8.30, checkapp(), checkAgePubkey(), validateInput() 
1046     local fn recipients recipientDir recFileLine updateRecipients
 
1047     local -a recPubKeysValid candRecPubKeysValid
 
1049     # Save function name 
1050     fn
="${FUNCNAME[0]}"; 
1051     vbm 
"STATUS:$fn:Starting magicParseRecipients() function."; 
1053     # Catch illegal option combinations 
1054     ## Catch case if '-e' is set but neither '-r' nor '-R' is set 
1055     if [[ "$optionEncrypt" = "true" ]] && \
 
1056            ! { [[ "$optionRecArg" = "true" ]] || 
[[ "$optionRecDir" = "true" ]]; }; then 
1057         yell 
"ERROR:$fn:\\'-e\\' set but no \\'-r\\' or \\'-R\\' set."; exit 1; fi; 
1058     ## Catch case if '-r' or '-R' set but '-e' is not 
1059     if [[ ! "$optionEncrypt" = "true" ]] && \
 
1060            { [[ "$optionRecArg" = "true" ]] || 
[[ "$optionRecDir" = "true" ]]; }; then 
1061         yell 
"ERROR:$fn:\\'-r\\' or \\'-R\\' set but \\'-e\\' is not set."; exit 1; fi; 
1063     # Handle no encryption cases 
1064     if [[ ! "$optionEncrypt" = "true" ]]; then 
1065         cmd_encrypt
="cat " && vbm 
"STATUS:$fn:cmd_encrypt:$cmd_encrypt"; 
1066         cmd_encrypt_suffix
="" && vbm 
"STATUS:$fn:cmd_encrypt_suffix:$cmd_encrypt_suffix"; 
1067         vbm 
"DEBUG :$fn:Encryption not enabled."; 
1070     # Handle encryption cases 
1071     ## Check age availability 
1072     if ! checkapp age
; then yell 
"ERROR:$fn:age not available. Exiting."; exit 1; fi 
1073     ## Parse '-r' options: validate and append pubkeys from argRecPubKeys to recPubKeysValid 
1074     if [[ "$optionRecArg" = "true" ]]; then 
1075         for pubkey 
in "${argRecPubKeys[@]}"; do # Validate recipient pubkey strings by forming test message 
1076             vbm 
"DEBUG :$fn:Testing pubkey string:$pubkey"; 
1077             if checkAgePubkey 
"$pubkey" && \
 
1078                     ( validateInput 
"$pubkey" "ssh_pubkey" || validateInput 
"$pubkey" "age_pubkey"); then 
1079                 #### Add validated pubkey to recPubKeysValid array 
1080                 recPubKeysValid
+=("$pubkey") && \
 
1081                     vbm 
"DEBUG :$fn:recPubkeysValid:pubkey added:$pubkey"; 
1083                 yell 
"ERROR:$fn:Exit code ""$?"". Invalid recipient pubkey string. Exiting."; exit 1; 
1086         vbm 
"STATUS:$fn:Finished processing argRecPubKeys array"; 
1087         vbm 
"DEBUG :$fn:Array of validated pubkeys:${recPubKeysValid[*]}"; 
1089     ## Parse '-R' options: validate and append pubkeys in argRecDir to recPubKeysValid 
1090     if [[ "$optionRecDir" = "true" ]]; then 
1091         ### Check that argRecDir is a directory 
1092         if [[ -d "$argRecDir" ]]; then 
1093             recipientDir
="$argRecDir" && \
 
1094                 vbm 
"STATUS:$fn:Recipient watch directory detected:\"$recipientDir\""; 
1095             #### Initialize variable indicating outcome of pubkey review 
1096             unset updateRecipients
 
1097             #### Add existing recipients from '-r' option 
1098             candRecPubKeysValid
=("${recPubKeysValid[@]}"); 
1099             #### Parse files in recipientDir 
1100             for file in "$recipientDir"/*; do 
1101                 ##### Read first line of each file 
1102                 recFileLine
="$(head -n1 "$file")" && \
 
1103                     vbm 
"STATUS:$fn:Checking if pubkey:\"$recFileLine\""; 
1104                 ##### check if first line is a valid pubkey 
1105                 if checkAgePubkey 
"$recFileLine" && \
 
1106                         ( validateInput 
"$recFileLine" "ssh_pubkey" || validateInput 
"$recFileLine" "age_pubkey"); then 
1107                     ###### T: add candidate pubkey to candRecPubKeysValid 
1108                     candRecPubKeysValid
+=("$recFileLine") && \
 
1109                         vbm 
"STATUS:$fn:RecDir pubkey is valid pubkey:\"$recFileLine\""; 
1111                     ###### F: throw warning; 
1112                     yell 
"ERROR:$fn:Invalid recipient file detected. Not modifying recipient list:$recFileLine"; 
1113                     updateRecipients
="false"; 
1116             #### Write candRecPubKeysValid array to recPubKeysValid if no invalid key detected 
1117             if ! [[ "$updateRecipients" = "false" ]]; then 
1118                 recPubKeysValid
=("${candRecPubKeysValid[@]}") && \
 
1119                     vbm 
"STATUS:$fn:Wrote candRecPubkeysValid to recPubKeysValid:\"${recPubKeysValid[*]}\""; 
1124     ## Form age recipient string from recPubKeysValid 
1125     for pubkey 
in "${recPubKeysValid[@]}"; do 
1126         recipients
="$recipients""-r '$pubkey' "; 
1127         vbm 
"STATUS:$fn:Added pubkey for forming age recipient string:""$pubkey"; 
1128         vbm 
"DEBUG :$fn:recipients:""$recipients";       
1131     ## Output cmd_encrypt, cmd_encrypt_suffix from recipients 
1132     cmd_encrypt
="age ""$recipients " && vbm 
"STATUS:$fn:cmd_encrypt:$cmd_encrypt"; 
1133     cmd_encrypt_suffix
=".age" && vbm 
"STATUS:$fn:cmd_encrypt_suffix:$cmd_encrypt_suffix"; 
1135     vbm 
"STATUS:$fn:Finished magicParseRecipients() function."; 
1136 } # Sets cmd_encrypt, cmd_encrypt_suffix from -r, -R args 
1137 magicSetScriptTTL
() { 
1138     #Desc: Sets script_TTL seconds from provided time_element string argument 
1139     #Usage: magicSetScriptTTL [str time_element] 
1140     #Input: arg1: string (Ex: scriptTTL_TE; "day" or "hour") 
1141     #Output: var: scriptTTL (integer seconds) 
1142     #Depends: timeUntilNextHour, timeUntilNextDay 
1143     local fn argTimeElement
 
1145     # Save function name 
1146     fn
="${FUNCNAME[0]}"; 
1148     vbm 
"STATUS:$fn:Starting magicSetScriptTTL() function."; 
1149     argTimeElement
="$1"; 
1150     if [[ "$argTimeElement" = "day" ]]; then 
1151         # Set script lifespan to end at start of next day 
1152         vbm 
"STATUS:$fn:Setting script lifespan to end at start of next day. argTimeElement:$argTimeElement"; 
1153         if ! scriptTTL
="$(timeUntilNextDay)"; then # sets scriptTTL, then checks exit code 
1154             if [[ "$scriptTTL" -eq 0 ]]; then 
1155                 ((scriptTTL
++)); # Add 1 because 0 would cause 'timeout' to never timeout. 
1156                 vbm 
"STATUS:$fn:scriptTTL:$scriptTTL"; 
1158             yell 
"ERROR:$fn:timeUntilNextDay exit code $?"; exit 1; 
1161     elif [[ "$argTimeElement" = "hour" ]]; then 
1162         # Set script lifespan to end at start of next hour 
1163         vbm 
"STATUS:$fn:Setting script lifespan to end at start of next hour. argTimeElement:$argTimeElement"; 
1164         if ! scriptTTL
="$(timeUntilNextHour)"; then # sets scriptTTL, then checks exit code 
1165             if [[ "$scriptTTL" -eq 0 ]]; then 
1166                 ((scriptTTL
++)); # Add 1 because 0 would cause 'timeout' to never timeout. 
1167                 vbm 
"STATUS:$fn:scriptTTL:$scriptTTL"; 
1169                 yell 
"ERROR:$fn:timeUntilNextHour exit code $?"; exit 1; 
1173         yell 
"ERROR:$fn:Invalid argument for setScriptTTL function:$argTimeElement"; exit 1; 
1175     vbm 
"STATUS:$fn:Finished magicSetScriptTTL() function."; 
1176 } # Set scriptTTL in seconds until next (day|hour). 
1177 magicWriteVersion
() { 
1178     # Desc: Appends time-stamped VERSION to pathout_tar 
1179     # Usage: magicWriteVersion 
1180     # Input: vars: pathout_tar, dir_tmp 
1181     # Input: vars: scriptVersion, scriptURL, ageVersion, ageURL, scriptHostname 
1182     # Input: array: recPubKeysValid 
1183     # Output: appends tar (pathout_tar) 
1184     # Depends: bash 5.0.3, dateTimeShort(), appendArgTar() 
1185     local fn fileoutVersion contentVersion pubKeyIndex pubKeyIndex
 
1187     # Save function name 
1188     fn
="${FUNCNAME[0]}"; 
1190     vbm 
"STATUS:$fn:Starting magicWriteVersion() function."; 
1191     # Set VERSION file name 
1192     fileoutVersion
="$(dateTimeShort)..VERSION"; 
1194     # Gather VERSION data in contentVersion 
1195     contentVersion
="scriptVersion=$scriptVersion"; 
1196     #contentVersion="$contentVersion""\\n"; 
1197     contentVersion
="$contentVersion""\\n""scriptName=$scriptName"; 
1198     contentVersion
="$contentVersion""\\n""scriptURL=$scriptURL"; 
1199     contentVersion
="$contentVersion""\\n""ageVersion=$ageVersion"; 
1200     contentVersion
="$contentVersion""\\n""ageURL=$ageURL"; 
1201     contentVersion
="$contentVersion""\\n""date=$(date --iso-8601=seconds)"; 
1202     contentVersion
="$contentVersion""\\n""hostname=$scriptHostname"; 
1203     ## Add list of recipient pubkeys 
1204     for pubkey 
in "${recPubKeysValid[@]}"; do 
1206         contentVersion
="$contentVersion""\\n""PUBKEY_$pubKeyIndex=$pubkey"; 
1208     ## Process newline escapes 
1209     contentVersion
="$(echo -e "$contentVersion")" 
1211     # Write contentVersion as file fileoutVersion and write-append to pathout_tar 
1212     appendArgTar 
"$contentVersion" "$fileoutVersion" "$pathout_tar" "$dir_tmp" && \
 
1213         vbm 
"STATUS:$fn:Appended $fileoutVersion to $pathout_tar"; 
1214     vbm 
"STATUS:$fn:Finished magicWriteVersion() function."; 
1215 } # write version data to pathout_tar via appendArgTar() 
1216 magicProcessWriteBuffer
() { 
1217     # Desc: process and write buffer 
1218     # In : vars: bufferTTL bufferTTL_STR scriptHostname label dir_tmp SECONDS 
1220     # Out: file:(pathout_tar) 
1221     # Depends: Bash 5.0.3, date 8.30, yell(), vbm(), dateTimeShort(), 
1222     ### Note: These arrays should all have the same number of elements: 
1223     ###       pathouts, fileouts, procFileExts, procStrings 
1225     local fn timeBufferStartLong timeBufferStart fileoutBasename
 
1226     local -a fileouts pathouts
 
1227     local writeCmd1 writeCmd2 writeCmd3 writeCmd4
 
1229     # Debug:Get function name 
1230     fn
="${FUNCNAME[0]}"; 
1232     vbm 
"STATUS:$fn:Started magicProcessWriteBuffer()."; 
1233     vbm 
"DEBUG :$fn:buffer array element count:${#buffer[@]}"; 
1234     vbm 
"DEBUG :$fn:buffer array first element:${buffer[0]}"; 
1235     vbm 
"DEBUG :$fn:buffer array last element :${buffer[-1]}"; 
1237     # Determine file paths (time is start of buffer period) 
1238     ## Calculate start time 
1239     timeBufferStartLong
="$(date --date="$bufferTTL seconds ago
" --iso-8601=seconds)" && \
 
1240         vbm 
"DEBUG :$fn:timeBufferStartLong:$timeBufferStartLong"; 
1241     timeBufferStart
="$(dateTimeShort "$timeBufferStartLong" )" && \
 
1242         vbm 
"DEBUG :$fn:timeBufferStart:$timeBufferStart"; # Note start time YYYYmmddTHHMMSS+zzzz (no separators) 
1243     ## Set common basename 
1244     fileoutBasename
="$timeBufferStart""--""$bufferTTL_STR""..""$scriptHostname""$label" && \
 
1245         vbm 
"STATUS:$fn:Set fileoutBasename to:$fileoutBasename"; 
1246     ## Determine output file name array 
1247     ### in: fileOutBasename cmd_compress_suffix cmd_encrypt_suffix procFileExts 
1248     for fileExt 
in "${procFileExts[@]}"; do 
1249         fileouts
+=("$fileoutBasename""$fileExt""$cmd_compress_suffix""$cmd_encrypt_suffix") && \
 
1250             vbm 
"STATUS:$fn:Added $fileExt to fileouts:${fileouts[*]}"; 
1252     for fileName 
in "${fileouts[@]}"; do 
1253         pathouts
+=("$dir_tmp"/"$fileName") && \
 
1254             vbm 
"STATUS:$fn:Added $fileName to pathouts:${pathouts[*]}"; 
1256     ## Update pathout_tar 
1259     # Process and write buffers to dir_tmp 
1260     ## Prepare command strings 
1261     writeCmd1
="printf \"%s\\\\n\" \"\${buffer[@]}\""; # printf "%s\\n" "${buffer[@]}" 
1262     #writeCmd2="" # NOTE: Specified by parsing array procStrings 
1263     writeCmd3
="$cmd_compress"; 
1264     writeCmd4
="$cmd_encrypt"; 
1266     ## Process buffer and write to dir_tmp 
1267     vbm 
"DEBUG :$fn:fileouts    element count:${#fileouts[@]}"; 
1268     vbm 
"DEBUG :$fn:pathouts    element count:${#pathouts[@]}"; 
1269     vbm 
"DEBUG :$fn:procStrings element count:${#pathouts[@]}"; 
1270     vbm 
"DEBUG :$fn:fileouts    contents:${fileouts[*]}"; 
1271     vbm 
"DEBUG :$fn:pathouts    contents:${pathouts[*]}"; 
1272     vbm 
"DEBUG :$fn:procStrings contents:${pathouts[*]}"; 
1273     for index 
in "${!pathouts[@]}"; do 
1274         writeCmd2
="${procStrings[$index]}"; 
1275         writeCmdAll
="$writeCmd1 | $writeCmd2 | $writeCmd3 | $writeCmd4" && vbm 
"STATUS:$fn:Assembled command:\"$writeCmdAll\""; 
1276         eval "$writeCmd1 | $writeCmd2 | $writeCmd3 | $writeCmd4" > "${pathouts[$index]}" && vbm 
"STATUS:$fn:Wrote command output to ${pathouts[$index]}"; 
1279     # Append dir_tmp files to pathout_tar 
1280     wait; # Wait to avoid collision with older magicProcessWriteBuffer() instances (see https://www.tldp.org/LDP/abs/html/x9644.html ) 
1281     for index 
in "${!pathouts[@]}"; do 
1282         tar --append --directory="$dir_tmp" --file="$pathout_tar" "${fileouts[$index]}" && \
 
1283             vbm 
"STATUS:$fn:Appended ${pathouts[$index]} to $pathout_tar"; 
1284         #appendFileTar "${pathouts[$index]}" "${fileouts[$index]}" "$pathout_tar" "$dir_tmp" && \ 
1287     # Remove secured chunks from dir_tmp 
1288     for path 
in "${pathouts[@]}"; do 
1289         rm "$path" && vbm 
"STATUS:$fn:Removed:$path"; 
1292     vbm 
"STATUS:$fn:Finished magicProcessWriteBuffer()."; 
1293 } # Process and Write buffer 
1297         cmd | bklog [ options ] 
1301                 Display help information. 
1303                 Display script version. 
1305                 Display debugging info. 
1308         -r, --recipient [ string pubkey ] 
1309                 Specify recipient. May be age or ssh pubkey. 
1310                 May be specified multiple times for multiple pubkeys. 
1311                 See https://github.com/FiloSottile/age 
1312         -o, --output [ path dir ] 
1313                 Specify output directory to save logs. This option is required 
1315         -p, --process-string [ filter command ] [ output file extension]  
1316                 Specify how to create and name a processed version of the stdin. 
1317                 For example, if stdin is 'nmea' location data: 
1319                 -p "gpsbabel -i nmea -f - -o gpx -F - " ".gpx" 
1321                 This option would cause the stdin to 'bklog' to be piped into 
1322                 the 'gpsbabel' command, interpreted as 'nmea' data, converted 
1323                 into 'gpx' format, and then appended to the output tar file 
1324                 as a file with a '.gpx' extension. 
1325                 This option may be specified multiple times in order to output 
1326                 results of multiple different processing methods. 
1327         -l, --label [ string ] 
1328                 Specify a label to be included in all output file names. 
1329                 Ex: 'location' if stdin is location data. 
1330         -w, --store-raw [ file extension ] 
1331                 Specify file extension of file within output tar that contains 
1332                 raw stdin data. The default behavior is to always save raw stdin 
1333                 data in a '.stdin' file. Example usage when 'bklog' receives 
1334                 'nmea' data from 'gpspipe -r': 
1338                 Stdin data is saved in a '.nmea' file within the output tar. 
1340                 Do not store raw stdin in output tar. 
1342                 Compress output with gzip (before encryption if enabled). 
1344                 Specify time zone. (ex: "America/New_York") 
1345         -t, --temp-dir [path dir] 
1346                 Specify parent directory for temporary working directory. 
1348         -R, --recipient-dir [path dir] 
1349                 Specify directory containing files whose first lines are 
1350                 to be interpreted as pubkey strings (see '-r' option). Only 
1351                 one directory may be specified. 
1352         -b, --buffer-ttl [integer] 
1353                 Specify custom buffer period in seconds (default: 300 seconds) 
1354         -B, --script-ttl [time element string] 
1355                 Specify custom script time-to-live in seconds (default: "day") 
1356                 Valid values: "day", "hour" 
1358     EXAMPLE: (bash script lines) 
1359     $ gpspipe -r | /bin/bash bklog -v -e -c -z "UTC" -t "/dev/shm" \ 
1360     -r age1mrmfnwhtlprn4jquex0ukmwcm7y2nxlphuzgsgv8ew2k9mewy3rs8u7su5 \ 
1361     -r age1ala848kqrvxc88rzaauc6vc5v0fqrvef9dxyk79m0vjea3hagclswu0lgq \ 
1362     -R ~/.config/bklog/recipients -w ".nmea" -b 300 -B "day" \ 
1363     -o ~/Sync/Logs -l "location" \ 
1364     -p "gpsbabel -i nmea -f - -o gpx -F - " ".gpx" \ 
1365     -p "gpsbabel -i nmea -f - -o kml -F - " ".kml" 
1367 } # Display information on how to use this script. 
1370     # Desc: Main function 
1373     # Outputs: file (pathout_tar) 
1377     # Debug:Get function name 
1378     fn
="${FUNCNAME[0]}"; 
1380     vbm 
"STATUS:$fn:Started function main()."; 
1382     processArguments 
"$@"; 
1383     ## Determine working directory 
1384     magicInitWorkingDir
; # Sets dir_tmp from argTempDirPriority 
1385     ## Set output encryption and compression option strings 
1386     ### React to "-e", "-r", and "-R" (encryption recipients) options 
1387     magicParseRecipients
; # Update cmd_encrypt, cmd_encrypt_suffix 
1388     ### React to "-c" ("compression") option 
1389     magicParseCompressionArg
; # Updates cmd_compress[_suffix] 
1390     ## React to "-b" and "-B" (custom buffer and script TTL) options 
1391     magicParseCustomTTL
; # Sets custom scriptTTL_TE and/or bufferTTL if specified 
1392     ## React to "-p" (user-supplied process command and file extension strings) options 
1393     magicParseProcessStrings
; # Sets arrays: procStrings, procFileExts 
1394     ## React to "-l" (output file label) option 
1395     magicParseLabel
; # sets label (ex: "_location") 
1397     # Perform secondary setup operations 
1398     ## Set script lifespan (scriptTTL from scriptTTL_TE) 
1399     magicSetScriptTTL 
"$scriptTTL_TE"; 
1400     ## File name substring (ISO-8601 duration from bufferTTL) 
1401     bufferTTL_STR
="$(timeDuration "$bufferTTL")" && vbm 
"DEBUG :$fn:bufferTTL_STR:$bufferTTL_STR"; 
1402     ## Init temp working dir 
1403     try mkdir 
"$dir_tmp" && vbm 
"DEBUG :$fn:Working dir created at dir_tmp:$dir_tmp"; 
1404     ## Initialize output tar (set pathout_tar) 
1406     ## Append VERSION file to tar 
1409     # Check vital apps, files, dirs 
1410     if ! checkapp 
tar && ! checkdir 
"$dirOut" "dir_tmp"; then 
1411         yell 
"ERROR:$fn:Critical components missing."; 
1412         displayMissing
; yell 
"Exiting."; exit 1; fi 
1414     # MAIN LOOP: Run until script TTL seconds pass 
1416     while [[ $SECONDS -lt "scriptTTL" ]]; do 
1417         vbm 
"STATUS:$fn:Starting buffer round:$bufferRound"; 
1418         bufferTOD
="$((SECONDS + bufferTTL))"; # Set buffer round time-of-death 
1419         # Consume stdin to fill buffer until buffer time-of-death (TOD) arrives 
1420         while read -r -t "$bufferTTL" line 
&& [[ $SECONDS -lt "$bufferTOD" ]]; do 
1421             # Append line to buffer array 
1424         # Create dir_tmp if missing 
1425         if ! [[ -d "$dir_tmp" ]]; then 
1426             yell 
"ERROR:$fn:dir_tmp existence failure:$dir_tmp"; 
1427             try mkdir 
"$dir_tmp" && vbm 
"DEBUG :$fn:Working dir recreated dir_tmp:$dir_tmp"; fi 
1428         # Update cmd_encrypt, cmd_encrypt_suffix 
1429         magicParseRecipients
; 
1430         # Export buffer to asynchronous processing. 
1431         magicProcessWriteBuffer 
& 
1432         unset buffer
; # Clear buffer array for next bufferRound 
1433         # Increment buffer round 
1439     try 
rm -r "$dir_tmp" && vbm 
"STATUS:$fn:Removed dir_tmp:$dir_tmp"; 
1441     vbm 
"STATUS:$fn:Finished function main()."; 
1444 #===END Declare local script functions=== 
1445 #==END Define script parameters== 
1447 #==BEGIN Perform work and exit== 
1448 main 
"$@" # Run main function. 
1450 #==END Perform work and exit== 
1452 # Author: Steven Baltakatei Sandoval;