2 # Desc: Records gps data 
   4 #==BEGIN Define script parameters== 
   5 ## Logging Behavior parameters 
   6 bufferTTL
="300"; # time between file writes 
   7 scriptTTL_TE
="day"; # (day|hour) 
   8 #### TZ="UTC"; export TZ; # Default time zone; overridden by '--time-zone=[str]' option 
   9 dirTmpDefault
="/dev/shm"; # Default parent of working directory 
  11 scriptTimeStart
=$(date +%Y%m%dT%H%M%S.%N); 
  12 PATH
="$HOME/.local/bin:$PATH";   # Add "$(systemd-path user-binaries)" path in case apps saved there 
  13 scriptHostname
=$(hostname);     # Save hostname of system running this script. 
  14 scriptVersion
="0.5.8";          # Define version of script. 
  15 scriptName
="bkgpslog";          # Define basename of script file. 
  16 scriptURL
="https://gitlab.com/baltakatei/ninfacyzga-01"; # Define wesite hosting this script. 
  17 ageVersion
="1.0.0-beta2";       # Define version of age (encryption program) 
  18 ageURL
="https://github.com/FiloSottile/age/releases/tag/v1.0.0-beta2"; # Define website hosting age. 
  20 declare -Ag appRollCall 
# Associative array for storing app status 
  21 declare -Ag fileRollCall 
# Associative array for storing file status 
  22 declare -Ag dirRollCall 
# Associative array for storing dir status 
  23 declare -a argRecPubKeys 
# for processArguments function 
  24 # declare -a errorHistory # for correcting buffer lag 
  26 ## Initialize variables 
  27 optionVerbose
=""; optionEncrypt
=""; optionCompress
=""; optionTmpDir
=""; 
  28 errReset
=0; bufferTTL_AdjFloat
=""; scriptTTL
=""; 
  30 #===BEGIN Declare local script functions=== 
  31 yell
() { echo "$0: $*" >&2; }      #o Yell, Die, Try Three-Fingered Claw technique 
  32 die
() { yell 
"$*"; exit 111; }     #o Ref/Attrib: https://stackoverflow.com/a/25515370 
  33 try
() { "$@" || die 
"cannot $*"; } #o  
  35     # Desc: If arg is a command, save result in assoc array 'appRollCall' 
  36     # Usage: checkapp arg1 arg2 arg3 ... 
  37     # Input: global assoc. array 'appRollCall' 
  38     # Output: adds/updates key(value) to global assoc array 'appRollCall' 
  40     #echo "DEBUG:$(date +%S.%N)..Starting checkapp function." 
  41     #echo "DEBUG:args: $@" 
  42     #echo "DEBUG:returnState:$returnState" 
  46         #echo "DEBUG:processing arg:$arg" 
  47         if command -v "$arg" 1>/dev
/null 
2>&1; then # Check if arg is a valid command 
  48             appRollCall
[$arg]="true"; 
  49             #echo "DEBUG:appRollCall[$arg]:"${appRollCall[$arg]} 
  50             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi 
  52             appRollCall
[$arg]="false"; returnState
="false"; 
  56     #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done 
  57     #echo "DEBUG:evaluating returnstate. returnState:"$returnState 
  59     #===Determine function return code=== 
  60     if [ "$returnState" = "true" ]; then 
  61         #echo "DEBUG:checkapp returns true for $arg"; 
  64         #echo "DEBUG:checkapp returns false for $arg"; 
  67 } # Check that app exists 
  69     # Desc: If arg is a file path, save result in assoc array 'fileRollCall' 
  70     # Usage: checkfile arg1 arg2 arg3 ... 
  71     # Input: global assoc. array 'fileRollCall' 
  72     # Output: adds/updates key(value) to global assoc array 'fileRollCall'; 
  73     # Output: returns 0 if app found, 1 otherwise 
  78         #echo "DEBUG:processing arg:$arg" 
  79         if [ -f "$arg" ]; then 
  80             fileRollCall
["$arg"]="true"; 
  81             #echo "DEBUG:fileRollCall[\"$arg\"]:"${fileRollCall["$arg"]} 
  82             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi 
  84             fileRollCall
["$arg"]="false"; returnState
="false"; 
  88     #for key in "${!fileRollCall[@]}"; do echo "DEBUG:fileRollCall key [$key] is:${fileRollCall[$key]}"; done 
  89     #echo "DEBUG:evaluating returnstate. returnState:"$returnState 
  91     #===Determine function return code=== 
  92     if [ "$returnState" = "true" ]; then 
  93         #echo "DEBUG:checkapp returns true for $arg"; 
  96         #echo "DEBUG:checkapp returns false for $arg"; 
  99 } # Check that file exists 
 101     # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' 
 102     # Usage: checkdir arg1 arg2 arg3 ... 
 103     # Input: global assoc. array 'dirRollCall' 
 104     # Output: adds/updates key(value) to global assoc array 'dirRollCall'; 
 105     # Output: returns 0 if app found, 1 otherwise 
 110         #echo "DEBUG:processing arg:$arg" 
 111         if [ -d "$arg" ]; then 
 112             dirRollCall
["$arg"]="true"; 
 113             #echo "DEBUG:dirRollCall[\"$arg\"]:"${dirRollCall["$arg"]} 
 114             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi 
 115         elif [ "$arg" = "" ]; then 
 116             dirRollCall
["$arg"]="false"; returnState
="false"; 
 122     #for key in "${!dirRollCall[@]}"; do echo "DEBUG:dirRollCall key [$key] is:${dirRollCall[$key]}"; done 
 123     #echo "DEBUG:evaluating returnstate. returnState:"$returnState 
 125     #===Determine function return code=== 
 126     if [ "$returnState" = "true" ]; then 
 127         #echo "DEBUG:checkapp returns true for $arg"; 
 130         #echo "DEBUG:checkapp returns false for $arg"; 
 133 } # Check that dir exists 
 135     echo "$@" 1>&2; # Define stderr echo function. 
 136 } # Define stderr message function. 
 139     echoerr 
"    bkgpslog [ options ]" 
 142     echoerr 
"    -h, --help" 
 143     echoerr 
"            Display help information." 
 145     echoerr 
"            Display script version." 
 146     echoerr 
"    -v, --verbose" 
 147     echoerr 
"            Display debugging info." 
 148     echoerr 
"    -e, --encrypt" 
 149     echoerr 
"            Encrypt output." 
 150     echoerr 
"    -r, --recipient [ string pubkey ]" 
 151     echoerr 
"            Specify recipient. May be age or ssh pubkey." 
 152     echoerr 
"            May be specified multiple times for multiple pubkeys." 
 153     echoerr 
"            See https://github.com/FiloSottile/age" 
 154     echoerr 
"    -o, --output [ path dir ]" 
 155     echoerr 
"            Specify output directory to save logs." 
 156     echoerr 
"    -c, --compress" 
 157     echoerr 
"            Compress output with gzip (before encryption if enabled)." 
 158     echoerr 
"    -z, --time-zone" 
 159     echoerr 
"            Specify time zone. (ex: \"America/New_York\")" 
 160     echoerr 
"    -t, --temp-dir [path dir]" 
 161     echoerr 
"            Specify parent directory for temporary working directory." 
 162     echoerr 
"            Default: \"/dev/shm\"" 
 163     echoerr 
"    -R, --recipient-dir [path dir]" 
 164     echoerr 
"            Specify directory containing files whose first lines are" 
 165     echoerr 
"            to be interpreted as pubkey strings (see \\'-r\\' option)." 
 166     echoerr 
"    -b, --buffer-ttl [integer]" 
 167     echoerr 
"            Specify custom buffer period in seconds (default: 300 seconds)" 
 168     echoerr 
"    -B, --script-ttl [integer]" 
 169     echoerr 
"            Specify custom script time-to-live in seconds (default: \"day\")" 
 171     echoerr 
"EXAMPLE: (bash script lines)" 
 172     echoerr 
"/bin/bash bkgpslog -v -e -c \\" 
 173     echoerr 
"-z \"UTC\" -t \"/dev/shm\" \\" 
 174     echoerr 
"-r age1mrmfnwhtlprn4jquex0ukmwcm7y2nxlphuzgsgv8ew2k9mewy3rs8u7su5 \\" 
 175     echoerr 
"-r age1ala848kqrvxc88rzaauc6vc5v0fqrvef9dxyk79m0vjea3hagclswu0lgq \\" 
 176     echoerr 
"-o ~/Sync/Location" 
 177 } # Display information on how to use this script. 
 179     echoerr 
"$scriptVersion" 
 180 } # Display script version. 
 182     # Description: Prints verbose message ("vbm") to stderr if optionVerbose is set to "true". 
 183     # Usage: vbm "DEBUG:verbose message here" 
 185     # Input: arg1: string 
 186     #        vars: optionVerbose 
 188     # Depends: echo 8, date 8 
 190     if [ "$optionVerbose" = "true" ]; then 
 191         functionTime
=$(date --iso-8601=ns); # Save current time in nano seconds. 
 192         echo "[$functionTime] ""$*" 1>&2; # Display argument text. 
 196     return 0; # Function finished. 
 197 } # Displays message if optionVerbose true 
 199     while [ ! $# -eq 0 ]; do   # While number of arguments ($#) is not (!) equal to (-eq) zero (0). 
 200         #echoerr "DEBUG:Starting processArguments while loop." 
 201         #echoerr "DEBUG:Provided arguments are:""$*" 
 203             -h | --help) showUsage
