2 # Desc: Recursively timestamp files with OpenTimestamp 
   7 declare -g max_job_count
="2"; # default max job count 
   8 declare -g age_threshold
="60"; # min age to add file; seconds; 
   9 declare -g stamp_throttle
="0.1"; # seconds delay between stamps 
  10 declare -g stamp_throttle_fail
="1"; # seconds delay if stamp errors 
  11 declare -Ag appRollCall 
# Associative array for storing app status 
  12 declare -Ag fileRollCall 
# Associative array for storing file status 
  13 declare -Ag dirRollCall 
# Associative array for storing dir status 
  14 declare -ag arrayPosArgs 
# Associative array for processArgs() function 
  15 declare -ag calendars
; 
  16 calendars
+=("https://finney.calendar.eternitywall.com"); 
  17 calendars
+=("https://btc.calendar.catallaxy.com"); 
  18 calendars
+=("https://alice.btc.calendar.opentimestamps.org"); 
  19 calendars
+=("https://bob.btc.calendar.opentimestamps.org"); 
  22 yell
() { echo "$0: $*" >&2; } # print script path and all args to stderr 
  23 die
() { yell 
"$*"; exit 111; } # same as yell() but non-zero exit status 
  24 must
() { "$@" || die 
"cannot $*"; } # runs args as command, reports args if command fails 
  26     # Desc: If arg is a command, save result in assoc array 'appRollCall' 
  27     # Usage: checkapp arg1 arg2 arg3 ... 
  29     # Input: global assoc. array 'appRollCall' 
  30     # Output: adds/updates key(value) to global assoc array 'appRollCall' 
  36         if command -v "$arg" 1>/dev
/null 
2>&1; then # Check if arg is a valid command 
  37             appRollCall
[$arg]="true"; 
  38             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi; 
  40             appRollCall
[$arg]="false"; returnState
="false"; 
  44     #===Determine function return code=== 
  45     if [ "$returnState" = "true" ]; then 
  50 } # Check that app exists 
  52     # Desc: If arg is a file path, save result in assoc array 'fileRollCall' 
  53     # Usage: checkfile arg1 arg2 arg3 ... 
  55     # Input: global assoc. array 'fileRollCall' 
  56     # Output: adds/updates key(value) to global assoc array 'fileRollCall'; 
  57     # Output: returns 0 if app found, 1 otherwise 
  63         if [ -f "$arg" ]; then 
  64             fileRollCall
["$arg"]="true"; 
  65             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi; 
  67             fileRollCall
["$arg"]="false"; returnState
="false"; 
  71     #===Determine function return code=== 
  72     if [ "$returnState" = "true" ]; then 
  77 } # Check that file exists 
  79     # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' 
  80     # Usage: checkdir arg1 arg2 arg3 ... 
  82     # Input: global assoc. array 'dirRollCall' 
  83     # Output: adds/updates key(value) to global assoc array 'dirRollCall'; 
  84     # Output: returns 0 if all args are dirs; 1 otherwise 
  90         if [ -z "$arg" ]; then 
  91             dirRollCall
["(Unspecified Dirname(s))"]="false"; returnState
="false"; 
  92         elif [ -d "$arg" ]; then 
  93             dirRollCall
["$arg"]="true"; 
  94             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi 
  96             dirRollCall
["$arg"]="false"; returnState
="false"; 
 100     #===Determine function return code=== 
 101     if [ "$returnState" = "true" ]; then 
 106 } # Check that dir exists 
 108     # Desc: Displays missing apps, files, and dirs 
 109     # Usage: displayMissing 
 111     # Input: associative arrays: appRollCall, fileRollCall, dirRollCall 
 112     # Output: stderr: messages indicating missing apps, file, or dirs 
 113     # Output: returns exit code 0 if nothing missing; 1 otherwise 
 114     # Depends: bash 5, checkAppFileDir() 
 115     local missingApps value appMissing missingFiles fileMissing
 
 116     local missingDirs dirMissing
 
 118     #==BEGIN Display errors== 
 119     #===BEGIN Display Missing Apps=== 
 120     missingApps
