Computing FoundationsThe Command Line › Day 11

Hands-on lab — Day 11: Environment Variables and Shell Configuration

Commands

Setup

cd labs/sections/computing-foundations/day-011-environment-variables-and-shell-configuration

Run

bash examples/inspect_env.sh
bash starter/inspect_env.sh

Test

bash tests/run_tests.sh

File tree

examples/inspect_env.sh
examples/sample.env
expected-output/FIELDS.md
expected-output/sample-macos.txt
metadata.yml
README.md
requirements/README.md
security.md
starter/env-worksheet.md
starter/inspect_env.sh
tests/run_tests.sh
troubleshooting.md

Lab README

Day 011 lab — Explore and Extend Your Environment

Lesson

Purpose

Day 11's lesson explains the invisible layer every program inherits from your shell. This lab makes it concrete and safe: you inspect your own environment, watch an exported variable flow into a child process and not leak back out, define an alias, and see how PATH turns a bare command name into a specific program — all without changing your real setup.

Learning objectives

  • Read the key environment variables HOME, USER, SHELL, and LANG.
  • Print your PATH one directory per line and count its entries.
  • Demonstrate that export sends a value down to a child process and that a subshell variable does not leak back into the parent environment.
  • Define and run an alias, and resolve a command with command -v and type.
  • Record your environment on a worksheet and design one alias you would use.

Prerequisites

  • The Day 11 lesson (read it first — it explains every idea this lab exercises).
  • Days 8–10: comfort opening a terminal and running basic commands.
  • A terminal with bash: Terminal.app (macOS), any terminal (Linux), or WSL (Windows). No programming experience required.

Supported operating systems

  • macOS — fully supported (tested on macOS with Apple Silicon, zsh login shell).
  • Linux — fully supported (any distribution with bash, tr, sed, grep).
  • Windows — run the scripts unmodified inside WSL; native PowerShell is a different shell and is not supported by these bash scripts.

Hardware requirements

Any computer made in roughly the last 15 years. The lab only reads environment information; it needs no particular RAM, disk, or GPU.

Required software

  • bash (3.2 or newer — preinstalled on macOS and Linux).
  • Standard utilities only: tr, sed, grep, id, and shell builtins (echo, export, alias, type, command). All preinstalled.

Free and open-source options

Everything here is free and ships with your OS. No account, API key, or purchase is needed. The examples/sample.env file shows the popular dotenv pattern and mentions direnv (a free, open-source tool) as a way to automate per-project environments — both optional and free.

Installation

None. Clone the repository (or copy this directory) and you are ready:

cd labs/sections/computing-foundations/day-011-environment-variables-and-shell-configuration

File structure

day-011-environment-variables-and-shell-configuration/
├── README.md                          ← you are here
├── metadata.yml                       ← machine-readable lab metadata
├── starter/
│   ├── inspect_env.sh                 ← YOUR working file (4 exercises)
│   └── env-worksheet.md               ← worksheet for the practice assignment
├── examples/
│   ├── inspect_env.sh                 ← completed reference implementation
│   └── sample.env                     ← safe dotenv example (FAKE values only)
├── tests/
│   └── run_tests.sh                   ← automated checks
├── expected-output/
│   ├── sample-macos.txt               ← real captured run (macOS)
│   └── FIELDS.md                      ← required fields on every platform
├── requirements/
│   └── README.md                      ← dependency statement (none beyond the OS)
├── troubleshooting.md
└── security.md

How to run

From this directory:

## 1. See the finished result first
bash examples/inspect_env.sh

## 2. Your task: complete the four exercises in the starter, then run it
bash starter/inspect_env.sh

## 3. Check your work
bash tests/run_tests.sh

