many bugfix, fixed getopt split

This commit is contained in:
2026-10-13 16:01:13 +02:00
parent b5be992d82
commit ba1e07744e
22 changed files with 478 additions and 125 deletions
+149 -62
View File
@@ -35,31 +35,38 @@
# * OF SUCH DAMAGE.
# ------------------------------------------------------------------------------
# This first test is POSIX on purpose: it runs before anything bash-specific, so
# that sourcing the file from dash, zsh, ... ends with a message instead of
# cascading errors. It checks the running shell: $SHELL is the user's login
# shell, which says nothing about it (and is unset under cron or systemd).
if [ -z "${BASH_VERSION:-}" ]; then
echo "That environment script is designed to be used with bash." >&2
echo "Please consider using bash to enjoy our features!" >&2
return 1 2>/dev/null || exit 1
fi
_profile_is_sourced()
{
[[ "${BASH_SOURCE[0]}" != "$0" ]]
}
_profile_finish()
{
local rc="${1:-0}"
if _profile_is_sourced; then
return "$rc"
fi
exit "$rc"
}
# Installation helper: write $2 (the source line) in the file $1, unless that
# file already sources $3 (the profile) in some form.
_profile_install_in_file()
{
local rc_file="$1"
local source_line="$2"
local target="$3"
[[ -f "$rc_file" ]] || touch "$rc_file" || {
printf "[ Error ] Cannot create %s\n" "$rc_file" >&2
return 1
}
if grep -Fqx "$source_line" "$rc_file"; then
# The exact line, or any other sourcing statement naming the same file
# (". ~/p/profile.sh", with other quotes, ...): adding another one would
# load the profile twice.
if grep -Fqx "$source_line" "$rc_file" || _profile_file_sources "$rc_file" "$target"; then
printf "[ Info ] Already configured in %s\n" "$rc_file"
return 0
fi
@@ -145,56 +152,62 @@ _profile_install()
fi
script_dir=$(dirname "$(realpath -s "${BASH_SOURCE[0]}")")
source_line="source \"$script_dir/profile.sh\""
# The path goes between double quotes in the rc file: escape what stays
# special there (a plain path is left unchanged, so existing lines keep
# matching)
local escaped_dir="$script_dir"
escaped_dir="${escaped_dir//\\/\\\\}"
escaped_dir="${escaped_dir//\"/\\\"}"
escaped_dir="${escaped_dir//\$/\\\$}"
escaped_dir="${escaped_dir//\`/\\\`}"
source_line="source \"$escaped_dir/profile.sh\""
if (( install_bashrc == 1 )); then
_profile_install_in_file "$HOME/.bashrc" "$source_line" || rc=$?
_profile_install_in_file "$HOME/.bashrc" "$source_line" "$script_dir/profile.sh" || rc=$?
fi
if (( install_profile == 1 )); then
_profile_install_in_file "$HOME/.profile" "$source_line" || rc=$?
_profile_install_in_file "$HOME/.profile" "$source_line" "$script_dir/profile.sh" || rc=$?
fi
return "$rc"
}
if [[ $# -gt 0 ]]; then
case "$1" in
# Options are only handled when the file is executed: a sourced file receives
# the positional parameters of whoever sources it ("./script --install" sourcing
# the profile must not install anything, nor fail on an option that is not for
# us).
if ! _profile_is_sourced; then
case "${1:-}" in
--install)
shift
_profile_install "$@"
_profile_finish $?
exit $?
;;
-h|--help)
printf "Usage: source %s\n" "${BASH_SOURCE[0]}"
printf " %s --install [--bashrc] [--profile]\n" "${BASH_SOURCE[0]}"
_profile_finish 0
exit 0
;;
"")
;;
*)
printf "[ Error ] Unknown option: %s\n" "$1" >&2
_profile_finish 2
exit 2
;;
esac
fi
if ! _profile_is_sourced; then
printf "[ Warning ] profile.sh is designed to be sourced, not executed directly.\n" >&2
printf "Use: source \"%s\"\n" "$(realpath -s "${BASH_SOURCE[0]}")" >&2
printf "Or run: %s --install [--bashrc] [--profile]\n" "${BASH_SOURCE[0]}" >&2
exit 1
fi
if [[ ! $SHELL =~ bash ]]; then
echo "That environment script is designed to be used with bash being the shell."
echo "Please consider using bash to enjoy our features!"
_profile_finish 1
fi
# Required for associative arrays (4.0+) and namerefs (4.3+)
if ((BASH_VERSINFO[0] < 4)) || [[ ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -lt 3 ]]; then
echo "[ Error ] This profile requires Bash 4.3 or higher."
echo "Current version: $BASH_VERSION"
(return 0 2>/dev/null) && return 1 || exit 1
echo "[ Error ] This profile requires Bash 4.3 or higher." >&2
echo "Current version: $BASH_VERSION" >&2
return 1
fi
# ------------------------------------------------------------------------------
@@ -227,6 +240,10 @@ pathremove()
# export "$pathvar=$1${!pathvar:+:${!pathvar}}"
#}
# Append one directory to a path variable (PATH by default), unless it is
# already in it: an entry that exists keeps its place. Moving it to the end
# would, for instance, demote ~/.local/bin that Debian's ~/.profile puts first,
# so that system programs shadow the user's own ones.
pathappend()
{
[[ -z "$1" ]] && return 0
@@ -235,7 +252,9 @@ pathappend()
printf "pathappend: unsafe variable name '%s'\n" "$pathvar" >&2
return 1
}
pathremove "$1" "$pathvar"
case ":${!pathvar}:" in
*":$1:"*) return 0 ;;
esac
export "$pathvar=${!pathvar:+${!pathvar}:}$1"
}
# ------------------------------------------------------------------------------
@@ -260,13 +279,21 @@ parse_conf()
# Skip comments and empty lines
[[ -z "$key" || "$key" =~ ^[#\;] ]] && continue
# Section Detection: [section_name]
if [[ "$key" =~ ^\[([a-zA-Z0-9_]+)\]$ ]]; then
# Section Detection: [section_name], optionally followed by a comment
if [[ "$key" =~ ^\[([a-zA-Z0-9_]+)\][[:space:]]*([#\;].*)?$ ]]; then
current_section="${BASH_REMATCH[1]}"
declare -g -A "CONF_$current_section"
continue
fi
# A key is a variable name: it ends up in "export" (load_conf) and as an
# array subscript. Anything else (a stray line, a typo) is reported.
if [[ ! "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
printf "[ Warning ] %s: invalid configuration line ignored: '%s'\n" \
"$config_file" "$key" >&2
continue
fi
# Secure Assignment (if inside a section)
if [[ -n "$current_section" ]]; then
# Clean the value
@@ -274,6 +301,20 @@ parse_conf()
value="${value#"${value%%[![:space:]]*}"}"
value="${value%$'\r'}"
# A quoted value is what is between the quotes (a trailing comment
# is dropped); an unquoted one ends at an inline comment, which is
# a "#" preceded by a blank ("dark # my theme").
if [[ "$value" == \"* ]]; then
value="${value#\"}"
value="${value%%\"*}"
elif [[ "$value" == \'* ]]; then
value="${value#\'}"
value="${value%%\'*}"
else
value="${value%%[[:space:]]#*}"
value="${value%"${value##*[![:space:]]}"}"
fi
# Protect against command injection by disallowing certain characters in keys
value="${value//\`/}"
value="${value//\$\(/}"
@@ -298,10 +339,6 @@ parse_conf()
# If envsubst is unavailable, $VAR references are left as-is.
fi
# Strip quotes (handling both " and ')
value="${value%\"}"; value="${value#\"}"
value="${value%\'}"; value="${value#\'}"
# Use a nameref for safe, eval-free assignment
local -n current_array="CONF_$current_section"
# shellcheck disable=SC2034 # Dynamic var creation
@@ -323,6 +360,7 @@ load_alias()
# Create a nameref to the section array
local -n current_aliases="$section_name"
local key
# Iterate safely over the keys of the associative array
for key in "${!current_aliases[@]}"; do
@@ -351,6 +389,7 @@ load_conf()
[[ "$(declare -p "$section_name" 2>/dev/null)" != "declare -A"* ]] && return 0
local -n current_vars="$section_name"
local key
for key in "${!current_vars[@]}"; do
# Export the key/value pair as a standard shell variable
@@ -367,32 +406,41 @@ load_conf()
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Store script's path (realpath -s resolve symlinks if profile.sh is a symlink)
# Because we're more likely to be sourced, we use BASH_SOURCE to get the path
# of the sourced file instead of $0
if [[ -z "$PROFILE_PATH" ]]; then
MYPATH=$(dirname "$(realpath -s "${BASH_SOURCE[0]}")")
# Store the script's path. Because we're more likely to be sourced, we use
# BASH_SOURCE to get the path of the sourced file instead of $0. If profile.sh
# itself is a symbolic link, it is followed to find the real installation
# (profile.d, version, ...); a symlinked directory is kept as the user sees it
# (realpath -s).
if [[ -z "${PROFILE_PATH:-}" ]]; then
_profile_src="${BASH_SOURCE[0]}"
[[ -L "$_profile_src" ]] && _profile_src=$(readlink -f -- "$_profile_src")
MYPATH=$(dirname "$(realpath -s -- "$_profile_src")")
unset _profile_src
else
MYPATH="$PROFILE_PATH"
fi
export MYPATH
if [[ ! -e "$MYPATH/profile.sh" ]]; then
echo "[ Warning ] Path detection failed, trying to use pwd..."
echo "[ Warning ] Path detection failed, trying to use pwd..." >&2
MYPATH=$(pwd)
if [[ ! -e "$MYPATH/profile.sh" ]]; then
echo "[ Error ] Unable to determine installation path, pretty much nothing will work."
echo "[ Error ] Unable to determine installation path, pretty much nothing will work." >&2
fi
fi
if [[ ! -s "$MYPATH/version" ]]; then
echo "[ Warning ] Unable to determine running profile version; your installation might be broken."
PROFVERSION=""
if [[ -s "$MYPATH/version" ]]; then
IFS= read -r PROFVERSION < "$MYPATH/version"
PROFVERSION="${PROFVERSION%$'\r'}"
else
echo "[ Warning ] Unable to determine running profile version; your installation might be broken." >&2
fi
PROFVERSION=$(cat "$MYPATH"/version)
export PROFVERSION
# Build PATH environment variable
if [[ $EUID -eq 0 ]]; then
pathappend /sbin:/usr/sbin
pathappend /sbin
pathappend /usr/sbin
fi
[[ -d ~/bin ]] && pathappend ~/bin
[[ -d ~/.local/bin ]] && pathappend ~/.local/bin
@@ -407,22 +455,31 @@ fi
load_conf system # Load Bash system behavior configuration (history, pager, etc.)
load_conf general # General purpose configuration (compilation flags, etc.)
# Load module scripts
# Load module scripts. nullglob is only needed for the expansion: the caller's
# setting is put back afterwards instead of being forced off.
_profile_ng=0
shopt -q nullglob && _profile_ng=1
shopt -s nullglob
for script in "$MYPATH/profile.d/"*.sh; do
if [[ -f "$script" && -r "$script" ]]; then
for _profile_script in "$MYPATH/profile.d/"*.sh; do
if [[ -f "$_profile_script" && -r "$_profile_script" ]]; then
# shellcheck source=/dev/null
. "$script" || printf "[ Warning ] Failed to source module: %s\n" "$script" >&2
. "$_profile_script" || printf "[ Warning ] Failed to source module: %s\n" "$_profile_script" >&2
fi
done
shopt -u nullglob
unset _profile_script
(( _profile_ng )) || shopt -u nullglob
# Interactive shell detection, two methods available each one of those might have different result
# depending on distribution
#shopt -q login_shell && INTERACTIVE=1
[[ $- == *i* ]] && export INTERACTIVE=1
# Interactive shell detection. INTERACTIVE is exported for the modules, so it
# is reset when the shell is not interactive: a non-interactive child of an
# interactive shell would otherwise inherit INTERACTIVE=1 and run the whole
# interactive part (banner, prompt, update check) inside scripts.
if [[ $- == *i* ]]; then
export INTERACTIVE=1
else
unset INTERACTIVE
fi
if [[ $INTERACTIVE ]]; then
if [[ -n "${INTERACTIVE:-}" ]]; then
# Load custom bash completions
shopt -s nullglob
for _compl in "$MYPATH/profile.d/bash-completion/"*.sh; do
@@ -430,14 +487,43 @@ if [[ $INTERACTIVE ]]; then
[[ -f "$_compl" && -r "$_compl" ]] && . "$_compl"
done
unset _compl
shopt -u nullglob
(( _profile_ng )) || shopt -u nullglob
# Aliases
load_alias aliases
load_alias aliases
# Define PS1
# Define PS1. The timer hook is a DEBUG trap: it replaces any DEBUG trap set
# before (it cannot even be read from a sourced file, bash hides it there),
# so load this profile before tools such as bash-preexec.
trap 'timer_start' DEBUG
PROMPT_COMMAND='set_prompt'
# set_prompt is added to PROMPT_COMMAND instead of replacing it, so that
# what exists (VTE's directory reporting, direnv, "history -a", ...) keeps
# working. It has to read the exit status of the user's last command, which
# any command running before it resets: _profile_save_status stores it
# first, set_prompt (which uses it) runs last. It must also run last for the
# timer: the DEBUG trap fires for every command of PROMPT_COMMAND, and the
# elapsed time is measured from the first one of the user's command.
# Nothing changes when PROMPT_COMMAND is empty or already has set_prompt.
case "$(declare -p PROMPT_COMMAND 2>/dev/null)" in
"declare -a"*)
# Array form (bash 5.1+)
if [[ " ${PROMPT_COMMAND[*]} " != *" set_prompt "* ]]; then
if (( ${#PROMPT_COMMAND[@]} )); then
PROMPT_COMMAND=(_profile_save_status "${PROMPT_COMMAND[@]}" set_prompt)
else
PROMPT_COMMAND=(set_prompt)
fi
fi
;;
*)
if [[ -z "${PROMPT_COMMAND:-}" ]]; then
PROMPT_COMMAND='set_prompt'
elif [[ "$PROMPT_COMMAND" != *set_prompt* ]]; then
PROMPT_COMMAND="_profile_save_status;${PROMPT_COMMAND};set_prompt"
fi
;;
esac
# Set default language from DEFAULT_LANG config key (set in [general]).
# The value must match one of the alias names defined in SET_LOCALE so that
@@ -455,9 +541,10 @@ if [[ $INTERACTIVE ]]; then
check_updates -q
disp I "Profile version $PROFVERSION loaded..."
fi
unset _profile_ng
# Cleanup
unset -f _profile_is_sourced _profile_finish _profile_install_in_file _profile_file_sources _profile_install
unset -f _profile_is_sourced _profile_install_in_file _profile_file_sources _profile_install
unset -f parse_conf load_alias load_conf
unset -f pathremove pathprepend pathappend