Module 2: Working with Files and Text
2. Creating files and folders: mkdir and touch
Description
By the end of this lesson you will be able to create a complete tree of nested folders with a single command using mkdir -p, generate several files or folders at once with brace expansion and ranges, and create or update files with touch while understanding what it really does underneath — not what it looks like it does. You will also name everything you create with a criterion that is going to save you headaches later, when that name has to travel through a script, a URL, or someone else's terminal.
This is not a tidiness exercise: it is the task almost any real job starts with. When you join a new project or start one from scratch, the first half hour is usually building the folder skeleton the team expects (src/, tests/, docs/) and a few placeholder files — a README, an empty configuration file you are going to fill in later. Doing it folder by folder, click by click, is slow and leaves no record anywhere. Doing it with a reproducible command is the difference between "it took me twenty minutes" and "it took me five seconds, and I can also paste it into a script for the next project." And you are going to understand why automation tools like make check a file's date to decide whether to rebuild something — a touch detail you are either going to use to your advantage or going to have to debug the day something does not update the way you expect.
Connection to the module: the previous lesson mapped out the module and warned you that from here on you really can break something. This is the first lesson where you build: mkdir and touch are the tools you are going to use to raise the entire structure the following lessons will reorganize, read, and search.
mkdir: building a full address before it exists
When a crew raises a building, they cannot rest the third floor on thin air: they need the first and second to already be standing. If someone asks directly "build the third floor" with nothing underneath, the logical answer is to refuse — there is nowhere to rest it. And if someone asks for a floor that is already built, it does not make sense to stop the whole job to complain either: work just continues.
mkdir (make directory) has exactly that first limitation by default: it only creates the last level of the path you ask for, and it requires everything before it to already exist. Check it out:
mkdir orders/2026/july
What to expect (on macOS/BSD):
mkdir: orders/2026/july: No such file or directory
On Linux/GNU you are going to see different wording but the same cause: mkdir: cannot create directory 'orders/2026/july': No such file or directory. In both cases the problem is identical: mkdir tried to create only july, and not even orders/2026 existed yet to rest it on.
The -p (parents) option solves exactly this, and in two ways at once: it automatically builds every missing level along the way, and if the final level already exists, it does not treat that as an error — it just moves on without complaining.
Worked example: the same command, with -p
mkdir -p orders/2026/july
ls -R orders
What to expect:
orders:
2026
orders/2026:
july
orders/2026/july:
All three folders got created in a single command, and the last one (july) is empty because it just got born. Now run exactly the same command again, without having deleted anything:
mkdir -p orders/2026/july
What to expect: nothing. mkdir does not print a single line — silence means success in the terminal, same as you are going to see with other commands in this module. Without -p, that same second attempt would have failed with mkdir: orders/2026/july: File exists. With -p, a directory that already exists simply is not a problem: it is the behavior you want when a script runs more than once and should not break the second time.
Brace expansion: creating several paths in one shot
Imagine you also need three sibling folders for the same project: src, tests, and docs. You could write three separate commands, or even a single one with three full paths — mkdir -p project/src project/tests project/docs works perfectly fine, and for three simple folders it is a perfectly reasonable option. But the terminal (bash and zsh, the two shells you are going to run into in practice) has a way to say "this thing here, but with these variations" without repeating the common prefix: brace expansion.
{a,b,c} is not a mkdir feature — it is a shell mechanism that happens before the command receives its arguments. The shell sees project/{src,tests,docs}, expands it into three complete words (project/src, project/tests, project/docs), and only then hands them to mkdir as if you had typed them yourself, one by one. That is why it works with any command, not just mkdir — it is a feature of the command interpreter, not of the program receiving it.
The real advantage shows up when the structure nests, because that is where repeating the prefix by hand gets tedious and raises the odds of a typo:
Worked example: a new project's skeleton, in one command
mkdir -p expense-tracker/{src/{models,views,controllers},tests,docs}
ls -R expense-tracker
What to expect:
expense-tracker:
docs src tests
expense-tracker/src:
controllers models views
expense-tracker/tests:
expense-tracker/docs:
The inner brace ({models,views,controllers}) resolves first and produces three paths inside src; those three combine with the outer brace along with tests and docs, and -p builds every intermediate level needed — including expense-tracker itself, which did not exist before running the command either. Six folders, three levels deep, one single command.
touch: touching a date, not creating content
Think about a shared lab's sign-in sheet: every time someone walks in, they write the time next to their name. If your name was already on the list, the entry only updates the time — nobody rewrites your history or gives you a new row. But if it is your first time and your name does not appear anywhere yet, writing down the time also means, as a side effect, adding your name to the list for the first time, with no prior activity recorded.
That is literally what touch does: its real job is updating a file's modification (and access) timestamp to right now. If the file already exists, the content is not touched by a single byte — only the date changes. If the file does not exist yet, updating that date requires something to exist to write it on, so touch creates it first — empty, with zero bytes. That is why a lot of people believe "creating files" is touch's main function, when in reality creating is a side effect of its real job: stamping a moment in time.
Worked example: the date changes, the content does not
touch expense-tracker/README.md
ls -l expense-tracker/README.md
What to expect (the username and group are going to vary depending on your machine):
-rw-r--r-- 1 ana staff 0 Jul 21 10:14 expense-tracker/README.md
Zero bytes: the file was born empty, with the permissions your system's umask gives it (you are going to see that in detail in the permissions module). Now, without writing anything inside it, run touch on that same file again, a while later:
touch expense-tracker/README.md
ls -l expense-tracker/README.md
What to expect:
-rw-r--r-- 1 ana staff 0 Jul 21 10:37 expense-tracker/README.md
The size is still 0, the permissions are the same — the only thing that changed is the time column. If that file already had a hundred lines written by someone else, the result would be identical in that sense: touch does not alter a single letter of the content, no matter how much the file weighs. It only moves the timestamp forward.
touch also accepts brace expansion with ranges, useful for creating several placeholder files at once:
touch expense-tracker/tests/test-{01..03}.py
ls expense-tracker/tests
What to expect:
test-01.py test-02.py test-03.py
The leading zero in {01..03} tells the shell to pad every number to the same width (this works in zsh, the default shell on macOS for several years now, and in any modern bash). Without the zero — {1..3} — the result would be 1 2 3, unpadded.
Naming conventions that save you pain later
Everything you just created has a name, and that name lives longer than you think: it is going to end up inside a script, in a URL, in a command someone else runs with a different language configured on their system. A name like Final Report (v2).txt works fine as long as you open it by double-clicking on your own machine — and it turns into a real problem the moment that file has to travel: spaces break commands that do not expect them, accented characters can render wrong depending on the receiving system's encoding, and parentheses are characters the shell interprets specially if you do not protect them.
The convention that avoids almost all of this is called kebab-case: lowercase only, words separated by a single dash, like pieces skewered on a kebab stick — final-report-v2.txt instead of Final Report (v2).txt. The truly portable character set between systems is unaccented letters, numbers, dash, underscore, and dot — it is literally the standard POSIX defines for safe file names on any Unix system. Adopting that discipline when you create the file is free: it costs nothing to type expense-tracker instead of Expense Tracker.
The real problem shows up when you inherit a file with a name like that, created by someone else (or by you, in another era, dragging and dropping in Finder). You are not going to rename it yet — that is exactly what you are going to learn in the next lesson with mv — but you do need to be able to reference it without the shell tearing it apart. Suppose you inherited this folder with a file someone named on Windows:
ls
What to expect:
Final Report.txt
You want to update its modification date, and you type the first thing that comes to mind:
touch Final Report.txt
ls
What to expect:
Final.txt Final Report.txt Report
None of what you wanted happened. The shell split your command on the space without asking, so touch received two separate arguments: Report and Final.txt. Since neither existed, touch did exactly what you already know it does with a name that does not exist: it created them empty. The real file, Final Report.txt, was not even touched — its date is still the original one.
The correct way is to protect the full name with double quotes, so the shell treats it as a single word:
touch "Final Report.txt"
ls -l "Final Report.txt"
What to expect: only Final Report.txt's date changes; no new file shows up. The backslash before each space (Final\ Report.txt) achieves exactly the same thing, character for character — and if in a real terminal you start typing touch Fin and press the Tab key, autocompletion is going to insert the full name already escaped for you, with no need to remember the quotes. That is the most reliable of the three techniques: letting the terminal escape it for you instead of doing it by hand.
Common mistakes
Thinking the last word of a mkdir path can be a file. You run mkdir -p src/utils/helpers.py thinking you are going to end up with a file helpers.py inside src/utils/ — and what you get is a folder literally named helpers.py, not a file. If you then try touch src/utils/helpers.py, the command does not fail, but it does not fix anything either: since something with that name already exists (even if it is a folder), touch just updates its timestamp. Why it happens: mkdir has no way to know that the last word "looked like" a file because it has an extension — for mkdir, every argument it receives is a directory to create, no exceptions, dot in the name or not. How to spot it: ls -l shows a d at the start of the permissions where you expected to see a regular file (-). How to fix it: create with mkdir -p only up to the last level that is really a folder, and use touch separately for the final file — mkdir -p src/utils && touch src/utils/helpers.py.
Leaving a space after the comma inside braces. mkdir -p project/{src, tests, docs} (with a space after each comma) does not create three clean folders — it creates entries with broken names that literally include the braces and the comma. Why it happens: the shell splits words on spaces before trying to expand the braces, so an unquoted space already cut {src, apart from tests, and from docs} into three separate arguments. When the shell looks for the closing brace to expand each one, it no longer finds it in the same word — and instead of failing with an error, it leaves the text as-is, literal. How to spot it: an ls right after shows names with stray braces and commas instead of the clean folders you expected. How to fix it: never leave a space after a comma inside braces — keep it tight, {src,tests,docs}.
Believing that touching an existing file "does nothing" because the content stays the same. It looks like a harmless command because it does not change a single byte of content — but if you have a Makefile or another automation tool that decides whether to rebuild something by comparing modification dates, touching the file can trigger an unnecessary rebuild (or, the other way around, fix a stuck build precisely because the date was not changing). Why it happens: touch updates the timestamp even if the content is bit-for-bit identical, and a lot of build tools compare dates, not content. How to spot it: ls -l before and after shows the date changed even though the byte size is exactly the same. How to fix it: if you really want to leave a file untouched — content and date — do not touch it; if you want to force a rebuild, touch is the right tool, the point is to do it on purpose and not by accident while trying out commands.
Exercises
Exercise 1. You need to build this project skeleton in a single command:
blog-api/
├── src/
│ ├── routes/
│ └── models/
├── tests/
└── docs/
Write the mkdir command with nested brace expansion that creates it all at once, assuming not even blog-api/ exists yet.
See solution
mkdir -p blog-api/{src/{routes,models},tests,docs}
Why it works: the inner brace {routes,models} expands first and produces two paths inside src; the outer brace combines them with tests and docs at the same level as src. -p takes care of creating blog-api/ and blog-api/src/ even though they did not exist before — without -p, the command would fail at the first missing level.
Exercise 2. You run this complete sequence, in this order, without deleting anything in between:
touch archive/report-{1..3}.csv
ls archive
touch archive/report-{1..3}.csv
ls archive
Does the second ls show duplicate files or different ones from the first? What really changed between one run and the other?
See solution
Both ls show exactly the same three names: report-1.csv, report-2.csv, and report-3.csv. There are no duplicates because touch on a file that already exists does not create a new one — it updates its timestamp. The only thing that changed between the first and second run is the modification date of those three files, invisible in a plain ls (you would need ls -l to see it) but real: the second pass left the three files with a more recent timestamp than the first.
Exercise 3. You inherited a folder called Year Report 2025 (with spaces). Without renaming it yet, you want to create a new file inside it, following this lesson's convention: summary-2025.md. Write the full command.
See solution
touch "Year Report 2025/summary-2025.md"
Why it works: the quotes wrap the entire path, including the inherited part with the space, so the shell treats it as a single word instead of splitting it into Year, Report, and 2025/summary-2025.md. The new file you create, on the other hand, already follows kebab-case from the start — it does not inherit the problem, it contains it.
Summary and next step
Before moving on, you should be able to create a tree of nested folders with a single mkdir -p, explain why -p does not fail if something already exists, use brace expansion and ranges to avoid repeating a prefix by hand, and predict when touch is going to create a new file and when it is only going to update its date without touching the content.
You now know how to create full structure in one shot. What you still do not know is how to reorganize it, rename it, or destroy it with judgment — and that is exactly where you step onto ground where a slip has real consequences. cp, mv, and rm are the next lesson's tools, and the first time the terminal stops being harmless.
Resources
- GNU Coreutils Manual — mkdir invocation — official
mkdirreference, including the-p/--parentsoption. - GNU Coreutils Manual — touch invocation — official
touchreference, its default creation behavior, and the-c/--no-createoption. - GNU Bash Manual — Brace Expansion — the exact specification for brace expansion, including
{1..5}-style sequences. - FreeBSD General Commands Manual — mkdir(1) — the BSD version of the command, the basis for
mkdiron macOS. - The Open Group Base Specifications — 3.282 Portable Filename Character Set — the POSIX standard defining the safe character set for file names across systems.