What the commands do

  • bash examples/inspect_env.sh — the reference script. It prints HOME, USER, SHELL, and LANG; lists PATH one directory per line and counts it; exports a variable inside a subshell and proves a child inherits it while the parent does not; defines and calls an alias; and resolves ls with command -v and cd with type. Everything runs in a child process and a subshell, so your real environment is untouched.
  • bash starter/inspect_env.sh — the same script with four exercises to fill in. Each comment names the exact command; replace each numbered placeholder.
  • bash tests/run_tests.sh — runs the reference (strict checks) and your starter (structure only until you finish, then strict), confirming the fields print, the export reaches the child, and the subshell variable does not leak.

Expected output

See expected-output/sample-macos.txt — a real captured run:

=== Environment Inspection ===
HOME:  <home>
USER:  you
SHELL: /bin/zsh
LANG:  C.UTF-8
PATH directories (one per line):
  - /usr/local/bin
  - /usr/bin
  - /bin
  - /usr/sbin
  - /sbin
PATH contains 5 directories.
--- Subshell variable demo ---
  child process sees demo_var = hello from a subshell
  after subshell, demo_var = '<empty, did not leak>'
--- Alias demo ---
  alias greet ran: hello
--- Command resolution ---
  command -v ls  -> /bin/ls
  type cd        -> cd is a shell builtin
=== End of inspection ===

Your HOME, USER, LANG, and PATH will differ — that is the point. The sample was captured with a short representative PATH for readability; a real machine's PATH is often longer. expected-output/FIELDS.md lists the fields that must appear on every platform.

Validation steps

  1. Run bash starter/inspect_env.sh after filling the exercises — it must exit without errors and print no REPLACE_ME text.
  2. Confirm the child process line shows hello from a subshell.
  3. Confirm the after subshell line shows <empty, did not leak> — the variable stayed inside the subshell.
  4. Run the tests (next section) — all checks must pass.

Tests

bash tests/run_tests.sh

Expected final line while the starter is unfinished: 13 checks, 0 failure(s). (12 strict checks against the reference script plus 1 structural check on the starter). Once you replace all four placeholders, the starter is held to the same strict standard and the count rises to 24 checks, 0 failure(s). The command exits 0 on success and non-zero on any failure, so it can run in CI.

Cleanup

Nothing to clean up: the scripts only read information and write nothing outside their own console output. The extension exercise in the lesson creates a ~/demo-bin directory and removes it in the same snippet. To reset your work, restore the starter from git: git checkout -- starter/inspect_env.sh.

Troubleshooting

See troubleshooting.md for the full list (variable expansion, alias expansion in scripts, subshell leakage, command -v misses, spaces around =, and WSL notes).

Security notes

See security.md. Short version: the scripts hold no real secrets, need no sudo, and make no network calls; the only example key is an obvious fake, and a real .env must always be listed in .gitignore.

