Skip to content
VLSI Mentor

Linux & Bash · Part I · Module 10 · The Environment, PATH and Shell Startup

Sourcing vs Executing: Why ./setup.sh Does Nothing

A new engineer is handed a setup script, runs it the obvious way, and finds the tool still missing. Nothing failed, no error appeared, and the script did exactly what it was written to do. The difference between the two ways of running it is one word, and it is the most consequential word in this module.

Intermediate17 min readLinuxPart IFundamentalsEnvironmentBash

Part I · Module 10 · Chapter 10.4 · Sourcing vs Executing

Chapter 7.4 proved that nothing a child does reaches its parent. Chapter 10.1 proved that the environment is copied rather than shared. This chapter is the practical consequence, and it is the one you will use most.

1. The Experiment

A small setup script, doing the three things such scripts do:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cd /tmp
export SETUP_RAN=yes
MYVAR=local
echo "  inside: PID=$$ pwd=$(pwd)"

Run it the obvious way:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo "shell PID=$$ pwd=$(pwd)"
shell PID=44741 pwd=/tmp/demo

$ ./setup.sh
  inside: PID=44743 pwd=/tmp

$ echo "after: pwd=$(pwd) SETUP_RAN=[${SETUP_RAN-unset}]"
after: pwd=/tmp/demo SETUP_RAN=[unset]

Now source it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ source ./setup.sh
  inside: PID=44741 pwd=/tmp

$ echo "after: pwd=$(pwd) SETUP_RAN=[${SETUP_RAN-unset}] MYVAR=[${MYVAR-unset}]"
after: pwd=/tmp SETUP_RAN=[yes] MYVAR=[local]

Look at the PIDs. That is the whole chapter.

  • Executed: the script reported 44743, a different process from the shell's 44741. Its cd and its export applied to that process, which then exited.
  • Sourced: the script reported 44741 — your shell's own PID. There was no child. The lines ran in the shell you are sitting in, so the directory changed and the variables stayed.

Note too that MYVAR persisted even though it was never exported. Sourcing is not "exporting harder" — there is simply no process boundary for anything to cross.

The difference between running a script and sourcing it. Running it starts a new process, which gets a copy of everything and takes its changes with it when it exits. Sourcing executes the same lines in the shell you are already in, so there is no copy and every change stays.Running it./setup.shA new process startswith a copy of everythingNothing persiststhe copy exits and takes thechangesSourcing itsource setup.shNo new process at allthe lines run in the shell youare inEverything persiststhere was never a copy to lose12
Figure 1 — the difference between running a script and sourcing it. Running it starts a new process which receives a copy of the environment and the working directory, makes its changes to that copy, and takes them away when it exits. Sourcing executes the same lines in the shell you are already in, so no copy is ever made and every change simply stays.

2. Two Spellings

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
source setup.sh
. setup.sh

Identical in Bash. . is the POSIX spelling and works in every shell; source is the Bash and C-shell name and reads better. Use . in anything with a #!/bin/sh line, and either elsewhere.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ . ./setup.sh >/dev/null
$ echo "dot worked: SETUP_RAN=[$SETUP_RAN]"
dot worked: SETUP_RAN=[yes]

A sourced file is read, not run

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ls -l lib.sh
-rw-r--r-- 1 engineer engineer 48 Oct  8 11:02 lib.sh

$ source ./lib.sh

No execute bit, and it works. Sourcing reads the file's text and feeds it to the shell, so only the read permission matters. A shebang line is likewise ignored — it is a comment as far as the sourcing shell is concerned, and the file runs in whatever shell sourced it regardless of what the first line claims.

That is worth remembering in both directions: a file written for bash and sourced into zsh will be interpreted by zsh, shebang or not.

Where it looks

source lib.sh with no slash searches PATH before the current directory — which is surprising and occasionally dangerous. Be explicit:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
source ./lib.sh                       # this directory, definitely
source "$script_dir/lib.sh"           # a path you computed

$script_dir from Chapter 8.4's $(cd "$(dirname "$0")" && pwd) idiom is the robust way to source a file that sits next to a script.

3. Why Every Tool Says "source"

