Skip to content
VLSI Mentor

Linux & Bash · Part I · Module 15 · Writing and Running Bash Scripts

What a Script Inherits, and What It Cannot Change

Modules 7 and 10 established how a child process works. This chapter applies it to the script you just wrote, which is where it stops being theory — because the most common bug in a new script is a relative path that was relative to somewhere else.

Intermediate17 min readLinuxPart IFundamentalsScripting

Part I · Module 15 · Chapter 15.3 · What a Script Inherits

Chapter 7.4 and Chapter 10.1 proved the rules. This chapter is them, applied to your own file.

1. A Script Is a Child Process

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo "parent PID=$$ cwd=$(pwd)"
parent PID=72879 cwd=/tmp/m15v

$ ./inherit.sh
  PID=72930  PPID=72879
  cwd=/tmp/m15v
  exported=[yes]  local=[unset]
  $0=[./inherit.sh]
  changed to /tmp

$ pwd
/tmp/m15v

Four facts in one run:

  • A different PID, with PPID matching the shell that started it — Chapter 7.1's parentage.
  • The working directory was inherited — the script started where the caller was standing.
  • An exported variable arrived; a plain one did not — Chapter 10.1.
  • The script's cd did not escape. The caller is still where it was.
What a script receives from whoever ran it, and what it cannot send back. It starts with a copy of the exported variables, the working directory and the open streams, and it may change any of them freely. But every change applies to its own copy, so when it finishes nothing it did reaches the shell that started it.It starts with a copyexported variables, directory,open streamsIt may change any ofthemand the changes work perfectlyInside the script,everything holdswhich is why testing it looksfineThen the scriptfinishesand its process endsThe copy goes with itthere is no path back to theparentOnly one number returnsthe exit status, and nothingelse at all12
Figure 1 — what a script receives from whoever ran it, and what it cannot send back. It starts with a copy of the exported variables, the working directory and the open streams, and it may change any of them freely — which is why it behaves correctly while you are testing it. But every change applies to its own copy, so when the script finishes, nothing it did reaches the shell that started it. One number comes back, and nothing else.

2. The Inventory

Inherited — a copy arrives:

ThingNote
Exported variablesOnly exported ones — 10.1
The working directoryWherever the caller was
The three streamsSo a redirect on the caller's line covers the script — 12.1
The umask5.6
Resource limitsOpen files, memory
Signal dispositionsIgnored signals stay ignored — 7.6
The controlling terminalWhich is why output appears — and why 7.8 matters

Not inherited — the script gets its own, or nothing:

ThingNote
Non-exported shell variablesThe caller's ordinary variables are invisible
Shell functions and aliasesUnless exported with export -f — 10.5
Shell optionsA script sets its own set and shopt — 10.6
The shell's historyNot a thing a script has

Returned — exactly one thing:

ThingNote
The exit statusOne integer, 0 to 255 — 15.6

3. The Bug Everybody Writes First

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cat proj/rel.sh
#!/bin/bash
cat data/f.txt

$ cd proj && ./rel.sh
the data

$ cd .. && ./proj/rel.sh
cat: data/f.txt: No such file or directory

The same script, run two ways, one of which fails.

data/f.txt is a relative path, and a relative path resolves from the working directory (Chapter 2.2) — which the script inherited from the caller. Run from inside proj, that is proj. Run from outside, it is wherever you were, where no data directory exists.

Nothing about the script changed. The caller's directory did.

4. Making a Script Independent of Where It Was Run

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

That line gives you the absolute directory the script itself lives in, regardless of where it was invoked from.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cd proj && ./abs.sh
the data
$ cd .. && ./proj/abs.sh
the data
$ cd / && /tmp/m15v/proj/abs.sh
the data

Three different working directories, one correct answer.

Reading it from the inside out, using Chapter 8.4:

PieceDoes
${BASH_SOURCE[0]}The script's own path, as invoked
dirname …Its directory, possibly relative
cd … && pwdTurn that into an absolute path
$( … )Capture it — in a subshell, so the cd does not affect the script