Extension exercises

  1. Prepend a personal directory to PATH inside a subshell, drop a tiny script named after a common command into it, and use command -v to watch your copy win because it is searched first (the lesson's extension challenge walks through this).
  2. Load examples/sample.env into a subshell with set -a; source examples/sample.env; set +a and confirm with printenv APP_ENV that the values arrived — then note why you would never do this with a real committed .env.
  3. Add a PATH contains N directories style summary line for a second variable of your choice, and extend tests/run_tests.sh to check for it.
  • Previous day: Day 10 — Working with Text: cat, grep, sed (labs/sections/computing-foundations/day-010-working-with-text-cat-grep-sed/).
  • Next day: Day 12 — Shell Scripting: Variables, Loops, and Conditionals (labs/sections/computing-foundations/day-012-shell-scripting-variables-loops-and-conditionals/, to be written).

Expected output

FIELDS.md

# Required output fields (all platforms)

A correct run of `examples/inspect_env.sh` prints, in order:

1. `=== Environment Inspection ===`
2. `HOME:  <absolute path>`
3. `USER:  <your login name>`
4. `SHELL: <path to your login shell>` (often `/bin/zsh` or `/bin/bash`)
5. `LANG:  <locale>` (for example `en_US.UTF-8`; may read `C.UTF-8` or be empty on minimal systems)
6. `PATH directories (one per line):` followed by one `  - <dir>` line per entry
7. `PATH contains <n> directories.`
8. `--- Subshell variable demo ---`
9. `  child process sees demo_var = hello from a subshell` (the exported value reached the child)
10. `  after subshell, demo_var = '<empty, did not leak>'` (the variable did **not** escape the subshell)
11. `--- Alias demo ---` then `  alias greet ran: hello`
12. `--- Command resolution ---` then the resolved path of `ls` and the note that `cd` is a builtin
13. `=== End of inspection ===`

## About the captured sample

`sample-macos.txt` is a real run captured on macOS (Apple Silicon, zsh,
2026-07-12). It was captured with a **representative five-directory PATH** so
the list stays readable; a real developer machine usually has a longer PATH,
and the `PATH contains <n> directories` count will be higher. Every other line
is exactly what the script prints.

## Platform differences

- **Linux:** identical shape. `SHELL` is commonly `/bin/bash`; `type cd` still
  reports a shell builtin; `command -v ls` resolves to `/usr/bin/ls` or
  `/bin/ls` depending on the distribution.
- **Windows:** run under WSL, where output matches Linux. Native PowerShell is
  not supported by this bash script.
- No field may read `<unset>` on a normally configured interactive machine,
  though `LANG` can legitimately be empty in a bare container.

sample-macos.txt

$ bash examples/inspect_env.sh
# (captured on macOS with a representative PATH for readability;
#  a real machine's PATH is often longer — yours will differ)

=== Environment Inspection ===
HOME:  <home>
USER:  you
SHELL: /bin/zsh
LANG:  C.UTF-8
PATH directories (one per line):
  - /usr/local/bin
  - /usr/bin
  - /bin
  - /usr/sbin
  - /sbin
PATH contains 5 directories.
--- Subshell variable demo ---
  child process sees demo_var = hello from a subshell
  after subshell, demo_var = '<empty, did not leak>'
--- Alias demo ---
  alias greet ran: hello
--- Command resolution ---
  command -v ls  -> /bin/ls
  type cd        -> cd is a shell builtin
=== End of inspection ===

Source files

examples/inspect_env.sh (1813 bytes)
#!/usr/bin/env bash
# Day 011 lab — completed reference: explore your shell environment safely.
#
# Everything here runs inside this script (already a child of your shell), and
# the variable/PATH experiments are wrapped in a subshell "( ... )" so they
# CANNOT change your real environment. Reading system info only; writes nothing.
set -euo pipefail
shopt -s expand_aliases   # allow alias expansion inside this non-interactive script

echo "=== Environment Inspection ==="

# --- The most important environment variables ---
echo "HOME:  ${HOME:-<unset>}"
echo "USER:  ${USER:-$(id -un)}"
echo "SHELL: ${SHELL:-<unset>}"
echo "LANG:  ${LANG:-<unset>}"

# --- PATH, one directory per line ---
echo "PATH directories (one per line):"
# tr replaces each ':' separator with a newline so PATH reads as a list.
echo "${PATH}" | tr ':' '\n' | sed 's/^/  - /'
path_count="$(echo "${PATH}" | tr ':' '\n' | grep -c .)"
echo "PATH contains ${path_count} directories."

# --- Subshell variable demo: export flows DOWN to children, never back UP ---
echo "--- Subshell variable demo ---"
(
  demo_var="hello from a subshell"
  export demo_var
  # A brand-new child bash process inherits the exported value:
  bash -c 'echo "  child process sees demo_var = ${demo_var}"'
)
# Outside the subshell demo_var was never set, so it is empty here — proof that
# nothing leaked into the real environment.
echo "  after subshell, demo_var = '${demo_var:-<empty, did not leak>}'"

# --- Alias demo: shorthand for a longer command ---
echo "--- Alias demo ---"
alias greet='echo "  alias greet ran: hello"'
greet

# --- Command resolution: how PATH turns a name into a program ---
echo "--- Command resolution ---"
echo "  command -v ls  -> $(command -v ls)"
echo "  type cd        -> $(type cd)"

echo "=== End of inspection ==="
examples/sample.env (620 bytes)
# sample.env — a SAFE example of the dotenv pattern.
#
# NEVER commit a file with real secrets. A real .env belongs in .gitignore.
# Every value below is an obvious FAKE placeholder for teaching only.
# Load it into your current shell with:  set -a; source sample.env; set +a

# An API key would normally look like a long random string. This one is fake:
ANTHROPIC_API_KEY=sk-example-not-a-real-key

# A database connection string (also fake, and local-only):
DATABASE_URL=postgres://user:example-password@127.0.0.1:5432/myapp

# A plain non-secret setting is fine to keep here too:
APP_ENV=development
LOG_LEVEL=info
metadata.yml (584 bytes)
lesson_id: D011
day: 11
kind: configuration-activity
languages: [bash]
setup_commands:
  - cd labs/sections/computing-foundations/day-011-environment-variables-and-shell-configuration
run_commands:
  - bash examples/inspect_env.sh
  - bash starter/inspect_env.sh
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'git checkout -- starter/inspect_env.sh  # optional: reset your work'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon), bash tests/run_tests.sh → 13 checks, 0 failures'
requirements/README.md (677 bytes)
# Dependencies — Day 011 lab

