3 # Desc: Records gps data until midnight 
   4 # Author: Steven Baltakatei Sandoval; License: GPLv3+ 
   5 # Usage: bkgpslog -o [output dir] 
   7 #==BEGIN Define script parameters== 
   8 ## Logging Behavior parameters 
   9 BUFFER_TTL
="300"; # time between file writes 
  10 SCRIPT_TTL_TE
="day"; # (day|hour) 
  11 #### TZ="UTC"; export TZ; # Default time zone; overridden by '--time-zone=[str]' option 
  12 DIR_TMP_DEFAULT
="/dev/shm"; # Default parent of working directory 
  14 SCRIPT_TIME_START
=$(date +%Y%m%dT%H%M%S.%N); 
  15 PATH
="$HOME/.local/bin:$PATH";   # Add "$(systemd-path user-binaries)" path in case apps saved there 
  16 SCRIPT_HOSTNAME
=$(hostname);     # Save hostname of system running this script. 
  17 SCRIPT_VERSION
="0.4.4-alpha";          # Define version of script. 
  18 SCRIPT_NAME
="bkgpslog";          # Define basename of script file. 
  19 SCRIPT_URL
="https://gitlab.com/baltakatei/ninfacyzga-01"; # Define wesite hosting this script. 
  20 AGE_VERSION
="1.0.0-beta2";       # Define version of age (encryption program) 
  21 AGE_URL
="https://github.com/FiloSottile/age/releases/tag/v1.0.0-beta2"; # Define website hosting age. 
  23 declare -Ag appRollCall 
# Associative array for storing app status 
  24 declare -Ag fileRollCall 
# Associative array for storing file status 
  25 declare -Ag dirRollCall 
# Associative array for storing dir status 
  26 declare -a argRecPubKeys 
# for processArguments function 
  27 # declare -a errorHistory # for correcting buffer lag 
  29 ## Initialize variables 
  30 OPTION_VERBOSE
=""; OPTION_ENCRYPT
=""; OPTION_COMPRESS
=""; OPTION_TMPDIR
=""; 
  31 errResetx10e3
=0; BUFFER_TTL_ADJ_FLOAT
=""; 
  32 ### PID Control factors 
  33 K_P
=10; # Gain for compensating buffer round lag 
  34 T_I
=2; # Consider this number of past buffer rounds to eliminate error 
  35 T_D
=1; # Predict value this number of buffer rounds into the future 
  37 #===BEGIN Declare local script functions=== 
  39     # Desc: If arg is a command, save result in assoc array 'appRollCall' 
  40     # Usage: checkapp arg1 arg2 arg3 ... 
  41     # Input: global assoc. array 'appRollCall' 
  42     # Output: adds/updates key(value) to global assoc array 'appRollCall' 
  44     #echo "DEBUG:$(date +%S.%N)..Starting checkapp function." 
  45     #echo "DEBUG:args: $@" 
  46     #echo "DEBUG:returnState:$returnState" 
  50         #echo "DEBUG:processing arg:$arg" 
  51         if command -v "$arg" 1>/dev
/null 
2>&1; then # Check if arg is a valid command 
  52             appRollCall
[$arg]="true"; 
  53             #echo "DEBUG:appRollCall[$arg]:"${appRollCall[$arg]} 
  54             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi 
  56             appRollCall
[$arg]="false"; returnState
="false"; 
  60     #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done 
  61     #echo "DEBUG:evaluating returnstate. returnState:"$returnState 
  63     #===Determine function return code=== 
  64     if [ "$returnState" = "true" ]; then 
  65         #echo "DEBUG:checkapp returns true for $arg"; 
  68         #echo "DEBUG:checkapp returns false for $arg"; 
  71 } # Check that app exists 
  73     # Desc: If arg is a file path, save result in assoc array 'fileRollCall' 
  74     # Usage: checkfile arg1 arg2 arg3 ... 
  75     # Input: global assoc. array 'fileRollCall' 
  76     # Output: adds/updates key(value) to global assoc array 'fileRollCall'; 
  77     # Output: returns 0 if app found, 1 otherwise 
  82         #echo "DEBUG:processing arg:$arg" 
  83         if [ -f "$arg" ]; then 
  84             fileRollCall
["$arg"]="true"; 
  85             #echo "DEBUG:fileRollCall[\"$arg\"]:"${fileRollCall["$arg"]} 
  86             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi 
  88             fileRollCall
["$arg"]="false"; returnState
="false"; 
  92     #for key in "${!fileRollCall[@]}"; do echo "DEBUG:fileRollCall key [$key] is:${fileRollCall[$key]}"; done 
  93     #echo "DEBUG:evaluating returnstate. returnState:"$returnState 
  95     #===Determine function return code=== 
  96     if [ "$returnState" = "true" ]; then 
  97         #echo "DEBUG:checkapp returns true for $arg"; 
 100         #echo "DEBUG:checkapp returns false for $arg"; 
 103 } # Check that file exists 
 105     # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' 
 106     # Usage: checkdir arg1 arg2 arg3 ... 
 107     # Input: global assoc. array 'dirRollCall' 
 108     # Output: adds/updates key(value) to global assoc array 'dirRollCall'; 
 109     # Output: returns 0 if app found, 1 otherwise 
 114         #echo "DEBUG:processing arg:$arg" 
 115         if [ -d "$arg" ]; then 
 116             dirRollCall
["$arg"]="true"; 
 117             #echo "DEBUG:dirRollCall[\"$arg\"]:"${dirRollCall["$arg"]} 
 118             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi 
 119         elif [ "$arg" = "" ]; then 
 120             dirRollCall
["$arg"]="false"; returnState
="false"; 
 126     #for key in "${!dirRollCall[@]}"; do echo "DEBUG:dirRollCall key [$key] is:${dirRollCall[$key]}"; done 
 127     #echo "DEBUG:evaluating returnstate. returnState:"$returnState 
 129     #===Determine function return code=== 
 130     if [ "$returnState" = "true" ]; then 
 131         #echo "DEBUG:checkapp returns true for $arg"; 
 134         #echo "DEBUG:checkapp returns false for $arg"; 
 137 } # Check that dir exists 
 139 # Yell, Die, Try Three-Fingered Claw technique 
 140 # Ref/Attrib: https://stackoverflow.com/a/25515370 
 141 yell
() { echo "$0: $*" >&2; } 
 142 die
() { yell 
"$*"; exit 111; } 
 143 try
() { "$@" || die 
"cannot $*"; } 
 146     echo "$@" 1>&2; # Define stderr echo function. 
 147 } # Define stderr message function. 
 150     echoerr 
"    bkgpslog [ options ]" 
 153     echoerr 
"    -h, --help" 
 154     echoerr 
"            Display help information." 
 156     echoerr 
"            Display script version." 
 157     echoerr 
"    -v, --verbose" 
 158     echoerr 
"            Display debugging info." 
 159     echoerr 
"    -e, --encrypt" 
 160     echoerr 
"            Encrypt output." 
 161     echoerr 
"    -r, --recipient [ string pubkey ]" 
 162     echoerr 
"            Specify recipient. May be age or ssh pubkey." 
 163     echoerr 
"            May be specified multiple times for multiple pubkeys." 
 164     echoerr 
"            See https://github.com/FiloSottile/age" 
 165     echoerr 
"    -o, --output [ path dir ]" 
 166     echoerr 
"            Specify output directory to save logs." 
 167     echoerr 
"    -c, --compress" 
 168     echoerr 
"            Compress output with gzip (before encryption if enabled)." 
 169     echoerr 
"    -z, --time-zone" 
 170     echoerr 
"            Specify time zone. (ex: \"America/New_York\")" 
 171     echoerr 
"    -t, --temp-dir [path dir]" 
 172     echoerr 
"            Specify parent directory for temporary working directory." 
 173     echoerr 
"            Default: \"/dev/shm\"" 
 174     echoerr 
"    -R, --recipient-dir [path dir]" 
 175     echoerr 
"            Specify directory containing files whose first lines are" 
 176     echoerr 
"            to be interpreted as pubkey strings (see \\'-r\\' option)." 
 177     echoerr 
"    -b, --buffer-ttl [integer]" 
 178     echoerr 
"            Specify custom buffer period in seconds (default: 300 seconds)" 
 179     echoerr 
"    -B, --script-ttl [integer]" 
 180     echoerr 
"            Specify custom script time-to-live in seconds (default: \"day\")" 
 182     echoerr 
"EXAMPLE: (bash script lines)" 
 183     echoerr 
"/bin/bash bkgpslog -v -e -c \\" 
 184     echoerr 
"-z \"UTC\" -t \"/dev/shm\" \\" 
 185     echoerr 
"-r age1mrmfnwhtlprn4jquex0ukmwcm7y2nxlphuzgsgv8ew2k9mewy3rs8u7su5 \\" 
 186     echoerr 
"-r age1ala848kqrvxc88rzaauc6vc5v0fqrvef9dxyk79m0vjea3hagclswu0lgq \\" 
 187     echoerr 
"-o ~/Sync/Location" 
 188 } # Display information on how to use this script. 
 190     echoerr 
"$SCRIPT_VERSION" 
 191 } # Display script version. 
 193     # Usage: vbm "DEBUG:verbose message here" 
 194     # Description: Prints verbose message ("vbm") to stderr if OPTION_VERBOSE is set to "true". 
 196     #   - OPTION_VERBOSE  variable set by processArguments function. (ex: "true", "false") 
 197     #   - "$@"            positional arguments fed to this function. 
 199     # Script function dependencies: echoerr 
 200     # External function dependencies: echo 
 201     # Last modified: 2020-04-11T23:57Z 
 202     # Last modified by: Steven Baltakatei Sandoval 
 206     if [ "$OPTION_VERBOSE" = "true" ]; then 
 207         FUNCTION_TIME
