Computing Foundations › The Command Line › Day 11
Hands-on lab — Day 11: Environment Variables and Shell Configuration
- ← Back to the Day 11 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/computing-foundations/day-011-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
- Lesson title: Environment Variables and Shell Configuration
- Day number: 11 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-011-environment-variables-and-shell-configuration
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-011-environment-variables-and-shell-configurationwhen the site is running.
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, andLANG. - Print your
PATHone directory per line and count its entries. - Demonstrate that
exportsends 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 -vandtype. - 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 printsHOME,USER,SHELL, andLANG; listsPATHone 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 resolveslswithcommand -vandcdwithtype. 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
- Run
bash starter/inspect_env.shafter filling the exercises — it must exit without errors and print noREPLACE_MEtext. - Confirm the child process line shows
hello from a subshell. - Confirm the
after subshellline shows<empty, did not leak>— the variable stayed inside the subshell. - 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
- Prepend a personal directory to
PATHinside a subshell, drop a tiny script named after a common command into it, and usecommand -vto watch your copy win because it is searched first (the lesson's extension challenge walks through this). - Load
examples/sample.envinto a subshell withset -a; source examples/sample.env; set +aand confirm withprintenv APP_ENVthat the values arrived — then note why you would never do this with a real committed.env. - Add a
PATH contains N directoriesstyle summary line for a second variable of your choice, and extendtests/run_tests.shto check for it.
Navigation
- 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.gitignoreso 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
sudoor touches the network. If any tutorial ever asks you tosudoa script you have not read, stop and read it first. - A dumped environment is mildly identifying.
printenvreveals your username, home path, locale, and installed tooling. Redact before sharing a full environment dump, and never share one that contains a real secret.