bug-bash
[Top][All Lists]
Advanced

[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



reply via email to

[Prev in Thread] Current Thread [Next in Thread]