That last point is Chapter 11.4 being used deliberately: the cd happens inside the substitution's subshell and the script's own directory is untouched.

Then every path is built from it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

source "$script_dir/lib/common.sh"
filelist="$script_dir/filelist.f"
outdir="$script_dir/runs"

The alternative: refuse to guess

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
project_root="${PROJECT_ROOT:?set PROJECT_ROOT before running this}"

Rather than working out where it is, the script requires the caller to say. Chapter 8.2's :? form names the variable in the error.

Both are legitimate. script_dir suits a script living inside the tree it operates on; an explicit variable suits one that operates on a tree chosen at run time.

What is not legitimate is a bare relative path. It is a bet that the caller is standing somewhere particular, and nothing records what that place was.

5. What the Script Cannot Do

Three things people try, all of which fail for the same reason.

It cannot change your directory.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
cd /home/engineer/project     # changes the script's copy and nothing else

It cannot set a variable in your shell.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
export TOOL_HOME=/opt/tool    # exports into the script's own environment

It cannot define a function or alias for you.

All three need the script to be sourced (Chapter 10.4), which runs it in your shell with no child process at all.

6. Being Deliberate About the Environment

A script inherits whatever the caller had, which is a different thing in a terminal, in cron, and in a build system. Three habits make that stop mattering.

Require what you need.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
: "${PROJECT_ROOT:?set PROJECT_ROOT}"
: "${LM_LICENSE_FILE:?the licence server is not configured}"

Set what you depend on.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
export PATH="/opt/tool/bin:/usr/local/bin:/usr/bin:/bin"
export LC_ALL=C      # so sort order is reproducible — Chapter 13.3

That PATH line is the standard fix for a cron job that cannot find a tool (Chapter 10.3 — a script reads no startup file).

Source the environment rather than assuming it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
. /opt/tools/vcs/2024.09/setup.sh

Explicit, versioned, and visible to the next reader — rather than depending on whatever the caller happened to have sourced.

7. Streams Are Inherited Too

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./outer.sh > everything.log 2>&1

Chapter 12.1 §7 showed this: the redirect applies to the script and everything it starts, because each child receives a copy of the stream table.

Two consequences for your own scripts:

Do not redirect inside the script when the caller might want to.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make > build.log 2>&1        # forces a destination on the caller
make                         # lets the caller decide

A script that writes its own log file cannot be used in a pipeline and cannot have its output captured. Unless logging is the script's purpose, let the output go where the caller pointed it.

Put diagnostics on the error stream. Chapter 12.1 and Chapter 12.3 both made the point; in a script it is the difference between a warning the caller sees and one that disappears into their log.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
echo "$result"                        # the answer — stream two
echo "warning: using defaults" >&2    # a diagnostic — stream three

8. Putting It Together

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/usr/bin/env bash
#
# run_regression.sh — run every test in the list and summarise.
#
# Works from any directory. Requires PROJECT_ROOT only if the tree is
# elsewhere; otherwise it uses its own location.

set -euo pipefail

# Where am I? — independent of where the caller was standing.
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

# What must come from outside, and what I will default.
project="${PROJECT_ROOT:-$script_dir}"
testlist="${1:-$script_dir/tests.csv}"

# What I depend on, set explicitly rather than inherited.
export LC_ALL=C
[ -r "$testlist" ] || { echo "cannot read $testlist" >&2; exit 2; }
command -v vcs >/dev/null 2>&1 || { echo "vcs not on PATH" >&2; exit 3; }

echo "project : $project"
echo "tests   : $testlist"

# No redirect here — the caller decides where output goes.

Five decisions, each from this chapter:

  • script_dir first, so nothing later depends on the caller's directory.
  • ${PROJECT_ROOT:-$script_dir} — allow an override, default to something sensible.
  • export LC_ALL=C rather than inheriting whatever locale the caller had.
  • Preconditions checked and named, with documented exit codes.
  • No internal redirect, so the script composes.

Practice

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mkdir -p /tmp/inh/proj/data && cd /tmp/inh
printf 'the data\n' > proj/data/f.txt
export OUTER_EXPORTED=yes; OUTER_LOCAL=no

