SSD Nodes Learn Hosting plans →
Guides Matt ConnorBy Matt Connor

source vs ./script: which shell runs it

Sourcing a bash script runs it in your current shell. Executing forks a child. Why cd and export do not stick, and why source can close your session.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, August 23, 2026.

source vs ./script: which shell runs the lines

source script.sh and ./script.sh differ in exactly one way, and every other difference follows from it: source makes the shell you are typing into read the file and run the lines itself, while ./script.sh starts a new shell process that reads the file instead. . is the POSIX spelling of source, and scripts that must also run under sh use that form.

A new process gets a copy of your environment and a copy of your working directory. It can change its copy freely. When it ends, the copy ends with it, and the only things that reach you are its exit status and whatever it wrote to the terminal. A sourced file has no copy to work on, so every change it makes is a change to your shell.

Why did the script close my terminal?

A script that ends your session is how most people first meet this difference. The file contained exit, and you started it with source.

exit ends the shell that runs the line. Sourced, the shell running that line is your interactive shell, so ending it closes the window or drops the SSH connection. Executed, the shell running that line is the child process, and your own shell was never involved.

Try it in a terminal you do not mind losing.

cat > /tmp/quit.sh <<'EOF'
echo "reached the exit line"
exit 0
EOF
bash /tmp/quit.sh
echo "the shell that ran that command is still here"

The second echo runs because exit ended the child, not you. Now run source /tmp/quit.sh in that spare terminal and watch what the same exit does when your own shell is the one reading the file.

Make the fork visible with $$ and $BASHPID

Do not take the fork on trust. Print it. Write one small file that reports where it is running and then changes a few things.

cat > /tmp/who.sh <<'EOF'
echo "in script: PID=$$ BASHPID=$BASHPID PWD=$PWD"
MSG=hello
export MSG
cd /etc
EOF

Run it as a child first, then look at your own shell.

echo "in shell:  PID=$$ BASHPID=$BASHPID PWD=$PWD"
bash /tmp/who.sh
echo "after run: PWD=$PWD MSG=[$MSG]"

The PID the script reports is not the PID your shell reports, because the script is a different process. Your directory is unchanged and MSG is empty, because both changes were made inside a process that has already ended. Now source the same file, with nothing edited.

source /tmp/who.sh
echo "after source: PWD=$PWD MSG=[$MSG]"
cd -

This time the PID matches your shell, your directory is /etc until you cd - back, and MSG holds its value. The file did not change. The shell that ran it changed.

$$ needs one warning of its own, because people reach for it to detect a fork and it does not report one. $$ holds the process ID of the shell that started the script, and bash deliberately keeps it fixed inside subshells for compatibility with older scripts. $BASHPID holds the process ID of the process actually running the code at that moment.

echo "parent:   $$ $BASHPID"
( echo "subshell: $$ $BASHPID" )

The first number is the same on both lines and the second is not. That gap is a subshell: a forked copy of your shell that bash still labels with the parent's $$. Pipelines and command substitution with $(...) create the same kind of fork, which is why a while read loop on the right side of a pipe cannot set a variable the rest of your script can see.

Why a script cannot change your shell's directory

The working directory belongs to the process. A child gets a copy of it at fork time, cd moves the copy, and the copy is destroyed when the child exits. No exit status and no output can move your shell. This is not a bash setting you can turn on. It is how processes work on Linux.

Two ways around it are worth knowing. Source the file, which is the right answer when the file exists only to change your shell. Or keep the script executable and let your shell run the cd on the path the script prints.

cd "$(/usr/local/bin/pick-project.sh)"

A third option is a shell function in ~/.bashrc rather than a script at all. A function runs in your shell, so it can cd and set variables the way a script cannot. That is why tools which move you around, such as a Python virtualenv activate file, ship as something you source rather than something you run.

Why export inside a script does nothing for you

export marks a variable to be copied into the environment of every process this shell starts from that point on. The copy travels down to children. Nothing carries it back up. An executed script that exports a value is preparing the environment for the commands it runs, and handing you nothing.

The same holds for everything else that lives inside a shell: functions, aliases, shopt settings, set -o options, traps and the directory stack. For any of those to reach your shell, your shell has to be the process that reads the file.

To move a value from a child back to a parent, print it and capture it with command substitution, or write it to a file that the parent reads afterwards. There is no other channel, whatever the child does to its own copy.

What each way of starting a script needs

  1. source ./setup.sh needs read permission only. The exec bit is never consulted, and the #! line is only a comment, because no new interpreter is started.
  2. bash ./setup.sh also needs read permission only. You named the interpreter on the command line, so the #! line is not used here either.
  3. ./setup.sh needs the exec bit, and the first line of the file decides which interpreter the kernel starts, because the kernel is the thing being asked to run the file. #!/bin/bash names bash by path, and #!/usr/bin/env bash looks bash up on PATH instead.

One file shows all three. Give it a first line that names a program which is not a shell at all, then start it the three ways.

printf '#!/bin/cat\necho "the body ran"\n' > /tmp/shebang.sh
chmod +x /tmp/shebang.sh
source /tmp/shebang.sh
bash /tmp/shebang.sh
/tmp/shebang.sh

The first two treat that line as a comment, because a shell was already running and the file was only input to it. The third hands the whole file to the program the line names. Read the three results next to each other and the rule stops being something to memorise.

chmod +x setup.sh sets that bit, and the numeric and symbolic forms of chmod are two ways of writing the same change. Without the bit, ./setup.sh fails on permissions even though you can read and edit the file normally, and that mismatch is the part that confuses people.