; exit 1;; # Display usage. 
 204             --version) showVersion
; exit 1;; # Show version 
 205             -v | --verbose) optionVerbose
="true"; vbm 
"DEBUG:Verbose mode enabled.";; # Enable verbose mode. 
 206             -o | --output) if [ -d "$2" ]; then dirOut
="$2"; vbm 
"DEBUG:dirOut:$dirOut"; shift; fi ;; # Define output directory. 
 207             -e | --encrypt) optionEncrypt
="true"; vbm 
"DEBUG:Encrypted output mode enabled.";; # Enable encryption 
 208             -r | --recipient) optionRecipients
="true"; argRecPubKeys
+=("$2"); vbm 
"STATUS:pubkey added:""$2"; shift;; # Add recipients 
 209             -c | --compress) optionCompress
="true"; vbm 
"DEBUG:Compressed output mode enabled.";; # Enable compression 
 210             -z | --time-zone) try setTimeZoneEV 
"$2"; shift;; # Set timestamp timezone 
 211             -t | --temp-dir) optionTmpDir
="true" && argTempDirPriority
="$2"; shift;; # Set time zone 
 212             -R | --recipient-dir) optionRecipients
="true"; optionRecDir
="true" && argRecDir
="$2"; shift;; # Add recipient watch dir 
 213             -b | --buffer-ttl) optionCustomBufferTTL
="true" && argCustomBufferTTL
="$2"; shift;; # Set custom buffer period (default: 300 seconds) 
 214             -B | --script-ttl) optionCustomScriptTTL_TE
="true" && argCustomScriptTTL
="$2"; shift;; # Set custom script TTL (default: "day") 
 215             *) echoerr 
"ERROR: Unrecognized argument: $1"; echoerr 
"STATUS:All arguments:$*"; exit 1;; # Handle unrecognized options. 
 219 } # Argument Processing 
 221     # Desc: Set time zone environment variable TZ 
 222     # Usage: setTimeZoneEV arg1 
 224     # Input: arg1: 'date'-compatible timezone string (ex: "America/New_York") 
 225     #        TZDIR env var (optional; default: "/usr/share/zoneinfo") 
 227     #         exit code 0 on success 
 228     #         exit code 1 on incorrect number of arguments 
 229     #         exit code 2 if unable to validate arg1 
 230     # Depends: yell, printenv, bash 5 
 231     # Tested on: Debian 10 
 232     local tzDir returnState argTimeZone
 
 235     if ! [[ $# -eq 1 ]]; then 
 236         yell 
"ERROR:Invalid argument count."; 
 240     # Read TZDIR env var if available 
 241     if printenv TZDIR 
1>/dev
/null 
2>&1; then 
 242         tzDir
="$(printenv TZDIR)"; 
 244         tzDir
="/usr/share/zoneinfo"; 
 248     if ! [[ -f "$tzDir"/"$argTimeZone" ]]; then 
 249         yell 
"ERROR:Invalid time zone argument."; 
 252     # Export ARG1 as TZ environment variable 
 253         TZ
="$argTimeZone" && export TZ 
&& returnState
="true"; 
 256     # Determine function return code 
 257     if [ "$returnState" = "true" ]; then 
 260 } # Exports TZ environment variable 
 262     # Desc: Report seconds until next day. 
 264     # Output: stdout: integer seconds until next day 
 265     # Output: exit code 0 if stdout > 0; 1 if stdout = 0; 2 if stdout < 0 
 266     # Usage: timeUntilNextDay 
 267     # Usage: if ! myTTL="$(timeUntilNextDay)"; then yell "ERROR in if statement"; exit 1; fi 
 268     # Depends: date 8, echo 8, yell, try 
 270     local returnState timeCurrent timeNextDay secondsUntilNextDay returnState
 
 271     timeCurrent
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second. 
 272     timeNextDay
="$(date -d "$timeCurrent next day" --iso-8601=date)"; # Produce timestamp of beginning of tomorrow with resolution of 1 second. 
 273     secondsUntilNextDay
="$(( $(date +%s -d "$timeNextDay") - $(date +%s -d "$timeCurrent") ))" ; # Calculate seconds until closest future midnight (res. 1 second). 
 274     if [[ "$secondsUntilNextDay" -gt 0 ]]; then 
 276     elif [[ "$secondsUntilNextDay" -eq 0 ]]; then 
 277         returnState
="warning_zero"; 
 278         yell 
"WARNING:Reported time until next day exactly zero."; 
 279     elif [[ "$secondsUntilNextDay" -lt 0 ]]; then 
 280         returnState
="warning_negative"; 
 281         yell 
"WARNING:Reported time until next day is negative."; 
 284     try 
echo "$secondsUntilNextDay"; # Report 
 286     # Determine function return code 
 287     if [[ "$returnState" = "true" ]]; then 
 289     elif [[ "$returnState" = "warning_zero" ]]; then 
 291     elif [[ "$returnState" = "warning_negative" ]]; then 
 294 } # Report seconds until next day 
 296     # Desc: Report seconds until next hour 
 298     # Output: stdout: integer seconds until next hour 
 299     # Output: exit code 0 if stdout > 0; 1 if stdout = 0; 2 if stdout < 0 
 300     # Usage: timeUntilNextHour 
 301     # Usage: if ! myTTL="$(timeUntilNextHour)"; then yell "ERROR in if statement"; exit 1; fi 
 303     local returnState timeCurrent timeNextHour secondsUntilNextHour
 
 304     timeCurrent
="$(date --iso-8601=seconds)"; # Produce `date`-parsable current timestamp with resolution of 1 second. 
 305     timeNextHour
="$(date -d "$timeCurrent next hour" --iso-8601=hours)"; # Produce `date`-parsable current time stamp with resolution of 1 second. 
 306     secondsUntilNextHour
="$(( $(date +%s -d "$timeNextHour") - $(date +%s -d "$timeCurrent") ))"; # Calculate seconds until next hour (res. 1 second). 
 307     if [[ "$secondsUntilNextHour" -gt 0 ]]; then 
 309     elif [[ "$secondsUntilNextHour" -eq 0 ]]; then 
 310         returnState
="warning_zero"; 
 311         yell 
"WARNING:Reported time until next hour exactly zero."; 
 312     elif [[ "$secondsUntilNextHour" -lt 0 ]]; then 
 313         returnState
="warning_negative"; 
 314         yell 
"WARNING:Reported time until next hour is negative."; 
 317     try 
echo "$secondsUntilNextHour"; # Report 
 319     # Determine function return code 
 320     if [[ "$returnState" = "true" ]]; then 
 322     elif [[ "$returnState" = "warning_zero" ]]; then 
 324     elif [[ "$returnState" = "warning_negative" ]]; then 
 327 } # Report seconds until next hour 
 329     # Desc: Timestamp without separators (YYYYmmddTHHMMSS+zzzz) 
 330     # Usage: dateTimeShort ([str date]) 
 332     # Input: arg1: 'date'-parsable timestamp string (optional) 
 333     # Output: stdout: timestamp (ISO-8601, no separators) 
 335     local argTime timeCurrent timeInput timeCurrentShort
 
 339     timeCurrent
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second. 
 340     # Decide to parse current or supplied date 
 341     ## Check if time argument empty 
 342     if [[ -z "$argTime" ]]; then 
 343         ## T: Time argument empty, use current time 
 344         timeInput
="$timeCurrent"; 
 346         ## F: Time argument exists, validate time 
 347         if date --date="$argTime" 1>/dev
/null 
2>&1; then 
 348             ### T: Time argument is valid; use it 
 349             timeInput
="$argTime"; 
 351             ### F: Time argument not valid; exit 
 352             yell 
"ERROR:Invalid time argument supplied. Exiting."; exit 1; 
 355     # Construct and deliver separator-les date string 
 356     timeCurrentShort
="$(date -d "$timeInput" +%Y%m%dT%H%M%S%z)"; 
 357     echo "$timeCurrentShort"; 
 358 } # Get YYYYmmddTHHMMSS±zzzz 
 360     # Desc: Date without separators (YYYYmmdd) 
 361     # Usage: dateShort ([str date]) 
 363     # Input: arg1: 'date'-parsable timestamp string (optional) 
 364     # Output: stdout: date (ISO-8601, no separators) 
 366     local argTime timeCurrent timeInput dateCurrentShort
 
 370     timeCurrent
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second. 
 371     # Decide to parse current or supplied date 
 372     ## Check if time argument empty 
 373     if [[ -z "$argTime" ]]; then 
 374         ## T: Time argument empty, use current time 
 375         timeInput
="$timeCurrent"; 
 377         ## F: Time argument exists, validate time 
 378         if date --date="$argTime" 1>/dev
/null 
2>&1; then 
 379             ### T: Time argument is valid; use it 
 380             timeInput
="$argTime"; 
 382             ### F: Time argument not valid; exit 
 383             yell 
"ERROR:Invalid time argument supplied. Exiting."; exit 1; 
 386     # Construct and deliver separator-les date string     
 387     dateCurrentShort
