Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions plugins/available/safe_autoenv.plugin.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# shellcheck shell=bash
# BASH_IT_LOAD_PRIORITY: 200

# Autoenv Plugin for Bash-it with automatic cleanup
# Automatically loads .env files when changing directories
# Automatically unsets variables when leaving directories

cite about-plugin
about-plugin 'Automatically loads .env files and cleans up when leaving directories'

# Global tracking for autoenv state
declare -A _AUTOENV_PROCESSED_DIRS # Track processed directories
declare -A _AUTOENV_DIR_VARS # Map directories to their variables
declare -A _AUTOENV_VAR_SOURCES # Map variables to their source directories
declare _AUTOENV_LAST_PWD="" # Track last working directory

_autoenv_init() {
typeset target home _file current_dir
typeset -a _files
target=$1
home="${HOME%/*}"

_files=($(
current_dir="$target"
while [[ "$current_dir" != "/" && "$current_dir" != "$home" ]]; do
_file="$current_dir/.env"
if [[ -e "${_file}" ]]; then
echo "${_file}"
fi
# Move to parent directory
current_dir="$(dirname "$current_dir")"
done
))
Comment on lines +23 to +33

Check warning

Code scanning / shellcheck

SC2207 Warning

Prefer mapfile or read -a to split command output (or quote to avoid splitting).

# Process files in reverse order (from root to current directory)
local _file_count=${#_files[@]}
local i
for ((i = _file_count - 1; i >= 0; i--)); do
local env_file="${_files[i]}"
local env_dir="$(dirname "$env_file")"

Check warning

Code scanning / shellcheck

SC2155 Warning

Declare and assign separately to avoid masking return values.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you install the pre-commit in this repo, it will detect this kind of stuff before you push any commits unfixed. thanks!


# Only process if this directory hasn't been processed yet
# OR if it was processed but its variables were cleaned up
if [[ -z "${_AUTOENV_PROCESSED_DIRS[$env_dir]}" ]] || [[ -z "${_AUTOENV_DIR_VARS[$env_dir]}" ]]; then
echo "Processing $env_file"
_autoenv_process_file "$env_file" "$env_dir"
_AUTOENV_PROCESSED_DIRS[$env_dir]=1
fi
done
}

_autoenv_process_file() {
local env_file="$1"
local env_dir="$2"
local line key value original_line line_number=0
local -a dir_vars=()

# Check if file is readable
if [[ ! -r "$env_file" ]]; then
echo "Warning: Cannot read $env_file" >&2
return 1
fi

# Read the file line by line
while IFS= read -r line || [[ -n "$line" ]]; do
((line_number++))
original_line="$line"

# Skip empty lines and comments
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue

# Remove leading whitespace
line="${line#"${line%%[![:space:]]*}"}"

# Check if line matches KEY=value pattern
if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
key="${BASH_REMATCH[1]}"
value="${BASH_REMATCH[2]}"

# Validate key name (additional safety check)
if [[ ! "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
echo "Warning: Invalid variable name '$key' at line $line_number in $env_file" >&2
continue
fi

# Handle quoted values (remove outer quotes if present)
if [[ "$value" =~ ^\"(.*)\"$ ]]; then
value="${BASH_REMATCH[1]}"
elif [[ "$value" =~ ^\'(.*)\'$ ]]; then
value="${BASH_REMATCH[1]}"
fi

# Export the variable silently
export "$key=$value"

# Track the variable
dir_vars+=("$key")
_AUTOENV_VAR_SOURCES["$key"]="$env_dir"

else
echo "Warning: Skipping invalid line $line_number in $env_file: $original_line" >&2
fi
done < "$env_file"

# Store variables for this directory
if [[ ${#dir_vars[@]} -gt 0 ]]; then
_AUTOENV_DIR_VARS["$env_dir"]="${dir_vars[*]}"
fi
}

# Check if a directory is an ancestor of the current directory
_autoenv_is_ancestor() {
local ancestor="$1"
local current="$PWD"

# Normalize paths (remove trailing slashes)
ancestor="${ancestor%/}"
current="${current%/}"

# Check if current path starts with ancestor path
[[ "$current" == "$ancestor"* ]]
}

# Clean up variables from directories we've left
_autoenv_cleanup() {
local dir var_list var

# Check each directory we've processed
for dir in "${!_AUTOENV_DIR_VARS[@]}"; do
# If this directory is no longer an ancestor of current directory
if ! _autoenv_is_ancestor "$dir"; then
var_list="${_AUTOENV_DIR_VARS[$dir]}"

# Unset each variable from this directory
for var in $var_list; do
if [[ "${_AUTOENV_VAR_SOURCES[$var]}" == "$dir" ]]; then
echo "Unsetting $var (from $dir)"
unset "$var"
unset "_AUTOENV_VAR_SOURCES[$var]"
fi
done

# Remove directory from tracking AND clear its processed status
unset "_AUTOENV_DIR_VARS[$dir]"
unset "_AUTOENV_PROCESSED_DIRS[$dir]"
fi
done
}

# Main prompt command function
_autoenv_prompt_command() {
local current_dir="$PWD"

# If directory changed, perform cleanup first
if [[ "$current_dir" != "$_AUTOENV_LAST_PWD" ]]; then
_autoenv_cleanup
_AUTOENV_LAST_PWD="$current_dir"
fi

# Always try to initialize the current directory and its ancestors
# The _autoenv_init function will handle checking if processing is needed
_autoenv_init "$current_dir"
}

# Public function for manual use
autoenv() {
case "$1" in
"reload"|"refresh")
# Clear all tracking and reload
_autoenv_clear_all
_autoenv_init "$PWD"
;;
"status")
echo "Processed directories:"
for dir in "${!_AUTOENV_PROCESSED_DIRS[@]}"; do
echo " $dir"
done
echo
echo "Active variables by directory:"
for dir in "${!_AUTOENV_DIR_VARS[@]}"; do
echo " $dir: ${_AUTOENV_DIR_VARS[$dir]}"
done
echo
echo "Variable sources:"
for var in "${!_AUTOENV_VAR_SOURCES[@]}"; do
echo " $var -> ${_AUTOENV_VAR_SOURCES[$var]}"
done
;;
"clean"|"cleanup")
_autoenv_cleanup
;;
"clear")
_autoenv_clear_all
;;
*)
_autoenv_init "${1:-$PWD}"
;;
esac
}

# Clear all autoenv state and unset tracked variables
_autoenv_clear_all() {
local var

# Unset all tracked variables
for var in "${!_AUTOENV_VAR_SOURCES[@]}"; do
echo "Clearing $var"
unset "$var"
done

# Clear all tracking arrays
unset _AUTOENV_PROCESSED_DIRS
unset _AUTOENV_DIR_VARS
unset _AUTOENV_VAR_SOURCES

# Reinitialize arrays
declare -A _AUTOENV_PROCESSED_DIRS
declare -A _AUTOENV_DIR_VARS
declare -A _AUTOENV_VAR_SOURCES

_AUTOENV_LAST_PWD=""
}

# Hook into bash-it's prompt command system
safe_append_prompt_command '_autoenv_prompt_command'
Loading