summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-03 19:36:55 +0300
committerPaul Buetow <paul@buetow.org>2026-06-03 19:36:55 +0300
commitda8b0d3ba6d27c17f511da0284db8ae44e5e906b (patch)
tree9637e0ea16d64752afc3934fc77244819744a296
parent53015df1627c035210f21845cacd401fae094bbf (diff)
Add output verbosity controls for zi0
-rw-r--r--README.md7
-rwxr-xr-xbin/photoalbum71
-rwxr-xr-xsrc/photoalbum.sh71
-rwxr-xr-xtests/cli.sh194
4 files changed, 319 insertions, 24 deletions
diff --git a/README.md b/README.md
index 62fc09f..7b471a4 100644
--- a/README.md
+++ b/README.md
@@ -84,6 +84,13 @@ The following long options override config values:
`--dry-run` accepts the same override options as `--generate`. `--clean` accepts
the same override options, but only `--dist` changes what it removes.
+Output is human-readable by default and reports routine generation progress.
+Use `--quiet` to suppress routine progress while still writing errors to stderr.
+Use `--verbose` for extra diagnostics, including the selected config file,
+effective paths, skipped existing files, staging output directory, and tarball
+decisions. If `--quiet` and `--verbose` are repeated or combined, the last output
+flag wins.
+
## Example usage
1. Run `photoalbum --init`.
diff --git a/bin/photoalbum b/bin/photoalbum
index 3e07fdb..f6123d5 100755
--- a/bin/photoalbum
+++ b/bin/photoalbum
@@ -6,6 +6,7 @@ set -euo pipefail
declare -r VERSION='0.6.0'
declare -r DEFAULTRC="${PHOTOALBUM_DEFAULT_RC:-/etc/default/photoalbum}"
+PHOTOALBUM_OUTPUT_MODE="${PHOTOALBUM_OUTPUT_MODE:-normal}"
usage() {
cat - <<USAGE >&2
@@ -29,9 +30,35 @@ usage() {
--no-shuffle
--tarball
--no-tarball
+ --verbose
+ --quiet
USAGE
}
+output_is_quiet() {
+ [ "$PHOTOALBUM_OUTPUT_MODE" = quiet ]
+}
+
+output_is_verbose() {
+ [ "$PHOTOALBUM_OUTPUT_MODE" = verbose ]
+}
+
+log_info() {
+ if ! output_is_quiet; then
+ printf '%s\n' "$*"
+ fi
+}
+
+log_verbose() {
+ if output_is_verbose; then
+ printf 'Verbose: %s\n' "$*"
+ fi
+}
+
+log_warning() {
+ printf 'WARNING: %s\n' "$*" >&2
+}
+
resolve_default_rc_file() {
local source_root
@@ -91,7 +118,7 @@ init_config() {
"$rc_file"
fi
- echo "Created ./$rc_file"
+ log_info "Created ./$rc_file"
}
imagemagick() {
@@ -114,7 +141,7 @@ tarball() {
find "$DIST_DIR" -maxdepth 1 -type f -name '*.tar' -delete
base=$(basename "$INCOMING_DIR")
- echo "Creating tarball $(_display_path "$DIST_DIR/$tarball_name")" \
+ log_info "Creating tarball $(_display_path "$DIST_DIR/$tarball_name")" \
"from $INCOMING_DIR"
(
cd "$(dirname "$INCOMING_DIR")"
@@ -248,7 +275,7 @@ template() {
local thumbs_dir_html
local title_html
- echo "Generating $(_display_path "$dist_html")/$html"
+ log_info "Generating $(_display_path "$dist_html")/$html"
mkdir -p "$dist_html"
animation_class_html=$(_html_escape "${animation_class:-}")
@@ -305,10 +332,11 @@ cleanphotos() {
continue
fi
- echo "Cleaning up $(_display_path "$photo")"
+ log_info "Cleaning up $(_display_path "$photo")"
for sub in thumbs blurs photos; do
if [ -f "$DIST_DIR/$sub/$basename" ]; then
- rm -v "$DIST_DIR/$sub/$basename"
+ rm -f "$DIST_DIR/$sub/$basename"
+ log_info "removed '$(_display_path "$DIST_DIR/$sub/$basename")'"
fi
done
done < <(find "$DIST_DIR/photos" -maxdepth 1 -type f)
@@ -351,7 +379,7 @@ warn_unsupported_incoming_files() {
while IFS= read -r file; do
if ! is_supported_image_file "$file"; then
- echo "WARNING: Ignoring unsupported incoming file: $file" >&2
+ log_warning "Ignoring unsupported incoming file: $file"
fi
done < <(find "$INCOMING_DIR" -maxdepth 1 -type f -printf '%f\n' | sort)
}
@@ -367,11 +395,11 @@ scalephotos() {
mkdir -p "$dirname"
if [ -f "$destphoto" ]; then
- echo "Already exists: $(_display_path "$destphoto")"
+ log_verbose "Skipped existing photo $(_display_path "$destphoto")"
continue
fi
- echo "Processing $photo to $(_display_path "$destphoto")"
+ log_info "Processing $photo to $(_display_path "$destphoto")"
if [ -n "${HEIGHT:-}" ]; then
# Scale down size.
imagemagick \
@@ -505,12 +533,13 @@ albumhtml() {
if [[ -f "$DIST_DIR/$thumbs_dir/$photo" \
&& -f "$DIST_DIR/$blurs_dir/$photo" ]]; then
- echo "Already exists: $(_display_path "$DIST_DIR/$thumbs_dir/$photo") and" \
+ log_verbose "Skipped existing thumb and blur" \
+ "$(_display_path "$DIST_DIR/$thumbs_dir/$photo") and" \
"$(_display_path "$DIST_DIR/$blurs_dir/$photo")"
else
dirname="$DIST_DIR/$thumbs_dir"
mkdir -p "$dirname"
- echo "Creating thumb $(_display_path "$DIST_DIR/$thumbs_dir/$photo")"
+ log_info "Creating thumb $(_display_path "$DIST_DIR/$thumbs_dir/$photo")"
# Double the height, as CSS scales images based on boxing too.
height=$(( THUMBHEIGHT * 2 ))
imagemagick \
@@ -520,7 +549,7 @@ albumhtml() {
dirname="$DIST_DIR/$blurs_dir"
mkdir -p "$dirname"
- echo "Creating blur $(_display_path "$DIST_DIR/$blurs_dir/$photo")"
+ log_info "Creating blur $(_display_path "$DIST_DIR/$blurs_dir/$photo")"
imagemagick \
"$DIST_DIR/$thumbs_dir/$photo" \
-flip \
@@ -696,6 +725,10 @@ generate() {
if [ "${TARBALL_INCLUDE:-no}" = yes ]; then
tarball_name=$(generated_tarball_name)
+ log_verbose \
+ "Tarball enabled; planned archive: $(_display_path "$DIST_DIR/$tarball_name")"
+ else
+ log_verbose 'Tarball disabled; no archive will be created'
fi
warn_unsupported_incoming_files
@@ -897,6 +930,8 @@ generate_staged() {
local -i status=0
staging_dir=$(generation_staging_dir "$final_dist")
+ log_verbose "Effective output directory: $final_dist"
+ log_verbose "Generation staging directory: $staging_dir"
PHOTOALBUM_ACTIVE_STAGING_DIR="$staging_dir"
trap cleanup_generation_staging_dir EXIT
trap 'cleanup_generation_staging_dir; exit 130' INT
@@ -1238,6 +1273,12 @@ main() {
cli_tarball_include='no'
has_config_overrides='yes'
;;
+ --verbose)
+ PHOTOALBUM_OUTPUT_MODE=verbose
+ ;;
+ --quiet)
+ PHOTOALBUM_OUTPUT_MODE=quiet
+ ;;
--version|--init|--clean|--generate|--dry-run)
if [ -n "$action" ]; then
usage
@@ -1282,11 +1323,19 @@ main() {
apply_cli_overrides
PHOTOALBUM_CONFIG_SOURCE="$rc_file"
export PHOTOALBUM_CONFIG_SOURCE
+ log_verbose "Selected config file: $rc_file"
+ log_verbose "Effective incoming directory: ${INCOMING_DIR:-}"
+ log_verbose "Effective output directory: ${DIST_DIR:-}"
+ log_verbose "Effective template directory: ${TEMPLATE_DIR:-}"
+ log_verbose "Effective tarball setting: ${TARBALL_INCLUDE:-no}"
case "$action" in
--clean)
if [ -d "$DIST_DIR" ]; then
+ log_info "Cleaning $DIST_DIR"
rm -rf "$DIST_DIR"
+ else
+ log_verbose "Output directory does not exist: $DIST_DIR"
fi
;;
--generate)
diff --git a/src/photoalbum.sh b/src/photoalbum.sh
index 5179c5c..88b6cfb 100755
--- a/src/photoalbum.sh
+++ b/src/photoalbum.sh
@@ -6,6 +6,7 @@ set -euo pipefail
declare -r VERSION='PHOTOALBUMVERSION'
declare -r DEFAULTRC="${PHOTOALBUM_DEFAULT_RC:-/etc/default/photoalbum}"
+PHOTOALBUM_OUTPUT_MODE="${PHOTOALBUM_OUTPUT_MODE:-normal}"
usage() {
cat - <<USAGE >&2
@@ -29,9 +30,35 @@ usage() {
--no-shuffle
--tarball
--no-tarball
+ --verbose
+ --quiet
USAGE
}
+output_is_quiet() {
+ [ "$PHOTOALBUM_OUTPUT_MODE" = quiet ]
+}
+
+output_is_verbose() {
+ [ "$PHOTOALBUM_OUTPUT_MODE" = verbose ]
+}
+
+log_info() {
+ if ! output_is_quiet; then
+ printf '%s\n' "$*"
+ fi
+}
+
+log_verbose() {
+ if output_is_verbose; then
+ printf 'Verbose: %s\n' "$*"
+ fi
+}
+
+log_warning() {
+ printf 'WARNING: %s\n' "$*" >&2
+}
+
resolve_default_rc_file() {
local source_root
@@ -91,7 +118,7 @@ init_config() {
"$rc_file"
fi
- echo "Created ./$rc_file"
+ log_info "Created ./$rc_file"
}
imagemagick() {
@@ -114,7 +141,7 @@ tarball() {
find "$DIST_DIR" -maxdepth 1 -type f -name '*.tar' -delete
base=$(basename "$INCOMING_DIR")
- echo "Creating tarball $(_display_path "$DIST_DIR/$tarball_name")" \
+ log_info "Creating tarball $(_display_path "$DIST_DIR/$tarball_name")" \
"from $INCOMING_DIR"
(
cd "$(dirname "$INCOMING_DIR")"
@@ -248,7 +275,7 @@ template() {
local thumbs_dir_html
local title_html
- echo "Generating $(_display_path "$dist_html")/$html"
+ log_info "Generating $(_display_path "$dist_html")/$html"
mkdir -p "$dist_html"
animation_class_html=$(_html_escape "${animation_class:-}")
@@ -305,10 +332,11 @@ cleanphotos() {
continue
fi
- echo "Cleaning up $(_display_path "$photo")"
+ log_info "Cleaning up $(_display_path "$photo")"
for sub in thumbs blurs photos; do
if [ -f "$DIST_DIR/$sub/$basename" ]; then
- rm -v "$DIST_DIR/$sub/$basename"
+ rm -f "$DIST_DIR/$sub/$basename"
+ log_info "removed '$(_display_path "$DIST_DIR/$sub/$basename")'"
fi
done
done < <(find "$DIST_DIR/photos" -maxdepth 1 -type f)
@@ -351,7 +379,7 @@ warn_unsupported_incoming_files() {
while IFS= read -r file; do
if ! is_supported_image_file "$file"; then
- echo "WARNING: Ignoring unsupported incoming file: $file" >&2
+ log_warning "Ignoring unsupported incoming file: $file"
fi
done < <(find "$INCOMING_DIR" -maxdepth 1 -type f -printf '%f\n' | sort)
}
@@ -367,11 +395,11 @@ scalephotos() {
mkdir -p "$dirname"
if [ -f "$destphoto" ]; then
- echo "Already exists: $(_display_path "$destphoto")"
+ log_verbose "Skipped existing photo $(_display_path "$destphoto")"
continue
fi
- echo "Processing $photo to $(_display_path "$destphoto")"
+ log_info "Processing $photo to $(_display_path "$destphoto")"
if [ -n "${HEIGHT:-}" ]; then
# Scale down size.
imagemagick \
@@ -505,12 +533,13 @@ albumhtml() {
if [[ -f "$DIST_DIR/$thumbs_dir/$photo" \
&& -f "$DIST_DIR/$blurs_dir/$photo" ]]; then
- echo "Already exists: $(_display_path "$DIST_DIR/$thumbs_dir/$photo") and" \
+ log_verbose "Skipped existing thumb and blur" \
+ "$(_display_path "$DIST_DIR/$thumbs_dir/$photo") and" \
"$(_display_path "$DIST_DIR/$blurs_dir/$photo")"
else
dirname="$DIST_DIR/$thumbs_dir"
mkdir -p "$dirname"
- echo "Creating thumb $(_display_path "$DIST_DIR/$thumbs_dir/$photo")"
+ log_info "Creating thumb $(_display_path "$DIST_DIR/$thumbs_dir/$photo")"
# Double the height, as CSS scales images based on boxing too.
height=$(( THUMBHEIGHT * 2 ))
imagemagick \
@@ -520,7 +549,7 @@ albumhtml() {
dirname="$DIST_DIR/$blurs_dir"
mkdir -p "$dirname"
- echo "Creating blur $(_display_path "$DIST_DIR/$blurs_dir/$photo")"
+ log_info "Creating blur $(_display_path "$DIST_DIR/$blurs_dir/$photo")"
imagemagick \
"$DIST_DIR/$thumbs_dir/$photo" \
-flip \
@@ -696,6 +725,10 @@ generate() {
if [ "${TARBALL_INCLUDE:-no}" = yes ]; then
tarball_name=$(generated_tarball_name)
+ log_verbose \
+ "Tarball enabled; planned archive: $(_display_path "$DIST_DIR/$tarball_name")"
+ else
+ log_verbose 'Tarball disabled; no archive will be created'
fi
warn_unsupported_incoming_files
@@ -897,6 +930,8 @@ generate_staged() {
local -i status=0
staging_dir=$(generation_staging_dir "$final_dist")
+ log_verbose "Effective output directory: $final_dist"
+ log_verbose "Generation staging directory: $staging_dir"
PHOTOALBUM_ACTIVE_STAGING_DIR="$staging_dir"
trap cleanup_generation_staging_dir EXIT
trap 'cleanup_generation_staging_dir; exit 130' INT
@@ -1238,6 +1273,12 @@ main() {
cli_tarball_include='no'
has_config_overrides='yes'
;;
+ --verbose)
+ PHOTOALBUM_OUTPUT_MODE=verbose
+ ;;
+ --quiet)
+ PHOTOALBUM_OUTPUT_MODE=quiet
+ ;;
--version|--init|--clean|--generate|--dry-run)
if [ -n "$action" ]; then
usage
@@ -1282,11 +1323,19 @@ main() {
apply_cli_overrides
PHOTOALBUM_CONFIG_SOURCE="$rc_file"
export PHOTOALBUM_CONFIG_SOURCE
+ log_verbose "Selected config file: $rc_file"
+ log_verbose "Effective incoming directory: ${INCOMING_DIR:-}"
+ log_verbose "Effective output directory: ${DIST_DIR:-}"
+ log_verbose "Effective template directory: ${TEMPLATE_DIR:-}"
+ log_verbose "Effective tarball setting: ${TARBALL_INCLUDE:-no}"
case "$action" in
--clean)
if [ -d "$DIST_DIR" ]; then
+ log_info "Cleaning $DIST_DIR"
rm -rf "$DIST_DIR"
+ else
+ log_verbose "Output directory does not exist: $DIST_DIR"
fi
;;
--generate)
diff --git a/tests/cli.sh b/tests/cli.sh
index 3a6be1a..939600e 100755
--- a/tests/cli.sh
+++ b/tests/cli.sh
@@ -502,6 +502,181 @@ test_generate_cli_no_tarball_overrides_config() {
test::teardown
}
+test_default_output_reports_routine_progress() {
+ local config_file
+ local fake_bin
+ local output
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+
+ test::install_fake_imagemagick "$fake_bin"
+ PATH="$fake_bin:$PATH" \
+ test::generate_fixture_images "$TEST_TMPDIR/incoming"
+ test::write_album_config \
+ "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
+ 'Default output album' 40
+
+ output=$(
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate
+ )
+
+ test::assert_contains 'Processing 01-landscape.jpg to' "$output"
+ test::assert_contains 'Generating ' "$output"
+ test::assert_contains 'Creating thumb ' "$output"
+ test::assert_not_contains 'Verbose:' "$output"
+ test::teardown
+}
+
+test_quiet_output_suppresses_routine_progress() {
+ local config_file
+ local fake_bin
+ local output
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+
+ test::install_fake_imagemagick "$fake_bin"
+ PATH="$fake_bin:$PATH" \
+ test::generate_fixture_images "$TEST_TMPDIR/incoming"
+ test::write_album_config \
+ "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
+ 'Quiet output album' 40
+
+ output=$(
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --quiet --generate
+ )
+
+ test::assert_not_contains 'Processing ' "$output"
+ test::assert_not_contains 'Generating ' "$output"
+ test::assert_not_contains 'Creating thumb ' "$output"
+ test::assert_file_exists "$TEST_TMPDIR/dist/photoalbum.json"
+ test::teardown
+}
+
+test_quiet_output_keeps_errors_on_stderr() {
+ local config_file
+ local output_file
+ local stderr_file
+ local stdout
+ local stderr
+ local -i status=0
+
+ test::setup
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+ output_file="$TEST_TMPDIR/stdout"
+ stderr_file="$TEST_TMPDIR/stderr"
+ test::write_album_config \
+ "$config_file" "$TEST_TMPDIR/missing" "$TEST_TMPDIR/dist" \
+ 'Quiet error album' 40
+
+ set +e
+ (
+ cd "$TEST_TMPDIR"
+ "$TEST_PHOTOALBUM" --quiet --generate \
+ > "$output_file" 2> "$stderr_file"
+ )
+ status=$?
+ set -e
+
+ if (( status == 0 )); then
+ echo 'FAIL: expected quiet generation to fail' >&2
+ exit 1
+ fi
+
+ stdout=$(<"$output_file")
+ stderr=$(<"$stderr_file")
+ test "$stdout" = ''
+ test::assert_contains \
+ "ERROR: You have to create $TEST_TMPDIR/missing first" \
+ "$stderr"
+ test::teardown
+}
+
+test_verbose_output_reports_processing_decisions() {
+ local config_file
+ local fake_bin
+ local output
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+
+ test::install_fake_imagemagick "$fake_bin"
+ PATH="$fake_bin:$PATH" \
+ test::generate_fixture_images "$TEST_TMPDIR/incoming"
+ test::write_album_config \
+ "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
+ 'Verbose output album' 40
+
+ (
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate >/dev/null
+ )
+ output=$(
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --verbose --generate
+ )
+
+ test::assert_contains 'Verbose: Selected config file: ./photoalbum.conf' \
+ "$output"
+ test::assert_contains "Verbose: Effective incoming directory: $TEST_TMPDIR/incoming" \
+ "$output"
+ test::assert_contains "Verbose: Effective output directory: $TEST_TMPDIR/dist" \
+ "$output"
+ test::assert_contains \
+ "Verbose: Effective template directory: $TEST_REPO_ROOT/share/templates/default" \
+ "$output"
+ test::assert_contains \
+ 'Verbose: Tarball disabled; no archive will be created' \
+ "$output"
+ test::assert_contains \
+ "Verbose: Skipped existing photo $TEST_TMPDIR/dist/photos/01-landscape.jpg" \
+ "$output"
+ test::assert_contains 'Verbose: Skipped existing thumb and blur' "$output"
+ test::teardown
+}
+
+test_repeated_output_flags_use_last_value() {
+ local config_file
+ local fake_bin
+ local quiet_last_output
+ local verbose_last_output
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+
+ test::install_fake_imagemagick "$fake_bin"
+ PATH="$fake_bin:$PATH" \
+ test::generate_fixture_images "$TEST_TMPDIR/incoming"
+ test::write_album_config \
+ "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
+ 'Repeated output flags album' 40
+
+ verbose_last_output=$(
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" \
+ --quiet --verbose --generate
+ )
+ quiet_last_output=$(
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" \
+ --verbose --quiet --generate
+ )
+
+ test::assert_contains 'Verbose: Selected config file: ./photoalbum.conf' \
+ "$verbose_last_output"
+ test::assert_not_contains 'Verbose:' "$quiet_last_output"
+ test::assert_not_contains 'Processing ' "$quiet_last_output"
+ test::assert_not_contains 'Generating ' "$quiet_last_output"
+ test::teardown
+}
+
test_dry_run_reports_cli_overrides_without_writes() {
local config_file
local dist_dir
@@ -1366,14 +1541,14 @@ test_generate_preserves_space_filename_without_reprocessing() {
)
second_output=$(
cd "$TEST_TMPDIR"
- PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate
+ PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --verbose --generate
)
test::assert_file_exists "$TEST_TMPDIR/dist/photos/$photo_name"
test::assert_path_absent "$TEST_TMPDIR/dist/photos/a_b.jpg"
test::assert_contains "Processing $photo_name to" "$first_output"
test::assert_contains \
- "Already exists: $TEST_TMPDIR/dist/photos/$photo_name" \
+ "Verbose: Skipped existing photo $TEST_TMPDIR/dist/photos/$photo_name" \
"$second_output"
test::assert_not_contains "Processing $photo_name to" "$second_output"
@@ -1510,6 +1685,21 @@ main() {
'--generate --no-tarball overrides config' \
test_generate_cli_no_tarball_overrides_config
test::run_case \
+ 'default output reports routine progress' \
+ test_default_output_reports_routine_progress
+ test::run_case \
+ '--quiet suppresses routine progress' \
+ test_quiet_output_suppresses_routine_progress
+ test::run_case \
+ '--quiet keeps errors on stderr' \
+ test_quiet_output_keeps_errors_on_stderr
+ test::run_case \
+ '--verbose reports processing decisions' \
+ test_verbose_output_reports_processing_decisions
+ test::run_case \
+ 'repeated output flags use last value' \
+ test_repeated_output_flags_use_last_value
+ test::run_case \
'--dry-run reports CLI overrides without writes' \
test_dry_run_reports_cli_overrides_without_writes
test::run_case \