1

Background

I have written a script to install node via nvm.

Issue

The command . $LOCAL_SHELL_CONFIG_FILE inside install_latest_stable_node() does not run and thus, when it prints the PATHs for node and npm (echo "$(which node)" && echo "$(which npm)"), it prints two blank lines.

Once the script terminates, I can then run . ~/.bashrc and then run, echo "$(which node)" && echo "$(which npm)" which will echo back the PATHs, this time without two blank lines.

Why?

Code

#!/bin/bash

ask_for_sudo() {

    # Ask for the administrator password upfront.

    sudo -v &> /dev/null

    # Update existing `sudo` time stamp
    # until this script has finished.
    #
    # https://gist.github.com/cowboy/3118588

    while true; do
        sudo -n true
        sleep 60
        kill -0 "$$" || exit
    done &> /dev/null &

}

show_spinner() {

    local -r FRAMES='/-\|'

    # shellcheck disable=SC2034
    local -r NUMBER_OR_FRAMES=${#FRAMES}

    local -r CMDS="$2"
    local -r MSG="$3"
    local -r PID="$1"

    local i=0
    local frameText=""

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Note: In order for the Travis CI site to display
    # things correctly, it needs special treatment, hence,
    # the "is Travis CI?" checks.

    if [ "$TRAVIS" != "true" ]; then

        # Provide more space so that the text hopefully
        # doesn't reach the bottom line of the terminal window.
        #
        # This is a workaround for escape sequences not tracking
        # the buffer position (accounting for scrolling).
        #
        # See also: https://unix.stackexchange.com/a/278888

        printf "\n\n\n"
        tput cuu 3

        tput sc

    fi

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Display spinner while the commands are being executed.

    while kill -0 "$PID" &>/dev/null; do

        frameText="   [${FRAMES:i++%NUMBER_OR_FRAMES:1}] $MSG"

        # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

        # Print frame text.

        if [ "$TRAVIS" != "true" ]; then
            printf "%s\n" "$frameText"
        else
            printf "%s" "$frameText"
        fi

        sleep 0.2

        # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

        # Clear frame text.

        if [ "$TRAVIS" != "true" ]; then
            tput rc
        else
            printf "\r"
        fi

    done

}

execute() {

    local -r CMDS="$1"
    local -r MSG="$2"
    local -r TMP_FILE="$(mktemp /tmp/XXXXX)"

    local exitCode=0
    local cmdsPID=""

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # If the current process is ended,
    # also end all its subprocesses.

    set_trap "EXIT" "kill_all_subprocesses"

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Execute commands in background

    eval "$CMDS" \
        &> /dev/null \
        2> "$TMP_FILE" &

    cmdsPID=$!

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Show a spinner if the commands
    # require more time to complete.

    show_spinner "$cmdsPID" "$CMDS" "$MSG"

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Wait for the commands to no longer be executing
    # in the background, and then get their exit code.

    wait "$cmdsPID" &> /dev/null
    exitCode=$?

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Print output based on what happened.

    print_result $exitCode "$MSG"

    if [ $exitCode -ne 0 ]; then
        print_error_stream < "$TMP_FILE"
    fi

    rm -rf "$TMP_FILE"

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    return $exitCode

}

set_trap() {

    trap -p "$1" | grep "$2" &> /dev/null \
        || trap '$2' "$1"

}

print_error_stream() {
    while read -r line; do
        print_error "↳ ERROR: $line"
    done
}

print_error() {
    print_in_red "   [✖] $1 $2\n"
}

print_success() {
    print_in_green "   [✔] $1\n"
}

print_in_red() {
    print_in_color "$1" 1
}

print_in_green() {
    print_in_color "$1" 2
}

print_in_purple() {
    print_in_color "$1" 5
}


print_in_color() {
    printf "%b" \
        "$(tput setaf "$2" 2> /dev/null)" \
        "$1" \
        "$(tput sgr0 2> /dev/null)"
}

print_result() {

    if [ "$1" -eq 0 ]; then
        print_success "$2"
    else
        print_error "$2"
    fi

    return "$1"

}

fix_dpkg() {
    declare -a files=("/var/lib/dpkg/lock" "/var/cache/apt/archives/" "/var/cache/apt/archives/lock")

    for i in "${files[@]}"
    do
        # If there is a dpkg lock, then remove it.
        if [ -e "$i" ]; then
            sudo rm -rf "$i" &> /dev/null
        fi
    done
}


install_package() {

    declare -r PACKAGE="$2"
    declare -r PACKAGE_READABLE_NAME="$1"

    if ! package_is_installed "$PACKAGE"; then
        fix_dpkg
        execute "sudo apt-get install --allow-unauthenticated -qqy $PACKAGE" "$PACKAGE_READABLE_NAME"
        #                                      suppress output ─┘│
        #            assume "yes" as the answer to all prompts ──┘
    else
        print_success "$PACKAGE_READABLE_NAME"
    fi

}

package_is_installed() {
    dpkg -s "$1" &> /dev/null
}

install_nvm() {

    # Install `nvm` and add the necessary
    # configs in the local shell config file.
    # One-liner:
    # wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | PROFILE=~/.bash.local bash && . ~/.bash.local

    execute \
        "wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | \
            PROFILE=$LOCAL_SHELL_CONFIG_FILE NVM_DIR=$NVM_DIRECTORY bash" \
        "nvm (install)"
}

update_nvm() {

    execute \
        "cd $NVM_DIRECTORY \
            && git fetch --quiet origin \
            && git checkout --quiet \$(git describe --abbrev=0 --tags) \
            && . $NVM_DIRECTORY/nvm.sh" \
        "nvm (upgrade)"

}

install_latest_stable_node() {

    # Install the latest stable version of Node
    # (this will also set it as the default).
    # One-liner:
    #. ~/.bash.local && nvm install node

    . $LOCAL_SHELL_CONFIG_FILE

    execute \
        "source $LOCAL_SHELL_CONFIG_FILE \
        && nvm install node" \
        "nvm (install latest Node)"

}

main() {
    ask_for_sudo

    touch "$LOCAL_SHELL_CONFIG_FILE"

    print_in_purple "\n   Dependencies\n\n"

    install_package "git" "git"

    print_in_purple "\n   nvm\n\n"

    if [ -d "$NVM_DIRECTORY" ]; then
        rm -rf "$NVM_DIRECTORY"
    fi

    install_nvm
    update_nvm

    install_latest_stable_node

    print_in_purple "\n   PATHs\n\n"

    echo "$(which node)"
    echo "$(which npm)"
}

declare -r LOCAL_SHELL_CONFIG_FILE="$HOME/.bashrc"
declare -r NVM_DIRECTORY="$HOME/.nvm"

main
  • What's the output you get? – Arkadiusz Drabczyk Aug 24 '17 at 19:21
  • BTW, does mktemp /tmp/XXXXX work for you? On my system mktemp takes at leas six Xs. – Arkadiusz Drabczyk Aug 24 '17 at 19:29
  • @ArkadiuszDrabczyk yes, mktemp /tmp/XXXXX does work for me as the man page specifies that there must be at least 3 consecutive X's in the last component. – Nicholas Adamou Aug 24 '17 at 19:34
  • @ArkadiuszDrabczyk Currently I am getting an error: claiming nvm is not found. – Nicholas Adamou Aug 24 '17 at 19:35
  • Indeed, I just checked that 3 Xs are enough on GNU systems but it wouldn't work on busybox for example. – Arkadiusz Drabczyk Aug 24 '17 at 19:35
3

Your script is sourcing the specified file.

The reason that you see two blank lines instead of the expected path names is because the script sources the $LOCAL_SHELL_CONFIG_FILE from inside your "execute()" function, which runs the commands in the background (that is, in a subshell).

Subshells cannot modify the environment of the parent shell.

  • Ahh okay, that makes sense. So, in order to modify the parent shell, I should just run . ~/.bashrc outside of the execute(). – Nicholas Adamou Aug 24 '17 at 19:38
  • That would appear to be correct. Can you try it and let us know for sure? – RobertL Aug 24 '17 at 19:43
  • However, it still claims nvm: command not found, even if I put . ~/.bashrc before the execute() in install_latest_stable_node(). – Nicholas Adamou Aug 24 '17 at 19:44
  • Is the path to nvm part of $PATH in ~/.bashrc? – Deathgrip Aug 24 '17 at 20:35
  • @Deathgrip Yes, the contents of ~/.bashrc that is relevant to nvm is here: pastebin.com/yGUkmU5B Which is why I need to source it in the parent shell to get nvm install node to work. The script fails at nvm install node because it cannot find the command nvm. – Nicholas Adamou Aug 24 '17 at 20:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.