The Black Screen Scared Me
Right after attending my first developer meetup, I watched an experienced developer deploy something. He opened a black terminal window and typed what looked like an incantation: cd /var/www && git pull && npm run build && pm2 reload all. Green and red text flooded the screen, then he said "deployment done" and closed the window.
I was stunned. Deployment happened without a single mouse click. "Do I just memorize these commands?" I asked. "Nope, you just write a shell script," he replied. Until then, I thought the terminal was just "a black screen developers use to look cool." I had no idea what it was actually for.
The Days I Tried to Memorize Commands
At first, I thought memorizing shell commands was the answer. ls, cd, mkdir, rm... I made a dictionary and memorized them. But commands were endless. Options were all different. ls -l, ls -al, ls -alh... I couldn't tell what was different, or why there were so many options.
Then I hit a wall with PATH. I installed Python3, but typing python3 in the terminal gave me "command not found." I definitely installed it, so why doesn't it work? Googling only returned answers like "add it to environment variable PATH." They said to add something to .bashrc, but I didn't even know what .bashrc was. Why does the filename start with a dot? I learned later that these were hidden files.
What was even more absurd was when I got a new MacBook and my old scripts stopped working. I had written them in bash syntax, but macOS now defaults to zsh, and some syntax didn't match. "There are multiple shells? What's going on?" That's when I started digging into shells themselves.
Aha Moment: Shell Is Just a Program
The real epiphany came when I realized "shell itself is just a program." I thought terminal and shell were the same thing. But they weren't. Terminal is the window, and shell is the program running inside it. Like running YouTube (shell) inside Chrome browser (terminal).
What surprised me more was that shells are also executable files. Files like /bin/bash, /bin/zsh actually exist on disk. Running these files launches the shell. So every command I typed in the terminal was being read, parsed, and executed by a program called shell. Like typing JavaScript into browser's dev console and having the V8 engine run it.
Then I realized. "Different shells can have different syntax." Whether it's bash or zsh, they're ultimately different programs. Just like Python2 and Python3 differ.
Deep Dive: How Shell Executes Commands
Kernel vs Shell: Core and Crust
At the center of the operating system sits the Kernel. The kernel actually manages hardware resources like CPU, memory, disk, and network. It's the real boss. When we run programs, read files, or send network requests, the kernel ultimately handles it.
But we can't talk directly to the kernel. The kernel only communicates through an interface called system calls, which is too complex for humans to use. It's a world of zeros and ones. So we need a translator in between, and that's the Shell.
Shell literally means crust. Like a seashell wrapping around a kernel, it serves as the outer layer. When we give commands in human language, the shell receives them and converts them into system calls the kernel understands. Shell is a translator and secretary.
For example, when I type mkdir project:
- Shell figures out "user wants to create a directory."
- Sends a
mkdir()system call to the kernel. - Kernel creates the directory in the file system.
- Returns the result to shell.
- Shell either displays nothing or shows an error message.
This happens so fast we feel like "commands execute immediately when I type them."
Command Interpretation Process: Read → Parse → Execute
Shell is ultimately an interpreter running an infinite loop. Also called REPL (Read-Eval-Print Loop). What shell does breaks down into three stages.
Stage 1: Read (Reading Input)
When you type ls -al /Users in terminal and hit Enter, shell reads the entire string. At this point, shell knows nothing yet. Just "user typed something."
Stage 2: Parse (Interpreting Command)
Now shell splits the received string into tokens. Divides by spaces into three chunks: ls, -al, /Users. Then it needs to figure out what the first token ls is.
Shell searches like this:
- Is it a built-in command? Things like
cd,echo,aliasare built into the shell. No separate executable file. - Is it a defined function? Could be a function I defined in
.bashrcor.zshrc. - Is it an external executable? Searches through directories registered in
PATHenvironment variable in order looking for a file namedls.
PATH is a list of paths shell references when finding commands. Usually looks like this:
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
Shell explores these paths from left to right. Is there /usr/local/bin/ls? If not, how about /usr/bin/ls? Eventually finds /bin/ls and confirms "aha, this is the file to execute."
Stage 3: Execute (Execution)
Now shell asks kernel "please run this program." Specifically:
- Calls
fork()system call to copy the shell process. A child process is created. - Child process calls
exec()system call and transforms into the/bin/lsprogram. - Child process executes the
lsprogram. - Execution result (file list) goes to standard output (stdout).
- Parent process (shell) waits for child to finish with
wait()system call. - When child finishes, shell displays prompt (
$) again and waits for next command.
This whole process takes less than a second. Shell runs an infinite loop waiting for input, parsing it when it arrives, executing, and waiting again.
Types of Shells: sh, bash, zsh, fish
There's not just one shell. Multiple implementations exist. Like browsers having Chrome, Safari, Firefox.
sh (Bourne Shell)
The grandfather of all shells. Created in 1977. Not many features, but comes default on almost every Unix system. When writing shell scripts, starting with #!/bin/sh makes them run almost anywhere. Compatibility is its strength.
bash (Bourne Again Shell)
Linux's standard shell. Created by GNU project. Extended sh's features. Added command history, auto-completion, variable expansion. Most Linux distributions use it as default shell. The first shell I learned was bash too.
zsh (Z Shell)
The evolution of bash. Has all bash features while providing more powerful auto-completion, themes, and plugin systems. Thanks to frameworks like Oh My Zsh, you can customize it beautifully and conveniently. macOS uses zsh as default shell since Catalina. I use zsh now too.
fish (Friendly Interactive Shell)
A new-generation shell with different syntax. Unlike bash and zsh, it doesn't follow POSIX standards. Instead provides user-friendly syntax. Auto-completion is genuinely good. When typing commands, it shows gray auto-complete suggestions. But the downside is existing bash scripts won't run.
Which shell to use? I recommend zsh. Has bash script compatibility and many convenience features. No reason to insist on sh or bash. Though it's preference of course.
Environment Variables: Shell's Configuration Storage
Shell uses a concept called Environment Variables. These are configuration values shell references during execution. Like game settings.
Representative environment variables:
PATH
List of paths to search for commands. Explained earlier. If you installed a new program but can't see it in terminal, you need to add that program's directory to PATH.
export PATH="/usr/local/bin:$PATH"
This adds /usr/local/bin to the front. Then shell checks there first when finding commands.
HOME
Current user's home directory. Typing cd ~ moves to $HOME. Contains paths like /Users/ratia.
SHELL
Path of currently used shell. Contains values like /bin/zsh.
USER
Current username. The whoami command outputs this.
Environment variables are set with export command. Putting them in shell configuration files automatically sets them every time shell starts.
Shell Configuration Files: .bashrc, .zshrc, .profile
Shell reads configuration files at startup. Defining aliases, environment variables, functions here automatically applies them every time shell launches.
.bashrc
Configuration file for bash. Located in home directory (~). Bash reads and executes this file at startup. Define aliases and functions here.
# .bashrc example
alias ll='ls -alh'
export PATH="/opt/homebrew/bin:$PATH"
function mkcd() {
mkdir -p "$1" && cd "$1"
}
.zshrc
Configuration file for zsh. Structure similar to .bashrc. If using zsh, put settings here.
.profile or .bash_profile
Configuration file executed only once at login. Mainly put environment variables here. .bashrc executes every time you open a new terminal window, but .profile executes only once per login session.
These files start with a dot (.) making them hidden files. Won't show with ls, need ls -a. I couldn't find these files at first either.
Piping and Redirection: The Art of Assembling Commands
Shell's real power comes from Pipe and Redirection. Knowing these lets you assemble commands like LEGO blocks.
Pipe (|)
Pipe connects one command's output to the next command's input. Data flows like water through pipes.
ls -al | grep ".js" | wc -l
This command:
ls -al: Lists all files in current directory.grep ".js": Filters only lines containing.js.wc -l: Counts filtered lines.
Result tells "how many .js files in current directory?" Combined three commands to create new functionality.
Redirection (>, >>, <)
Redirection sends input/output to files or reads from files.
# Save output to file (overwrite)
echo "Hello World" > hello.txt
# Append output to file
echo "Second Line" >> hello.txt
# Read input from file
wc -l < hello.txt
> overwrites file, >> appends to file end. < uses file as input.
Error Redirection (2>&1)
Standard output (stdout) is stream 1, standard error (stderr) is stream 2. To save errors to file too:
npm run build > build.log 2>&1
This puts both standard output and standard error into build.log file. Often used to log when writing deployment scripts.
Shell Expansion: Globbing and Brace Expansion
Shell does expansion first before executing commands. Shell transforms strings we input on its own.
Globbing
Replaces wildcards like * or ? with actual filenames.
rm *.log
Shell first replaces *.log with list of all .log files in current directory. Then passes that list to rm command. Actually executed command looks like:
rm error.log debug.log access.log
Shell automatically expanded it.
Brace Expansion
Uses curly braces to generate multiple strings.
mkdir -p project/{src,tests,docs}
This expands to:
mkdir -p project/src project/tests project/docs
Creates three directories at once. Super convenient.
Subshell
Wrapping commands in parentheses executes them in a subshell. Subshell is a copy of current shell. Changes made in subshell (variables, directory moves) don't affect parent shell.
(cd /tmp && ls)
pwd # still original directory
Moved to /tmp inside parentheses, but when parentheses end, back to original location. Because changes disappear when subshell terminates.
Command substitution also uses subshell:
echo "Today is $(date)"
The $(date) part executes in subshell, and result gets inserted as string.
Real Application: Deployment Automation with Shell Scripts
Theory ends here, now let's write genuinely useful scripts. A deployment script usable in production.
Basic Deployment Script
#!/bin/bash
# deploy.sh - Frontend deployment automation script
set -e # Exit immediately on error
echo "🚀 Starting deployment..."
# 1. Get latest Git code
echo "📦 Fetching latest code..."
git pull origin main
# 2. Install dependencies
echo "📚 Installing dependencies..."
npm ci # Installs exactly based on package-lock.json
# 3. Build
echo "🔨 Building project..."
npm run build
# 4. Backup previous build
echo "💾 Backing up previous build..."
if [ -d "/var/www/html/backup" ]; then
rm -rf /var/www/html/backup
fi
if [ -d "/var/www/html" ]; then
mv /var/www/html /var/www/html/backup
fi
# 5. Deploy new build
echo "📤 Deploying new build..."
cp -r dist /var/www/html
# 6. Restart process (if using pm2)
if command -v pm2 &> /dev/null; then
echo "🔄 Restarting PM2 processes..."
pm2 reload all
fi
echo "✅ Deployment complete!"
This script starts with set -e. This commands to stop immediately on error. Need to stop if something goes wrong during deployment.
And command -v pm2 &> /dev/null checks if pm2 is installed. If not, just skip. Doesn't throw error.
More Production-Ready Deployment Script
#!/bin/bash
# deploy_advanced.sh - Deployment script with rollback on failure
set -e
DEPLOY_DIR="/var/www/html"
BACKUP_DIR="/var/www/html_backup_$(date +%Y%m%d_%H%M%S)"
BUILD_DIR="dist"
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Log functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Rollback function
rollback() {
log_error "Deployment failed! Starting rollback..."
if [ -d "$BACKUP_DIR" ]; then
rm -rf "$DEPLOY_DIR"
mv "$BACKUP_DIR" "$DEPLOY_DIR"
log_info "Rollback complete!"
else
log_error "Cannot find backup directory."
fi
exit 1
}
# Execute rollback function on error
trap rollback ERR
log_info "Starting deployment..."
# Git pull
log_info "Fetching latest code..."
git fetch origin main
git reset --hard origin/main
# Install dependencies
log_info "Installing dependencies..."
npm ci
# Lint check
log_info "Checking code quality..."
npm run lint || {
log_warn "Lint warnings present. Continuing anyway."
}
# Build
log_info "Building project..."
npm run build
# Verify build result
if [ ! -d "$BUILD_DIR" ]; then
log_error "Build directory was not created!"
exit 1
fi
# Backup current deployment
if [ -d "$DEPLOY_DIR" ]; then
log_info "Backing up current deployment..."
cp -r "$DEPLOY_DIR" "$BACKUP_DIR"
fi
# Deploy new build
log_info "Deploying new build..."
rm -rf "$DEPLOY_DIR"
cp -r "$BUILD_DIR" "$DEPLOY_DIR"
# Restart process
if command -v pm2 &> /dev/null; then
log_info "Restarting PM2 processes..."
pm2 reload all
fi
# Health check
log_info "Performing service health check..."
sleep 3
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 || echo "000")
if [ "$HTTP_STATUS" != "200" ]; then
log_error "Health check failed! (HTTP $HTTP_STATUS)"
rollback
fi
# Clean old backups on success (delete backups older than 7 days)
log_info "Cleaning old backups..."
find /var/www -maxdepth 1 -name "html_backup_*" -mtime +7 -exec rm -rf {} \;
log_info "✅ Deployment successful!"
log_info "Backup: $BACKUP_DIR"
This script is production-ready. The key is trap rollback ERR. Automatically executes rollback function when error occurs. So if deployment fails, automatically reverts to previous version.
There's also a health check. After deployment, waits 3 seconds then sends request to server with curl. If doesn't get HTTP 200, considers it failure and rolls back.
Scheduling Regular Tasks with Cron Jobs
Shell scripts combined with Cron can automate regular tasks. Cron is Unix's scheduler. Can set things like "run this script every day at 3 AM."
# Edit cron settings
crontab -e
# Example: Run backup script every day at 3 AM
0 3 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1
# Example: Generate report every Monday at 9 AM
0 9 * * 1 /home/user/scripts/generate_report.sh
Cron syntax goes: minute hour day month weekday command
0 3 * * *: Every day at 3:00 AM0 9 * * 1: Every Monday at 9:00 AM*/5 * * * *: Every 5 minutes
I automate DB backups, log cleanup, statistics report generation with cron. Set it once and it runs on its own.
Creating Shortcuts with Alias
Alias is functionality that gives commands nicknames. Can shorten long complex commands.
# Add to .zshrc or .bashrc
alias ll='ls -alh'
alias gs='git status'
alias gp='git pull'
alias gps='git push'
alias dc='docker-compose'
alias k='kubectl'
# Functions can accept arguments
gco() {
git checkout "$1"
}
mkcd() {
mkdir -p "$1" && cd "$1"
}
# Add confirmation prompts for dangerous commands
alias rm='rm -i'
alias mv='mv -i'
alias cp='cp -i'
I type aliases like ll, gs, gp dozens of times daily. Saves tremendous finger effort.
Wrapping Up
When I first saw shell, I thought "do I need to memorize commands?" But shell isn't about memorizing, it's about understanding. Once I understood how shell parses commands, where it finds executables, how it runs processes, commands became naturally comprehensible.
Once I started writing shell scripts, development productivity shot up. I turned deployment processes I used to type manually into scripts, automated them with cron, created shortcuts with aliases. Repetitive work should be done by computers.
Shell is ultimately a tool. Learn to use shell like learning to use a hammer. Unfamiliar at first, but once familiar, much faster and more accurate than mouse. And cool. The sight of green text flooding a black screen as deployment happens is still cool.