Find files and search text: find and grep recipes

Find files by name, size and age, search inside them, and delete old files without surprises.

Part 7 of 12 in Linux basics for self-hosters. Last updated

In short: find searches file names and properties: find . -type f -name '*.log' -mtime +7 lists log files older than a week, and find . -type f -size +200M lists files over 200 MiB. grep searches content: grep -rn 'error' /etc prints every matching line with its file and line number. GNU find takes plain numbers for -mtime (+7, not +7d). Always run a find with -print before you add -delete.

find: files by name, type, size and age

find walks a directory tree and prints every path that matches all the tests you give it. The first argument is where to start; . is the current directory. Every recipe below is for GNU find, the one on every mainstream Linux distribution; the reference is find(1).

find . -name '*.log'                    # by name; quote the pattern
find . -iname '*.jpg'                   # name, ignoring case
find /etc -type d -name 'systemd'       # directories only (f = files, l = links)
find . -type f -mtime +7                # changed more than 7 days ago
find . -type f -mtime -1                # changed in the last 24 hours
find . -type f -mmin -30                # changed in the last 30 minutes
find . -type f -newermt '2026-09-01'    # changed after a date
find . -type f -size +200M              # bigger than 200 MiB
find . -type d -empty                   # empty directories
find . -type f -user alice              # owned by alice
find . -type f -perm -o+w               # writable by everyone: worth a look
find / -xdev -type f -size +1G 2>/dev/null   # stay on one file system, hide "Permission denied"
  • Age is counted in whole days with the fraction dropped: +7 means "more than 7 full days", so in practice 8 days or older. -7 means less than 7.
  • Size units are k, M and G, in steps of 1024. c means bytes. Stick to + with sizes: find rounds up to the unit, so -size -1M only matches empty files.
  • Quote patterns such as '*.log'. Without quotes the shell expands the star first, and find gets a list of names from the current directory instead of a pattern.

The examples on this page ran in a small practice tree: two config files, three logs (the first one dated 40 days back with touch -d) and a 120 MiB file. Build the same tree to follow along, then try the tests:

$ find . -type f | sort
./etc/app/app.conf
./etc/db.conf
./logs/app-1.log
./logs/app-2.log
./logs/app-3.log
./logs/big.img

$ find . -name '*.conf'
./etc/app/app.conf
./etc/db.conf

$ find logs -type f -mtime +30
logs/app-1.log

$ find . -size +100M -exec ls -lh {} +
-rw-r--r-- 1 user user 120M Sep 28 06:30 ./logs/big.img

Real output on openSUSE Tumbleweed, GNU findutils, 2026-09-28; user and host names and addresses redacted.

Doing something with the results

find . -type f -size +200M -exec ls -lh {} +              # details of each match
find . -type f -size +5M -printf '%s\t%p\n' | sort -rn | head   # biggest first, in bytes
find . -name '*.log' -mtime +30 -print                        # 1. look
find . -name '*.log' -mtime +30 -delete                       # 2. delete the same list
find . -name '*.sh' -exec chmod u+x {} +                     # run a command on all of them

-exec ... {} + passes many file names to one command, which is fast. -exec ... {} \; runs the command once per file, which you only need when the command accepts a single name. Both handle spaces in names correctly, which a pipe from ls does not.

grep: searching inside files

grep 'error' app.log                     # lines containing error
grep -i 'error' app.log                  # ignore case: Error, ERROR
grep -rn 'PermitRootLogin' /etc/ssh      # recursive, with file and line number
grep -rl 'example.com' /etc              # only the names of matching files
grep -rw 'disk' .                        # whole word: not "diskette"
grep -v '^#' sshd_config                 # lines that do not start with #
grep -c 'Failed password' auth.log       # count matching lines
grep -E 'error|warning' app.log          # either word (extended regex)
grep -F '1.2.3.4' access.log             # fixed string: dots are dots
grep -C 2 'panic' app.log                # 2 lines of context around each match
grep -rn --include='*.conf' 'Listen' /etc
grep -rn --exclude-dir=.git 'TODO' .

