many bugfix, fixed getopt split
This commit is contained in:
+149
-62
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user