="$(date -d "$timeInput" +%Y%m%d)"; # Produce separator-less current date with resolution 1 day. 
 388     echo "$dateCurrentShort"; 
 391     # Desc: Given seconds, output ISO-8601 duration string 
 392     # Ref/Attrib: ISO-8601:2004(E), §4.4.4.2 Representations of time intervals by duration and context information 
 393     # Note: "1 month" ("P1M") is assumed to be "30 days" (see ISO-8601:2004(E), §2.2.1.2) 
 394     # Usage: timeDuration [1:seconds] ([2:precision]) 
 396     # Input: arg1: seconds as base 10 integer >= 0  (ex: 3601) 
 397     #        arg2: precision level (optional; default=2) 
 398     # Output: stdout: ISO-8601 duration string (ex: "P1H1S", "P2Y10M15DT10H30M20S") 
 399     #         exit code 0: success 
 400     #         exit code 1: error_input 
 401     #         exit code 2: error_unknown 
 402     # Example: 'timeDuration 111111 3' yields 'P1DT6H51M' 
 403     # Depends: date 8, bash 5, yell, 
 404     local argSeconds argPrecision precision returnState remainder
 
 405     local fullYears fullMonths fullDays fullHours fullMinutes fullSeconds
 
 406     local hasYears hasMonths hasDays hasHours hasMinutes hasSeconds
 
 407     local witherPrecision output
 
 408     local displayYears displayMonths displayDays displayHours displayMinutes displaySeconds
 
 410     argSeconds
="$1"; # read arg1 (seconds) 
 411     argPrecision
="$2"; # read arg2 (precision) 
 412     precision
