1

Inspired by the Php require_once I figured how it could be implemented for modern Bash with associative arrays here:

a.sh

#!/usr/bin/env bash

declare -gAi __REQUIRES

require_once() {
  for p; do
    r=$(readlink --canonicalize-existing "$p")
    # shellcheck disable=SC1090 # Dynamic source
    [[ -r "$r" && 1 -ne "${__REQUIRES[$r]}" ]] && __REQUIRES[$r]=1 && . "$r"
  done
}

require_once ./b.sh
require_once ./b.sh

hello

b.sh

hello() {
  printf 'I am the hello function in b.sh.\n'
}

This works as intended:

  1. Get the real path of the source.
  2. Check it is readable, check it is not already loaded by looking-up the global associative array key.
  3. If so, register it in the global associative array and source it.

Now I am still wondering how to:

  1. Get the real path of the source in a more portable/standard way not depending on Linux Gnu Tools?
  2. Have it work with older Bash like 3.2
  3. Have it work with POSIX-shell grammar without array.
Léa Gris
  • 17,497
  • 4
  • 32
  • 41

3 Answers3

2

bash 3.2 does not implement associative arrays, so we could create individual "required" variable names for each script.

Don't forget that readlink -e or -f fails if the symlink is broken, so let's work that into the function.

MacOS readlink does not have a "canonicalize" option at all, so I don't how this can be made portable.

Perhaps this: works on my Mac with GNU readlink installed with homebrew.

require_once() {
    local script canonical varname val
    for script; do
        if canonical=$(readlink -f -- "$script") && [[ -r "$canonical" ]]; then
            varname=$(printf '__REQUIRED__%s' "${canonical//[^[:alnum:]]/__}")
            val=$(eval "echo \$$varname")
            if [[ -z $val ]]; then
                eval "$varname=1"
                . "$canonical"
            fi
        fi
    done
}
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
1

Get the real path of the source in a more portable/standard way not depending on Linux Gnu Tools?

readlink -f is available on Busybox. Do readlink after checking that the path exists.

Anyway, https://www.google.com/search?q=readlink+POSIX -> https://medium.com/mkdir-awesome/posix-alternatives-for-readlink-21a4bfe0455c , https://github.com/ko1nksm/readlinkf .

Have it work with older Bash like 3.2

Have it work with POSIX-shell grammar without array.

POSIX does not like newlines in filenames anyway, so just store files as lines:

__REQUIRES=""
if ! printf "%s" "$__REQUIRES" | grep -Fq "$r"; then
   __REQUIRES="$__REQUIRES""$r
"
   . "$r"
fi

Or maybe use case so that you do not fork:

__REQUIRES="
"
case "$__REQUIRES" in
*"
$r
"*) ;;
*) 
    __REQUIRES="$__REQUIRES""$r
"
    . "$r"
    ;;
esac

If you want to handle newlines in filenames, convert filename via xxd or od (both are available on Busybox, od is POSIX) and store hex representation of filenames as lines in the variable.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
1

POSIX-shell grammar method using symbolic links in temp directory as already required source markers:

a.sh

#!/usr/bin/env sh

unset __REQUIRES;
trap 'rm -fr -- "$__REQUIRES"' EXIT INT
__REQUIRES=$(mktemp -d)

require_once() {
  for p; do
    # Path is readable or continue
    [ -r "$p" ] || continue

    # Get real path
    r=$(readlink -f -- "$p")

    # New real path in __REQUIRES temp dir
    nr="$__REQUIRES$r"

    # If path __REQUIRES temp dir exists, file is already sourced
    [ -r "$nr" ] && continue

    # Make equivalent path for file in __REQUIRES temp dir
    mkdir -p -- "${nr%/*}"

    # Create symbolic link to __REQUIRES temp dir
    ln -sf -- "$r" "$nr"

    # shellcheck disable=SC1090 # Dynamic source
    . "$r"
  done
}

counter=0

require_once ./b.sh
require_once ./b.sh

hello
printf 'counter=%d\n' "$counter"

b.sh

counter=$((counter + 1))

hello() {
  printf 'I am the hello function in b.sh.\n'
}
Léa Gris
  • 17,497
  • 4
  • 32
  • 41