=$(date --iso-8601=ns); # Save current time in nano seconds. 
 208         echoerr 
"[$FUNCTION_TIME] ""$*"; # Display argument text. 
 212     return 0; # Function finished. 
 213 } # Verbose message display function. 
 215     while [ ! $# -eq 0 ]; do   # While number of arguments ($#) is not (!) equal to (-eq) zero (0). 
 216         #echoerr "DEBUG:Starting processArguments while loop." 
 217         #echoerr "DEBUG:Provided arguments are:""$*" 
 219             -h | --help) showUsage
; exit 1;; # Display usage. 
 220             --version) showVersion
; exit 1;; # Show version 
 221             -v | --verbose) OPTION_VERBOSE
="true"; vbm 
"DEBUG:Verbose mode enabled.";; # Enable verbose mode. 
 222             -o | --output) if [ -d "$2" ]; then DIR_OUT
="$2"; vbm 
"DEBUG:DIR_OUT:$DIR_OUT"; shift; fi ;; # Define output directory. 
 223             -e | --encrypt) OPTION_ENCRYPT
="true"; vbm 
"DEBUG:Encrypted output mode enabled.";; # Enable encryption 
 224             -r | --recipient) OPTION_RECIPIENTS
="true"; argRecPubKeys
+=("$2"); vbm 
"STATUS:pubkey added:""$2"; shift;; # Add recipients 
 225             -c | --compress) OPTION_COMPRESS
="true"; vbm 
"DEBUG:Compressed output mode enabled.";; # Enable compression 
 226             -z | --time-zone) try setTimeZoneEV 
"$2"; shift;; # Set timestamp timezone 
 227             -t | --temp-dir) OPTION_TMPDIR
="true" && argTempDirPriority
="$2"; shift;; # Set time zone 
 228             -R | --recipient-dir) OPTION_RECIPIENTS
="true"; OPTION_RECDIR
="true" && argRecDir
="$2"; shift;; # Add recipient watch dir 
 229             -b | --buffer-ttl) OPTION_CUSTOM_BUFFERTTL
="true" && argCustomBufferTTL
="$2"; shift;; # Set custom buffer period (default: 300 seconds) 
 230             -B | --script-ttl) OPTION_CUSTOM_SCRIPTTTL_TE
="true" && argCustomScriptTTL
="$2"; shift;; # Set custom script TTL (default: "day") 
 231             *) echoerr 
"ERROR: Unrecognized argument: $1"; echoerr 
"STATUS:All arguments:$*"; exit 1;; # Handle unrecognized options. 
 235 } # Argument Processing 
 237     # Desc: Set time zone environment variable TZ 
 238     # Usage: setTimeZoneEV arg1 
 239     # Input: arg1: 'date'-compatible timezone string (ex: "America/New_York") 
 240     #        TZDIR env var (optional; default: "/usr/share/zoneinfo") 
 242     #         exit code 0 on success 
 243     #         exit code 1 on incorrect number of arguments 
 244     #         exit code 2 if unable to validate arg1 
 245     # Depends: yell, printenv, bash 5 
 246     # Tested on: Debian 10 
 248     local tzDir returnState
 
 249     if ! [[ $# -eq 1 ]]; then 
 250         yell 
"ERROR:Invalid argument count."; 
 254     # Read TZDIR env var if available 
 255     if printenv TZDIR 
1>/dev
/null 
2>&1; then 
 256         tzDir
="$(printenv TZDIR)"; 
 258         tzDir
="/usr/share/zoneinfo"; 
 262     if ! [[ -f "$tzDir"/"$ARG1" ]]; then 
 263         yell 
"ERROR:Invalid time zone argument."; 
 266     # Export ARG1 as TZ environment variable 
 267         TZ
="$ARG1" && export TZ 
&& returnState
="true"; 
 270     # Determine function return code 
 271     if [ "$returnState" = "true" ]; then 
 274 } # Exports TZ environment variable 
 276     # Desc: Report seconds until next day. 
 278     # Output: stdout: integer seconds until next day 
 279     # Output: exit code 0 if stdout > 0; 1 if stdout = 0; 2 if stdout < 0 
 280     # Usage: timeUntilNextDay 
 281     # Usage: if ! myTTL="$(timeUntilNextDay)"; then yell "ERROR in if statement"; exit 1; fi 
 282     # Depends: date 8, echo 8, yell, try 
 284     local returnState TIME_CURRENT TIME_NEXT_DAY SECONDS_UNTIL_NEXT_DAY
 
 286     TIME_CURRENT
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second. 
 287     TIME_NEXT_DAY
="$(date -d "$TIME_CURRENT next day" --iso-8601=date)"; # Produce timestamp of beginning of tomorrow with resolution of 1 second. 
 288     SECONDS_UNTIL_NEXT_DAY
="$(( $(date +%s -d "$TIME_NEXT_DAY") - $(date +%s -d "$TIME_CURRENT") ))" ; # Calculate seconds until closest future midnight (res. 1 second). 
 289     if [[ "$SECONDS_UNTIL_NEXT_DAY" -gt 0 ]]; then 
 291     elif [[ "$SECONDS_UNTIL_NEXT_DAY" -eq 0 ]]; then 
 292         returnState
="warning_zero"; 
 293         yell 
"WARNING:Reported time until next day exactly zero."; 
 294     elif [[ "$SECONDS_UNTIL_NEXT_DAY" -lt 0 ]]; then 
 295         returnState
="warning_negative"; 
 296         yell 
"WARNING:Reported time until next day is negative."; 
 299     try 
echo "$SECONDS_UNTIL_NEXT_DAY"; # Report 
 301     # Determine function return code 
 302     if [[ "$returnState" = "true" ]]; then 
 304     elif [[ "$returnState" = "warning_zero" ]]; then 
 306     elif [[ "$returnState" = "warning_negative" ]]; then 
 309 } # Report seconds until next day 
 311     # Desc: Report seconds until next hour 
 313     # Output: stdout: integer seconds until next hour 
 314     # Output: exit code 0 if stdout > 0; 1 if stdout = 0; 2 if stdout < 0 
 315     # Usage: timeUntilNextHour 
 316     # Usage: if ! myTTL="$(timeUntilNextHour)"; then yell "ERROR in if statement"; exit 1; fi 
 318     local returnState TIME_CURRENT TIME_NEXT_HOUR SECONDS_UNTIL_NEXT_HOUR
 
 319     TIME_CURRENT
="$(date --iso-8601=seconds)"; # Produce `date`-parsable current timestamp with resolution of 1 second. 
 320     TIME_NEXT_HOUR
="$(date -d "$TIME_CURRENT next hour" --iso-8601=hours)"; # Produce `date`-parsable current time stamp with resolution of 1 second. 
 321     SECONDS_UNTIL_NEXT_HOUR
="$(( $(date +%s -d "$TIME_NEXT_HOUR") - $(date +%s -d "$TIME_CURRENT") ))"; # Calculate seconds until next hour (res. 1 second). 
 322     if [[ "$SECONDS_UNTIL_NEXT_HOUR" -gt 0 ]]; then 
 324     elif [[ "$SECONDS_UNTIL_NEXT_HOUR" -eq 0 ]]; then 
 325         returnState
="warning_zero"; 
 326         yell 
"WARNING:Reported time until next hour exactly zero."; 
 327     elif [[ "$SECONDS_UNTIL_NEXT_HOUR" -lt 0 ]]; then 
 328         returnState
="warning_negative"; 
 329         yell 
"WARNING:Reported time until next hour is negative."; 
 332     try 
echo "$SECONDS_UNTIL_NEXT_HOUR"; # Report 
 334     # Determine function return code 
 335     if [[ "$returnState" = "true" ]]; then 
 337     elif [[ "$returnState" = "warning_zero" ]]; then 
 339     elif [[ "$returnState" = "warning_negative" ]]; then 
 342 } # Report seconds until next hour 
 344     # Desc: Timestamp without separators (YYYYmmddTHHMMSS+zzzz) 
 345     # Usage: dateTimeShort ([str date]) 
 347     # Input: arg1: 'date'-parsable timestamp string (optional) 
 348     # Output: stdout: timestamp (ISO-8601, no separators) 
 350     local TIME_CURRENT TIME_CURRENT_SHORT
 
 354     TIME_CURRENT
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second. 
 355     # Decide to parse current or supplied date 
 356     ## Check if time argument empty 
 357     if [[ -z "$argTime" ]]; then 
 358         ## T: Time argument empty, use current time 
 359         TIME_INPUT
="$TIME_CURRENT"; 
 361         ## F: Time argument exists, validate time 
 362         if date --date="$argTime" 1>/dev
/null 
2>&1; then 
 363             ### T: Time argument is valid; use it 
 364             TIME_INPUT
="$argTime"; 
 366             ### F: Time argument not valid; exit 
 367             yell 
"ERROR:Invalid time argument supplied. Exiting."; exit 1; 
 370     # Construct and deliver separator-les date string 
 371     TIME_CURRENT_SHORT
