diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-03 19:57:19 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-03 19:57:19 +0300 |
| commit | 9b3f994be65fe37516c74a60aa158b5bb340b20d (patch) | |
| tree | dc461b452003d25b815a542867a584bc99ee80c9 | |
| parent | 11da40444ce1bf74545f87a4bfe339f61b4660f0 (diff) | |
Add print-config mode for task 0j0
| -rw-r--r-- | README.md | 27 | ||||
| -rwxr-xr-x | bin/photoalbum | 83 | ||||
| -rwxr-xr-x | src/photoalbum.sh | 83 | ||||
| -rwxr-xr-x | tests/cli.sh | 308 |
4 files changed, 480 insertions, 21 deletions
@@ -22,6 +22,7 @@ modern `magick` command and falls back to `convert` when needed. photoalbum --init photoalbum --generate [--config PATH] [OPTIONS] photoalbum --dry-run [--config PATH] [OPTIONS] +photoalbum --print-config [--config PATH] [OPTIONS] photoalbum --clean [--config PATH] [OPTIONS] photoalbum --version ``` @@ -32,14 +33,17 @@ photoalbum --version * `--dry-run` loads the config and overrides, validates the planned generation, and prints the effective paths, image count, tarball plan, and generated file plan without writing output or running ImageMagick or tar. +* `--print-config` loads the config and overrides, validates basic config values, + and prints the effective configuration without writing output, running + ImageMagick, running tar, cleaning, or initializing. * `--clean` removes the configured output directory. * `--version` prints the program version. -* `--config PATH` selects the config file for `--generate`, `--dry-run`, or - `--clean`. +* `--config PATH` selects the config file for `--generate`, `--dry-run`, + `--print-config`, or `--clean`. -When `--config PATH` is not provided, `--generate`, `--dry-run`, and `--clean` -read `./photoalbum.conf`. If the file is missing, run `photoalbum --init` -first. +When `--config PATH` is not provided, `--generate`, `--dry-run`, +`--print-config`, and `--clean` read `./photoalbum.conf`. If the file is +missing, run `photoalbum --init` first. The config file is a Bash file with assignments such as `INCOMING_DIR`, `DIST_DIR`, `TEMPLATE_DIR`, `TITLE`, `HEIGHT`, `THUMBHEIGHT`, `MAXPREVIEWS`, @@ -60,6 +64,14 @@ notes, are ignored with a warning so generation can continue. values that generation would use after applying command-line overrides. Its tarball filename uses `<timestamp>` as a placeholder so the output is stable. +`--print-config` writes stable shell-style assignments to stdout in this order: +`CONFIG_SOURCE`, `INCOMING_DIR`, `DIST_DIR`, `TEMPLATE_DIR`, `TITLE`, `HEIGHT`, +`THUMBHEIGHT`, `MAXPREVIEWS`, `SHUFFLE`, `TARBALL_INCLUDE`, `TARBALL_SUFFIX`, +`TAR_OPTS`, and `ORIGINAL_BASEPATH`. Scalar values use Bash `%q` quoting and +`TAR_OPTS` is normalized to a Bash array assignment, so the output can be parsed +by shell tooling. `--quiet` does not suppress this output, and `--verbose` does +not add human-readable diagnostics to it. + Successful generation writes `photoalbum.json` into the output directory. This metadata records the generator version and timestamp, config source, template directory, supported source image and generated file counts, tarball status, and @@ -81,8 +93,9 @@ The following long options override config values: | `--tarball` | `TARBALL_INCLUDE=yes` | | `--no-tarball` | `TARBALL_INCLUDE=no` | -`--dry-run` accepts the same override options as `--generate`. `--clean` accepts -the same override options, but only `--dist` changes what it removes. +`--dry-run` and `--print-config` accept 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. diff --git a/bin/photoalbum b/bin/photoalbum index a16b660..a388e41 100755 --- a/bin/photoalbum +++ b/bin/photoalbum @@ -13,6 +13,7 @@ usage() { Usage: $0 --generate [--config PATH] [OPTIONS] $0 --dry-run [--config PATH] [OPTIONS] + $0 --print-config [--config PATH] [OPTIONS] $0 --clean [--config PATH] [OPTIONS] $0 --version $0 --init @@ -840,6 +841,44 @@ dry_run() { fi } +print_shell_assignment() { + local -r name="$1"; shift + local -r value="$1"; shift + + printf '%s=%q\n' "$name" "$value" +} + +print_shell_array_assignment() { + local -r name="$1"; shift + local value + + printf '%s=(' "$name" + for value in "$@"; do + printf ' %q' "$value" + done + printf ' )\n' +} + +print_config() { + local -a tar_opts=() + + resolve_tar_opts tar_opts + + print_shell_assignment CONFIG_SOURCE "${PHOTOALBUM_CONFIG_SOURCE:-}" + print_shell_assignment INCOMING_DIR "$INCOMING_DIR" + print_shell_assignment DIST_DIR "$DIST_DIR" + print_shell_assignment TEMPLATE_DIR "$TEMPLATE_DIR" + print_shell_assignment TITLE "$TITLE" + print_shell_assignment HEIGHT "${HEIGHT:-}" + print_shell_assignment THUMBHEIGHT "$THUMBHEIGHT" + print_shell_assignment MAXPREVIEWS "$MAXPREVIEWS" + print_shell_assignment SHUFFLE "${SHUFFLE:-no}" + print_shell_assignment TARBALL_INCLUDE "${TARBALL_INCLUDE:-no}" + print_shell_assignment TARBALL_SUFFIX "${TARBALL_SUFFIX:-.tar}" + print_shell_array_assignment TAR_OPTS "${tar_opts[@]}" + print_shell_assignment ORIGINAL_BASEPATH "${ORIGINAL_BASEPATH:-}" +} + existing_parent_dir() { local -r path="$1"; shift local existing_parent @@ -1216,6 +1255,30 @@ validate_generation_config() { fi } +validate_print_config() { + local required_var + local -a required_vars=( + TITLE + THUMBHEIGHT + MAXPREVIEWS + INCOMING_DIR + DIST_DIR + TEMPLATE_DIR + ) + local -a tar_opts=() + + for required_var in "${required_vars[@]}"; do + require_config_var "$required_var" + done + + validate_optional_positive_integer_config_var HEIGHT + validate_positive_integer_config_var THUMBHEIGHT + validate_positive_integer_config_var MAXPREVIEWS + validate_yes_no_config_var SHUFFLE + validate_yes_no_config_var TARBALL_INCLUDE + resolve_tar_opts tar_opts +} + main() { local action='' local config_file='' @@ -1309,7 +1372,7 @@ main() { --quiet) PHOTOALBUM_OUTPUT_MODE=quiet ;; - --version|--init|--clean|--generate|--dry-run) + --version|--init|--clean|--generate|--dry-run|--print-config) if [ -n "$action" ]; then usage exit 1 @@ -1341,7 +1404,7 @@ main() { init_config ;; - --clean|--generate|--dry-run) + --clean|--generate|--dry-run|--print-config) rc_file="$(resolve_config_file "$config_file")" if [ ! -f "$rc_file" ]; then @@ -1353,11 +1416,13 @@ 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}" + if [ "$action" != --print-config ]; then + 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}" + fi case "$action" in --clean) @@ -1376,6 +1441,10 @@ main() { validate_generation_config no dry_run ;; + --print-config) + validate_print_config + print_config + ;; esac ;; *) diff --git a/src/photoalbum.sh b/src/photoalbum.sh index e2beffb..1f33ea2 100755 --- a/src/photoalbum.sh +++ b/src/photoalbum.sh @@ -13,6 +13,7 @@ usage() { Usage: $0 --generate [--config PATH] [OPTIONS] $0 --dry-run [--config PATH] [OPTIONS] + $0 --print-config [--config PATH] [OPTIONS] $0 --clean [--config PATH] [OPTIONS] $0 --version $0 --init @@ -840,6 +841,44 @@ dry_run() { fi } +print_shell_assignment() { + local -r name="$1"; shift + local -r value="$1"; shift + + printf '%s=%q\n' "$name" "$value" +} + +print_shell_array_assignment() { + local -r name="$1"; shift + local value + + printf '%s=(' "$name" + for value in "$@"; do + printf ' %q' "$value" + done + printf ' )\n' +} + +print_config() { + local -a tar_opts=() + + resolve_tar_opts tar_opts + + print_shell_assignment CONFIG_SOURCE "${PHOTOALBUM_CONFIG_SOURCE:-}" + print_shell_assignment INCOMING_DIR "$INCOMING_DIR" + print_shell_assignment DIST_DIR "$DIST_DIR" + print_shell_assignment TEMPLATE_DIR "$TEMPLATE_DIR" + print_shell_assignment TITLE "$TITLE" + print_shell_assignment HEIGHT "${HEIGHT:-}" + print_shell_assignment THUMBHEIGHT "$THUMBHEIGHT" + print_shell_assignment MAXPREVIEWS "$MAXPREVIEWS" + print_shell_assignment SHUFFLE "${SHUFFLE:-no}" + print_shell_assignment TARBALL_INCLUDE "${TARBALL_INCLUDE:-no}" + print_shell_assignment TARBALL_SUFFIX "${TARBALL_SUFFIX:-.tar}" + print_shell_array_assignment TAR_OPTS "${tar_opts[@]}" + print_shell_assignment ORIGINAL_BASEPATH "${ORIGINAL_BASEPATH:-}" +} + existing_parent_dir() { local -r path="$1"; shift local existing_parent @@ -1216,6 +1255,30 @@ validate_generation_config() { fi } +validate_print_config() { + local required_var + local -a required_vars=( + TITLE + THUMBHEIGHT + MAXPREVIEWS + INCOMING_DIR + DIST_DIR + TEMPLATE_DIR + ) + local -a tar_opts=() + + for required_var in "${required_vars[@]}"; do + require_config_var "$required_var" + done + + validate_optional_positive_integer_config_var HEIGHT + validate_positive_integer_config_var THUMBHEIGHT + validate_positive_integer_config_var MAXPREVIEWS + validate_yes_no_config_var SHUFFLE + validate_yes_no_config_var TARBALL_INCLUDE + resolve_tar_opts tar_opts +} + main() { local action='' local config_file='' @@ -1309,7 +1372,7 @@ main() { --quiet) PHOTOALBUM_OUTPUT_MODE=quiet ;; - --version|--init|--clean|--generate|--dry-run) + --version|--init|--clean|--generate|--dry-run|--print-config) if [ -n "$action" ]; then usage exit 1 @@ -1341,7 +1404,7 @@ main() { init_config ;; - --clean|--generate|--dry-run) + --clean|--generate|--dry-run|--print-config) rc_file="$(resolve_config_file "$config_file")" if [ ! -f "$rc_file" ]; then @@ -1353,11 +1416,13 @@ 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}" + if [ "$action" != --print-config ]; then + 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}" + fi case "$action" in --clean) @@ -1376,6 +1441,10 @@ main() { validate_generation_config no dry_run ;; + --print-config) + validate_print_config + print_config + ;; esac ;; *) diff --git a/tests/cli.sh b/tests/cli.sh index 590afa4..26470e6 100755 --- a/tests/cli.sh +++ b/tests/cli.sh @@ -752,6 +752,288 @@ test_repeated_output_flags_use_last_value() { test::teardown } +test_print_config_reflects_defaults() { + local expected + local output + + test::setup + + output=$( + cd "$TEST_TMPDIR" + "$TEST_PHOTOALBUM" \ + --print-config \ + --config "$TEST_REPO_ROOT/src/photoalbum.default.conf" + ) + expected=$(cat <<EOF +CONFIG_SOURCE=$TEST_REPO_ROOT/src/photoalbum.default.conf +INCOMING_DIR=$TEST_TMPDIR/incoming +DIST_DIR=$TEST_TMPDIR/dist +TEMPLATE_DIR=/usr/share/photoalbum/templates/default +TITLE=A\\ simple\\ Photoalbum +HEIGHT=1200 +THUMBHEIGHT=300 +MAXPREVIEWS=40 +SHUFFLE=no +TARBALL_INCLUDE=yes +TARBALL_SUFFIX=.tar +TAR_OPTS=( -c ) +ORIGINAL_BASEPATH='' +EOF +) + + test "$output" = "$expected" + test::teardown +} + +test_print_config_reads_selected_config() { + local config_file + local expected + local output + + test::setup + config_file="$TEST_TMPDIR/custom.conf" + test::write_album_config \ + "$config_file" "$TEST_TMPDIR/custom-incoming" \ + "$TEST_TMPDIR/custom-dist" 'Selected config' 7 + printf 'SHUFFLE=yes\n' >> "$config_file" + + output=$( + cd "$TEST_TMPDIR" + "$TEST_PHOTOALBUM" --print-config --config "$config_file" + ) + expected=$(cat <<EOF +CONFIG_SOURCE=$config_file +INCOMING_DIR=$TEST_TMPDIR/custom-incoming +DIST_DIR=$TEST_TMPDIR/custom-dist +TEMPLATE_DIR=$TEST_REPO_ROOT/share/templates/default +TITLE=Selected\\ config +HEIGHT=120 +THUMBHEIGHT=30 +MAXPREVIEWS=7 +SHUFFLE=yes +TARBALL_INCLUDE=no +TARBALL_SUFFIX=.tar +TAR_OPTS=( -c ) +ORIGINAL_BASEPATH='' +EOF +) + + test "$output" = "$expected" + test::teardown +} + +test_print_config_reads_current_directory_config() { + local expected + local output + + test::setup + test::write_album_config \ + "$TEST_TMPDIR/photoalbum.conf" "$TEST_TMPDIR/incoming" \ + "$TEST_TMPDIR/dist" 'Current directory config' 8 + + output=$( + cd "$TEST_TMPDIR" + "$TEST_PHOTOALBUM" --print-config + ) + expected=$(cat <<EOF +CONFIG_SOURCE=./photoalbum.conf +INCOMING_DIR=$TEST_TMPDIR/incoming +DIST_DIR=$TEST_TMPDIR/dist +TEMPLATE_DIR=$TEST_REPO_ROOT/share/templates/default +TITLE=Current\\ directory\\ config +HEIGHT=120 +THUMBHEIGHT=30 +MAXPREVIEWS=8 +SHUFFLE=no +TARBALL_INCLUDE=no +TARBALL_SUFFIX=.tar +TAR_OPTS=( -c ) +ORIGINAL_BASEPATH='' +EOF +) + + test "$output" = "$expected" + test::teardown +} + +test_print_config_applies_cli_overrides_without_writes() { + local config_file + local dist_dir + local expected + local fake_bin + local forbidden_log + local output + + test::setup + fake_bin="$TEST_TMPDIR/bin" + config_file="$TEST_TMPDIR/photoalbum.conf" + dist_dir="$TEST_TMPDIR/cli-dist" + forbidden_log="$TEST_TMPDIR/forbidden-tools.log" + + test::install_failing_generation_tools "$fake_bin" + test::write_album_config \ + "$config_file" "$TEST_TMPDIR/config-incoming" \ + "$TEST_TMPDIR/config-dist" 'Config title' 40 + + output=$( + cd "$TEST_TMPDIR" + PATH="$fake_bin:$PATH" \ + TEST_FORBIDDEN_TOOL_LOG="$forbidden_log" \ + "$TEST_PHOTOALBUM" \ + --print-config \ + --incoming "$TEST_TMPDIR/cli-incoming" \ + --dist "$dist_dir" \ + --template "$TEST_TMPDIR/cli-template" \ + --title 'CLI title' \ + --height 456 \ + --thumbheight 45 \ + --maxpreviews 9 \ + --shuffle \ + --tarball + ) + expected=$(cat <<EOF +CONFIG_SOURCE=./photoalbum.conf +INCOMING_DIR=$TEST_TMPDIR/cli-incoming +DIST_DIR=$dist_dir +TEMPLATE_DIR=$TEST_TMPDIR/cli-template +TITLE=CLI\\ title +HEIGHT=456 +THUMBHEIGHT=45 +MAXPREVIEWS=9 +SHUFFLE=yes +TARBALL_INCLUDE=yes +TARBALL_SUFFIX=.tar +TAR_OPTS=( -c ) +ORIGINAL_BASEPATH='' +EOF +) + + test "$output" = "$expected" + test::assert_path_absent "$dist_dir" + test::assert_path_absent "$TEST_TMPDIR/config-dist" + test::assert_path_absent "$forbidden_log" + test::assert_no_staging_dirs "$TEST_TMPDIR" + test::teardown +} + +test_print_config_applies_negative_cli_overrides() { + local config_file + local output + + test::setup + config_file="$TEST_TMPDIR/photoalbum.conf" + test::write_album_config \ + "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \ + 'Negative overrides' 40 + { + printf 'SHUFFLE=yes\n' + printf 'TARBALL_INCLUDE=yes\n' + } >> "$config_file" + + output=$( + cd "$TEST_TMPDIR" + "$TEST_PHOTOALBUM" --print-config --no-shuffle --no-tarball + ) + + test::assert_contains 'SHUFFLE=no' "$output" + test::assert_contains 'TARBALL_INCLUDE=no' "$output" + test::teardown +} + +test_print_config_normalizes_scalar_and_array_tar_opts() { + local array_config + local array_output + local scalar_config + local scalar_output + + test::setup + scalar_config="$TEST_TMPDIR/scalar.conf" + array_config="$TEST_TMPDIR/array.conf" + test::write_album_config \ + "$scalar_config" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \ + 'Scalar tar opts' 40 + { + printf 'TARBALL_INCLUDE=yes\n' + printf 'TAR_OPTS=%q\n' '--sort=name --mtime=@0 -c' + } >> "$scalar_config" + test::write_album_config \ + "$array_config" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \ + 'Array tar opts' 40 + printf 'TAR_OPTS=(--sort=name --mtime=@0 -c)\n' >> "$array_config" + + scalar_output=$("$TEST_PHOTOALBUM" --print-config --config "$scalar_config") + array_output=$("$TEST_PHOTOALBUM" --print-config --config "$array_config") + + test::assert_contains \ + 'TAR_OPTS=( --sort=name --mtime=@0 -c )' \ + "$scalar_output" + test::assert_contains \ + 'TAR_OPTS=( --sort=name --mtime=@0 -c )' \ + "$array_output" + test::teardown +} + +test_print_config_quiet_and_verbose_keep_machine_output() { + local config_file + local plain_output + local quiet_output + local verbose_output + + test::setup + config_file="$TEST_TMPDIR/photoalbum.conf" + test::write_album_config \ + "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \ + 'Output mode config' 40 + + plain_output=$( + cd "$TEST_TMPDIR" + "$TEST_PHOTOALBUM" --print-config + ) + quiet_output=$( + cd "$TEST_TMPDIR" + "$TEST_PHOTOALBUM" --quiet --print-config + ) + verbose_output=$( + cd "$TEST_TMPDIR" + "$TEST_PHOTOALBUM" --verbose --print-config + ) + + test "$quiet_output" = "$plain_output" + test "$verbose_output" = "$plain_output" + test::assert_not_contains 'Verbose:' "$verbose_output" + test::teardown +} + +test_print_config_validates_basic_values_without_generation_preflight() { + local config_file + local output + + test::setup + config_file="$TEST_TMPDIR/photoalbum.conf" + test::write_album_config \ + "$config_file" "$TEST_TMPDIR/missing-incoming" \ + "$TEST_TMPDIR/missing-parent/dist" 'Printable missing paths' 40 + printf 'TEMPLATE_DIR=%q\n' "$TEST_TMPDIR/missing-template" >> "$config_file" + + output=$( + cd "$TEST_TMPDIR" + "$TEST_PHOTOALBUM" --print-config + ) + test::assert_contains "INCOMING_DIR=$TEST_TMPDIR/missing-incoming" "$output" + test::assert_contains "DIST_DIR=$TEST_TMPDIR/missing-parent/dist" "$output" + test::assert_contains "TEMPLATE_DIR=$TEST_TMPDIR/missing-template" "$output" + + printf 'MAXPREVIEWS=not-a-number\n' >> "$config_file" + output=$( + cd "$TEST_TMPDIR" + test::capture_failure_output "$TEST_PHOTOALBUM" --print-config + ) + test::assert_contains 'ERROR: MAXPREVIEWS must be a positive integer' \ + "$output" + test::assert_path_absent "$TEST_TMPDIR/missing-parent" + test::teardown +} + test_dry_run_reports_cli_overrides_without_writes() { local config_file local dist_dir @@ -1686,6 +1968,8 @@ test_unknown_options_and_conflicting_actions_fail() { "$TEST_PHOTOALBUM" --generate --init test::assert_failure 'clean/version conflict is rejected' \ "$TEST_PHOTOALBUM" --clean --version + test::assert_failure 'print-config/dry-run conflict is rejected' \ + "$TEST_PHOTOALBUM" --print-config --dry-run } test_empty_args_fail() { @@ -1781,6 +2065,30 @@ main() { 'repeated output flags use last value' \ test_repeated_output_flags_use_last_value test::run_case \ + '--print-config reflects defaults' \ + test_print_config_reflects_defaults + test::run_case \ + '--print-config reads selected config' \ + test_print_config_reads_selected_config + test::run_case \ + '--print-config reads current directory config' \ + test_print_config_reads_current_directory_config + test::run_case \ + '--print-config applies CLI overrides without writes' \ + test_print_config_applies_cli_overrides_without_writes + test::run_case \ + '--print-config applies negative CLI overrides' \ + test_print_config_applies_negative_cli_overrides + test::run_case \ + '--print-config normalizes scalar and array TAR_OPTS' \ + test_print_config_normalizes_scalar_and_array_tar_opts + test::run_case \ + '--print-config quiet and verbose keep machine output' \ + test_print_config_quiet_and_verbose_keep_machine_output + test::run_case \ + '--print-config validates values without generation preflight' \ + test_print_config_validates_basic_values_without_generation_preflight + test::run_case \ '--dry-run reports CLI overrides without writes' \ test_dry_run_reports_cli_overrides_without_writes test::run_case \ |