1 — Prove the four inheritance facts.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cat > p.sh <<'EOF'
#!/bin/bash
echo "  PID=$$ PPID=$PPID"
echo "  cwd=$(pwd)"
echo "  exported=[${OUTER_EXPORTED-unset}] local=[${OUTER_LOCAL-unset}]"
cd /tmp; echo "  moved to $(pwd)"
EOF
chmod +x p.sh
echo "shell PID=$$ cwd=$(pwd)"
./p.sh
echo "shell cwd after: $(pwd)"

2 — Write the bug.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf '#!/bin/bash\ncat data/f.txt\n' > proj/rel.sh; chmod +x proj/rel.sh
( cd proj && ./rel.sh )
./proj/rel.sh

3 — Fix it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cat > proj/abs.sh <<'EOF'
#!/bin/bash
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
cat "$script_dir/data/f.txt"
EOF
chmod +x proj/abs.sh
( cd proj && ./abs.sh )
./proj/abs.sh
( cd / && /tmp/inh/proj/abs.sh )

Three directories, one answer.

4 — See $0 and BASH_SOURCE diverge.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'echo "  \$0=[$0]  BASH_SOURCE=[${BASH_SOURCE[0]}]"\n' > src.sh
chmod +x src.sh
./src.sh
. ./src.sh

5 — Prove a script cannot change your shell.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf '#!/bin/bash\nexport FROM_SCRIPT=yes\ncd /tmp\n' > setenv.sh; chmod +x setenv.sh
./setenv.sh;      echo "  executed: [${FROM_SCRIPT-unset}] cwd=$(pwd)"
. ./setenv.sh;    echo "  sourced:  [${FROM_SCRIPT-unset}] cwd=$(pwd)"
cd /tmp/inh

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; echo "  status $?"
. ./guarded.sh && echo "  sourced fine: $GUARDED"

7 — Test with a scrubbed environment.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf '#!/bin/bash\necho "HOME=[$HOME] PATH head=[${PATH%%:*}]"\nls /tmp >/dev/null && echo "ls found"\n' > envtest.sh
chmod +x envtest.sh
./envtest.sh
env -i HOME="$HOME" PATH=/usr/bin:/bin ./envtest.sh
env -i ./envtest.sh

8 — Confirm streams are inherited.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf '#!/bin/bash\necho "outer"\n./p.sh\n' > outer.sh; chmod +x outer.sh
./outer.sh > all.log 2>&1
cat all.log

9 — Clean up. cd /tmp && rm -rf /tmp/inh

Check Your Understanding

1. Name four things a script inherits and two it does not.

2. ./build.sh works from the project directory and fails from your home directory. What is wrong?

3. What does $(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) give you, and why does the cd not affect the script?

4. When do $0 and ${BASH_SOURCE[0]} differ?

5. Your setup script exports a variable and nothing happens. What are the two fixes?

6. Why should a script usually not redirect its own output to a log file?

9. Remember

  • A script is a child process: new PID, the caller's PPID, a copy of everything.
  • Inherited: exported variables, the working directory, the three streams, the umask, limits, signal dispositions, the terminal.
  • Not inherited: non-exported variables, functions, aliases, shell options.
  • Returned: one integer, and nothing else.
  • The whole interface is eight items — arguments, environment, input, directory in; output, errors, status, filesystem out.
  • A relative path resolves from the caller's directory, which is why a script works where you wrote it and fails everywhere else.
  • script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) makes a script independent of where it was run.
  • The cd in that idiom is inside a subshell, which is why it is safe.
  • Use BASH_SOURCE[0], not $0, so the idiom works when sourced too.
  • A script cannot change your directory, your variables or your functions. Those need sourcing.
  • Decide which kind of script you are writing, and guard a sourced one so the mistake reports itself.
  • Require what you need with :?, set what you depend on, source environments explicitly.
  • Test with env -i to find undeclared dependencies on your own setup.
  • Do not redirect your own output unless logging is the point — the caller decides.

A script knows where it is. The next chapter is how it learns what it was asked to do.

Where this fits

Part of the Linux & Bash curriculum.