2.5.2. Searching files#
Now that you know how you can inspect files in a directory and what the output of ls -l means, itâs time to learn how to search for specific files. There are a large number of files on a typical Unix system, so finding that one file you want can be hard.
Here we present to general strategies:
Searching for particular files by their name, extension, or some other properties.
Searching in files, to find files that contain some specific text.
Weâll discuss useful commands for each strategy separately.
2.5.2.1. Searching for particular files#
If you know what file or what kind of file you are looking for, there are various useful commands that you could use:
ls -R: usinglswith the-R(for recursive) option will show you all files in all subdirectories of the directory youâre listing. This can quickly give a lot of information!Exercise 2.84
Try this in the root directory:
$ ls -R /
What do you think
ls -lRwill do? Try it.which: the output ofwhich programis the full path to the programprogram. Use this if you want to find out which version of a certain program youâre actually using.whereis: does more or less the same aswhich, but it only looks for programs in the standard places where Unix stores its programs. It will not find programs you made yourself. When it finds a program, it also tries to print the location of themanpage.Exercise 2.85
Use
whichandwhereisto find out where thels,whichandwhereiscommands are located.find: the Swiss army knife of search tools. This command has a lot of options. The most often used one is-name, for example:$ find -name test.me # list all files under working directory called `test.me` $ find / -name "*.jpg" # all files under the root directory that end with `.jpg` (Note: use quotes because of *)
This instructs
findto look at all file and directory names in the root directory/, or any of its subdirectories (so it searches recursively through the directory tree, similar tols -R), and reports only the names which end in.jpg. If you want to exclude the possibility thatfindreports on any directory name that ends with.jpg(which is uncommon, but possible), then you could explicitly specify to only report on files with the-type fargument.Exercise 2.86
Find all files below
/usr/sharewhich end in.txt. Also find all files below/etc.Exercise 2.87
Using
findâs-nameoption,list all files called exactly
READMEin the root directory/list all the files whose name contains with the string
report(this should match names likeold_report.txt,report.bin, etc.) in the root directory/. Hint: use quotes when using*patterns as command arguments.
{ "filesystem": { "/opt/science_project/logo.dat": { "base64": "iVBORw0KGgoAAAAN" }, "/opt/science_project/report.bin": "%PDF-1.4\n%%EOF\n", "/opt/science_project/deploy": { "content": "#!/bin/bash\necho deploying\n", "mode": "755" }, "/opt/science_project/README": {"content": "Events will be logged in engine.dat", "mtime": "2021-03-04T05:07"}, "/opt/science_project/engine.dat": {"content": "", "mtime": "2021-03-04T05:07"}, "/home/student/vacation.txt": { "base64": "/9j/4AAQSkZJRgAB" }, "/home/student/.thrash/report_placeholder": "Lorem ipsum", "/home/student/.thrash/README": "My first project", "/home/student/License.txt": "None yet\n", "/home/student/old_report.txt": { "content": "Q1 numbers, ancient history by now.\n", "mtime": "2023-01-01T00:00:00Z" }, "/home/student/new_report.txt": "Q4 numbers, fresh off the press.\n" }, "checks": [ { "type": "commandEvent", "eventType": "coreutil.find", "match": { "absPaths": ["/"], "namePattern": "README" }, "desc": "should run find on the root directory, using `-name` to list all files called `README`" }, { "type": "commandEvent", "eventType": "coreutil.find", "match": { "absPaths": ["/"], "namePattern": "*report*" }, "desc": "should run find on the root directory, using `-name` to list all files with `report` in the filename" } ] }
Other useful options of
findallow you to just select files newer or older than a certain date, files of certain types, sizes, files with certain permissions, files belonging to a certain user or group, etc. Seeman findfor the complete overview. For example:$ find . -size +80k -ctime +100
Recursively finds all files in the current directory larger than 80 kilobytes, and which are older than 100 days.
Note
This last example uses
-ctime, andfindalso supports combining searches with-exec(see below). Both are too involved to fully support in this bookâs in-browser terminal. If you try them here,findwill tell you so and point you to a real terminal instead.Exercise 2.88
Use
findto list only the directories under/.Use
findto list files larger than 30 kilobytes in/usr/share.Use
findto list all empty files under the home directory.Use
findto list files under the home directory that were modified more than 20 days ago.Use
findto list empty files under the home directory that were modified more than 1 year ago.
{ "filesystemZip": "bash-exercise-fs/fs-usr-share-common-licenses.zip", "filesystem": { "/home/student/vacation.txt": { "base64": "/9j/4AAQSkZJRgAB" }, "/home/student/projects/logo.dat": { "base64": "iVBORw0KGgoAAAAN" }, "/home/student/projects/report.bin": "%PDF-1.4\n%%EOF\n", "/home/student/projects/deploy": { "content": "#!/bin/bash\necho deploying\n", "mode": "755" }, "/home/student/projects/README": {"content": "Events will be logged in engine.dat", "mtime": "2021-03-04T05:07"}, "/home/student/projects/engine.dat": {"content": "", "mtime": "2021-03-04T05:07"}, "/home/student/placeholder": "", "/home/student/License.txt": "None yet\n", "/home/student/old_report.txt": { "content": "Q1 numbers, ancient history by now.\n", "mtime": "2023-01-01T00:00:00Z" }, "/home/student/new_report.txt": "Q4 numbers, fresh off the press.\n" }, "checks": [ { "type": "commandEvent", "eventType": "coreutil.find", "match": { "absPaths": ["/"], "typeFilter": "d" }, "desc": "should use find to list directories under /" }, { "type": "commandEvent", "eventType": "coreutil.find", "match": { "absPaths": ["/usr/share"], "sizeSpec": "+30k" }, "desc": "should use find to list files larger than 30 kilobytes under /usr/share" }, { "type": "commandEvent", "eventType": "coreutil.find", "match": { "absPaths": ["/home/student"], "empty": true }, "desc": "should use find to list empty files in the home directory" }, { "type": "commandEvent", "eventType": "coreutil.find", "match": { "absPaths": ["/home/student"], "mtimeSpec": "+20" }, "desc": "should use find to list files modified more than 20 days ago in the home directory" }, { "type": "commandEvent", "eventType": "coreutil.find", "match": { "absPaths": ["/home/student"], "mtimeSpec": "+365", "empty": true }, "desc": "should use find to list empty files modified more than 365 days ago in the home directory" } ] }
Warning
findalso supports a-deleteaction, which removes every matched file or directory. On a real system this is permanent, and there is no undo. Before you ever add-deleteto afindcommand, always run the exact same command without-deletefirst, and carefully check the list of matches. Only add-deleteonce youâre sure that list is exactly what you want removed, and nothing more.This sandbox is a safe place to get comfortable with
-delete, since nothing here is real. Resetting the exercise restores everything.Exercise 2.89
Using
findand the-deleteaction, remove all empty files under your home directory. Donât deleteprojects/itself, or any file that actually has content.{ "filesystem": { "/home/student/placeholder": "", "/home/student/projects/old_version/engine.dat": "", "/home/student/projects/README": "Read this first.\n", "/home/student/notes.txt": "Meeting notes.\n" }, "checks": [ { "type": "commandEvent", "eventType": "coreutil.find", "match": { "absPaths": ["/home/student"], "empty": true, "delete": false }, "desc": "first use find -empty in the home directory without -delete, to list relevant files" }, { "type": "commandEvent", "eventType": "coreutil.find", "match": { "absPaths": ["/home/student"], "empty": true, "delete": true }, "desc": "should use find with -delete in the home directory to remove the empty files" }, { "type": "fileExists", "path": "/home/student/placeholder", "negate": true, "desc": "placeholder should be deleted" }, { "type": "fileExists", "path": "/home/student/projects/old_version/engine.dat", "negate": true, "desc": "projects/old_version/engine.dat should be deleted" }, { "type": "fileExists", "path": "/home/student/projects/README", "desc": "projects/README should not be deleted" }, { "type": "fileExists", "path": "/home/student/notes.txt", "desc": "notes.txt should not be deleted" }, { "type": "dirExists", "path": "/home/student/projects", "desc": "the projects directory itself should still exist" } ] }
Note: locate is another tool for finding a file regardless of where it is on the system.
Instead of actually searching the disk, it consults a database that is updated daily.
This is much faster then find on the root directory, but it wonât show recently added files that are not indexed in its database yet.
Ubuntu nowadays does not come with locate installed by default, but you could install it yourself with
sudo apt install plocate
The locate database would be rebuild regularly in the background, but if you want to force building the database immediately,
you would need to run sudo updatedb. Afterwards, you can rapidly find any name x on your filesystem with locate x.
This can be useful on a powerful desktop or laptop where you often use locate,
but you would not want such a database update to occur regularly on a robot with limited computing power.
2.5.2.2. Identifying what a file is#
Just by the name, itâs usually very hard to judge what a file contains.
It might be a program, some text, some program source code, a figure, an image, a sound etc.
Often, the extension in a filename (like jpg in DSC0005.jpg, or txt in README.txt) is a good indication that a file is an image (extensions jpg, jpeg, png, tiff, etc.), or text (txt for plain text, md for markdown, etc.), or something else.
However, this is just a convention and not a hard requirement, a file could lack any extension.
A more robust way of identifying what type of data a file contains is by inspecting it, and try to recognize the data format inside it.
Does it contain image information? Some binary data? Perhaps it is the name of a directory, or just simple text?
You donât have to do this inspection and guessing yourself,
instead you can rely on the file command.
The basic usage is as follows:
file somefile
The file command would inspect the content of the file somefile, and based on various rules and heuristics
give a description of the type of data it contains.
Exercise 2.90
Try this in a terminal on your real Ubuntu Linux computer.
Run file on all the names (files and directories) in the directory /usr/share/backgrounds, e.g.
$ file /usr/share/backgrounds/*
What does this directory contain?
File descriptors of every background process running on your computer
Log files for services running in the background (daemons)
Shell scripts that launch programs with
&to run them in the backgroundConfiguration files for background system tasks like cron jobs
Compiled binaries for background daemons
A list of process IDs (PIDs) for currently backgrounded jobs
Symbolic links to the current desktop themeâs asset folder
Cached thumbnails generated by the file manager
Desktop wallpaper images
You can use file to find out whether you can view a file on the screen or not. File types you can show on the screen are: ASCII text, C++ code files, Bourne shell script text, Python scripts, etc. File types you cannot view this easy are: data, directory, symbolic link to âŠ, executable, etc.
Exercise 2.91
The directory ~/mystery already exists, full of files with deliberately unhelpful names. file doesnât look at a name or extension at all, it looks at the actual bytes inside a file. Use file mystery/* (or cd mystery then file *) there to see for yourself, and select all of the following that are true.
{
"filesystem": {
"/home/student/mystery/vacation.txt": { "base64": "/9j/4AAQSkZJRgAB" },
"/home/student/mystery/logo.dat": { "base64": "iVBORw0KGgoAAAAN" },
"/home/student/mystery/report.bin": "%PDF-1.4\n%%EOF\n",
"/home/student/mystery/deploy": { "content": "#!/bin/bash\necho deploying\n", "mode": "755" },
"/home/student/mystery/README": "Read this first.\n",
"/home/student/mystery/engine.dat": "#include <vector>\ntemplate<typename T>\nclass Engine {};\n",
"/home/student/mystery/shortcut": { "symlink": "README" },
"/home/student/mystery/License.txt": "None yet\n",
"/home/student/mystery/projects/placeholder.txt": "I should start one\n"
}
}
vacation.txtis actually JPEG image data, despite its.txtextension.logo.datis a plain text file.report.binis a PDF document.deployis abashshell script, and is executable.READMEis a directory.License.txtis a directory, despite its.txtextension.engine.datis a directory.engine.datis C++ source code, despite its.datextension.shortcutis a symbolic link toREADME.shortcutis a regular text file.projectsis a directory.projectsis a symbolic link toLicense.txt.
2.5.2.3. Searching through files#
Besides looking for particular files, you may also want to find files containing particular pieces of text. In this case, the command to use is grep. This command takes as parameters a string to search for and one or more files:
$ grep "hello world" myfile.txt
The search string can be just a piece of text, in this case the string hello world, and grep reports all lines containing the string.
Itâs a good idea to put this string between quotes ("), otherwise the shell would interpret the search string as being just hello, and the word world to be a new (unknown) grep parameter.
It is also possible to search in multiple files in a directory at once, simply by using a wildcard for the filename, such as:
$ grep -i error *.txt
Here the -i flag indicates a case insensitive search, so it matches lines with the word error, but also Error or ERROR.
Exercise 2.92
Using grep
Perform a case insensitive search for all lines containing the string
yoyoin all licenses in/usr/share/common-licenses.Find all lines in the file
/usr/share/common-licenses/GPLwhich do not contain a space (" "), and make grep also print the line numbers for the matched lines. Hint: usegrep --helpto find the relevant command switches.Report all filenames in the directory
/usr/share/common-licenses/of files that contain the wordMozilla.
{
"filesystemZip": "bash-exercise-fs/fs-usr-share-common-licenses.zip",
"checks": [
{ "type": "commandEvent", "eventType": "coreutil.grep",
"match": { "pattern": "yoyo", "flags": ["i"], "matchCount": 4 },
"desc": "grep for insensitive search yoyo across all the license files, matching exactly 4 sentences" },
{ "type": "commandEvent", "eventType": "coreutil.grep",
"match": { "pattern": " ", "flags": ["n", "v"], "absPaths": ["/usr/share/common-licenses/GPL"] },
"desc": "grep for lines with no empty space in the GPL file, also printing line numbers" },
{ "type": "commandEvent", "eventType": "coreutil.grep",
"match": { "pattern": "Mozilla", "flags": ["l"], "matchCount": 2 },
"desc": "grep to print all filenames in /usr/share/common-licenses containing Mozilla" }
]
}
However, you can also supply grep with a regular expression.
In fact, the name grep stands for global regular expression print.
A regular expression is a pattern describing how to match various strings according to a number of rules,
which it does using so-called meta characters:
"^string"matchesstringonly when it is located at the start of a line."string$"matchesstringonly when it is located at the end of the line."."matches any character."[abc]"matches ana,borc."[0123456789]"or"[0-9]"matches any single digit number."[^abc]"matches anything but ana,borc."[abc]*"matches zero or more occurrences of any of the charactersa,borc."[abc]\?"matches zero or one occurrence of any of the charactersa,borc."[abc]\+"matches one or more occurrences of any of the charactersa,borc."[abc]\{n\}"matches exactlynoccurrences of any of the charactersa,borc.
Unlike *, the ?, + and {n} repeat-operators above only work as repeat-operators if you put a \ in front of them. Without the backslash, they are just ordinary, literal characters to grep. So, "[abc]?" (no backslash) really means âan a, b or c, followed by a literal question markâ, not âzero or oneâ. This is the reverse of what you might expect: normally a \ in front of a character makes it literal (see below); for ?, + and {n} specifically, it is the backslash that turns them into repeat-operators.
To search for any of the characters used as meta characters (such as ., *, [ or ]) so that they are matched literally instead, you put a \ in front of them. For example, to look for some text between square brackets, use grep "\[.*\]".
Exercise 2.93
Use grep to find the following expressions in /usr/share/common-licenses/GPL. In each case, try to predict what will be the output before running it:
"[A-Z]"" [A-Z] "" [A-Z][a-z]*""U[a-z]* ""U[a-z]\{3\} "
{
"filesystemZip": "bash-exercise-fs/fs-usr-share-common-licenses.zip",
"checks": [
{ "type": "commandEvent", "eventType": "coreutil.grep",
"match": { "pattern": "[A-Z]", "absPaths": ["/usr/share/common-licenses/GPL"], "matchCount": 308 },
"desc": "grep \"[A-Z]\" GPL should match 308 lines" },
{ "type": "commandEvent", "eventType": "coreutil.grep",
"match": { "pattern": " [A-Z] ", "absPaths": ["/usr/share/common-licenses/GPL"], "matchCount": 12 },
"desc": "grep \" [A-Z] \" GPL should match 12 lines" },
{ "type": "commandEvent", "eventType": "coreutil.grep",
"match": { "pattern": " [A-Z][a-z]*", "absPaths": ["/usr/share/common-licenses/GPL"], "matchCount": 293 },
"desc": "grep \" [A-Z][a-z]*\" GPL should match 293 lines" },
{ "type": "commandEvent", "eventType": "coreutil.grep",
"match": { "pattern": "U[a-z]* ", "absPaths": ["/usr/share/common-licenses/GPL"], "matchCount": 29 },
"desc": "grep \"U[a-z]* \" GPL should match 29 lines" },
{ "type": "commandEvent", "eventType": "coreutil.grep",
"match": { "pattern": "U[a-z]\\{3\\} ", "absPaths": ["/usr/share/common-licenses/GPL"], "matchCount": 7 },
"desc": "grep \"U[a-z]\\{3\\} \" GPL should match 7 lines" }
]
}
We can combine finding files with searching in them using a clever find option, -exec,
which makes it execute a command for every file it finds.
The command to execute is specified by additional arguments after -exec,
up to a final \; argument which indicates that the command started by -exec has been completely specified.
Within this command, find will replace each {} argument by the path it found.
For example, we can run a grep command separately on all files found by find like this:
$ find /bin/ -type f -exec grep -H "# Copyright" {} \;
/bin/zmore:# Copyright (C) 2001, 2002, 2007 Free Software Foundation
/bin/zmore:# Copyright (C) 1992, 1993 Jean-loup Gailly
/bin/zdiff:# Copyright (C) 1998, 2002, 2006-2007, 2009-2022 Free Software Foundation, Inc.
/bin/zdiff:# Copyright (C) 1993 Jean-loup Gailly
(This probably produces many more results than shown here)
So in this example, find /bin/ -type f will find in a list of file paths,
such as /bin/zmore, /bin/zdiff, etcetera.
As a result, find will execute grep -H "# Copyright" /bin/zmore,
then grep -H "# Copyright" /bin/zdiff, etcetera
(the -H option of grep will make grep also print the filename for every matched line).