#!/bin/bash # # List environment variables grouped by the first part before underscore # protecting environment variables that possibly contain sensitive information. # # Author: Ismo Vuorinen 2025 # License: MIT # # vim: ft=bash fileencoding=utf-8 sw=2 ts=2 sts=2 et tw=100 # X_ENV_GROUPING is a file that contains custom groupings for environment variables. # The file should contain lines in the format "KEY:GROUP". One line per key. : "${X_ENV_GROUPING:=${XDG_CONFIG_HOME:-$HOME/.config}/zsh/env_list_grouping.yaml}" # Define protected keywords. Values of these keys are displayed as [protected value]. # The keys are case-insensitive and are matched as substrings. PROTECTED_KEYS=("*TOKEN*" "*SECRET*" "DIRENV_DIFF" "DIRENV_WATCHES") # Default grouping is based on the first part before underscore, but can be overridden # either by custom grouping file or by the get_custom_group function. # The following grouping is used by default and for example groups Golang environment variables # under the "GO" group. The keys BASH, COMMAND, FPATH, etc. are grouped under the "SHELL" group. DEFINED_GROUPS=( "AUTOSWITCH_VIRTUAL_ENV_DIR=PYTHON" "BASH=SHELL" "COMMAND=SHELL" "COMPLETION=SHELL" "DISABLE_LS_COLORS=SHELL" "FPATH=SHELL" "GOBIN=GO" "GOPATH=GO" "GOROOT=GO" "GREP=SHELL" "HIST=SHELL" "HISTCONTROL=SHELL" "HISTFILE=SHELL" "HISTIGNORE=SHELL" "HISTORY=SHELL" "HISTSIZE=SHELL" "HOME=SHELL" "INFOPATH=SHELL" "LESS=SHELL" "LESSHISTFILE=SHELL" "LOGNAME=SHELL" "MANPAGER=SHELL" "PAGER=SHELL" "PATH=SHELL" "PWD=SHELL" "PYENV_ROOT=PYTHON" "PYENV_SHELL=PYTHON" "PYTHONPATH=PYTHON" "POETRY_HOME=PYTHON" "RUSTUP_HOME=RUST" "RUST_WITHOUT=RUST" "SHELL=SHELL" "TMPDIR=SHELL" "USER=SHELL" "SECURITYSESSIONID=SHELL" "SHLVL=SHELL" "WORKON_HOME=PYTHON" "ZSH=ZSH" "LANG=SHELL" "EDITOR=SHELL" "VISUAL=SHELL" "COMMAND_MODE=SHELL" "COLORTERM=SHELL" "CARGO_BIN_HOME=RUST" "CARGO_HOME=RUST" "LaunchInstanceID=SHELL" "SECURITYSESSIONID=SHELL" "TERM=SHELL" "TERM_PROGRAM=SHELL" "TERM_PROGRAM_VERSION=SHELL" "XPC_FLAGS=SHELL" "XPC_SERVICE_NAME=SHELL" "NPM_CONFIG_PREFIX=NODE" "YARN_GLOBAL_FOLDER=NODE" "MASON_HOME=NVIM" "asdf_data_dir=ASDF" "nvm_current_version=NODE" "NVM_NODE_BIN_DIR=NODE" "_=SHELL" "npm_config_cache=NPM" ) SKIPPED_KEYS=( "_tide*" "__FISH_*" "___paths_plugin_colors" "__CFBundleIdentifier" "__CF_USER_TEXT_ENCODING" "PATH" "FPATH" ) CONFIG_FILE="$X_ENV_GROUPING" # If we have configuration file, run extra checks so we can process it. if [[ -f "$CONFIG_FILE" ]]; then # Check if yq is installed if ! command -v yq &> /dev/null; then echo "Error: yq is not installed. Please install it to proceed." >&2 exit 1 fi # Validate the YAML file if ! yq '.' "$CONFIG_FILE" &> /dev/null; then echo "Error: Invalid YAML structure in '$CONFIG_FILE'." >&2 exit 1 fi # Check if required keys exist in the YAML structure if ! yq '.custom_grouping, .protected_keys' "$CONFIG_FILE" &> /dev/null; then echo "Error: Missing required keys ('custom_grouping' or 'protected_keys') in '$CONFIG_FILE'." >&2 exit 1 fi # If X_ENV_GROUPING is set, it will be used as the file path for custom grouping, and # protected keys will be read from the file. The values in the file will be appended to the # processing algorithm. CUSTOM_KEYS=$(yq '.protected_keys[]' "$CONFIG_FILE") while IFS= read -r key; do # Add to default_protected_keys PROTECTED_KEYS+=("$key") done <<< "$CUSTOM_KEYS" SKIPPED+=("$(yq '.skipped_keys[]' "$CONFIG_FILE")") while IFS= read -r key; do # Add to default_skipped_keys SKIPPED_KEYS+=("$key") done <<< "$SKIPPED" CUSTOM_GROUPS=$(yq '.custom_grouping[]' "$CONFIG_FILE") while IFS= read -r group; do group_name=$(echo "$group" | yq 'keys[0]') GROUP_KEYS=$(yq ".custom_grouping[] | .[\"$group_name\"][]" "$CONFIG_FILE") while IFS= read -r key; do # Add to default_custom_grouping in "GROUP=KEY" format DEFINED_GROUPS+=("$group_name=$key") done <<< "$GROUP_KEYS" done <<< "$CUSTOM_GROUPS" fi if [[ -f "$X_ENV_GROUPING" ]]; then while IFS=':' read -r key group; do DEFINED_GROUPS+=("$key=$group") done < "$X_ENV_GROUPING" fi # Check if the key is in the protected keywords list is_protected() { local key=$1 for protected_key in "${PROTECTED_KEYS[@]}"; do # Direct match if [[ "$key" == "$protected_key" ]]; then return 0 fi # Wildcard match (protected_key contains '*') if [[ "$protected_key" == *"*"* ]] && [[ "$key" == $protected_key ]]; then return 0 fi done return 1 } # Custom function to determine a custom group. # # If custom grouping file was found and was read, # the default grouping was already overridden. get_custom_group() { local key=$1 for entry in "${DEFINED_GROUPS[@]}"; do local mapping_key=${entry%%=*} local mapping_group=${entry#*=} if [[ $key == "$mapping_key" ]]; then echo "$mapping_group" return 0 fi done # Automatically create TOKENS group if the key contains "TOKEN". if [[ $key == *TOKEN* ]]; then echo "TOKENS" return 0 fi return 1 } is_skipped() { local key=$1 for skipped_key in "${SKIPPED_KEYS[@]}"; do # Direct match if [[ "$key" == "$skipped_key" ]]; then return 0 fi # Wildcard match (skipped_key contains '*') if [[ "$skipped_key" == *"*"* ]] && [[ "$key" == $skipped_key ]]; then return 0 fi done return 1 } # Create arrays to store all groups, group data and max lengths for each group all_groups=() group_data=() group_max_lengths=() # Get environment variables and group them while IFS='=' read -r key value; do # Skip keys that are in the skipped list if is_skipped "$key"; then continue fi if is_skipped "$value"; then continue fi # Check for custom group group=$(get_custom_group "$key") # If there is no custom group, use the default algorithm: # 1) First part before underscore is used as the group name. # 2) If the key starts with an underscore, the group is determined by the second part. # 3) If the key does not contain an underscore, the group is the key itself. if [[ -z $group ]]; then if [[ $key == _* ]]; then group="${key#_}" group="${group%%_*}" [[ -z $group ]] && group="Ungrouped" else group="${key%%_*}" [[ -z $group ]] && group="Ungrouped" fi fi # Hide values of protected keys if is_protected "$key"; then value="[protected value]" fi # Update group data if [[ ! " ${all_groups[*]} " =~ " $group " ]]; then all_groups+=("$group") fi group_data+=("$group|$key|$value") key_length=${#key} for i in "${!all_groups[@]}"; do if [[ ${all_groups[$i]} == "$group" ]]; then if [[ ${group_max_lengths[$i]:-0} -lt $key_length ]]; then group_max_lengths[i]=$key_length fi break fi done done < <(env | sort | awk -F'=' '{print $1"="$2}') # Print groups in order, "Ungrouped" last sorted_groups=() while IFS= read -r line; do sorted_groups+=("$line") done < <(printf "%s\n" "${all_groups[@]}" | grep -v "^Ungrouped$" | sort) sorted_groups+=("Ungrouped") for group in "${sorted_groups[@]}"; do echo -e "\n# $group" for i in "${!all_groups[@]}"; do if [[ ${all_groups[$i]} == "$group" ]]; then max_length=${group_max_lengths[$i]} break fi done for entry in "${group_data[@]}"; do IFS='|' read -r g k v <<< "$entry" if [[ $g == "$group" ]]; then printf "%-*s = %s\n" "$max_length" "$k" "$v" fi done done