0

I'm on Ubuntu 20.04 Focal, amd64, Bash version 5.0.17(1)-release.

In previous versions of Ubuntu, I've been able to write intuitive scripts to modify PATH in a way in which multiple entries containing the same value aren't possible.

Source Code

My /etc/bash.bashrc is what is provided by the distro, and so is my ~/.bashrc, apart from the following lines:

~/.bashrc

# ... distro defaults above

export DEBUG=y

if [ -e "$HOME/.bash_functions" ]; then
    source "$HOME/.bash_functions"
fi

if [ -d "$HOME/.bashrc.d" ]; then
    find "$HOME/.bashrc.d" -maxdepth 1 -type f -name '*.sh' | sort -u | while read file ; do
        source "$file"
    done
fi

My ~/.bash_functions includes my helper functions for modifying PATH safely:

~/.bash_functions

#!/usr/bin/env bash

# log MESSAGE...
#
# Prints the provided arguments to standard error if the DEBUG variable is set to 'y'.
_log() {
    test "$DEBUG" = "y" && ( echo -n 'DEBUG: ' && echo "$@" ) >&2
}

# path_exists PATH_ENTRY
#
# Returns true if the specified path entry exists within $PATH, and false otherwise.
_path_exists() {
    path="$1" && shift
    echo "$PATH" | tr ':' '\n' | while read path_entry ; do
        test "$path" = "$path_entry" && return 0
    done && return 0

    return 1
}

# prepend_path PATH_ENTRY
#
# Pushes PATH_ENTRY to the beginning of $PATH, provided that PATH_ENTRY does not already exist in $PATH.
_prepend_path() {
    path="$1" && shift

    if ! _path_exists "$path" ; then
        _log "prepend_path - Prepending $path to PATH..."
        PATH="$path:$PATH"
    export PATH
    else
        _log "prepend_path - $path is already present in PATH, nothing to do."
    fi
}

# append_path PATH_ENTRY
#
# Pushes PATH_ENTRY to the end of $PATH, provided that PATH_ENTRY does not already exist in $PATH.
_append_path() {
    path="$1" && shift

    if ! _path_exists "$path" ; then
        _log "append_path - Appending $path to PATH..."
        PATH="$PATH:$path"
    export PATH
    else
        _log "append_path - $path is already present in PATH, nothing to do."
    fi
}

Finally, I have two files in ~/.bashrc.d, one for adding Rust to the PATH and one for nodenv:

~/.bashrc.d/50-rust.sh

#!/usr/bin/env bash

if [ -d "$HOME/.cargo/bin" ]; then
    _prepend_path "$HOME/.cargo/bin"
fi

~/.bashrc.d/50-node.sh

#!/usr/bin/env bash

NODENV_ROOT="$HOME/.nodenv"

if [ -d "$NODENV_ROOT/bin" ]; then
    _prepend_path "$NODENV_ROOT/bin"

    if ! _path_exists "$NODENV_ROOT/shims" ; then
        eval "$(nodenv init -)"
    fi
fi

Issue

Since I have exported DEBUG=y in my ~/.bashrc, I see output when I open a new shell:

DEBUG: prepend_path - Prepending /home/naftuli/.nodenv/bin to PATH...
DEBUG: prepend_path - Prepending /home/naftuli/.cargo/bin to PATH...

Clearly, the code is executing. If I emend my loop to print PATH after sourcing each file, I see that it did, in fact, modify the environment. My modifications to ~/.bashrc:

if [ -d "$HOME/.bashrc.d" ]; then
    find "$HOME/.bashrc.d" -maxdepth 1 -type f -name '*.sh' | sort -u | while read file ; do
        source "$file"
        echo "PATH=$PATH"
    done
fi

When I start a new shell, I observe:

DEBUG: prepend_path - Prepending /home/naftuli/.nodenv/bin to PATH...
PATH=/home/naftuli/.nodenv/shims:/home/naftuli/.nodenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
DEBUG: prepend_path - Prepending /home/naftuli/.cargo/bin to PATH...
PATH=/home/naftuli/.cargo/bin:/home/naftuli/.nodenv/shims:/home/naftuli/.nodenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

Again, clearly it is working, but somehow export within those sourced files does not propagate all the way up to the actual shell environment, because after my shell starts:

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

In previous versions of Bash, this setup did work.

Question

Why aren't my exports propagating upward to the shell on shell start? My code is running, and PATH is modified, but outside of the loop, it's as if PATH was never modified. What changes do I need to make in order to propagate variables upward to the shell?

Naftuli Kay
  • 87,710
  • 93
  • 269
  • 411
  • 1
    Exports only propagate to child processes, not parent processes. – Barmar Oct 29 '21 at 22:14
  • 1
    You're sourcing the scripts in a subshell process because it's in a pipeline. – Barmar Oct 29 '21 at 22:16
  • 1
    `source "$file"` is inside of a pipe, which happens in a subshell. see: https://stackoverflow.com/q/16854280/1032785 – jordanm Oct 29 '21 at 22:51
  • It looks to me like you could replace the `find ... | while read` loop with a simple `for file in "$HOME/.bashrc.d"/*.sh; do` – Gordon Davisson Oct 30 '21 at 00:32
  • @GordonDavisson the for loop will fail if there are spaces in a filename. I don't expect there to be spaces in a filename, but `find` is my default pattern for looping in order to avoid this problem. – Naftuli Kay Oct 30 '21 at 00:39
  • @NaftuliKay No, the `for` loop with wildcard handles spaces in filenames fine (as long as you double-quote `$file` when you use it). Give it a try! The only limitation I know of is that if there are no matching files, it'll run the loop once with `file` set literally to `/homepath/.bashrc.d/*.sh`. If this is a concern, use `[ -f "$file" ] && source "$file"` inside the loop. – Gordon Davisson Oct 30 '21 at 01:37

0 Answers0