feat(user/bkots):Add script to run ots recursively
authorSteven Baltakatei Sandoval <baltakatei@gmail.com>
Sat, 30 Apr 2022 02:08:45 +0000 (02:08 +0000)
committerSteven Baltakatei Sandoval <baltakatei@gmail.com>
Sat, 30 Apr 2022 02:08:45 +0000 (02:08 +0000)
doc/user/bkots..flowchart.odg [new file with mode: 0644]
unitproc/bktemp-get_parent_dirnames [new file with mode: 0644]
unitproc/bktemp-processArgs
user/bkots [new file with mode: 0644]

diff --git a/doc/user/bkots..flowchart.odg b/doc/user/bkots..flowchart.odg
new file mode 100644 (file)
index 0000000..ea0787d
Binary files /dev/null and b/doc/user/bkots..flowchart.odg differ
diff --git a/unitproc/bktemp-get_parent_dirnames b/unitproc/bktemp-get_parent_dirnames
new file mode 100644 (file)
index 0000000..1a5184f
--- /dev/null
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+# Desc: Provides newline-delimited list of each parent dir
+
+yell() { echo "$0: $*" >&2; } # print script path and all args to stderr
+die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status
+try() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails
+get_parent_dirnames() {
+    # Desc: Provides newline-delimited list of each parent dir of a file or dir
+    # Usage: get_parent_dirnames arg1
+    # Input: arg1  input  path
+    # Output: stdout   newline-delimited list of parent dirs
+    # Version: 0.0.1
+    # Depends: yell(), die(), try()
+    local path
+
+    # Check input
+    if [[ $# -ne 1 ]]; then die "FATAL:Incorrect number of arguments:$#"; fi;
+    if ! { [[ -f $1 ]] || [[ -d $1 ]]; }; then die "FATAL:Not a file or dir:$1"; fi;
+
+    # Process path
+    path="$1";
+    while [[ -f $path ]] || [[ -d $path ]]; do
+        path="$(dirname "$path")";
+        name_base_previous="$name_base";
+        name_base="$(basename "$path")";
+        ## Check for stop condition (dirname returns same result as previous iteration)
+        if [[ $name_base == "$name_base_previous" ]]; then break; fi;
+        echo "$name_base";
+    done;    
+}; # Output parent dirnames to stdout
+
+# Test code
+sleep 1 && get_parent_dirnames /home/baltakatei/Downloads/ & # should work
+sleep 2 && get_parent_dirnames /home/baltakatei/Downloads/ foo & # should fail
+sleep 3 && get_parent_dirnames bar/baz & # should fail
+sleep 4;
index ca29c35235b9f9fded5650bc8ac9b7b0b779f397..27679a2550db6177140cccf0e033cd39ed297dc8 100644 (file)
@@ -4,6 +4,7 @@
 #==BEGIN Define script parameters==
 #===BEGIN Define variables===
 :
 #==BEGIN Define script parameters==
 #===BEGIN Define variables===
 :
+declare -ag arrayPosArgs
 #===END Define variables===
 
 #===BEGIN Declare local script functions===
 #===END Define variables===
 
 #===BEGIN Declare local script functions===
@@ -17,7 +18,7 @@ vbm() {
     # Input: arg1: string
     #        vars: opVerbose
     # Output: stderr
     # Input: arg1: string
     #        vars: opVerbose
     # Output: stderr
-    # Depends: bash 5.0.3, GNU-coreutils 8.30 (echo, date)
+    # Depends: bash 5.1.16, GNU-coreutils 8.30 (echo, date)
 
     if [ "$opVerbose" = "true" ]; then
        functionTime="$(date --iso-8601=ns)"; # Save current time in nano seconds.
 
     if [ "$opVerbose" = "true" ]; then
        functionTime="$(date --iso-8601=ns)"; # Save current time in nano seconds.
@@ -30,7 +31,7 @@ vbm() {
 showUsage() {
     # Desc: Display script usage information
     # Usage: showUsage
 showUsage() {
     # Desc: Display script usage information
     # Usage: showUsage
-    # Version 0.0.1
+    # Version 0.0.2
     # Input: none
     # Output: stdout
     # Depends: GNU-coreutils 8.30 (cat)
     # Input: none
     # Output: stdout
     # Depends: GNU-coreutils 8.30 (cat)
@@ -50,18 +51,21 @@ showUsage() {
         -I, --input-dir
                 Define input directory path.
         -o, --output-file
         -I, --input-dir
                 Define input directory path.
         -o, --output-file
-                Define output file path
+                Define output file path.
         -O, --output-dir
         -O, --output-dir
-                Define output directory path
+                Define output directory path.
+        --
+                Indicate end of options.
 
     EXAMPLE:
       bktemp-processArgs -o foo.txt
 
     EXAMPLE:
       bktemp-processArgs -o foo.txt
+      bktemp-processArgs -o foo.txt -- some_file.txt
 EOF
 } # Display information on how to use this script.
 showVersion() {
     # Desc: Displays script version and license information.
     # Usage: showVersion
 EOF
 } # Display information on how to use this script.
 showVersion() {
     # Desc: Displays script version and license information.
     # Usage: showVersion
-    # Version: 0.0.1
+    # Version: 0.0.2
     # Input: scriptVersion   var containing version string
     # Output: stdout
     # Depends: vbm(), yell, GNU-coreutils 8.30
     # Input: scriptVersion   var containing version string
     # Output: stdout
     # Depends: vbm(), yell, GNU-coreutils 8.30
@@ -70,14 +74,14 @@ showVersion() {
     vbm "DEBUG:showVersion function called."
 
     cat <<'EOF'
     vbm "DEBUG:showVersion function called."
 
     cat <<'EOF'
-bktemp-processArgs 0.0.1
+bktemp-processArgs 1.0.0
 Copyright (C) 2021 Steven Baltakatei Sandoval
 License GPLv3: GNU GPL version 3
 This is free software; you are free to change and redistribute it.
 There is NO WARRANTY, to the extent permitted by law.
 
 Copyright (C) 2021 Steven Baltakatei Sandoval
 License GPLv3: GNU GPL version 3
 This is free software; you are free to change and redistribute it.
 There is NO WARRANTY, to the extent permitted by law.
 
-    GNU Coreutils 8.30
-    Copyright (C) 2018 Free Software Foundation, Inc.
+    GNU Coreutils 8.32
+    Copyright (C) 2020 Free Software Foundation, Inc.
     License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
     This is free software: you are free to change and redistribute it.
     There is NO WARRANTY, to the extent permitted by law.
     License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
     This is free software: you are free to change and redistribute it.
     There is NO WARRANTY, to the extent permitted by law.
@@ -90,7 +94,7 @@ EOF
 processArgs() {
     # Desc: Processes arguments provided to script.
     # Usage: processArgs "$@"
 processArgs() {
     # Desc: Processes arguments provided to script.
     # Usage: processArgs "$@"
-    # Version: 0.0.1
+    # Version: 1.0.0
     # Input: "$@"          (list of arguments provided to the function)
     # Output: Sets following variables used by other functions:
     #   opVerbose            Indicates verbose mode enable status.  (ex: "true", "false")
     # Input: "$@"          (list of arguments provided to the function)
     # Output: Sets following variables used by other functions:
     #   opVerbose            Indicates verbose mode enable status.  (ex: "true", "false")
@@ -98,15 +102,18 @@ processArgs() {
     #   pathFileOut1         Path to output file.
     #   pathDirIn1           Path to input directory.
     #   pathFileIn1          Path to input file.
     #   pathFileOut1         Path to output file.
     #   pathDirIn1           Path to input directory.
     #   pathFileIn1          Path to input file.
-    #   opFileOut1_overwrite Indicates whether file pathFileOut1 should be overwritten (ex: "true", "false')
+    #   opFileOut1_overwrite Indicates whether file pathFileOut1 should be overwritten (ex: "true", "false").
+    #   arrayPosArgs         Array of remaining positional argments
     # Depends:
     #   yell()           Displays messages to stderr.
     #   vbm()            Displays messsages to stderr if opVerbose set to "true".
     # Depends:
     #   yell()           Displays messages to stderr.
     #   vbm()            Displays messsages to stderr if opVerbose set to "true".
-    #   showUsage()      Displays usage information about parent script
-    #   showVersion()    Displays version about parent script
-    # External dependencies: bash (5.0.3), echo
+    #   showUsage()      Displays usage information about parent script.
+    #   showVersion()    Displays version about parent script.
+    #   arrayPosArgs     Global array for storing non-option positional arguments (i.e. arguments following the `--` option).
+    # External dependencies: bash (5.1.16), echo
     # Ref./Attrib.:
     #  [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347
     # Ref./Attrib.:
     #  [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347
+    #  [2]: "Handling positional parameters" (2018-05-12). https://wiki.bash-hackers.org/scripting/posparams
 
     # Initialize function
     vbm "DEBUG:processArgs function called."
 
     # Initialize function
     vbm "DEBUG:processArgs function called."
@@ -169,8 +176,16 @@ processArgs() {
                    yell "ERROR:Specified output directory is not valid:""$2";
                    yell "Exiting.";
                    exit 1;
                    yell "ERROR:Specified output directory is not valid:""$2";
                    yell "Exiting.";
                    exit 1;
-               fi ;; 
-           *) yell "ERROR: Unrecognized argument."; exit 1;; # Handle unrecognized options. See [1].
+               fi ;;
+            --) # End of all options. See [2].
+                shift;
+                for arg in "$@"; do
+                    vbm "DEBUG:adding to arrayPosArgs:$arg";
+                    arrayPosArgs+=("$arg");
+                done;
+                break;;
+            -*) showUsage; yell "ERROR: Unrecognized option."; exit 1;; # Display usage
+           *) showUsage; yell "ERROR: Unrecognized argument."; exit 1;; # Handle unrecognized options. See [1].
        esac
        shift
     done
        esac
        shift
     done
@@ -186,12 +201,14 @@ processArgs() {
 #==BEGIN sample code==
 processArgs "$@";
 yell "DEBUG:Provided arguments..:""$*" 
 #==BEGIN sample code==
 processArgs "$@";
 yell "DEBUG:Provided arguments..:""$*" 
-yell "DEBUG:opVerbose...........:$opVerbose";
-yell "DEBUG:pathDirOut1.........:$pathDirOut1";
-yell "DEBUG:pathFileOut1........:$pathFileOut1";
-yell "DEBUG:pathDirIn1..........:$pathDirIn1";
-yell "DEBUG:pathFileIn1.........:$pathFileIn1";
-yell "DEBUG:opFileOut1_overwrite:$opFileOut1_overwrite";
+yell "DEBUG:opVerbose...........:${opVerbose:-<unset>}";
+yell "DEBUG:pathDirOut1.........:${pathDirOut1:-<unset>}";
+yell "DEBUG:pathFileOut1........:${pathFileOut1:-<unset>}";
+yell "DEBUG:pathDirIn1..........:${pathDirIn1:-<unset>}";
+yell "DEBUG:pathFileIn1.........:${pathFileIn1:-<unset>}";
+yell "DEBUG:opFileOut1_overwrite:${opFileOut1_overwrite:-<unset>}";
+yell "DEBUG:arrayPosArgs........:$(if [[ -v arrayPosArgs ]]; then declare -p arrayPosArgs; else printf "<unset>"; fi )";
+#yell "DEBUG:arrayPosArgs........:${arrayPosArgs[*]}";
 #==END sample code==
 
 # Author: Steven Baltaktei Sandoval
 #==END sample code==
 
 # Author: Steven Baltaktei Sandoval
diff --git a/user/bkots b/user/bkots
new file mode 100644 (file)
index 0000000..fa9b7da
--- /dev/null
@@ -0,0 +1,658 @@
+#!/usr/bin/env bash
+
+# Define variables
+declare -Ag appRollCall # Associative array for storing app status
+declare -Ag fileRollCall # Associative array for storing file status
+declare -Ag dirRollCall # Associative array for storing dir status
+declare -ag arrayPosArgs # Associative array for processArgs() function
+
+# Declare functions
+yell() { echo "$0: $*" >&2; } # print script path and all args to stderr
+die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status
+try() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails
+checkapp() {
+    # Desc: If arg is a command, save result in assoc array 'appRollCall'
+    # Usage: checkapp arg1 arg2 arg3 ...
+    # Version: 0.1.1
+    # Input: global assoc. array 'appRollCall'
+    # Output: adds/updates key(value) to global assoc array 'appRollCall'
+    # Depends: bash 5.0.3
+    local returnState    
+
+    #===Process Args===
+    for arg in "$@"; do
+       if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command
+           appRollCall[$arg]="true";
+           if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
+       else
+           appRollCall[$arg]="false"; returnState="false";
+       fi;
+    done;
+
+    #===Determine function return code===
+    if [ "$returnState" = "true" ]; then
+       return 0;
+    else
+       return 1;
+    fi;
+} # Check that app exists
+checkfile() {
+    # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
+    # Usage: checkfile arg1 arg2 arg3 ...
+    # Version: 0.1.1
+    # Input: global assoc. array 'fileRollCall'
+    # Output: adds/updates key(value) to global assoc array 'fileRollCall';
+    # Output: returns 0 if app found, 1 otherwise
+    # Depends: bash 5.0.3
+    local returnState
+
+    #===Process Args===
+    for arg in "$@"; do
+       if [ -f "$arg" ]; then
+           fileRollCall["$arg"]="true";
+           if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
+       else
+           fileRollCall["$arg"]="false"; returnState="false";
+       fi;
+    done;
+    
+    #===Determine function return code===
+    if [ "$returnState" = "true" ]; then
+       return 0;
+    else
+       return 1;
+    fi;
+} # Check that file exists
+checkdir() {
+    # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
+    # Usage: checkdir arg1 arg2 arg3 ...
+    # Version 0.1.2
+    # Input: global assoc. array 'dirRollCall'
+    # Output: adds/updates key(value) to global assoc array 'dirRollCall';
+    # Output: returns 0 if all args are dirs; 1 otherwise
+    # Depends: Bash 5.0.3
+    local returnState
+
+    #===Process Args===
+    for arg in "$@"; do
+       if [ -z "$arg" ]; then
+           dirRollCall["(Unspecified Dirname(s))"]="false"; returnState="false";
+       elif [ -d "$arg" ]; then
+           dirRollCall["$arg"]="true";
+           if ! [ "$returnState" = "false" ]; then returnState="true"; fi
+       else
+           dirRollCall["$arg"]="false"; returnState="false";
+       fi
+    done
+    
+    #===Determine function return code===
+    if [ "$returnState" = "true" ]; then
+       return 0;
+    else
+       return 1;
+    fi
+} # Check that dir exists
+displayMissing() {
+    # Desc: Displays missing apps, files, and dirs
+    # Usage: displayMissing
+    # Version 1.0.0
+    # Input: associative arrays: appRollCall, fileRollCall, dirRollCall
+    # Output: stderr: messages indicating missing apps, file, or dirs
+    # Output: returns exit code 0 if nothing missing; 1 otherwise
+    # Depends: bash 5, checkAppFileDir()
+    local missingApps value appMissing missingFiles fileMissing
+    local missingDirs dirMissing
+
+    #==BEGIN Display errors==
+    #===BEGIN Display Missing Apps===
+    missingApps="Missing apps  :";
+    #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done
+    for key in "${!appRollCall[@]}"; do
+       value="${appRollCall[$key]}";
+       if [ "$value" = "false" ]; then
+           #echo "DEBUG:Missing apps: $key => $value";
+           missingApps="$missingApps""$key ";
+           appMissing="true";
+       fi;
+    done;
+    if [ "$appMissing" = "true" ]; then  # Only indicate if an app is missing.
+       echo "$missingApps" 1>&2;
+    fi;
+    unset value;
+    #===END Display Missing Apps===
+
+    #===BEGIN Display Missing Files===
+    missingFiles="Missing files:";
+    #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done
+    for key in "${!fileRollCall[@]}"; do
+       value="${fileRollCall[$key]}";
+       if [ "$value" = "false" ]; then
+           #echo "DEBUG:Missing files: $key => $value";
+           missingFiles="$missingFiles""$key ";
+           fileMissing="true";
+       fi;
+    done;
+    if [ "$fileMissing" = "true" ]; then  # Only indicate if an app is missing.
+       echo "$missingFiles" 1>&2;
+    fi;
+    unset value;
+    #===END Display Missing Files===
+
+    #===BEGIN Display Missing Directories===
+    missingDirs="Missing dirs:";
+    #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done
+    for key in "${!dirRollCall[@]}"; do
+       value="${dirRollCall[$key]}";
+       if [ "$value" = "false" ]; then
+           #echo "DEBUG:Missing dirs: $key => $value";
+           missingDirs="$missingDirs""$key ";
+           dirMissing="true";
+       fi;
+    done;
+    if [ "$dirMissing" = "true" ]; then  # Only indicate if an dir is missing.
+       echo "$missingDirs" 1>&2;
+    fi;
+    unset value;
+    #===END Display Missing Directories===
+
+    #==END Display errors==
+    #==BEGIN Determine function return code===
+    if [ "$appMissing" == "true" ] || [ "$fileMissing" == "true" ] || [ "$dirMissing" == "true" ]; then
+       return 1;
+    else
+       return 0;
+    fi
+    #==END Determine function return code===
+} # Display missing apps, files, dirs
+vbm() {
+    # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true".
+    # Usage: vbm "DEBUG :verbose message here"
+    # Version 0.2.0
+    # Input: arg1: string
+    #        vars: opVerbose
+    # Output: stderr
+    # Depends: bash 5.0.3, GNU-coreutils 8.30 (echo, date)
+
+    if [ "$opVerbose" = "true" ]; then
+       functionTime="$(date --iso-8601=ns)"; # Save current time in nano seconds.
+       echo "[$functionTime]:$0:""$*" 1>&2;  # Display argument text.
+    fi
+
+    # End function
+    return 0; # Function finished.
+} # Displays message if opVerbose true
+showVersion() {
+    # Desc: Displays script version and license information.
+    # Usage: showVersion
+    # Version: 0.0.1
+    # Input: scriptVersion   var containing version string
+    # Output: stdout
+    # Depends: vbm(), yell, GNU-coreutils 8.30
+
+    # Initialize function
+    vbm "DEBUG:showVersion function called."
+
+    cat <<'EOF'
+bkots 0.0.1
+Copyright (C) 2022 Steven Baltakatei Sandoval
+License GPLv3: GNU GPL version 3
+This is free software; you are free to change and redistribute it.
+There is NO WARRANTY, to the extent permitted by law.
+
+    GNU Coreutils 8.32
+    Copyright (C) 2020 Free Software Foundation, Inc.
+    License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+    This is free software: you are free to change and redistribute it.
+    There is NO WARRANTY, to the extent permitted by law.
+EOF
+    
+    # End function
+    vbm "DEBUG:showVersion function ended."
+    return 0; # Function finished.
+} # Display script version.
+showUsage() {
+    # Desc: Display script usage information
+    # Usage: showUsage
+    # Version 0.0.1
+    # Input: none
+    # Output: stdout
+    # Depends: GNU-coreutils 8.30 (cat)
+    cat <<'EOF'
+    USAGE:
+        bkots [ options ] [PATH...]
+
+    POSITIONAL ARGUMENTS:
+        PATH    Path(s) of file(s) or directory(ies)
+
+    OPTIONS:
+        --dry-run
+                Do everything except run 'ots' commands.
+        -h, --help
+                Display help information.
+        --include-dotfiles
+                Include files and directories starting with '.' (not
+                included by default).
+        -r, --recursive
+                Consider files in dirs recursively.
+        --version
+                Display script version.
+        -v, --verbose
+                Display debugging info.
+        --
+                Mark end of options. Interpret remaining arguments as
+                positional arguments.
+
+    DESCRIPTION:
+        Scans files by file paths or directory paths provided by
+        positional arguments to see if Open Timestamps '.ots' file
+        exists. If so, attempt to upgrade and verify the '.ots'
+        file. If no '.ots' file exists, attempt to create one.
+
+        Files with a dotfile parent directory located anywhere in the
+        file path are ignored by default. (e.g. 'HEAD' in
+        '/home/user/diary/.git/logs/HEAD' because of '.git'). Dotfiles
+        themselves are also ignored by default
+        (e.g. '/home/user/.gitconfig').
+
+    EXAMPLES:
+      bkots -v foo.txt
+      bkots foo.txt bar.pdf /home/username/Pictures/
+EOF
+} # Display information on how to use this script.
+processArgs() {
+    # Desc: Processes arguments provided to script.
+    # Usage: processArgs "$@"
+    # Version: 1.0.0
+    # Input: "$@"          (list of arguments provided to the function)
+    # Output: Sets following variables used by other functions:
+    #   opVerbose            Indicates verbose mode enable status.  (ex: "true", "false")
+    #   arrayPosArgs         Array of remaining positional argments
+    # Depends:
+    #   yell()           Displays messages to stderr.
+    #   vbm()            Displays messsages to stderr if opVerbose set to "true".
+    #   showUsage()      Displays usage information about parent script.
+    #   showVersion()    Displays version about parent script.
+    #   arrayPosArgs     Global array for storing non-option positional arguments (i.e. arguments following the `--` option).
+    # External dependencies: bash (5.1.16), echo
+    # Ref./Attrib.:
+    #  [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347
+    #  [2]: "Handling positional parameters" (2018-05-12). https://wiki.bash-hackers.org/scripting/posparams
+
+    # Initialize function
+    vbm "DEBUG:processArgs function called."
+
+    # Perform work
+    while [ ! $# -eq 0 ]; do   # While number of arguments ($#) is not (!) equal to (-eq) zero (0).
+       #yell "DEBUG:Starting processArgs while loop." # Debug stderr message. See [1].
+        #yell "DEBUG:Provided arguments are:""$*"      # Debug stderr message. See [1].
+       case "$1" in
+            --dry-run) # Do not run ots commands
+                option_dry_run="true";
+                vbm "DEBUG:Option enabled:dry run";;
+           -h | --help) showUsage; exit 1;; # Display usage.
+            --include-dotfiles) # Include dotfiles
+                option_include_dotfiles="true";
+                vbm "DEBUG:Option enabled:include dotfiles";;
+            -r | --recursive) # Specify recursive option
+                option_recursive="true";
+                vbm "DEBUG:option enabled:include files in dirs recursively";;
+           --version) showVersion; exit 1;; # Show version
+           -v | --verbose) opVerbose="true"; vbm "DEBUG:Verbose mode enabled.";; # Enable verbose mode. See [1].
+            --) # End of all options. See [2].
+                shift;
+                for arg in "$@"; do
+                    vbm "DEBUG:adding to arrayPosArgs:$arg";
+                    arrayPosArgs+=("$arg");
+                done;
+                break;;
+            -*) showUsage; yell "ERROR: Unrecognized option."; exit 1;; # Display usage
+            *) # Assume remaining arguments are positional arguments
+                for arg in "$@"; do
+                    vbm "DEBUG:adding to arrayPosArgs:$arg";
+                    arrayPosArgs+=("$arg");
+                done;
+                break;;
+           #*) showUsage; yell "ERROR: Unrecognized argument."; exit 1;; # Handle unrecognized options. See [1].
+       esac
+       shift
+    done
+
+    # End function
+    vbm "DEBUG:processArgs function ended."
+    return  0; # Function finished.
+}; # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.).
+get_parent_dirnames() {
+    # Desc: Provides newline-delimited list of each parent dir of a file or dir
+    # Usage: get_parent_dirnames arg1
+    # Input: arg1  input  path
+    # Output: stdout   newline-delimited list of parent dirs
+    # Version: 0.0.1
+    # Depends: yell(), die(), try()
+    local path
+
+    # Check input
+    if [[ $# -ne 1 ]]; then die "FATAL:Incorrect number of arguments:$#"; fi;
+    if ! { [[ -f $1 ]] || [[ -d $1 ]]; }; then die "FATAL:Not a file or dir:$1"; fi;
+
+    # Process path
+    path="$1";
+    while [[ -f $path ]] || [[ -d $path ]]; do
+        path="$(dirname "$path")";
+        name_base_previous="$name_base";
+        name_base="$(basename "$path")";
+        ## Check for stop condition (dirname returns same result as previous iteration)
+        if [[ $name_base == "$name_base_previous" ]]; then break; fi;
+        echo "$name_base";
+    done;    
+}; # Output parent dirnames to stdout
+main() {
+    # Desc: Creates `.ots` file:
+    #     - for each file specified in arrayPosArgs array
+    #     - for each file in each dir specified in arrayPosArgs array
+    #   Output file created alongside each file or in output directory specified by pathDirIn1
+    # Usage: main "$@";
+    # Input: arrayPosArgs  array with positional arguments
+    #        pathDirOut1   path for output `.ots` files (if pathDirOut1 is specified and is a path)
+    # Output: file(s) creates `.ots` file alongside specified files
+    # Depends: find (GNU findutils) 4.8.0, GNU Coreutils 8.32 (sort)
+    # Ref/Attrib: [1] How to create an array of unique elements from a string/array in bash https://unix.stackexchange.com/a/167194
+    #             [2] How to find files containing newlines in their names https://stackoverflow.com/a/21727028
+    local -a file_list file_list_pruned;
+    local -a files_to_verify files_to_upgrade files_to_stamp
+    local -a files_to_verify_pruned files_to_upgrade_pruned files_to_stamp_pruned 
+
+    # Process args
+    processArgs "$@";
+    
+    # Check dependencies
+    if ! checkapp ots find; then
+        displayMissing;
+        die "FATAL:Missing dependencies.";
+    fi;
+    
+    # Check arguments    
+    ## Mark if output dir option specified
+    if [[ -v pathDirOut1 ]]; then
+        vbm "DEBUG:output directory specified:pathDirOut1:$pathDirOut1";
+        if [[ -d $pathDirOut1 ]]; then
+            vbm "DEBUG:pathDirOut1:$pathDirOut1";
+            config_output_dir="true";
+        else
+            die "ERROR:Not a dir:$pathDirOut1";
+        fi;
+    fi;
+
+    # Display ots details
+    vbm "$(type ots)"; # show how 'ots' is defined
+        #TODO: add option to define 'ots' as a bash function that
+        #populates the ots option '--bitcoin-node FILE' with a
+        #user-specified FILE.
+    
+    # Populate file_list
+    vbm "DEBUG:begin populate file_list array";
+    for item in "${arrayPosArgs[@]}"; do
+        vbm "DEBUG:adding to file list:item:$item";
+
+        ## Get full canonicalized path (follow symlinks)
+        item="$(readlink -f "$item")";
+        vbm "DEBUG:item full path:item:$item";
+        
+        ## Add to list: files
+        if [[ -f $item ]]; then
+            vbm "DEBUG:is a file:item:$item";
+            file_list+=("$item");
+            vbm "DEBUG:added to file_list:$item";
+        ## Add to list: files in dirs
+        elif [[ -d $item ]]; then
+            vbm "DEBUG:is a dir:item:$item";
+            ### Check for recursive flag
+            if [[ "$option_recursive" == "true" ]]; then
+                vbm "DEBUG:option_recursive:$option_recursive";
+                while read -r line; do
+                    file_list+=("$line");
+                    vbm "DEBUG:added to file_list:$line";
+                done < <(find "$item" -type f);
+            else
+                while read -r line; do
+                    file_list+=("$line");
+                    vbm "DEBUG:added to file_list:$line";
+                done < <(find "$item" -maxdepth 1 -type f);
+            fi;
+        else
+            die "ERROR:Not a file or dir:item:$item";
+        fi;
+    done;
+    if [[ $opVerbose == "true" ]]; then
+        vbm "DEBUG:file_list:";
+        printf "%s\n" "${file_list[@]}";
+    fi;
+
+    # Prune file_list
+    for item in "${file_list[@]}"; do
+        if ! [[ $option_include_dotfiles == "true" ]]; then
+            ## Ignore files located beneath a dotfile directory (e.g. '/home/my_repo/.git/config')
+            unset flag_contains_dotfile_parent;
+            while read -r line; do
+                ### Check line from output of get_parent_dirnames
+                pattern="^\.";
+                if [[ $line =~ $pattern ]]; then
+                    #### line starts with '.'
+                    vbm "DEBUG:Dotfile parent detected. Not including in file_list_pruned:$item";
+                    vbm "DEBUG:Dotfile in path:item:$item";
+                    vbm "DEBUG:Dotfile parent:line:$line";
+                    flag_contains_dotfile_parent="true";
+                    break
+                fi;
+            done < <(get_parent_dirnames "$item");
+            if [[ $flag_contains_dotfile_parent == "true" ]]; then
+                unset flag_contains_dotfile_parent;
+                continue; # skip to next item (i.e. don't add to file_list_pruned)
+            fi;
+
+            ## Ignore dotfiles themselves
+            item_basename="$(basename "$item")";
+            pattern="^\.";
+            if [[ $item_basename =~ $pattern ]]; then
+                vbm "INFO :Skipping dotfile:item:$item";
+                continue; # skip to next item
+            fi;
+        fi;
+
+        ## Ignore files with newlines present in filename. See [2].
+        if [[ $item =~ $'\n' ]]; then
+            yell "INFO :Skipping file name with newline:$item";
+            continue; # skip to next item
+        fi;
+
+        ## Ignore files that end in '~'.
+        if [[ $item =~ ~$ ]]; then
+            yell "INFO :Skipping file ending in tilde:$item";
+            continue; # skip to next item
+        fi;
+        
+        ## Add item to file_list_pruned
+        file_list_pruned+=("$item");
+    done;
+    if [[ $opVerbose == "true" ]]; then
+        vbm "DEBUG:file_list_pruned:";
+        printf "%s\n" "${file_list_pruned[@]}";
+    fi;
+    
+    # Decide what actions to take for items in file_list_pruned
+    for item in "${file_list_pruned[@]}"; do
+        vbm "DEBUG:considering action to take for item:$item";
+        unset path_src path_prf dir_parent dir_source;
+            
+        ## Check file extension
+        if [[ $item =~ .ots$ ]]; then
+            ### item ends in '.ots'. Item is proof file.
+            vbm "DEBUG:item ends in '.ots'. Item is proof file:item:$item";
+            if [[ -f ${item%.ots} ]]; then
+                #### Proof file (item) is adjacent to source file
+                vbm "DEBUG:Proof file (item) is adjacent to source file.";
+                ##### Upgrade and verify proof file against adjacent source file
+                vbm "DEBUG:Marking proof file to be upgraded and verified.";
+                path_src="${item%.ots}";
+                path_prf="$item";
+                files_to_upgrade+=("$(printf "%s" "$path_prf")");
+                files_to_verify+=("$(printf "%s\n%s" "$path_src" "$path_prf")");
+            else
+                #### Proof file (item) is not adjacent to source file
+                vbm "DEBUG:Proof file (item) is not adjacent to source file.";
+                #### Check if source file in parent dir
+                dir_parent="$(dirname "$(dirname "$item")" )";
+                cand_src_filename="$(basename "$item")";
+                cand_src_path="$dir_parent/$cand_src_filename";
+                if [[ -f "$cand_src_path" ]]; then
+                    ##### source file in parent dir
+                    vbm "DEBUG:found source file in parent:cand_src_path:$cand_src_path";
+                    path_src="$cand_src_path";
+                    path_prf="$item";
+                    files_to_upgrade+=("$(printf "%s" "$path_prf")");
+                    files_to_verify+=("$(printf "%s\n%s" "$path_src" "$path_prf")");
+                else
+                    #### Throw non-fatal error
+                    vbm "DEBUG:Source file not found for proof file:item:$item";
+                    yell "ERROR:Item is proof file but source filei not adjacent in parent dir. item:$item";
+                    #### Attempt upgrade only
+                    vbm "DEBUG:Marking proof file to be upgraded.";
+                    path_prf="$item";
+                    files_to_upgrade+=("$(printf "%s" "$path_prf")");
+                fi;
+            fi;
+        else
+            ### item does not end in '.ots'. Item is source file.
+            vbm "DEBUG:item does NOT end in '.ots'. Item is source file.";
+            if [[ -f "$item".ots ]]; then
+                #### Proof file is adjacent to source file (item).
+                vbm "DEBUG:Proof file is adjacent to source file (item).";
+                ##### Upgrade and verify proof file against adjacent source file.
+                vbm "DEBUG:Marking proof file to be upgraded and verified.";
+                path_src="$item";
+                path_prf="$item.ots";                
+                files_to_upgrade+=("$(printf "%s" "$path_prf")");
+                files_to_verify+=("$(printf "%s\n%s" "$path_src" "$path_prf")");
+            else
+                #### Proof file is not adjacent to source file (item).
+                #### Check if proof file is in subdir
+                vbm "DEBUG:checking if proof file for source file (item) is in subdir:item:$item";
+                unset flag_proof_in_subdir;
+                dir_item="$(dirname "$item")";
+                cand_prf_filename="$(basename "$item")".ots;
+                while read -r line; do
+                    line_basename="$(basename "$line")";
+                    if [[ $line_basename == "$cand_prf_filename" ]]; then
+                        flag_proof_in_subdir="true";
+                        path_prf="$line";
+                        vbm "DEBUG:proof found in subdir at:line:$line";
+                        break;
+                    fi;
+                done < <(find "$dir_item" -mindepth 2 -maxdepth 2 -type f)
+                if [[ $flag_proof_in_subdir == "true" ]]; then
+                    ##### Proof file is in subdir
+                    vbm "DEBUG:Proof file detected in subdir relative to source file (item)";
+                    #path_prf="$path_prf"; # set in while loop
+                    path_src="$item";
+                    files_to_upgrade+=("$(printf "%s" "$path_prf")");
+                    files_to_verify+=("$(printf "%s\n%s" "$path_src" "$path_prf")");
+                else
+                    ##### Proof file is not in subdir
+                    vbm "DEBUG:Proof file not detected in subdir relative to source file (item).";
+                    #### Stamp source file
+                    vbm "DEBUG:Marking source file to be stamped.";
+                    path_src="$item";
+                    files_to_stamp+=("$(printf "%s" "$path_src")")
+                fi;
+                unset flag_proof_in_subdir;
+            fi;            
+        fi;
+    done;
+    unset path_src path_prf dir_item dir_parent cand_prf_filename cand_src_filename line_basename cand_src_path
+
+    # Prune action lists.
+    ## Sort and prune file action arrays
+    ### files to upgrade
+    while read -r -d $'\0' line; do
+        vbm "DEBUG:adding to files_to_upgrade_pruned:line:$line";
+        files_to_upgrade_pruned+=("$line");
+    done < <(printf "%s\0" "${files_to_upgrade[@]}" | sort -zu | shuf -z); # See [1]
+    if [[ $opVerbose == "true" ]]; then
+        vbm "DEBUG:files_to_upgrade_pruned:";
+        printf "%s\n" "${files_to_upgrade_pruned[@]}";
+    fi;
+
+    ### files to verify
+    while read -r -d $'\0' line; do
+        vbm "DEBUG:adding to files_to_verify_pruned:line:$line";
+        files_to_verify_pruned+=("$line");
+    done < <(printf "%s\0" "${files_to_verify[@]}" | sort -zu | shuf -z); # See [1]
+    if [[ $opVerbose == "true" ]]; then
+        vbm "DEBUG:files_to_verify_pruned:";
+        printf "%s\n\n" "${files_to_verify_pruned[@]}";
+    fi;
+
+    ### files to stamp
+    while read -r -d $'\0' line; do
+        vbm "DEBUG:adding to files_to_stamp_pruned:line:$line";
+        files_to_stamp_pruned+=("$line");
+    done < <(printf "%s\0" "${files_to_stamp[@]}" | sort -zu | shuf -z); # See [1]
+    if [[ $opVerbose == "true" ]]; then
+        vbm "DEBUG:files_to_stamp_pruned:";
+        printf "%s\n" "${files_to_stamp_pruned[@]}";
+    fi;
+    
+    # Act on files
+    ## Upgrade files
+    for item in "${files_to_upgrade_pruned[@]}"; do
+        path_prf="$(cut -d $'\n' -f1 < <(echo "$item"))";
+        if [[ -z "$path_prf" ]]; then
+            yell "ERROR:blank upgrade item encountered. Skipping:item:$item";
+            continue;
+        fi;
+        vbm "DEBUG:Attempting to upgrade proof file:path_prf:$path_prf";
+        if [[ ! $option_dry_run == "true" ]]; then
+            ots upgrade "$path_prf";
+        else
+            yell "DEBUG:DRY RUN:Not running:\"ots upgrade $path_prf\"";
+        fi;
+        
+    done;
+
+    ## Verify files
+    for item in "${files_to_verify_pruned[@]}"; do
+        path_src="$(cut -d $'\n' -f1 < <(echo "$item"))";
+        path_prf="$(cut -d $'\n' -f2 < <(echo "$item"))";
+        if [[ -z "$path_src" ]] || [[ -z "$path_prf" ]]; then
+            yell "ERROR:blank verify item encountered. Skipping:item:$item";
+            continue;
+        fi;
+        vbm "DEBUG:Attempting to verify source file:path_src:$path_src";
+        vbm "DEBUG:    against proof file:          path_prf:$path_prf";
+        if [[ ! $option_dry_run == "true" ]]; then
+            ots verify -f "$path_src" "$path_prf";
+        else
+            yell "DEBUG:DRY RUN:Not running:\"ots verify -f $path_src $path_prf\"";
+        fi;
+        
+    done;
+    
+    ## Stamp files
+    for item in "${files_to_stamp_pruned[@]}"; do
+        path_src="$(cut -d $'\n' -f1 < <(echo "$item"))";
+        if [[ -z "$path_src" ]]; then
+            yell "ERROR:blank stamp item encountered. Skipping:item:$item";
+            continue;
+        fi;
+        vbm "DEBUG:Attempting to stamp source file:path_src:$path_src";
+        if [[ ! $option_dry_run == "true" ]]; then
+            ots stamp "$item";
+        else
+            yell "DEBUG:DRY RUN:Not running:\"ots stamp $item\"";
+        fi;
+        
+    done;
+
+}; # main program
+
+# Run program
+main "$@";