Home / Learn Linux / Lesson 9
Finding files and text: find, locate and grep
Find any file by name, size, age or owner, act on what you find, and search inside files.
- Lesson
- 9 of 30
- Time
- About 50 minutes
On this page
- Set up something to search
- Where's that command? which and whereis
- locate: fast, from an index
- find: search the live filesystem
- Doing something with what you find
- grep: search inside files
- Text inside binary files: strings
- Common mistakes
- Practice
- Cheat sheet
Set up something to search
Make a small project with files of different types, sizes and ages. truncate -s makes a file of any size instantly, and touch -d (lesson 5) backdates it:
$ mkdir -p ~/find-lab/{src,docs,logs,tmp} $ cd ~/find-lab $ printf "def main():\n print('hello')\n# TODO: add args\n" > src/app.py $ printf "import os\n# todo: clean up\n" > src/util.py $ printf "# Notes\nDeploy on Friday.\nTODO: write tests\n" > docs/README.md $ printf "ERROR disk full\nINFO started\nerror: timeout\nINFO done\n" > logs/app.log $ truncate -s 20M logs/old.log $ truncate -s 5M tmp/cache.bin $ touch -d "40 days ago" logs/old.log docs/README.md $ mkdir src/Tests && touch src/Tests/test_app.py $ tree . ├── docs │ └── README.md ├── logs │ ├── app.log │ └── old.log ├── src │ ├── Tests │ │ └── test_app.py │ ├── app.py │ └── util.py └── tmp └── cache.bin 6 directories, 7 files
Run the examples from ~/find-lab unless they say otherwise.
Where's that command? which and whereis
You met which in lesson 2. whereis also finds a command's man pages:
$ whereis -b grep grep: /usr/bin/grep $ whereis -m grep grep: /usr/share/man/man1/grep.1.gz /usr/share/info/grep.info.gz
-b shows only the program and -m only the manual pages. Both commands only look in the standard places for programs, so for anything else you need locate or find.
locate: fast, from an index
locate searches a database of every file name on the system, so it answers in milliseconds even on a huge disk. Ubuntu's modern version is called plocate, and it isn't installed by default:
$ sudo apt install -y plocate Installing: plocate ... $ locate sources.list /etc/apt/sources.list /etc/apt/sources.list.d /etc/apt/sources.list.d/ubuntu.sources /etc/cloud/templates/sources.list.ubuntu.deb822.tmpl /usr/share/man/de/man5/sources.list.5.gz /usr/share/man/fr/man5/sources.list.5.gz /usr/share/man/man5/sources.list.5.gz /usr/share/man/nl/man5/sources.list.5.gz /usr/share/man/pt/man5/sources.list.5.gz /var/lib/dpkg/info/python3-pkg-resources.list /var/lib/ubuntu-advantage/apt-esm/etc/apt/sources.list /var/lib/ubuntu-advantage/apt-esm/etc/apt/sources.list.d /var/lib/ubuntu-advantage/apt-esm/etc/apt/sources.list.d/ubuntu-esm-apps.sources
Installing it builds the database for the first time. After that, a timer rebuilds it once a day. That's the catch: locate only knows what was there at the last rebuild. A file you created five minutes ago isn't in it until you run sudo updatedb:
$ touch brand-new.txt $ locate brand-new.txt $ echo $? 1 $ sudo updatedb $ locate brand-new.txt /home/ubuntu/find-lab/brand-new.txt
Useful options:
| Option | Does |
|---|---|
-i | Ignore upper and lower case |
-c | Only count the matches |
-b | Match only the file's own name, not the whole path |
-l 5 | Stop after 5 results |
$ locate -c .conf 619 $ locate -i -l 3 readme /etc/alternatives/README /etc/apparmor.d/local/README /etc/apparmor.d/rsyslog.d/README
Try it
- How many paths on the system contain
python3? - Find files named exactly
os-release, matching only the name.
Answer
$ locate -c python3 15228 $ locate -b '\os-release' /etc/os-release /usr/lib/os-release /usr/share/man/man5/os-release.5.gz
The backslash before the name tells locate -b to match that exact name rather than any name containing it.
find: search the live filesystem
find walks the directories themselves, so it's slower than locate but always current, and it can search by much more than the name. Its shape is:
find where what-to-match what-to-do
By name
$ find . -name "*.py" ./src/util.py ./src/Tests/test_app.py ./src/app.py $ find . -iname "test*" ./src/Tests ./src/Tests/test_app.py
. means start here. -name matches the file name, and -iname ignores case, which is why Tests and test_app.py both matched. Always put quotes around patterns with *. Without them, Bash expands the * itself before find sees it, and you get strange errors or wrong results.
By type
-type f is regular files, -type d directories and -type l links:
$ find . -type d
.
./tmp
./src
./src/Tests
./docs
./logsBy size
-size takes + for bigger than and - for smaller than, with k, M or G:
$ find . -size +1M ./tmp/cache.bin ./logs/old.log $ find . -type f -size -1k ./brand-new.txt ./src/Tests/test_app.py
By time
-mtime -7 means modified in the last 7 days, -mtime +30 more than 30 days ago. -mmin does the same in minutes. -newer file finds anything modified after that file:
$ find . -type f -mtime +30 ./docs/README.md ./logs/old.log $ find . -type f -mmin -60 ./brand-new.txt ./tmp/cache.bin ./src/util.py ./src/Tests/test_app.py ./src/app.py ./logs/app.log
By owner and permissions
-user, -group and -perm match on who owns a file and its permissions (lesson 14). Searching the whole system means lots of directories you can't read, so throw away the errors with 2> /dev/null (lesson 8):
$ find /etc -user ubuntu 2> /dev/null $ find /usr/bin -perm -4000 /usr/bin/chsh /usr/bin/mount /usr/bin/passwd /usr/bin/su ... $ find /var/log -maxdepth 1 -group adm -type f -name "*.log" /var/log/kern.log /var/log/cloud-init.log /var/log/cloud-init-output.log /var/log/apport.log /var/log/auth.log
The first finds nothing, because ubuntu doesn't own anything in /etc. The second lists programs with the SUID bit set (lesson 15 explains why passwd needs it). -maxdepth 1 stops find going into subdirectories.
Combining tests
Several tests in a row must all match. -o means or, ! means not, and escaped parentheses \( \) group them:
$ find . -type f -name "*.py" ! -path "*Tests*" ./src/util.py ./src/app.py $ find . -type f \( -name "*.md" -o -name "*.log" \) ./docs/README.md ./logs/old.log ./logs/app.log
Try it
- Find every directory under
/etcnamedssh. - Find files in
/var/logbigger than 100 KB, without any error messages. - Find files in
~/find-labthat are not.pyfiles.
Answer
$ find /etc -type d -name ssh 2> /dev/null /etc/ssh $ find /var/log -type f -size +100k 2> /dev/null /var/log/kern.log /var/log/syslog /var/log/lastlog /var/log/cloud-init.log /var/log/journal/8a186bd3d15b4c01a715344980aa3926/system.journal /var/log/journal/8a186bd3d15b4c01a715344980aa3926/user-1000.journal /var/log/dpkg.log /var/log/auth.log $ find . -type f ! -name "*.py" ./brand-new.txt ./tmp/cache.bin ./docs/README.md ./logs/old.log ./logs/app.log
Doing something with what you find
By default find prints each match. It can also act on them.
-ls shows details like ls -l:
$ find . -size +1M -ls
534133 0 -rw-rw-r-- 1 ubuntu ubuntu 5242880 Oct 21 11:36 ./tmp/cache.bin
534132 0 -rw-rw-r-- 1 ubuntu ubuntu 20971520 Sep 11 11:36 ./logs/old.log-exec runs any command on each match. {} stands for the file found, and the command ends with \;. With + instead of \;, find passes many files to one run of the command, which is much faster:
$ find . -name "*.py" -exec wc -l {} \; 2 ./src/util.py 0 ./src/Tests/test_app.py 3 ./src/app.py $ find . -name "*.py" -exec wc -l {} + 2 ./src/util.py 0 ./src/Tests/test_app.py 3 ./src/app.py 5 total
-ok is -exec that asks first, and -delete deletes every match. Run any find without -delete first and read the list, because -delete doesn't ask:
$ find tmp -name "*.bin" tmp/cache.bin $ find tmp -name "*.bin" -delete $ ls tmp
That pattern, "find files older than N days and delete them", is how log and backup cleanup scripts work:
$ find /var/backups/myapp -name "*.tar.gz" -mtime +30 -deleteTry it
- Show
ls -lstyle details for every.logfile in~/find-lab. - Count the lines of every
.mdand.pyfile in onewcrun. - Delete every empty file in
~/find-lab(-emptymatches empty files), but look at the list first.
Answer
$ find . -name "*.log" -ls 534132 0 -rw-rw-r-- 1 ubuntu ubuntu 20971520 Sep 11 11:36 ./logs/old.log 534131 4 -rw-rw-r-- 1 ubuntu ubuntu 54 Oct 21 11:36 ./logs/app.log $ find . -type f \( -name "*.md" -o -name "*.py" \) -exec wc -l {} + 2 ./src/util.py 0 ./src/Tests/test_app.py 3 ./src/app.py 3 ./docs/README.md 8 total $ find . -type f -empty ./brand-new.txt ./src/Tests/test_app.py $ find . -type f -empty -delete
grep: search inside files
find looks at names and details. grep looks at what's inside, and prints every line that contains a pattern:
$ grep ERROR logs/app.log ERROR disk full $ grep -i error logs/app.log ERROR disk full error: timeout
These options cover almost everything:
| Option | Does |
|---|---|
-i | Ignore case |
-n | Show line numbers |
-c | Count matching lines |
-v | Show lines that don't match |
-w | Match whole words only |
-r | Search every file under a directory |
-l | Only list the files that match |
-o | Print only the matching part |
-A 2, -B 2, -C 2 | Also show 2 lines after, before, or around each match |
-E | Extended patterns, like a|b for "a or b" |
$ grep -n -i todo src/*.py docs/README.md src/app.py:3:# TODO: add args src/util.py:2:# todo: clean up docs/README.md:3:TODO: write tests $ grep -c INFO logs/app.log 2 $ grep -v INFO logs/app.log ERROR disk full error: timeout $ grep -rl -i todo . ./src/util.py ./src/app.py ./docs/README.md $ grep -E "ERROR|timeout" logs/app.log ERROR disk full error: timeout
-r with -n is how you find where something is defined in a big codebase or config tree, and -l gives you just the file names to open:
$ grep -rn "PermitRootLogin" /etc/ssh 2> /dev/null
/etc/ssh/sshd_config:54:#PermitRootLogin prohibit-password
/etc/ssh/sshd_config:104:# the setting of "PermitRootLogin prohibit-password".grep in pipes
grep reads from a pipe just as well as from a file, so it's the filter you'll put in the middle of everything:
$ ls /usr/bin | grep -c zip 13 $ cat /etc/passwd | grep -w ubuntu ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash $ apt list --installed 2> /dev/null | grep -i "^openssh" openssh-client/resolute-updates,resolute-security,now 1:10.2p1-2ubuntu3.8 arm64 [installed,automatic] openssh-server/resolute-updates,resolute-security,now 1:10.2p1-2ubuntu3.8 arm64 [installed] openssh-sftp-server/resolute-updates,resolute-security,now 1:10.2p1-2ubuntu3.8 arm64 [installed]
A first taste of patterns
grep patterns are regular expressions. Lesson 12 covers them properly, but three characters will already save you time: ^ means the start of a line, $ the end, and . any one character:
$ grep "^INFO" logs/app.log INFO started INFO done $ grep "done$" logs/app.log INFO done $ grep -v "^#" /etc/hosts 127.0.0.1 localhost ::1 ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters ff02::3 ip6-allhosts 127.0.1.1 linux
grep -v "^#" hides comment lines, which is the quickest way to see what a long config file actually sets. Add | grep -v "^$" to hide the blank lines too.
Try it
- Count the lines in
/etc/passwdthat end innologin. - Show the
ERRORline inlogs/app.logwith one line of context after it. - List every file under
/etc/aptthat mentionsubuntu.com.
Answer
$ grep -c "nologin$" /etc/passwd 27 $ grep -A 1 ERROR logs/app.log ERROR disk full INFO started $ grep -rl ubuntu.com /etc/apt /etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg /etc/apt/trusted.gpg.d/ubuntu-keyring-2012-cdimage.gpg /etc/apt/sources.list.d/ubuntu.sources
Text inside binary files: strings
Programs and data files are binary, so cat turns them into garbage. strings pulls out every run of readable text, which is a quick way to see what's in a file you don't recognise, or to find a version number or a hidden path inside a program:
$ strings /usr/bin/hostname ha/lib/ld-linux-aarch64.so.1 __libc_start_main __cxa_finalize abort __errno_location ... $ strings /usr/bin/hostname | grep -i -m 3 usage Usage: hostname [-b] {hostname|-F file} set host name (from file)
grep -m 3 stops after three matches.
Common mistakes
- **Unquoted wildcards in
find.**find . -name *.pylets Bash expand*.pyfirst. Write"*.py". - **
-deletebefore checking.** Run the samefindwithout it, read the list, then add it. - **Thinking
locateis broken.** It doesn't see new files untilsudo updatedb. - **Drowning in
Permission denied.** Add2> /dev/null, or run it withsudoif you need those directories too. - **Forgetting
-r.**grep word /etcfails withIs a directory. Usegrep -r word /etc. - **
grepmatching too much.**grep ubuntualso matchesubuntu-advantage. Use-wfor whole words, or^and$to anchor.
Practice
- Find every
.conffile directly inside/etc(not in subdirectories) that's bigger than 2 KB.
Answer
$ find /etc -maxdepth 1 -name "*.conf" -size +2k
/etc/debconf.conf
/etc/gai.conf
/etc/overlayroot.conf
/etc/adduser.conf
/etc/sensors3.conf
/etc/hdparm.conf
/etc/sudo.conf
/etc/ca-certificates.conf
/etc/sudo_logsrvd.conf- Find the files in
/var/logmodified in the last 10 minutes.
Answer
$ find /var/log -type f -mmin -10 2> /dev/null
/var/log/wtmp.db
/var/log/kern.log
/var/log/syslog
/var/log/lastlog
/var/log/sysstat/sa21
/var/log/journal/8a186bd3d15b4c01a715344980aa3926/system.journal
/var/log/journal/8a186bd3d15b4c01a715344980aa3926/user-1000.journal
/var/log/apt/history.log
/var/log/apt/eipp.log.xz
/var/log/apt/term.log
/var/log/alternatives.log
/var/log/dpkg.log
/var/log/wtmp
/var/log/auth.log- In
~/find-lab, list the files containingtodoin any case, with how many matches each has.
Answer
$ grep -ric todo .
./src/util.py:1
./src/app.py:1
./docs/README.md:1
./logs/old.log:0
./logs/app.log:0Files with 0 matches are listed too. Add | grep -v ":0$" to hide them.
- Show
/etc/ssh/sshd_configwithout comments and blank lines.
Answer
$ grep -v "^#" /etc/ssh/sshd_config | grep -v "^$"
Include /etc/ssh/sshd_config.d/*.conf
KbdInteractiveAuthentication no
UsePAM yes
X11Forwarding yes
PrintMotd no
AcceptEnv LANG LC_* COLORTERM NO_COLOR
Subsystem sftp /usr/lib/openssh/sftp-server- Find every
.pyfile in~/find-laband count the lines that containimport, in one command.
Answer
$ find . -name "*.py" -exec grep -c import {} +
./src/util.py:1
./src/app.py:0- Use
locateto count how many.pngfiles there are on the system, ignoring case.
Answer
$ locate -i -c "*.png"
51- Create
old-backup.tar.gzin~/find-labdated 60 days ago, then find and delete every.tar.gzfile there older than 30 days, checking the list first.
Answer
$ touch -d "60 days ago" old-backup.tar.gz $ find . -name "*.tar.gz" -mtime +30 ./old-backup.tar.gz $ find . -name "*.tar.gz" -mtime +30 -delete $ ls *.tar.gz ls: cannot access '*.tar.gz': No such file or directory
Cheat sheet
| Command | What it does |
|---|---|
which cmd, whereis cmd | Where a program (and its manual) lives |
locate name, locate -i -c name | Search the file name index (ignore case, count) |
sudo updatedb | Refresh the locate index |
find . -name "*.py" | By name (-iname ignores case) |
find . -type f / d / l | Files / directories / links |
find . -size +10M | Bigger than 10 MB |
find . -mtime -7, -mmin -60 | Changed in the last 7 days, last hour |
find . -user ubuntu, -perm -4000 | By owner, by permission bits |
-maxdepth 1 | Don't go into subdirectories |
! -name x, \( a -o b \) | Not, or |
-ls, -delete | Show details, delete (check first) |
-exec cmd {} \;, -exec cmd {} + | Run cmd on each, or on many at once |
grep word file | Lines containing word |
grep -i -n -c -v -w | Ignore case, numbers, count, invert, whole word |
grep -rl word dir | Files under dir containing word |
grep -A 2 -B 2 word | With context |
grep -E "a|b" | a or b |
grep -v "^#" file | Hide comment lines |
strings file | Readable text in a binary file |