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 + ( errNS * 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     ## Calculate nanosecond remainder 
 993     BUFFER_TTL_ADJ_FLOATFRAC
="$((BUFFER_TTL_NS - (BUFFER_TTL_ADJ_INT*(10**9)) ))" && vbm 
"BUFFER_TTL_ADJ_FLOATFRAC:$BUFFER_TTL_ADJ_FLOATFRAC"; 
 994     ## Form float BUFFER_TTL_ADJ_FLOAT 
 995     BUFFER_TTL_ADJ_FLOAT
="$BUFFER_TTL_ADJ_INT".
"$BUFFER_TTL_ADJ_FLOATFRAC" && vbm 
"BUFFER_TTL_ADJ_FLOAT:$BUFFER_TTL_ADJ_FLOAT"; 
 996     vbm 
"STATUS:Calculated adjusted BUFFER_TTL (seconds):$BUFFER_TTL_ADJ_FLOAT"; 
 998 } # Calc BUFFER_TTL_ADJ_FLOAT so buffer starts every BUFFER_TTL seconds 
 999 magicWriteVersion
() { 
1000     # Desc: Appends time-stamped VERSION to PATHOUT_TAR 
1001     # Usage: magicWriteVersion 
1003     # Input: CONTENT_VERSION, FILEOUT_VERSION, PATHOUT_TAR, DIR_TMP 
1004     # Input: SCRIPT_VERSION, SCRIPT_URL, AGE_VERSION, AGE_URL, SCRIPT_HOSTNAME 
1005     # Output: appends tar PATHOUT_TAR 
1006     # Depends: dateTimeShort, appendArgTar 
1007     local CONTENT_VERSION pubKeyIndex
 
1009     # Set VERSION file name 
1010     FILEOUT_VERSION
="$(dateTimeShort)..VERSION"; 
1012     # Gather VERSION data in CONTENT_VERSION 
1013     CONTENT_VERSION
="SCRIPT_VERSION=$SCRIPT_VERSION"; 
1014     #CONTENT_VERSION="$CONTENT_VERSION""\\n"; 
1015     CONTENT_VERSION
="$CONTENT_VERSION""\\n""SCRIPT_NAME=$SCRIPT_NAME"; 
1016     CONTENT_VERSION
="$CONTENT_VERSION""\\n""SCRIPT_URL=$SCRIPT_URL"; 
1017     CONTENT_VERSION
="$CONTENT_VERSION""\\n""AGE_VERSION=$AGE_VERSION"; 
1018     CONTENT_VERSION
="$CONTENT_VERSION""\\n""AGE_URL=$AGE_URL"; 
1019     CONTENT_VERSION
="$CONTENT_VERSION""\\n""DATE=$(date --iso-8601=seconds)"; 
1020     CONTENT_VERSION
="$CONTENT_VERSION""\\n""HOSTNAME=$SCRIPT_HOSTNAME"; 
1021     ## Add list of recipient pubkeys 
1022     for pubkey 
in "${recPubKeysValid[@]}"; do 
1024         CONTENT_VERSION
="$CONTENT_VERSION""\\n""PUBKEY_$pubKeyIndex=$pubkey"; 
1026     ## Process newline escapes 
1027     CONTENT_VERSION
="$(echo -e "$CONTENT_VERSION")" 
1029     # Write CONTENT_VERSION as file FILEOUT_VERSION and write-append to PATHOUT_TAR 
1030     appendArgTar 
"$CONTENT_VERSION" "$FILEOUT_VERSION" "$PATHOUT_TAR" "$DIR_TMP"; 
1032 } # bkgpslog: write version data to PATHOUT_TAR via appendArgTar() 
1033 magicGatherWriteBuffer
() { 
1034     # Desc: bkgpslog-specific meta function for writing data to DIR_TMP then appending each file to PATHOUT_TAR 
1035     # Inputs: vars: PATHOUT_TAR FILEOUT_{NMEA,GPX,KML} CMD_CONV_{NMEA,GPX,KML} CMD_{COMPRESS,ENCRYPT} DIR_TMP, 
1036     # Inputs: vars: BUFFER_TTL bufferTTL_STR SCRIPT_HOSTNAME CMD_COMPRESS_SUFFIX CMD_ENCRYPT_SUFFIX 
1037     # Output: file: (PATHOUT_TAR) 
1038     # Depends: yell(), try(), vbm(), appendArgTar(), tar 1, sleep 8, checkMakeTar() 
1039     # Depends: magicWriteVersion(), appendFileTar() 
1042     # Debug:Get function name 
1043     FN
="${FUNCNAME[0]}"; 
1045     # Create buffer file with unique name 
1046     PATHOUT_BUFFER
="$DIR_TMP/buffer$SECONDS"; 
1048     timeout 
"$BUFFER_TTL"s gpspipe 
-r -o "$PATHOUT_BUFFER" ; 
1049     timeBufferStart
="$(dateTimeShort  "$
(date --date="$BUFFER_TTL seconds ago")")"; # Note start time 
1050     vbm 
"DEBUG:STATUS:$FN:Started magicWriteBuffer()."; 
1051     # Determine file paths (time is start of buffer period) 
1052     FILEOUT_BASENAME
="$timeBufferStart""--""$bufferTTL_STR""..""$SCRIPT_HOSTNAME""_location" && vbm 
"STATUS:Set FILEOUT_BASENAME to:$FILEOUT_BASENAME"; 
1053     ## Files saved to DIR_TMP 
1054     FILEOUT_NMEA
="$FILEOUT_BASENAME".nmea
"$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX" && vbm 
"STATUS:Set FILEOUT_NMEA to:$FILEOUT_NMEA"; 
1055     FILEOUT_GPX
="$FILEOUT_BASENAME".gpx
"$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX" && vbm 
"STATUS:Set FILEOUT_GPX to:$FILEOUT_GPX"; 
1056     FILEOUT_KML
="$FILEOUT_BASENAME".kml
"$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX" && vbm 
"STATUS:Set FILEOUT_KML to:$FILEOUT_KML"; 
1057     PATHOUT_NMEA
="$DIR_TMP"/"$FILEOUT_NMEA" && vbm 
"STATUS:Set PATHOUT_NMEA to:$PATHOUT_NMEA"; 
1058     PATHOUT_GPX
="$DIR_TMP"/"$FILEOUT_GPX" && vbm 
"STATUS:Set PATHOUT_GPX to:$PATHOUT_GPX"; 
1059     PATHOUT_KML
="$DIR_TMP"/"$FILEOUT_KML" && vbm 
"STATUS:Set PATHOUT_KML to:$PATHOUT_KML"; 
1060     ## Files saved to disk (DIR_OUT) 
1061     ### one file per day (Ex: "20200731..hostname_location.[.gpx.gz].tar") 
1062     PATHOUT_TAR
="$DIR_OUT"/"$(dateShort "$
(date --date="$BUFFER_TTL seconds ago")")"..
"$SCRIPT_HOSTNAME""_location""$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX".
tar && \
 
1063         vbm 
"STATUS:Set PATHOUT_TAR to:$PATHOUT_TAR"; 
1065     vbm 
"STATUS:DIR_TMP     :$DIR_TMP"; 
1066     vbm 
"STATUS:PATHOUT_TAR :$PATHOUT_TAR"; 
1067     vbm 
"STATUS:PATHOUT_NMEA:$PATHOUT_NMEA"; 
1068     vbm 
"STATUS:PATHOUT_GPX:$PATHOUT_GPX"; 
1069     vbm 
"STATUS:PATHOUT_KML:$PATHOUT_KML"; 
1072     # Validate PATHOUT_TAR as tar. 
1073     checkMakeTar 
"$PATHOUT_TAR"; 
1074     ## Add VERSION file if checkMakeTar had to create a tar (exited 1) or replace one (exited 2) 
1075     if [[ $? 
-eq 1 ]] || 
[[ $? 
-eq 2 ]]; then magicWriteVersion
; fi 
1077     # Write bufferBash to PATHOUT_TAR 
1078     wait; # Wait to avoid collision with older magicWriteBuffer() instances (see https://www.tldp.org/LDP/abs/html/x9644.html ) 
1079     appendFileTar 
"$PATHOUT_BUFFER" "$FILEOUT_NMEA" "$PATHOUT_TAR" "$DIR_TMP" "$CMD_CONV_NMEA" "$CMD_COMPRESS" "$CMD_ENCRYPT"; # Write NMEA data 
1080     appendFileTar 
"$PATHOUT_BUFFER" "$FILEOUT_GPX" "$PATHOUT_TAR" "$DIR_TMP" "$CMD_CONV_GPX" "$CMD_COMPRESS" "$CMD_ENCRYPT"; # Write GPX file 
1081     appendFileTar 
"$PATHOUT_BUFFER" "$FILEOUT_KML" "$PATHOUT_TAR" "$DIR_TMP" "$CMD_CONV_KML" "$CMD_COMPRESS" "$CMD_ENCRYPT"; # Write KML file 
1083     # Remove secured chunks from DIR_TMP 
1084     rm "$PATHOUT_BUFFER" "$PATHOUT_NMEA" "$PATHOUT_GPX" "$PATHOUT_KML"; 
1085     vbm 
"DEBUG:STATUS:$FN:Finished magicWriteBuffer()."; 
1086 } # write buffer to disk 
1087 magicParseRecipientDir
() { 
1088     # Desc: Updates recPubKeysValid with pubkeys in dir specified by '-R' option ("recipient directory") 
1089     # Inputs:  vars: OPTION_RECDIR, argRecDir, OPTION_ENCRYPT 
1090     #          arry: recPubKeysValid 
1091     # Outputs: arry: recPubKeysValid 
1092     # Depends: processArguments, 
1093     local recFileLine updateRecipients recipientDir
 
1094     declare -a candRecPubKeysValid
 
1096     # Check that '-e' and '-R' set 
1097     if [[ "$OPTION_ENCRYPT" = "true" ]] && [[ "$OPTION_RECDIR" = "true" ]]; then 
1098         ### Check that argRecDir is a directory. 
1099         if [[ -d "$argRecDir" ]]; then 
1100             recipientDir
="$argRecDir" && vbm 
"STATUS:Recipient watch directory detected:\"$recipientDir\""; 
1101             #### Initialize variable indicating outcome of pubkey review 
1102             unset updateRecipients
 
1103             #### Add existing recipients 
1104             candRecPubKeysValid
=("${recPubKeysValidStatic[@]}"); 
1105             #### Parse files in recipientDir 
1106             for file in "$recipientDir"/*; do 
1107                 ##### Read first line of each file 
1108                 recFileLine
="$(head -n1 "$file")" && vbm 
"STATUS:Checking if pubkey:\"$recFileLine\""; 
1109                 ##### check if first line is a valid pubkey 
1110                 if checkAgePubkey 
"$recFileLine" && \
 
1111                         ( validateInput 
"$recFileLine" "ssh_pubkey" || validateInput 
"$recFileLine" "age_pubkey"); then 
1112                     ###### T: add candidate pubkey to candRecPubKeysValid 
1113                     candRecPubKeysValid
+=("$recFileLine") && vbm 
"STATUS:RecDir pubkey is valid pubkey:\"$recFileLine\""; 
1115                     ###### F: throw warning; 
1116                     yell 
"ERROR:Invalid recipient file detected. Not modifying recipient list." 
1117                     updateRecipients
="false"; 
1120             #### Write updated recPubKeysValid array to recPubKeysValid if no failure detected 
1121             if ! [[ "$updateRecipients" = "false" ]]; then 
1122                 recPubKeysValid
=("${candRecPubKeysValid[@]}") && vbm 
"STATUS:Wrote candRecPubkeysValid to recPubKeysValid:\"${recPubKeysValid[*]}\""; 
1125             yell 
"ERROR:$0:Recipient directory $argRecDir does not exist. Exiting."; exit 1; 
1128     # Handle case if '-R' set but '-e' not set 
1129     if [[ ! "$OPTION_ENCRYPT" = "true" ]] && [[ "$OPTION_RECDIR" = "true" ]]; then 
1130         yell 
"ERROR: \\'-R\\' is set but \\'-e\\' is not set."; fi; 
1131 } # Update recPubKeysValid with argRecDir 
1132 magicParseRecipientArgs
() { 
1133     # Desc: Parses recipient arguments specified by '-r' option 
1134     # Input:  vars: OPTION_ENCRYPT from processArguments() 
1135     #         arry: argRecPubKeys from processArguments() 
1136     # Output: vars: CMD_ENCRYPT, CMD_ENCRYPT_SUFFIX 
1137     #         arry: recPubKeysValid, recPubKeysValidStatic 
1138     # Depends: checkapp(), checkAgePubkey(), validateInput(), processArguments() 
1141     # Check if encryption option active. 
1142     if [[ "$OPTION_ENCRYPT" = "true" ]] && [[ "$OPTION_RECIPIENTS" = "true" ]]; then  
1143         if checkapp age
; then # Check that age is available. 
1144             for pubkey 
in "${argRecPubKeys[@]}"; do # Validate recipient pubkey strings by forming test message 
1145                 vbm 
"DEBUG:Testing pubkey string:$pubkey"; 
1146                 if checkAgePubkey 
"$pubkey" && \
 
1147                         ( validateInput 
"$pubkey" "ssh_pubkey" || validateInput 
"$pubkey" "age_pubkey"); then 
1148                     #### Form age recipient string 
1149                     recipients
="$recipients""-r '$pubkey' "; 
1150                     vbm 
"STATUS:Added pubkey for forming age recipient string:""$pubkey"; 
1151                     vbm 
"DEBUG:recipients:""$recipients"; 
1152                     #### Add validated pubkey to recPubKeysValid array 
1153                     recPubKeysValid
+=("$pubkey") && vbm 
"DEBUG:recPubkeysValid:pubkey added:$pubkey"; 
1155                     yell 
"ERROR:Exit code ""$?"". Invalid recipient pubkey string. Exiting."; exit 1; 
1158             vbm 
"DEBUG:Finished processing argRecPubKeys array"; 
1159             vbm 
"STATUS:Array of validated pubkeys:${recPubKeysValid[*]}"; 
1160             recPubKeysValidStatic
=("${recPubKeysValid[@]}"); # Save static image of pubkeys validated by this function 
1162             ##  Form age command string 
1163             CMD_ENCRYPT
="age ""$recipients " && vbm 
"CMD_ENCRYPT:$CMD_ENCRYPT"; 
1164             CMD_ENCRYPT_SUFFIX
=".age" && vbm 
"CMD_ENCRYPT_SUFFIX:$CMD_ENCRYPT_SUFFIX"; 
1166             yell 
"ERROR:Encryption enabled but \"age\" not found. Exiting."; exit 1; 
1169         CMD_ENCRYPT
="tee /dev/null " && vbm 
"CMD_ENCRYPT:$CMD_ENCRYPT"; 
1170         CMD_ENCRYPT_SUFFIX
="" && vbm 
"CMD_ENCRYPT_SUFFIX:$CMD_ENCRYPT_SUFFIX"; 
1171         vbm 
"DEBUG:Encryption not enabled." 
1173     # Catch case if '-e' is set but '-r' or '-R' is not 
1174     if [[ "$OPTION_ENCRYPT" = "true" ]] && [[ ! "$OPTION_RECIPIENTS" = "true" ]]; then 
1175         yell 
"ERROR:\\'-e\\' set but no \\'-r\\' or \\'-R\\' set."; exit 1; fi; 
1176     # Catch case if '-r' or '-R' set but '-e' is not 
1177     if [[ ! "$OPTION_ENCRYPT" = "true" ]] && [[ "$OPTION_RECIPIENTS" = "true" ]]; then 
1178         yell 
"ERROR:\\'-r\\' or \\'-R\\' set but \\'-e\\' is not set."; exit 1; fi;  
1179 } # Populate recPubKeysValid with argRecPubKeys; form encryption cmd string and filename suffix 
1180 magicParseCompressionArg
() { 
1181     # Desc: Parses compression arguments specified by '-c' option 
1182     # Input:  vars: OPTION_COMPRESS 
1183     # Output: CMD_COMPRESS, CMD_COMPRESS_SUFFIX 
1184     # Depends: checkapp(), vbm(), gzip,  
1185     if [[ "$OPTION_COMPRESS" = "true" ]]; then # Check if compression option active 
1186         if checkapp 
gzip; then # Check if gzip available 
1187             CMD_COMPRESS
="gzip " && vbm 
"CMD_COMPRESS:$CMD_COMPRESS"; 
1188             CMD_COMPRESS_SUFFIX
=".gz" && vbm 
"CMD_COMPRESS_SUFFIX:$CMD_COMPRESS_SUFFIX"; 
1190             yell 
"ERROR:Compression enabled but \"gzip\" not found. Exiting."; exit 1; 
1193         CMD_COMPRESS
="tee /dev/null " && vbm 
"CMD_COMPRESS:$CMD_COMPRESS"; 
1194         CMD_COMPRESS_SUFFIX
="" && vbm 
"CMD_COMPRESS_SUFFIX:$CMD_COMPRESS_SUFFIX"; 
1195         vbm 
"DEBUG:Compression not enabled."; 
1197 } # Form compression cmd string and filename suffix 
1198 magicInitWorkingDir
() { 
1199     # Desc: Determine temporary working directory from defaults or user input 
1200     # Usage: magicInitWorkignDir 
1201     # Input:  vars: OPTION_TEMPDIR, argTempDirPriority, DIR_TMP_DEFAULT 
1202     # Input:  vars: SCRIPT_TIME_START 
1203     # Output: vars: DIR_TMP 
1204     # Depends: processArguments(), vbm(), yell() 
1205     # Parse '-t' option (user-specified temporary working dir) 
1206     ## Set DIR_TMP_PARENT to user-specified value if specified 
1207     local DIR_TMP_PARENT
 
1209     if [[ "$OPTION_TMPDIR" = "true" ]]; then 
1210         if [[ -d "$argTempDirPriority" ]]; then 
1211             DIR_TMP_PARENT
="$argTempDirPriority";  
1213             yell 
"WARNING:Specified temporary working directory not valid:$argTempDirPriority"; 
1214             exit 1; # Exit since user requires a specific temp dir and it is not available. 
1217     ## Set DIR_TMP_PARENT to default or fallback otherwise 
1218         if [[ -d "$DIR_TMP_DEFAULT" ]]; then 
1219             DIR_TMP_PARENT
="$DIR_TMP_DEFAULT"; 
1220         elif [[ -d /tmp 
]]; then 
1221             yell 
"WARNING:$DIR_TMP_DEFAULT not available. Falling back to /tmp ."; 
1222             DIR_TMP_PARENT
="/tmp"; 
1224             yell 
"ERROR:No valid working directory available. Exiting."; 
1228     ## Set DIR_TMP using DIR_TMP_PARENT and nonce (SCRIPT_TIME_START) 
1229     DIR_TMP
="$DIR_TMP_PARENT"/"$SCRIPT_TIME_START""..bkgpslog" && vbm 
"DEBUG:Set DIR_TMP to:$DIR_TMP"; # Note: removed at end of main().     
1230 } # Sets working dir 
1231 magicParseCustomTTL
() { 
1232     # Desc: Set user-specified TTLs for buffer and script 
1233     # Input: vars: argCustomBufferTTL (integer), argCustomScriptTTL_TE (string) 
1234     # Input: vars: OPTION_CUSTOM_BUFFERTTL, OPTION_CUSTOM_SCRIPTTTL 
1235     # Input: vars: BUFFER_TTL (integer), SCRIPT_TTL_TE (string) 
1236     # Output: BUFFER_TTL (integer), SCRIPT_TTL_TE (string) 
1237     # Depends validateInput(), showUsage(), yell 
1239     # React to '-b, --buffer-ttl' option 
1240     if [[ "$OPTION_CUSTOM_BUFFERTTL" = "true" ]]; then 
1241         ## T: Check if argCustomBufferTTL is an integer 
1242         if validateInput 
"$argCustomBufferTTL" "integer"; then 
1243             ### T: argCustomBufferTTL is an integer 
1244             BUFFER_TTL
="$argCustomBufferTTL"; 
1246             ### F: argcustomBufferTTL is not an integer 
1247             yell 
"ERROR:Invalid integer argument for custom buffer time-to-live."; showUsage
; exit 1; 
1249         ## F: do not change BUFFER_TTL 
1252     # React to '-B, --script-ttl' option 
1253     if [[ "$OPTION_CUSTOM_SCRIPTTTL_TE" = "true" ]]; then 
1254         ## T: Check if argCustomScriptTTL is a time element (ex: "day", "hour") 
1255         if validateInput 
"$argCustomScriptTTL" "time_element"; then 
1256             ### T: argCustomScriptTTL is a time element 
1257             SCRIPT_TTL_TE
="$argCustomScriptTTL"; 
1259             ### F: argcustomScriptTTL is not a time element 
1260             yell 
"ERROR:Invalid time element argument for custom script time-to-live."; showUsage
; exit 1; 
1262         ## F: do not change SCRIPT_TTL_TE 
1264 } # Sets custom script or buffer TTL if specified 
1269     processArguments 
"$@"; 
1270     ## Act upon arguments 
1271     ### Determine working directory 
1272     magicInitWorkingDir
; # Sets DIR_TMP from argTempDirPriority 
1273     ### Set output encryption and compression option strings 
1274     #### React to "-r" ("encryption recipients") option 
1275     magicParseRecipientArgs
; # Updates recPubKeysValid, CMD_ENCRYPT[_SUFFIX] from argRecPubKeys 
1276     #### React to "-c" ("compression") option 
1277     magicParseCompressionArg
; # Updates CMD_COMPRESS[_SUFFIX] 
1278     #### React to "-R" ("recipient directory") option 
1279     magicParseRecipientDir
; # Updates recPubKeysValid 
1280     #### React to custom buffer and script TTL options ("-b", "-B") 
1281     magicParseCustomTTL
; # Sets custom SCRIPT_TTL_TE and/or BUFFER_TTL if specified 
1283     # Check that critical apps and dirs are available, display missing ones. 
1284     if ! checkapp gpspipe 
tar && ! checkdir 
"$DIR_OUT" "DIR_TMP"; then 
1285         yell 
"ERROR:Critical components missing."; 
1286         displayMissing
; yell 
"Exiting."; exit 1; fi 
1288     # Set script lifespan (SCRIPT_TTL from SCRIPT_TTL_TE) 
1289     magicSetScriptTTL 
"$SCRIPT_TTL_TE"; 
1290     ## Note: SCRIPT_TTL_TE is time element string (ex: "day") while SCRIPT_TTL is integer seconds 
1292     # File name substring (ISO-8601 duration from BUFFER_TTL) 
1293     bufferTTL_STR
="$(timeDuration "$BUFFER_TTL")"; 
1295     # Init temp working dir 
1296     try mkdir 
"$DIR_TMP" && vbm 
"DEBUG:Working dir creatd at:$DIR_TMP"; 
1298     # Initialize 'tar' archive 
1299     ## Define output tar path (note: each day gets *one* tar file (Ex: "20200731..hostname_location.[.gpx.gz].tar")) 
1300     PATHOUT_TAR
="$DIR_OUT"/"$(dateShort)"..
"$SCRIPT_HOSTNAME""_location""$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX".
tar && \
 
1301         vbm 
"STATUS:Set PATHOUT_TAR to:$PATHOUT_TAR"; 
1302     ## Check that PATHOUT_TAR is a tar. Rename old and create empty one otherwise. 
1303     checkMakeTar 
"$PATHOUT_TAR" && vbm 
"DEBUG:Confirmed or Created to be a tar:$PATHOUT_TAR"; 
1304     ## Append VERSION file to PATHOUT_TAR 
1307     # Define GPS conversion commands 
1308     CMD_CONV_NMEA
="tee /dev/null " && vbm 
"STATUS:Set CMD_CONV_NMEA to:$CMD_CONV_NMEA"; # tee as passthrough 
1309     CMD_CONV_GPX
="gpsbabel -i nmea -f - -o gpx -F - " && vbm 
"STATUS:Set CMD_CONV_GPX to:$CMD_CONV_GPX"; # convert NMEA to GPX 
1310     CMD_CONV_KML
="gpsbabel -i nmea -f - -o kml -F - " && vbm 
"STATUS:Set CMD_CONV_KML to:$CMD_CONV_KML"; # convert NMEA to KML 
1312     # MAIN LOOP:Record gps data until script lifespan ends 
1313     timeBufferFirstNS
="$(timeEpochNS)"; bufferRound
=0 
1314     while [[ "$SECONDS" -lt "$SCRIPT_TTL" ]]; do 
1315         magicParseRecipientDir
; 
1316         magicGatherWriteBuffer 
& 
1317         magicBufferSleepPID
; # Calculates BUFFER_TTL_ADJ from BUFFER_TTL given buffer expected start time vs. actual 
1318         sleep "$BUFFER_TTL_ADJ_FLOAT"; 
1324     try 
rm -r "$DIR_TMP"; 
1326     vbm 
"STATUS:Main function finished."; 
1328 #===END Declare local script functions=== 
1329 #==END Define script parameters== 
1332 #==BEGIN Perform work and exit== 
1333 main 
"$@" # Run main function. 
1335 #==END Perform work and exit==