Without -F, grep treats the pattern as a regular expression, where . matches any character. The full list of options is in grep(1). Logs of systemd services are not in text files; search those with journalctl -g, covered in the systemctl and journalctl guide.

In the same practice tree:

$ grep -rn 'port' etc/
etc/app/app.conf:1:port = 8080
etc/db.conf:2:port=5432

$ grep -rc ERROR logs/ --include='*.log'
logs/app-1.log:1
logs/app-2.log:1
logs/app-3.log:1

$ grep -v '^#' etc/app/app.conf
port = 8080
listen = 127.0.0.1

Real output on openSUSE Tumbleweed, GNU grep, 2026-09-28; user and host names and addresses redacted.

find and grep together

grep's own -r is enough most of the time. Combine the two when you need find's tests, such as age or size, to pick the files first:

find /var/log -name '*.log' -mtime -2 -exec grep -l -i 'error' {} +
find . -type f -name '*.txt' -print0 | xargs -0 grep -l 'invoice'
find chooses the files, grep searches inside them: find . -name '*.conf' -exec grep -n port {} +
$ find . -name '*.conf' -exec grep -n port {} +
./etc/app/app.conf:1:port = 8080
./etc/db.conf:2:port=5432

$ find . -name '*.log' -print0 | xargs -0 grep -l ERROR | sort
./logs/app-1.log
./logs/app-2.log
./logs/app-3.log

Real output on openSUSE Tumbleweed, 2026-09-28; user and host names and addresses redacted.

-print0 and xargs -0 separate names with a null byte, so names with spaces or newlines survive the pipe. Click such commands together with the find and grep command builder, or paste one you found online into the shell command explainer before running it.

Faster alternatives

ripgrep (rg) searches recursively by default, skips files listed in .gitignore and is much faster on big trees: rg -n 'error' /srv/app. fd does the same for file names: fd -e log. Both are packaged for openSUSE, Fedora, Debian and Ubuntu (on Debian and Ubuntu the fd package is fd-find and the command is fdfind). Learn find and grep anyway: they are on every server you will ever log in to.

$ rg -n listen
etc/app/app.conf:2:# listen = 0.0.0.0
etc/app/app.conf:3:listen = 127.0.0.1

Real output on openSUSE Tumbleweed, ripgrep, 2026-09-28; user and host names and addresses redacted.

Common mistakes

find: invalid argument `+7d' to `-mtime'

GNU find takes a plain number of days. Write -mtime +7, or -mmin +90 for minutes. Units like 7d belong to other tools, such as fd --changed-within 7d.

-delete in the wrong place

find evaluates its expression from left to right. find . -delete -name '*.log' deletes everything first and checks the name afterwards. Put -delete last, and run the command with -print first.

Piping ls into grep or xargs

ls | xargs rm breaks on the first file name with a space and may remove the wrong file. Use find with -exec or -print0 | xargs -0, or plain shell globs such as rm -i -- *.tmp.

Frequently asked questions

How do I find files older than 7 days in Linux?

find /path -type f -mtime +7. The number counts whole days of 24 hours and GNU find ignores the fraction, so +7 matches files last changed at least 8 days ago. Use -mmin +60 for minutes. There is no unit suffix: -mtime +7d is an error.

How do I find large files on Linux?

find / -xdev -type f -size +500M -exec ls -lh {} + lists files over 500 MiB on the root file system, without crossing into other mounts. For a sorted list: find / -xdev -type f -size +100M -printf '%s\t%p\n' 2>/dev/null | sort -rn | head.

How do I search for text in all files of a directory?

grep -rn 'text' /path searches recursively and prints file names and line numbers. Add -i to ignore case, -w to match whole words, --include='*.conf' to limit it to certain files, and -l to print only the names of matching files.

How do I delete files older than 30 days?

First look: find /path -type f -mtime +30 -print. When the list is right, run the same command with -delete instead of -print, and keep -delete at the very end. Put -type f in so directories are never touched.