This is the practical payoff, and it is most of what you will meet in engineering work.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# /opt/synopsys/vcs/2024.09/setup.sh
export VCS_HOME=/opt/synopsys/vcs/2024.09
export PATH="$VCS_HOME/bin:$PATH"
export LM_LICENSE_FILE=27000@licsrv1
export UVM_HOME="$VCS_HOME/etc/uvm"

Every line sets something the caller needs to keep. Run it as ./setup.sh and all four apply to a process that immediately exits:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./setup.sh
$ which vcs
vcs not found

No error. No warning. The script succeeded completely and achieved nothing.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ source /opt/synopsys/vcs/2024.09/setup.sh
$ which vcs
/opt/synopsys/vcs/2024.09/bin/vcs

4. Two Ways It Bites Back

Sourcing runs code in your shell with your authority. Two consequences follow, and both catch people once.

exit in a sourced script exits your shell

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# bad.sh
echo "  about to exit"
exit 3
echo "never reached"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ( source ./bad.sh; echo "this line does NOT print" )
  about to exit
$ echo "subshell exited with $?"
subshell exited with 3

The parentheses are protecting the demonstration — they run it in a subshell. Without them, that exit 3 closes your terminal.

This is not a bug; exit means "end this shell", and when a script is sourced, this shell is yours. A script that might be sourced should use return instead:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# good.sh
if [ -z "${TOOL_HOME:-}" ]; then
  echo "TOOL_HOME is not set" >&2
  return 1 2>/dev/null || exit 1
fi

return 1 works when sourced; the || exit 1 covers being executed, where return outside a function is an error. It is ugly and it is the standard idiom. (return proper is Chapter 18.3, and Chapter 18.6 is this idiom used for a real library.)

A sourced script sees your arguments

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ set -- outer1 outer2

$ source ./args.sh
  $1=[outer1] $#=[2]

$ source ./args.sh inner1
  $1=[inner1] $#=[1]

With no arguments of its own, a sourced script inherits the caller's positional parameters. Give it arguments and it gets those instead — and note that the caller's are restored afterwards.

The first case surprises people: a sourced file that reads $1 picks up whatever the enclosing script happened to have, which is rarely what its author meant. A sourced library should take nothing from $@ unless it was designed to.

5. Making Sourcing Idempotent

Setup scripts get sourced more than once — someone sources it, then sources a wrapper that sources it again. Chapter 10.2 showed the result: duplicate PATH entries.

The standard guard:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# setup.sh
if [ -n "${TOOL_SETUP_DONE:-}" ]; then
  return 0 2>/dev/null || exit 0
fi
export TOOL_SETUP_DONE=1

export TOOL_HOME=/opt/tool
case ":$PATH:" in
  *":$TOOL_HOME/bin:"*) ;;
  *) export PATH="$TOOL_HOME/bin:$PATH" ;;
esac

Two defences doing different jobs: the flag makes the whole file a no-op on a second sourcing, and the case makes the PATH line safe even if the flag is cleared or the file is split. Both are worth having, because setup scripts get copied and edited by people who will not read the top.

6. Where Else This Rule Appears

Once you can see it, the same mechanism explains several things you have already met:

SituationWhy it works the way it does
.bash_profile reading .bashrc (10.3)Sourcing — a child would change nothing
cd being a builtin (1.7)An external cd could not change your directory
A script cannot leave you elsewhere (7.1)It is a child
A function changing your shell (7.4)Functions also run without forking
$(cd … && pwd) leaving you put (8.4)The subshell is a child, deliberately
A piped while loop losing its counter (8.6)Each pipeline stage is a child

Six apparently unrelated behaviours, one rule. That is the sign of a model worth having rather than a list of exceptions.

Practice

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mkdir -p /tmp/srcdemo && cd /tmp/srcdemo

