6

I often find myself searching for a file in the current folder and subfolders based on part of its name. It seems to me that in this case find + grep requires less typing than only find. E.g.

find . | grep user   #19 chars typed

has to be written with only find:

find . -path "user*" #21 chars typed

It feels kind of silly to type more characters when using a single command that was meant to find files... then using two of them in combination.

Is there any way of making the use of only find to be more efficient in terms of characters typed?

  • 9
    find .|grep user would be more like find . -path '*user*'. – Stéphane Chazelas Mar 05 '18 at 12:28
  • 2
    If you search for a file in the current folder, as you write (not in a subfolder!), why don't you do ls user* (8 chars typed)? – Philippos Mar 05 '18 at 12:34
  • 1
    what I have see from long usage with find and grep is that when you need a precise complexe patern to match like *someT*hing_* Then using find | grep is way faster than find -name I personnaly don't care bout length of a command as soon as it works fast. – Kiwy Mar 05 '18 at 12:39
  • @Philippos - thanks for the observation; i've updated the question to make it clear that i'm interested in subfolders too. – mircealungu Mar 05 '18 at 12:48
  • Just an observation: find .|grep user is two less characters typed. – Nick Gammon Mar 06 '18 at 04:55
  • Assuming you are using gnu find, you can omit the period and, instead, just type: find |grep string – Hopping Bunny Mar 07 '18 at 04:43

5 Answers5

11

Yes,

ff () {
    find . -path "*$1*"
}

This function is invoked as

ff user

and will return all pathnames (of files, directories etc.) in or beneath the current directory that contain the given string.

The function definition should go into your ~/.bashrc file (or the corresponding shell initialization file that is used by your shell) and will be usable in the next interactive shell that you start.


The following variation only considers the filename portion of the pathname:

ff () {
    find . -name "*$1*"
}

If you also want to restrict the results to only regular files, then add -type f to the find invocation:

ff () {
    find . -type f -name "*$1*"
}

Note that your command

find . -path "user*"

will never output anything. This is because every considered pathname will start with ..

And finally a word of caution: I'm assuming this will be used interactively and that you will use your eyes to look at the result. If you're planning to use it in a script or for doing any looping over filenames returned by the function, please see "Why is looping over find's output bad practice?".

Kusalananda
  • 333,661
6

Assuming you want to eventually do something with those files afterwards, you could let your shell completion system show you that list and select entries for them.

For instance, with zsh:

$ echo **/*user*Tab
Completing expansions
[4/user]      1/2/3/user
Completing all expansions
1/2/3/user 4/user
Completing original
**/*user*

And then use the arrow keys to select which file (or files with Alt+A) you want to select (shown in reverse video, above indicated with the [...]). See also Ctrl+D to only list the completions without starting to select any.

Here with your ~/.zshrc containing at least:

zstyle ':completion:*' completer _expand _complete
zstyle ':completion:*' menu select=0
zstyle ':completion:*' verbose true
zstyle ':completion:*' format 'Completing %d'
zstyle ':completion:*' group-name ''
autoload -Uz compinit
compinit

I'd suggest you run compinstall to tune your completion preferences.

Above that **/*user* returns the files whose name contains user (excluding hidden ones and files in hidden directories, add the (D) glob qualifier if you want them back). For files whose path contains user, change to **/*~^*user* (needs set -o extendedglob in your ~/.zshrc).

That also has the benefit of giving a sorted list, and you can get the list in colour (à la GNU ls --colour) in addition to the /, @ suffices to help you identify the types of files if you add:

eval "$(dircolors ~/.dircolors)"
zstyle ':completion:*' list-colors ${(s.:.)LS_COLORS}

To your ~/.zshrc.

It will also avoid problems with file names with newline characters or other non-printable characters as that completion output as opposed to find's output is meant for human consumption.

