unix-foundations Lesson 6 18 min read

How Do I Configure My Shell Environment?

Environment variables, PATH, aliases, and setting up your C development toolchain

Reading: Linux Text: Ch. 8–9, pp. 236–300

After this lesson, you will be able to:

  • Explain what the PATH variable is and how the shell uses it to find commands
  • View and set environment variables with echo, export, and env
  • Modify your shell configuration in ~/.bashrc and apply changes with source
  • Understand the difference between shell variables and environment variables
  • Add directories to PATH and make changes persistent across sessions

Why Does ./myprogram Work but myprogram Doesn’t?

You compile your first C program:

gcc -Wall -o hello hello.c

You try to run it:

hello
Command 'hello' not found

But this works:

./hello
Hello, world!

The program is right there. Why does the shell find it with ./ but not without? The answer involves something called the PATH — an environment variable that controls where the shell looks for programs. Understanding PATH (and environment variables in general) is the key to making your shell work the way you want.

Check Your Understanding
You compile hello.c and the executable hello is in your current directory. Why does hello give "command not found" but ./hello works?
A The file doesn't have execute permission
B You need to type the full path like /home/student/hello
C The current directory (.) is not in PATH, so the shell doesn't search here — ./ explicitly tells it to look in the current directory
D The program wasn't compiled correctly
Answer: C. When you type hello, the shell searches each directory in your PATH variable (like /usr/bin, /usr/local/bin) but NOT the current directory. This is a security feature — it prevents accidentally running a malicious program in the current directory. ./hello explicitly says "run the hello in this directory."

Variables, PATH, and Customization

Environment Variables

The shell maintains a collection of variables that store configuration information. Programs can read these to adapt their behavior.

See them all:

printenv | head -10

Or look at specific ones:

echo $HOME           # /home/student
echo $USER           # student
echo $SHELL          # /bin/bash
echo $PWD            # Current working directory

From Java: In CSCD 210, Gradle handled build configuration through build.gradle. Environment variables are Unix’s equivalent — they configure how programs behave, where to find things, and what defaults to use. In C, you’ll read them with getenv("HOME").

Creating Variables

Shell variables are local to your current session:

MY_NAME="Jessica Doner"
echo $MY_NAME

Environment variables are visible to child processes (programs you launch):

export GREETING="Hello from the shell"
echo $GREETING

The difference matters: a shell variable is only visible in your current shell. An exported environment variable is passed to every program you run from that shell.

Common Pitfall: Variable assignment has NO SPACES around =. X=5 works. X = 5 does NOT — bash interprets it as a command X with arguments = and 5. This is the #1 syntax error for shell beginners.

Check Your Understanding
You define CC=gcc in your shell and then run a Makefile that references $(CC). The Makefile can't find the variable. What did you forget?
A You need to use set CC=gcc instead
B You need export CC=gcc so child processes can see it
C You need to restart the terminal for the variable to take effect
D Shell variables can't be used in Makefiles under any circumstances
Answer: B. Without export, a variable is local to your shell session — child processes (like make) don't inherit it. export promotes a shell variable to an environment variable, making it visible to every program launched from that shell. This is the same distinction C's getenv() relies on.

The PATH Variable

PATH is a colon-separated list of directories where the shell searches for commands:

echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

When you type gcc, the shell searches these directories left to right:

  1. Check /usr/local/sbin — not there
  2. Check /usr/local/bin — not there
  3. Check /usr/sbin — not there
  4. Check /usr/binfound it! → runs /usr/bin/gcc
which gcc              # /usr/bin/gcc — shows where it was found

Why ./hello Is Required

The current directory (.) is NOT in your PATH by default. This is a security feature.

Imagine someone put a malicious program named ls in a shared directory. If . were in PATH, running ls there would run the malicious version instead of /usr/bin/ls. By requiring ./hello, you’re explicitly saying “run the program here, not from PATH.”

Key Insight: The ./ prefix explicitly tells the shell “look in the current directory.” Without it, the shell only searches the PATH directories. This is intentional security — never add . to your PATH on a shared system.

Why does this matter?

Every single lab in this course requires running your compiled program with ./programname. If you don’t understand why the ./ is necessary, you’ll waste time troubleshooting “command not found” errors on perfectly good executables. This is also your first taste of how Unix treats security as a default, not an afterthought.

