Finding files and text: find and grep

Search by name, time, owner and content.

Beginner14 min · lesson 7 of 29

Two questions come up on every server you look after: where is the file I need, and which file says this? find answers the first by walking the directory tree and testing every file against conditions such as name, age, size, owner and permissions, and it can run a command on each match. grep answers the second by reading files and printing the lines that match a pattern. This lesson covers both, the errors they print when they reach something you may not read, and how to act on a list of files without deleting the wrong ones.

Which search tool
What do you need to find?
a file by name, age, size, owner
find
walks the tree now; always current
text inside files
grep, grep -r
prints matching lines, or file names with -l
text in the files find picked
find ... -exec grep
find chooses the files, grep reads them
a name, instantly, from an index
plocate (not installed)
as fresh as its last daily update

find: searching by name and type

The examples use a practice directory, ~/search, with a few files the lesson comes back to: two documents, one with a space in its name, a small allow list of addresses, and report.txt, created with sudo so that root owns it, the way a file is left behind when a command runs with sudo in the wrong directory.

deploy@web01 · Ubuntu 26.04 LTS
$ mkdir -p ~/search/docs touch ~/search/docs/"q1 report.txt" ~/search/docs/q2.txt printf "192.0.2.1 gateway\n192.0.2.10 backup\n192.0.2.100 monitoring\n" > ~/search/allowlist.txt sudo touch ~/search/report.txt

find takes one or more starting directories and then tests. -name compares the file name with a pattern in which * stands for any characters. Put the pattern in quotes, so it reaches find as written:

deploy@web01 · Ubuntu 26.04 LTS
$ find /etc/ssh -name "*.conf"
/etc/ssh/sshd_config.d/60-cloudimg-settings.conf /etc/ssh/sshd_config.d/10-acceptenv-colorterm.conf /etc/ssh/ssh_config.d/20-systemd-ssh-proxy.conf

Without the quotes, the shell tries to expand *.conf itself, against the current directory, before find starts (the lesson on quoting and wildcards explains the rule). If the current directory happens to contain a .conf file, find receives that name instead of the pattern and silently finds nothing:

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/search touch local.conf find /etc/ssh -name *.conf

The shell turned the command into find /etc/ssh -name local.conf. No error, no output, exit status 0, and a wrong conclusion if you trust it. Quote every pattern you give to find.

Searching a bigger tree as an ordinary user, find meets directories it may not open and says so on stderr, mixed in with the results:

deploy@web01 · Ubuntu 26.04 LTS
$ find /etc -name "sshd_config*"
find: ‘/etc/credstore.encrypted’: Permission denied find: ‘/etc/cryptsetup-keys.d’: Permission denied find: ‘/etc/ssl/private’: Permission denied find: ‘/etc/lvm/archive’: Permission denied find: ‘/etc/lvm/backup’: Permission denied /etc/ssh/sshd_config.d /etc/ssh/sshd_config find: ‘/etc/multipath’: Permission denied find: ‘/etc/credstore’: Permission denied find: ‘/etc/polkit-1/rules.d’: Permission denied find: ‘/etc/sudoers.d’: Permission denied find: ‘/etc/audit’: Permission denied

Each Permission denied names a directory find could not look inside; the two other lines are the results. The exit status is 1 because some of the tree could not be searched, so a result from an ordinary user can be incomplete. Here one of the matches is a directory, sshd_config.d. -type f keeps only regular files (-type d keeps directories and -type l symbolic links), and 2>/dev/null from the previous lesson hides the errors once you know they are only noise:

deploy@web01 · Ubuntu 26.04 LTS
$ find /etc -name "sshd_config*" -type f 2>/dev/null
/etc/ssh/sshd_config

locate (the plocate package) answers name searches instantly from a database instead of walking the tree. Neither Ubuntu Server 26.04 nor the Rocky Linux 10 lab image installs it. After sudo apt install plocate, a timer, plocate-updatedb.timer, rebuilds the database once a day, so files created since the last run are missing from its answers.

Searching by age and size

After an incident, or when a disk fills up, the questions are about time and size. To have files of known ages, the practice directory gets four log files whose modification times are set with touch -d, which accepts dates such as 2 hours ago or -84 hours:

deploy@web01 · Ubuntu 26.04 LTS
$ mkdir -p ~/search/logs && cd ~/search/logs touch -d "2 hours ago" app.log touch -d "-84 hours" app.log.1 touch -d "-180 hours" app.log.2 touch -d "-252 hours" app.log.3 ls -l
total 0 -rw-rw-r-- 1 deploy deploy 0 Sep 27 06:07 app.log -rw-rw-r-- 1 deploy deploy 0 Sep 23 20:07 app.log.1 -rw-rw-r-- 1 deploy deploy 0 Sep 19 20:07 app.log.2 -rw-rw-r-- 1 deploy deploy 0 Sep 16 20:07 app.log.3

app.log is two hours old and the others are 3.5, 7.5 and 10.5 days old. -mtime +7 looks like "modified more than seven days ago":

deploy@web01 · Ubuntu 26.04 LTS
$ find ~/search/logs -mtime +7
/home/deploy/search/logs/app.log.3
$ find ~/search/logs -mtime +6
/home/deploy/search/logs/app.log.2 /home/deploy/search/logs/app.log.3

app.log.2, at seven and a half days, is missing from the first answer. find counts a file's age in whole days and drops the fraction, so that file counts as 7, and +7 means more than 7: at least eight full days. To catch everything older than seven days, ask for +6. -mtime -1 means less than a day, and -mmin does the same in minutes, which is how you ask what changed recently:

deploy@web01 · Ubuntu 26.04 LTS
$ find ~/search/logs -mmin -180
/home/deploy/search/logs /home/deploy/search/logs/app.log

The directory itself is in the answer because its modification time changed when the files were created in it.

-size +100M matches files larger than 100 MiB (a mebibyte, 1,048,576 bytes; k and G work too). Searching from / needs sudo to see everything, and -xdev, which keeps find on the filesystem it started on:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo find / -xdev -type f -size +100M
/var/cache/apt/archives/linux-modules-7.0.0-34-generic_7.0.0-34.34_arm64.deb /usr/lib/aarch64-linux-gnu/libLLVM.so.21.1
$ sudo find / -type f -size +100M 2>/dev/null
/var/cache/apt/archives/linux-modules-7.0.0-34-generic_7.0.0-34.34_arm64.deb /usr/lib/aarch64-linux-gnu/libLLVM.so.21.1 /proc/kcore

The first command lists a kernel package kept in apt's download cache and a large library. Without -xdev, find also walks /proc, /sys, /run and the tmpfs /tmp, which are not on the disk at all, and reports /proc/kcore, a view of the kernel's memory whose size is a figure of hundreds of terabytes that uses no disk space. When you are looking for what fills a disk, add -xdev and start at the mount point of the full filesystem, which df names (the storage lesson continues from there). Remember what -xdev leaves out: servers installed from an installer image often put /var, /home or /boot on filesystems of their own, and a search from / with -xdev does not look inside them. For a sweep of the whole machine, run it once for each disk filesystem that findmnt -rno TARGET -t ext4,xfs,btrfs,vfat lists.

Searching by owner and permission

-user NAME matches files owned by that account, and ! (or -not) negates the next test. Files in your home directory that you do not own are usually left behind by a command run with sudo in the wrong place, and you cannot edit or delete them:

deploy@web01 · Ubuntu 26.04 LTS
$ find ~ ! -user deploy
/home/deploy/search/report.txt

-perm tests permission bits. -perm -o+w matches files that other users, meaning everyone, may write; the leading - means "at least these bits are set, whatever the others are", and without it find would want the permissions to be exactly that. A world-writable file that a privileged job runs is a way for any local user to get their commands run with those privileges, so this is a sweep worth running on a server. Run it with sudo, so that no directory is skipped. On the lab server it printed nothing, so for this example a root-owned script was left in /var/tmp with that mistake:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo find / -xdev -type f -perm -o+w
/var/tmp/cmd-search-backup.sh
$ ls -l /var/tmp/cmd-search-backup.sh
-rwxrwxrwx 1 root root 45 Sep 27 08:07 /var/tmp/cmd-search-backup.sh

The three rwx groups in ls -l are for the owner, the group and everyone else, and the w in the last group is the problem. The permissions lesson explains the notation and the fix, and the lesson on SUID uses the same -perm test to list programs that run with their owner's privileges.

Acting on what find finds

-exec COMMAND {} \; runs a command once for every match, with {} replaced by the file's path. Ending it with + instead passes as many paths as fit to one run of the command, which is much faster on long lists. Putting echo in front of a command shows what would run without running it:

deploy@web01 · Ubuntu 26.04 LTS
$ find ~/search/logs -mtime +6 -exec echo rm {} \;
rm /home/deploy/search/logs/app.log.2 rm /home/deploy/search/logs/app.log.3
$ find ~/search/logs -mtime +6 -exec echo rm {} +
rm /home/deploy/search/logs/app.log.2 /home/deploy/search/logs/app.log.3

With \;, two rm commands would have run; with +, one. When the list is right, -delete removes the matches:

deploy@web01 · Ubuntu 26.04 LTS
$ find ~/search/logs -mtime +6 -delete ls ~/search/logs
app.log app.log.1
Print the list before you delete
Run the find command without -delete first and read what it prints. find evaluates its expression left to right, so -delete must come last: find DIR -delete -name "*.log" deletes everything under DIR, including DIR, before it ever looks at the name.

xargs is the other way to act on a list: it reads names from its stdin and runs a command with them as arguments. By default it splits its input at spaces and newlines, which breaks names that contain a space. The practice directory holds docs/q1 report.txt and docs/q2.txt:

deploy@web01 · Ubuntu 26.04 LTS
$ find ~/search/docs -name "*.txt" | xargs ls -l
ls: cannot access '/home/deploy/search/docs/q1': No such file or directory ls: cannot access 'report.txt': No such file or directory -rw-rw-r-- 1 deploy deploy 0 Sep 27 08:07 /home/deploy/search/docs/q2.txt
$ find ~/search/docs -name "*.txt" -print0 | xargs -0 ls -l
-rw-rw-r-- 1 deploy deploy 0 Sep 27 08:07 /home/deploy/search/docs/q1 report.txt -rw-rw-r-- 1 deploy deploy 0 Sep 27 08:07 /home/deploy/search/docs/q2.txt

In the first command q1 report.txt arrived at ls as two names, .../q1 and report.txt; with rm instead of ls, a file called report.txt in your current directory would have been deleted. -print0 makes find end each name with a null byte, the one byte a file name cannot contain, and xargs -0 splits only there. Add -r as well, so that xargs runs nothing when find found nothing; without it GNU xargs runs the command once with no arguments.

-exec also connects find to grep: find picks the files, grep reads them, and grep prints the file name and line number of each match:

deploy@web01 · Ubuntu 26.04 LTS
$ find /etc/ssh -name "*.conf" -exec grep -n Authentication {} +
/etc/ssh/sshd_config.d/60-cloudimg-settings.conf:1:PasswordAuthentication no

grep: searching inside files

grep PATTERN FILE prints every line of the file that contains the pattern. Which groups is deploy in?

deploy@web01 · Ubuntu 26.04 LTS
$ grep deploy /etc/group
adm:x:4:syslog,deploy sudo:x:27:deploy deploy:x:1001:

Each line of /etc/group is a group, and the members come after the last colon: deploy is in adm and sudo, and has its own group. Case matters unless you add -i, and -n prints line numbers. A pattern also matches inside longer words, which -w (whole word) prevents:

deploy@web01 · Ubuntu 26.04 LTS
$ grep -n -i port /etc/ssh/sshd_config
13:# For options that accept multiple values, like 'Port', subsequent definitions 27:# configuration must be re-generated after changing Port, AddressFamily, or 35:#Port 22 112:#GatewayPorts no
$ grep -n -w Port /etc/ssh/sshd_config
13:# For options that accept multiple values, like 'Port', subsequent definitions 27:# configuration must be re-generated after changing Port, AddressFamily, or 35:#Port 22

-i found GatewayPorts too; -w did not, because the letter after Port makes it part of a longer word. Every match here is a comment, which brings up the most useful grep for configuration files: show only the lines that are in force. -v inverts the match, and -E enables extended patterns in which | means "or" and parentheses group:

deploy@web01 · Ubuntu 26.04 LTS
$ grep -Ev '^\s*(#|$)' /etc/ssh/sshd_config
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