="$(date -d "$TIME_INPUT" +%Y%m%dT%H%M%S%z)"; 
 372     echo "$TIME_CURRENT_SHORT"; 
 373 } # Get YYYYmmddTHHMMSS±zzzz 
 375     # Desc: Date without separators (YYYYmmdd) 
 376     # Usage: dateShort ([str date]) 
 378     # Input: arg1: 'date'-parsable timestamp string (optional) 
 379     # Output: stdout: date (ISO-8601, no separators) 
 381     local TIME_CURRENT DATE_CURRENT_SHORT
 
 385     TIME_CURRENT
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second. 
 386     # Decide to parse current or supplied date 
 387     ## Check if time argument empty 
 388     if [[ -z "$argTime" ]]; then 
 389         ## T: Time argument empty, use current time 
 390         TIME_INPUT
="$TIME_CURRENT"; 
 392         ## F: Time argument exists, validate time 
 393         if date --date="$argTime" 1>/dev
/null 
2>&1; then 
 394             ### T: Time argument is valid; use it 
 395             TIME_INPUT
="$argTime"; 
 397             ### F: Time argument not valid; exit 
 398             yell 
"ERROR:Invalid time argument supplied. Exiting."; exit 1; 
 401     # Construct and deliver separator-les date string     
 402     DATE_CURRENT_SHORT
="$(date -d "$TIME_INPUT" +%Y%m%d)"; # Produce separator-less current date with resolution 1 day. 
 403     echo "$DATE_CURRENT_SHORT"; 
 406     # Desc: Given seconds, output ISO-8601 duration string 
 407     # Ref/Attrib: ISO-8601:2004(E), §4.4.4.2 Representations of time intervals by duration and context information 
 408     # Note: "1 month" ("P1M") is assumed to be "30 days" (see ISO-8601:2004(E), §2.2.1.2) 
 409     # Usage: timeDuration [1:seconds] ([2:precision]) 
 411     # Input: arg1: seconds as base 10 integer >= 0  (ex: 3601) 
 412     #        arg2: precision level (optional; default=2) 
 413     # Output: stdout: ISO-8601 duration string (ex: "P1H1S", "P2Y10M15DT10H30M20S") 
 414     #         exit code 0: success 
 415     #         exit code 1: error_input 
 416     #         exit code 2: error_unknown 
 417     # Example: 'timeDuration 111111 3' yields 'P1DT6H51M' 
 418     # Depends: date 8 (gnucoreutils), yell,  
 419     local returnState argSeconds argPrecision remainder precision witherPrecision
 
 420     local fullYears fullMonths fullDays fullHours fullMinutes fullSeconds
 
 421     local displayYears displayMonths displayDays displayHours displayMinutes displaySeconds
 
 422     local hasYears hasMonths hasDays hasHours hasMinutes hasSeconds
 
 424     argSeconds
="$1"; # read arg1 (seconds) 
 425     argPrecision
="$2"; # read arg2 (precision) 
 426     precision
