4 declare -Ag appRollCall
# Associative array for storing app status
5 declare -Ag fileRollCall
# Associative array for storing file status
6 declare -Ag dirRollCall
# Associative array for storing dir status
7 declare -ag arrayPosArgs
# Associative array for processArgs() function
8 declare -g ots_delay
; ots_delay
=1 # minimum time in seconds between ots operations
10 calendars
+=("https://finney.calendar.eternitywall.com");
11 calendars
+=("https://btc.calendar.catallaxy.com");
12 calendars
+=("https://alice.btc.calendar.opentimestamps.org");
13 calendars
+=("https://bob.btc.calendar.opentimestamps.org");
16 yell
() { echo "$0: $*" >&2; } # print script path and all args to stderr
17 die
() { yell
"$*"; exit 111; } # same as yell() but non-zero exit status
18 try
() { "$@" || die
"cannot $*"; } # runs args as command, reports args if command fails
20 # Desc: If arg is a command, save result in assoc array 'appRollCall'
21 # Usage: checkapp arg1 arg2 arg3 ...
23 # Input: global assoc. array 'appRollCall'
24 # Output: adds/updates key(value) to global assoc array 'appRollCall'
30 if command -v "$arg" 1>/dev
/null
2>&1; then # Check if arg is a valid command
31 appRollCall
[$arg]="true";
32 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi;
34 appRollCall
[$arg]="false"; returnState
="false";
38 #===Determine function return code===
39 if [ "$returnState" = "true" ]; then
44 } # Check that app exists
46 # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
47 # Usage: checkfile arg1 arg2 arg3 ...
49 # Input: global assoc. array 'fileRollCall'
50 # Output: adds/updates key(value) to global assoc array 'fileRollCall';
51 # Output: returns 0 if app found, 1 otherwise
57 if [ -f "$arg" ]; then
58 fileRollCall
["$arg"]="true";
59 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi;
61 fileRollCall
["$arg"]="false"; returnState
="false";
65 #===Determine function return code===
66 if [ "$returnState" = "true" ]; then
71 } # Check that file exists
73 # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
74 # Usage: checkdir arg1 arg2 arg3 ...
76 # Input: global assoc. array 'dirRollCall'
77 # Output: adds/updates key(value) to global assoc array 'dirRollCall';
78 # Output: returns 0 if all args are dirs; 1 otherwise
84 if [ -z "$arg" ]; then
85 dirRollCall
["(Unspecified Dirname(s))"]="false"; returnState
="false";
86 elif [ -d "$arg" ]; then
87 dirRollCall
["$arg"]="true";
88 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi
90 dirRollCall
["$arg"]="false"; returnState
="false";
94 #===Determine function return code===
95 if [ "$returnState" = "true" ]; then
100 } # Check that dir exists
102 # Desc: Displays missing apps, files, and dirs
103 # Usage: displayMissing
105 # Input: associative arrays: appRollCall, fileRollCall, dirRollCall
106 # Output: stderr: messages indicating missing apps, file, or dirs
107 # Output: returns exit code 0 if nothing missing; 1 otherwise
108 # Depends: bash 5, checkAppFileDir()
109 local missingApps value appMissing missingFiles fileMissing
110 local missingDirs dirMissing
112 #==BEGIN Display errors==
113 #===BEGIN Display Missing Apps===
114 missingApps
="Missing apps :";
115 #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done
116 for key
in "${!appRollCall[@]}"; do
117 value
="${appRollCall[$key]}";
118 if [ "$value" = "false" ]; then
119 #echo "DEBUG:Missing apps: $key => $value";
120 missingApps
="$missingApps""$key ";
124 if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing.
125 echo "$missingApps" 1>&2;
128 #===END Display Missing Apps===
130 #===BEGIN Display Missing Files===
131 missingFiles
="Missing files:";
132 #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done
133 for key
in "${!fileRollCall[@]}"; do
134 value
="${fileRollCall[$key]}";
135 if [ "$value" = "false" ]; then
136 #echo "DEBUG:Missing files: $key => $value";
137 missingFiles
="$missingFiles""$key ";
141 if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing.
142 echo "$missingFiles" 1>&2;
145 #===END Display Missing Files===
147 #===BEGIN Display Missing Directories===
148 missingDirs
="Missing dirs:";
149 #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done
150 for key
in "${!dirRollCall[@]}"; do
151 value
="${dirRollCall[$key]}";
152 if [ "$value" = "false" ]; then
153 #echo "DEBUG:Missing dirs: $key => $value";
154 missingDirs
="$missingDirs""$key ";
158 if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing.
159 echo "$missingDirs" 1>&2;
162 #===END Display Missing Directories===
164 #==END Display errors==
165 #==BEGIN Determine function return code===
166 if [ "$appMissing" == "true" ] ||
[ "$fileMissing" == "true" ] ||
[ "$dirMissing" == "true" ]; then
171 #==END Determine function return code===
172 } # Display missing apps, files, dirs
174 # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true".
175 # Usage: vbm "DEBUG :verbose message here"
177 # Input: arg1: string
180 # Depends: bash 5.0.3, GNU-coreutils 8.30 (echo, date)
182 if [ "$opVerbose" = "true" ]; then
183 functionTime
="$(date --iso-8601=ns)"; # Save current time in nano seconds.
184 echo "[$functionTime]:$0:""$*" 1>&2; # Display argument text.
188 return 0; # Function finished.
189 } # Displays message if opVerbose true
191 # Desc: Displays script version and license information.
194 # Input: scriptVersion var containing version string
196 # Depends: vbm(), yell, GNU-coreutils 8.30
198 # Initialize function
199 vbm
"DEBUG:showVersion function called."
203 Copyright (C) 2022 Steven Baltakatei Sandoval
204 License GPLv3: GNU GPL version 3
205 This is free software; you are free to change and redistribute it.
206 There is NO WARRANTY, to the extent permitted by law.
209 Copyright (C) 2020 Free Software Foundation, Inc.
210 License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
211 This is free software: you are free to change and redistribute it.
212 There is NO WARRANTY, to the extent permitted by law.
216 vbm
"DEBUG:showVersion function ended."
217 return 0; # Function finished.
218 } # Display script version.
220 # Desc: Display script usage information
225 # Depends: GNU-coreutils 8.30 (cat)
228 bkots [ options ] [PATH...]
230 POSITIONAL ARGUMENTS:
231 PATH Path(s) of file(s) or directory(ies)
235 Do everything except run 'ots' commands.
237 Display help information.
239 Include files and directories starting with '.' (not
240 included by default).
242 Consider files in dirs recursively.
244 Display script version.
246 Display debugging info.
248 Mark end of options. Interpret remaining arguments as
249 positional arguments.
252 Scans files by file paths or directory paths provided by
253 positional arguments to see if Open Timestamps '.ots' file
254 exists. If so, attempt to upgrade and verify the '.ots'
255 file. If no '.ots' file exists, attempt to create one.
257 Files with a dotfile parent directory located anywhere in the
258 file path are ignored by default. (e.g. 'HEAD' in
259 '/home/user/diary/.git/logs/HEAD' because of '.git'). Dotfiles
260 themselves are also ignored by default
261 (e.g. '/home/user/.gitconfig').
265 bkots foo.txt bar.pdf /home/username/Pictures/
267 } # Display information on how to use this script.
269 # Desc: Processes arguments provided to script.
270 # Usage: processArgs "$@"
272 # Input: "$@" (list of arguments provided to the function)
273 # Output: Sets following variables used by other functions:
274 # opVerbose Indicates verbose mode enable status. (ex: "true", "false")
275 # arrayPosArgs Array of remaining positional argments
277 # yell() Displays messages to stderr.
278 # vbm() Displays messsages to stderr if opVerbose set to "true".
279 # showUsage() Displays usage information about parent script.
280 # showVersion() Displays version about parent script.
281 # arrayPosArgs Global array for storing non-option positional arguments (i.e. arguments following the `--` option).
282 # External dependencies: bash (5.1.16), echo
284 # [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347
285 # [2]: "Handling positional parameters" (2018-05-12). https://wiki.bash-hackers.org/scripting/posparams
287 # Initialize function
288 vbm
"DEBUG:processArgs function called."
291 while [ ! $# -eq 0 ]; do # While number of arguments ($#) is not (!) equal to (-eq) zero (0).
292 #yell "DEBUG:Starting processArgs while loop." # Debug stderr message. See [1].
293 #yell "DEBUG:Provided arguments are:""$*" # Debug stderr message. See [1].
295 --dry-run) # Do not run ots commands
296 option_dry_run
="true";
297 vbm
"DEBUG:Option enabled:dry run";;
298 -h |
--help) showUsage
; exit 1;; # Display usage.
299 --include-dotfiles) # Include dotfiles
300 option_include_dotfiles
="true";
301 vbm
"DEBUG:Option enabled:include dotfiles";;
302 -r |
--recursive) # Specify recursive option
303 option_recursive
="true";
304 vbm
"DEBUG:option enabled:include files in dirs recursively";;
305 --version) showVersion
; exit 1;; # Show version
306 -v |
--verbose) opVerbose
="true"; vbm
"DEBUG:Verbose mode enabled.";; # Enable verbose mode. See [1].
307 --) # End of all options. See [2].
310 vbm
"DEBUG:adding to arrayPosArgs:$arg";
311 arrayPosArgs
+=("$arg");
314 -*) showUsage
; yell
"ERROR: Unrecognized option."; exit 1;; # Display usage
315 *) # Assume remaining arguments are positional arguments
317 vbm
"DEBUG:adding to arrayPosArgs:$arg";
318 arrayPosArgs
+=("$arg");
321 #*) showUsage; yell "ERROR: Unrecognized argument."; exit 1;; # Handle unrecognized options. See [1].
327 vbm
"DEBUG:processArgs function ended."
328 return 0; # Function finished.
329 }; # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.).
330 get_parent_dirnames
() {
331 # Desc: Provides newline-delimited list of each parent dir of a file or dir
332 # Usage: get_parent_dirnames arg1
333 # Input: arg1 input path
334 # Output: stdout newline-delimited list of parent dirs
336 # Depends: yell(), die(), try()
340 if [[ $# -ne 1 ]]; then die
"FATAL:Incorrect number of arguments:$#"; fi;
341 if ! { [[ -f $1 ]] ||
[[ -d $1 ]]; }; then die
"FATAL:Not a file or dir:$1"; fi;
345 while [[ -f $path ]] ||
[[ -d $path ]]; do
346 path
="$(dirname "$path")";
347 name_base_previous
="$name_base";
348 name_base
="$(basename "$path")";
349 ## Check for stop condition (dirname returns same result as previous iteration)
350 if [[ $name_base == "$name_base_previous" ]]; then break; fi;
353 }; # Output parent dirnames to stdout
355 # Desc: Creates `.ots` file:
356 # - for each file specified in arrayPosArgs array
357 # - for each file in each dir specified in arrayPosArgs array
358 # Output file created alongside each file or in output directory specified by pathDirIn1
360 # Input: arrayPosArgs array with positional arguments
361 # pathDirOut1 path for output `.ots` files (if pathDirOut1 is specified and is a path)
362 # Output: file(s) creates `.ots` file alongside specified files
363 # Depends: find (GNU findutils) 4.8.0, GNU Coreutils 8.32 (sort)
364 # Ref/Attrib: [1] How to create an array of unique elements from a string/array in bash https://unix.stackexchange.com/a/167194
365 # [2] How to find files containing newlines in their names https://stackoverflow.com/a/21727028
366 local -a file_list file_list_pruned
;
367 local -a files_to_verify files_to_upgrade files_to_stamp
368 local -a files_to_verify_pruned files_to_upgrade_pruned files_to_stamp_pruned
374 if ! checkapp ots
find; then
376 die
"FATAL:Missing dependencies.";
380 ## Mark if output dir option specified
381 if [[ -v pathDirOut1
]]; then
382 vbm
"DEBUG:output directory specified:pathDirOut1:$pathDirOut1";
383 if [[ -d $pathDirOut1 ]]; then
384 vbm
"DEBUG:pathDirOut1:$pathDirOut1";
385 config_output_dir
="true";
387 die
"ERROR:Not a dir:$pathDirOut1";
391 # Display ots details
392 vbm
"$(type ots)"; # show how 'ots' is defined
393 #TODO: add option to define 'ots' as a bash function that
394 #populates the ots option '--bitcoin-node FILE' with a
395 #user-specified FILE.
398 vbm
"DEBUG:begin populate file_list array";
399 for item
in "${arrayPosArgs[@]}"; do
400 vbm
"DEBUG:adding to file list:item:$item";
402 ## Get full canonicalized path (follow symlinks)
403 item
="$(readlink -f "$item")";
404 vbm
"DEBUG:item full path:item:$item";
406 ## Add to list: files
407 if [[ -f $item ]]; then
408 vbm
"DEBUG:is a file:item:$item";
409 file_list
+=("$item");
410 vbm
"DEBUG:added to file_list:$item";
411 ## Add to list: files in dirs
412 elif [[ -d $item ]]; then
413 vbm
"DEBUG:is a dir:item:$item";
414 ### Check for recursive flag
415 if [[ "$option_recursive" == "true" ]]; then
416 vbm
"DEBUG:option_recursive:$option_recursive";
417 while read -r line
; do
418 file_list
+=("$line");
419 vbm
"DEBUG:added to file_list:$line";
420 done < <(find "$item" -type f
);
422 while read -r line
; do
423 file_list
+=("$line");
424 vbm
"DEBUG:added to file_list:$line";
425 done < <(find "$item" -maxdepth 1 -type f
);
428 die
"ERROR:Not a file or dir:item:$item";
431 if [[ $opVerbose == "true" ]]; then
432 vbm
"DEBUG:file_list:";
433 printf "%s\n" "${file_list[@]}";
437 for item
in "${file_list[@]}"; do
438 if ! [[ $option_include_dotfiles == "true" ]]; then
439 ## Ignore files located beneath a dotfile directory (e.g. '/home/my_repo/.git/config')
440 unset flag_contains_dotfile_parent
;
441 while read -r line
; do
442 ### Check line from output of get_parent_dirnames
444 if [[ $line =~
$pattern ]]; then
445 #### line starts with '.'
446 vbm
"DEBUG:Dotfile parent detected. Not including in file_list_pruned:$item";
447 vbm
"DEBUG:Dotfile in path:item:$item";
448 vbm
"DEBUG:Dotfile parent:line:$line";
449 flag_contains_dotfile_parent
="true";
452 done < <(get_parent_dirnames
"$item");
453 if [[ $flag_contains_dotfile_parent == "true" ]]; then
454 unset flag_contains_dotfile_parent
;
455 continue; # skip to next item (i.e. don't add to file_list_pruned)
458 ## Ignore dotfiles themselves
459 item_basename
="$(basename "$item")";
461 if [[ $item_basename =~
$pattern ]]; then
462 vbm
"INFO :Skipping dotfile:item:$item";
463 continue; # skip to next item
467 ## Ignore files with newlines present in filename. See [2].
468 if [[ $item =~ $
'\n' ]]; then
469 yell
"INFO :Skipping file name with newline:$item";
470 continue; # skip to next item
473 ## Ignore files that end in '~'.
474 if [[ $item =~ ~$
]]; then
475 yell
"INFO :Skipping file ending in tilde:$item";
476 continue; # skip to next item
479 ## Add item to file_list_pruned
480 file_list_pruned
+=("$item");
482 if [[ $opVerbose == "true" ]]; then
483 vbm
"DEBUG:file_list_pruned:";
484 printf "%s\n" "${file_list_pruned[@]}";
487 # Decide what actions to take for items in file_list_pruned
488 for item
in "${file_list_pruned[@]}"; do
489 vbm
"DEBUG:considering action to take for item:$item";
490 unset path_src path_prf dir_parent dir_source
;
492 ## Check file extension
493 if [[ $item =~ .ots$
]]; then
494 ### item ends in '.ots'. Item is proof file.
495 vbm
"DEBUG:item ends in '.ots'. Item is proof file:item:$item";
496 if [[ -f ${item%.ots} ]]; then
497 #### Proof file (item) is adjacent to source file
498 vbm
"DEBUG:Proof file (item) is adjacent to source file.";
499 ##### Upgrade and verify proof file against adjacent source file
500 vbm
"DEBUG:Marking proof file to be upgraded and verified.";
501 path_src
="${item%.ots}";
503 files_to_upgrade
+=("$(printf "%s
" "$path_prf")");
504 files_to_verify
+=("$(printf "%s
\n%s
" "$path_src" "$path_prf")");
506 #### Proof file (item) is not adjacent to source file
507 vbm
"DEBUG:Proof file (item) is not adjacent to source file.";
508 #### Check if source file in parent dir
509 dir_parent
="$(dirname "$
(dirname "$item")" )";
510 cand_src_filename
="$(basename "$item")";
511 cand_src_path
="$dir_parent/$cand_src_filename";
512 if [[ -f "$cand_src_path" ]]; then
513 ##### source file in parent dir
514 vbm
"DEBUG:found source file in parent:cand_src_path:$cand_src_path";
515 path_src
="$cand_src_path";
517 files_to_upgrade
+=("$(printf "%s
" "$path_prf")");
518 files_to_verify
+=("$(printf "%s
\n%s
" "$path_src" "$path_prf")");
520 #### Throw non-fatal error
521 vbm
"DEBUG:Source file not found for proof file:item:$item";
522 yell
"ERROR:Item is proof file but source filei not adjacent in parent dir. item:$item";
523 #### Attempt upgrade only
524 vbm
"DEBUG:Marking proof file to be upgraded.";
526 files_to_upgrade
+=("$(printf "%s
" "$path_prf")");
530 ### item does not end in '.ots'. Item is source file.
531 vbm
"DEBUG:item does NOT end in '.ots'. Item is source file.";
532 if [[ -f "$item".ots
]]; then
533 #### Proof file is adjacent to source file (item).
534 vbm
"DEBUG:Proof file is adjacent to source file (item).";
535 ##### Upgrade and verify proof file against adjacent source file.
536 vbm
"DEBUG:Marking proof file to be upgraded and verified.";
538 path_prf
="$item.ots";
539 files_to_upgrade
+=("$(printf "%s
" "$path_prf")");
540 files_to_verify
+=("$(printf "%s
\n%s
" "$path_src" "$path_prf")");
542 #### Proof file is not adjacent to source file (item).
543 #### Check if proof file is in subdir
544 vbm
"DEBUG:checking if proof file for source file (item) is in subdir:item:$item";
545 unset flag_proof_in_subdir
;
546 dir_item
="$(dirname "$item")";
547 cand_prf_filename
="$(basename "$item")".ots
;
548 while read -r line
; do
549 line_basename
="$(basename "$line")";
550 if [[ $line_basename == "$cand_prf_filename" ]]; then
551 flag_proof_in_subdir
="true";
553 vbm
"DEBUG:proof found in subdir at:line:$line";
556 done < <(find "$dir_item" -mindepth 2 -maxdepth 2 -type f
)
557 if [[ $flag_proof_in_subdir == "true" ]]; then
558 ##### Proof file is in subdir
559 vbm
"DEBUG:Proof file detected in subdir relative to source file (item)";
560 #path_prf="$path_prf"; # set in while loop
562 files_to_upgrade
+=("$(printf "%s
" "$path_prf")");
563 files_to_verify
+=("$(printf "%s
\n%s
" "$path_src" "$path_prf")");
565 ##### Proof file is not in subdir
566 vbm
"DEBUG:Proof file not detected in subdir relative to source file (item).";
567 #### Stamp source file
568 vbm
"DEBUG:Marking source file to be stamped.";
570 files_to_stamp
+=("$(printf "%s
" "$path_src")")
572 unset flag_proof_in_subdir
;
576 unset path_src path_prf dir_item dir_parent cand_prf_filename cand_src_filename line_basename cand_src_path
578 # Prune action lists.
579 ## Sort and prune file action arrays
581 while read -r -d $
'\0' line
; do
582 vbm
"DEBUG:adding to files_to_upgrade_pruned:line:$line";
583 files_to_upgrade_pruned
+=("$line");
584 done < <(printf "%s\0" "${files_to_upgrade[@]}" |
sort -zu | shuf
-z); # See [1]
585 if [[ $opVerbose == "true" ]]; then
586 vbm
"DEBUG:files_to_upgrade_pruned:";
587 printf "%s\n" "${files_to_upgrade_pruned[@]}";
591 while read -r -d $
'\0' line
; do
592 vbm
"DEBUG:adding to files_to_verify_pruned:line:$line";
593 files_to_verify_pruned
+=("$line");
594 done < <(printf "%s\0" "${files_to_verify[@]}" |
sort -zu | shuf
-z); # See [1]
595 if [[ $opVerbose == "true" ]]; then
596 vbm
"DEBUG:files_to_verify_pruned:";
597 printf "%s\n\n" "${files_to_verify_pruned[@]}";
601 while read -r -d $
'\0' line
; do
602 vbm
"DEBUG:adding to files_to_stamp_pruned:line:$line";
603 files_to_stamp_pruned
+=("$line");
604 done < <(printf "%s\0" "${files_to_stamp[@]}" |
sort -zu | shuf
-z); # See [1]
605 if [[ $opVerbose == "true" ]]; then
606 vbm
"DEBUG:files_to_stamp_pruned:";
607 printf "%s\n" "${files_to_stamp_pruned[@]}";
612 for item
in "${files_to_upgrade_pruned[@]}"; do
613 path_prf
="$(cut -d $'\n' -f1 < <(echo "$item"))";
614 if [[ -z "$path_prf" ]]; then
615 yell
"ERROR:blank upgrade item encountered. Skipping:item:$item";
618 vbm
"DEBUG:Attempting to upgrade proof file:path_prf:$path_prf";
619 if [[ ! $option_dry_run == "true" ]]; then
620 #ots upgrade "$path_prf";
621 for url
in "${calendars[@]}"; do
622 ots
-l "$url" --no-default-whitelist upgrade
"$path_prf";
625 yell
"DEBUG:DRY RUN:Not running:\"ots upgrade $path_prf\"";
631 for item
in "${files_to_verify_pruned[@]}"; do
632 path_src
="$(cut -d $'\n' -f1 < <(echo "$item"))";
633 path_prf
="$(cut -d $'\n' -f2 < <(echo "$item"))";
634 if [[ -z "$path_src" ]] ||
[[ -z "$path_prf" ]]; then
635 yell
"ERROR:blank verify item encountered. Skipping:item:$item";
638 vbm
"DEBUG:Attempting to verify source file:path_src:$path_src";
639 vbm
"DEBUG: against proof file: path_prf:$path_prf";
640 if [[ ! $option_dry_run == "true" ]]; then
641 #ots verify -f "$path_src" "$path_prf";
642 for url
in "${calendars[@]}"; do
643 ots
-l "$url" --no-default-whitelist verify
-f "$path_src" "$path_prf";
646 yell
"DEBUG:DRY RUN:Not running:\"ots verify -f $path_src $path_prf\"";
652 for item
in "${files_to_stamp_pruned[@]}"; do
653 path_src
="$(cut -d $'\n' -f1 < <(echo "$item"))";
654 if [[ -z "$path_src" ]]; then
655 yell
"ERROR:blank stamp item encountered. Skipping:item:$item";
658 vbm
"DEBUG:Attempting to stamp source file:path_src:$path_src";
659 if [[ ! $option_dry_run == "true" ]]; then
661 for url
in "${calendars[@]}"; do
662 ots
-l "$url" --no-default-whitelist verify stamp
"$item";
665 yell
"DEBUG:DRY RUN:Not running:\"ots stamp $item\"";