50
find . -type f -exec  echo {} \;

Using the above command, I want to get file names without leading "./" characters. So basically, I want to get:

filename

Instead of:

./filename

Any way to do this?

Diplomat
  • 503
  • 2
    For files in sub-directories, do you just want filename or path/to/sub/dir/filename (without the leading ./)? – xhienne Dec 20 '16 at 17:30

6 Answers6

66

Use * instead of . and the leading ./ disappears.

find * -type f

Note that it's the shell that first expands that * to the sorted list of non-hidden (by default) files in the current working directory and passes them instead of . as separate arguments to find. That has several implications that should be noted:

  • hidden files are omitted at the top level but not at deeper levels (the ones traversed by find).
  • the list is sorted at the top level (generally a plus)
  • you may run into the limit on the number of arguments that can be passed to command if the current working directory has a lot of file.
  • That won't work if there are files named !, (, ) or with a name starting with -, with possible nasty consequences if there's a file called -delete for instance.
Georg Puchta
  • 709
  • 5
  • 2
  • works on macOS Catalina zsh – Gi0rgi0s Aug 20 '20 at 21:43
  • 7
    This is neat. It's worth mentioning that * is expanded by the shell to a list of all files that is then passed to find. Depending on the shell and its settings that listing may or may not contain files whose names start in a dot. That may be different from what find would do if it were given .. For bash you can use find * .* -type f to list all files. – Hannes Dec 29 '20 at 22:45
  • 8
    @Hannes Don't use .* but .[^.]* *, else find will explore .. See my answer for the full command. BTW, this is quite depressing to notice that this answer only repeats one of the commands I posted 3 years before, and yet this answer has twice more votes than mine despite it is incomplete (hidden files are omitted) and despite I warned such a construct should be avoided. Go figure... clearly, people prefer short answers, even if such answers fail in certain situations. – xhienne Apr 02 '21 at 09:20
  • You are right. I upvoted your answer. – Hannes Apr 06 '21 at 01:49
  • 2
    With this I get shellcheck issue SC2012 - just imagine one of the files is named -name. – Johannes Aug 08 '22 at 20:58
  • Lovely answer. Thank you! – Binita Bharati Aug 31 '22 at 10:21
  • This command fails if there are files beginning with - in the folder. – zomega Mar 08 '23 at 10:26
  • It fails if there are too many files for the shell to expand – Chris Davies Oct 12 '23 at 06:13
38

Assuming that you don't want the filename alone, but also the full relative path without the leading ./, if you have GNU find you can use:

find . -type f -printf '%P\n'

From find(1) man page:

%P - File's name with the name of the starting-point under which it was found removed.

Else, you can try one of these:

find . -type f -print | cut -d/ -f2-

find .[!.]* * -type f -print

You should avoid this last one (think of the excessively long command line if you have a lot of entries in your directory).

-print does the same thing as your -exec echo {} \; but is much better: no external process call so lower overhead and no undesirable side-effects with filenames beginning with a dash.