="Missing apps  :"; 
 121     #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done 
 122     for key 
in "${!appRollCall[@]}"; do 
 123         value
="${appRollCall[$key]}"; 
 124         if [ "$value" = "false" ]; then 
 125             #echo "DEBUG:Missing apps: $key => $value"; 
 126             missingApps
="$missingApps""$key "; 
 130     if [ "$appMissing" = "true" ]; then  # Only indicate if an app is missing. 
 131         echo "$missingApps" 1>&2; 
 134     #===END Display Missing Apps=== 
 136     #===BEGIN Display Missing Files=== 
 137     missingFiles
="Missing files:"; 
 138     #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done 
 139     for key 
in "${!fileRollCall[@]}"; do 
 140         value
="${fileRollCall[$key]}"; 
 141         if [ "$value" = "false" ]; then 
 142             #echo "DEBUG:Missing files: $key => $value"; 
 143             missingFiles
="$missingFiles""$key "; 
 147     if [ "$fileMissing" = "true" ]; then  # Only indicate if an app is missing. 
 148         echo "$missingFiles" 1>&2; 
 151     #===END Display Missing Files=== 
 153     #===BEGIN Display Missing Directories=== 
 154     missingDirs
="Missing dirs:"; 
 155     #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done 
 156     for key 
in "${!dirRollCall[@]}"; do 
 157         value
="${dirRollCall[$key]}"; 
 158         if [ "$value" = "false" ]; then 
 159             #echo "DEBUG:Missing dirs: $key => $value"; 
 160             missingDirs
="$missingDirs""$key "; 
 164     if [ "$dirMissing" = "true" ]; then  # Only indicate if an dir is missing. 
 165         echo "$missingDirs" 1>&2; 
 168     #===END Display Missing Directories=== 
 170     #==END Display errors== 
 171     #==BEGIN Determine function return code=== 
 172     if [ "$appMissing" == "true" ] || 
[ "$fileMissing" == "true" ] || 
[ "$dirMissing" == "true" ]; then 
 177     #==END Determine function return code=== 
 178 } # Display missing apps, files, dirs 
 180     # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true". 
 181     # Usage: vbm "DEBUG :verbose message here" 
 183     # Input: arg1: string 
 186     # Depends: bash 5.0.3, GNU-coreutils 8.30 (echo, date) 
 188     if [ "$opVerbose" = "true" ]; then 
 189         functionTime
="$(date --iso-8601=ns)"; # Save current time in nano seconds. 
 190         echo "[$functionTime]:$0:""$*" 1>&2;  # Display argument text. 
 194     return 0; # Function finished. 
 195 } # Displays message if opVerbose true 
 197     # Desc: Displays script version and license information. 
 200     # Input: scriptVersion   var containing version string 
 202     # Depends: vbm(), yell, GNU-coreutils 8.30 
 204     # Initialize function 
 205     vbm 
"DEBUG:showVersion function called." 
 209 Copyright (C) 2022 Steven Baltakatei Sandoval 
 210 License GPLv3: GNU GPL version 3 
 211 This is free software; you are free to change and redistribute it. 
 212 There is NO WARRANTY, to the extent permitted by law. 
 215     Copyright (C) 2020 Free Software Foundation, Inc. 
 216     License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. 
 217     This is free software: you are free to change and redistribute it. 
 218     There is NO WARRANTY, to the extent permitted by law. 
 222     vbm 
