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
10 yell
() { echo "$0: $*" >&2; } # print script path and all args to stderr
11 die
() { yell
"$*"; exit 111; } # same as yell() but non-zero exit status
12 try
() { "$@" || die
"cannot $*"; } # runs args as command, reports args if command fails
14 # Desc: If arg is a command, save result in assoc array 'appRollCall'
15 # Usage: checkapp arg1 arg2 arg3 ...
17 # Input: global assoc. array 'appRollCall'
18 # Output: adds/updates key(value) to global assoc array 'appRollCall'
24 if command -v "$arg" 1>/dev
/null
2>&1; then # Check if arg is a valid command
25 appRollCall
[$arg]="true";
26 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi;
28 appRollCall
[$arg]="false"; returnState
="false";
32 #===Determine function return code===
33 if [ "$returnState" = "true" ]; then
38 } # Check that app exists
40 # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
41 # Usage: checkfile arg1 arg2 arg3 ...
43 # Input: global assoc. array 'fileRollCall'
44 # Output: adds/updates key(value) to global assoc array 'fileRollCall';
45 # Output: returns 0 if app found, 1 otherwise
51 if [ -f "$arg" ]; then
52 fileRollCall
["$arg"]="true";
53 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi;
55 fileRollCall
["$arg"]="false"; returnState
="false";
59 #===Determine function return code===
60 if [ "$returnState" = "true" ]; then
65 } # Check that file exists
67 # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
68 # Usage: checkdir arg1 arg2 arg3 ...
70 # Input: global assoc. array 'dirRollCall'
71 # Output: adds/updates key(value) to global assoc array 'dirRollCall';
72 # Output: returns 0 if all args are dirs; 1 otherwise
78 if [ -z "$arg" ]; then
79 dirRollCall
["(Unspecified Dirname(s))"]="false"; returnState
="false";
80 elif [ -d "$arg" ]; then
81 dirRollCall
["$arg"]="true";
82 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi
84 dirRollCall
["$arg"]="false"; returnState
="false";
88 #===Determine function return code===
89 if [ "$returnState" = "true" ]; then
94 } # Check that dir exists
96 # Desc: Displays missing apps, files, and dirs
97 # Usage: displayMissing
99 # Input: associative arrays: appRollCall, fileRollCall, dirRollCall
100 # Output: stderr: messages indicating missing apps, file, or dirs
101 # Output: returns exit code 0 if nothing missing; 1 otherwise
102 # Depends: bash 5, checkAppFileDir()
103 local missingApps value appMissing missingFiles fileMissing
104 local missingDirs dirMissing
106 #==BEGIN Display errors==
107 #===BEGIN Display Missing Apps===
108 missingApps
="Missing apps :";
109 #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done
110 for key
in "${!appRollCall[@]}"; do
111 value
="${appRollCall[$key]}";
112 if [ "$value" = "false" ]; then
113 #echo "DEBUG:Missing apps: $key => $value";
114 missingApps
="$missingApps""$key ";
118 if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing.
119 echo "$missingApps" 1>&2;
122 #===END Display Missing Apps===
124 #===BEGIN Display Missing Files===
125 missingFiles
="Missing files:";
126 #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done
127 for key
in "${!fileRollCall[@]}"; do
128 value
="${fileRollCall[$key]}";
129 if [ "$value" = "false" ]; then
130 #echo "DEBUG:Missing files: $key => $value";
131 missingFiles
="$missingFiles""$key ";
135 if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing.
136 echo "$missingFiles" 1>&2;
139 #===END Display Missing Files===
141 #===BEGIN Display Missing Directories===
142 missingDirs
="Missing dirs:";
143 #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done
144 for key
in "${!dirRollCall[@]}"; do
145 value
="${dirRollCall[$key]}";
146 if [ "$value" = "false" ]; then
147 #echo "DEBUG:Missing dirs: $key => $value";
148 missingDirs
="$missingDirs""$key ";
152 if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing.
153 echo "$missingDirs" 1>&2;
156 #===END Display Missing Directories===
158 #==END Display errors==
159 #==BEGIN Determine function return code===
160 if [ "$appMissing" == "true" ] ||
[ "$fileMissing" == "true" ] ||
[ "$dirMissing" == "true" ]; then
165 #==END Determine function return code===
166 } # Display missing apps, files, dirs
168 # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true".
169 # Usage: vbm "DEBUG :verbose message here"
171 # Input: arg1: string
174 # Depends: bash 5.0.3, GNU-coreutils 8.30 (echo, date)
176 if [ "$opVerbose" = "true" ]; then
177 functionTime
="$(date --iso-8601=ns)"; # Save current time in nano seconds.
178 echo "[$functionTime]:$0:""$*" 1>&2; # Display argument text.
182 return 0; # Function finished.
183 } # Displays message if opVerbose true
185 # Desc: Displays script version and license information.
188 # Input: scriptVersion var containing version string
190 # Depends: vbm(), yell, GNU-coreutils 8.30
192 # Initialize function
193 vbm
"DEBUG:showVersion function called."
197 Copyright (C) 2022 Steven Baltakatei Sandoval
198 License GPLv3: GNU GPL version 3
199 This is free software; you are free to change and redistribute it.
200 There is NO WARRANTY, to the extent permitted by law.
203 Copyright (C) 2020 Free Software Foundation, Inc.
204 License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
205 This is free software: you are free to change and redistribute it.
206 There is NO WARRANTY, to the extent permitted by law.
210 vbm
"DEBUG:showVersion function ended."
211 return 0; # Function finished.
212 } # Display script version.
214 # Desc: Display script usage information
219 # Depends: GNU-coreutils 8.30 (cat)
222 bkots [ options ] [PATH...]
224 POSITIONAL ARGUMENTS:
225 PATH Path(s) of file(s) or directory(ies)
229 Do everything except run 'ots' commands.
231 Display help information.
233 Include files and directories starting with '.' (not
234 included by default).
236 Consider files in dirs recursively.
238 Display script version.
240 Display debugging info.
242 Mark end of options. Interpret remaining arguments as
243 positional arguments.
246 Scans files by file paths or directory paths provided by
247 positional arguments to see if Open Timestamps '.ots' file
248 exists. If so, attempt to upgrade and verify the '.ots'
249 file. If no '.ots' file exists, attempt to create one.
251 Files with a dotfile parent directory located anywhere in the
252 file path are ignored by default. (e.g. 'HEAD' in
253 '/home/user/diary/.git/logs/HEAD' because of '.git'). Dotfiles
254 themselves are also ignored by default
255 (e.g. '/home/user/.gitconfig').
259 bkots foo.txt bar.pdf /home/username/Pictures/
261 } # Display information on how to use this script.
263 # Desc: Processes arguments provided to script.
264 # Usage: processArgs "$@"
266 # Input: "$@" (list of arguments provided to the function)
267 # Output: Sets following variables used by other functions:
268 # opVerbose Indicates verbose mode enable status. (ex: "true", "false")
269 # arrayPosArgs Array of remaining positional argments
271 # yell() Displays messages to stderr.
272 # vbm() Displays messsages to stderr if opVerbose set to "true".
273 # showUsage() Displays usage information about parent script.
274 # showVersion() Displays version about parent script.
275 # arrayPosArgs Global array for storing non-option positional arguments (i.e. arguments following the `--` option).
276 # External dependencies: bash (5.1.16), echo
278 # [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347
279 # [2]: "Handling positional parameters" (2018-05-12). https://wiki.bash-hackers.org/scripting/posparams
281 # Initialize function
282 vbm
"DEBUG:processArgs function called."
285 while [ ! $# -eq 0 ]; do # While number of arguments ($#) is not (!) equal to (-eq) zero (0).
286 #yell "DEBUG:Starting processArgs while loop." # Debug stderr message. See [1].
287 #yell "DEBUG:Provided arguments are:""$*" # Debug stderr message. See [1].
289 --dry-run) # Do not run ots commands
290 option_dry_run
="true";
291 vbm
"DEBUG:Option enabled:dry run";;
292 -h |
--help) showUsage
; exit 1;; # Display usage.
293 --include-dotfiles) # Include dotfiles
294 option_include_dotfiles
="true";
295 vbm
"DEBUG:Option enabled:include dotfiles";;
296 -r |
--recursive) # Specify recursive option
297 option_recursive
="true";
298 vbm
"DEBUG:option enabled:include files in dirs recursively";;
299 --version) showVersion
; exit 1;; # Show version
300 -v |
--verbose) opVerbose
="true"; vbm
"DEBUG:Verbose mode enabled.";; # Enable verbose mode. See [1].
301 --) # End of all options. See [2].
304 vbm
"DEBUG:adding to arrayPosArgs:$arg";
305 arrayPosArgs
+=("$arg");
308 -*) showUsage
; yell
"ERROR: Unrecognized option."; exit 1;; # Display usage
309 *) # Assume remaining arguments are positional arguments
311 vbm
"DEBUG:adding to arrayPosArgs:$arg";
312 arrayPosArgs
+=("$arg");
315 #*) showUsage; yell "ERROR: Unrecognized argument."; exit 1;; # Handle unrecognized options. See [1].
321 vbm
"DEBUG:processArgs function ended."
322 return 0; # Function finished.
323 }; # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.).
324 get_parent_dirnames
() {
325 # Desc: Provides newline-delimited list of each parent dir of a file or dir
326 # Usage: get_parent_dirnames arg1
327 # Input: arg1 input path
328 # Output: stdout newline-delimited list of parent dirs
330 # Depends: yell(), die(), try()
334 if [[ $# -ne 1 ]]; then die
"FATAL:Incorrect number of arguments:$#"; fi;
335 if ! { [[ -f $1 ]] ||
[[ -d $1 ]]; }; then die
"FATAL:Not a file or dir:$1"; fi;
339 while [[ -f $path ]] ||
[[ -d $path ]]; do
340 path
="$(dirname "$path")";
341 name_base_previous
="$name_base";
342 name_base
="$(basename "$path")";
343 ## Check for stop condition (dirname returns same result as previous iteration)
344 if [[ $name_base == "$name_base_previous" ]]; then break; fi;
347 }; # Output parent dirnames to stdout
349 # Desc: Creates `.ots` file:
350 # - for each file specified in arrayPosArgs array
351 # - for each file in each dir specified in arrayPosArgs array
352 # Output file created alongside each file or in output directory specified by pathDirIn1
354 # Input: arrayPosArgs array with positional arguments
355 # pathDirOut1 path for output `.ots` files (if pathDirOut1 is specified and is a path)
356 # Output: file(s) creates `.ots` file alongside specified files
357 # Depends: find (GNU findutils) 4.8.0, GNU Coreutils 8.32 (sort)
358 # Ref/Attrib: [1] How to create an array of unique elements from a string/array in bash https://unix.stackexchange.com/a/167194
359 # [2] How to find files containing newlines in their names https://stackoverflow.com/a/21727028
360 local -a file_list file_list_pruned
;
361 local -a files_to_verify files_to_upgrade files_to_stamp
362 local -a files_to_verify_pruned files_to_upgrade_pruned files_to_stamp_pruned
368 if ! checkapp ots
find; then
370 die
"FATAL:Missing dependencies.";
374 ## Mark if output dir option specified
375 if [[ -v pathDirOut1
]]; then
376 vbm
"DEBUG:output directory specified:pathDirOut1:$pathDirOut1";
377 if [[ -d $pathDirOut1 ]]; then
378 vbm
"DEBUG:pathDirOut1:$pathDirOut1";
379 config_output_dir
="true";
381 die
"ERROR:Not a dir:$pathDirOut1";
385 # Display ots details
386 vbm
"$(type ots)"; # show how 'ots' is defined
387 #TODO: add option to define 'ots' as a bash function that
388 #populates the ots option '--bitcoin-node FILE' with a
389 #user-specified FILE.
392 vbm
"DEBUG:begin populate file_list array";
393 for item
in "${arrayPosArgs[@]}"; do
394 vbm
"DEBUG:adding to file list:item:$item";
396 ## Get full canonicalized path (follow symlinks)
397 item
="$(readlink -f "$item")";
398 vbm
"DEBUG:item full path:item:$item";
400 ## Add to list: files
401 if [[ -f $item ]]; then
402 vbm
"DEBUG:is a file:item:$item";
403 file_list
+=("$item");
404 vbm
"DEBUG:added to file_list:$item";
405 ## Add to list: files in dirs
406 elif [[ -d $item ]]; then
407 vbm
"DEBUG:is a dir:item:$item";
408 ### Check for recursive flag
409 if [[ "$option_recursive" == "true" ]]; then
410 vbm
"DEBUG:option_recursive:$option_recursive";
411 while read -r line
; do
412 file_list
+=("$line");
413 vbm
"DEBUG:added to file_list:$line";
414 done < <(find "$item" -type f
);
416 while read -r line
; do
417 file_list
+=("$line");
418 vbm
"DEBUG:added to file_list:$line";
419 done < <(find "$item" -maxdepth 1 -type f
);
422 die
"ERROR:Not a file or dir:item:$item";
425 if [[ $opVerbose == "true" ]]; then
426 vbm
"DEBUG:file_list:";
427 printf "%s\n" "${file_list[@]}";
431 for item
in "${file_list[@]}"; do
432 if ! [[ $option_include_dotfiles == "true" ]]; then
433 ## Ignore files located beneath a dotfile directory (e.g. '/home/my_repo/.git/config')
434 unset flag_contains_dotfile_parent
;
435 while read -r line
; do
436 ### Check line from output of get_parent_dirnames
438 if [[ $line =~
$pattern ]]; then
439 #### line starts with '.'
440 vbm
"DEBUG:Dotfile parent detected. Not including in file_list_pruned:$item";
441 vbm
"DEBUG:Dotfile in path:item:$item";
442 vbm
"DEBUG:Dotfile parent:line:$line";
443 flag_contains_dotfile_parent
="true";
446 done < <(get_parent_dirnames
"$item");
447 if [[ $flag_contains_dotfile_parent == "true" ]]; then
448 unset flag_contains_dotfile_parent
;
449 continue; # skip to next item (i.e. don't add to file_list_pruned)
452 ## Ignore dotfiles themselves
453 item_basename
="$(basename "$item")";
455 if [[ $item_basename =~
$pattern ]]; then
456 vbm
"INFO :Skipping dotfile:item:$item";
457 continue; # skip to next item
461 ## Ignore files with newlines present in filename. See [2].
462 if [[ $item =~ $
'\n' ]]; then
463 yell
"INFO :Skipping file name with newline:$item";
464 continue; # skip to next item
467 ## Ignore files that end in '~'.
468 if [[ $item =~ ~$
]]; then
469 yell
"INFO :Skipping file ending in tilde:$item";
470 continue; # skip to next item
473 ## Add item to file_list_pruned
474 file_list_pruned
+=("$item");
476 if [[ $opVerbose == "true" ]]; then
477 vbm
"DEBUG:file_list_pruned:";
478 printf "%s\n" "${file_list_pruned[@]}";
481 # Decide what actions to take for items in file_list_pruned
482 for item
in "${file_list_pruned[@]}"; do
483 vbm
"DEBUG:considering action to take for item:$item";
484 unset path_src path_prf dir_parent dir_source
;
486 ## Check file extension
487 if [[ $item =~ .ots$
]]; then
488 ### item ends in '.ots'. Item is proof file.
489 vbm
"DEBUG:item ends in '.ots'. Item is proof file:item:$item";
490 if [[ -f ${item%.ots} ]]; then
491 #### Proof file (item) is adjacent to source file
492 vbm
"DEBUG:Proof file (item) is adjacent to source file.";
493 ##### Upgrade and verify proof file against adjacent source file
494 vbm
"DEBUG:Marking proof file to be upgraded and verified.";
495 path_src
="${item%.ots}";
497 files_to_upgrade
+=("$(printf "%s
" "$path_prf")");
498 files_to_verify
+=("$(printf "%s
\n%s
" "$path_src" "$path_prf")");
500 #### Proof file (item) is not adjacent to source file
501 vbm
"DEBUG:Proof file (item) is not adjacent to source file.";
502 #### Check if source file in parent dir
503 dir_parent
="$(dirname "$
(dirname "$item")" )";
504 cand_src_filename
="$(basename "$item")";
505 cand_src_path
="$dir_parent/$cand_src_filename";
506 if [[ -f "$cand_src_path" ]]; then
507 ##### source file in parent dir
508 vbm
"DEBUG:found source file in parent:cand_src_path:$cand_src_path";
509 path_src
="$cand_src_path";
511 files_to_upgrade
+=("$(printf "%s
" "$path_prf")");
512 files_to_verify
+=("$(printf "%s
\n%s
" "$path_src" "$path_prf")");
514 #### Throw non-fatal error
515 vbm
"DEBUG:Source file not found for proof file:item:$item";
516 yell
"ERROR:Item is proof file but source filei not adjacent in parent dir. item:$item";
517 #### Attempt upgrade only
518 vbm
"DEBUG:Marking proof file to be upgraded.";
520 files_to_upgrade
+=("$(printf "%s
" "$path_prf")");
524 ### item does not end in '.ots'. Item is source file.
525 vbm
"DEBUG:item does NOT end in '.ots'. Item is source file.";
526 if [[ -f "$item".ots
]]; then
527 #### Proof file is adjacent to source file (item).
528 vbm
"DEBUG:Proof file is adjacent to source file (item).";
529 ##### Upgrade and verify proof file against adjacent source file.
530 vbm
"DEBUG:Marking proof file to be upgraded and verified.";
532 path_prf
="$item.ots";
533 files_to_upgrade
+=("$(printf "%s
" "$path_prf")");
534 files_to_verify
+=("$(printf "%s
\n%s
" "$path_src" "$path_prf")");
536 #### Proof file is not adjacent to source file (item).
537 #### Check if proof file is in subdir
538 vbm
"DEBUG:checking if proof file for source file (item) is in subdir:item:$item";
539 unset flag_proof_in_subdir
;
540 dir_item
="$(dirname "$item")";
541 cand_prf_filename
="$(basename "$item")".ots
;
542 while read -r line
; do
543 line_basename
="$(basename "$line")";
544 if [[ $line_basename == "$cand_prf_filename" ]]; then
545 flag_proof_in_subdir
="true";
547 vbm
"DEBUG:proof found in subdir at:line:$line";
550 done < <(find "$dir_item" -mindepth 2 -maxdepth 2 -type f
)
551 if [[ $flag_proof_in_subdir == "true" ]]; then
552 ##### Proof file is in subdir
553 vbm
"DEBUG:Proof file detected in subdir relative to source file (item)";
554 #path_prf="$path_prf"; # set in while loop
556 files_to_upgrade
+=("$(printf "%s
" "$path_prf")");
557 files_to_verify
+=("$(printf "%s
\n%s
" "$path_src" "$path_prf")");
559 ##### Proof file is not in subdir
560 vbm
"DEBUG:Proof file not detected in subdir relative to source file (item).";
561 #### Stamp source file
562 vbm
"DEBUG:Marking source file to be stamped.";
564 files_to_stamp
+=("$(printf "%s
" "$path_src")")
566 unset flag_proof_in_subdir
;
570 unset path_src path_prf dir_item dir_parent cand_prf_filename cand_src_filename line_basename cand_src_path
572 # Prune action lists.
573 ## Sort and prune file action arrays
575 while read -r -d $
'\0' line
; do
576 vbm
"DEBUG:adding to files_to_upgrade_pruned:line:$line";
577 files_to_upgrade_pruned
+=("$line");
578 done < <(printf "%s\0" "${files_to_upgrade[@]}" |
sort -zu | shuf
-z); # See [1]
579 if [[ $opVerbose == "true" ]]; then
580 vbm
"DEBUG:files_to_upgrade_pruned:";
581 printf "%s\n" "${files_to_upgrade_pruned[@]}";
585 while read -r -d $
'\0' line
; do
586 vbm
"DEBUG:adding to files_to_verify_pruned:line:$line";
587 files_to_verify_pruned
+=("$line");
588 done < <(printf "%s\0" "${files_to_verify[@]}" |
sort -zu | shuf
-z); # See [1]
589 if [[ $opVerbose == "true" ]]; then
590 vbm
"DEBUG:files_to_verify_pruned:";
591 printf "%s\n\n" "${files_to_verify_pruned[@]}";
595 while read -r -d $
'\0' line
; do
596 vbm
"DEBUG:adding to files_to_stamp_pruned:line:$line";
597 files_to_stamp_pruned
+=("$line");
598 done < <(printf "%s\0" "${files_to_stamp[@]}" |
sort -zu | shuf
-z); # See [1]
599 if [[ $opVerbose == "true" ]]; then
600 vbm
"DEBUG:files_to_stamp_pruned:";
601 printf "%s\n" "${files_to_stamp_pruned[@]}";
606 for item
in "${files_to_upgrade_pruned[@]}"; do
607 path_prf
="$(cut -d $'\n' -f1 < <(echo "$item"))";
608 if [[ -z "$path_prf" ]]; then
609 yell
"ERROR:blank upgrade item encountered. Skipping:item:$item";
612 vbm
"DEBUG:Attempting to upgrade proof file:path_prf:$path_prf";
613 if [[ ! $option_dry_run == "true" ]]; then
614 ots upgrade
"$path_prf";
616 yell
"DEBUG:DRY RUN:Not running:\"ots upgrade $path_prf\"";
622 for item
in "${files_to_verify_pruned[@]}"; do
623 path_src
="$(cut -d $'\n' -f1 < <(echo "$item"))";
624 path_prf
="$(cut -d $'\n' -f2 < <(echo "$item"))";
625 if [[ -z "$path_src" ]] ||
[[ -z "$path_prf" ]]; then
626 yell
"ERROR:blank verify item encountered. Skipping:item:$item";
629 vbm
"DEBUG:Attempting to verify source file:path_src:$path_src";
630 vbm
"DEBUG: against proof file: path_prf:$path_prf";
631 if [[ ! $option_dry_run == "true" ]]; then
632 ots verify
-f "$path_src" "$path_prf";
634 yell
"DEBUG:DRY RUN:Not running:\"ots verify -f $path_src $path_prf\"";
640 for item
in "${files_to_stamp_pruned[@]}"; do
641 path_src
="$(cut -d $'\n' -f1 < <(echo "$item"))";
642 if [[ -z "$path_src" ]]; then
643 yell
"ERROR:blank stamp item encountered. Skipping:item:$item";
646 vbm
"DEBUG:Attempting to stamp source file:path_src:$path_src";
647 if [[ ! $option_dry_run == "true" ]]; then
650 yell
"DEBUG:DRY RUN:Not running:\"ots stamp $item\"";