Finding Things

Table of Contents

  1. Two Kinds of Searching
  2. A Small Project to Search
  3. Finding Files by Name: find
  4. Finding Files by Type, Size, and Age
  5. Searching Inside Files: grep -r
  6. Narrowing a Search
  7. Where Is This Command? which and type
  8. Try It Yourself

1. Two Kinds of Searching

When you're looking for something on a computer, you're usually asking one of two questions:

  • "Where is the file called…?" — you know (part of) the name, but not where it is. That's a job for find.
  • "Which file contains…?" — you know some text inside the file, but not which file it's in. That's a job for grep -r.

As a developer you'll ask the second question constantly: where is this function defined? Which files still use the old API URL? Where did that error message come from? Your editor has a search box for this, but on a server, in a container, or in a script, the terminal is all you have.

In chapter 4 you created projects/website with src, tests, and docs folders. Let's put some code in it. Run each block from inside ~/cli-practice:

cd ~/cli-practice
mkdir -p projects/website/{src,tests,docs}
printf '%s\n' \
  "// Home page" \
  "function showHome() {" \
  "  // TODO: add a welcome message" \
  "  return 'Welcome';" \
  "}" > projects/website/src/home.js
printf '%s\n' \
  "// Login page" \
  "function login(user) {" \
  "  // todo: check the password" \
  "  return true;" \
  "}" > projects/website/src/login.js
printf '%s\n' \
  "test('home works', () => {});" \
  "// TODO: test the login page" > projects/website/tests/home.test.js
printf '%s\n' \
  "# Website" \
  "Run npm install, then npm run dev." > projects/website/docs/README.md

You don't need to understand the JavaScript — it's just text to search through. TODO comments are notes developers leave for work that still needs doing, and finding them all is a very real task.

3. Finding Files by Name: find

find walks through a folder, and every folder inside it, and prints every path that matches your conditions. The basic shape is:

find  where-to-look  conditions

Find every JavaScript file under the current folder:

find . -name "*.js"
./projects/website/tests/home.test.js
./projects/website/src/home.js
./projects/website/src/login.js

The . means "start here" (as you learned in chapter 3). You could also start somewhere else, like find ~/Documents -name "*.pdf" or find projects -name "*.md".

Notice the results aren't in alphabetical order — find prints them in the order it comes across them on the disk. Pipe the output into sort if order matters.

Always quote the pattern. Without quotes, the shell expands *.js (chapter 4) before find ever sees it. If the current folder happens to contain app.js, the command becomes find . -name app.js and silently finds only files with that exact name. With quotes, the * reaches find, which matches it against every folder it visits.

-name is case-sensitive: -name "*.JS" won't match home.js. Use -iname to ignore case:

find . -iname "readme*"

4. Finding Files by Type, Size, and Age

find can match much more than names. A few of the most useful conditions:

ConditionMatches
-type fRegular files only
-type dDirectories (folders) only
-size +100MFiles bigger than 100 MB (k, M, G work)
-size -10kFiles smaller than 10 KB
-mtime -7Modified in the last 7 days
-mmin -30Modified in the last 30 minutes

Conditions can be combined, and a file has to match all of them:

find . -type d                        # every folder
find projects -type f -name "*.js"    # files ending in .js (not folders)
find . -type f -mmin -60              # files you changed in the last hour

That last one is surprisingly handy: "I just saved a file somewhere, but where?"

A real-world classic is hunting down what's eating your disk space:

find ~ -type f -size +500M 2> /dev/null

Searching your whole home folder hits some folders you're not allowed to read, and each one prints a Permission denied error. 2> /dev/null from the last chapter throws those errors away, so only the results are left.

Finding and deleting

find has a -delete option that deletes every match. It's powerful and dangerous — there's no confirmation and no trash. If you ever use it, follow the same rule as with rm: run the exact command without -delete first, read the list, and only then add -delete at the end.

find . -name "*.tmp"           # 1. look
find . -name "*.tmp" -delete   # 2. only then delete

