Files
dotfiles/local/bin/x-env-list
Ismo Vuorinen 6d72003446 fix(lint): fix all sonarcloud detected issues (#279)
* fix(ci): replace broad permissions with specific scopes in workflows

Replace read-all/write-all with minimum required permission scopes
across all GitHub Actions workflows to follow the principle of least
privilege (SonarCloud rule githubactions:S8234).

* fix(shell): use [[ instead of [ for conditional tests

Replace single brackets with double brackets in bash conditional
expressions across 14 files (28 changes). All scripts use bash
shebangs so [[ is safe everywhere (SonarCloud rule shelldre:S7688).

* fix(shell): add explicit return statements to functions

Add return 0 as the last statement in ~46 shell functions across
17 files that previously relied on implicit return codes
(SonarCloud rule shelldre:S7682).

* fix(shell): assign positional parameters to local variables

Replace direct $1/$2/$3 usage with named local variables in _log(),
msg(), msg_err(), msg_done(), msg_run(), msg_ok(), and array_diff()
(SonarCloud rule shelldre:S7679).

* fix(python): replace dict() constructor with literal

Use {} instead of dict() for empty dictionary initialization
(SonarCloud rule python:S7498).

* fix(shell): fix husky shebang and tolerate npm outdated exit code

* docs(shell): add function docstring comments

* fix(shell): fix heredoc indentation in x-sonarcloud

* feat(python): add ruff linter and formatter configuration

* fix(ci): align megalinter config with biome, ruff, and shfmt settings

* fix(ci): disable black and yaml-prettier in megalinter config

* chore(ci): update ruff-pre-commit to v0.15.0 and fix hook name

* fix(scripts): check for .git dir before skipping clone in install-fonts

* fix(shell): address code review issues in scripts and shared.sh

- Guard wezterm show-keys failure in create-wezterm-keymaps.sh
- Stop masking git failures with return 0 in install-cheat-purebashbible.sh
- Add missing shared.sh source in install-xcode-cli-tools.sh
- Replace exit 1 with return 1 in sourced shared.sh

* fix(scripts): address code review and security findings

- Guard wezterm show-keys failure in create-wezterm-keymaps.sh
- Stop masking git failures with return 0 in install-cheat-purebashbible.sh
- Add missing shared.sh source in install-xcode-cli-tools.sh
- Replace exit 1 with return 1 in sourced shared.sh
- Remove shell=True subprocess calls in x-git-largest-files.py

* style(shell): apply shfmt formatting and add args to pre-commit hook

* fix(python): suppress bandit false positives in x-git-largest-files

* fix(python): add nosemgrep suppression for check_output call

* feat(format): add prettier for YAML formatting

Install prettier, add .prettierrc.json config (200-char width, 2-space
indent, LF endings), .prettierignore, yarn scripts (lint:prettier,
fix:prettier, format:yaml), and pre-commit hook scoped to YAML files.

* style(yaml): apply prettier formatting

* fix(scripts): address remaining code review findings

- Python: use list comprehension to filter empty strings instead of
  slicing off the last element
- create-wezterm-keymaps: write to temp file and mv for atomic updates
- install-xcode-cli-tools: fix shellcheck source directive path

* fix(python): sort imports alphabetically in x-git-largest-files

* fix(lint): disable PYTHON_ISORT in MegaLinter, ruff handles it

* chore(git): add __pycache__ to gitignore

* fix(python): rename ambiguous variable l to line (E741)

* style: remove trailing whitespace and blank lines

* style(fzf): apply shfmt formatting

* style(shell): apply shfmt formatting

* docs(plans): add design documents

* style(docs): add language specifier to fenced code block

* feat(lint): add markdown-table-formatter to dev tooling

Add markdown-table-formatter as a dev dependency with yarn scripts
(lint:md-table, fix:md-table) and a local pre-commit hook to
automatically format markdown tables on commit.
2026-02-07 19:01:02 +02:00

301 lines
7.9 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# List environment variables grouped by the first part before underscore
# protecting environment variables that possibly contain sensitive information.
#
# Author: Ismo Vuorinen <https://github.com/ivuorinen> 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"
mapfile -t SKIPPED < <(yq '.skipped_keys[]' "$CONFIG_FILE")
for key in "${SKIPPED[@]}"; do
# Add to default_skipped_keys
SKIPPED_KEYS+=("$key")
done
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 '*')
# shellcheck disable=SC2053 # Intentional glob matching - protected_key contains wildcard patterns
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
}
# Check if a key matches the skipped keys list
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 '*')
# shellcheck disable=SC2053 # Intentional glob matching - skipped_key contains wildcard patterns
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 - check if group already exists
group_exists=false
for existing_group in "${all_groups[@]}"; do
if [[ "$existing_group" == "$group" ]]; then
group_exists=true
break
fi
done
if [[ "$group_exists" == false ]]; 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