diff --git a/base_api_manifest.yaml b/base_api_manifest.yaml index 8b9504e..a07a3fa 100644 --- a/base_api_manifest.yaml +++ b/base_api_manifest.yaml @@ -24,7 +24,7 @@ environment: - BASE_BASH_LIBS_LOG_DEBUG|caller-control|enable debug diagnostics - BASE_BASH_LIBS_LOG_UTC|caller-control|render log timestamps in UTC - BASE_BASH_LIBS_GIT_PULL_MAX_ATTEMPTS|caller-control|maximum git pull attempts - - NO_COLOR|caller-input|disable color output when set + - NO_COLOR|caller-input|disable automatic color output when set; explicit always mode overrides it - TMPDIR|caller-input|temporary directory policy - GH_TOKEN|caller-input|GitHub CLI authentication input - TZ|caller-input|caller-selected timezone for diagnostics diff --git a/docs/v2-api-contract.md b/docs/v2-api-contract.md index 739bd91..b466aad 100644 --- a/docs/v2-api-contract.md +++ b/docs/v2-api-contract.md @@ -174,7 +174,8 @@ argv. This mapping is the v2 migration reference: | `--debug-wrapper` | Enables DEBUG logging and is removed from application argv. | | `--verbose-wrapper` | Preserves the deprecated VERBOSE compatibility level and is removed from application argv. | | `--utc-wrapper` | Exports UTC logging for the initialized runtime and is removed from application argv. | -| `--color` | Requests terminal colors and is removed from application argv. | +| `--color` | Requests automatic terminal colors (stderr TTY and `NO_COLOR` unset) and is removed from application argv. A `--color MODE` pair remains available to the app's own parser. | +| `--color-mode MODE` | Selects `auto`, `always`, or `never` for wrappers that do not use the standard app-option model; removed from application argv and takes precedence over app-level `--color`. `always` overrides `NO_COLOR`. | | `base_init --` | Stops wrapper parsing; the separator and all following values remain literal application argv. (The launcher's own script-selection `--` is not forwarded.) | No other launcher option is implicitly forwarded. Applications that need an diff --git a/lib/bash/app/README.md b/lib/bash/app/README.md index 08264a0..5c0faed 100644 --- a/lib/bash/app/README.md +++ b/lib/bash/app/README.md @@ -54,9 +54,13 @@ zero. No configuration value is evaluated as shell code. `--verbose`, `--quiet`, `--color`, `--dry-run`, `--non-interactive`, `--config`, and `--user-config` options to a declarative CLI model. `--color` accepts `auto`, `always`, or `never`; the bare legacy launcher -`--color` flag remains supported when it is not followed by one of those -modes. `--quiet` sets the default logger threshold to `WARN`, while -`--verbose` sets it to `DEBUG`; the options are mutually exclusive. +`--color` flag remains supported, and wrappers can use `--color-mode MODE` +when they need an explicit policy independent of the app's own CLI model. +An explicit wrapper mode takes precedence over the app's `--color` selection. +`auto` colors only when stderr is a terminal and `NO_COLOR` is unset; +`always` forces ANSI colors even for captured stderr or a set `NO_COLOR`, and +`never` disables them. `--quiet` sets the default logger threshold to `WARN`, +while `--verbose` sets it to `DEBUG`; the options are mutually exclusive. `base_app_apply_standard_options` publishes the parsed policy in `BASE_BASH_LIBS_APP_*` globals and applies the logging/color policy. Applications should call diff --git a/lib/bash/app/lib_app.sh b/lib/bash/app/lib_app.sh index 59724fe..a3e5efa 100644 --- a/lib/bash/app/lib_app.sh +++ b/lib/bash/app/lib_app.sh @@ -677,15 +677,16 @@ base_app_config_report() { # base_app_add_standard_options - Opts a CLI model into common policy options. base_app_add_standard_options() { - local cli_model="${1-}" path="${2-}" + local cli_model="${1-}" path="${2-}" color_modes (($# == 2)) || { __base_bash_libs_app_error__ 'base_app_add_standard_options: usage: base_app_add_standard_options CLI_MODEL COMMAND_PATH' return 2 } + color_modes="$(__base_bash_libs_std_color_modes__)" || return $? base_cli_option "$cli_model" "$path" verbose flag --verbose -v help='Enable verbose diagnostics' || return $? base_cli_option "$cli_model" "$path" quiet flag --quiet -q conflicts=verbose help='Suppress informational output' || return $? - base_cli_option "$cli_model" "$path" color value --color default=auto enum=auto,always,never metavar=MODE help='Color policy' || return $? + base_cli_option "$cli_model" "$path" color value --color default=auto enum="$color_modes" metavar=MODE help='Color policy' || return $? base_cli_option "$cli_model" "$path" dry_run flag --dry-run help='Plan without mutating' || return $? base_cli_option "$cli_model" "$path" noninteractive flag --non-interactive help='Never prompt' || return $? base_cli_option "$cli_model" "$path" config value --config metavar=FILE help='Use a project configuration file' || return $? @@ -694,13 +695,19 @@ base_app_add_standard_options() { # base_app_apply_standard_options - Publishes parsed common options as policy state. base_app_apply_standard_options() { - local model="${1-}" value + local model="${1-}" value cli_color (($# == 1)) || { __base_bash_libs_app_error__ 'base_app_apply_standard_options: usage: base_app_apply_standard_options MODEL' return 2 } __base_bash_libs_app_model_exists__ "$model" || return 1 + cli_color="${BASE_BASH_LIBS_CLI_RESULT_OPTIONS[color]-auto}" + if ! __base_bash_libs_std_color_mode_is_valid__ "$cli_color"; then + __base_bash_libs_app_error__ "base_app_apply_standard_options: invalid color mode '$cli_color'." + return 2 + fi + value="${__base_bash_libs_std_wrapper_color_mode:-$cli_color}" # These are caller-visible policy globals consumed by application code. # shellcheck disable=SC2034 BASE_BASH_LIBS_APP_VERBOSE="${BASE_BASH_LIBS_CLI_RESULT_OPTIONS[verbose]-0}" @@ -708,8 +715,6 @@ base_app_apply_standard_options() { BASE_BASH_LIBS_APP_QUIET="${BASE_BASH_LIBS_CLI_RESULT_OPTIONS[quiet]-0}" BASE_BASH_LIBS_APP_DRY_RUN="${BASE_BASH_LIBS_CLI_RESULT_OPTIONS[dry_run]-0}" BASE_BASH_LIBS_APP_NONINTERACTIVE="${BASE_BASH_LIBS_CLI_RESULT_OPTIONS[noninteractive]-0}" - value="${BASE_BASH_LIBS_CLI_RESULT_OPTIONS[color]-auto}" - BASE_BASH_LIBS_APP_COLOR="$value" if [[ -n "${BASE_BASH_LIBS_STD_LOG_LEVELS[INFO]+set}" ]]; then case "${BASE_BASH_LIBS_APP_QUIET}:${BASE_BASH_LIBS_APP_VERBOSE}" in 1:0) base_std_set_log_level WARN || return $? ;; @@ -724,21 +729,8 @@ base_app_apply_standard_options() { __base_bash_libs_app_error__ 'base_app_apply_standard_options: quiet and verbose cannot both be enabled.' return 2 fi - case "$value" in - auto | always) - # shellcheck disable=SC2034 - BASE_BASH_LIBS_STD_COLOR_ENABLED=1 - ;; - never) - # shellcheck disable=SC2034 - BASE_BASH_LIBS_STD_COLOR_ENABLED=0 - ;; - *) - __base_bash_libs_app_error__ "base_app_apply_standard_options: invalid color mode '$value'." - return 2 - ;; - esac - __base_bash_libs_std_init_colors__ + __base_bash_libs_std_apply_color_mode__ "$value" || return $? + BASE_BASH_LIBS_APP_COLOR="$value" BASE_BASH_LIBS_DRY_RUN="$BASE_BASH_LIBS_APP_DRY_RUN" export BASE_BASH_LIBS_APP_DRY_RUN BASE_BASH_LIBS_APP_NONINTERACTIVE BASE_BASH_LIBS_APP_COLOR export BASE_BASH_LIBS_DRY_RUN diff --git a/lib/bash/app/tests/lib_app.bats b/lib/bash/app/tests/lib_app.bats index 4ece5f1..74f23c6 100644 --- a/lib/bash/app/tests/lib_app.bats +++ b/lib/bash/app/tests/lib_app.bats @@ -379,6 +379,73 @@ assert_demo_snapshot() { [ "${BASE_BASH_LIBS_STD_LOGGER_LEVELS[default]}" -eq 4 ] } +@test "standard color modes distinguish auto always and never" { + local stderr_file="$TEST_TMPDIR/color.stderr" + local -a init_args=() + + base_init init_args -- + base_cli_model_init color name=color + base_cli_command color run "Run" + base_app_init color_policy name=color + base_app_add_standard_options color run + + unset NO_COLOR + base_cli_parse color -- run --color auto + base_app_apply_standard_options color_policy + base_std_print_error auto 2>"$stderr_file" + [[ "$(<"$stderr_file")" != *$'\033['* ]] + + export NO_COLOR=1 + base_cli_parse color -- run --color always + base_app_apply_standard_options color_policy + base_std_print_error always 2>"$stderr_file" + [[ "$(<"$stderr_file")" == *$'\033['* ]] + + base_cli_parse color -- run --color never + base_app_apply_standard_options color_policy + base_std_print_error never 2>"$stderr_file" + [[ "$(<"$stderr_file")" != *$'\033['* ]] + unset NO_COLOR +} + +@test "invalid standard color policy fails before publishing color state" { + local status + + base_cli_model_init invalid_color name=invalid-color + base_cli_command invalid_color run "Run" + base_app_init invalid_color_policy + base_cli_parse invalid_color -- run + __base_bash_libs_std_color_mode=never + BASE_BASH_LIBS_APP_COLOR=never + BASE_BASH_LIBS_CLI_RESULT_OPTIONS[color]=unsupported + + if base_app_apply_standard_options invalid_color_policy >/dev/null 2>"$TEST_TMPDIR/color-error"; then + status=0 + else + status=$? + fi + + [ "$status" -eq 2 ] + [ "$__base_bash_libs_std_color_mode" = never ] + [ "$BASE_BASH_LIBS_APP_COLOR" = never ] + grep -F "invalid color mode 'unsupported'" "$TEST_TMPDIR/color-error" +} + +@test "explicit wrapper color mode overrides the app's standard color option" { + base_cli_model_init wrapper_color name=wrapper-color + base_cli_command wrapper_color run "Run" + base_app_init wrapper_color_policy + base_app_add_standard_options wrapper_color run + base_cli_parse wrapper_color -- run --color always + __base_bash_libs_std_wrapper_color_mode=never + + base_app_apply_standard_options wrapper_color_policy + + [ "$BASE_BASH_LIBS_APP_COLOR" = never ] + [ "$__base_bash_libs_std_color_mode" = never ] + [ -z "$BASE_BASH_LIBS_STD_COLOR_RED" ] +} + @test "base_app_prompt validates usage and applies the prompt policy contract" { local prompt_calls=() base_app_init prompt diff --git a/lib/bash/std/README.md b/lib/bash/std/README.md index b8da776..7a498d8 100644 --- a/lib/bash/std/README.md +++ b/lib/bash/std/README.md @@ -419,8 +419,16 @@ base_std_print_message "plain stdout message" `log_*`, `base_std_print_error`, `base_std_print_warn`, `base_std_print_info`, and `base_std_print_success` write to stderr. `base_std_print_bold` and `base_std_print_message` write to stdout. -Colors are only enabled for terminal stderr when `--color` is passed. Set -`NO_COLOR` to disable colored output even when `--color` is present. +The legacy `base_init --color` control requests automatic color and only enables +it when stderr is a terminal and `NO_COLOR` is unset. Wrappers can use +`base_init --color-mode auto|always|never` to select an explicit standard +color policy; this explicit wrapper setting takes precedence over an app-level +`--color MODE`. `always` overrides `NO_COLOR`, and `never` disables color. +The application's own `--color MODE` pair remains untouched for its parser. +Arguments after the application's separator remain application-owned. Applications using +`base_app_add_standard_options` can select `auto`, `always`, or `never`: +`always` explicitly forces ANSI output even when stderr is captured or +`NO_COLOR` is set; `never` disables it. ## Error Handling diff --git a/lib/bash/std/lib_std.sh b/lib/bash/std/lib_std.sh index e72a54f..66b006f 100644 --- a/lib/bash/std/lib_std.sh +++ b/lib/bash/std/lib_std.sh @@ -77,7 +77,7 @@ # Notes: # - Call base_init [--source