diff --git a/.bashrc b/.bashrc index c2a2883..e191d2a 100644 --- a/.bashrc +++ b/.bashrc @@ -217,6 +217,15 @@ alias ping='ping -c 10' alias less='less -R' alias cls='clear' +# Debian-family bat packages expose batcat; other platforms normally expose bat. +# Keep paging disabled, but retain bat's header, grid, and line numbers so plain +# text files are visibly rendered by bat in an interactive terminal. +if command -v batcat >/dev/null 2>&1; then + alias cat='batcat --paging=never --style=full' +elif command -v bat >/dev/null 2>&1; then + alias cat='bat --paging=never --style=full' +fi + if command -v trash-put >/dev/null 2>&1; then alias rm='trash-put' elif command -v trash >/dev/null 2>&1; then diff --git a/.github/workflows/bashtest.yml b/.github/workflows/bashtest.yml index feb23a9..666a58b 100644 --- a/.github/workflows/bashtest.yml +++ b/.github/workflows/bashtest.yml @@ -26,6 +26,9 @@ jobs: if: steps.cache-shellcheck.outputs.cache-hit != 'true' run: sudo apt-get install -y shellcheck + - name: Install rendering test dependency + run: sudo apt-get install -y bat + - name: Lint Bash scripts run: | echo "Running ShellCheck..." @@ -36,3 +39,6 @@ jobs: else echo "No linting errors found." fi + + - name: Test shell integration + run: tests/test_shell_integration.sh diff --git a/README.md b/README.md index f906cfe..b815d25 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,14 @@ The `setup.sh` script automates the installation process by: - Installing Homebrew on macOS if it is not already installed - Installing Bash 5 with Homebrew on macOS - Adding Homebrew Bash to `/etc/shells` and setting it as the default login shell on macOS -- Installing dependencies (bash-completion, neovim, starship, fzf, zoxide) +- Installing dependencies (bash-completion, bat, neovim, starship, fzf, zoxide) - Installing Starship and JetBrainsMono Nerd Font on Linux - Selecting JetBrainsMono Nerd Font in Ptyxis or GNOME Terminal when available - Installing the MesloLGS Nerd Font required for the prompt on macOS when available - Linking configuration files from `~/.local/share/mybash` to your home directory - Linking the fastfetch config to `~/.config/fastfetch/config.jsonc` - Ensuring `~/.bash_profile` initializes Homebrew on macOS -- Ensuring `~/.bash_profile` sources `~/.bashrc` on macOS +- Ensuring the active Bash login profile sources `~/.bashrc` on Linux and macOS - Setting up additional utilities like `fastfetch` On macOS, `setup.sh` may prompt for your password when it adds Homebrew Bash to `/etc/shells` and changes your default shell. Restart Terminal after installation, then verify with: @@ -103,6 +103,7 @@ The `.bashrc` file defines aliases, functions, and environment variables to enha - **Aliases**: Shortcuts for common commands (e.g., `alias cp='cp -i'`) - **Functions**: Custom functions for tasks like extracting archives and copying files with progress +- **Enhanced `cat` output**: Interactive shells use `batcat --paging=never --style=full` on Debian-family systems, or `bat` with the same options elsewhere. This preserves bat's header, grid, line numbers, and terminal colors. Non-interactive scripts keep the normal `cat`; use `command cat` to bypass the alias interactively. ### `starship.toml` @@ -132,6 +133,7 @@ The `config.jsonc` file configures [fastfetch](https://github.com/AlexRogalskiy/ 3. **Enhancements and Utilities** - Improves command output readability with colors + - Uses `batcat` or `bat` for interactive `cat` output without changing non-interactive script behavior - Introduces safer file operations (e.g., using `trash` instead of `rm`) - Integrates Zoxide for easy directory navigation diff --git a/setup.sh b/setup.sh index bcf1345..7edb065 100755 --- a/setup.sh +++ b/setup.sh @@ -325,21 +325,49 @@ link_file() { print_colored "$GREEN" "Linked $target" } -ensure_bash_profile_sources_bashrc() { - [ "$OS_NAME" = Darwin ] || return 0 +ensure_login_profile_sources_bashrc() { + if [ -f "$HOME/.bash_profile" ]; then + profile=$HOME/.bash_profile + elif [ -f "$HOME/.bash_login" ]; then + profile=$HOME/.bash_login + elif [ "$OS_NAME" = Darwin ]; then + profile=$HOME/.bash_profile + else + profile=$HOME/.profile + fi - profile=$HOME/.bash_profile - if [ -f "$profile" ] && grep -q 'HOME/.bashrc' "$profile"; then + if [ -f "$profile" ] && grep -Eq '^[[:space:]]*(\.|source)[[:space:]]+"?(\$\{?HOME\}?|~)/\.bashrc"?[[:space:]]*($|#)' "$profile"; then return 0 fi { - printf '\n# Source .bashrc for interactive bash shells\n' - printf '%s\n' "if [ -f \"\$HOME/.bashrc\" ]; then" + printf '\n# >>> mybash .bashrc >>>\n' + printf '%s\n' "if [ -n \"\${BASH_VERSION:-}\" ] && [ -f \"\$HOME/.bashrc\" ]; then" printf '%s\n' ". \"\$HOME/.bashrc\"" printf 'fi\n' + printf '# <<< mybash .bashrc <<<\n' } >>"$profile" - print_colored "$GREEN" "Updated $profile to source .bashrc" + print_colored "$GREEN" "Updated $profile to source .bashrc in login shells" +} + +verify_interactive_cat_alias() { + if command_exists batcat; then + expected="batcat --paging=never --style=full" + elif command_exists bat; then + expected="bat --paging=never --style=full" + else + print_colored "$RED" "bat was installed, but neither batcat nor bat is available in PATH." + return 1 + fi + + alias_output=$(bash --login -ic 'alias cat >&3' 3>&1 >/dev/null 2>/dev/null || true) + expected_output="alias cat='$expected'" + if [ "$alias_output" = "$expected_output" ]; then + print_colored "$GREEN" "Verified interactive cat alias: $expected" + else + print_colored "$RED" "The interactive cat alias was not loaded from $HOME/.bashrc." + return 1 + fi } ensure_bash_profile_brew_shellenv() { @@ -378,13 +406,16 @@ install_configs() { link_file "$MYBASHDIR/starship-theme" "$HOME/.local/bin/starship-theme" ensure_homebrew_bash_macos ensure_bash_profile_brew_shellenv - ensure_bash_profile_sources_bashrc + ensure_login_profile_sources_bashrc + verify_interactive_cat_alias } -install_dependencies -install_starship_linux -install_nerd_font_linux -install_configs -configure_terminal_font_linux +if [ "${MYBASH_SETUP_LIB_ONLY:-0}" -eq 0 ]; then + install_dependencies + install_starship_linux + install_nerd_font_linux + install_configs + configure_terminal_font_linux -print_colored "$GREEN" "Installation complete. Restart your shell or run: source ~/.bashrc" + print_colored "$GREEN" "Installation complete. Restart your shell or run: source ~/.bashrc" +fi diff --git a/tests/test_shell_integration.sh b/tests/test_shell_integration.sh new file mode 100755 index 0000000..e5c89e4 --- /dev/null +++ b/tests/test_shell_integration.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2030,SC2031 # HOME changes are intentionally isolated in test subshells. +set -euo pipefail + +ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P) +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +grep -Fq "alias cat='batcat --paging=never --style=full'" "$ROOT/.bashrc" || fail 'missing batcat alias' +grep -Fq "alias cat='bat --paging=never --style=full'" "$ROOT/.bashrc" || fail 'missing bat fallback alias' + +BAT_TEST_DIR="$TMPDIR/bat-test" +mkdir -p "$BAT_TEST_DIR" +cat >"$BAT_TEST_DIR/batcat" <<'EOF' +#!/bin/sh +printf 'args:%s\n' "$*" +cat +EOF +chmod +x "$BAT_TEST_DIR/batcat" +bat_output=$(PATH="$BAT_TEST_DIR:/usr/bin:/bin" HOME="$TMPDIR" bash --noprofile --rcfile "$ROOT/.bashrc" -ic 'printf payload | cat' 2>/dev/null) +[[ $bat_output == $'args:--paging=never --style=full\npayload' ]] || fail "interactive cat did not invoke batcat directly: $bat_output" + +BAT_FALLBACK_DIR="$TMPDIR/bat-fallback-test" +mkdir -p "$BAT_FALLBACK_DIR" +cat >"$BAT_FALLBACK_DIR/bat" <<'EOF' +#!/bin/sh +printf 'args:%s\n' "$*" +/bin/cat +EOF +chmod +x "$BAT_FALLBACK_DIR/bat" +bat_fallback_output=$(PATH="$BAT_FALLBACK_DIR" HOME="$TMPDIR" /bin/bash --noprofile --rcfile "$ROOT/.bashrc" -ic 'printf payload | cat' 2>/dev/null) +[[ $bat_fallback_output == $'args:--paging=never --style=full\npayload' ]] || fail "cat alias did not invoke bat fallback correctly: $bat_fallback_output" + +REAL_BAT=$(command -v batcat || command -v bat || true) +[[ -n $REAL_BAT ]] || fail 'batcat/bat is required for the rendering contract' +command -v script >/dev/null 2>&1 || fail 'script is required for the rendering contract' +RENDER_HOME="$TMPDIR/render-home" +mkdir -p "$RENDER_HOME/bin" +ln -s "$REAL_BAT" "$RENDER_HOME/bin/$(basename "$REAL_BAT")" +printf '%s\n' 'PING 192.168.88.1 (192.168.88.1)' '64 bytes from 192.168.88.1' >"$RENDER_HOME/ping.txt" +render_output=$(TERM=xterm-256color PATH="$RENDER_HOME/bin:/usr/bin:/bin" HOME="$RENDER_HOME" \ + script -qec "bash --noprofile --rcfile '$ROOT/.bashrc' -ic 'cat \"$RENDER_HOME/ping.txt\"'" /dev/null 2>/dev/null) +[[ $render_output == *'ping.txt'* ]] || fail 'interactive cat rendering lacks the bat file header' +[[ $render_output == *'1'*'PING 192.168.88.1'* ]] || fail 'interactive cat rendering lacks bat line numbers/grid' +[[ $render_output == *$'\033['* ]] || fail 'interactive cat rendering lacks terminal color escapes' + +noninteractive_output=$(PATH="$BAT_TEST_DIR:/usr/bin:/bin" HOME="$TMPDIR" bash --noprofile -c '. "$1"; printf payload | cat' bash "$ROOT/.bashrc" 2>/dev/null) +[[ $noninteractive_output == payload ]] || fail "non-interactive cat behavior changed: $noninteractive_output" +bypass_output=$(PATH="$BAT_TEST_DIR:/usr/bin:/bin" HOME="$TMPDIR" bash --noprofile --rcfile "$ROOT/.bashrc" -ic 'printf payload | command cat' 2>/dev/null) +[[ $bypass_output == payload ]] || fail "command cat did not bypass the alias: $bypass_output" + +LOGIN_HOME="$TMPDIR/login-home" +mkdir -p "$LOGIN_HOME" +printf '# existing profile\n' >"$LOGIN_HOME/.profile" +ln -s "$ROOT/.bashrc" "$LOGIN_HOME/.bashrc" +( + export HOME="$LOGIN_HOME" MYBASH_SETUP_LIB_ONLY=1 PATH="$BAT_TEST_DIR:/usr/bin:/bin" + # shellcheck source=/dev/null + . "$ROOT/setup.sh" + # shellcheck disable=SC2034 # Consumed by the sourced setup function. + OS_NAME=Linux + ensure_login_profile_sources_bashrc + ensure_login_profile_sources_bashrc +) +# shellcheck disable=SC2016 # Match the literal profile command. +grep -Fq '. "$HOME/.bashrc"' "$LOGIN_HOME/.profile" || fail 'Linux login profile does not source .bashrc' +# shellcheck disable=SC2016 # Match the literal profile command. +[[ $(grep -Fc '. "$HOME/.bashrc"' "$LOGIN_HOME/.profile") -eq 1 ]] || fail 'login profile sources .bashrc more than once' +/bin/sh -c '. "$1"' sh "$LOGIN_HOME/.profile" >/dev/null 2>&1 || fail 'generated .profile is unsafe for non-Bash shells' + +PROFILE_HOME="$TMPDIR/bash-profile-home" +mkdir -p "$PROFILE_HOME" +# shellcheck disable=SC2016 # Write the literal ${HOME} profile form. +printf '%s\n' '. "${HOME}/.bashrc"' >"$PROFILE_HOME/.bash_profile" +( + export HOME="$PROFILE_HOME" MYBASH_SETUP_LIB_ONLY=1 + # shellcheck source=/dev/null + . "$ROOT/setup.sh" + # shellcheck disable=SC2034 # Consumed by the sourced setup function. + OS_NAME=Linux + ensure_login_profile_sources_bashrc +) +[[ $(grep -c '\.bashrc' "$PROFILE_HOME/.bash_profile") -eq 1 ]] || fail 'existing bash profile received a duplicate .bashrc source block' +[[ ! -e $PROFILE_HOME/.profile ]] || fail 'setup ignored existing .bash_profile precedence' + +VERIFY_HOME="$TMPDIR/verify-home" +mkdir -p "$VERIFY_HOME/bin" +cat >"$VERIFY_HOME/bin/batcat" <<'EOF' +#!/bin/sh +exit 0 +EOF +cat >"$VERIFY_HOME/bin/bash" <<'EOF' +#!/bin/sh +printf '%s\n' "$*" >"$HOME/bash-args" +printf "%s\n" "alias cat='batcat --paging=never --style=full'" >&3 +EOF +chmod +x "$VERIFY_HOME/bin/batcat" "$VERIFY_HOME/bin/bash" +( + export HOME="$VERIFY_HOME" MYBASH_SETUP_LIB_ONLY=1 PATH="$VERIFY_HOME/bin:/usr/bin:/bin" + # shellcheck source=/dev/null + . "$ROOT/setup.sh" + verify_interactive_cat_alias >/dev/null +) +grep -Fq -- '--login -ic alias cat >&3' "$VERIFY_HOME/bash-args" || fail 'alias verification did not use a login shell' + +printf 'Shell integration tests passed.\n' \ No newline at end of file