=2; # set default precision 
 428     # Check that between one and two arguments is supplied 
 429     if ! { [[ $# -ge 1 ]] && [[ $# -le 2 ]]; }; then 
 430         yell 
"ERROR:Invalid number of arguments:$# . Exiting."; 
 431         returnState
="error_input"; fi 
 433     # Check that argSeconds provided 
 434     if [[ $# -ge 1 ]]; then 
 435         ## Check that argSeconds is a positive integer 
 436         if [[ "$argSeconds" =~ ^
[[:digit
:]]+$ 
]]; then 
 439             yell 
"ERROR:argSeconds not a digit."; 
 440             returnState
="error_input"; 
 443         yell 
"ERROR:No argument provided. Exiting."; 
 447     # Consider whether argPrecision was provided 
 448     if  [[ $# -eq 2 ]]; then 
 449         # Check that argPrecision is a positive integer 
 450         if [[ "$argPrecision" =~ ^
[[:digit
:]]+$ 
]] && [[ "$argPrecision" -gt 0 ]]; then 
 451         precision
="$argPrecision"; 
 453             yell 
"ERROR:argPrecision not a positive integer. (is $argPrecision ). Leaving early."; 
 454             returnState
="error_input"; 
 460     remainder
="$argSeconds" ; # seconds 
 461     ## Calculate full years Y, update remainder 
 462     fullYears
=$(( remainder / (365*24*60*60) )); 
 463     remainder
=$(( remainder - (fullYears*365*24*60*60) )); 
 464     ## Calculate full months M, update remainder 
 465     fullMonths
=$(( remainder / (30*24*60*60) )); 
 466     remainder
=$(( remainder - (fullMonths*30*24*60*60) )); 
 467     ## Calculate full days D, update remainder 
 468     fullDays
=$(( remainder / (24*60*60) )); 
 469     remainder
=$(( remainder - (fullDays*24*60*60) )); 
 470     ## Calculate full hours H, update remainder 
 471     fullHours
=$(( remainder / (60*60) )); 
 472     remainder
=$(( remainder - (fullHours*60*60) )); 
 473     ## Calculate full minutes M, update remainder 
 474     fullMinutes
=$(( remainder / (60) )); 
 475     remainder
=$(( remainder - (fullMinutes*60) )); 
 476     ## Calculate full seconds S, update remainder 
 477     fullSeconds
=$(( remainder / (1) )); 
 478     remainder
=$(( remainder - (remainder*1) )); 
 479     ## Check which fields filled 
 480     if [[ $fullYears -gt 0 ]]; then hasYears
="true"; else hasYears
="false"; fi 
 481     if [[ $fullMonths -gt 0 ]]; then hasMonths
="true"; else hasMonths
="false"; fi 
 482     if [[ $fullDays -gt 0 ]]; then hasDays
="true"; else hasDays
="false"; fi 
 483     if [[ $fullHours -gt 0 ]]; then hasHours
="true"; else hasHours
="false"; fi 
 484     if [[ $fullMinutes -gt 0 ]]; then hasMinutes
="true"; else hasMinutes
="false"; fi 
 485     if [[ $fullSeconds -gt 0 ]]; then hasSeconds
="true"; else hasSeconds
="false"; fi 
 487     ## Determine which fields to display (see ISO-8601:2004 §4.4.3.2) 
 488     witherPrecision
="false" 
 491     if $hasYears && [[ $precision -gt 0 ]]; then 
 493         witherPrecision
="true"; 
 495         displayYears
="false"; 
 497     if $witherPrecision; then ((precision
--)); fi; 
 500     if $hasMonths && [[ $precision -gt 0 ]]; then 
 501         displayMonths
="true"; 
 502         witherPrecision
="true"; 
 504         displayMonths
="false"; 
 506     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 507         displayMonths
="true"; 
 509     if $witherPrecision; then ((precision
--)); fi; 
 512     if $hasDays && [[ $precision -gt 0 ]]; then 
 514         witherPrecision
="true"; 
 518     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 521     if $witherPrecision; then ((precision
--)); fi; 
 524     if $hasHours && [[ $precision -gt 0 ]]; then 
 526         witherPrecision
="true"; 
 528         displayHours
="false"; 
 530     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 533     if $witherPrecision; then ((precision
--)); fi; 
 536     if $hasMinutes && [[ $precision -gt 0 ]]; then 
 537         displayMinutes
="true"; 
 538         witherPrecision
="true"; 
 540         displayMinutes
="false"; 
 542     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 543         displayMinutes
="true"; 
 545     if $witherPrecision; then ((precision
--)); fi; 
 549     if $hasSeconds && [[ $precision -gt 0 ]]; then 
 550         displaySeconds
="true"; 
 551         witherPrecision
="true"; 
 553         displaySeconds
="false"; 
 555     if $witherPrecision && [[ $precision -gt 0 ]]; then 
 556         displaySeconds
="true"; 
 558     if $witherPrecision; then ((precision
--)); fi; 
 560     ## Determine whether or not the "T" separator is needed to separate date and time elements 
 561     if ( $displayHours || $displayMinutes || $displaySeconds); then 
 562         displayDateTime
="true"; else displayDateTime
="false"; fi 
 564     ## Construct duration output string 
 566     if $displayYears; then 
 567         OUTPUT
=$OUTPUT$fullYears"Y"; fi 
 568     if $displayMonths; then 
 569         OUTPUT
=$OUTPUT$fullMonths"M"; fi 
 570     if $displayDays; then 
 571         OUTPUT
=$OUTPUT$fullDays"D"; fi 
 572     if $displayDateTime; then 
 573         OUTPUT
=$OUTPUT"T"; fi 
 574     if $displayHours; then 
 575         OUTPUT
=$OUTPUT$fullHours"H"; fi 
 576     if $displayMinutes; then 
 577         OUTPUT
=$OUTPUT$fullMinutes"M"; fi 
 578     if $displaySeconds; then 
 579         OUTPUT
=$OUTPUT$fullSeconds"S"; fi 
 581     ## Output duration string to stdout 
 582     echo "$OUTPUT" && returnState
="true"; 
 584     #===Determine function return code=== 
 585     if [ "$returnState" = "true" ]; then 
 587     elif [ "$returnState" = "error_input" ]; then 
 591         yell 
"ERROR:Unknown"; 
 595 } # Get duration (ex: PT10M4S ) 
 597     # Desc: Displays missing apps, files, and dirs 
 598     # Usage: displayMissing 
 599     # Input: associative arrays: appRollCall, fileRollCall, dirRollCall 
 600     # Output: stderr messages 
 601     #==BEGIN Display errors== 
 602     #===BEGIN Display Missing Apps=== 
 603     missingApps
="Missing apps  :" 
 604     #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done 
 605     for key 
in "${!appRollCall[@]}"; do 
 606         value
="${appRollCall[$key]}" 
 607         if [ "$value" = "false" ]; then 
 608             #echo "DEBUG:Missing apps: $key => $value"; 
 609             missingApps
="$missingApps""$key " 
 613     if [ "$appMissing" = "true" ]; then  # Only indicate if an app is missing. 
 614         echo "$missingApps" 1>&2; 
 616     #===END Display Missing Apps=== 
 618     #===BEGIN Display Missing Files=== 
 619     missingFiles
="Missing files:" 
 620     #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done 
 621     for key 
in "${!fileRollCall[@]}"; do 
 622         value
="${fileRollCall[$key]}" 
 623         if [ "$value" = "false" ]; then 
 624             #echo "DEBUG:Missing files: $key => $value"; 
 625             missingFiles
="$missingFiles""$key " 
 629     if [ "$fileMissing" = "true" ]; then  # Only indicate if an app is missing. 
 630         echo "$missingFiles" 1>&2; 
 632     #===END Display Missing Files=== 
 634     #===BEGIN Display Missing Directories=== 
 635     missingDirs
="Missing dirs:" 
 636     #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done 
 637     for key 
in "${!dirRollCall[@]}"; do 
 638         value
="${dirRollCall[$key]}" 
 639         if [ "$value" = "false" ]; then 
 640             #echo "DEBUG:Missing dirs: $key => $value"; 
 641             missingDirs
="$missingDirs""$key " 
 645     if [ "$dirMissing" = "true" ]; then  # Only indicate if an dir is missing. 
 646         echo "$missingDirs" 1>&2; 
 648     #===END Display Missing Directories=== 
 650     #==END Display errors== 
 651 } # Display missing apps, files, dirs 
 652 magicSetScriptTTL
() { 
 653     #Desc: Sets script_TTL seconds from provided time_element string argument 
 654     #Usage: magicSetScriptTTL [str time_element] 
 655     #Input: arg1: string (Ex: SCRIPT_TTL_TE; "day" or "hour") 
 656     #Output: var: SCRIPT_TTL (integer seconds) 
 657     #Depends: timeUntilNextHour, timeUntilNextDay 
 660     if [[ "$argTimeElement" = "day" ]]; then 
 661             # Set script lifespan to end at start of next day 
 662         if ! SCRIPT_TTL
="$(timeUntilNextDay)"; then 
 663             if [[ "$SCRIPT_TTL" -eq 0 ]]; then 
 664             ((SCRIPT_TTL
++)); # Add 1 because 0 would cause 'timeout' to never timeout. 
 666             yell 
"ERROR: timeUntilNextDay exit code $?"; exit 1; 
 669     elif [[ "$argTimeElement" = "hour" ]]; then 
 670         # Set script lifespan to end at start of next hour 
 671         if ! SCRIPT_TTL
="$(timeUntilNextHour)"; then 
 672             if [[ "$SCRIPT_TTL" -eq 0 ]]; then 
 673                 ((SCRIPT_TTL
++)); # Add 1 because 0 would cause 'timeout' to never timeout. 
 675                 yell 
"ERROR: timeUntilNextHour exit code $?"; exit 1; 
 679         yell 
"ERROR:Invalid argument for setScriptTTL function:$argTimeElement"; exit 1; 
 681 } # Seconds until next (day|hour). 
 683     # Desc: Checks that a valid tar archive exists, creates one otherwise 
 684     # Usage: checkMakeTar [ path ] 
 686     # Input: arg1: path of tar archive 
 687     # Output: exit code 0 : tar readable 
 688     #         exit code 1 : tar missing; created 
 689     #         exit code 2 : tar not readable; moved; replaced 
 690     # Depends: try, tar, date 
 691     local PATH_TAR returnFlag0 returnFlag1 returnFlag2
 
 694     # Check if file is a valid tar archive 
 695     if tar --list --file="$PATH_TAR" 1>/dev
/null 
2>&1; then 
 696         ## T1: return success 
 697         returnFlag0
="tar valid"; 
 699         ## F1: Check if file exists 
 700         if [[ -f "$PATH_TAR" ]]; then 
 702             try 
mv "$PATH_TAR" "$PATH_TAR""--broken--""$(date +%Y%m%dT%H%M%S)" && \
 
 703                 returnFlag1
="tar moved"; 
 708         ## F2: Create tar archive, return 0 
 709         try 
tar --create --file="$PATH_TAR" --files-from=/dev
/null 
&& \
 
 710             returnFlag2
="tar created"; 
 713     # Determine function return code 
 714     if [[ "$returnFlag0" = "tar valid" ]]; then 
 716     elif [[ "$returnFlag2" = "tar created" ]] && ! [[ "$returnFlag1" = "tar moved" ]]; then 
 717         return 1; # tar missing so created 
 718     elif [[ "$returnFlag2" = "tar created" ]] && [[ "$returnFlag1" = "tar moved" ]]; then 
 719         return 2; # tar not readable so moved; replaced 
 721 } # checks if arg1 is tar; creates one otherwise 
 723     # Desc: Writes first argument to temporary file with arguments as options, then appends file to tar 
 724     # Usage: appendArgTar "$(echo "Data to be written.")" [name of file to be inserted] [tar path] [temp dir] ([cmd1] [cmd2] [cmd3] [cmd4]...) 
 726     # Input: arg1: data to be written 
 727     #        arg2: file name of file to be inserted into tar 
 728     #        arg3: tar archive path (must exist first) 
 729     #        arg4: temporary working dir 
 730     #        arg5+: command strings (ex: "gpsbabel -i nmea -f - -o kml -F - ") 
 731     # Output: file written to disk 
 732     # Example: decrypt multiple large files in parallel 
 733     #          appendArgTar "$(cat /tmp/largefile1.gpg)" "largefile1" $HOME/archive.tar /tmp "gpg --decrypt" & 
 734     #          appendArgTar "$(cat /tmp/largefile2.gpg)" "largefile2" $HOME/archive.tar /tmp "gpg --decrypt" & 
 735     #          appendArgTar "$(cat /tmp/largefile3.gpg)" "largefile3" $HOME/archive.tar /tmp "gpg --decrypt" & 
 737     # Ref/Attrib: Using 'eval' to construct command strings https://askubuntu.com/a/476533 
 740     local FN
="${FUNCNAME[0]}"; 
 741     #yell "DEBUG:STATUS:$FN:Finished appendArgTar()." 
 744     if ! [ -z "$2" ]; then FILENAME
="$2"; else yell 
"ERROR:$FN:Not enough arguments."; exit 1; fi 
 746     # Check tar path is a file 
 747     if [ -f "$3" ]; then TAR_PATH
="$3"; else yell 
"ERROR:$FN:Tar archive arg not a file."; exit 1; fi 
 750     if ! [ -z "$4" ]; then TMP_DIR
="$4"; else yell 
"ERROR:$FN:No temporary working dir set."; exit 1; fi 
 752     # Set command strings 
 753     if ! [ -z "$5" ]; then CMD1
="$5"; else CMD1
="tee /dev/null "; fi # command string 1 
 754     if ! [ -z "$6" ]; then CMD2
="$6"; else CMD2
="tee /dev/null "; fi # command string 2 
 755     if ! [ -z "$7" ]; then CMD3
="$7"; else CMD3
="tee /dev/null "; fi # command string 3 
 756     if ! [ -z "$8" ]; then CMD4
="$8"; else CMD4
="tee /dev/null "; fi # command string 4 
 762     # yell "DEBUG:STATUS:$FN:CMD0:$CMD0" 
 763     # yell "DEBUG:STATUS:$FN:CMD1:$CMD1" 
 764     # yell "DEBUG:STATUS:$FN:CMD2:$CMD2" 
 765     # yell "DEBUG:STATUS:$FN:CMD3:$CMD3" 
 766     # yell "DEBUG:STATUS:$FN:CMD4:$CMD4" 
 767     # yell "DEBUG:STATUS:$FN:FILENAME:$FILENAME" 
 768     # yell "DEBUG:STATUS:$FN:TAR_PATH:$TAR_PATH" 
 769     # yell "DEBUG:STATUS:$FN:TMP_DIR:$TMP_DIR" 
 771     # Write to temporary working dir 
 772     eval "$CMD0"" | ""$CMD1"" | ""$CMD2"" | ""$CMD3"" | ""$CMD4" > "$TMP_DIR"/"$FILENAME"; 
 775     try 
tar --append --directory="$TMP_DIR" --file="$TAR_PATH" "$FILENAME"; 
 776     #yell "DEBUG:STATUS:$FN:Finished appendArgTar()." 
 777 } # Append Bash var to file appended to Tar archive 
 779     # Desc: Processes first file and then appends to tar 
 780     # Usage: appendFileTar [file path] [name of file to be inserted] [tar path] [temp dir] ([cmd1] [cmd2] [cmd3] [cmd4]...) 
 782     # Input: arg1: path of file to be (processed and) written 
 783     #        arg2: name to use for file inserted into tar 
 784     #        arg3: tar archive path (must exist first) 
 785     #        arg4: temporary working dir 
 786     #        arg5+: command strings (ex: "gpsbabel -i nmea -f - -o kml -F - ") 
 787     # Output: file written to disk 
 788     # Example: decrypt multiple large files in parallel 
 789     #          appendFileTar /tmp/largefile1.gpg "largefile1" $HOME/archive.tar /tmp "gpg --decrypt" & 
 790     #          appendFileTar /tmp/largefile2.gpg "largefile2" $HOME/archive.tar /tmp "gpg --decrypt" & 
 791     #          appendFileTar /tmp/largefile3.gpg "largefile3" $HOME/archive.tar /tmp "gpg --decrypt" & 
 795     local FN
="${FUNCNAME[0]}"; 
 796     #yell "DEBUG:STATUS:$FN:Finished appendFileTar()." 
 799     if ! [ -z "$2" ]; then FILENAME
="$2"; else yell 
"ERROR:$FN:Not enough arguments."; exit 1; fi 
 800     # Check tar path is a file 
 801     if [ -f "$3" ]; then TAR_PATH
="$3"; else yell 
"ERROR:$FN:Tar archive arg not a file."; exit 1; fi 
 803     if ! [ -z "$4" ]; then TMP_DIR
="$4"; else yell 
"ERROR:$FN:No temporary working dir set."; exit 1; fi 
 804     # Set command strings 
 805     if ! [ -z "$5" ]; then CMD1
="$5"; else CMD1
="tee /dev/null "; fi # command string 1 
 806     if ! [ -z "$6" ]; then CMD2
="$6"; else CMD2
="tee /dev/null "; fi # command string 2 
 807     if ! [ -z "$7" ]; then CMD3
="$7"; else CMD3
="tee /dev/null "; fi # command string 3 
 808     if ! [ -z "$8" ]; then CMD4
="$8"; else CMD4
="tee /dev/null "; fi # command string 4 
 810     # Input command string 
 814     # yell "DEBUG:STATUS:$FN:CMD0:$CMD0" 
 815     # yell "DEBUG:STATUS:$FN:CMD1:$CMD1" 
 816     # yell "DEBUG:STATUS:$FN:CMD2:$CMD2" 
 817     # yell "DEBUG:STATUS:$FN:CMD3:$CMD3" 
 818     # yell "DEBUG:STATUS:$FN:CMD4:$CMD4" 
 819     # yell "DEBUG:STATUS:$FN:FILENAME:$FILENAME" 
 820     # yell "DEBUG:STATUS:$FN:TAR_PATH:$TAR_PATH" 
 821     # yell "DEBUG:STATUS:$FN:TMP_DIR:$TMP_DIR" 
 823     # Write to temporary working dir 
 824     eval "$CMD0 | $CMD1 | $CMD2 | $CMD3 | $CMD4" > "$TMP_DIR"/"$FILENAME"; 
 827     try 
tar --append --directory="$TMP_DIR" --file="$TAR_PATH" "$FILENAME"; 
 828     #yell "DEBUG:STATUS:$FN:Finished appendFileTar()." 
 829 } # Append file to Tar archive 
 831     # Desc: Checks if string is an age-compatible pubkey 
 832     # Usage: checkAgePubkey [str pubkey] 
 834     # Input: arg1: string 
 835     # Output: return code 0: string is age-compatible pubkey 
 836     #         return code 1: string is NOT an age-compatible pubkey 
 837     #         age stderr (ex: there is stderr if invalid string provided) 
 838     # Depends: age (v0.1.0-beta2; https://github.com/FiloSottile/age/releases/tag/v1.0.0-beta2 ) 
 842     if echo "test" | age 
-a -r "$argPubkey" 1>/dev
/null
; then 
 849     # Desc: Validates Input 
 850     # Usage: validateInput [str input] [str input type] 
 852     # Input: arg1: string to validate 
 853     #        arg2: string specifying input type (ex:"ssh_pubkey") 
 854     # Output: return code 0: if input string matched specified string type 
 855     # Depends: bash 5, yell 
 858     local FN
="${FUNCNAME[0]}"; 
 863     if [[ $# -gt 2 ]]; then yell 
"ERROR:$0:$FN:Too many arguments."; exit 1; fi; 
 866     if [[ -z "$argInput" ]]; then return 1; fi 
 870     ### Check for alnum/dash base64 (ex: "ssh-rsa AAAAB3NzaC1yc2EAAA") 
 871     if [[ "$argType" = "ssh_pubkey" ]]; then 
 872         if [[ "$argInput" =~ ^
[[:alnum
:]-]*[\ ]*[[:alnum
:]+/=]*$ 
]]; then 
 876     ### Check for age1[:bech32:] 
 877     if [[ "$argType" = "age_pubkey" ]]; then 
 878         if [[ "$argInput" =~ ^age1
[qpzry9x8gf2tvdw0s3jn54khce6mua7l
]*$ 
]]; then 
 882     if [[ "$argType" = "integer" ]]; then 
 883         if [[ "$argInput" =~ ^
[[:digit
:]]*$ 
]]; then 
 886     ## time element (year, month, week, day, hour, minute, second) 
 887     if [[ "$argType" = "time_element" ]]; then 
 888         if [[ "$argInput" = "year" ]] || \
 
 889                [[ "$argInput" = "month" ]] || \
 
 890                [[ "$argInput" = "week" ]] || \
 
 891                [[ "$argInput" = "day" ]] || \
 
 892                [[ "$argInput" = "hour" ]] || \
 
 893                [[ "$argInput" = "minute" ]] || \
 
 894                [[ "$argInput" = "second" ]]; then 
 897     # Return error if no condition matched. 
 899 } # Validates strings 
 901     # Desc: Get epoch nanoseconds 
 904     # Input: arg1: 'date'-parsable timestamp string (optional) 
 905     # Output: Nanoseconds since 1970-01-01 
 906     # Depends: date 8, yell() 
 907     # Ref/Attrib: Force base 10 Bash arith with '10#'. https://stackoverflow.com/a/24777667 
 908     local TIME_CURRENT TIME_INPUT TIME_EPOCH_FLOAT TIME_EPOCH_NSFRAC
 
 914     TIME_CURRENT
="$(date --iso-8601=ns)"; # Produce `date`-parsable current timestamp with resolution of 1 nanosecond. 
 916     # Decide to parse current or supplied time 
 917     ## Check if time argument empty 
 918     if [[ -z "$argTime" ]]; then 
 919         ## T: Time argument empty, use current time 
 920         TIME_INPUT
="$TIME_CURRENT"; 
 922         ## F: Time argument exists, validate time 
 923         if date --date="$argTime" 1>/dev
/null 
2>&1; then 
 924             ### T: Time argument is valid; use it 
 925             TIME_INPUT
="$argTime"; 
 927             ### F: Time argument not valid; exit 
 928             yell 
"ERROR:Invalid time argument supplied. Exiting."; exit 1; 
 931     # Construct and deliver nanoseconds since 1970-01-01     
 932     TIME_EPOCH_FLOAT
="$(date --date="$TIME_INPUT" +%s.%N)"; # Save ssss.NNNNNNNNN 
 933     TIME_EPOCH_INT
="$(echo "$TIME_EPOCH_FLOAT" | cut -d. -f1)"; # Get ssss 
 934     TIME_EPOCH_NSFRAC
="$(echo "$TIME_EPOCH_FLOAT" | cut -d. -f2)"; # Get NNNNNNNNN 
 935     TIME_EPOCH_NS
="$(( (10#"$TIME_EPOCH_INT" * 10**9) + (10#"$TIME_EPOCH_NSFRAC") ))"; 
 936     echo "$TIME_EPOCH_NS"; 
 937 } # Nanoseconds since 1970-01-01 
 938 magicBufferSleepPID
() { 
 939     # Desc: Compensates for lag so buffer rounds start every BUFFER_TTL seconds 
 940     # Input: vars: BUFFER_TTL, errResetx10e3, K_P, T_I, T_D 
 941     # # Input: array: errorHistory 
 942     # Output: vars: BUFFER_TTL_ADJ_FLOAT 
 943     # Re/Attrib: https://en.wikipedia.org/wiki/PID_controller#Standard_versus_parallel_(ideal)_form 
 945     local timeBufferStartNS timeBufferStartNSExp errNS errNSx10e3
 
 946     local errResetx10e3 errRatex10e3 ADJ BUFFER_TTL_ADJ_NS BUFFER_TTL_ADJ_INT
 
 947     local BUFFER_TTL_ADJ_FLOATFRAC
 
 948     # local errorHistorySize 
 950     # ## Define errorHistorySize 
 951     # errorHistorySize=100; 
 952     ## Define BUFFER_TTL in nanoseconds 
 953     BUFFER_TTL_NS
=$((BUFFER_TTL * 10**9)) && vbm 
"BUFFER_TTL_NS:$BUFFER_TTL_NS"; 
 955     # Calculate Error, errNS, in nanoseconds 
 957     timeBufferStartNS
="$(timeEpochNS)" && vbm 
"timeBufferStartNS:$timeBufferStartNS"; 
 958     ## Calculate expected time (from start time, current buffer round number, nominal BUFFER_TTL) 
 959     timeBufferStartNSExp
="$(( (timeBufferFirstNS) + (BUFFER_TTL_NS * bufferRound) ))" && vbm 
"timeBufferStartNSExp:$timeBufferStartNSExp"; 
 960     ## Calculate error (diff between timeBufferStartNSExp and timeBufferStartNS; usually negative) 
 961     errNS
="$(( timeBufferStartNSExp - timeBufferStartNS ))" && vbm 
"errNS:$errNS"; 
 962     errNSx10e3
="$((errNS*10**3))" && vbm 
"errNSx10e3:$errNSx10e3"; 
 963     # ## Append error to errorHistory 
 964     # errorHistory+=("errNS"); 
 965     # ### Trim errorHistory array if over errorHistorySize 
 966     # while [[ "${#errorHistory[@]}" -gt "errorHistorySize" ]]; then do 
 967     #   unset "errorHistory[0]"; # remove oldest entry, creating sparse array 
 968     #   errorHistory=("${errorHistory[@]}"); # reindex sparse array 
 969     #   vbm "STATUS:Trimmed errorHistory array. Entry count:${#errorHistory[@]}"; 
 972     # Calculate errReset in nanoseconds^2 
 973     ## errReset = int(errHistory(t),wrt(delta_BUFFER_TTL)) 
 974     ## Integrate errorHistory with respect to time 
 975     # for value in "${errorHistory[@]}"; do 
 976     #   errReset=$(( errReset + ( value*BUFFER_TTL_NS ) )); 
 978     errResetx10e3
="$(( ( errResetx10e3 + ( errNSx10e3 * BUFFER_TTL_NS ) )*10**3 ))" && vbm 
"errResetx10e3:$errResetx10e3"; 
 980     # Calculate errRate in nanoseconds per 1000 nanoseconds 
 981     errRatex10e3
="$(( ( errNSx10e3 ) / BUFFER_TTL_NS ))" && vbm 
"errRatex10e3:$errRatex10e3"; 
 983     # Calculate PID control signal 
 984     ## ADJ = K_P * (errNS + errReset/T_I + errRate*T_D) 
 985     ADJ
="$((K_P*(errNSx10e3 + (errResetx10e3/T_I) + (errRatex10e3*T_D) )/(10**3)))" && vbm 
"ADJ:$ADJ"; 
 987     # Calculate BUFFER_TTL_ADJ_FLOAT from ADJ (ns) 
 988     ## Calculate BUFFER_TTL_ADJ in nanoseconds (BUFFER_TTL_ADJ_NS = BUFFER_TTL_NS + ADJ) 
 989     BUFFER_TTL_ADJ_NS
="$((BUFFER_TTL_NS + ADJ))" && vbm 
"BUFFER_TTL_ADJ_NS:$BUFFER_TTL_ADJ_NS"; 
 990     ## Calculate integer seconds 
 991     BUFFER_TTL_ADJ_INT
="$((BUFFER_TTL_ADJ_NS/(10**9)))" && vbm 
"BUFFER_TTL_ADJ_INT:$BUFFER_TTL_ADJ_INT"; 
 992     ### Catch negative integer seconds, set minimum of BUFFER_TTL/10 seconds 
 993     if [[ "$BUFFER_TTL_ADJ_INT" -le "$((BUFFER_TTL/10))" ]]; then 
 994         BUFFER_TTL_ADJ_INT
="$((BUFFER_TTL/10))"; 
 995         yell 
"WARNING:Buffer lag adjustment yielded negative seconds."; 
 997     ## Calculate nanosecond remainder 
 998     BUFFER_TTL_ADJ_FLOATFRAC
="$((BUFFER_TTL_NS - (BUFFER_TTL_ADJ_INT*(10**9)) ))" && vbm 
"BUFFER_TTL_ADJ_FLOATFRAC:$BUFFER_TTL_ADJ_FLOATFRAC"; 
 999     ## Form float BUFFER_TTL_ADJ_FLOAT 
1000     BUFFER_TTL_ADJ_FLOAT
="$BUFFER_TTL_ADJ_INT".
"$BUFFER_TTL_ADJ_FLOATFRAC" && vbm 
"BUFFER_TTL_ADJ_FLOAT:$BUFFER_TTL_ADJ_FLOAT"; 
1001     vbm 
"STATUS:Calculated adjusted BUFFER_TTL (seconds):$BUFFER_TTL_ADJ_FLOAT"; 
1003 } # Calc BUFFER_TTL_ADJ_FLOAT so buffer starts every BUFFER_TTL seconds 
1004 magicWriteVersion
() { 
1005     # Desc: Appends time-stamped VERSION to PATHOUT_TAR 
1006     # Usage: magicWriteVersion 
1008     # Input: CONTENT_VERSION, FILEOUT_VERSION, PATHOUT_TAR, DIR_TMP 
1009     # Input: SCRIPT_VERSION, SCRIPT_URL, AGE_VERSION, AGE_URL, SCRIPT_HOSTNAME 
1010     # Output: appends tar PATHOUT_TAR 
1011     # Depends: dateTimeShort, appendArgTar 
1012     local CONTENT_VERSION pubKeyIndex
 
1014     # Set VERSION file name 
1015     FILEOUT_VERSION
="$(dateTimeShort)..VERSION"; 
1017     # Gather VERSION data in CONTENT_VERSION 
1018     CONTENT_VERSION
="SCRIPT_VERSION=$SCRIPT_VERSION"; 
1019     #CONTENT_VERSION="$CONTENT_VERSION""\\n"; 
1020     CONTENT_VERSION
="$CONTENT_VERSION""\\n""SCRIPT_NAME=$SCRIPT_NAME"; 
1021     CONTENT_VERSION
="$CONTENT_VERSION""\\n""SCRIPT_URL=$SCRIPT_URL"; 
1022     CONTENT_VERSION
="$CONTENT_VERSION""\\n""AGE_VERSION=$AGE_VERSION"; 
1023     CONTENT_VERSION
="$CONTENT_VERSION""\\n""AGE_URL=$AGE_URL"; 
1024     CONTENT_VERSION
="$CONTENT_VERSION""\\n""DATE=$(date --iso-8601=seconds)"; 
1025     CONTENT_VERSION
="$CONTENT_VERSION""\\n""HOSTNAME=$SCRIPT_HOSTNAME"; 
1026     ## Add list of recipient pubkeys 
1027     for pubkey 
in "${recPubKeysValid[@]}"; do 
1029         CONTENT_VERSION
="$CONTENT_VERSION""\\n""PUBKEY_$pubKeyIndex=$pubkey"; 
1031     ## Process newline escapes 
1032     CONTENT_VERSION
="$(echo -e "$CONTENT_VERSION")" 
1034     # Write CONTENT_VERSION as file FILEOUT_VERSION and write-append to PATHOUT_TAR 
1035     appendArgTar 
"$CONTENT_VERSION" "$FILEOUT_VERSION" "$PATHOUT_TAR" "$DIR_TMP"; 
1037 } # bkgpslog: write version data to PATHOUT_TAR via appendArgTar() 
1038 magicGatherWriteBuffer
() { 
1039     # Desc: bkgpslog-specific meta function for writing data to DIR_TMP then appending each file to PATHOUT_TAR 
1040     # Inputs: vars: PATHOUT_TAR FILEOUT_{NMEA,GPX,KML} CMD_CONV_{NMEA,GPX,KML} CMD_{COMPRESS,ENCRYPT} DIR_TMP, 
1041     # Inputs: vars: BUFFER_TTL bufferTTL_STR SCRIPT_HOSTNAME CMD_COMPRESS_SUFFIX CMD_ENCRYPT_SUFFIX 
1042     # Output: file: (PATHOUT_TAR) 
1043     # Depends: yell(), try(), vbm(), appendArgTar(), tar 1, sleep 8, checkMakeTar() 
1044     # Depends: magicWriteVersion(), appendFileTar() 
1047     # Debug:Get function name 
1048     FN
="${FUNCNAME[0]}"; 
1050     # Create buffer file with unique name 
1051     PATHOUT_BUFFER
="$DIR_TMP/buffer$SECONDS"; 
1053     timeout 
"$BUFFER_TTL"s gpspipe 
-r -o "$PATHOUT_BUFFER" ; 
1054     timeBufferStart
="$(dateTimeShort  "$(date --date="$BUFFER_TTL seconds ago")")"; # Note start time 
1055     vbm "DEBUG
:STATUS
:$FN:Started magicWriteBuffer
().
"; 
1056     # Determine file paths (time is start of buffer period) 
1057     FILEOUT_BASENAME="$timeBufferStart""--""$bufferTTL_STR""..
""$SCRIPT_HOSTNAME""_location
" && vbm "STATUS
:Set FILEOUT_BASENAME to
:$FILEOUT_BASENAME"; 
1058     ## Files saved to DIR_TMP 
1059     FILEOUT_NMEA="$FILEOUT_BASENAME".nmea"$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX" && vbm "STATUS
:Set FILEOUT_NMEA to
:$FILEOUT_NMEA"; 
1060     FILEOUT_GPX="$FILEOUT_BASENAME".gpx"$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX" && vbm "STATUS
:Set FILEOUT_GPX to
:$FILEOUT_GPX"; 
1061     FILEOUT_KML="$FILEOUT_BASENAME".kml"$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX" && vbm "STATUS
:Set FILEOUT_KML to
:$FILEOUT_KML"; 
1062     PATHOUT_NMEA="$DIR_TMP"/"$FILEOUT_NMEA" && vbm "STATUS
:Set PATHOUT_NMEA to
:$PATHOUT_NMEA"; 
1063     PATHOUT_GPX="$DIR_TMP"/"$FILEOUT_GPX" && vbm "STATUS
:Set PATHOUT_GPX to
:$PATHOUT_GPX"; 
1064     PATHOUT_KML="$DIR_TMP"/"$FILEOUT_KML" && vbm "STATUS
:Set PATHOUT_KML to
:$PATHOUT_KML"; 
1065     ## Files saved to disk (DIR_OUT) 
1066     ### one file per day (Ex: "20200731..hostname_location.
[.gpx.gz
].
tar") 
1067     PATHOUT_TAR="$DIR_OUT"/"$(dateShort "$(date --date="$BUFFER_TTL seconds ago")")"..
"$SCRIPT_HOSTNAME""_location""$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX".
tar && \
 
1068         vbm 
"STATUS:Set PATHOUT_TAR to:$PATHOUT_TAR"; 
1070     vbm 
"STATUS:DIR_TMP     :$DIR_TMP"; 
1071     vbm 
"STATUS:PATHOUT_TAR :$PATHOUT_TAR"; 
1072     vbm 
"STATUS:PATHOUT_NMEA:$PATHOUT_NMEA"; 
1073     vbm 
"STATUS:PATHOUT_GPX:$PATHOUT_GPX"; 
1074     vbm 
"STATUS:PATHOUT_KML:$PATHOUT_KML"; 
1077     # Validate PATHOUT_TAR as tar. 
1078     checkMakeTar 
"$PATHOUT_TAR"; 
1079     ## Add VERSION file if checkMakeTar had to create a tar (exited 1) or replace one (exited 2) 
1080     if [[ $? 
-eq 1 ]] || [[ $? 
-eq 2 ]]; then magicWriteVersion
; fi 
1082     # Write bufferBash to PATHOUT_TAR 
1083     wait; # Wait to avoid collision with older magicWriteBuffer() instances (see https://www.tldp.org/LDP/abs/html/x9644.html ) 
1084     appendFileTar 
"$PATHOUT_BUFFER" "$FILEOUT_NMEA" "$PATHOUT_TAR" "$DIR_TMP" "$CMD_CONV_NMEA" "$CMD_COMPRESS" "$CMD_ENCRYPT"; # Write NMEA data 
1085     appendFileTar 
"$PATHOUT_BUFFER" "$FILEOUT_GPX" "$PATHOUT_TAR" "$DIR_TMP" "$CMD_CONV_GPX" "$CMD_COMPRESS" "$CMD_ENCRYPT"; # Write GPX file 
1086     appendFileTar 
"$PATHOUT_BUFFER" "$FILEOUT_KML" "$PATHOUT_TAR" "$DIR_TMP" "$CMD_CONV_KML" "$CMD_COMPRESS" "$CMD_ENCRYPT"; # Write KML file 
1088     # Remove secured chunks from DIR_TMP 
1089     rm "$PATHOUT_BUFFER" "$PATHOUT_NMEA" "$PATHOUT_GPX" "$PATHOUT_KML"; 
1090     vbm 
"DEBUG:STATUS:$FN:Finished magicWriteBuffer()."; 
1091 } # write buffer to disk 
1092 magicParseRecipientDir
() { 
1093     # Desc: Updates recPubKeysValid with pubkeys in dir specified by '-R' option ("recipient directory") 
1094     # Inputs:  vars: OPTION_RECDIR, argRecDir, OPTION_ENCRYPT 
1095     #          arry: recPubKeysValid 
1096     # Outputs: arry: recPubKeysValid 
1097     # Depends: processArguments, 
1098     local recFileLine updateRecipients recipientDir
 
1099     declare -a candRecPubKeysValid
 
1101     # Check that '-e' and '-R' set 
1102     if [[ "$OPTION_ENCRYPT" = "true" ]] && [[ "$OPTION_RECDIR" = "true" ]]; then 
1103         ### Check that argRecDir is a directory. 
1104         if [[ -d "$argRecDir" ]]; then 
1105             recipientDir
="$argRecDir" && vbm 
"STATUS:Recipient watch directory detected:\"$recipientDir\""; 
1106             #### Initialize variable indicating outcome of pubkey review 
1107             unset updateRecipients
 
1108             #### Add existing recipients 
1109             candRecPubKeysValid
=("${recPubKeysValidStatic[@]}"); 
1110             #### Parse files in recipientDir 
1111             for file in "$recipientDir"/*; do 
1112                 ##### Read first line of each file 
1113                 recFileLine
="$(head -n1 "$file")" && vbm 
"STATUS:Checking if pubkey:\"$recFileLine\""; 
1114                 ##### check if first line is a valid pubkey 
1115                 if checkAgePubkey 
"$recFileLine" && \
 
1116                         ( validateInput 
"$recFileLine" "ssh_pubkey" || validateInput 
"$recFileLine" "age_pubkey"); then 
1117                     ###### T: add candidate pubkey to candRecPubKeysValid 
1118                     candRecPubKeysValid
+=("$recFileLine") && vbm 
"STATUS:RecDir pubkey is valid pubkey:\"$recFileLine\""; 
1120                     ###### F: throw warning; 
1121                     yell 
"ERROR:Invalid recipient file detected. Not modifying recipient list." 
1122                     updateRecipients
="false"; 
1125             #### Write updated recPubKeysValid array to recPubKeysValid if no failure detected 
1126             if ! [[ "$updateRecipients" = "false" ]]; then 
1127                 recPubKeysValid
=("${candRecPubKeysValid[@]}") && vbm 
"STATUS:Wrote candRecPubkeysValid to recPubKeysValid:\"${recPubKeysValid[*]}\""; 
1130             yell 
"ERROR:$0:Recipient directory $argRecDir does not exist. Exiting."; exit 1; 
1133     # Handle case if '-R' set but '-e' not set 
1134     if [[ ! "$OPTION_ENCRYPT" = "true" ]] && [[ "$OPTION_RECDIR" = "true" ]]; then 
1135         yell 
"ERROR: \\'-R\\' is set but \\'-e\\' is not set."; fi; 
1136 } # Update recPubKeysValid with argRecDir 
1137 magicParseRecipientArgs
() { 
1138     # Desc: Parses recipient arguments specified by '-r' option 
1139     # Input:  vars: OPTION_ENCRYPT from processArguments() 
1140     #         arry: argRecPubKeys from processArguments() 
1141     # Output: vars: CMD_ENCRYPT, CMD_ENCRYPT_SUFFIX 
1142     #         arry: recPubKeysValid, recPubKeysValidStatic 
1143     # Depends: checkapp(), checkAgePubkey(), validateInput(), processArguments() 
1146     # Check if encryption option active. 
1147     if [[ "$OPTION_ENCRYPT" = "true" ]] && [[ "$OPTION_RECIPIENTS" = "true" ]]; then  
1148         if checkapp age
; then # Check that age is available. 
1149             for pubkey 
in "${argRecPubKeys[@]}"; do # Validate recipient pubkey strings by forming test message 
1150                 vbm 
"DEBUG:Testing pubkey string:$pubkey"; 
1151                 if checkAgePubkey 
"$pubkey" && \
 
1152                         ( validateInput 
"$pubkey" "ssh_pubkey" || validateInput 
"$pubkey" "age_pubkey"); then 
1153                     #### Form age recipient string 
1154                     recipients
="$recipients""-r '$pubkey' "; 
1155                     vbm 
"STATUS:Added pubkey for forming age recipient string:""$pubkey"; 
1156                     vbm 
"DEBUG:recipients:""$recipients"; 
1157                     #### Add validated pubkey to recPubKeysValid array 
1158                     recPubKeysValid
+=("$pubkey") && vbm 
"DEBUG:recPubkeysValid:pubkey added:$pubkey"; 
1160                     yell 
"ERROR:Exit code ""$?"". Invalid recipient pubkey string. Exiting."; exit 1; 
1163             vbm 
"DEBUG:Finished processing argRecPubKeys array"; 
1164             vbm 
"STATUS:Array of validated pubkeys:${recPubKeysValid[*]}"; 
1165             recPubKeysValidStatic
=("${recPubKeysValid[@]}"); # Save static image of pubkeys validated by this function 
1167             ##  Form age command string 
1168             CMD_ENCRYPT
="age ""$recipients " && vbm 
"CMD_ENCRYPT:$CMD_ENCRYPT"; 
1169             CMD_ENCRYPT_SUFFIX
=".age" && vbm 
"CMD_ENCRYPT_SUFFIX:$CMD_ENCRYPT_SUFFIX"; 
1171             yell 
"ERROR:Encryption enabled but \"age\" not found. Exiting."; exit 1; 
1174         CMD_ENCRYPT
="tee /dev/null " && vbm 
"CMD_ENCRYPT:$CMD_ENCRYPT"; 
1175         CMD_ENCRYPT_SUFFIX
="" && vbm 
"CMD_ENCRYPT_SUFFIX:$CMD_ENCRYPT_SUFFIX"; 
1176         vbm 
"DEBUG:Encryption not enabled." 
1178     # Catch case if '-e' is set but '-r' or '-R' is not 
1179     if [[ "$OPTION_ENCRYPT" = "true" ]] && [[ ! "$OPTION_RECIPIENTS" = "true" ]]; then 
1180         yell 
"ERROR:\\'-e\\' set but no \\'-r\\' or \\'-R\\' set."; exit 1; fi; 
1181     # Catch case if '-r' or '-R' set but '-e' is not 
1182     if [[ ! "$OPTION_ENCRYPT" = "true" ]] && [[ "$OPTION_RECIPIENTS" = "true" ]]; then 
1183         yell 
"ERROR:\\'-r\\' or \\'-R\\' set but \\'-e\\' is not set."; exit 1; fi;  
1184 } # Populate recPubKeysValid with argRecPubKeys; form encryption cmd string and filename suffix 
1185 magicParseCompressionArg
() { 
1186     # Desc: Parses compression arguments specified by '-c' option 
1187     # Input:  vars: OPTION_COMPRESS 
1188     # Output: CMD_COMPRESS, CMD_COMPRESS_SUFFIX 
1189     # Depends: checkapp(), vbm(), gzip,  
1190     if [[ "$OPTION_COMPRESS" = "true" ]]; then # Check if compression option active 
1191         if checkapp 
gzip; then # Check if gzip available 
1192             CMD_COMPRESS
="gzip " && vbm 
"CMD_COMPRESS:$CMD_COMPRESS"; 
1193             CMD_COMPRESS_SUFFIX
=".gz" && vbm 
"CMD_COMPRESS_SUFFIX:$CMD_COMPRESS_SUFFIX"; 
1195             yell 
"ERROR:Compression enabled but \"gzip\" not found. Exiting."; exit 1; 
1198         CMD_COMPRESS
="tee /dev/null " && vbm 
"CMD_COMPRESS:$CMD_COMPRESS"; 
1199         CMD_COMPRESS_SUFFIX
="" && vbm 
"CMD_COMPRESS_SUFFIX:$CMD_COMPRESS_SUFFIX"; 
1200         vbm 
"DEBUG:Compression not enabled."; 
1202 } # Form compression cmd string and filename suffix 
1203 magicInitWorkingDir
() { 
1204     # Desc: Determine temporary working directory from defaults or user input 
1205     # Usage: magicInitWorkignDir 
1206     # Input:  vars: OPTION_TEMPDIR, argTempDirPriority, DIR_TMP_DEFAULT 
1207     # Input:  vars: SCRIPT_TIME_START 
1208     # Output: vars: DIR_TMP 
1209     # Depends: processArguments(), vbm(), yell() 
1210     # Parse '-t' option (user-specified temporary working dir) 
1211     ## Set DIR_TMP_PARENT to user-specified value if specified 
1212     local DIR_TMP_PARENT
 
1214     if [[ "$OPTION_TMPDIR" = "true" ]]; then 
1215         if [[ -d "$argTempDirPriority" ]]; then 
1216             DIR_TMP_PARENT
="$argTempDirPriority";  
1218             yell 
"WARNING:Specified temporary working directory not valid:$argTempDirPriority"; 
1219             exit 1; # Exit since user requires a specific temp dir and it is not available. 
1222     ## Set DIR_TMP_PARENT to default or fallback otherwise 
1223         if [[ -d "$DIR_TMP_DEFAULT" ]]; then 
1224             DIR_TMP_PARENT
="$DIR_TMP_DEFAULT"; 
1225         elif [[ -d /tmp 
]]; then 
1226             yell 
"WARNING:$DIR_TMP_DEFAULT not available. Falling back to /tmp ."; 
1227             DIR_TMP_PARENT
="/tmp"; 
1229             yell 
"ERROR:No valid working directory available. Exiting."; 
1233     ## Set DIR_TMP using DIR_TMP_PARENT and nonce (SCRIPT_TIME_START) 
1234     DIR_TMP
="$DIR_TMP_PARENT"/"$SCRIPT_TIME_START""..bkgpslog" && vbm 
"DEBUG:Set DIR_TMP to:$DIR_TMP"; # Note: removed at end of main().     
1235 } # Sets working dir 
1236 magicParseCustomTTL
() { 
1237     # Desc: Set user-specified TTLs for buffer and script 
1238     # Input: vars: argCustomBufferTTL (integer), argCustomScriptTTL_TE (string) 
1239     # Input: vars: OPTION_CUSTOM_BUFFERTTL, OPTION_CUSTOM_SCRIPTTTL 
1240     # Input: vars: BUFFER_TTL (integer), SCRIPT_TTL_TE (string) 
1241     # Output: BUFFER_TTL (integer), SCRIPT_TTL_TE (string) 
1242     # Depends validateInput(), showUsage(), yell 
1244     # React to '-b, --buffer-ttl' option 
1245     if [[ "$OPTION_CUSTOM_BUFFERTTL" = "true" ]]; then 
1246         ## T: Check if argCustomBufferTTL is an integer 
1247         if validateInput 
"$argCustomBufferTTL" "integer"; then 
1248             ### T: argCustomBufferTTL is an integer 
1249             BUFFER_TTL
="$argCustomBufferTTL"; 
1251             ### F: argcustomBufferTTL is not an integer 
1252             yell 
"ERROR:Invalid integer argument for custom buffer time-to-live."; showUsage
; exit 1; 
1254         ## F: do not change BUFFER_TTL 
1257     # React to '-B, --script-ttl' option 
1258     if [[ "$OPTION_CUSTOM_SCRIPTTTL_TE" = "true" ]]; then 
1259         ## T: Check if argCustomScriptTTL is a time element (ex: "day", "hour") 
1260         if validateInput 
"$argCustomScriptTTL" "time_element"; then 
1261             ### T: argCustomScriptTTL is a time element 
1262             SCRIPT_TTL_TE
="$argCustomScriptTTL"; 
1264             ### F: argcustomScriptTTL is not a time element 
1265             yell 
"ERROR:Invalid time element argument for custom script time-to-live."; showUsage
; exit 1; 
1267         ## F: do not change SCRIPT_TTL_TE 
1269 } # Sets custom script or buffer TTL if specified 
1274     processArguments 
"$@"; 
1275     ## Act upon arguments 
1276     ### Determine working directory 
1277     magicInitWorkingDir
; # Sets DIR_TMP from argTempDirPriority 
1278     ### Set output encryption and compression option strings 
1279     #### React to "-r" ("encryption recipients") option 
1280     magicParseRecipientArgs
; # Updates recPubKeysValid, CMD_ENCRYPT[_SUFFIX] from argRecPubKeys 
1281     #### React to "-c" ("compression") option 
1282     magicParseCompressionArg
; # Updates CMD_COMPRESS[_SUFFIX] 
1283     #### React to "-R" ("recipient directory") option 
1284     magicParseRecipientDir
; # Updates recPubKeysValid 
1285     #### React to custom buffer and script TTL options ("-b", "-B") 
1286     magicParseCustomTTL
; # Sets custom SCRIPT_TTL_TE and/or BUFFER_TTL if specified 
1288     # Check that critical apps and dirs are available, display missing ones. 
1289     if ! checkapp gpspipe 
tar && ! checkdir 
"$DIR_OUT" "DIR_TMP"; then 
1290         yell 
"ERROR:Critical components missing."; 
1291         displayMissing
; yell 
"Exiting."; exit 1; fi 
1293     # Set script lifespan (SCRIPT_TTL from SCRIPT_TTL_TE) 
1294     magicSetScriptTTL 
"$SCRIPT_TTL_TE"; 
1295     ## Note: SCRIPT_TTL_TE is time element string (ex: "day") while SCRIPT_TTL is integer seconds 
1297     # File name substring (ISO-8601 duration from BUFFER_TTL) 
1298     bufferTTL_STR
="$(timeDuration "$BUFFER_TTL")"; 
1300     # Init temp working dir 
1301     try 
mkdir "$DIR_TMP" && vbm 
"DEBUG:Working dir creatd at:$DIR_TMP"; 
1303     # Initialize 'tar' archive 
1304     ## Define output tar path (note: each day gets *one* tar file (Ex: "20200731..hostname_location.[.gpx.gz].tar")) 
1305     PATHOUT_TAR
="$DIR_OUT"/"$(dateShort)"..
"$SCRIPT_HOSTNAME""_location""$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX".
tar && \
 
1306         vbm 
"STATUS:Set PATHOUT_TAR to:$PATHOUT_TAR"; 
1307     ## Check that PATHOUT_TAR is a tar. Rename old and create empty one otherwise. 
1308     checkMakeTar 
"$PATHOUT_TAR" && vbm 
"DEBUG:Confirmed or Created to be a tar:$PATHOUT_TAR"; 
1309     ## Append VERSION file to PATHOUT_TAR 
1312     # Define GPS conversion commands 
1313     CMD_CONV_NMEA
="tee /dev/null " && vbm 
"STATUS:Set CMD_CONV_NMEA to:$CMD_CONV_NMEA"; # tee as passthrough 
1314     CMD_CONV_GPX
="gpsbabel -i nmea -f - -o gpx -F - " && vbm 
"STATUS:Set CMD_CONV_GPX to:$CMD_CONV_GPX"; # convert NMEA to GPX 
1315     CMD_CONV_KML
="gpsbabel -i nmea -f - -o kml -F - " && vbm 
"STATUS:Set CMD_CONV_KML to:$CMD_CONV_KML"; # convert NMEA to KML 
1317     # MAIN LOOP:Record gps data until script lifespan ends 
1318     timeBufferFirstNS
="$(timeEpochNS)"; bufferRound
=0; BUFFER_TTL_ADJ_FLOAT
="10.0"; 
1319     while [[ "$SECONDS" -lt "$SCRIPT_TTL" ]]; do 
1320         magicParseRecipientDir
; 
1321         magicGatherWriteBuffer 
& 
1322         sleep "$BUFFER_TTL_ADJ_FLOAT"; 
1323         magicBufferSleepPID
; # Calculates BUFFER_TTL_ADJ from BUFFER_TTL given buffer expected start time vs. actual 
1329     try 
rm -r "$DIR_TMP"; 
1331     vbm 
"STATUS:Main function finished."; 
1333 #===END Declare local script functions=== 
1334 #==END Define script parameters== 
1337 #==BEGIN Perform work and exit== 
1338 main 
"$@" # Run main function. 
1340 #==END Perform work and exit==