5. Searching Inside Files: grep -r

In chapter 6 you used grep on a single file. Add -r (recursive) and give it a folder instead, and it searches every file inside:

grep -r "TODO" projects
projects/website/tests/home.test.js:// TODO: test the login page
projects/website/src/home.js:  // TODO: add a welcome message

Each result shows the file it came from, a colon, and the matching line. Add -n to see the line number too, so you can jump straight to it in your editor:

grep -rn "TODO" projects
projects/website/tests/home.test.js:2:// TODO: test the login page
projects/website/src/home.js:3:  // TODO: add a welcome message

Notice that login.js is missing — its comment says todo in lowercase. Add -i to ignore case:

grep -rni "todo" projects

Now all three show up. -rni is worth memorizing: recursive, line numbers, ignore case — it's the combination you'll use most.

If you only want to know which files match, not every line, use -l:

grep -rli "todo" projects
projects/website/tests/home.test.js
projects/website/src/home.js
projects/website/src/login.js

And -w matches whole words only, so searching for log with -w won't match login:

grep -rnw "login" projects

In a real project, a plain grep -r quickly drowns you in noise. A Node.js project's node_modules folder can easily contain tens of thousands of files, and Git keeps its own history in .git. You almost never want to search those.

Two options keep searches focused:

grep -rn "TODO" --include="*.test.js" projects     # only test files
grep -rn "TODO" --exclude-dir=node_modules .       # skip node_modules

You can repeat --exclude-dir for several folders:

grep -rn "apiUrl" --exclude-dir=node_modules --exclude-dir=.git .

And of course, grep results can go into a pipe like anything else:

grep -rni "todo" projects | wc -l     # how many TODOs are left?

A faster alternative: many developers install ripgrep (command name rg), a modern replacement for grep -r. It's much faster on big projects and skips node_modules, .git, and anything in .gitignore automatically. Everything you learn about grep carries straight over. VS Code's own search box actually uses ripgrep under the hood.

7. Where Is This Command? which and type

Commands are files too. When you type ls or git, the shell runs a program file stored somewhere on the disk. To find out where, use which:

which git
/usr/bin/git

This becomes important the moment something odd happens: you installed a new version of Node.js, but node --version still shows the old one. which node tells you which copy of node the shell is actually running — and very often, it's not the one you expected.

Add -a to see every copy the shell can find, in the order it checks them:

which -a python3

Some commands aren't program files at all. cd, for example, is built into the shell itself — it has to be, because a separate program can't change the shell's own current directory. type tells you exactly what a name is:

type cd
type ls
type git
cd is a shell builtin
ls is an alias for ls -G
git is /usr/bin/git

That middle line may surprise you. On many systems, ls is an alias — a shortcut the shell expands into a longer command, here ls with an option that adds colours (on Ubuntu you'll see ls --color=auto instead). If yours just says ls is /bin/ls, there's no alias set up. You'll make aliases of your own in chapter 9.

If which prints nothing, or type says not found, the shell can't find that command at all — which is exactly what command not found means. How the shell decides where to look is the subject of chapter 9, on PATH.

8. Try It Yourself

Do all of this inside ~/cli-practice.

  1. Use find to list every .txt file in cli-practice, then pipe the result into sort and wc -l to count them.
  2. Find every folder (not file) inside projects.
  3. Find all the files you've modified in the last 30 minutes.
  4. Run find ~ -type f -size +100M 2> /dev/null. Do you recognize the large files it finds?
  5. Use grep -rni to find every TODO in projects. Then use -l to show only the file names, and count them with wc -l.
  6. Search for the word function in projects, but only in files under src. (Hint: you can give grep a more specific starting folder.)
  7. Run which and type on ls, cd, git, and printf. Which ones are programs on disk, and which are built into the shell?

You can now find any file by name and any line of text across a whole project. Next, let's look at the programs you run as processes: how to see them, stop them, and run them in the background.

Buy Me a Coffee

Found this tutorial helpful? Consider buying me a coffee to support more free content!