zsh also doesn't have find's (at least GNU find's) issue about filenames containing byte sequences that don't form valid characters (where find -path '*user*' would fail to find a user file in a $'St\xe9phane' directory when in a UTF-8 locale for instance).

2

The GNU version of find defaults to the current directory if no path is defined.

find -name "user*" # 18 chars typed plus enter.

Richard Neumann
  • 1,399
  • 3
  • 14
  • 24
2

find . | grep user and find . -path "user*" do two quite different things. They might seem like they're the same, but they're not.

I'll mention the two most obvious differences first just to get them out of the way. The first is that you'd need to use -path '*user*' to get exactly the same result as the grep user..this isn't very important. The second, slightly more important obvious difference is that running two processes when one will do is inherently less efficient (in terms of CPU and RAM usage) - a more significant efficiency than saving two keystrokes.

The real difference, though, is what you can easily do with the filenames found by find itself, and those found using grep. Like most command line utilities in linux/unix, find is a very flexible tool - it's capable of a lot more than just finding files and printing their filenames.

With the pipe to grep, what you have is just a text list of filenames matching a particular pattern.

If you've used A NUL character as the filename separator rather than just a newline (w.g. with find's -print0 action and GNU grep's -z aka --null-data option), you can safely pipe it into xargs -0 (or perl or awk or any other program that can take NUL-separated input) and process the files.

e.g. using du -sch as a very simple example:

find . -print0 | grep -z user | xargs -0r du -sch --

If you didn't do that, or if your version of grep doesn't support -z then you can only safely process your filenames if a) you use a newline as filename separator and b) you are absolutely certain that none of the filenames will contain a newline (newlines are valid in filenames - annoying but true, and something you occasionally have to deal with).

find . | grep user | xargs -d '\n' -r du -sch --

Both of these use 4 processes (find, grep, xargs, and du) in a pipeline to total disk usage of the filenames.

With find, however, you can use the -exec or -execdir options to directly process the filename without needing to care about what delimiter you're using.

e.g.

find . -path '*user*' -exec du -sch -- {} +

This uses 2 processes (find and du) to achieve the same result.

The second real difference (and this is a big one) is that while grep can only select files by matching a regex pattern against each file's name, find can use each file's metadata to filter out unwanted matches (or to select only very specific matches - same thing)

Some examples of what find is capable of:

If you want only regular files, use -type f, if you want only directories, use -type d, symlinks -type l and so on.

If you want only files owned by a particular user, use -uid nnn or -user username..same for -gid nnn and -group groupname.

find can match on permissions, on timestamps, on size, and on whether a file is -readable or -writable by the user running it.

It also has built in regular expression matching (-regex, with a choice of regex styles using -regextype), as well as glob matching (-name, -path).

You can use -prune to exclude entire subdirectory trees from being searched (reducing the amount of disk i/o and time required).

It can combined all of these with boolean logic operators AND, OR, and NOT.

find also has various built in actions. The default is -print, and I've already mentioned -print0. It can also output any of the available metadata of a file in whatever format you want with its -printf action. It can delete files with -delete, or output a detailed list (similar to ls -l) with -ls.

-exec runs any external process and gives it the list of matching files on the command line with {}. Either one file at a time if you terminate the -exec command with \; or as many as will fit into a shell command line at a time with +.

-execdir does the same but, if using \; changes to each file's directory before running the external process. With + it changes to the directory and then runs the external command with as many filename arguments as will fit on a command line.

BTW, some of the above find predicates and actions are GNU extensions, and may not work in other non-GNU versions of find.

cas
  • 78,579
  • 2
    BTW, yes find is difficult to use at first. And it's hard to make any sense of the man page (it's quick reference material, not documentation). I can remember feeling aghast a few decades ago when I first ran man find and saw this enormous wall of near-incomprehensible text...and thought "No F-ing Way!". But I found uses for it, and it was obvious that the horrible complexity came from the fact that it is extremely capable and versatile. I started using a few of its features and learning more about it over time. It's definitely worth putting in the effort to learn. – cas Mar 05 '18 at 15:50
0

If you use, once, the command pip install grin you will gain both the grin & grind utilities, (python 2 only at the moment I am afraid).

Using grind your query becomes:

grind user*

Which is 11 characters.

Of course if you are going the python way you could always have on your path a file called floc that is tailored to do the sort of searches that you do the most often:

#!/usr/bin/env python3
from glob import glob
import sys
for arg in sys.argv[1:]:
   print('\n'.join(glob('**/{}*'.format(arg), recursive=True)))

Then chmod +x floc to make it executable. Your query then becomes:

floc user

Which is down to 9 characters. Of course you could also create a shell alias for your original command to get a similar reduction.