**None beyond a POSIX shell.** This lab has zero installable dependencies:

- `bash` ≥ 3.2 (preinstalled on macOS and every mainstream Linux distribution)
- Standard OS utilities: `tr`, `sed`, `grep`, `id`, and the shell builtins
  `echo`, `export`, `alias`, `type`, and `command` — all part of the base
  system.

The scripts only **read** environment information and print it. They make no
network connections, need no elevated privileges, and write nothing outside
their own console output.

There is deliberately no `requirements.txt`/`package.json` here. Later labs
that use Python or Node declare their dependencies in this directory.
starter/env-worksheet.md (1436 bytes)
# Environment worksheet — Day 011

Fill this in for **your own machine** using the commands from the lesson and lab.
Do **not** edit your real shell config file for this assignment — just identify
the correct one.

## 1. Your PATH

- Command used: `echo "$PATH" | tr ':' '\n' | grep -c .`
- **How many directories are in your PATH?** ______

- Look at the first three entries. Command: `echo "$PATH" | tr ':' '\n' | head -3`
  1. ______________________________________
  2. ______________________________________
  3. ______________________________________

## 2. Your shell and locale

- Command used: `printenv SHELL LANG`
- **Your SHELL:** ________________________  (e.g. `/bin/zsh` or `/bin/bash`)
- **Your LANG:** _________________________  (e.g. `en_US.UTF-8`)
- Which config file does this shell read for a new interactive terminal?
  (`~/.zshrc` for zsh, `~/.bashrc` for bash): ________________________

## 3. An alias you would add

- **Alias definition** (name and command):

  ```bash
  alias ______='______________________________'
  ```

- **Why would you use it?** (1–2 sentences: what does it save you?)

  ____________________________________________________________________

  ____________________________________________________________________

- **Which config file would you add it to, and why is that the right file
  for your shell?**

  ____________________________________________________________________
starter/inspect_env.sh (1831 bytes)
#!/usr/bin/env bash
# Day 011 lab — YOUR working file: explore your shell environment safely.
#
# Four exercises are marked below. Each comment names the EXACT command to use.
# Replace each numbered placeholder with that command, then run:
#   bash starter/inspect_env.sh
# The completed reference is in examples/inspect_env.sh — try yours first.
#
# Everything runs inside this script (a child of your shell); the subshell
# "( ... )" experiment cannot touch your real environment.
set -euo pipefail
shopt -s expand_aliases   # allow alias expansion inside this non-interactive script