"DEBUG:showVersion function ended." 
 223     return 0; # Function finished. 
 224 } # Display script version. 
 226     # Desc: Display script usage information 
 231     # Depends: GNU-coreutils 8.30 (cat) 
 234         bkots [ options ] [PATH...] 
 236     POSITIONAL ARGUMENTS: 
 237         PATH    Path(s) of file(s) or directory(ies) 
 241                 Do everything except run 'ots' commands. 
 243                 Display help information. 
 245                 Include files and directories starting with '.' (not 
 246                 included by default). 
 248                 Specify simultaneous job count (default: 2) 
 250                 Consider files in dirs recursively. 
 252                 Display script version. 
 254                 Display debugging info. 
 256                 Mark end of options. Interpret remaining arguments as 
 257                 positional arguments. 
 260         Scans files by file paths or directory paths provided by 
 261         positional arguments to see if Open Timestamps '.ots' file 
 262         exists. If so, attempt to upgrade and verify the '.ots' 
 263         file. If no '.ots' file exists, attempt to create one. 
 265         Files with a dotfile parent directory located anywhere in the 
 266         file path are ignored by default. (e.g. 'HEAD' in 
 267         '/home/user/diary/.git/logs/HEAD' because of '.git'). Dotfiles 
 268         themselves are also ignored by default 
 269         (e.g. '/home/user/.gitconfig'). 
 271         Files modified less than 1 minute ago are ignored. 
 275       bkots foo.txt bar.pdf /home/username/Pictures/ 
 277 } # Display information on how to use this script. 
 279     # Desc: Count and return total number of jobs 
 282     # Output: stdout   integer number of jobs 
 283     # Depends: Bash 5.1.16 
 284     # Example: while [[$(count_jobs) -gt 0]]; do echo "Working..."; sleep 1; done; 
 288     job_count
="$(jobs -r | wc -l | tr -d ' ' )"; 
 289     #yell "DEBUG:job_count:$job_count"; 
 290     if [[ -z $job_count ]]; then job_count
="0"; fi; 
 292 }; # Return number of background jobs 
 294     # Desc: Does not return until count_jobs() falls below $max_job_count 
 295     # Input: var max_job_count 
 296     # Output: return code 0 
 297     # Depends: count_jobs(), yell(); 
 298     while [[ $
(count_jobs
) -ge $max_job_count ]]; do 
 299         printf "\r%d:Working..." "$SECONDS"; 
 305     # Desc: Processes arguments provided to script. 
 306     # Usage: processArgs "$@" 
 308     # Input: "$@"          (list of arguments provided to the function) 
 309     # Output: Sets following variables used by other functions: 
 310     #   opVerbose            Indicates verbose mode enable status.  (ex: "true", "false") 
 311     #   arrayPosArgs         Array of remaining positional argments 
 313     #   yell()           Displays messages to stderr. 
 314     #   vbm()            Displays messsages to stderr if opVerbose set to "true". 
 315     #   showUsage()      Displays usage information about parent script. 
 316     #   showVersion()    Displays version about parent script. 
 317     #   arrayPosArgs     Global array for storing non-option positional arguments (i.e. arguments following the `--` option). 
 318     # External dependencies: bash (5.1.16), echo 
 320     #  [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347 
 321     #  [2]: "Handling positional parameters" (2018-05-12). https://wiki.bash-hackers.org/scripting/posparams 
 323     # Initialize function 
 324     vbm 
"DEBUG:processArgs function called." 
 327     while [ ! $# -eq 0 ]; do   # While number of arguments ($#) is not (!) equal to (-eq) zero (0). 
 328         #yell "DEBUG:Starting processArgs while loop." # Debug stderr message. See [1]. 
 329         #yell "DEBUG:Provided arguments are:""$*"      # Debug stderr message. See [1]. 
 331             --dry-run) # Do not run ots commands 
 332                 option_dry_run
="true"; 
 333                 vbm 
"DEBUG:Option enabled:dry run";; 
 334             -h | 
--help) showUsage
; exit 1;; # Display usage. 
 335             --include-dotfiles) # Include dotfiles 
 336                 option_include_dotfiles
="true"; 
 337                 vbm 
"DEBUG:Option enabled:include dotfiles";; 
 338             -j | 
--jobs) # Specify max_job_count 
 339                 if [[ -n "$2" ]] && [[ "$2" =~ ^
[0-9]+$ 
]]; then 
 341                     vbm 
"STATUS:Max job count set to:$max_job_count"; 
 345                     die 
"FATAL:Invalid job count:$2"; 
 347             -r | 
--recursive) # Specify recursive option 
 348                 option_recursive
="true"; 
 349                 vbm 
"DEBUG:option enabled:include files in dirs recursively";; 
 350             --version) showVersion
