3

My .bash_profile script takes several seconds to run each time I am opening a terminal so this becomes quite annoying.

My testing shows that certain commands are taking a long time so I moved them out of .bash_profile to a new script .bash_profile_load_in_background

From .bash_profile I am trying to source in the background.

.bash_profile

# fast stuff here

#.....

# slow stuff here

source .bash_profile_load_in_background & # notice the &

In .bash_profile_load_in_background I am setting some variables but they are not propogated correctly when the source is sent to background &.

here is an abridged version of my "slow" script:

[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm

[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

if type brew &>/dev/null; then
  HOMEBREW_PREFIX="$(brew --prefix)"
  if [[ -r "${HOMEBREW_PREFIX}/etc/profile.d/bash_completion.sh" ]]; then
    source "${HOMEBREW_PREFIX}/etc/profile.d/bash_completion.sh"
  else
    for COMPLETION in "${HOMEBREW_PREFIX}/etc/bash_completion.d/"*; do
      [[ -r "$COMPLETION" ]] && source "$COMPLETION"
    done
  fi
fi

if [ -f $(brew --prefix)/etc/bash_completion ]; then
 . $(brew --prefix)/etc/bash_completion
fi

# parse the git branch
gb() {
     __git_ps1
}

# change the prompt to show working directory and git branch
PS1="\[\e[32m\]\w \[\e[91m\]\$(gb)\[\e[00m\]? "

Easy enough to see that the PS1 prompt isn't changing once the script in the background has completed.

On the other hand if I source the script in "blocking" inline fashion, everything works as expected:

i.e.

Modify

source .bash_profile_load_in_background & # notice the &

to

source .bash_profile_load_in_background # REMOVE THE &

Why is this happening?

Any way to make this work and speed up the .bash_profile load time in a simple fashion? I don't care if some the functionality takes several seconds to take effect, but it must take effect in the current scope.

2 Answers 2

5

Trick

This other answer explains why your attempt doesn't work. There's a trick to make it work, at least to some degree. I don't really recommend it, it's not how .bash_profile is supposed to work.

Here's the idea:

  1. Let .bash_profile create a temporary file and remember the path to it:

    tmpf="$(mktemp)"
    

    (mktemp is not portable.)

  2. Set a trap, so the shell sources the temporary file upon SIGUSR1. After sourcing, the file and the trap are no longer needed, so consider cleaning as a part of the trap.

    trap '
      . "$tmpf"
      rm "$tmpf"
      trap - USR1
    ' USR1
    
  3. Run .bash_profile_background_wizard in the background. This script must be able to write to the temporary file, so give it a descriptor or the path (as an argument or in the environment). Example:

    tmpf="$tmpf" ~/.bash_profile_background_wizard &
    

    The script should start the long-running tasks and write shell code into the temporary file. It should build definitions of functions and variables, anything that depends on the long-running tasks, anything you want the main shell to "adopt" when it's ready. Depending on what you want to do, it may not be easy to write a script that writes a script. Remember the temporary file is about to be parsed by the main shell. declare -p variable >>"$tmpf" or the syntax ${variable@Q} will be useful.

    After writing all the code to the temporary file, the script should send SIGUSR1 to the main shell

    kill -s USR1 "$PPID"
    

    and exit.

  4. Thanks to the trap the main shell will source the code prepared by .bash_profile_background_wizard.


Notes

  • If the signal comes when the main shell is waiting for some synchronous command to complete then the trap will not be executed until the command completes.

  • If you spawn a child bash before the signal comes, don't expect the child to react to the signal. Because:

    • it won't get the signal due to having a different PID,
    • it won't inherit the trap.

    Note if you spawn a child bash in a way that causes it to source .bash_profile then the whole trick will start for the child bash independently.

  • If you spawn a background process before the signal comes, don't expect the environment of the process to be updated when the main shell finally sources the temporary file and possibly changes its own environment.

  • If you exit the main shell before the trap runs then the temporary file will remain (trap '…' EXIT may be handy).


Proof of concept

The following snippet is designed to be pasted into an interactive Bash. This way you can test the idea without polluting your .bash_profile. The mechanics is the same as presented above.

# imagine this block is sourced from .bash_profile
{
export PS1='poor prompt > '
tmpf="$(mktemp)"
trap '
  . "$tmpf"
  rm "$tmpf"
  trap - USR1
' USR1
# wizard in background
tmpf="$tmpf" bash -c '
  sleep 10  # delay
  PS1="RICH PROMPT >>> "
  declare -p PS1 >>"$tmpf"
  kill -s USR1 "$PPID"
' &
}
# Initially you will see a poor prompt.
# Run few basic commands or keep striking Enter.
# After 10 seconds the prompt should change.
2

You are trying to combine two incompatible things.

The source command makes the shell read a file and execute the commands therein in the current shell environment.

Starting an asynchronous job, i.e. running a command in the background with &, runs the command in its own environment, separate from the invoking shell. This environment is a copy of the parent shells environment and it will be destroyed as the command terminates. There is no way for the background job to set environment variables etc. in the parent shell's environment.

The effect of your command

source .bash_profile_load_in_background &

is that bash starts a new shell process (in the background) in which it runs the source command. That process then dies, and the modified environment with it.

The effect is the same as if you had run the .bash_profile_load_in_background as a separate script (which also runs in its own environment).

Related:

3
  • so how can i speed up things or cache this between terminals? I split my terminal a lot and this delay is very annoying
    – Avba
    Commented Aug 10, 2021 at 11:13
  • @AvnerBarr This is something that you may want to ask another question about. This was not what you current question was aiming at, and what "NVM" is and how bash completions work on macOS (which I'm presuming you're using based on the use of the brew command and mentionings of "Homebrew") is something I'm blissfully unaware of.
    – Kusalananda
    Commented Aug 10, 2021 at 11:39
  • What is actually taking the time? The nvm invocation? Commented Aug 10, 2021 at 21:00

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.