Finding files and text: find and grep
Search by name, time, owner and content.
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.
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.
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:
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:
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:
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:
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:
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":
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:
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:
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:
-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:
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:
With \;, two rm commands would have run; with +, one. When the list is right, -delete removes the matches:
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:
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:
grep: searching inside files
grep PATTERN FILE prints every line of the file that contains the pattern. Which groups is deploy in?
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:
-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:
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:
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?
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:
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:
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.