1 — Run the experiment.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'cd /tmp\nexport SETUP_RAN=yes\nMYVAR=local\necho "  inside: PID=$$ pwd=$(pwd)"\n' > setup.sh
chmod +x setup.sh
echo "shell PID=$$ pwd=$(pwd)"
./setup.sh;        echo "  after exec:   pwd=$(pwd) SETUP_RAN=[${SETUP_RAN-unset}]"
cd /tmp/srcdemo
source ./setup.sh; echo "  after source: pwd=$(pwd) SETUP_RAN=[${SETUP_RAN-unset}] MYVAR=[${MYVAR-unset}]"
cd /tmp/srcdemo

The PID line is the one to read.

2 — Confirm . is the same.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
unset SETUP_RAN; . ./setup.sh >/dev/null; echo "SETUP_RAN=[$SETUP_RAN]"; cd /tmp/srcdemo

3 — Source a file with no execute bit.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'export FROM_LIB=yes\n' > lib.sh
ls -l lib.sh
source ./lib.sh && echo "sourced without +x: FROM_LIB=[$FROM_LIB]"
./lib.sh

The last line fails — executing does need the bit.

4 — Watch exit in a sourced script, safely.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'echo "  about to exit"\nexit 3\necho "never reached"\n' > bad.sh
( source ./bad.sh; echo "this does NOT print" )
echo "subshell status: $?"

Keep the parentheses.

5 — See the argument inheritance.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'echo "  \$1=[$1] \$#=[$#]"\n' > args.sh
set -- outer1 outer2
source ./args.sh
source ./args.sh inner1
echo "caller still has: \$1=[$1] \$#=[$#]"

6 — Add the must-be-sourced guard.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cat > guarded.sh <<'EOF'
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
  echo "error: source this, do not execute it" >&2
  exit 1
fi
export GUARDED=ok
EOF
chmod +x guarded.sh
./guarded.sh
source ./guarded.sh && echo "sourced fine: GUARDED=$GUARDED"

7 — Make it idempotent. Add the TOOL_SETUP_DONE flag and the case guard from §5 to a script that prepends to PATH, source it three times, and confirm with echo "$PATH" | tr ':' '\n' | sort | uniq -d that nothing duplicated.

8 — Clean up. cd /tmp && rm -rf /tmp/srcdemo

Check Your Understanding

1. You run ./setup.sh and which vcs still reports nothing. What happened, and what is the fix?

2. How can you prove, from inside a script, whether it was sourced or executed?

3. Why does a sourced script not need the execute bit?

4. A colleague's setup script calls exit 1 on an error. What happens when you source it and the error occurs?

5. A sourced library reads $1 and gets a value nobody passed it. Where did it come from?

6. You source a tool setup three times and PATH has three copies of its bin. What two guards prevent that?

7. Remember

  • Executing runs a script in a new process; sourcing runs its lines in the shell you already have.
  • The PID is the proof. A sourced script reports your shell's $$.
  • Anything meant to change your current shell must be sourced. No flag makes an executed script change its parent.
  • source file and . file are identical; . is the POSIX spelling.
  • A sourced file needs only read permission, and its shebang is ignored — it runs in whatever shell sourced it.
  • source lib.sh searches PATH first. Write source ./lib.sh or a full path.
  • Tool setup scripts must be sourced because every line sets something the caller keeps. Executing one succeeds and achieves nothing.
  • Detect the mistake: compare ${BASH_SOURCE[0]} with $0.
  • exit in a sourced script exits your shell. Use return 1 2>/dev/null || exit 1.
  • A sourced script inherits the caller's positional parameters when given none of its own.
  • Sourcing runs everything with your authority. Read the file first.
  • Make setup scripts idempotent with a done-flag and a case guard on PATH.
  • One rule explains six earlier behaviours — cd as a builtin, scripts not changing your directory, the subshell in $(cd … && pwd), and the piped while loop that loses its counter.
  • When a compile differs between two engineers, the cause is usually a define or a flag in a build script — Chapter 24.5 finds them, and measures a -D that is accepted and ignored.
  • A setup script sourced twice doubles PATH unless it guards — and Chapter 25.6 measures the substring bug in the guard everyone writes first.

Sourcing a file can also define functions, and a function defined in your shell shadows a command of the same name. That is the last piece of "why did this name do something unexpected" — and the next chapter.

Where this fits

Part of the Linux & Bash curriculum.