xhienne
  • 17,793
  • 2
  • 53
  • 69
  • Using the .[^.]* syntax does not work in any (posix) shell that I am aware of. – Juan Jul 12 '21 at 15:19
  • @Juan From what I read, you know at least one shell that does not understand the .[^.]* syntax or that includes . and .. in the resulting pathname expansion. Would you be kind enough to name it and explain what does not work? I do agree this is not strictly POSIX compliant and one should use .[!.]* instead, but I have used this syntax on many platforms for more than 30 years and never had a problem with it. – xhienne Jul 14 '21 at 11:33
  • some shells where the .[^.]* syntax does not match strings that start with a dot followed by a non-dot: mksh r59, dash 0.5.10.2, ksh 2020.0.0, pdksh 5.2.14 - some cause the error: "find: .[^.]: No such file or directory". However, it looks liks POSIX currently does* specify bracket expressions as valid (and not optional) for pattern matching (see the 'Shell and Utilities' document, 'Pattern Matching Notation': https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13). So the shells where it does not work appear to be deficient as far as POSIX is concerned. – Juan Aug 20 '21 at 14:40
  • Regarding the previous comment. Interestingly, in the posix doc specified in the previous comment, section 2.13.1 explains that '!' can be used for the character class negation. And using '^' unquoted can produce "unspecified results". If you do ls -d .[^.]* with dash, it only lists .., but ls -d .[!.]* lists dot files and not . nor ... So dash does support bracket expressions, albeit a bit differently than bash. It looks like dash doesn't support the ^ negation at all (which is not what posix is specifying as I read it). – Juan Aug 20 '21 at 15:35
  • It looks like using .[!.]* works with more shells than .[^.]* does. The ksh flavors and dash seem to only accept ! for character class negation and not ^ - unless there is some way to properly quote the ^ for dash & ksh and get it working (but I didn't figure out a way to do that in some quick quoting / escaping attempts). Maybe an edit to this answer that describes this would be helpful. – Juan Aug 20 '21 at 15:42
  • Thank you a lot @Juan for all your useful comments. Answer corrected (finally!) – xhienne Oct 12 '23 at 00:04
7

find . -type f -exec echo {} \;

Default action of find is to print results. When not explicitly told, find defaults to searching current directory. Your command can be simplified to find -type f.

Why/when does this matter? Only when you run this command on a sufficiently large directory, you will start to see the performance difference. -exec echo {} \; will make your computer work needlessly more because it has to start an external process like /bin/echo to print the filenames. One instance of echo executable is run per file found by find.

To answer your question about removing ./, a more efficient method would be to use cut. You know that the first two characters are always ./. cut -c3- will only retain characters from position 3 and beyond.

> cd /etc/fonts/conf.d
> find -type f | cut -c3-
99pdftoopvp.conf
00kde.conf
65-khmer.conf
README

This fails to work as expected if your filenames contain new line character, but that is another whole new story.

Gowtham
  • 2,081
  • The cut option is the only one that works on my Linux machine for multilevel outputs. Thanks! – GenesRus Mar 18 '21 at 20:43
  • 3
    The form that omits specifying a search path/paths (find -type f) is not currently POSIX compatible behavior. This fails with variants of find(1) that are not GNU find. – Juan Jul 12 '21 at 15:22
  • 2
    cut cuts each line but there's nothing stopping a file path from containing newline characters. – Stéphane Chazelas Jul 05 '23 at 16:19
6

The GNU version of find provides the builtin action -printf format which allows you to format the output of your query.

So to answer your question

find . -type f -printf '%f\n'

Will output only the filenames with no leading characters (or pathnames) with a newline so each filename is on it's own line

Chris
  • 61
1

The answers with printf are missing the point for the case where I want to run a command via -exec on the params and echo is just an example. In reality I may want to run my-command on the found files instead of echo

Here's a maybe not efficient but working solution:

find . -type f -exec sh -c 'echo ${0#./}' "{}" \;

Explanation:

  • We rely on the fact that sh -c 'echo $0' foobar runs a subshell which prints $0, and foobar is passed as $0, so it prints foobar
  • instead of $0, we actually use ${0#./}, which means "use $0, but strip leading ./ (#./) - this is parameter expansion
  • we pass {} from exec (the found filename from find with leading slash) as $0 to sh -c

So in the end to run my-command on found files without leading ./, you'd do:

find . -type f -exec sh -c 'my-command ${0#./}' "{}" \;
jakub.g
  • 3,263
  • 5
  • 21
  • 18
0

In:

find path/to/dir some/file other-file other/dir <criteria> -print

Or:

find path/to/dir some/file other-file other/dir <criteria> -exec cmd -- {} ';'

We tell find to find files matching the <criterial> starting with the list of file paths given as initial arguments.

If path/to/dir above matches the <criteria>, it will be printed or cmd will be called with -- and path/to/dir as arguments. Same for some/file or other/dir.

If path/to/dir is a file of type directory, and -prune has not been called for it, then find will carry on processing files within it, with path/to/dir/file-within as the path being processed.

. in that regard is just like any other path. It's a path to the current working directory, so if . matches the criteria, that will print ., and if the file-within also does, it will print ./file-within.

As others have said, the GNU implementation of find has a -printf predicate which can be used in place of -print to print other attributes of the matching files than the file path. -print is the equivalent of -printf '%p\n', but in place of %p, you can also use:

  • %f: the file's name without any leading path component (the basename or tail)
  • %h: the directory containing the file or . for /-less path (the dirname or head).
  • %P: the file path relative to the initial dir/file it was found under, or the empty string for that file itself.
  • %H: the initial dir/file it was found under
  • and many other metadata of the file.

So except for /-less files, %p is the same as %h/%f and except for the initial dir/file themselves, the same as %H/%P.

For example:

$ find path/to/dir other-file -printf '%%p="%p" %%h="%h" %%f="%f" %%H="%H" %%P="%P"\n'
%p="path/to/dir" %h="path/to" %f="dir" %H="path/to/dir" %P=""
%p="path/to/dir/file-within" %h="path/to/dir" %f="file-within" %H="path/to/dir" %P="file-within"
%p="other-file" %h="." %f="other-file" %H="other-file" %P=""
$ cd path/to/dir
$ find . -printf '%%p="%p" %%h="%h" %%f="%f" %%H="%H" %%P="%P"\n'
%p="." %h="." %f="." %H="." %P=""
%p="./file-within" %h="." %f="file-within" %H="." %P="file-within"
find . <criteria> -printf '%P\n'

Will report the paths of matching files relative to the current working directory but not for . itself, so in the general case, you rather need:

find . <criteria> '(' -name . -print -o -printf '%P\n' ')'

(. wouldn't match a -type f criteria though, so we do not need to special case it in that case or any other case where we can guarantee . won't match the criteria).

Now, assuming your echo was just an example standing in for any another command, -printf alone won't help.

With the GNU implementation of xargs, and a shell with ksh-style process substitution, you can do though:

xargs -0I {} -a <(
  find . <criterial> '(' -name . -print0 -o -printf '%P\0' ')'
) cmd -- {}

Where find prints those relative paths NUL-delimited, which xargs retrieves and passes as argument to cmd.

Or even:

xargs -r0 -a <(
  find . <criterial> '(' -name . -printf '.\0' -o -printf '%P\0' ')'
) cmd --

If cmd can take more than one file path as argument (similar to what you can do with -exec cmd {} +).

Now, that won't help if -exec cmd {} ';' was intended to be used as a condition, like in:

find . -type f -exec grep -l needle {} ';' \
  -exec cmd-for-files-with-needle {} ';'

Where the cmd-for-files-with-needle is executed for files for which grep succeeds (after having printed the path of the matching files with -l).

Then, you could do instead (and that does not require GNU find nor xargs):

find . <criteria> -exec sh -c '
  for file do
    file=${file#./} # strip the leading ./ if any. It leaves . as is,
                    # so we do not need to special-case it
    grep -l -- needle "$file" && 
      cmd-for-files-with-needle -- "$file"
  done' sh {} +

Note the need for --, now that the file paths are not guaranteed to start with . and therefore may start with -.

Or you could use zsh where globs can do most of what find can do. For instance:

for file in **/*(ND.m-1); do
  grep -l -- needle $file &&
    cmd-for-files-with-needle -- $file
done

Where the . glob qualifier is the equivalent of find's -type f, m-1 of -mtime -1, N for nullglob, D for dotglob.

Here, a ./ prefix is not included (you could do ./**/* if you wanted it), . is not included (like -mindepth 1 in GNU find), and the list is sorted (and the sorting order can be changed with the o/O/n glob qualifiers)