; exit 1;; # Show version 
 351             -v | 
--verbose) opVerbose
="true"; vbm 
"DEBUG:Verbose mode enabled.";; # Enable verbose mode. See [1]. 
 352             --) # End of all options. See [2]. 
 355                     vbm 
"DEBUG:adding to arrayPosArgs:$arg"; 
 356                     arrayPosArgs
+=("$arg"); 
 359             -*) showUsage
; yell 
"ERROR: Unrecognized option."; exit 1;; # Display usage 
 360             *) # Assume remaining arguments are positional arguments 
 362                     vbm 
"DEBUG:adding to arrayPosArgs:$arg"; 
 363                     arrayPosArgs
+=("$arg"); 
 366             #*) showUsage; yell "ERROR: Unrecognized argument."; exit 1;; # Handle unrecognized options. See [1]. 
 372     vbm 
"DEBUG:processArgs function ended." 
 373     return  0; # Function finished. 
 374 }; # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.). 
 375 get_parent_dirnames
() { 
 376     # Desc: Provides newline-delimited list of each parent dir of a file or dir 
 377     # Usage: get_parent_dirnames arg1 
 378     # Input: arg1  input  path 
 379     # Output: stdout   newline-delimited list of parent dirs 
 381     # Depends: yell(), die(), must() 
 385     if [[ $# -ne 1 ]]; then die 
"FATAL:Incorrect number of arguments:$#"; fi; 
 386     if ! { [[ -f $1 ]] || 
[[ -d $1 ]]; }; then die 
"FATAL:Not a file or dir:$1"; fi; 
 390     while [[ -f $path ]] || 
[[ -d $path ]]; do 
 391         path
="$(dirname "$path")"; 
 392         name_base_previous
="$name_base"; 
 393         name_base
="$(basename "$path")"; 
 394         ## Check for stop condition (dirname returns same result as previous iteration) 
 395         if [[ $name_base == "$name_base_previous" ]]; then break; fi; 
 398 }; # Output parent dirnames to stdout 
 400     # Desc: Creates `.ots` file: 
 401     #     - for each file specified in arrayPosArgs array 
 402     #     - for each file in each dir specified in arrayPosArgs array 
 403     #   Output file created alongside each file or in output directory specified by pathDirIn1 
 405     # Input: arrayPosArgs   array with positional arguments 
 406     #        pathDirOut1    path for output `.ots` files (if pathDirOut1 is specified and is a path) 
 407     #        age_threshold  var: mininum age in seconds to timestamp file 
 409     # Output: file(s) creates `.ots` file alongside specified files 
 410     # Depends: find (GNU findutils) 4.8.0, GNU Coreutils 8.32 (sort), GNU Parallel 20210822 
 411     # Ref/Attrib: [1] How to create an array of unique elements from a string/array in bash https://unix.stackexchange.com/a/167194 
 412     #             [2] How to find files containing newlines in their names https://stackoverflow.com/a/21727028 
 413     #             [3] Get mtime of specific file using Bash? https://stackoverflow.com/a/4774377 
 414     local -a file_list file_list_pruned
; 
 415     local -a files_to_verify files_to_upgrade files_to_stamp
 
 416     local -a files_to_verify_pruned files_to_upgrade_pruned files_to_stamp_pruned
 
 422     if ! checkapp ots 
find parallel
; then 
 424         die 
"FATAL:Missing dependencies."; 
 428     for arg 
in "${arrayPosArgs[@]}"; do 
 429         arg
="$(readlink -f "$arg")"; 
 430         if ! { [[ -d $arg ]] || 
[[ -f $arg ]]; }; then 
 431             die 
"FATAL:Not a file or dir:arg:$arg"; 
 435     # Display ots details 
 436     vbm 
"$(type ots)"; # show how 'ots' is defined 
 437         #TODO: add option to define 'ots' as a bash function that 
 438         #populates the ots option '--bitcoin-node FILE' with a 
 439         #user-specified FILE. 
 442     vbm 
"DEBUG:begin populate file_list array"; 
 443     for item 
in "${arrayPosArgs[@]}"; do 
 444         vbm 
"DEBUG:adding to file list:item:$item"; 
 446         ## Get full canonicalized path (follow symlinks) 
 447         item
="$(readlink -f "$item")"; 
 448         vbm 
"DEBUG:item full path:item:$item"; 
 450         ## Add to list: files 
 451         if [[ -f $item ]]; then 
 452             vbm 
"DEBUG:is a file:item:$item"; 
 453             file_list
+=("$item"); 
 454             vbm 
"DEBUG:added to file_list:$item"; 
 455         ## Add to list: files in dirs 
 456         elif [[ -d $item ]]; then 
 457             vbm 
"DEBUG:is a dir:item:$item"; 
 458             ### Check for recursive flag 
 459             if [[ "$option_recursive" == "true" ]]; then 
 460                 vbm 
"DEBUG:option_recursive:$option_recursive"; 
 461                 while read -r line
; do 
 462                     file_list
+=("$line"); 
 463                     vbm 
"DEBUG:added to file_list:$line"; 
 464                 done < <(find "$item" -type f
); 
 466                 while read -r line
; do 
 467                     file_list
+=("$line"); 
 468                     vbm 
"DEBUG:added to file_list:$line"; 
 469                 done < <(find "$item" -maxdepth 1 -type f
); 
 472             die 
"FATAL:Not a file or dir:item:$item"; 
 475     if [[ $opVerbose == "true" ]]; then 
 476         vbm 
"DEBUG:file_list:"; 
 477         printf "%s\n" "${file_list[@]}"; 
 481     for item 
in "${file_list[@]}"; do 
 482         ## Ignore files that end in '.ots.bak'. 
 483         if [[ $item =~ 
'.ots.bak'$ 
]]; then 
 484             vbm 
"DEBUG:Skipping file ending in '.ots.bak':item:$item"; 
 485             continue; # skip to next item 
 489         if ! [[ $option_include_dotfiles == "true" ]]; then 
 490             ### Ignore files if '/.' contained within canonical path 
 491             pattern
="/\."; # a dot after a forward slash 
 492             if [[ $item =~ 
$pattern ]]; then 
 496             # ### Ignore files located beneath a dotfile directory (e.g. '/home/my_repo/.git/config') 
 497             # unset flag_contains_dotfile_parent; 
 498             # while read -r line; do 
 499             #     #### Check line from output of get_parent_dirnames 
 501             #     if [[ $line =~ $pattern ]]; then 
 502             #         ##### line starts with '.' 
 503             #         vbm "DEBUG:Dotfile parent detected. Not including in file_list_pruned:$item"; 
 504             #         vbm "DEBUG:Dotfile in path:item:$item"; 
 505             #         vbm "DEBUG:Dotfile parent:line:$line"; 
 506             #         flag_contains_dotfile_parent="true"; 
 509             # done < <(get_parent_dirnames "$item"); 
 510             # if [[ $flag_contains_dotfile_parent == "true" ]]; then 
 511             #     unset flag_contains_dotfile_parent; 
 512             #     continue; # skip to next item (i.e. don't add to file_list_pruned) 
 515             # ### Ignore dotfiles themselves 
 516             # item_basename="$(basename "$item")"; 
 518             # if [[ $item_basename =~ $pattern ]]; then 
 519             #     vbm "INFO :Skipping dotfile:item:$item"; 
 520             #     continue; # skip to next item 
 524         ## Ignore files with newlines present in filename. See [2]. 
 525         if [[ $item =~ $
'\n' ]]; then 
 526             vbm 
"DEBUG:Skipping file name with newline:$item"; 
 527             continue; # skip to next item 
 530         ## Ignore files that end in '~'. 
 531         if [[ $item =~ ~$ 
]]; then 
 532             vbm 
"DEBUG:Skipping file ending in tilde:$item"; 
 533             continue; # skip to next item 
 536         ## Ignore files modified less than age_threshold. See [3]. 
 537         time_now
="$(date +%s)"; # epoch seconds 
 538         item_age
="$(stat -c %Y "$item")"; # age in seconds 
 539         if [[ $
(( time_now 
- item_age 
)) -lt "$age_threshold" ]]; then 
 540             yell 
"INFO :Skipping file modified less than $age_threshold seconds ago:item:$item"; 
 541             continue; # skip to next item 
 544         ## Add item to file_list_pruned 
 545         file_list_pruned
+=("$item"); 
 547     if [[ $opVerbose == "true" ]]; then 
 548         vbm 
"DEBUG:file_list_pruned:"; 
 549         printf "%s\n" "${file_list_pruned[@]}"; 
 552     # Decide what actions to take for items in file_list_pruned 
 553     for item 
in "${file_list_pruned[@]}"; do 
 554         vbm 
"DEBUG:considering action to take for item:$item"; 
 555         unset path_src path_prf dir_parent dir_source
; 
 557         ## Check file extension 
 558         if [[ $item =~ .ots$ 
]]; then 
 559             ### item ends in '.ots'. Item is proof file. 
 560             vbm 
"DEBUG:item ends in '.ots'. Item is proof file:item:$item"; 
 561             if [[ -f ${item%.ots} ]]; then 
 562                 #### Proof file (item) is adjacent to source file 
 563                 vbm 
"DEBUG:Proof file (item) is adjacent to source file."; 
 564                 ##### Upgrade and verify proof file against adjacent source file 
 565                 vbm 
"DEBUG:Marking proof file to be upgraded and verified."; 
 566                 path_src
="${item%.ots}"; 
 568                 files_to_upgrade
+=("$(printf "%s
" "$path_prf")"); 
 569                 files_to_verify
+=("$(printf "%s
\n%s
" "$path_src" "$path_prf")"); 
 571                 #### Proof file (item) is not adjacent to source file 
 572                 vbm 
"DEBUG:Proof file (item) is not adjacent to source file."; 
 573                 #### Check if source file in parent dir 
 574                 dir_parent
="$(dirname "$
(dirname "$item")" )"; 
 575                 cand_src_filename
="$(basename "$item")"; 
 576                 cand_src_path
="$dir_parent/$cand_src_filename"; 
 577                 if [[ -f "$cand_src_path" ]]; then 
 578                     ##### source file in parent dir 
 579                     vbm 
"DEBUG:found source file in parent:cand_src_path:$cand_src_path"; 
 580                     path_src
="$cand_src_path"; 
 582                     files_to_upgrade
+=("$(printf "%s
" "$path_prf")"); 
 583                     files_to_verify
+=("$(printf "%s
\n%s
" "$path_src" "$path_prf")"); 
 585                     #### Throw non-fatal error 
 586                     vbm 
"DEBUG:Source file not found for proof file:item:$item"; 
 587                     yell 
"ERROR:Item is proof file but source filei not adjacent in parent dir. item:$item"; 
 588                     #### Attempt upgrade only 
 589                     vbm 
"DEBUG:Marking proof file to be upgraded."; 
 591                     files_to_upgrade
+=("$(printf "%s
" "$path_prf")"); 
 595             ### item does not end in '.ots'. Item is source file. 
 596             vbm 
"DEBUG:item does NOT end in '.ots'. Item is source file."; 
 597             if [[ -f "$item".ots 
]]; then 
 598                 #### Proof file is adjacent to source file (item). 
 599                 vbm 
"DEBUG:Proof file is adjacent to source file (item)."; 
 600                 ##### Upgrade and verify proof file against adjacent source file. 
 601                 vbm 
"DEBUG:Marking proof file to be upgraded and verified."; 
 603                 path_prf
="$item.ots";                 
 604                 files_to_upgrade
+=("$(printf "%s
" "$path_prf")"); 
 605                 files_to_verify
+=("$(printf "%s
\n%s
" "$path_src" "$path_prf")"); 
 607                 #### Proof file is not adjacent to source file (item). 
 608                 #### Check if proof file is in subdir 
 609                 vbm 
"DEBUG:checking if proof file for source file (item) is in subdir:item:$item"; 
 610                 unset flag_proof_in_subdir
; 
 611                 dir_item
="$(dirname "$item")"; 
 612                 cand_prf_filename
="$(basename "$item")".ots
; 
 613                 while read -r line
; do 
 614                     line_basename
="$(basename "$line")"; 
 615                     if [[ $line_basename == "$cand_prf_filename" ]]; then 
 616                         flag_proof_in_subdir
="true"; 
 618                         vbm 
"DEBUG:proof found in subdir at:line:$line"; 
 621                 done < <(find "$dir_item" -mindepth 2 -maxdepth 2 -type f
) 
 622                 if [[ $flag_proof_in_subdir == "true" ]]; then 
 623                     ##### Proof file is in subdir 
 624                     vbm 
"DEBUG:Proof file detected in subdir relative to source file (item)"; 
 625                     #path_prf="$path_prf"; # set in while loop 
 627                     files_to_upgrade
+=("$(printf "%s
" "$path_prf")"); 
 628                     files_to_verify
+=("$(printf "%s
\n%s
" "$path_src" "$path_prf")"); 
 630                     ##### Proof file is not in subdir 
 631                     vbm 
"DEBUG:Proof file not detected in subdir relative to source file (item)."; 
 632                     #### Stamp source file 
 633                     vbm 
"DEBUG:Marking source file to be stamped."; 
 635                     files_to_stamp
+=("$(printf "%s
" "$path_src")") 
 637                 unset flag_proof_in_subdir
; 
 641     unset path_src path_prf dir_item dir_parent cand_prf_filename cand_src_filename line_basename cand_src_path
 
 643     # Prune action lists. 
 644     ## Sort and prune file action arrays 
 646     while read -r -d $
'\0' line
; do 
 647         vbm 
"DEBUG:adding to files_to_upgrade_pruned:line:$line"; 
 648         files_to_upgrade_pruned
+=("$line"); 
 649     done < <(printf "%s\0" "${files_to_upgrade[@]}" | 
sort -zu | shuf 
-z); # See [1] 
 650     if [[ $opVerbose == "true" ]]; then 
 651         vbm 
"DEBUG:files_to_upgrade_pruned:"; 
 652         printf "%s\n" "${files_to_upgrade_pruned[@]}"; 
 656     while read -r -d $
'\0' line
; do 
 657         vbm 
"DEBUG:adding to files_to_verify_pruned:line:$line"; 
 658         files_to_verify_pruned
+=("$line"); 
 659     done < <(printf "%s\0" "${files_to_verify[@]}" | 
sort -zu | shuf 
-z); # See [1] 
 660     if [[ $opVerbose == "true" ]]; then 
 661         vbm 
"DEBUG:files_to_verify_pruned:"; 
 662         printf "%s\n\n" "${files_to_verify_pruned[@]}"; 
 666     while read -r -d $
'\0' line
; do 
 667         vbm 
"DEBUG:adding to files_to_stamp_pruned:line:$line"; 
 668         files_to_stamp_pruned
+=("$line"); 
 669     done < <(printf "%s\0" "${files_to_stamp[@]}" | 
sort -zu | shuf 
-z); # See [1] 
 670     if [[ $opVerbose == "true" ]]; then 
 671         vbm 
"DEBUG:files_to_stamp_pruned:"; 
 672         printf "%s\n" "${files_to_stamp_pruned[@]}"; 
 676     ## Assemble and execute upgrade file commands 
 677     for item 
in "${files_to_upgrade_pruned[@]}"; do 
 678         wait_for_jobslot 
&& { 
 679             path_prf
="$(cut -d $'\n' -f1 < <(echo "$item"))"; 
 680             if [[ -z "$path_prf" ]]; then 
 681                 yell 
"ERROR:blank upgrade item encountered. Skipping:item:$item"; 
 682                 return 1; # would have been `continue` were it not in a subshell 
 684             vbm 
"DEBUG:Attempting to upgrade proof file:path_prf:$path_prf"; 
 685             if [[ ! $option_dry_run == "true" ]]; then 
 686                 ### Try upgrade with known calendars in random order 
 687                 while read -r url
; do 
 688                     vbm 
"DEBUG:Upgrading with calendar:url:$url"; 
 690                     #### assemble command 
 693                     if [[ "$opVerbose" = "true" ]]; then cmd_temp
+=("-v"); fi; 
 694                     cmd_temp
+=("-l" "$url" "--no-default-whitelist"); 
 695                     cmd_temp
+=("upgrade" "$path_prf"); 
 696                     if [[ "$opVerbose" = "true" ]]; then declare -p cmd_temp
; fi; 
 702                     #ots -l "$url" --no-default-whitelist upgrade "$path_prf" && break; 
 703                 done < <(printf "%s\n" "${calendars[@]}" | shuf
); 
 705                 yell 
"DEBUG:DRY RUN:Not running:\"ots upgrade $path_prf\""; 
 710     ## Assemble and execute verify file commands 
 711     for item 
in "${files_to_verify_pruned[@]}"; do 
 712         wait_for_jobslot 
&& { 
 713             path_src
="$(cut -d $'\n' -f1 < <(echo "$item"))"; 
 714             path_prf
="$(cut -d $'\n' -f2 < <(echo "$item"))"; 
 715             if [[ -z "$path_src" ]] || 
[[ -z "$path_prf" ]]; then 
 716                 yell 
"ERROR:blank verify item encountered. Skipping:item:$item"; 
 717                 return 1; # would have been `continue` were it not in a subshell 
 719             vbm 
"DEBUG:Attempting to verify source file:path_src:$path_src"; 
 720             vbm 
"DEBUG:    against proof file:          path_prf:$path_prf"; 
 721             if [[ ! $option_dry_run == "true" ]]; then 
 722                 ### Try verify with known calendars in random order 
 723                 while read -r url
; do 
 724                     vbm 
"DEBUG:Verifying with calendar:url:$url"; 
 726                     #### assemble command 
 729                     if [[ "$opVerbose" = "true" ]]; then cmd_temp
+=("-v"); fi; 
 730                     cmd_temp
+=("-l" "$url" "--no-default-whitelist"); 
 731                     cmd_temp
+=("verify" "-f" "$path_src" "$path_prf"); 
 732                     if [[ "$opVerbose" = "true" ]]; then declare -p cmd_temp
; fi; 
 738                     #ots -l "$url" --no-default-whitelist verify -f "$path_src" "$path_prf" && break; 
 739                 done < <(printf "%s\n" "${calendars[@]}" | shuf
); 
 741                 yell 
"DEBUG:DRY RUN:Not running:\"ots verify -f $path_src $path_prf\""; 
 746     ## Assemble and execute stamp file commands 
 747     for item 
in "${files_to_stamp_pruned[@]}"; do 
 748         wait_for_jobslot 
&& { 
 749             path_src
="$(cut -d $'\n' -f1 < <(echo "$item"))"; 
 750             if [[ -z "$path_src" ]]; then 
 751                 yell 
"ERROR:blank stamp item encountered. Skipping:item:$item"; 
 752                 return 1; # would have been `continue` were it not in a subshell 
 754             vbm 
"DEBUG:Attempting to stamp source file:path_src:$path_src"; 
 755             if [[ ! $option_dry_run == "true" ]]; then 
 757                 #### assemble command 
 760                 if [[ "$opVerbose" = "true" ]]; then cmd_temp
+=("-v"); fi; 
 761                 cmd_temp
+=("stamp" "$path_src"); 
 762                 if [[ "$opVerbose" = "true" ]]; then declare -p cmd_temp
; fi; 
 765                 if "${cmd_temp[@]}"; then 
 766                     sleep "$stamp_throttle" || die 
"FATAL:Invalid stamp throttle."; 
 768                     yell 
"ERROR:Could not stamp file with command:$(declare -p cmd_temp)"; 
 769                     sleep "$stamp_throttle_fail" || die 
"FATAL:Invalid stamp throttle."; 
 773                 #ots stamp "$path_src"; 
 775                 yell 
"DEBUG:DRY RUN:Not running:\"ots stamp $path_src\""; 
 780     ## Wait for jobs to finish.