To setup custom scripts to be available only when you are within any subdirectory of your project, do the following:

  1. install direnv and git
  2. add .envrc file to the root directory of your project with this content:
export PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
PATH_add "$PROJECT_ROOT/.env/scripts"
  1. run direnv allow in the root directory of your project
  2. create folder .env/scripts and add any scripts you want available there – make them executable

Already now, if you have .env/scripts/test by typing test in any subdirectory of the project the test script should run.

Scoping the scripts into a custom command

If you don’t want the scripts exposed directly, not to pollute your PATH, do the following instead:

  1. install direnv and git
  2. add .envrc file to the root directory of your project with this content:
export PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
  1. run direnv allow in the root directory of your project
  2. create ~/bin/ folder
  3. add export PATH="~/bin/:$PATH" to the end of your .bashrc
  4. create an executable file ~/bin/cmd with the following content
#!/usr/bin/env bash
set -eo pipefail

SCRIPT_FOLDER="$PROJECT_ROOT/.env/scripts/"

show-help(){
    printf "usage: $(basename "$0") <command> [<args>]\n"
    items=()
    if [[ -d "$SCRIPT_FOLDER" ]]; then
        cd "$SCRIPT_FOLDER" || exit
        script_list="$(find . -type f | sed 's|^\./||')"
        while IFS='' read -r line; do
            items+=("$line")
        done <<< "$script_list"
        printf "   %s\n" "${items[@]}"
    else
        printf "\nfirst change directory to a project"
    fi
}

command="$1"
case "$command" in
    "" | "-h" | "--help" | "help")
        show-help
        ;;
    *)
        shift
        if [[ -f "$SCRIPT_FOLDER/$command" ]]; then
            "$SCRIPT_FOLDER/$command" "$@"
        else
            echo "command not found: $command"
            exit 1
        fi
        ;;
esac

After your source .bashrc, when you run cmd from subfolder of your project the command prints whatever scripts you have in .env/scripts. And, for example, if you run cmd test [<pars>] then the .env/test [<pars>] is invoked.

A more detailed description

The setup and the description below is meant for Linux machines.

Note that .envrc and .env are by hidden and will not be shown by default (use ls -a or enable hidden files in your file explorer).

direnv

direnv is a utility that changes your environment based on the subdirectory where you are. You will find it within your package manager (apt, pacman, etc.). We use this to detect the root folder of the project. This utility detects whether a folder (or any ancestor folder) contains .envrc file, and executes it if so – however, to prevent unwanted invocation the user needs to allow every particular .envrc with direnv allow Whenever the .envrc file changes direnv allow needs to be re-run.

The commands that should be used within the .env script are meant NOT to be permanent. If you leave your project, whatever the script changed should be reverted. For this to work you need to use a set of revertible commands that direnv defines, e.g. PATH_add, see its documentation ↗.

Custom scripts

Create your project scripts within .env/scripts/, you may rename this to anything. I chose .env as it makes sense with .envrc setup. Note that:

  • to make your scripts executable run chmod u+x <script>
  • omit extensions like .sh as those will be the necessary part of the command
  • setup command that runs your script using shebang: #!/usr/bin/env bash on the first line of the script

Note that invoking a script does not change your folder – the script is run from your current directory. You may use cd "$PROJECT_ROOT" for script to jump to the root of the project. Alternately, if you want the scripts to be usable by users that do not have direnv then use cd "$(dirname "$0")/../.." || exit instead.

cmd

The instructions simply add the new script to be always available. Its functionality is based on the PROJECT_ROOT variable that that is set up by direnv.

It would be nice to improve cmd help so that it prints descriptions of each command – which for now, I keep as comment on the second line of the file.

References

This setup is inspired by Upgrade your scripts using ‘direnv’ and ‘run’ script ↗ post by Oliver Nguyen. His setup extracts commands from functions of a single shell script. I wanted this simple functionality for too many years, that post is quite long, but will give you a list of reasons to set this up.

All the code in this post is dedicated to the public domain, use it without any restrictions but without any warranty.