=2; # set default precision 
 414     # Check that between one and two arguments is supplied 
 415     if ! { [[ $# -ge 1 ]] && [[ $# -le 2 ]]; }; then 
 416         yell 
"ERROR:Invalid number of arguments:$# . Exiting."; 
 417         returnState
="error_input"; fi 
 419     # Check that argSeconds provided 
 420     if [[ $# -ge 1 ]]; then 
 421         ## Check that argSeconds is a positive integer 
 422         if [[ "$argSeconds" =~ ^
[[:digit
:]]+$ 
]]; then 
 425             yell 
"ERROR:argSeconds not a digit."; 
 426             returnState
="error_input"; 
 429         yell 
"ERROR:No argument provided. Exiting."; 
 433     # Consider whether argPrecision was provided 
 434     if  [[ $# -eq 2 ]]; then 
 435         # Check that argPrecision is a positive integer 
 436         if [[ "$argPrecision" =~ ^
[[:digit
:]]+$ 
]] && [[ "$argPrecision" -gt 0 ]]; then 
 437         precision
="$argPrecision"; 
 439             yell 
"ERROR:argPrecision not a positive integer. (is $argPrecision ). Leaving early."; 
 440             returnState
="error_input"; 
 446     remainder
="$argSeconds" ; # seconds 
 447     ## Calculate full years Y, update remainder 
 448     fullYears
=$(( remainder / (365*24*60*60) )); 
 449     remainder
=$(( remainder - (fullYears*365*24*60*60) )); 
 450     ## Calculate full months M, update remainder 
 451     fullMonths
=$(( remainder / (30*24*60*60) )); 
 452     remainder
=$(( remainder - (fullMonths*30*24*60*60) )); 
 453     ## Calculate full days D, update remainder 
 454     fullDays
=$(( remainder / (24*60*60) )); 
 455     remainder
=$(( remainder - (fullDays*24*60*60) )); 
 456     ## Calculate full hours H, update remainder 
 457     fullHours
=$(( remainder / (60*60) )); 
 458     remainder
=$(( remainder - (fullHours*60*60) )); 
 459     ## Calculate full minutes M, update remainder 
 460     fullMinutes
=$(( remainder / (60) )); 
 461     remainder
=$(( remainder - (fullMinutes*60) )); 
 462     ## Calculate full seconds S, update remainder 
 463     fullSeconds
=$(( remainder / (1) )); 
 464     remainder
=$(( remainder - (remainder*1) )); 
 465     ## Check which fields filled 
 466     if [[ $fullYears -gt 0 ]]; then hasYears
="true"; else hasYears
="false"; fi 
 467     if [[ $fullMonths -gt 0 ]]; then hasMonths
="true"; else hasMonths
="false"; fi 
 468     if [[ $fullDays -gt 0 ]]; then hasDays
="true"; else hasDays
="false"; fi 
 469     if [[ $fullHours -gt 0 ]]; then hasHours
="true"; else hasHours
="false"; fi 
 470     if [[ $fullMinutes -gt 0 ]]; then hasMinutes
="true"; else hasMinutes
="false"; fi 
 471     if [[ $fullSeconds -gt 0 ]]; then hasSeconds
="true"; else hasSeconds
="false"; fi 
 473     ## Determine which fields to display (see ISO-8601:2004 §4.4.3.2) 
 474     witherPrecision
="false" 
 477     if $hasYears && [[ $precision -gt 0 ]]; then 
 479         witherPrecision
="true"; 
 481         displayYears
="false"; 
 483     if $witherPrecision; then ((precision
--)); fi; 
 486     if $hasMonths && [[ $precision -gt 0 ]]; then 
 487         displayMonths
="true"; 
 488         witherPrecision
="true"; 
 490         displayMonths
="false"; 
 492     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 493         displayMonths
="true"; 
 495     if $witherPrecision; then ((precision
--)); fi; 
 498     if $hasDays && [[ $precision -gt 0 ]]; then 
 500         witherPrecision
="true"; 
 504     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 507     if $witherPrecision; then ((precision
--)); fi; 
 510     if $hasHours && [[ $precision -gt 0 ]]; then 
 512         witherPrecision
="true"; 
 514         displayHours
="false"; 
 516     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 519     if $witherPrecision; then ((precision
--)); fi; 
 522     if $hasMinutes && [[ $precision -gt 0 ]]; then 
 523         displayMinutes
="true"; 
 524         witherPrecision
="true"; 
 526         displayMinutes
="false"; 
 528     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 529         displayMinutes
="true"; 
 531     if $witherPrecision; then ((precision
--)); fi; 
 535     if $hasSeconds && [[ $precision -gt 0 ]]; then 
 536         displaySeconds
="true"; 
 537         witherPrecision
="true"; 
 539         displaySeconds
="false"; 
 541     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 542         displaySeconds
="true"; 
 544     if $witherPrecision; then ((precision
--)); fi; 
 546     ## Determine whether or not the "T" separator is needed to separate date and time elements 
 547     if ( $displayHours || $displayMinutes || $displaySeconds); then 
 548         displayDateTime
="true"; else displayDateTime
="false"; fi 
 550     ## Construct duration output string 
 552     if $displayYears; then 
 553         output
=$output$fullYears"Y"; fi 
 554     if $displayMonths; then 
 555         output
=$output$fullMonths"M"; fi 
 556     if $displayDays; then 
 557         output
=$output$fullDays"D"; fi 
 558     if $displayDateTime; then 
 559         output
=$output"T"; fi 
 560     if $displayHours; then 
 561         output
=$output$fullHours"H"; fi 
 562     if $displayMinutes; then 
 563         output
=$output$fullMinutes"M"; fi 
 564     if $displaySeconds; then 
 565         output
=$output$fullSeconds"S"; fi 
 567     ## Output duration string to stdout 
 568     echo "$output" && returnState
="true"; 
 570     #===Determine function return code=== 
 571     if [ "$returnState" = "true" ]; then 
 573     elif [ "$returnState" = "error_input" ]; then 
 577         yell 
"ERROR:Unknown"; 
 581 } # Get duration (ex: PT10M4S ) 
 583     # Desc: Displays missing apps, files, and dirs 
 584     # Usage: displayMissing 
 586     # Input: associative arrays: appRollCall, fileRollCall, dirRollCall 
 587     # Output: stderr: messages indicating missing apps, file, or dirs 
 588     # Depends: bash 5, checkAppFileDir() 
 589     #==BEGIN Display errors== 
 590     #===BEGIN Display Missing Apps=== 
 591     missingApps
="Missing apps  :" 
 592     #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done 
 593     for key 
in "${!appRollCall[@]}"; do 
 594         value
="${appRollCall[$key]}" 
 595         if [ "$value" = "false" ]; then 
 596             #echo "DEBUG:Missing apps: $key => $value"; 
 597             missingApps
="$missingApps""$key " 
 601     if [ "$appMissing" = "true" ]; then  # Only indicate if an app is missing. 
 602         echo "$missingApps" 1>&2; 
 604     #===END Display Missing Apps=== 
 606     #===BEGIN Display Missing Files=== 
 607     missingFiles
="Missing files:" 
 608     #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done 
 609     for key 
in "${!fileRollCall[@]}"; do 
 610         value
="${fileRollCall[$key]}" 
 611         if [ "$value" = "false" ]; then 
 612             #echo "DEBUG:Missing files: $key => $value"; 
 613             missingFiles
="$missingFiles""$key " 
 617     if [ "$fileMissing" = "true" ]; then  # Only indicate if an app is missing. 
 618         echo "$missingFiles" 1>&2; 
 620     #===END Display Missing Files=== 
 622     #===BEGIN Display Missing Directories=== 
 623     missingDirs
="Missing dirs:" 
 624     #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done 
 625     for key 
in "${!dirRollCall[@]}"; do 
 626         value
="${dirRollCall[$key]}" 
 627         if [ "$value" = "false" ]; then 
 628             #echo "DEBUG:Missing dirs: $key => $value"; 
 629             missingDirs
="$missingDirs""$key " 
 633     if [ "$dirMissing" = "true" ]; then  # Only indicate if an dir is missing. 
 634         echo "$missingDirs" 1>&2; 
 636     #===END Display Missing Directories=== 
 638     #==END Display errors== 
 639 } # Display missing apps, files, dirs 
 640 magicSetScriptTTL
() { 
 641     #Desc: Sets script_TTL seconds from provided time_element string argument 
 642     #Usage: magicSetScriptTTL [str time_element] 
 643     #Input: arg1: string (Ex: scriptTTL_TE; "day" or "hour") 
 644     #Output: var: scriptTTL (integer seconds) 
 645     #Depends: timeUntilNextHour, timeUntilNextDay 
 648     if [[ "$argTimeElement" = "day" ]]; then 
 649             # Set script lifespan to end at start of next day 
 650         if ! scriptTTL
="$(timeUntilNextDay)"; then 
 651             if [[ "$scriptTTL" -eq 0 ]]; then 
 652             ((scriptTTL
++)); # Add 1 because 0 would cause 'timeout' to never timeout. 
 654             yell 
"ERROR: timeUntilNextDay exit code $?"; exit 1; 
 657     elif [[ "$argTimeElement" = "hour" ]]; then 
 658         # Set script lifespan to end at start of next hour 
 659         if ! scriptTTL
="$(timeUntilNextHour)"; then 
 660             if [[ "$scriptTTL" -eq 0 ]]; then 
 661                 ((scriptTTL
++)); # Add 1 because 0 would cause 'timeout' to never timeout. 
 663                 yell 
"ERROR: timeUntilNextHour exit code $?"; exit 1; 
 667         yell 
"ERROR:Invalid argument for setScriptTTL function:$argTimeElement"; exit 1; 
 669 } # Seconds until next (day|hour). 
 671     # Desc: Checks that a valid tar archive exists, creates one otherwise 
 672     # Usage: checkMakeTar [ path ] 
 674     # Input: arg1: path of tar archive 
 675     # Output: exit code 0 : tar readable 
 676     #         exit code 1 : tar missing; created 
 677     #         exit code 2 : tar not readable; moved; replaced 
 678     # Depends: bash 5, date 8, tar 1, try() 
 679     local pathTar returnFlag0 returnFlag1 returnFlag2
 
 682     # Check if file is a valid tar archive 
 683     if tar --list --file="$pathTar" 1>/dev
/null 
2>&1; then 
 684         ## T1: return success 
 685         returnFlag0
="tar valid"; 
 687         ## F1: Check if file exists 
 688         if [[ -f "$pathTar" ]]; then 
 690             try 
mv "$pathTar" "$pathTar""--broken--""$(date +%Y%m%dT%H%M%S)" && \
 
 691                 returnFlag1
="tar moved"; 
 696         ## F2: Create tar archive, return 0 
 697         try 
tar --create --file="$pathTar" --files-from=/dev
/null 
&& \
 
 698             returnFlag2
="tar created"; 
 701     # Determine function return code 
 702     if [[ "$returnFlag0" = "tar valid" ]]; then 
 704     elif [[ "$returnFlag2" = "tar created" ]] && ! [[ "$returnFlag1" = "tar moved" ]]; then 
 705         return 1; # tar missing so created 
 706     elif [[ "$returnFlag2" = "tar created" ]] && [[ "$returnFlag1" = "tar moved" ]]; then 
 707         return 2; # tar not readable so moved; replaced 
 709 } # checks if arg1 is tar; creates one otherwise 
 711     # Desc: Writes first argument to temporary file with arguments as options, then appends file to tar 
 712     # Usage: appendArgTar "$(echo "Data to be written.")" [name of file to be inserted] [tar path] [temp dir] ([cmd1] [cmd2] [cmd3] [cmd4]...) 
 714     # Input: arg1: data to be written 
 715     #        arg2: file name of file to be inserted into tar 
 716     #        arg3: tar archive path (must exist first) 
 717     #        arg4: temporary working dir 
 718     #        arg5+: command strings (ex: "gpsbabel -i nmea -f - -o kml -F - ") 
 719     # Output: file written to disk 
 720     # Example: decrypt multiple large files in parallel 
 721     #          appendArgTar "$(cat /tmp/largefile1.gpg)" "largefile1" $HOME/archive.tar /tmp "gpg --decrypt" & 
 722     #          appendArgTar "$(cat /tmp/largefile2.gpg)" "largefile2" $HOME/archive.tar /tmp "gpg --decrypt" & 
 723     #          appendArgTar "$(cat /tmp/largefile3.gpg)" "largefile3" $HOME/archive.tar /tmp "gpg --decrypt" & 
 724     # Depends: bash 5, tar 1, yell() 
 725     # Ref/Attrib: Using 'eval' to construct command strings https://askubuntu.com/a/476533 
 727     local fn fileName tarPath tmpDir cmd0 cmd1 cmd2 cmd3 cmd4
 
 731     #yell "DEBUG:STATUS:$fn:Finished appendArgTar()." 
 734     if ! [ -z "$2" ]; then fileName
="$2"; else yell 
"ERROR:$fn:Not enough arguments."; exit 1; fi 
 736     # Check tar path is a file 
 737     if [ -f "$3" ]; then tarPath
="$3"; else yell 
"ERROR:$fn:Tar archive arg not a file."; exit 1; fi 
 740     if ! [ -z "$4" ]; then tmpDir
="$4"; else yell 
"ERROR:$fn:No temporary working dir set."; exit 1; fi 
 742     # Set command strings 
 743     if ! [ -z "$5" ]; then cmd1
="$5"; else cmd1
="cat "; fi # command string 1 
 744     if ! [ -z "$6" ]; then cmd2
="$6"; else cmd2
="cat "; fi # command string 2 
 745     if ! [ -z "$7" ]; then cmd3
="$7"; else cmd3
="cat "; fi # command string 3 
 746     if ! [ -z "$8" ]; then cmd4
="$8"; else cmd4
="cat "; fi # command string 4 
 752     # yell "DEBUG:STATUS:$fn:cmd0:$cmd0" 
 753     # yell "DEBUG:STATUS:$fn:cmd1:$cmd1" 
 754     # yell "DEBUG:STATUS:$fn:cmd2:$cmd2" 
 755     # yell "DEBUG:STATUS:$fn:cmd3:$cmd3" 
 756     # yell "DEBUG:STATUS:$fn:cmd4:$cmd4" 
 757     # yell "DEBUG:STATUS:$fn:fileName:$fileName" 
 758     # yell "DEBUG:STATUS:$fn:tarPath:$tarPath" 
 759     # yell "DEBUG:STATUS:$fn:tmpDir:$tmpDir" 
 761     # Write to temporary working dir 
 762     eval "$cmd0 | $cmd1 | $cmd2 | $cmd3 | $cmd4" > "$tmpDir"/"$fileName"; 
 765     try 
tar --append --directory="$tmpDir" --file="$tarPath" "$fileName"; 
 766     #yell "DEBUG:STATUS:$fn:Finished appendArgTar()." 
 767 } # Append Bash var to file appended to Tar archive 
 769     # Desc: Processes first file and then appends to tar 
 770     # Usage: appendFileTar [file path] [name of file to be inserted] [tar path] [temp dir] ([cmd1] [cmd2] [cmd3] [cmd4]...) 
 772     # Input: arg1: path of file to be (processed and) written 
 773     #        arg2: name to use for file inserted into tar 
 774     #        arg3: tar archive path (must exist first) 
 775     #        arg4: temporary working dir 
 776     #        arg5+: command strings (ex: "gpsbabel -i nmea -f - -o kml -F - ") 
 777     # Output: file written to disk 
 778     # Example: decrypt multiple large files in parallel 
 779     #          appendFileTar /tmp/largefile1.gpg "largefile1" $HOME/archive.tar /tmp "gpg --decrypt" & 
 780     #          appendFileTar /tmp/largefile2.gpg "largefile2" $HOME/archive.tar /tmp "gpg --decrypt" & 
 781     #          appendFileTar /tmp/largefile3.gpg "largefile3" $HOME/archive.tar /tmp "gpg --decrypt" & 
 782     # Depends: bash 5, tar 1, yell() 
 784     local fn fileName tarPath tmpDir
 
 788     #yell "DEBUG:STATUS:$fn:Finished appendFileTar()." 
 791     if ! [ -z "$2" ]; then fileName
="$2"; else yell 
"ERROR:$fn:Not enough arguments."; exit 1; fi 
 792     # Check tar path is a file 
 793     if [ -f "$3" ]; then tarPath
="$3"; else yell 
"ERROR:$fn:Tar archive arg not a file:$3"; exit 1; fi 
 795     if ! [ -z "$4" ]; then tmpDir
="$4"; else yell 
"ERROR:$fn:No temporary working dir set."; exit 1; fi 
 796     # Set command strings 
 797     if ! [ -z "$5" ]; then cmd1
="$5"; else cmd1
="cat "; fi # command string 1 
 798     if ! [ -z "$6" ]; then cmd2
="$6"; else cmd2
="cat "; fi # command string 2 
 799     if ! [ -z "$7" ]; then cmd3
="$7"; else cmd3
="cat "; fi # command string 3 
 800     if ! [ -z "$8" ]; then cmd4
="$8"; else cmd4
="cat "; fi # command string 4 
 802     # Input command string 
 806     # yell "DEBUG:STATUS:$fn:cmd0:$cmd0" 
 807     # yell "DEBUG:STATUS:$fn:cmd1:$cmd1" 
 808     # yell "DEBUG:STATUS:$fn:cmd2:$cmd2" 
 809     # yell "DEBUG:STATUS:$fn:cmd3:$cmd3" 
 810     # yell "DEBUG:STATUS:$fn:cmd4:$cmd4" 
 811     # yell "DEBUG:STATUS:$fn:fileName:$fileName" 
 812     # yell "DEBUG:STATUS:$fn:tarPath:$tarPath" 
 813     # yell "DEBUG:STATUS:$fn:tmpDir:$tmpDir" 
 815     # Write to temporary working dir 
 816     eval "$cmd0 | $cmd1 | $cmd2 | $cmd3 | $cmd4" > "$tmpDir"/"$fileName"; 
 819     try 
tar --append --directory="$tmpDir" --file="$tarPath" "$fileName"; 
 820     #yell "DEBUG:STATUS:$fn:Finished appendFileTar()." 
 821 } # Append file to Tar archive 
 823     # Desc: Checks if string is an age-compatible pubkey 
 824     # Usage: checkAgePubkey [str pubkey] 
 826     # Input: arg1: string 
 827     # Output: return code 0: string is age-compatible pubkey 
 828     #         return code 1: string is NOT an age-compatible pubkey 
 829     #         age stderr (ex: there is stderr if invalid string provided) 
 830     # Depends: age (v0.1.0-beta2; https://github.com/FiloSottile/age/releases/tag/v1.0.0-beta2 ) 
 834     if echo "test" | age 
-a -r "$argPubkey" 1>/dev
/null
; then 
 841     # Desc: Validates Input 
 842     # Usage: validateInput [str input] [str input type] 
 844     # Input: arg1: string to validate 
 845     #        arg2: string specifying input type (ex:"ssh_pubkey") 
 846     # Output: return code 0: if input string matched specified string type 
 847     # Depends: bash 5, yell() 
 849     local fn argInput argType
 
 857     if [[ $# -gt 2 ]]; then yell 
"ERROR:$0:$fn:Too many arguments."; exit 1; fi; 
 860     if [[ -z "$argInput" ]]; then return 1; fi 
 864     ### Check for alnum/dash base64 (ex: "ssh-rsa AAAAB3NzaC1yc2EAAA") 
 865     if [[ "$argType" = "ssh_pubkey" ]]; then 
 866         if [[ "$argInput" =~ ^
[[:alnum
:]-]*[\ ]*[[:alnum
:]+/=]*$ 
]]; then 
 870     ### Check for age1[:bech32:] 
 871     if [[ "$argType" = "age_pubkey" ]]; then 
 872         if [[ "$argInput" =~ ^age1
[qpzry9x8gf2tvdw0s3jn54khce6mua7l
]*$ 
]]; then 
 876     if [[ "$argType" = "integer" ]]; then 
 877         if [[ "$argInput" =~ ^
[[:digit
:]]*$ 
]]; then 
 880     ## time element (year, month, week, day, hour, minute, second) 
 881     if [[ "$argType" = "time_element" ]]; then 
 882         if [[ "$argInput" = "year" ]] || \
 
 883                [[ "$argInput" = "month" ]] || \
 
 884                [[ "$argInput" = "week" ]] || \
 
 885                [[ "$argInput" = "day" ]] || \
 
 886                [[ "$argInput" = "hour" ]] || \
 
 887                [[ "$argInput" = "minute" ]] || \
 
 888                [[ "$argInput" = "second" ]]; then 
 891     # Return error if no condition matched. 
 893 } # Validates strings 
 895     # Desc: Get epoch nanoseconds 
 898     # Input: arg1: 'date'-parsable timestamp string (optional) 
 899     # Output: Nanoseconds since 1970-01-01 
 900     # Depends: date 8, cut 8, yell() 
 901     # Ref/Attrib: Force base 10 Bash arith with '10#'. https://stackoverflow.com/a/24777667 
 902     local argTime timeCurrent timeInput timeEpochFloat timeEpochInt
 
 903     local timeEpochNsFrac timeEpochNs
 
 908     timeCurrent
="$(date --iso-8601=ns)"; # Produce `date`-parsable current timestamp with resolution of 1 nanosecond. 
 910     # Decide to parse current or supplied time 
 911     ## Check if time argument empty 
 912     if [[ -z "$argTime" ]]; then 
 913         ## T: Time argument empty, use current time 
 914         timeInput
="$timeCurrent"; 
 916         ## F: Time argument exists, validate time 
 917         if date --date="$argTime" 1>/dev
/null 
2>&1; then 
 918             ### T: Time argument is valid; use it 
 919             timeInput
="$argTime"; 
 921             ### F: Time argument not valid; exit 
 922             yell 
"ERROR:Invalid time argument supplied. Exiting."; exit 1; 
 925     # Construct and deliver nanoseconds since 1970-01-01     
 926     timeEpochFloat
="$(date --date="$timeInput" +%s.%N)"; # Save ssss.NNNNNNNNN 
 927     timeEpochInt
="$(echo "$timeEpochFloat" | cut -d. -f1)"; # Get ssss 
 928     timeEpochNsFrac
="$(echo "$timeEpochFloat" | cut -d. -f2)"; # Get NNNNNNNNN 
 929     timeEpochNs
="$(( (10#"$timeEpochInt" * 10**9) + (10#"$timeEpochNsFrac") ))"; 
 931 } # Nanoseconds since 1970-01-01 
 932 magicBufferSleepPID
() { 
 933     # Desc: Compensates for lag so buffer rounds start every bufferTTL seconds 
 934     # Input: vars: bufferTTL, errReset 
 935     # # Input: array: errorHistory errResetx10e3 
 936     # Output: vars: bufferTTL_AdjFloat 
 937     # Re/Attrib: https://en.wikipedia.org/wiki/PID_controller#Standard_versus_parallel_(ideal)_form 
 938     local buffer_ttl_ns k_p t_i t_d
 
 939     local timeBufferStartNS timeBufferStartNSExp errNS errReset errRate
 
 940     local adj buffer_ttl_adj_ns buffer_ttl_adj_int buffer_ttl_adj_floatfrac
 
 941     # local errNSx10e3 errResetx10e3 errRatex10e3 
 942     # local errorHistorySize 
 944     # ## Define errorHistorySize 
 945     # errorHistorySize=100; 
 946     ## Define bufferTTL in nanoseconds 
 947     buffer_ttl_ns
=$((bufferTTL * 10**9)) && vbm 
"buffer_ttl_ns:$buffer_ttl_ns"; 
 949     ### PID Control factors 
 950     k_p
=1; # Gain for compensating buffer round lag 
 951     t_i
="$(((4)*buffer_ttl_ns/(1)))"; # Consider this number of past nanoseconds to eliminate error 
 952     t_d
="$(((1)*buffer_ttl_ns/(1)))"; # Predict value this number of nanoseconds into the future 
 954     # Calculate Error, errNS, in nanoseconds 
 956     timeBufferStartNS
="$(timeEpochNS)" && vbm 
"timeBufferStartNS   :$timeBufferStartNS"; 
 957     ## Calculate expected time (from start time, current buffer round number, nominal bufferTTL) 
 958     timeBufferStartNSExp
="$(( (timeBufferFirstNS) + (buffer_ttl_ns * bufferRound) ))" && vbm 
"timeBufferStartNSExp:$timeBufferStartNSExp"; 
 959     ## Calculate error (diff between timeBufferStartNSExp and timeBufferStartNS; usually negative) 
 960     errNS
="$(( timeBufferStartNSExp - timeBufferStartNS ))" && vbm 
"errNS:$errNS"; 
 961 #    errNSx10e3="$((errNS*10**3))" && vbm "errNSx10e3:$errNSx10e3"; 
 962     # ## Append error to errorHistory 
 963     # errorHistory+=("errNS"); 
 964     # ### Trim errorHistory array if over errorHistorySize 
 965     # while [[ "${#errorHistory[@]}" -gt "errorHistorySize" ]]; then do 
 966     #   unset "errorHistory[0]"; # remove oldest entry, creating sparse array 
 967     #   errorHistory=("${errorHistory[@]}"); # reindex sparse array 
 968     #   vbm "STATUS:Trimmed errorHistory array. Entry count:${#errorHistory[@]}"; 
 971     # Calculate errReset in nanoseconds^2 
 972     ## errReset = int(errHistory(t),wrt(delta_buffer_ttl)) 
 973     ## Integrate errorHistory with respect to time 
 974     # for value in "${errorHistory[@]}"; do 
 975     #   errReset=$(( errReset + ( value*buffer_ttl_ns ) )); 
 977     vbm 
"errReset(orig):$errReset" 
 978     errReset
="$(( (errReset + (errNS*buffer_ttl_ns)) ))" && vbm 
"errReset(post):$errReset"; 
 979 #    errResetx10e3="$(( ( errResetx10e3 + ( errNSx10e3 * buffer_ttl_ns ) )*10**3 ))" && vbm "errResetx10e3:$errResetx10e3"; 
 981     # Calculate errRate in nanoseconds per nanosecond 
 982     errRate
="$(( errNS / buffer_ttl_ns ))" && vbm 
"errRate:$errRate"; 
 983 #    errRatex10e3="$(( ( errNSx10e3 ) / buffer_ttl_ns ))" && vbm "errRatex10e3:$errRatex10e3"; 
 987     vbm 
"errResetTerm:$((errReset/t_i))"; 
 988     vbm 
"errRateTerm :$((errRate*t_d))"; 
 990     # Calculate PID control signal 
 991     ## adj = k_p * (errNS + errReset/t_i + errRate*t_d) 
 992     adj
="$(( k_p*(errNS + errReset/t_i + errRate*t_d) ))" && vbm 
"adj:$adj"; 
 993 #    adj="$((k_p*(errNSx10e3 + (errResetx10e3/t_i) + (errRatex10e3*t_d) )/(10**3)))" && vbm "adj:$adj"; 
 995     # Calculate bufferTTL_AdjFloat from adj (ns) 
 996     ## Calculate buffer_ttl_adj in nanoseconds (buffer_ttl_adj_ns = buffer_ttl_ns + adj) 
 997     buffer_ttl_adj_ns
="$((buffer_ttl_ns + adj))" && vbm 
"buffer_ttl_adj_ns:$buffer_ttl_adj_ns"; 
 998     ## Calculate integer seconds 
 999     buffer_ttl_adj_int
="$((buffer_ttl_adj_ns/(10**9)))" && vbm 
"buffer_ttl_adj_int:$buffer_ttl_adj_int"; 
1000     ### Catch negative integer seconds, set minimum of bufferTTL/10 seconds 
1001     if [[ "$buffer_ttl_adj_int" -le "$((bufferTTL/10))" ]]; then 
1002         buffer_ttl_adj_int
="$((bufferTTL/10))"; 
1003         yell 
"WARNING:Buffer lag adjustment yielded negative seconds."; 
1005     ## Calculate nanosecond remainder 
1007     buffer_ttl_adj_floatfrac
="$((buffer_ttl_adj_ns - (buffer_ttl_adj_int*(10**9)) ))" && vbm 
"buffer_ttl_adj_floatfrac:$buffer_ttl_adj_floatfrac"; 
1008     ### Calc absolute value of fraction (by removing '-' if present; see https://stackoverflow.com/a/47240327 
1009     buffer_ttl_adj_floatfrac
="${buffer_ttl_adj_floatfrac#-}" && vbm 
"buffer_ttl_adj_floatfrac:$buffer_ttl_adj_floatfrac"; 
1010     ## Form float bufferTTL_AdjFloat (function output) 
1011     bufferTTL_AdjFloat
="$buffer_ttl_adj_int".
"$buffer_ttl_adj_floatfrac" && vbm 
"bufferTTL_AdjFloat:$bufferTTL_AdjFloat"; 
1012     vbm 
"STATUS:Calculated adjusted bufferTTL (seconds):$bufferTTL_AdjFloat"; 
1013 } # Calc bufferTTL_AdjFloat so buffer starts every bufferTTL seconds 
1014 magicWriteVersion
() { 
1015     # Desc: Appends time-stamped VERSION to pathout_tar 
1016     # Usage: magicWriteVersion 
1017     # Input: vars: pathout_tar, dir_tmp 
1018     # Input: array: recPubKeysValid 
1019     # Input: scriptVersion, scriptURL, ageVersion, ageURL, scriptHostname 
1020     # Output: appends tar (pathout_tar) 
1021     # Depends: dateTimeShort, appendArgTar 
1022     local fileoutVersion contentVersion pubKeyIndex
 
1024     # Set VERSION file name 
1025     fileoutVersion
="$(dateTimeShort)..VERSION"; 
1027     # Gather VERSION data in contentVersion 
1028     contentVersion
="scriptVersion=$scriptVersion"; 
1029     #contentVersion="$contentVersion""\\n"; 
1030     contentVersion
="$contentVersion""\\n""scriptName=$scriptName"; 
1031     contentVersion
="$contentVersion""\\n""scriptURL=$scriptURL"; 
1032     contentVersion
="$contentVersion""\\n""ageVersion=$ageVersion"; 
1033     contentVersion
="$contentVersion""\\n""ageURL=$ageURL"; 
1034     contentVersion
="$contentVersion""\\n""date=$(date --iso-8601=seconds)"; 
1035     contentVersion
="$contentVersion""\\n""hostname=$scriptHostname"; 
1036     ## Add list of recipient pubkeys 
1037     for pubkey 
in "${recPubKeysValid[@]}"; do 
1039         contentVersion
="$contentVersion""\\n""PUBKEY_$pubKeyIndex=$pubkey"; 
1041     ## Process newline escapes 
1042     contentVersion
="$(echo -e "$contentVersion")" 
1044     # Write contentVersion as file fileoutVersion and write-append to pathout_tar 
1045     appendArgTar 
"$contentVersion" "$fileoutVersion" "$pathout_tar" "$dir_tmp"; 
1046 } # bkgpslog: write version data to pathout_tar via appendArgTar() 
1047 magicGatherWriteBuffer
() { 
1048     # Desc: bkgpslog-specific meta function for writing data to dir_tmp then appending each file to pathout_tar 
1049     # Inputs: vars: pathout_tar, cmd_conv_{nmea,gpx,kml}, cmd_{compress,encrypt} dir_tmp, 
1050     # Inputs: vars: bufferTTL bufferTTL_STR scriptHostname cmd_compress_suffix cmd_encrypt_suffix 
1051     # Output: file: (pathout_tar) 
1052     # Depends: tar 1, date 8, sleep 8, yell(), try(), vbm(), appendArgTar(), checkMakeTar() 
1053     # Depends: magicWriteVersion(), appendFileTar() 
1054     local fn pathoutBuffer timeBufferStartLong timeBufferStart fileoutBasename
 
1055     local fileout_nmea fileout_gpx fileout_kml pathout_nmea pathout_gpx pathout_kml pathout_tar
 
1057     # Debug:Get function name 
1058     fn
="${FUNCNAME[0]}"; 
1060     vbm 
"DEBUG:STATUS:$fn:Started magicGatherWriteBuffer()."; 
1062     # Create buffer file with unique name 
1063     pathoutBuffer
="$dir_tmp/buffer$SECONDS" && vbm 
"pathoutBuffer:$pathoutBuffer"; 
1065     timeout 
"$bufferTTL"s gpspipe 
-r -o "$pathoutBuffer" ; 
1066     timeBufferStartLong
="$(date --date="$bufferTTL seconds ago" --iso-8601=seconds)" && vbm 
"timeBufferStartLong:$timeBufferStartLong"; 
1067     timeBufferStart
="$(dateTimeShort "$timeBufferStartLong" )" && vbm 
"timeBufferStart:$timeBufferStart"; # Note start time 
1068     # Determine file paths (time is start of buffer period) 
1069     fileoutBasename
="$timeBufferStart""--""$bufferTTL_STR""..""$scriptHostname""_location" && vbm 
"STATUS:Set fileoutBasename to:$fileoutBasename"; 
1070     ## Files saved to dir_tmp 
1071     fileout_nmea
="$fileoutBasename".nmea
"$cmd_compress_suffix""$cmd_encrypt_suffix" && vbm 
"STATUS:Set fileout_nmea to:$fileout_nmea"; 
1072     fileout_gpx
="$fileoutBasename".gpx
"$cmd_compress_suffix""$cmd_encrypt_suffix" && vbm 
"STATUS:Set fileout_gpx to:$fileout_gpx"; 
1073     fileout_kml
="$fileoutBasename".kml
"$cmd_compress_suffix""$cmd_encrypt_suffix" && vbm 
"STATUS:Set fileout_kml to:$fileout_kml"; 
1074     pathout_nmea
="$dir_tmp"/"$fileout_nmea" && vbm 
"STATUS:Set pathout_nmea to:$pathout_nmea"; 
1075     pathout_gpx
="$dir_tmp"/"$fileout_gpx" && vbm 
"STATUS:Set pathout_gpx to:$pathout_gpx"; 
1076     pathout_kml
="$dir_tmp"/"$fileout_kml" && vbm 
"STATUS:Set pathout_kml to:$pathout_kml"; 
1077     ## Files saved to disk (dirOut) 
1078     ### one file per day (Ex: "20200731..hostname_location.[.gpx.gz].tar") 
1079     pathout_tar
="$dirOut"/"$(dateShort "$(date --date="$bufferTTL seconds ago" --iso-8601=seconds)")".."$scriptHostname""_location
""$cmd_compress_suffix""$cmd_encrypt_suffix".tar && \ 
1080         vbm "STATUS
:Set pathout_tar to
:$pathout_tar"; 
1082     vbm "STATUS
:fn             
:$fn"; 
1083     vbm "STATUS
:dir_tmp        
:$dir_tmp"; 
1084     vbm "STATUS
:pathout_tar    
:$pathout_tar"; 
1085     vbm "STATUS
:pathout_nmea   
:$pathout_nmea"; 
1086     vbm "STATUS
:pathout_gpx    
:$pathout_gpx"; 
1087     vbm "STATUS
:pathout_kml    
:$pathout_kml"; 
1088     vbm "STATUS
:bufferTTL      
:$bufferTTL"; 
1089     vbm "STATUS
:pathoutBuffer  
:$pathoutBuffer"; 
1090     vbm "STATUS
:timeBufferStart
:$timeBufferStart"; 
1091     vbm "fileoutBasename      
:$fileoutBasename"; 
1093     # Validate pathout_tar as tar. 
1094     checkMakeTar "$pathout_tar"; 
1095     ## Add VERSION file if checkMakeTar had to create a tar (exited 1) or replace one (exited 2) 
1096     vbm "exit status before magicWriteVersion
:$?
" 
1097     if [[ $? -eq 1 ]] || [[ $? -eq 2 ]]; then magicWriteVersion; fi 
1099     # Write bufferBash to pathout_tar 
1100     wait; # Wait to avoid collision with older magicWriteBuffer() instances (see https://www.tldp.org/LDP/abs/html/x9644.html ) 
1101     appendFileTar "$pathoutBuffer" "$fileout_nmea" "$pathout_tar" "$dir_tmp" "$cmd_conv_nmea" "$cmd_compress" "$cmd_encrypt"; # Write NMEA data 
1102     appendFileTar "$pathoutBuffer" "$fileout_gpx" "$pathout_tar" "$dir_tmp" "$cmd_conv_gpx" "$cmd_compress" "$cmd_encrypt"; # Write GPX file 
1103     appendFileTar "$pathoutBuffer" "$fileout_kml" "$pathout_tar" "$dir_tmp" "$cmd_conv_kml" "$cmd_compress" "$cmd_encrypt"; # Write KML file 
1105     # Remove secured chunks from dir_tmp 
1106     rm "$pathoutBuffer" "$pathout_nmea" "$pathout_gpx" "$pathout_kml"; 
1107     vbm "DEBUG
:STATUS
:$fn:Finished magicGatherWriteBuffer
().
"; 
1108 } # write buffer to disk 
1109 magicParseRecipientDir() { 
1110     # Desc: Updates recPubKeysValid with pubkeys in dir specified by '-R' option ("recipient directory
") 
1111     # Inputs:  vars: optionRecDir, argRecDir, optionEncrypt 
1112     #          arry: recPubKeysValid 
1113     # Outputs: arry: recPubKeysValid 
1114     # Depends: processArguments, 
1115     local recipientDir recFileLine updateRecipients  
1116     declare -a candRecPubKeysValid 
1118     # Check that '-e' and '-R' set 
1119     if [[ "$optionEncrypt" = "true
" ]] && [[ "$optionRecDir" = "true
" ]]; then 
1120         ### Check that argRecDir is a directory. 
1121         if [[ -d "$argRecDir" ]]; then 
1122             recipientDir="$argRecDir" && vbm "STATUS
:Recipient watch directory detected
:\"$recipientDir\""; 
1123             #### Initialize variable indicating outcome of pubkey review 
1124             unset updateRecipients 
1125             #### Add existing recipients 
1126             candRecPubKeysValid=("${recPubKeysValidStatic[@]}"); 
1127             #### Parse files in recipientDir 
1128             for file in "$recipientDir"/*; do 
1129                 ##### Read first line of each file 
1130                 recFileLine="$(head -n1 "$file")" && vbm "STATUS
:Checking 
if pubkey
:\"$recFileLine\""; 
1131                 ##### check if first line is a valid pubkey 
1132                 if checkAgePubkey "$recFileLine" && \ 
1133                         ( validateInput "$recFileLine" "ssh_pubkey
" || validateInput "$recFileLine" "age_pubkey
"); then 
1134                     ###### T: add candidate pubkey to candRecPubKeysValid 
1135                     candRecPubKeysValid+=("$recFileLine") && vbm "STATUS
:RecDir pubkey is valid pubkey
:\"$recFileLine\""; 
1137                     ###### F: throw warning; 
1138                     yell "ERROR
:Invalid recipient 
file detected. Not modifying recipient list.
" 
1139                     updateRecipients="false
"; 
1142             #### Write updated recPubKeysValid array to recPubKeysValid if no failure detected 
1143             if ! [[ "$updateRecipients" = "false
" ]]; then 
1144                 recPubKeysValid=("${candRecPubKeysValid[@]}") && vbm "STATUS
:Wrote candRecPubkeysValid to recPubKeysValid
:\"${recPubKeysValid[*]}\""; 
1147             yell "ERROR
:$0:Recipient directory 
$argRecDir does not exist. Exiting.
"; exit 1; 
1150     # Handle case if '-R' set but '-e' not set 
1151     if [[ ! "$optionEncrypt" = "true
" ]] && [[ "$optionRecDir" = "true
" ]]; then 
1152         yell "ERROR
: \\'-R\\' is 
set but 
\\'-e\\' is not 
set.
"; fi; 
1153 } # Update recPubKeysValid with argRecDir 
1154 magicParseRecipientArgs() { 
1155     # Desc: Parses recipient arguments specified by '-r' option 
1156     # Input:  vars: optionEncrypt, optionRecipients 
1157     #         arry: argRecPubKeys from processArguments() 
1158     # Output: vars: cmd_encrypt, cmd_encrypt_suffix 
1159     #         arry: recPubKeysValid, recPubKeysValidStatic 
1160     # Depends: checkapp(), checkAgePubkey(), validateInput(), processArguments() 
1163     # Check if encryption option active. 
1164     if [[ "$optionEncrypt" = "true
" ]] && [[ "$optionRecipients" = "true
" ]]; then  
1165         if checkapp age; then # Check that age is available. 
1166             for pubkey in "${argRecPubKeys[@]}"; do # Validate recipient pubkey strings by forming test message 
1167                 vbm "DEBUG
:Testing pubkey string
:$pubkey"; 
1168                 if checkAgePubkey "$pubkey" && \ 
1169                         ( validateInput "$pubkey" "ssh_pubkey
" || validateInput "$pubkey" "age_pubkey
"); then 
1170                     #### Form age recipient string 
1171                     recipients="$recipients""-r '$pubkey' "; 
1172                     vbm "STATUS
:Added pubkey 
for forming age recipient string
:""$pubkey"; 
1173                     vbm "DEBUG
:recipients
:""$recipients"; 
1174                     #### Add validated pubkey to recPubKeysValid array 
1175                     recPubKeysValid+=("$pubkey") && vbm "DEBUG
:recPubkeysValid
:pubkey added
:$pubkey"; 
1177                     yell "ERROR
:Exit code 
""$?
"". Invalid recipient pubkey string. Exiting.
"; exit 1; 
1180             vbm "DEBUG
:Finished processing argRecPubKeys array
"; 
1181             vbm "STATUS
:Array of validated pubkeys
:${recPubKeysValid[*]}"; 
1182             recPubKeysValidStatic=("${recPubKeysValid[@]}"); # Save static image of pubkeys validated by this function 
1184             ##  Form age command string 
1185             cmd_encrypt="age 
""$recipients " && vbm "cmd_encrypt
:$cmd_encrypt"; 
1186             cmd_encrypt_suffix=".age
" && vbm "cmd_encrypt_suffix
:$cmd_encrypt_suffix"; 
1188             yell "ERROR
:Encryption enabled but 
\"age
\" not found. Exiting.
"; exit 1; 
1191         cmd_encrypt="tee /dev
/null 
" && vbm "cmd_encrypt
:$cmd_encrypt"; 
1192         cmd_encrypt_suffix="" && vbm "cmd_encrypt_suffix
:$cmd_encrypt_suffix"; 
1193         vbm "DEBUG
:Encryption not enabled.
" 
1195     # Catch case if '-e' is set but '-r' or '-R' is not 
1196     if [[ "$optionEncrypt" = "true
" ]] && [[ ! "$optionRecipients" = "true
" ]]; then 
1197         yell "ERROR
:\\'-e\\' set but no 
\\'-r\\' or 
\\'-R\\' set.
"; exit 1; fi; 
1198     # Catch case if '-r' or '-R' set but '-e' is not 
1199     if [[ ! "$optionEncrypt" = "true
" ]] && [[ "$optionRecipients" = "true
" ]]; then 
1200         yell "ERROR
:\\'-r\\' or 
\\'-R\\' set but 
\\'-e\\' is not 
set.
"; exit 1; fi;  
1201 } # Populate recPubKeysValid with argRecPubKeys; form encryption cmd string and filename suffix 
1202 magicParseCompressionArg() { 
1203     # Desc: Parses compression arguments specified by '-c' option 
1204     # Input:  vars: optionCompress 
1205     # Output: cmd_compress, cmd_compress_suffix 
1206     # Depends: checkapp(), vbm(), gzip,  
1207     if [[ "$optionCompress" = "true
" ]]; then # Check if compression option active 
1208         if checkapp gzip; then # Check if gzip available 
1209             cmd_compress="gzip " && vbm "cmd_compress
:$cmd_compress"; 
1210             cmd_compress_suffix=".gz
" && vbm "cmd_compress_suffix
:$cmd_compress_suffix"; 
1212             yell "ERROR
:Compression enabled but 
\"gzip\" not found. Exiting.
"; exit 1; 
1215         cmd_compress="tee /dev
/null 
" && vbm "cmd_compress
:$cmd_compress"; 
1216         cmd_compress_suffix="" && vbm "cmd_compress_suffix
:$cmd_compress_suffix"; 
1217         vbm "DEBUG
:Compression not enabled.
"; 
1219 } # Form compression cmd string and filename suffix 
1220 magicInitWorkingDir() { 
1221     # Desc: Determine temporary working directory from defaults or user input 
1222     # Usage: magicInitWorkingDir 
1223     # Input:  vars: optionTmpDir, argTempDirPriority, dirTmpDefault 
1224     # Input:  vars: scriptTimeStart 
1225     # Output: vars: dir_tmp 
1226     # Depends: processArguments(), vbm(), yell() 
1227     # Parse '-t' option (user-specified temporary working dir) 
1228     ## Set dir_tmp_parent to user-specified value if specified 
1229     local dir_tmp_parent 
1231     if [[ "$optionTmpDir" = "true
" ]]; then 
1232         if [[ -d "$argTempDirPriority" ]]; then 
1233             dir_tmp_parent="$argTempDirPriority";  
1235             yell "WARNING
:Specified temporary working directory not valid
:$argTempDirPriority"; 
1236             exit 1; # Exit since user requires a specific temp dir and it is not available. 
1239     ## Set dir_tmp_parent to default or fallback otherwise 
1240         if [[ -d "$dirTmpDefault" ]]; then 
1241             dir_tmp_parent="$dirTmpDefault"; 
1242         elif [[ -d /tmp ]]; then 
1243             yell "WARNING
:$dirTmpDefault not available. Falling back to 
/tmp .
"; 
1244             dir_tmp_parent="/tmp
"; 
1246             yell "ERROR
:No valid working directory available. Exiting.
"; 
1250     ## Set dir_tmp using dir_tmp_parent and nonce (scriptTimeStart) 
1251     dir_tmp="$dir_tmp_parent"/"$scriptTimeStart""..bkgpslog
" && vbm "DEBUG
:Set dir_tmp to
:$dir_tmp"; # Note: removed at end of main(). 
1252 } # Sets working dir 
1253 magicParseCustomTTL() { 
1254     # Desc: Set user-specified TTLs for buffer and script 
1255     # Input: vars: argCustomBufferTTL (integer), argCustomScriptTTL_TE (string) 
1256     # Input: vars: optionCustomBufferTTL, optionCustomScriptTTL_TE 
1257     # Input: vars: bufferTTL (integer), scriptTTL_TE (string) 
1258     # Output: bufferTTL (integer), scriptTTL_TE (string) 
1259     # Depends validateInput(), showUsage(), yell 
1261     # React to '-b, --buffer-ttl' option 
1262     if [[ "$optionCustomBufferTTL" = "true
" ]]; then 
1263         ## T: Check if argCustomBufferTTL is an integer 
1264         if validateInput "$argCustomBufferTTL" "integer
"; then 
1265             ### T: argCustomBufferTTL is an integer 
1266             bufferTTL="$argCustomBufferTTL" && vbm "Custom bufferTTL from 
-b:$bufferTTL"; 
1268             ### F: argcustomBufferTTL is not an integer 
1269             yell "ERROR
:Invalid integer argument 
for custom buffer 
time-to-live.
"; showUsage; exit 1; 
1271         ## F: do not change bufferTTL 
1274     # React to '-B, --script-ttl' option 
1275     if [[ "$optionCustomScriptTTL_TE" = "true
" ]]; then 
1276         ## T: Check if argCustomScriptTTL is a time element (ex: "day
", "hour
") 
1277         if validateInput "$argCustomScriptTTL" "time_element
"; then 
1278             ### T: argCustomScriptTTL is a time element 
1279             scriptTTL_TE="$argCustomScriptTTL" && vbm "Custom scriptTTL_TE from 
-B:$scriptTTL_TE"; 
1281             ### F: argcustomScriptTTL is not a time element 
1282             yell "ERROR
:Invalid 
time element argument 
for custom 
script time-to-live.
"; showUsage; exit 1; 
1284         ## F: do not change scriptTTL_TE 
1286 } # Sets custom script or buffer TTL if specified 
1290     # DEBUG: Print environment variables 
1291     vbm "echo $(printenv)"; 
1293     processArguments "$@
"; 
1294     ## Act upon arguments 
1295     ### Determine working directory 
1296     magicInitWorkingDir; # Sets dir_tmp from argTempDirPriority 
1297     ### Set output encryption and compression option strings 
1298     #### React to "-r" ("encryption recipients
") option 
1299     magicParseRecipientArgs; # Updates recPubKeysValid, cmd_encrypt[_suffix] from argRecPubKeys 
1300     #### React to "-c" ("compression
") option 
1301     magicParseCompressionArg; # Updates cmd_compress[_suffix] 
1302     #### React to "-R" ("recipient directory
") option 
1303     magicParseRecipientDir; # Updates recPubKeysValid 
1304     #### React to custom buffer and script TTL options ("-b", "-B") 
1305     magicParseCustomTTL; # Sets custom scriptTTL_TE and/or bufferTTL if specified 
1307     # Check that critical apps and dirs are available, display missing ones. 
1308     if ! checkapp gpspipe tar && ! checkdir "$dirOut" "dir_tmp
"; then 
1309         yell "ERROR
:Critical components missing.
"; 
1310         displayMissing; yell "Exiting.
"; exit 1; fi 
1312     # Set script lifespan (scriptTTL from scriptTTL_TE) 
1313     magicSetScriptTTL "$scriptTTL_TE"; 
1314     ## Note: scriptTTL_TE is time element string (ex: "day
") while scriptTTL is integer seconds 
1316     # File name substring (ISO-8601 duration from bufferTTL) 
1317     bufferTTL_STR="$(timeDuration "$bufferTTL")" && vbm "DEBUG
:bufferTTL_STR
:$bufferTTL_STR"; 
1319     # Init temp working dir 
1320     try mkdir "$dir_tmp" && vbm "DEBUG
:Working dir created 
at dir_tmp
:$dir_tmp"; 
1322     # Initialize 'tar' archive 
1323     ## Define output tar path (note: each day gets *one* tar file (Ex: "20200731..hostname_location.
[.gpx.gz
].
tar")) 
1324     pathout_tar="$dirOut"/"$(dateShort "$(date --date="$bufferTTL seconds ago" --iso-8601=seconds)")"..
"$scriptHostname""_location""$cmd_compress_suffix""$cmd_encrypt_suffix".
tar && \
 
1325         vbm 
"STATUS:Set pathout_tar to:$pathout_tar"; 
1326     ## Check that pathout_tar is a tar. Rename old and create empty one otherwise. 
1327     checkMakeTar 
"$pathout_tar" && vbm 
"DEBUG:Confirmed or Created to be a tar:$pathout_tar"; 
1328     ## Append VERSION file to pathout_tar 
1331     # Define GPS conversion commands 
1332     cmd_conv_nmea
="tee /dev/null " && vbm 
"STATUS:Set cmd_conv_nmea to:$cmd_conv_nmea"; # tee as passthrough 
1333     cmd_conv_gpx
="gpsbabel -i nmea -f - -o gpx -F - " && vbm 
"STATUS:Set cmd_conv_gpx to:$cmd_conv_gpx"; # convert NMEA to GPX 
1334     cmd_conv_kml
="gpsbabel -i nmea -f - -o kml -F - " && vbm 
"STATUS:Set cmd_conv_kml to:$cmd_conv_kml"; # convert NMEA to KML 
1336     # MAIN LOOP:Record gps data until script lifespan ends 
1337     timeBufferFirstNS
="$(timeEpochNS)"; bufferRound
=0; bufferTTL_AdjFloat
="$bufferTTL"; 
1338     while [[ "$SECONDS" -lt "$scriptTTL" ]]; do 
1339         if ! [[ -d "$dir_tmp" ]]; then yell 
"ERROR:dir_tmp existence failure:$dir_tmp"; try 
mkdir "$dir_tmp" && vbm 
"DEBUG:Working dir recreated dir_tmp:$dir_tmp"; fi 
1340         magicParseRecipientDir
; 
1341         magicGatherWriteBuffer 
& 
1342         sleep "$bufferTTL_AdjFloat"; # adjusted by magicBufferSleepPID 
1344         magicBufferSleepPID
; # Calculates bufferTTL_AdjFloat from bufferTTL given buffer expected start time vs. actual 
1349     try 
rm -r "$dir_tmp" && vbm 
"Removed dir_tmp:$dir_tmp"; 
1351     vbm 
"STATUS:Main function finished."; 
1353 #===END Declare local script functions=== 
1354 #==END Define script parameters== 
1357 #==BEGIN Perform work and exit== 
1358 main 
"$@" # Run main function. 
1360 #==END Perform work and exit== 
1362 # Author: Steven Baltakatei Sandoval;