The leading ./ is required because the current directory is not on PATH, and it should stay off PATH. sh ./setup.sh is a different case again: on Debian and Ubuntu /bin/sh is dash, so bash-only syntax such as the double bracket test and arrays stops working, and the bash shebang inside the file does not save you, because you named the interpreter yourself.

exit or return: which one belongs in a sourced file

Use return in a file you source. return ends the sourced file and hands control back to the caller, and the number you pass becomes the caller's $?. exit in the same position ends the caller, which is the session-closing behaviour above.

In an executed script it is the other way round. exit is correct there. return outside a function in a script that was not sourced is not valid, so bash refuses that line and carries on with the next one, which is rarely what the author meant.

A file that must be sourced should say so rather than fail in a confusing way. Put this at the top:

if [ "${BASH_SOURCE[0]}" = "$0" ]; then
  echo "this file must be sourced: source ${BASH_SOURCE[0]}" >&2
  exit 1
fi

$0 holds the name the current shell was started with, and ${BASH_SOURCE[0]} holds the path of the file bash is currently reading. When the file is executed, both hold the script path. When it is sourced, they differ, because $0 still belongs to your shell. The mirror image of the same test lets one file be a library and a command at once:

if [ "${BASH_SOURCE[0]}" = "$0" ]; then
  main "$@"
fi

Put that at the bottom. source lib.sh then defines the functions and runs nothing, while ./lib.sh defines them and calls main.

When sourcing is the right call on a server

Sourcing is correct when the whole point of the file is to change the shell that reads it. Four cases cover most server work.

  • An environment file for an interactive session: set -a, then source /etc/myapp.env, then set +a. set -a exports every variable assigned while it is on, so plain KEY=value lines become environment variables without an export on each one.
  • A virtualenv activate file, or any tool that edits PATH and defines a deactivate function. Executing it would edit a child's PATH and then throw the child away.
  • ~/.bashrc and the files in /etc/profile.d/, which exist to configure a shell. Never put exit in one of those.
  • A shared function library that several of your scripts pull in with source /usr/local/lib/myapp/common.sh at the top.

Sourcing an environment file carries one risk worth naming: source runs the file as bash code, so a $(...) or a backtick inside it executes as your user. Treat an env file from an unfamiliar place as a script, because that is what it is. Note also that systemd reads EnvironmentFile= without a shell, so quoting that a sourced file accepts can behave differently in a unit. Check both if the same file feeds your login shell and a systemd service and its timer.

Why source ~/.bashrc is the usual advice

Your shell read ~/.bashrc once, when it started. Editing the file changes the file. Nothing re-reads it, so the running shell still holds the old aliases and functions. source ~/.bashrc makes the shell you are typing into read the new version now, which is the exact effect you want, and the exact effect executing the file could never have.

It is not free. Sourcing runs the whole file again, so a line such as export PATH="$HOME/bin:$PATH" adds a second copy of that directory, and any command with a side effect happens a second time. Opening a new terminal is the clean version. exec bash sits in the middle: it replaces your shell with a fresh one in the same window, so the file is read from scratch, at the cost of losing shell variables you never exported.

When executing is the only option

Anything started by something other than your interactive shell has to be executed. There is no shell of yours for cron, systemd, an at job or a CI runner to source into. Each of them starts a process, and that process is the script.

Those environments do not read ~/.bashrc or ~/.profile, because those files are meant for interactive and login shells. The variables and PATH entries you take for granted at the prompt are simply not there. Set PATH explicitly at the top of the script and source your environment file from inside the script, and the job stops depending on how you happened to log in. A job that works when you type it and fails on a schedule is nearly always this, and the reasons a cron job does not run start at the same place.

Sourcing side effects that surface later

  • Shell options set in a sourced file stay set. set -u and shopt changes apply to your interactive shell and outlive the file, so the next command you type is judged by rules you no longer remember choosing.
  • A trap ... EXIT in a sourced file is installed in your shell. It fires when that shell exits, possibly hours later, not when the file finishes.
  • Variable names collide. A sourced file that assigns i or dir overwrites yours. Inside functions, declare with local so the name stays scoped.
  • A cd in a sourced file leaves you in the new directory. Wrap the work in a subshell, ( cd /srv/app && ./deploy.sh ), so the directory change is confined to the fork.

One question survives every edge case: which process runs this line? If the answer has to be your shell, source the file. If a child can do the work, execute it, and the same file will be safe to run from a cron entry or a unit file.

FAQ

What is the difference between source script.sh and ./script.sh?

source script.sh makes your current shell read the file and run every line itself, so variables, cd, functions and shell options all persist after it finishes. ./script.sh starts a separate shell process that reads the file, and that process works on a copy of your environment. The copy is discarded when it exits, so the only things you get back are its output and its exit status.

Why does my script not change my shell's current directory?

Because cd in an executed script moves the working directory of the child process, and that directory is destroyed along with the child. Source the file if it exists to move your shell, or define a shell function in ~/.bashrc instead of a script. A third option keeps the script executable: have it print the path and run cd "$(script.sh)" so your own shell performs the move.

How do I tell inside a script whether it was sourced or executed?

Compare ${BASH_SOURCE[0]} with $0. When the file is executed, both hold the script path. When it is sourced, $0 still holds the name of the shell you are in, so the two differ. Writing if [ "${BASH_SOURCE[0]}" = "$0" ]; then main "$@"; fi at the bottom of a file lets that file act as a library when sourced and as a command when run.

Does a script need chmod +x to be sourced?

No. source file and bash file both need read permission only, because no new program is launched from the file itself, and the #! line is ignored in both cases. The exec bit and the shebang matter for ./file, where the kernel starts the interpreter named on the first line.