echo "=== Environment Inspection ==="

# The most important environment variables are already printed for you:
echo "HOME:  ${HOME:-<unset>}"
echo "USER:  ${USER:-$(id -un)}"
echo "SHELL: ${SHELL:-<unset>}"
echo "LANG:  ${LANG:-<unset>}"

# --- Exercise 1: print PATH one directory per line ---
# Use: echo "${PATH}" | tr ':' '\n'
echo "PATH directories (one per line):"
REPLACE_ME_1

# --- Exercise 2: export a variable inside the subshell so the child sees it ---
echo "--- Subshell variable demo ---"
(
  demo_var="hello from a subshell"
  # Exercise 2: mark demo_var for inheritance. Use: export demo_var
  REPLACE_ME_2
  bash -c 'echo "  child process sees demo_var = ${demo_var}"'
)
echo "  after subshell, demo_var = '${demo_var:-<empty, did not leak>}'"

# --- Exercise 3: define an alias, then call it ---
echo "--- Alias demo ---"
# Exercise 3: define an alias named greet. Use: alias greet='echo "  alias greet ran: hello"'
REPLACE_ME_3
greet

# --- Exercise 4: show which file PATH resolves for the ls command ---
echo "--- Command resolution ---"
# Exercise 4: print the resolved path of ls. Use: command -v ls
echo "  command -v ls  -> $(REPLACE_ME_4)"
echo "  type cd        -> $(type cd)"

echo "=== End of inspection ==="
tests/run_tests.sh (3395 bytes)
#!/usr/bin/env bash
# Tests for the Day 011 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Verifies that the completed reference script prints every required field,
# that an exported variable reaches a child process, and — the key safety
# check — that the subshell variable does NOT leak into the environment.
# Also checks the starter's structure (or holds it to the strict standard
# once the learner has replaced every REPLACE_ME placeholder).
set -u

lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
failures=0
checks=0

check() {
  local label="$1" ok="$2"
  checks=$((checks + 1))
  if [ "${ok}" = "yes" ]; then
    echo "  ok: ${label}"
  else
    echo "  FAIL: ${label}"
    failures=$((failures + 1))
  fi
}

