diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6067ab..361f52c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,23 +15,43 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Detect Python project + id: python_project + shell: bash + run: | + if [[ -f pyproject.toml || -f setup.py || -f setup.cfg || -f requirements.txt ]]; then + echo "present=true" >> "$GITHUB_OUTPUT" + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "No Python project files found; skipping Python CI." + fi + - name: Set up Python + if: steps.python_project.outputs.present == 'true' uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.13" - name: Install dependencies + if: steps.python_project.outputs.present == 'true' + shell: bash run: | - pip install -r requirements.txt + python -m pip install --upgrade pip + if [[ -f requirements.txt ]]; then + pip install -r requirements.txt + fi pip install ruff pytest build - name: Lint (ruff) + if: steps.python_project.outputs.present == 'true' run: ruff check . - name: Test (pytest) + if: steps.python_project.outputs.present == 'true' run: pytest - name: Build package + if: steps.python_project.outputs.present == 'true' && hashFiles('pyproject.toml', 'setup.py') != '' run: python -m build # ─── Go ─────────────────────────────────────────────────── @@ -41,18 +61,33 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Detect Go project + id: go_project + shell: bash + run: | + if [[ -f go.mod ]]; then + echo "present=true" >> "$GITHUB_OUTPUT" + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "No go.mod found; skipping Go CI." + fi + - name: Set up Go + if: steps.go_project.outputs.present == 'true' uses: actions/setup-go@v5 with: go-version: "1.22" - name: Lint (go vet) + if: steps.go_project.outputs.present == 'true' run: go vet ./... - name: Test + if: steps.go_project.outputs.present == 'true' run: go test ./... - name: Build + if: steps.go_project.outputs.present == 'true' run: go build ./... # ─── Shell Scripts ──────────────────────────────────────── @@ -62,8 +97,34 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install shellcheck - run: sudo apt-get install -y shellcheck + - name: Install shell tooling + run: | + sudo apt-get update + sudo apt-get install -y bats shellcheck zsh - name: Lint shell scripts - run: find . -name "*.sh" -exec shellcheck {} + + shell: bash + run: | + mapfile -t shell_files < <( + find cli \ + -type f \ + \( -name "*.sh" -o -name "bash-wrapper" \) \ + -print | sort + ) + + if ((${#shell_files[@]} == 0)); then + echo "No shell scripts found." + exit 0 + fi + + shellcheck -x -e SC1090,SC1091 "${shell_files[@]}" + + - name: Run Bash tests + run: | + bats \ + cli/env/tests/banyanenv.bats \ + cli/bash/bin/tests/bash-wrapper.bats \ + cli/bash/commands/setup/tests/setup.bats \ + cli/bash/lib/std/tests/lib_std.bats \ + cli/bash/lib/file/tests/lib_file.bats \ + cli/bash/lib/git/tests/lib_git.bats diff --git a/.gitignore b/.gitignore index 3a47d61..124eca1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ __pycache__/ .ruff_cache/ .coverage htmlcov/ + +# Temporary files +test.log +run.log diff --git a/cli/bash/bin/bash-wrapper b/cli/bash/bin/bash-wrapper index b0ff106..0ec7603 100755 --- a/cli/bash/bin/bash-wrapper +++ b/cli/bash/bin/bash-wrapper @@ -149,7 +149,7 @@ main() { stdlib_path="$bash_root/lib/std/lib_std.sh" [[ -f "$stdlib_path" ]] || die "Required stdlib '$stdlib_path' was not found." - BANYAN_BASH_BOOTSTRAP_SOURCE="$command_script" + export BANYAN_BASH_BOOTSTRAP_SOURCE="$command_script" # Source the stdlib in the wrapper shell so command scripts inherit the shared helpers. source "$stdlib_path" unset BANYAN_BASH_BOOTSTRAP_SOURCE diff --git a/cli/bash/commands/setup/README.md b/cli/bash/commands/setup/README.md index 722d09a..b07e938 100644 --- a/cli/bash/commands/setup/README.md +++ b/cli/bash/commands/setup/README.md @@ -9,7 +9,9 @@ The command is intentionally small and idempotent. ## Commands - `install` - Installs Homebrew, Xcode Command Line Tools, Python, and creates `$HOME/.banyan_venv`. + Installs Homebrew, Xcode Command Line Tools, Python 3.13, BATS, and creates `$HOME/.banyanlabs.d/.venv`. +- `check` + Verifies the required local CLI setup without making changes. Exits non-zero if anything is missing. - `update-profile` Reserved for future shell profile updates. This subcommand is not implemented yet. @@ -19,8 +21,23 @@ The `install` command performs these steps: 1. install Homebrew if it is not already installed 2. install Xcode Command Line Tools if they are not already installed -3. install Python via Homebrew if it is not already installed -4. create `$HOME/.banyan_venv` if it does not already exist +3. install Python 3.13 via Homebrew if it is not already installed +4. install BATS via Homebrew if it is not already installed +5. create `$HOME/.banyanlabs.d/.venv` if it does not already exist + +The `.banyanlabs.d` directory is intended to hold additional Banyan Labs CLI state in the future, so the virtual environment now lives under that shared home. + +## Check Behavior + +The `check` command verifies the same base requirements as `install`: + +1. Homebrew is installed +2. Xcode Command Line Tools are installed +3. Python 3.13 is installed via Homebrew +4. BATS is installed via Homebrew +5. `$HOME/.banyanlabs.d/.venv` exists + +It exits with status `0` when everything is present and `1` when any required item is missing. ## What It Does Not Do Yet @@ -42,6 +59,12 @@ Via the symlinked entrypoint: cli/bash/bin/setup.sh install ``` +Check: + +```bash +cli/bash/bin/setup.sh check +``` + Help: ```bash @@ -66,6 +89,7 @@ The command supports a few environment-variable overrides, mainly for automation - `BANYAN_SETUP_VENV_DIR` - `BANYAN_SETUP_PYTHON_FORMULA` +- `BANYAN_SETUP_BATS_FORMULA` - `BANYAN_SETUP_PYTHON_BIN` - `BANYAN_SETUP_BREW_BIN` - `BANYAN_SETUP_HOMEBREW_INSTALLER_SCRIPT` diff --git a/cli/bash/commands/setup/setup.sh b/cli/bash/commands/setup/setup.sh index 5b3f911..ddde826 100644 --- a/cli/bash/commands/setup/setup.sh +++ b/cli/bash/commands/setup/setup.sh @@ -7,7 +7,9 @@ Usage: Commands: install - Install Homebrew, Xcode Command Line Tools, Python, and ~/.banyan_venv. + Install Homebrew, Xcode Command Line Tools, Python, BATS, and ~/.banyanlabs.d/.venv. + check + Verify the required local CLI setup without making changes. update-profile Reserved for future shell profile updates. @@ -17,13 +19,21 @@ Options: -h, --help Show this help text. Purpose: - Prepare the local Banyan Labs CLI environment on macOS. + Prepare and verify the local Banyan Labs CLI environment on macOS. -What it does: +Install does: 1. Install Homebrew if needed. 2. Install Xcode Command Line Tools if needed. - 3. Install Python via Homebrew if needed. - 4. Create ~/.banyan_venv if it does not already exist. + 3. Install Python 3.13 via Homebrew if needed. + 4. Install BATS via Homebrew if needed. + 5. Create ~/.banyanlabs.d/.venv if it does not already exist. + +Check does: + 1. Verify Homebrew is installed. + 2. Verify Xcode Command Line Tools are installed. + 3. Verify Python 3.13 is installed via Homebrew. + 4. Verify BATS is installed via Homebrew. + 5. Verify ~/.banyanlabs.d/.venv exists. Notes: - This command is intentionally idempotent. @@ -35,12 +45,23 @@ setup_is_dry_run() { [[ "${DRY_RUN-}" == true || "${dry_run-}" == true ]] } +setup_virtualenv_exists() { + local venv_dir + + venv_dir="$(setup_venv_dir)" + [[ -f "$venv_dir/bin/activate" || -f "$venv_dir/pyvenv.cfg" ]] +} + setup_venv_dir() { - printf '%s\n' "${BANYAN_SETUP_VENV_DIR:-$HOME/.banyan_venv}" + printf '%s\n' "${BANYAN_SETUP_VENV_DIR:-$HOME/.banyanlabs.d/.venv}" } setup_python_formula() { - printf '%s\n' "${BANYAN_SETUP_PYTHON_FORMULA:-python}" + printf '%s\n' "${BANYAN_SETUP_PYTHON_FORMULA:-python@3.13}" +} + +setup_bats_formula() { + printf '%s\n' "${BANYAN_SETUP_BATS_FORMULA:-bats-core}" } setup_xcode_tools_dir() { @@ -195,8 +216,36 @@ setup_install_python() { run brew install "$formula" } +setup_bats_installed() { + local formula + + formula="$(setup_bats_formula)" + command -v brew >/dev/null 2>&1 && brew list "$formula" >/dev/null 2>&1 +} + +setup_install_bats() { + local formula + + formula="$(setup_bats_formula)" + + if setup_bats_installed; then + log_info "BATS formula '$formula' is already installed via Homebrew." + return 0 + fi + + if setup_is_dry_run; then + log_info "[DRY-RUN] Would install BATS formula '$formula' via Homebrew." + return 0 + fi + + command -v brew >/dev/null 2>&1 || fatal_error "Homebrew is required to install BATS formula '$formula'." + + log_info "Installing BATS formula '$formula' via Homebrew." + run brew install "$formula" +} + setup_find_python_bin() { - local formula prefix + local formula prefix candidate candidates=() if [[ -n "${BANYAN_SETUP_PYTHON_BIN:-}" && -x "${BANYAN_SETUP_PYTHON_BIN}" ]]; then printf '%s\n' "${BANYAN_SETUP_PYTHON_BIN}" @@ -206,9 +255,19 @@ setup_find_python_bin() { formula="$(setup_python_formula)" if command -v brew >/dev/null 2>&1; then prefix="$(brew --prefix "$formula" 2>/dev/null || true)" - if [[ -n "$prefix" && -x "$prefix/bin/python3" ]]; then - printf '%s\n' "$prefix/bin/python3" - return 0 + if [[ -n "$prefix" ]]; then + candidates+=("$prefix/bin/python3") + candidates+=("$prefix/libexec/bin/python3") + if [[ "$formula" == python@* ]]; then + candidates+=("$prefix/bin/python${formula#python@}") + candidates+=("$prefix/libexec/bin/python${formula#python@}") + fi + for candidate in "${candidates[@]}"; do + if [[ -x "$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + done fi fi @@ -225,7 +284,7 @@ setup_create_virtualenv() { venv_dir="$(setup_venv_dir)" - if [[ -f "$venv_dir/bin/activate" || -f "$venv_dir/pyvenv.cfg" ]]; then + if setup_virtualenv_exists; then log_info "Virtual environment already exists at '$venv_dir'." return 0 fi @@ -242,14 +301,71 @@ setup_create_virtualenv() { run "$python_bin" -m venv "$venv_dir" } +setup_run_check() { + local brew_bin="" venv_dir missing=0 + + setup_require_macos + venv_dir="$(setup_venv_dir)" + + if brew_bin="$(setup_find_brew_bin)"; then + setup_refresh_brew_path || fatal_error "Homebrew is installed, but its bin directory could not be added to PATH." + log_info "Homebrew is installed." + log_debug "Resolved Homebrew binary: $brew_bin" + else + log_warn "Homebrew is not installed." + missing=1 + fi + + if setup_xcode_tools_installed; then + log_info "Xcode Command Line Tools are installed." + else + log_warn "Xcode Command Line Tools are not installed." + missing=1 + fi + + if setup_python_installed; then + log_info "Python formula '$(setup_python_formula)' is installed via Homebrew." + else + log_warn "Python formula '$(setup_python_formula)' is not installed via Homebrew." + missing=1 + fi + + if setup_bats_installed; then + log_info "BATS formula '$(setup_bats_formula)' is installed via Homebrew." + else + log_warn "BATS formula '$(setup_bats_formula)' is not installed via Homebrew." + missing=1 + fi + + if setup_virtualenv_exists; then + log_info "Virtual environment exists at '$venv_dir'." + else + log_warn "Virtual environment is missing at '$venv_dir'." + missing=1 + fi + + if ((missing == 0)); then + log_info "Banyan Labs CLI environment check passed." + return 0 + fi + + log_warn "Banyan Labs CLI environment check found missing requirements." + return 1 +} + setup_run_install() { setup_require_macos setup_install_homebrew setup_install_xcode_tools setup_install_python + setup_install_bats setup_create_virtualenv - print_success "Banyan Labs CLI setup is complete." + if setup_is_dry_run; then + log_info "[DRY-RUN] Banyan Labs CLI setup check is complete." + else + log_info "Banyan Labs CLI setup is complete." + fi } setup_run_update_profile() { @@ -274,7 +390,7 @@ setup_main() { set_log_level DEBUG export LOG_DEBUG=1 ;; - install|update-profile) + install|check|update-profile) if [[ -n "$command" ]]; then print_error "Only one setup command may be provided." setup_usage >&2 @@ -303,6 +419,9 @@ setup_main() { install) setup_run_install ;; + check) + setup_run_check + ;; update-profile) setup_run_update_profile ;; diff --git a/cli/bash/commands/setup/tests/setup.bats b/cli/bash/commands/setup/tests/setup.bats index e3fde70..072b5c8 100644 --- a/cli/bash/commands/setup/tests/setup.bats +++ b/cli/bash/commands/setup/tests/setup.bats @@ -61,13 +61,21 @@ create_brew_stub() { #!/usr/bin/env bash state_dir="${BANYAN_SETUP_TEST_STATE_DIR:?}" python_prefix="${BANYAN_SETUP_TEST_PYTHON_PREFIX:?}" -python_formula="${BANYAN_SETUP_PYTHON_FORMULA:-python}" +python_formula="${BANYAN_SETUP_PYTHON_FORMULA:-python@3.13}" +bats_formula="${BANYAN_SETUP_BATS_FORMULA:-bats-core}" case "${1:-}" in list) - if [[ "${2:-}" == "$python_formula" && -f "$state_dir/python-installed" ]]; then - exit 0 - fi + case "${2:-}" in + "$python_formula") + [[ -f "$state_dir/python-installed" ]] + exit $? + ;; + "$bats_formula") + [[ -f "$state_dir/bats-installed" ]] + exit $? + ;; + esac exit 1 ;; install) @@ -89,6 +97,11 @@ PYEOF chmod +x "$python_prefix/bin/python3" exit 0 fi + if [[ "${2:-}" == "$bats_formula" ]]; then + touch "$state_dir/bats-install-ran" + touch "$state_dir/bats-installed" + exit 0 + fi printf 'unexpected brew install args: %s\n' "$*" >&2 exit 1 ;; @@ -118,13 +131,21 @@ cat > "${BANYAN_SETUP_TEST_MOCKBIN:?}/brew" <<'BREWEOF' #!/usr/bin/env bash state_dir="${BANYAN_SETUP_TEST_STATE_DIR:?}" python_prefix="${BANYAN_SETUP_TEST_PYTHON_PREFIX:?}" -python_formula="${BANYAN_SETUP_PYTHON_FORMULA:-python}" +python_formula="${BANYAN_SETUP_PYTHON_FORMULA:-python@3.13}" +bats_formula="${BANYAN_SETUP_BATS_FORMULA:-bats-core}" case "${1:-}" in list) - if [[ "${2:-}" == "$python_formula" && -f "$state_dir/python-installed" ]]; then - exit 0 - fi + case "${2:-}" in + "$python_formula") + [[ -f "$state_dir/python-installed" ]] + exit $? + ;; + "$bats_formula") + [[ -f "$state_dir/bats-installed" ]] + exit $? + ;; + esac exit 1 ;; install) @@ -146,6 +167,11 @@ PYEOF chmod +x "$python_prefix/bin/python3" exit 0 fi + if [[ "${2:-}" == "$bats_formula" ]]; then + touch "$state_dir/bats-install-ran" + touch "$state_dir/bats-installed" + exit 0 + fi printf 'unexpected brew install args: %s\n' "$*" >&2 exit 1 ;; @@ -193,7 +219,8 @@ run_setup() { [ "$status" -eq 0 ] [[ "$output" == *"Usage:"* ]] [[ "$output" == *"setup [options] "* ]] - [[ "$output" == *"Prepare the local Banyan Labs CLI environment on macOS."* ]] + [[ "$output" == *"check"* ]] + [[ "$output" == *"Prepare and verify the local Banyan Labs CLI environment on macOS."* ]] } @test "setup requires an explicit command" { @@ -213,13 +240,14 @@ run_setup() { } @test "setup is idempotent when brew, xcode tools, python, and the venv already exist" { - local venv_dir="$TEST_HOME/.banyan_venv" + local venv_dir="$TEST_HOME/.banyanlabs.d/.venv" create_brew_stub create_xcode_stubs touch "$TEST_STATE_DIR/xcode-installed" mkdir -p "$TEST_TMPDIR/CommandLineTools" touch "$TEST_STATE_DIR/python-installed" + touch "$TEST_STATE_DIR/bats-installed" mkdir -p "$venv_dir/bin" printf '#!/usr/bin/env bash\n' > "$venv_dir/bin/activate" @@ -228,14 +256,16 @@ run_setup() { [ "$status" -eq 0 ] [[ "$output" == *"Homebrew is already installed."* ]] [[ "$output" == *"Xcode Command Line Tools are already installed."* ]] - [[ "$output" == *"Python formula 'python' is already installed via Homebrew."* ]] + [[ "$output" == *"Python formula 'python@3.13' is already installed via Homebrew."* ]] + [[ "$output" == *"BATS formula 'bats-core' is already installed via Homebrew."* ]] [[ "$output" == *"Virtual environment already exists at '$venv_dir'."* ]] [ ! -f "$TEST_STATE_DIR/python-install-ran" ] + [ ! -f "$TEST_STATE_DIR/bats-install-ran" ] } @test "setup installs missing dependencies and creates the Banyan virtual environment" { local installer - local venv_dir="$TEST_HOME/.banyan_venv" + local venv_dir="$TEST_HOME/.banyanlabs.d/.venv" create_xcode_stubs installer="$(create_homebrew_installer_stub)" @@ -249,11 +279,13 @@ run_setup() { [[ "$output" == *"Installing Homebrew."* ]] [[ "$output" == *"Installing Xcode Command Line Tools."* ]] [[ "$output" == *"Xcode Command Line Tools installation detected."* ]] - [[ "$output" == *"Installing Python formula 'python' via Homebrew."* ]] + [[ "$output" == *"Installing Python formula 'python@3.13' via Homebrew."* ]] + [[ "$output" == *"Installing BATS formula 'bats-core' via Homebrew."* ]] [[ "$output" == *"Creating Python virtual environment at '$venv_dir'."* ]] [[ "$output" == *"Banyan Labs CLI setup is complete."* ]] [ -f "$TEST_STATE_DIR/homebrew-install-ran" ] [ -f "$TEST_STATE_DIR/python-install-ran" ] + [ -f "$TEST_STATE_DIR/bats-install-ran" ] [ -f "$venv_dir/pyvenv.cfg" ] } @@ -263,9 +295,46 @@ run_setup() { [ "$status" -eq 0 ] [[ "$output" == *"[DRY-RUN] Would install Homebrew using the official installer."* ]] [[ "$output" == *"[DRY-RUN] Would wait for Xcode Command Line Tools installation to complete."* ]] - [[ "$output" == *"[DRY-RUN] Would install Python formula 'python' via Homebrew."* ]] - [[ "$output" == *"[DRY-RUN] Would create Python virtual environment at '$TEST_HOME/.banyan_venv'."* ]] - [ ! -e "$TEST_HOME/.banyan_venv" ] + [[ "$output" == *"[DRY-RUN] Would install Python formula 'python@3.13' via Homebrew."* ]] + [[ "$output" == *"[DRY-RUN] Would install BATS formula 'bats-core' via Homebrew."* ]] + [[ "$output" == *"[DRY-RUN] Would create Python virtual environment at '$TEST_HOME/.banyanlabs.d/.venv'."* ]] + [[ "$output" == *"[DRY-RUN] Banyan Labs CLI setup check is complete."* ]] + [ ! -e "$TEST_HOME/.banyanlabs.d/.venv" ] +} + +@test "setup check passes when all required components are present" { + local venv_dir="$TEST_HOME/.banyanlabs.d/.venv" + + create_brew_stub + create_xcode_stubs + touch "$TEST_STATE_DIR/xcode-installed" + mkdir -p "$TEST_TMPDIR/CommandLineTools" + touch "$TEST_STATE_DIR/python-installed" + touch "$TEST_STATE_DIR/bats-installed" + mkdir -p "$venv_dir/bin" + printf '#!/usr/bin/env bash\n' > "$venv_dir/bin/activate" + + run_setup "$BANYAN_REPO_ROOT/cli/bash/bin/setup.sh" check + + [ "$status" -eq 0 ] + [[ "$output" == *"Homebrew is installed."* ]] + [[ "$output" == *"Xcode Command Line Tools are installed."* ]] + [[ "$output" == *"Python formula 'python@3.13' is installed via Homebrew."* ]] + [[ "$output" == *"BATS formula 'bats-core' is installed via Homebrew."* ]] + [[ "$output" == *"Virtual environment exists at '$venv_dir'."* ]] + [[ "$output" == *"Banyan Labs CLI environment check passed."* ]] +} + +@test "setup check fails when required components are missing" { + run_setup "$BANYAN_REPO_ROOT/cli/bash/bin/setup.sh" check + + [ "$status" -eq 1 ] + [[ "$output" == *"Homebrew is not installed."* ]] + [[ "$output" == *"Xcode Command Line Tools are not installed."* ]] + [[ "$output" == *"Python formula 'python@3.13' is not installed via Homebrew."* ]] + [[ "$output" == *"BATS formula 'bats-core' is not installed via Homebrew."* ]] + [[ "$output" == *"Virtual environment is missing at '$TEST_HOME/.banyanlabs.d/.venv'."* ]] + [[ "$output" == *"Banyan Labs CLI environment check found missing requirements."* ]] } @test "setup install enables DEBUG logs with -v" { diff --git a/cli/bash/commands/test_cmd/test_cmd.sh b/cli/bash/commands/test_cmd/test_cmd.sh index b1f91e0..4653f04 100644 --- a/cli/bash/commands/test_cmd/test_cmd.sh +++ b/cli/bash/commands/test_cmd/test_cmd.sh @@ -1 +1,3 @@ +#!/usr/bin/env bash + log_info "I am starting" diff --git a/cli/bash/lib/file/lib_file.sh b/cli/bash/lib/file/lib_file.sh index 684e914..992bd86 100644 --- a/cli/bash/lib/file/lib_file.sh +++ b/cli/bash/lib/file/lib_file.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # # lib_file.sh - Bash library of generic file manipulation functions. # @@ -87,7 +88,7 @@ update_file_section() { if grep -qF -- "$beginning_marker" "$target_file" && grep -qF -- "$end_marker" "$target_file"; then if [[ "$remove_section" == true ]]; then - awk -v START_M="$beginning_marker" -v END_M="$end_marker" ' + if awk -v START_M="$beginning_marker" -v END_M="$end_marker" ' BEGIN { in_section = 0 } $0 == START_M { in_section = 1; next } $0 == END_M { in_section = 0; next } @@ -96,11 +97,13 @@ update_file_section() { print $0 } } - ' "$target_file" > "$temp_file" + ' "$target_file" > "$temp_file" && mv -f "$temp_file" "$target_file"; then + return 0 + fi else # FIX: This awk script now correctly handles multiple sections. It only replaces the first one. export AWK_NEW_TEXT="$new_content_string" - awk -v START_M="$beginning_marker" -v END_M="$end_marker" ' + if awk -v START_M="$beginning_marker" -v END_M="$end_marker" ' BEGIN { processed = 0 # 0 = not yet processed, 1 = processing, 2 = done } @@ -118,19 +121,16 @@ update_file_section() { processed != 1 { # Print the line if we are not inside the section being replaced print $0 } - ' "$target_file" > "$temp_file" - + ' "$target_file" > "$temp_file" && mv -f "$temp_file" "$target_file"; then + unset AWK_NEW_TEXT + return 0 + fi unset AWK_NEW_TEXT fi - if [[ $? -eq 0 ]]; then - mv -f "$temp_file" "$target_file" - return 0 - else - log_error "Failed to process sections in '$target_file'." - rm -f "$temp_file" - return 1 - fi + log_error "Failed to process sections in '$target_file'." + rm -f "$temp_file" + return 1 else # Markers not found in the file if [[ "$remove_section" == true ]]; then @@ -143,20 +143,17 @@ update_file_section() { echo "" >> "$temp_file" fi - { + if { echo "$beginning_marker" printf "%s" "$new_content_string" echo "$end_marker" - } >> "$temp_file" - - if [[ $? -eq 0 ]]; then - mv -f "$temp_file" "$target_file" + } >> "$temp_file" && mv -f "$temp_file" "$target_file"; then return 0 - else - log_error "Failed to add new section to '$target_file'." - rm -f "$temp_file" - return 1 fi + + log_error "Failed to add new section to '$target_file'." + rm -f "$temp_file" + return 1 fi fi } diff --git a/cli/bash/lib/git/lib_git.sh b/cli/bash/lib/git/lib_git.sh index dd17e84..76a7a9e 100644 --- a/cli/bash/lib/git/lib_git.sh +++ b/cli/bash/lib/git/lib_git.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # # lib_git.sh: Git operations # @@ -37,6 +38,8 @@ _git_only_path_dirty() { git_update_repo() { local git_repo="$1" local allowed_dirty_path="${2:-}" + local git_log + if [[ -z "$git_repo" ]]; then log_error "No git repository path provided." log_info "Usage: update_repo /path/to/repo [allowed_dirty_path]" @@ -57,7 +60,7 @@ git_update_repo() { # Check if it's a valid git repo if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then log_error "'$git_repo' is not a Git repository." - popd > /dev/null + popd >/dev/null || return 1 return 1 fi @@ -66,7 +69,7 @@ git_update_repo() { current_branch=$(git rev-parse --abbrev-ref HEAD) if [[ "$current_branch" != "master" ]]; then log_debug "Current branch of '$git_repo' is '${current_branch}', not 'master'. Skipping update." - popd > /dev/null + popd >/dev/null || return 1 return 1 fi @@ -82,31 +85,29 @@ git_update_repo() { log_debug "Repo '$git_repo' only has tracked changes in '$allowed_dirty_path'; attempting git pull." else log_debug "Repo '$git_repo' has local changes; skipping auto-update. Commit or stash to enable git pull." - popd > /dev/null + popd >/dev/null || return 1 return 0 fi fi # sometimes git pull throws warnings and we need a second git pull to address it - { git pull || git pull; } >"$git_log" 2>&1 - if (($? != 0)); then + if ! { git pull || git pull; } >"$git_log" 2>&1; then log_error "git pull failed on repo '$git_repo'" [[ -s "$git_log" ]] && log_info_file "$git_log" - popd > /dev/null + popd >/dev/null || return 1 return 1 fi # it is safe to run submodule commands even if the repo has no submodules - { git submodule init && git submodule sync && git submodule update; } >/dev/null - if (($? != 0)); then + if ! { git submodule init && git submodule sync && git submodule update; } >/dev/null; then log_error "git submodule update failed on repo '$git_repo'" [[ -s "$git_log" ]] && log_info_file "$git_log" - popd > /dev/null + popd >/dev/null || return 1 return 1 fi log_debug "Git repo '$git_repo' updated to latest master" - popd > /dev/null + popd >/dev/null || return 1 return 0 } @@ -128,16 +129,17 @@ git_update_repo() { # git_get_current_branch() { local target_dir="$1" - # Create a name reference to the variable name passed as the second argument. - local -n result_var="$2" - result_var="" # --- Argument Validation --- - if [[ -z "$target_dir" || -z "$2" ]]; then + if [[ -z "$target_dir" || -z "${2:-}" ]]; then log_error "Usage: get_git_branch " return 1 fi + # Create a name reference to the variable name passed as the second argument. + local -n result_var="$2" + result_var="" + if [[ ! -d "$target_dir" ]]; then return 1 fi @@ -153,7 +155,7 @@ git_get_current_branch() { # Check if we are inside a Git repository. if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then # Not a Git repo, result is already an empty string. - popd > /dev/null + popd >/dev/null || return 1 return 0 fi @@ -169,7 +171,7 @@ git_get_current_branch() { result_var="detached head" fi - popd > /dev/null + popd >/dev/null || return 1 return 0 } diff --git a/cli/bash/lib/std/lib_std.sh b/cli/bash/lib/std/lib_std.sh index 12b7ffc..f988bd9 100644 --- a/cli/bash/lib/std/lib_std.sh +++ b/cli/bash/lib/std/lib_std.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # # lib_std.sh - Foundation library for Bash scripts # Requires Bash version 4.0 or higher. @@ -140,8 +141,7 @@ check_bash_version_and_upgrade() { fi echo "Updating Homebrew and installing Bash..." >&2 - brew update && brew install bash - if [[ $? -ne 0 ]]; then + if ! { brew update && brew install bash; }; then echo "Error: Failed to install Bash via Homebrew." >&2 exit 1 fi @@ -151,7 +151,9 @@ check_bash_version_and_upgrade() { local new_bash_path new_bash_path="$(brew --prefix)/bin/bash" if [[ -f "$new_bash_path" ]]; then - echo "Relaunching script with the new Bash from: $new_bash_path ${__SCRIPT_ARGS__[@]}" >&2 + printf 'Relaunching script with the new Bash from: %s' "$new_bash_path" >&2 + printf ' %q' "${__SCRIPT_ARGS__[@]}" >&2 + printf '\n' >&2 exec "$new_bash_path" "$0" "${__SCRIPT_ARGS__[@]}" else echo "Error: Could not find the new Bash executable at '$new_bash_path'." >&2 @@ -242,13 +244,15 @@ import() { # Unless an absolute library path is given, make it relative to the script's location if [[ "$lib" != /* ]]; then [[ $__SCRIPT_DIR__ ]] || { printf '%s\n' "ERROR: __SCRIPT_DIR__ not set; import functionality needs it" >&2; exit 1; } - pushd "$__SCRIPT_DIR__" >/dev/null + pushd "$__SCRIPT_DIR__" >/dev/null || exit_if_error 1 "Failed to enter script directory '$__SCRIPT_DIR__'." pushed=1 fi if [[ -f "$lib" ]]; then source "$lib" exit_if_error $? "Import of library '$lib' not successful." - ((pushed)) && popd >/dev/null + if ((pushed)); then + popd >/dev/null || exit_if_error 1 "Failed to leave script directory '$__SCRIPT_DIR__'." + fi else exit_if_error 1 "Library '$lib' does not exist" fi @@ -455,9 +459,9 @@ _print_log() { *) color="";; # No color for VERBOSE or others esac - local source_path="" source_line="" frame=1 caller_info caller_line caller_func caller_file + local source_path="" source_line="" frame=1 caller_info caller_line _caller_func caller_file while caller_info=$(caller "$frame"); do - read -r caller_line caller_func caller_file <<<"$caller_info" + read -r caller_line _caller_func caller_file <<<"$caller_info" if [[ -n "$caller_file" && "$caller_file" != "$__LIB_STD_PATH__" ]]; then source_path="$caller_file" source_line="$caller_line" @@ -600,9 +604,14 @@ dump_trace() { # exit_if_error() { (($#)) || return - local num_re='^[0-9]+' + local num_re='^[0-9]+$' local rc=$1; shift - local message="${@:-No message specified}" + local message + if (($#)); then + message="$(__join_message__ "$@")" + else + message="No message specified" + fi if ! [[ $rc =~ $num_re ]]; then log_error "'$rc' is not a valid exit code; it needs to be a number greater than zero. Treating it as 1." rc=1 @@ -610,7 +619,7 @@ exit_if_error() { ((rc)) && { log_fatal "$message" dump_trace - exit $rc + exit "$rc" } return 0 } @@ -729,15 +738,16 @@ run() { # Usage: safe_mkdir [-p] dir1 dir2 ... # safe_mkdir() { - local p dir failed_dirs=() + local dir failed_dirs=() mkdir_args=() if [[ "${1-}" == "-p" ]]; then shift - p="-p" + mkdir_args=(-p) fi for dir; do [[ -d "$dir" ]] && continue - mkdir $p -- "$dir" - (($?)) && failed_dirs+=("$dir") + if ! mkdir "${mkdir_args[@]}" -- "$dir"; then + failed_dirs+=("$dir") + fi done ((${#failed_dirs[@]} > 0)) && exit_if_error 1 "Failed to create directories: ${failed_dirs[*]}" return 0 @@ -806,7 +816,7 @@ safe_truncate() { # The > redirection is the simplest way to truncate a file. # We redirect stderr to /dev/null to suppress system error messages, # as we will provide our own comprehensive error message. - if ! > "$file" 2>/dev/null; then + if ! : > "$file" 2>/dev/null; then failed_files+=("$file") fi done @@ -1069,9 +1079,11 @@ safe_unalias() { get_my_source_dir() { local result_name="${1-}" [[ -n "$result_name" ]] || fatal_error "get_my_source_dir: No result variable name provided." - local -n result="$result_name" + local source_dir # Reference: https://stackoverflow.com/a/246128/6862601 - result="$(cd "$(dirname "${BASH_SOURCE[1]}")" >/dev/null 2>&1 && pwd -P)" + source_dir="$(cd "$(dirname "${BASH_SOURCE[1]}")" >/dev/null 2>&1 && pwd -P)" || + fatal_error "get_my_source_dir: Unable to resolve source directory." + printf -v "$result_name" '%s' "$source_dir" } # diff --git a/cli/env/banyanenv.sh b/cli/env/banyanenv.sh index d3fc7f3..54eb5d0 100644 --- a/cli/env/banyanenv.sh +++ b/cli/env/banyanenv.sh @@ -111,6 +111,6 @@ banyanenv_main _banyanenv_rc=$? unset -f banyanenv_error banyanenv_is_sourced banyanenv_get_source_path banyanenv_prepend_path banyanenv_main if [[ $_banyanenv_rc -ne 0 ]]; then - return "$_banyanenv_rc" 2>/dev/null || exit "$_banyanenv_rc" + return "$_banyanenv_rc" fi unset _banyanenv_rc