Read the pattern piece by piece: ^ is the start of the line, \s* any amount of white space, and (#|$) either a # or the end of the line ($). Lines that are comments or empty match, -v drops them, and seven active lines remain out of more than a hundred. Single quotes keep the shell away from the pattern. When you need the lines around a match, -A, -B and -C add lines after, before or on both sides:

deploy@web01 · Ubuntu 26.04 LTS
$ grep -n -B4 -A3 "take effect" /etc/ssh/sshd_config
26-# When systemd socket activation is used (the default), the socket 27-# configuration must be re-generated after changing Port, AddressFamily, or 28-# ListenAddress. 29-# 30:# For changes to take effect, run: 31-# 32-# systemctl daemon-reload 33-# systemctl restart ssh.socket

The context explains the match: on Ubuntu the listening socket (the network port that waits for connections) belongs to ssh.socket, a systemd unit that starts the SSH server when a connection arrives, so changing the port takes these two extra commands (the systemd lesson explains units).

-r searches every file under a directory, and -l prints only the names of files that match. Which files in /etc contain this server's name?

deploy@web01 · Ubuntu 26.04 LTS
$ grep -rl web01 /etc
grep: /etc/.pwd.lock: Permission denied grep: /etc/iscsi/initiatorname.iscsi: Permission denied … grep: /etc/ssl/private: Permission denied … /etc/hostname grep: /etc/ufw/user.rules: Permission denied … /etc/hosts … grep: /etc/shadow: Permission denied …

As an ordinary user the answer is buried in errors, and the exit status is 2 although two files matched: grep reports an error whenever it could not read something, so the answer may be incomplete. Here the errors are what you would hope to see, since private keys, password hashes and firewall rules are not readable by ordinary users. With sudo the search is complete:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo grep -rl web01 /etc
/etc/hostname /etc/hosts

grep patterns are regular expressions, in which a dot means any character, so an address used as a pattern matches more than itself. The allow list from the start of the lesson shows it:

deploy@web01 · Ubuntu 26.04 LTS
$ grep 192.0.2.1 ~/search/allowlist.txt
192.0.2.1 gateway 192.0.2.10 backup 192.0.2.100 monitoring
$ grep -Fw 192.0.2.1 ~/search/allowlist.txt
192.0.2.1 gateway

192.0.2.1 matched two other addresses that start with the same digits (and would also match text such as 192x0y2z1). -F treats the pattern as fixed text with no special characters, and -w requires it to stand as a whole word.

Try this

In a practice directory, make three files aged two, five and nine days with touch -d "-48 hours" two.log, touch -d "-120 hours" five.log and touch -d "-216 hours" nine.log. Predict which names find . -name "*.log" -mtime +4 prints, then check (five and nine). Add -exec echo rm {} + to see the single rm that would run, then replace it with -delete and confirm with ls that only two.log is left. Finally run grep -rn PasswordAuthentication /etc/ssh/ and work out which line is in force, which lines are comments, and why three files could not be read.

Takeaway

Quote find patterns, add -xdev when you search from /, and print the list before you add -delete or rm. Hand file names to other commands with -exec ... + or -print0 | xargs -0 -r, and treat Permission denied from find or grep as a sign that the answer may be incomplete.

Quick check
01You run find /srv/reports -name "*.tmp" | xargs rm. rm prints "cannot remove '/srv/reports/weekly': No such file or directory", and a file named summary.tmp has vanished from your current directory. What happened?
Incorrect — find searches only the starting points you give it. The current directory was involved only because of what xargs did with a name.
Incorrect — rm never guesses. It removed exactly the path it received, which was the wrong one.
Correct — The second half, summary.tmp, is a relative name, so rm resolved it in the current directory.
Incorrect — xargs may start early, but it passes only complete lines. The damage came from splitting a name at a space.
02A nightly job runs find /var/backups/app -mtime +7 -delete. A backup taken seven days and twenty hours ago is still there in the morning. Why?
Correct — The age is counted in whole days with the remainder dropped, so +7 means at least eight full days.
Incorrect — Counting from midnight happens only with -daystart. By default find measures 24-hour periods back from now.
Incorrect — The access time is -atime. -mtime is the modification time, which reading the file does not change.
Incorrect — A number without a sign means exactly; +7 already means more than seven. find has no range syntax.
03Looking for what filled the root filesystem, you run sudo find / -type f -size +100M and one hit is /proc/kcore, apparently hundreds of terabytes. What should you change?
Incorrect — /proc/kcore is not a file on disk but a view of kernel memory. It uses no space, and it cannot be removed.
Incorrect — 2>/dev/null only hides error messages. /proc/kcore is a result on stdout, so it would still be listed.
Incorrect — Without sudo you miss real files you may not read, and /proc is still walked. Privilege is not the issue.
Correct — -xdev keeps find on the filesystem where it started, so /proc, /sys and tmpfs mounts are left out.

Related