[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
source -i prototype implementation and other features
From: |
konsolebox |
Subject: |
source -i prototype implementation and other features |
Date: |
Sat, 18 May 2024 17:13:04 +0800 |
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
- source -i prototype implementation and other features,
konsolebox <=