run_strict_checks() {
  local script="$1" output
  echo "Testing ${script} ..."
  if ! output="$(bash "${script}" 2>&1)"; then
    check "script exits successfully" "no"
    echo "${output}" | sed 's/^/    /'
    return
  fi
  check "script exits successfully" "yes"

  echo "${output}" | grep -q '^=== Environment Inspection ===$' && check "prints header" "yes" || check "prints header" "no"
  echo "${output}" | grep -q '^=== End of inspection ===$' && check "prints footer" "yes" || check "prints footer" "no"

  for field in "HOME:" "USER:" "SHELL:" "LANG:"; do
    echo "${output}" | grep -q "^${field}" && check "prints '${field}'" "yes" || check "prints '${field}'" "no"
  done

  echo "${output}" | grep -q 'PATH directories (one per line):' && check "prints PATH heading" "yes" || check "prints PATH heading" "no"

  # Export flowed DOWN: the child process must see the exported value.
  echo "${output}" | grep -q 'child process sees demo_var = hello from a subshell' \
    && check "exported variable reached the child process" "yes" \
    || check "exported variable reached the child process" "no"

  # Safety check: the variable must NOT have leaked past the subshell.
  after_line="$(echo "${output}" | grep 'after subshell, demo_var =')"
  if echo "${after_line}" | grep -q 'hello from a subshell'; then
    check "subshell variable did NOT leak into the environment" "no"
  else
    check "subshell variable did NOT leak into the environment" "yes"
  fi

  echo "${output}" | grep -q 'alias greet ran' && check "alias was defined and ran" "yes" || check "alias was defined and ran" "no"

  # command -v resolved ls to a real, absolute path.
  ls_path="$(echo "${output}" | sed -n 's/^  command -v ls  -> //p')"
  case "${ls_path}" in
    /*) check "command -v resolved ls to an absolute path" "yes" ;;
    *)  check "command -v resolved ls to an absolute path" "no" ;;
  esac
}

# The reference example is always held to the strict standard.
run_strict_checks "${lab_dir}/examples/inspect_env.sh"

# The starter ships with REPLACE_ME placeholders. Only run it once the learner
# has replaced them all; before that, just confirm the exercises are present.
starter="${lab_dir}/starter/inspect_env.sh"
if grep -q 'REPLACE_ME_[1-4]' "${starter}"; then
  echo "Testing ${starter} (structure only — exercises not yet completed) ..."
  markers="$(grep -c 'REPLACE_ME_[1-4]' "${starter}")"
  [ "${markers}" -eq 4 ] && check "starter defines all 4 exercises" "yes" || check "starter defines all 4 exercises" "no"
else
  run_strict_checks "${starter}"
fi

echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 011 lab

echo "$PATH" prints a literal $PATH

You are in a context that does not expand variables, or you used single quotes. In bash or zsh, use double quotes: echo "$PATH". Inside single quotes the shell does not substitute variables.

The alias in the script did not run (greet: command not found)

By default bash does not expand aliases in a non-interactive script. The scripts here fix this with shopt -s expand_aliases near the top, and define the alias on its own line before calling it. If you write your own script, include that line, and remember an alias must be defined on a line before the line that uses it.

DEMO_VAR / demo_var is still set after the subshell

You ran the export outside the parentheses. The whole point of the ( ... ) wrapper is to confine the variable to a child shell. Keep the assignment and export inside the parentheses; after the subshell exits the variable is gone, which the script confirms with the after subshell line.

command -v prints nothing for a command

That command is genuinely not on your PATH. Try one you know exists (command -v ls) to confirm the tool works. If a tool you just installed is not found, its directory is probably missing from PATH, or the shell has cached an old location — open a new terminal, or run hash -r.

Spaces around = cause an error

NAME = value is not an assignment; the shell tries to run a command called NAME. Always write NAME=value with no spaces around the equals sign.

bash -c inside the subshell shows an empty value

You set the variable but did not export it, so the child bash -c process did not inherit it. Add export demo_var before the bash -c line.

Windows: bash is not recognized

Install WSL (wsl --install), open the Ubuntu terminal, and run the scripts there. Native PowerShell is a different shell and is not supported by these bash scripts. You can still inspect variables in PowerShell with Get-ChildItem Env: and $Env:PATH, but the lab scripts expect bash.

Security notes

Security notes — Day 011 lab

Environment variables are the correct place to hold secrets — and therefore a place to handle with care. This lab is built so you can practice safely.

  • The scripts hold no real secrets. Everything is read-only inspection of your own environment. The only "secret" anywhere is in examples/sample.env, and it is an obviously fake placeholder (sk-example-not-a-real-key), present purely to show the dotenv shape.
  • Never commit a real .env. A file holding real keys or passwords must be listed in .gitignore so it is never added to version control. The single most common way developers leak credentials is committing a .env (or pasting one into an issue or chat). Treat any leaked key as compromised and rotate it — generate a new one and revoke the old.
  • The subshell keeps experiments contained. The variable and PATH demos run inside ( ... ), a child shell. Nothing they set can change your real environment, so you can run the lab repeatedly with no lasting effect.
  • No elevated privileges, no network. Nothing here needs sudo or touches the network. If any tutorial ever asks you to sudo a script you have not read, stop and read it first.
  • A dumped environment is mildly identifying. printenv reveals your username, home path, locale, and installed tooling. Redact before sharing a full environment dump, and never share one that contains a real secret.