Check Your Understanding
Your PATH is /usr/local/bin:/usr/bin:/bin. You type python3 and it runs /usr/bin/python3. Then you install a newer Python to /usr/local/bin/python3. Which version runs when you type python3 now?
A The newer one in /usr/local/bin — PATH is searched left to right
B The older one in /usr/bin — the first installed version always wins
C You get an error because two versions exist
D Whichever was most recently modified
Answer: A. The shell searches PATH directories from left to right and runs the first match. Since /usr/local/bin comes before /usr/bin in the PATH, the newer installation is found first. This is exactly why /usr/local/bin is listed first — it's where user-installed programs go, and they should take priority over system defaults. Use which python3 to verify which one the shell finds.

Shell Expansion

Before your command runs, the shell performs several expansions. Understanding these helps you write more efficient commands.

Brace expansion — generates sequences:

echo {a,b,c}                          # a b c
echo file{1,2,3}.txt                  # file1.txt file2.txt file3.txt
echo {01..05}                         # 01 02 03 04 05
mkdir -p project/{src,test,docs}      # Creates 3 directories at once

Tilde expansion:

echo ~                                # /home/student
echo ~/cscd240                        # /home/student/cscd240

Arithmetic expansion:

echo $((2 + 3))                       # 5
echo $((100 / 7))                     # 14 (integer division)
echo $((2 ** 10))                     # 1024

Command substitution — use a command’s output as text:

echo "Today is $(date)"
file_count=$(ls | wc -l)
echo "Found $file_count files"

Quoting: Controlling Expansion

Double quotes — allow variable and command substitution, suppress wildcards:

echo "My home is $HOME"              # My home is /home/student
echo "Files: *.txt"                   # Files: *.txt (no glob expansion)
echo "Today is $(date +%A)"          # Today is Friday

Single quotes — suppress ALL expansion:

echo 'My home is $HOME'              # My home is $HOME (literal)
echo 'Today is $(date)'              # Today is $(date) (literal)

Key Insight: Double quotes allow $variable and $(command) expansion but suppress filename globbing. Single quotes suppress everything — what you see is what you get. When in doubt, use double quotes.

Check Your Understanding
What does echo "There are $(ls | wc -l) files in $PWD" print?
A Literally: There are $(ls | wc -l) files in $PWD
B An error — you can't nest pipes inside $()
C The file count but $PWD printed literally
D Something like: There are 12 files in /home/student/cscd240
Answer: D. Double quotes allow both variable expansion ($PWD becomes the current directory path) and command substitution ($(ls | wc -l) runs the pipeline and substitutes the result). If this were in single quotes, you'd get option A — everything printed literally. Pipes work fine inside $() because the subshell processes the entire pipeline.
Deep dive: Shell expansion order — what happens before your command runs

The shell performs expansions in a specific order. Understanding this order explains why some combinations work and others don’t:

  1. Brace expansion{a,b,c}a b c (happens first, before any variable substitution)
  2. Tilde expansion~/home/student
  3. Parameter/variable expansion$HOME/home/student
  4. Command substitution$(date)Fri Mar 21 ...
  5. Arithmetic expansion$((2+3))5
  6. Word splitting — results are split on spaces/tabs/newlines
  7. Pathname expansion (globbing)*.cmain.c hello.c

This order explains some surprising behaviors:

# Braces expand BEFORE variables, so this doesn't work:
FILES="a,b,c"
echo {$FILES}         # Prints: {a,b,c}  — brace expansion already happened

# But this works because the variable is used in globbing (step 7):
EXT="c"
ls *.$EXT             # Lists all .c files — variable expands at step 3, glob at step 7

# Double quotes suppress steps 6 and 7 (word splitting and globbing):
echo "$HOME"          # /home/student — variable expanded, no splitting
echo '$HOME'          # $HOME — single quotes suppress ALL expansion

The key takeaway: double quotes let variables and commands expand but prevent accidental word splitting and globbing. Single quotes suppress everything. When you’re unsure, double-quote your variables: "$variable".

Escaping — the backslash suppresses the next character’s special meaning:

echo "The price is \$5.00"           # The price is $5.00

Aliases: Command Shortcuts

Aliases let you create shortcut names for commands:

