Putting It All Together
Table of Contents
- What We're Building
- What a Shell Script Is
- Writing the Script
- Reading It Line by Line
- Running and Testing It
- Why the Script Can't
cdfor You - Installing It as Your Own Command
- The Whole Series in One Page
- Where to Go Next
1. What We're Building
Every time you start a new project, you do the same few things: make a
folder, add src, tests, and docs inside it, write a README, add a
.gitignore, create an empty .env and lock it down. That's maybe ten
commands — and exactly the kind of repetitive work the command line is
meant to automate.
In this chapter you'll write a script called new-project that does all
of it in one go:
new-project my-app
Creating project: my-app
Done! Files created:
my-app
my-app/.env
my-app/.gitignore
my-app/README.md
my-app/docs
my-app/src
my-app/src/index.js
my-app/tests
Almost every line of it uses something from an earlier chapter, and by
the end, it'll be a real command you can run from anywhere — just like
ls or git.
2. What a Shell Script Is
A shell script is a text file containing commands — the same
commands you've been typing — that the shell runs from top to bottom.
You made a tiny one, hello.sh, in chapter 10.
Anything you can type at the prompt, you can put in a script. A script just adds two things that are less useful when you're typing by hand:
- Arguments: the script can receive values when you run it, like
my-appabove. Inside the script, the first argument is available as$1, the second as$2, and so on. - Decisions: the script can check something — "was a name given? does that folder already exist?" — and react to it.
Full shell programming (loops, functions, and the rest) is a big topic
of its own, and this series deliberately stops short of it. One small
if is all we need.
3. Writing the Script
Go to your practice folder and open a new file in nano:
cd ~/cli-practice
nano new-project.sh
Type or paste in the following, then save with Ctrl + O, Enter, and
exit with Ctrl + X:
#!/bin/bash
# new-project.sh — create a starter folder for a new project.
# Usage: new-project.sh <project-name>
set -e
name="$1"
if [ -z "$name" ]; then
echo "Usage: new-project.sh <project-name>" >&2
exit 1
fi
if [ -e "$name" ]; then
echo "Error: '$name' already exists." >&2
exit 1
fi
echo "Creating project: $name"
mkdir -p "$name"/{src,tests,docs}
echo "# $name" > "$name/README.md"
echo "Created on $(date +%Y-%m-%d) by $USER." >> "$name/README.md"
printf '%s\n' "node_modules/" ".env" ".DS_Store" > "$name/.gitignore"
touch "$name/src/index.js" "$name/.env"
chmod 600 "$name/.env"
echo "Done! Files created:"
find "$name" | sort
4. Reading It Line by Line
Before running a script — even your own — read it. Here's what each part does, and where you learned it.
#!/bin/bash
The shebang: #! followed by the path of the program that should
run this file. When you run ./new-project.sh, the operating system
reads this first line and hands the file to /bin/bash. That's why the
script works the same even if your terminal uses zsh. You'll also see
#!/usr/bin/env bash, which looks bash up on your PATH (chapter
9) instead of using a fixed location.
# new-project.sh — create a starter folder for a new project.
Comments (chapter 2). Everything after # is ignored. A good script
says at the top what it does and how to use it.
set -e
"Stop the script as soon as any command fails." Without it, if mkdir
failed, the script would carry on and run every other command anyway.
set -e uses exit codes (chapter 8): any command that exits with
something other than 0 stops the script.
name="$1"
A variable (chapter 9) holding the first argument. When you run
new-project.sh my-app, $1 is my-app. Giving it a readable name
makes the rest of the script easier to follow.
if [ -z "$name" ]; then
echo "Usage: new-project.sh <project-name>" >&2
exit 1
fi
The one decision in this script. [ -z "$name" ] tests whether name
is empty (-z means "zero length"). If it is — you forgot the
argument — the script prints how to use it and stops. if ... then ... fi marks the start and end of the block (fi is if backwards).
Two details come from earlier chapters. >&2 sends the message to
stderr (chapter 6), because it's an error, not normal output. And
exit 1 ends the script with a non-zero exit code, so anything
that runs this script can tell it failed.
if [ -e "$name" ]; then
The same idea: -e tests whether something with that name already
exists. Without this check, running the script twice would quietly
add files into a project you'd already started.
mkdir -p "$name"/{src,tests,docs}
mkdir -p and brace expansion (chapter 4). The quotes around
"$name" matter: if someone runs new-project.sh "my app", the space
is kept as part of one name instead of splitting it into two. Quoting
variables is the most important habit in shell scripting.
echo "# $name" > "$name/README.md"
echo "Created on $(date +%Y-%m-%d) by $USER." >> "$name/README.md"
Redirection (chapter 6): > creates the README with a heading, and
>> adds a second line. $USER is an environment variable (chapter
9). And $(...) is new: it's command substitution — the shell runs
the command inside the brackets and drops its output into the line. Here
date +%Y-%m-%d prints today's date, like 2026-10-01.
printf '%s\n' "node_modules/" ".env" ".DS_Store" > "$name/.gitignore"
The printf trick from chapter 6, writing three lines into
.gitignore so Git won't track dependencies, secrets, or macOS clutter.
touch "$name/src/index.js" "$name/.env"
chmod 600 "$name/.env"
Empty files (chapter 4), and permissions (chapter 10): .env
will hold secrets, so only you can read it.
find "$name" | sort
find (chapter 7) lists everything created, piped into sort
(chapter 6) so the order is predictable.
5. Running and Testing It
The file isn't executable yet (chapter 10), so add x first:
chmod +x new-project.sh
Now test it properly — not just the happy path, but the mistakes too.
No argument:
./new-project.sh
echo $?
Usage: new-project.sh <project-name>
1
A real project:
./new-project.sh my-app
You should see the list of files from section 1. Check the results with the tools you know:
cat my-app/README.md
ls -la my-app
# my-app
Created on 2026-10-01 by dara.
In the ls -la output, notice the hidden .env and .gitignore
(chapter 3), and that .env shows -rw-------.
The same name again:
./new-project.sh my-app
echo $?
Error: 'my-app' already exists.
1
Your script now handles the three cases that matter. Testing the failure cases, not just the success, is a habit worth taking into every piece of code you write.
6. Why the Script Can't cd for You
It would be nice if the script finished by moving you into the new
folder. Try adding cd "$name" as its last line and running it on a new
name — then run pwd. You're still in ~/cli-practice.
This isn't a bug. Remember chapter 8: running a script starts a child
process — a separate copy of Bash. The cd happens inside that child,
which changes its current directory, then exits. Your own shell, the
parent, was never touched. A child process can't change its parent's
current directory, variables, or anything else (chapter 9).
That's also why cd itself has to be built into the shell (chapter 7).
The simple fix is to chain it yourself with && (chapter 8):
./new-project.sh my-api && cd my-api
The cd only runs if the script succeeded. (Remove the cd line from
the script if you added it.)
7. Installing It as Your Own Command
Right now you have to type ./new-project.sh, and only from inside
~/cli-practice. Let's make it a real command that works from anywhere.
This is exactly how command-line tools are installed — they're
executable files in a folder on your PATH.
Make a bin folder in your home folder (bin is short for binaries,
the traditional name for program folders) and move the script into it,
dropping the .sh so the command looks like any other:
mkdir -p ~/bin
mv new-project.sh ~/bin/new-project
Then add that folder to your PATH, permanently, in your startup file (chapter 9):
nano ~/.zshrc # or: nano ~/.bashrc
Add this line at the end, then save and exit:
export PATH="$HOME/bin:$PATH"
Load the change and check that the shell can find your command:
source ~/.zshrc # or: source ~/.bashrc
which new-project
/Users/dara/bin/new-project
Now it works from anywhere:
cd ~/cli-practice/projects
new-project portfolio && cd portfolio
You've written a program, given it permission to run, put it on your
PATH, and run it like any other command. Every command you've used in
this series — ls, git, node — got onto your computer the same way.
You can also update the usage message inside the script to say
new-project instead of new-project.sh, now that that's its name.
8. The Whole Series in One Page
| Chapter | The one idea to remember |
|---|---|
| 1. What Is the Command Line? | The terminal is the window, the shell is the program that understands you. Read before you press Enter |
| 2. Your First Commands | command -options arguments, separated by spaces. Use Tab, ↑, Ctrl + C, --help, and man |
| 3. Moving Around | pwd, ls, cd. Paths are absolute (from /) or relative (from here). ~, ., .. |
| 4. Working with Files and Folders | mkdir, touch, cp, mv, rm. Wildcards are expanded by the shell. rm has no undo |
| 5. Reading and Editing Files | cat for short files, less for long ones, tail -f for logs, nano to edit, :q! to escape vim |
| 6. Pipes and Redirection | > replaces, >> appends, 2> catches errors, | connects small tools into pipelines |
| 7. Finding Things | find searches names, grep -rni searches contents, which and type find commands |
| 8. Processes from the Terminal | ps, top, kill, &, jobs, fg. Exit code 0 means success. lsof -i :3000 finds who has a port |
| 9. Environment Variables and PATH | export passes variables to child processes. PATH is where commands are found. Open a new terminal |
10. Permissions and sudo | rwx for owner, group, others. Scripts need ./ and +x. Use sudo only when you know why |
| 11. Putting It All Together | A script is just commands in a file. Quote your variables, test the failures, put it on PATH |
9. Where to Go Next
The terminal is no longer a mystery — it's a tool you can use. Here's how it connects to what comes next on the Start Here roadmap:
- Git & GitHub for Beginners — the next step, and you'll drive it
entirely from the terminal:
git init,git add,git commit, and that vim window you now know how to escape. - How the Web Works and Networking for Developers — what's
actually behind that port 3000, and tools like
curlthat talk to servers from the command line. - Docker CLI Tutorial — every
Docker command builds on this series:
docker psanddocker logs -farepsandtail -ffor containers, and you'll spend time in shells inside them. - Hardening a Linux Server — permissions, users, and
sudofor real, on a server with no screen at all.
Keep ~/cli-practice around and come back to it whenever you want to
try something safely. And the next time a tutorial says "just run this
in your terminal", you'll know exactly what you're typing — and what it
will do.
Buy Me a Coffee
Found this tutorial helpful? Consider buying me a coffee to support more free content!