2 # Desc: Create and sign a checksum
3 # Usage: bksum file.txt
4 # Input: stdin: file list (newline delimited)
5 # arg(s): file paths (IFS delimited)
6 # Output: file containing sha256 hashes and file paths
7 # Depends: bash v5.1.16, date (GNU Coreutils 8.32), gpg v2.2.27, ots v0.7.0
10 declare -Ag appRollCall
# Associative array for storing app status
11 declare -Ag fileRollCall
# Associative array for storing file status
12 declare -Ag dirRollCall
# Associative array for storing dir status
13 declare -ag arrPosArgs
; # positional arguments
14 declare -ag arrStdin
; # standard input lines
15 declare -ag arrInFiles
; # input files
17 yell
() { echo "$0: $*" >&2; } # print script path and all args to stderr
18 die
() { yell
"$*"; exit 111; } # same as yell() but non-zero exit status
19 try
() { "$@" || die
"cannot $*"; } # runs args as command, reports args if command fails
21 # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true".
22 # Usage: vbm "DEBUG :verbose message here"
27 # Depends: bash 5.1.16, GNU-coreutils 8.30 (echo, date)
29 if [ "$opVerbose" = "true" ]; then
30 functionTime
="$(date --iso-8601=ns)"; # Save current time in nano seconds.
31 echo "[$functionTime]:$0:""$*" 1>&2; # Display argument text.
35 return 0; # Function finished.
36 } # Displays message if opVerbose true
38 # Desc: Display script usage information
43 # Depends: GNU-coreutils 8.30 (cat)
46 bksum [ options ] [FILE...]
49 Creates sha256 checksum of files.
53 Display help information.
55 Display script version.
57 Display debugging info.
59 Define output file path. By default, the file
60 name includes the full ISO-8601 timestamp
61 without separators, e.g.:
62 20220920T2117+0000..SHA256SUM
64 Sign with GnuPG the checksum file.
66 Timestamp with OpenTimestamps the checksum file
67 (and GnuPG signature if -s/--sign specified).
69 Same as '-t/--timestamp' except that
70 OpenTimestamps will run as a background process
71 until a calendar server returns a completed
74 Indicate end of options.
78 bksum file1.txt file2.txt
79 bksum -v -- file1.txt file2.txt
80 bksum -v -t -- file1.txt file2.txt
81 find . -type f | bksum
82 find . -type f | bksum -v -s -t -- file.txt
85 If GNU Coreutils 8.32 `sha256sum` used, checksum file
86 can be verified using:
87 sha256sum --check 20220920T2117+0000..SHA256SUM
89 } # Display information on how to use this script.
91 # Desc: Displays script version and license information.
94 # Input: scriptVersion var containing version string
96 # Depends: vbm(), yell, GNU-coreutils 8.30
99 vbm
"DEBUG:showVersion function called."
103 Copyright (C) 2022 Steven Baltakatei Sandoval
104 License GPLv3: GNU GPL version 3
105 This is free software; you are free to change and redistribute it.
106 There is NO WARRANTY, to the extent permitted by law.
111 vbm
"DEBUG:showVersion function ended."
112 return 0; # Function finished.
113 } # Display script version.
115 # Desc: If arg is a command, save result in assoc array 'appRollCall'
116 # Usage: checkapp arg1 arg2 arg3 ...
118 # Input: global assoc. array 'appRollCall'
119 # Output: adds/updates key(value) to global assoc array 'appRollCall'
120 # Depends: bash 5.0.3
125 if command -v "$arg" 1>/dev
/null
2>&1; then # Check if arg is a valid command
126 appRollCall
[$arg]="true";
127 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi;
129 appRollCall
[$arg]="false"; returnState
="false";
133 #===Determine function return code===
134 if [ "$returnState" = "true" ]; then
139 } # Check that app exists
141 # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
142 # Usage: checkfile arg1 arg2 arg3 ...
144 # Input: global assoc. array 'fileRollCall'
145 # Output: adds/updates key(value) to global assoc array 'fileRollCall';
146 # Output: returns 0 if app found, 1 otherwise
147 # Depends: bash 5.0.3
152 if [ -f "$arg" ]; then
153 fileRollCall
["$arg"]="true";
154 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi;
155 elif [ -z "$arg" ]; then
156 fileRollCall
["(no name)"]="false"; returnState
="false";
158 fileRollCall
["$arg"]="false"; returnState
="false";
162 #===Determine function return code===
163 if [ "$returnState" = "true" ]; then
168 } # Check that file exists
170 # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
171 # Usage: checkdir arg1 arg2 arg3 ...
173 # Input: global assoc. array 'dirRollCall'
174 # Output: adds/updates key(value) to global assoc array 'dirRollCall';
175 # Output: returns 0 if all args are dirs; 1 otherwise
176 # Depends: Bash 5.0.3
181 if [ -z "$arg" ]; then
182 dirRollCall
["(Unspecified Dirname(s))"]="false"; returnState
="false";
183 elif [ -d "$arg" ]; then
184 dirRollCall
["$arg"]="true";
185 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi
187 dirRollCall
["$arg"]="false"; returnState
="false";
191 #===Determine function return code===
192 if [ "$returnState" = "true" ]; then
197 } # Check that dir exists
199 # Desc: Displays missing apps, files, and dirs
200 # Usage: displayMissing
202 # Input: associative arrays: appRollCall, fileRollCall, dirRollCall
203 # Output: stderr: messages indicating missing apps, file, or dirs
204 # Output: returns exit code 0 if nothing missing; 1 otherwise
205 # Depends: bash 5, checkAppFileDir()
206 local missingApps value appMissing missingFiles fileMissing
207 local missingDirs dirMissing
209 #==BEGIN Display errors==
210 #===BEGIN Display Missing Apps===
211 missingApps
="Missing apps :";
212 #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done
213 for key
in "${!appRollCall[@]}"; do
214 value
="${appRollCall[$key]}";
215 if [ "$value" = "false" ]; then
216 #echo "DEBUG:Missing apps: $key => $value";
217 missingApps
="$missingApps""$key ";
221 if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing.
222 echo "$missingApps" 1>&2;
225 #===END Display Missing Apps===
227 #===BEGIN Display Missing Files===
228 missingFiles
="Missing files:";
229 #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done
230 for key
in "${!fileRollCall[@]}"; do
231 value
="${fileRollCall[$key]}";
232 if [ "$value" = "false" ]; then
233 #echo "DEBUG:Missing files: $key => $value";
234 missingFiles
="$missingFiles""$key ";
238 if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing.
239 echo "$missingFiles" 1>&2;
242 #===END Display Missing Files===
244 #===BEGIN Display Missing Directories===
245 missingDirs
="Missing dirs:";
246 #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done
247 for key
in "${!dirRollCall[@]}"; do
248 value
="${dirRollCall[$key]}";
249 if [ "$value" = "false" ]; then
250 #echo "DEBUG:Missing dirs: $key => $value";
251 missingDirs
="$missingDirs""$key ";
255 if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing.
256 echo "$missingDirs" 1>&2;
259 #===END Display Missing Directories===
261 #==END Display errors==
262 #==BEGIN Determine function return code===
263 if [ "$appMissing" == "true" ] ||
[ "$fileMissing" == "true" ] ||
[ "$dirMissing" == "true" ]; then
268 #==END Determine function return code===
269 } # Display missing apps, files, dirs
271 # Desc: Processes arguments provided to script
272 # Usage: processArgs "$@"
273 # Version: 1.0.0 (modified)
274 # Input: "$@" (list of arguments provided to the function)
275 # Output: Sets following variables used by other functions:
276 # opVerbose Indicates verbose mode enable status. (ex: "true", "false")
277 # pathDirOut1 Path to output directory.
278 # pathFileOut1 Path to output file.
279 # opFileOut1_overwrite Indicates whether file pathFileOut1 should be overwritten (ex: "true", "false").
280 # opTs Indicates timestamp mode
281 # opTsWait Indicates timestamp mode with wait option
282 # arrPosArgs Array of remaining positional argments
284 # yell() Displays messages to stderr.
285 # vbm() Displays messsages to stderr if opVerbose set to "true".
286 # showUsage() Displays usage information about parent script.
287 # showVersion() Displays version about parent script.
288 # arrPosArgs Global array for storing non-option positional arguments (i.e. arguments following the `--` option).
289 # External dependencies: bash (5.1.16), echo
291 # [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347
292 # [2]: "Handling positional parameters" (2018-05-12). https://wiki.bash-hackers.org/scripting/posparams
294 # Initialize function
295 vbm
"DEBUG:processArgs function called."
298 while [ ! $# -eq 0 ]; do # While number of arguments ($#) is not (!) equal to (-eq) zero (0).
299 #yell "DEBUG:Starting processArgs while loop." # Debug stderr message. See [1].
300 #yell "DEBUG:Provided arguments are:""$*" # Debug stderr message. See [1].
302 -h |
--help) showUsage
; exit 1;; # Display usage.
303 --version) showVersion
; exit 1;; # Show version
304 -v |
--verbose) opVerbose
="true"; vbm
"DEBUG:Verbose mode enabled.";; # Enable verbose mode. See [1].
305 -o |
--output-file) # Define output file path
306 if [ -f "$2" ]; then # If $2 is file that exists, prompt user to continue to overwrite, set pathFileOut1 to $2, pop $2.
307 yell
"Specified output file $2 already exists. Overwrite? (y/n):"
308 read -r m
; case $m in
309 y | Y |
yes) opFileOut1_overwrite
="true";;
310 n | N | no
) opFileOut1_overwrite
="false";;
311 *) yell
"Invalid selection. Exiting."; exit 1;;
313 if [ "$opFileOut1_overwrite" == "true" ]; then
316 vbm
"DEBUG:Output file pathFileOut1 set to:""$2";
318 yell
"ERORR:Exiting in order to not overwrite output file:""$pathFileOut1";
324 vbm
"DEBUG:Output file pathFileOut1 set to:""$2";
326 -s |
--sign) # sign with gpg
327 opSign
="true"; vbm
"DEBUG:Signing mode enabled.";;
328 -t |
--timestamp) # timestamp with ots
329 opTs
="true"; vbm
"DEBUG:Timestamp mode enabled.";;
330 -T |
--timestamp-wait) # timestamp with ots with wait option
331 opTs
="true"; vbm
"DEBUG:Timestamp mode enabled.";
332 opTsWait
="true"; vbm
"DEBUG:Timestamp wait mode enabled.";;
333 --) # End of all options. See [2].
336 vbm
"DEBUG:adding to arrPosArgs:$arg";
337 arrPosArgs
+=("$arg");
340 -*) showUsage
; yell
"ERROR: Unrecognized option."; exit 1;; # Display usage
343 vbm
"DEBUG:adding to arrPosArgs:$arg";
344 arrPosArgs
+=("$arg");
352 vbm
"DEBUG:processArgs function ended."
353 return 0; # Function finished.
354 } # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.).
356 # Desc: Save stdin lines to array
357 # Input: stdin standard input
358 # arrStdin array for storing stdin lines
359 # Output: arrStdin array for storing stdin lines
360 # Ref/Attrib: [1] https://unix.stackexchange.com/a/484643 Check if no command line arguments and STDIN is empty
363 return 1; # error if file descriptor 0 (stdin) open
365 while read -r line
; do
366 arrStdin
+=("$line"); done;
369 }; # Save stdin to array
371 # Desc: Check that files (specified by positional arguments and
372 # standard input lines) exist and are in the same directory.
373 # Input: arrPosArgs[@] positional arguments
374 # arrStdin[@] standard input lines
375 # Output: return code 0 success
376 # return code 1 failure
377 # arrInFiles list of verified files
378 # Depends: displayMissing(), checkfile();
379 local flagMissing flagDirErr workDir
;
381 # Check that positional arguments are files
382 for elem
in "${arrPosArgs[@]}"; do
383 if checkfile
"$elem"; then
384 arrInFiles
+=("$elem");
390 # Check that stdin lines are files
391 for elem
in "${arrStdin[@]}"; do
392 if checkfile
"$elem"; then
393 arrInFiles
+=("$elem");
399 # Check that all files are in the same directory
400 if [[ "$flagMissing" != "true" ]]; then
401 workDir
="$( dirname "$
( readlink
-f "${arrInFiles[0]}" )" )";
402 for elem
in "${arrInFiles[@]}"; do
403 elem
="$(readlink -f "$elem")"; # full path
404 if [[ "$(dirname "$elem")" != "$workDir" ]]; then
410 # Return non-zero if displayMissing() reports files missing.
411 if [[ "$flagMissing" == "true" ]]; then
412 displayMissing
; return 1; fi;
413 if [[ "$flagDirErr" == "true" ]]; then
414 yell
"ERROR:All files not in same directory.";
417 }; # Check positional arguments
419 # Desc: Check if expected commands available
421 checkapp
date sha256sum
;
422 if [[ $opSign == "true" ]]; then checkapp gpg
; fi;
423 if [[ $opTs == "true" ]]; then checkapp ots
; fi;
425 # Return failure is displayMissing() reports apps missing.
426 if ! displayMissing
; then return 1; else return 0; fi;
427 }; # Check dependencies
429 # Desc: Count and return total number of jobs
432 # Output: stdout integer number of jobs
433 # Depends: Bash 5.1.16
434 # Example: while [[$(count_jobs) -gt 0]]; do echo "Working..."; sleep 1; done;
438 job_count
="$(jobs -r | wc -l | tr -d ' ' )";
439 #yell "DEBUG:job_count:$job_count";
440 if [[ -z $job_count ]]; then job_count
="0"; fi;
442 }; # Return number of background jobs
444 # Input: pathFileOut1 option-specified output file path
445 # appRollCall assoc-array for checkapp(), displayMissing()
446 # fileRollCall assoc-array for checkfile(), displayMissing()
447 # dirRollCall assoc-array for checkdir(), displayMissing()
448 # arrPosArgs array for processArgs()
449 # arrStdin array for processStdin()
450 # arrInFiles array for checkInput()
451 # (args) for processArgs()
452 # (stdin) for processStdin()
453 # Output: file written to $pathSumOut
454 # file written to $pathSigOut
456 local fileSumOut dirOut pathSumOut pathSigOut
;
458 # Check dependencies and input
459 if ! checkDepends
; then die
"FATAL:Missing apps."; fi;
462 vbm
"DEBUG:$(declare -p arrPosArgs)";
463 vbm
"DEBUG:$(declare -p arrStdin)";
464 if ! [[ "${#arrPosArgs[@]}" -ge 1 ||
"${#arrStdin[@]}" -ge 1 ]]; then
465 yell
"ERROR:No positional arguments or stdin lines.";
466 showUsage
; exit 1; fi;
467 if ! checkInput
; then die
"FATAL:Invalid file list."; fi;
468 vbm
"DEBUG:$(declare -p arrInFiles)";
471 if [[ -n "$pathFileOut1" ]]; then
472 pathSumOut
="$pathFileOut1";
474 dirOut
="$( dirname "${arrInFiles[0]}" )";
475 fileSumOut
="$(date +%Y%m%dT%H%M%S%z)..SHA256SUMS";
476 pathSumOut
="$dirOut"/"$fileSumOut";
478 pathSigOut
="$pathSumOut".asc
;
479 for file in "${arrInFiles[@]}"; do
480 sha256sum
"$file" >> "$pathSumOut";
484 ## Sign checksum file.
485 if [[ $opSign == "true" ]]; then
486 yell
"STATUS:Signing checksum file...";
487 try gpg
--detach-sign --armor --output "$pathSigOut" "$pathSumOut" && yell
"STATUS:Checksum created.";
489 ## Timestamp checksum file.
490 if [[ $opTs == "true" ]]; then
491 yell
"STATUS:Timestamping checksum file...";
492 if [[ $opTsWait != "true" ]]; then
493 try ots s
"$pathSumOut" &
494 elif [[ $opTsWait == "true" ]]; then
495 yell
"NOTICE:Waiting for calendar server response in background. (This may take 8 to 24 hours)...";
496 yell
"ADVICE:Do not close or suspend this terminal.";
497 try ots
--wait s
"$pathSumOut" &
500 ## Timestamp checksum signature file.
501 if [[ $opTs == "true" && $opSign == "true" ]]; then
502 yell
"STATUS:Timestamping signature file...";
503 if [[ $opTsWait != "true" ]]; then
504 try ots s
"$pathSigOut" && yell
"STATUS:Timestamp of checksum signature file created.";
505 elif [[ $opTsWait == "true" ]]; then
506 yell
"NOTICE:Waiting for calendar server response in background. (This may take 8 to 24 hours)...";
507 yell
"ADVICE:Do not close or suspend this terminal.";
508 try ots
--wait s
"$pathSigOut" &
512 ## Wait until background jobs (if any) completed
513 for (( n
= 1; "$(count_jobs)" > 0; n
++ )); do
514 if ! [[ $
((n
% 60)) -eq 0 ]]; then
517 printf ".%05ds passed.\n" "$SECONDS";
528 # Author: Steven Baltakatei Sandoval