alias ll='ls -la'
alias rm='rm -i'                      # Always ask before deleting
alias compile='gcc -Wall -o'

Now ll runs ls -la, and rm asks for confirmation. Check your aliases:

alias                                 # List all aliases
type ll                               # ll is aliased to 'ls -la'
unalias ll                            # Remove alias

But there’s a problem: aliases defined at the command line disappear when you close the terminal.

Making It Permanent: ~/.bashrc

The file ~/.bashrc runs every time you open a new terminal. This is where you put your customizations:

nano ~/.bashrc

Scroll to the bottom and add:

# My CSCD 240 customizations
alias ll='ls -la'
alias rm='rm -i'
alias compile='gcc -Wall -Wextra -std=c17 -o'

export EDITOR=nano
export CC=gcc
export CFLAGS="-Wall -Wextra -std=c17"

Save and reload:

source ~/.bashrc

The Trick: source ~/.bashrc re-reads your config file without opening a new terminal. You’ll use this every time you add a new alias or variable. It’s the equivalent of restarting your IDE after changing settings — except it takes less than a second.

Now those aliases and variables are available in every future terminal session.

Shell Startup Files

There are two main config files:

  • ~/.bash_profile — runs on login (SSH sessions, first terminal)
  • ~/.bashrc — runs when opening a new terminal window

On Ubuntu, ~/.bashrc is what matters for daily customization. If you’re confused about which to edit, edit ~/.bashrc.

From Java: In CSCD 210, Gradle’s build.gradle configured your project. In C development, your shell environment is your build configuration. Setting CC=gcc and CFLAGS="-Wall -Wextra" is equivalent to configuring compiler settings in your build tool. Your ~/.bashrc is your personal build configuration file.

Setting Up for C Development

Here’s a practical ~/.bashrc setup for this course:

# Compiler settings
export CC=gcc
export CFLAGS="-Wall -Wextra -std=c17"

# Convenient aliases
alias ll='ls -la'
alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'

# Quick compile shortcut
alias ccompile='gcc -Wall -Wextra -std=c17 -o'

With these in place, your C development workflow becomes:

cd ~/cscd240/labs/lab4
nano water_bill.c                    # Edit
ccompile water_bill water_bill.c     # Compile with all warnings
./water_bill                          # Run
Quick Check: What's the difference between MY_VAR="hello" and export MY_VAR="hello"?

Without export, MY_VAR is a shell variable — only visible in the current shell. With export, it becomes an environment variable — visible to any program you launch from that shell. In C, getenv("MY_VAR") only sees exported variables.

Quick Check: Why is the current directory (.) not in PATH by default?

Security. If . were in PATH, a malicious program with the same name as a common command (like ls) in any directory you visit could hijack that command. Requiring ./program is explicit and intentional.

Quick Check: What does source ~/.bashrc do?

It re-reads and applies your ~/.bashrc configuration file in the current shell session. Without it, changes to ~/.bashrc only take effect when you open a new terminal window.

Quick Check: What's the output of echo 'Home: $HOME'?

Literally Home: $HOME. Single quotes suppress all variable expansion. To see the actual home directory, use double quotes: echo "Home: $HOME".


Your Shell Is Now Yours

You’ve gone from a blank terminal to a personalized development environment. Your ~/.bashrc has aliases that save typing, compiler variables that enforce good habits (-Wall), and safety nets (rm -i). This is how every professional Unix developer works — they customize their shell once and carry those settings everywhere.

And with that, Series 1: Unix Foundations is complete. You can navigate the file system, read documentation, control permissions, redirect I/O, build pipelines, and configure your environment. That’s the entire Unix toolkit you need to start writing C.

In Series 2: C Foundations, you’ll write your first C program, learn how gcc turns source code into an executable, and start translating your Java knowledge into C. The shell you’ve been configuring is about to become your daily driver.

Ready to test your Unix skills? The CTF Challenges start with shell and file system puzzles that use everything from this series.

Big Picture: The six skills from this series — navigation, documentation, permissions, redirection, pipes, and environment — are the foundation for every lab in this course. When a lab says “compile with warnings enabled,” you know that means gcc -Wall. When it says “redirect output to a file,” you know >. When something breaks, you know how to read man pages and check permissions. These aren’t just Unix trivia — they’re your development workflow for the next nine weeks.