chore(user/bksum):Bump version to note executable name change
[BK-2020-03.git] / user / bksum
1 #!/usr/bin/env bash
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
8 # Version: 0.0.2
9
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
16
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
20 vbm() {
21 # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true".
22 # Usage: vbm "DEBUG :verbose message here"
23 # Version 0.2.0
24 # Input: arg1: string
25 # vars: opVerbose
26 # Output: stderr
27 # Depends: bash 5.1.16, GNU-coreutils 8.30 (echo, date)
28
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.
32 fi
33
34 # End function
35 return 0; # Function finished.
36 } # Displays message if opVerbose true
37 showUsage() {
38 # Desc: Display script usage information
39 # Usage: showUsage
40 # Version 0.0.2
41 # Input: none
42 # Output: stdout
43 # Depends: GNU-coreutils 8.30 (cat)
44 cat <<'EOF'
45 USAGE:
46 bksum [ options ] [FILE...]
47
48 DESCRIPTION:
49 Creates sha256 checksum of files.
50
51 OPTIONS:
52 -h, --help
53 Display help information.
54 --version
55 Display script version.
56 -v, --verbose
57 Display debugging info.
58 -o, --output-file
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
63 -s, --sign
64 Sign with GnuPG the checksum file.
65 -t, --timestamp
66 Timestamp with OpenTimestamps the checksum file
67 (and GnuPG signature if -s/--sign specified).
68 --
69 Indicate end of options.
70
71 EXAMPLE:
72 bksum file.txt
73 bksum file1.txt file2.txt
74 bksum -v -- file1.txt file2.txt
75 bksum -v -t -- file1.txt file2.txt
76 find . -type f | bksum
77 find . -type f | bksum -v -s -t -- file.txt
78
79 NOTE:
80 If GNU Coreutils 8.32 `sha256sum` used, checksum file
81 can be verified using:
82 sha256sum --check 20220920T2117+0000..SHA256SUM
83 EOF
84 } # Display information on how to use this script.
85 showVersion() {
86 # Desc: Displays script version and license information.
87 # Usage: showVersion
88 # Version: 0.0.2
89 # Input: scriptVersion var containing version string
90 # Output: stdout
91 # Depends: vbm(), yell, GNU-coreutils 8.30
92
93 # Initialize function
94 vbm "DEBUG:showVersion function called."
95
96 cat <<'EOF'
97 bksum 0.0.1
98 Copyright (C) 2022 Steven Baltakatei Sandoval
99 License GPLv3: GNU GPL version 3
100 This is free software; you are free to change and redistribute it.
101 There is NO WARRANTY, to the extent permitted by law.
102
103 EOF
104
105 # End function
106 vbm "DEBUG:showVersion function ended."
107 return 0; # Function finished.
108 } # Display script version.
109 checkapp() {
110 # Desc: If arg is a command, save result in assoc array 'appRollCall'
111 # Usage: checkapp arg1 arg2 arg3 ...
112 # Version: 0.1.1
113 # Input: global assoc. array 'appRollCall'
114 # Output: adds/updates key(value) to global assoc array 'appRollCall'
115 # Depends: bash 5.0.3
116 local returnState
117
118 #===Process Args===
119 for arg in "$@"; do
120 if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command
121 appRollCall[$arg]="true";
122 if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
123 else
124 appRollCall[$arg]="false"; returnState="false";
125 fi;
126 done;
127
128 #===Determine function return code===
129 if [ "$returnState" = "true" ]; then
130 return 0;
131 else
132 return 1;
133 fi;
134 } # Check that app exists
135 checkfile() {
136 # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
137 # Usage: checkfile arg1 arg2 arg3 ...
138 # Version: 0.1.2
139 # Input: global assoc. array 'fileRollCall'
140 # Output: adds/updates key(value) to global assoc array 'fileRollCall';
141 # Output: returns 0 if app found, 1 otherwise
142 # Depends: bash 5.0.3
143 local returnState
144
145 #===Process Args===
146 for arg in "$@"; do
147 if [ -f "$arg" ]; then
148 fileRollCall["$arg"]="true";
149 if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
150 elif [ -z "$arg" ]; then
151 fileRollCall["(no name)"]="false"; returnState="false";
152 else
153 fileRollCall["$arg"]="false"; returnState="false";
154 fi;
155 done;
156
157 #===Determine function return code===
158 if [ "$returnState" = "true" ]; then
159 return 0;
160 else
161 return 1;
162 fi;
163 } # Check that file exists
164 checkdir() {
165 # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
166 # Usage: checkdir arg1 arg2 arg3 ...
167 # Version 0.1.2
168 # Input: global assoc. array 'dirRollCall'
169 # Output: adds/updates key(value) to global assoc array 'dirRollCall';
170 # Output: returns 0 if all args are dirs; 1 otherwise
171 # Depends: Bash 5.0.3
172 local returnState
173
174 #===Process Args===
175 for arg in "$@"; do
176 if [ -z "$arg" ]; then
177 dirRollCall["(Unspecified Dirname(s))"]="false"; returnState="false";
178 elif [ -d "$arg" ]; then
179 dirRollCall["$arg"]="true";
180 if ! [ "$returnState" = "false" ]; then returnState="true"; fi
181 else
182 dirRollCall["$arg"]="false"; returnState="false";
183 fi
184 done
185
186 #===Determine function return code===
187 if [ "$returnState" = "true" ]; then
188 return 0;
189 else
190 return 1;
191 fi
192 } # Check that dir exists
193 displayMissing() {
194 # Desc: Displays missing apps, files, and dirs
195 # Usage: displayMissing
196 # Version 1.0.0
197 # Input: associative arrays: appRollCall, fileRollCall, dirRollCall
198 # Output: stderr: messages indicating missing apps, file, or dirs
199 # Output: returns exit code 0 if nothing missing; 1 otherwise
200 # Depends: bash 5, checkAppFileDir()
201 local missingApps value appMissing missingFiles fileMissing
202 local missingDirs dirMissing
203
204 #==BEGIN Display errors==
205 #===BEGIN Display Missing Apps===
206 missingApps="Missing apps :";
207 #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done
208 for key in "${!appRollCall[@]}"; do
209 value="${appRollCall[$key]}";
210 if [ "$value" = "false" ]; then
211 #echo "DEBUG:Missing apps: $key => $value";
212 missingApps="$missingApps""$key ";
213 appMissing="true";
214 fi;
215 done;
216 if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing.
217 echo "$missingApps" 1>&2;
218 fi;
219 unset value;
220 #===END Display Missing Apps===
221
222 #===BEGIN Display Missing Files===
223 missingFiles="Missing files:";
224 #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done
225 for key in "${!fileRollCall[@]}"; do
226 value="${fileRollCall[$key]}";
227 if [ "$value" = "false" ]; then
228 #echo "DEBUG:Missing files: $key => $value";
229 missingFiles="$missingFiles""$key ";
230 fileMissing="true";
231 fi;
232 done;
233 if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing.
234 echo "$missingFiles" 1>&2;
235 fi;
236 unset value;
237 #===END Display Missing Files===
238
239 #===BEGIN Display Missing Directories===
240 missingDirs="Missing dirs:";
241 #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done
242 for key in "${!dirRollCall[@]}"; do
243 value="${dirRollCall[$key]}";
244 if [ "$value" = "false" ]; then
245 #echo "DEBUG:Missing dirs: $key => $value";
246 missingDirs="$missingDirs""$key ";
247 dirMissing="true";
248 fi;
249 done;
250 if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing.
251 echo "$missingDirs" 1>&2;
252 fi;
253 unset value;
254 #===END Display Missing Directories===
255
256 #==END Display errors==
257 #==BEGIN Determine function return code===
258 if [ "$appMissing" == "true" ] || [ "$fileMissing" == "true" ] || [ "$dirMissing" == "true" ]; then
259 return 1;
260 else
261 return 0;
262 fi
263 #==END Determine function return code===
264 } # Display missing apps, files, dirs
265 processArgs() {
266 # Desc: Processes arguments provided to script
267 # Usage: processArgs "$@"
268 # Version: 1.0.0 (modified)
269 # Input: "$@" (list of arguments provided to the function)
270 # Output: Sets following variables used by other functions:
271 # opVerbose Indicates verbose mode enable status. (ex: "true", "false")
272 # pathDirOut1 Path to output directory.
273 # pathFileOut1 Path to output file.
274 # opFileOut1_overwrite Indicates whether file pathFileOut1 should be overwritten (ex: "true", "false").
275 # arrPosArgs Array of remaining positional argments
276 # Depends:
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 # arrPosArgs Global array for storing non-option positional arguments (i.e. arguments following the `--` option).
282 # External dependencies: bash (5.1.16), echo
283 # Ref./Attrib.:
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
286
287 # Initialize function
288 vbm "DEBUG:processArgs function called."
289
290 # Perform work
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].
294 case "$1" in
295 -h | --help) showUsage; exit 1;; # Display usage.
296 --version) showVersion; exit 1;; # Show version
297 -v | --verbose) opVerbose="true"; vbm "DEBUG:Verbose mode enabled.";; # Enable verbose mode. See [1].
298 -o | --output-file) # Define output file path
299 if [ -f "$2" ]; then # If $2 is file that exists, prompt user to continue to overwrite, set pathFileOut1 to $2, pop $2.
300 yell "Specified output file $2 already exists. Overwrite? (y/n):"
301 read -r m; case $m in
302 y | Y | yes) opFileOut1_overwrite="true";;
303 n | N | no) opFileOut1_overwrite="false";;
304 *) yell "Invalid selection. Exiting."; exit 1;;
305 esac
306 if [ "$opFileOut1_overwrite" == "true" ]; then
307 pathFileOut1="$2";
308 shift;
309 vbm "DEBUG:Output file pathFileOut1 set to:""$2";
310 else
311 yell "ERORR:Exiting in order to not overwrite output file:""$pathFileOut1";
312 exit 1;
313 fi
314 else
315 pathFileOut1="$2";
316 shift;
317 vbm "DEBUG:Output file pathFileOut1 set to:""$2";
318 fi ;;
319 -s | --sign) # sign with gpg
320 opSign="true"; vbm "DEBUG:Signing mode enabled.";;
321 -t | --timestamp) # timestamp with ots
322 opTimestamp="true"; vbm "DEBUG:Timestamp mode enabled.";;
323 --) # End of all options. See [2].
324 shift;
325 for arg in "$@"; do
326 vbm "DEBUG:adding to arrPosArgs:$arg";
327 arrPosArgs+=("$arg");
328 done;
329 break;;
330 -*) showUsage; yell "ERROR: Unrecognized option."; exit 1;; # Display usage
331 *)
332 for arg in "$@"; do
333 vbm "DEBUG:adding to arrPosArgs:$arg";
334 arrPosArgs+=("$arg");
335 done;
336 break;;
337 esac;
338 shift;
339 done;
340
341 # End function
342 vbm "DEBUG:processArgs function ended."
343 return 0; # Function finished.
344 } # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.).
345 processStdin() {
346 # Desc: Save stdin lines to array
347 # Input: stdin standard input
348 # arrStdin array for storing stdin lines
349 # Output: arrStdin array for storing stdin lines
350 # Ref/Attrib: [1] https://unix.stackexchange.com/a/484643 Check if no command line arguments and STDIN is empty
351
352 if [[ -t 0 ]]; then
353 return 1; # error if file descriptor 0 (stdin) open
354 else
355 while read -r line; do
356 arrStdin+=("$line"); done;
357 return 0;
358 fi;
359 }; # Save stdin to array
360 checkInput() {
361 # Desc: Check that files (specified by positional arguments and
362 # standard input lines) exist and are in the same directory.
363 # Input: arrPosArgs[@] positional arguments
364 # arrStdin[@] standard input lines
365 # Output: return code 0 success
366 # return code 1 failure
367 # arrInFiles list of verified files
368 # Depends: displayMissing(), checkfile();
369 local flagMissing flagDirErr workDir;
370
371 # Check that positional arguments are files
372 for elem in "${arrPosArgs[@]}"; do
373 if checkfile "$elem"; then
374 arrInFiles+=("$elem");
375 else
376 flagMissing="true";
377 fi;
378 done;
379
380 # Check that stdin lines are files
381 for elem in "${arrStdin[@]}"; do
382 if checkfile "$elem"; then
383 arrInFiles+=("$elem");
384 else
385 flagMissing="true";
386 fi;
387 done;
388
389 # Check that all files are in the same directory
390 if [[ "$flagMissing" != "true" ]]; then
391 workDir="$( dirname "$( readlink -f "${arrInFiles[0]}" )" )";
392 for elem in "${arrInFiles[@]}"; do
393 elem="$(readlink -f "$elem")"; # full path
394 if [[ "$(dirname "$elem")" != "$workDir" ]]; then
395 flagDirErr="true";
396 fi;
397 done;
398 fi;
399
400 # Return non-zero if displayMissing() reports files missing.
401 if [[ "$flagMissing" == "true" ]]; then
402 displayMissing; return 1; fi;
403 if [[ "$flagDirErr" == "true" ]]; then
404 yell "ERROR:All files not in same directory.";
405 return 1; fi;
406 return 0;
407 }; # Check positional arguments
408 checkDepends() {
409 # Desc: Check if expected commands available
410
411 checkapp date sha256sum;
412 if [[ $opSign == "true" ]]; then checkapp gpg; fi;
413 if [[ $opTimestamp == "true" ]]; then checkapp ots; fi;
414
415 # Return failure is displayMissing() reports apps missing.
416 if ! displayMissing; then return 1; else return 0; fi;
417 }; # Check dependencies
418 main() {
419 # Input: pathFileOut1 option-specified output file path
420 # appRollCall assoc-array for checkapp(), displayMissing()
421 # fileRollCall assoc-array for checkfile(), displayMissing()
422 # dirRollCall assoc-array for checkdir(), displayMissing()
423 # arrPosArgs array for processArgs()
424 # arrStdin array for processStdin()
425 # arrInFiles array for checkInput()
426 # (args) for processArgs()
427 # (stdin) for processStdin()
428 # Output: file written to $pathSumOut
429 # file written to $pathSigOut
430 #
431 local fileSumOut dirOut pathSumOut pathSigOut;
432
433 # Check dependencies and input
434 if ! checkDepends; then die "FATAL:Missing apps."; fi;
435 processArgs "$@";
436 processStdin;
437 vbm "DEBUG:$(declare -p arrPosArgs)";
438 vbm "DEBUG:$(declare -p arrStdin)";
439 if ! [[ "${#arrPosArgs[@]}" -ge 1 || "${#arrStdin[@]}" -ge 1 ]]; then
440 yell "ERROR:No positional arguments or stdin lines.";
441 showUsage; exit 1; fi;
442 if ! checkInput; then die "FATAL:Invalid file list."; fi;
443 vbm "DEBUG:$(declare -p arrInFiles)";
444
445 # Do work
446 if [[ -n "$pathFileOut1" ]]; then
447 pathSumOut="$pathFileOut1";
448 else
449 dirOut="$( dirname "${arrInFiles[0]}" )";
450 fileSumOut="$(date +%Y%m%dT%H%M%S%z)..SHA256SUMS";
451 pathSumOut="$dirOut"/"$fileSumOut";
452 fi;
453 pathSigOut="$pathSumOut".asc;
454 for file in "${arrInFiles[@]}"; do
455 sha256sum "$file" >> "$pathSumOut";
456 done;
457
458 # Do optional work
459 ## Sign checksum file.
460 if [[ $opSign == "true" ]]; then
461 try gpg --detach-sign --armor --output "$pathSigOut" "$pathSumOut";
462 fi;
463 ## Timestamp checksum file.
464 if [[ $opTimestamp == "true" ]]; then
465 try ots s "$pathSumOut"; fi;
466
467 ## Timestamp checksum signature file.
468 if [[ $opTimestamp == "true" && $opSign == "true" ]]; then
469 try ots s "$pathSigOut";
470 fi;
471
472 return 0;
473 }; # main program
474
475 main "$@";
476
477 # Author: Steven Baltakatei Sandoval
478 # License: GPLv3+