This is what I think an acceptable implementation of `source -i` should look like:
-------------------- #!/bin/bash if [[ BASH_VERSINFO -ge 5 && ${SOURCE_EXTENSIONS_LOADED-} != true ]]; then function source._die { printf '%s\n' "$1" >&2 exit "${2-1}" } function source._get_calling_script_dir { [[ ${BASH_SOURCE[3]+.} ]] || source._die "Calling script's location unknown." [[ ${BASH_SOURCE[3]} == /* ]] || source._die "Calling script's path not absolute." _calling_script_dir=${BASH_SOURCE[2]%/*} _calling_script_dir=${_calling_script_dir:-/} } function source._a { local paths realpath p _calling_script_dir=() IFS=: read -r paths <<< "$1" for p in "${paths[@]}"; do [[ $p ]] || continue if [[ $p != /* ]]; then [[ ${_calling_script_dir+.} ]] || source._get_calling_script_dir p=${_calling_script_dir}/$p fi realpath=$(realpath -Pm -- "$p") || source._die "Failed to get realpath of '$p'." BASH_SOURCE_PATH+=${BASH_SOURCE_PATH:+:}${realpath} done } function source._I { declare -gA BASH_SOURCE_INCLUDED local filename=$2 realpath shift 2 realpath=$(realpath -Pe -- "${filename}") || \ source._die "Failed to get realpath of '${filename}'." if [[ -z ${BASH_SOURCE_INCLUDED[${realpath}]+.} ]]; then BASH_SOURCE_INCLUDED[${realpath}]=. command source -- "${realpath}" "$@" fi } function source._i { local callback=$1 filename=$2 main_script_dir=() p _calling_script_dir=() shift 2 if [[ ${filename} == @(/*|./*|../*) ]]; then if [[ $1 != /* ]]; then [[ ${_calling_script_dir+.} ]] || source._get_calling_script_dir filename=${_calling_script_dir}/${filename} fi [[ -e ${filename} ]] || source._die "File doesn't exist: ${filename}" "${callback}" -- "${filename}" "$@" else IFS=: read -r paths <<< "${BASH_SOURCE_PATH}" [[ ${#paths[@]} -gt 0 ]] || paths=(.) for p in "${paths[@]}"; do [[ $p ]] || continue if [[ $p != /* ]]; then if [[ -z ${main_script_dir+.} ]]; then [[ ${#BASH_SOURCE[@]} -gt 2 ]] || source._die "Main script's location unknown." [[ ${BASH_SOURCE[-1]} == /* ]] || source._die "Main script's path isn't absolute." main_script_dir=${BASH_SOURCE[-1]%/*} main_script_dir=${main_script_dir:-/} fi p=${main_script_dir}/$p fi if [[ -e ${p}/${filename} ]]; then "${callback}" -- "${p}/${filename}" "$@" return fi done source._die "File not found in BASH_SOURCE_PATH: ${filename}" fi } function source { local mode= while [[ $# -gt 0 ]]; do case $1 in -[aA]) [[ ${2+.} ]] || source._die "No argument specified to $1." [[ $1 == -A ]] && BASH_SOURCE_PATH= source._a "$2" shift ;; -[iI]) mode=${1#-} ;; --) shift break ;; -?*) source._die "Invalid option: $1" ;; *) break ;; esac shift done [[ ${1+.} ]] || source._die "Filename argument required" [[ $1 ]] || source._die "Invalid empty filename" if [[ ${mode} == i ]]; then source._i source "$@" elif [[ ${mode} == I ]]; then source._i source._I "$@" else command source -- "$@" fi } SOURCE_EXTENSIONS_LOADED=true fi -------------------- The other features like `source -I`, `source -a` and `source -A` are optional. `source -I` is the same as `source -i` but it only allows a file to be source'd if it hasn't been included using `source -I` yet. `source -a` adds paths to BASH_SOURCE_PATH in their realpath format using the calling script as reference. `source -A` works the same as `source -a` except it overrides the values in BASH_SOURCE_PATH. Details on how `source -i` works are the following: - Paths beginning with /, ./, or ../ aren't searched in BASH_SOURCE_PATH. - Paths beginning with ./ or ../ use the calling script's directory as reference. - Paths not beginning with /, ./, or ../ are searched in BASH_SOURCE_PATH. - If BASH_SOURCE_PATH is unset, '.' is used as a default value. - If a path in BASH_SOURCE_PATH points to a relative location, it uses the directory of the main script as reference. The main script is basically ${BASH_SOURCE[-1]}. - If "main script" is unset, it won't rely on PWD to look for the script that was specified using a relative path instead. - No part of `source -i` will rely on PWD. source without -i already does that. - The main script is the only reliable reference that is least likely to change. Relying on changing values like PWD is broken behavior even as a fallback. Besides those I'd like to emphasize that: - I only added the ". being a default value to BASH_SOURCE_PATH" and "BASH_SOURCE_PATH allowing relative pathnames" features for the sake of making the example implementation complete but: - I think BASH_SOURCE_PATH having a default value makes things a little bit less predictable. For example, if a script from a different location source's another script that relies on the default BASH_SOURCE_PATH value, that meaning of that default value will change for the second script and will cause the second script's sub-scripts to not load. It's just a confusing behavior so I suggest to just not have a default value instead. People should explicitly specify the locations where scripts will be looked up. - Relative paths in BASH_SOURCE_PATH should just be ignored and people should rely on a helper feature like `source -a` to add complete paths instead. Even the least changing reference which is the main script's location can be unpredictable when the supposed main script is delegated by another script. - Not adding those two features can simplify the code especially in C. Not to mention less runtime factors to consider for users. To make the implementation script above work, Bash also needs to be patched so BASH_SOURCE always contain real path values: -------------------- diff --git a/shell.c b/shell.c index 01fffac2..e9c19406 100644 --- a/shell.c +++ b/shell.c @@ -1573,7 +1573,7 @@ static int open_shell_script (char *script_name) { int fd, e, fd_is_tty; - char *filename, *path_filename, *t; + char *filename, *path_filename, *real_filename, realbuf[PATH_MAX], *t; char sample[80]; int sample_len; struct stat sb; @@ -1638,7 +1638,8 @@ open_shell_script (char *script_name) GET_ARRAY_FROM_VAR ("BASH_SOURCE", bash_source_v, bash_source_a); GET_ARRAY_FROM_VAR ("BASH_LINENO", bash_lineno_v, bash_lineno_a); - array_push (bash_source_a, filename); + real_filename = sh_realpath (filename, realbuf); + array_push (bash_source_a, real_filename != NULL ? real_filename : filename); if (bash_lineno_a) { t = itos (executing_line_number ()); diff --git a/shell.h b/shell.h index b9d259a5..721e0086 100644 --- a/shell.h +++ b/shell.h @@ -246,3 +246,5 @@ extern void uw_restore_parser_state (void *); extern sh_input_line_state_t *save_input_line_state (sh_input_line_state_t *); extern void restore_input_line_state (sh_input_line_state_t *); + +extern char *sh_physpath (char *, int); -------------------- The recommended way to use the script is by placing it to a common PATH location like /usr/local/bin and name it as 'source-extensions'. Scripts can import it by simply calling `source source-extensions` before other source commands. Consistency before simplicity. -- konsolebox