What you're asking for doesn't make much sense in the general case, so it's not surprising that find
has no provision for it.
A symlink with a relative target is relative to the path of the symlink. So for instance, if by traversing a directory by following symlinks, find
encounters a/b/c/d
and a
, a/b
, a/b/c
are all relative or absolute symlinks (or symlinks to paths with symlink components), what should it do?
If you're looking for a find
predicate or a GNU -printf
%
directive that expands to a symlink-free path to the file relative to the current directory or any directory, I'm afraid there's none.
If you're on Linux, you can get the absolute path of those files with:
find -L foo -type f -exec readlink -f {} \;
As you found out, there exists at least one realpath
command which accepts more than one path argument which in combination with the standard -exec cmd {} +
syntax is going to be a lot more efficient since it's running as few realpath commands as necessary:
find -L foo -type f -exec realpath {} +
find -L foo -type f -print0 | xargs -r0 realpath
might be quicker as if more than one realpath
command is needed, find
can keep on looking for more files while the first realpath
starts working which even on a single processor system might make it more efficient.
-print0
and xargs -r0
are not standard, come from GNU but are found in a number of other implementations like most modern BSDs.
Zsh has builtin support for it:
print -rl foo/***/*(-.:A)
If you don't care about the sorting order, you can disable sorting and make it a bit more efficient with:
print -rl foo/***/*(-.oN:A)
If you want to convert those to relative paths to the current directory, you could have a look at that SO question.
If you know that all those files have an absolute canonical path (whose none of the components are symlinks) inside the current directory, you can simplify it to (still with zsh
):
files=(foo/***/*(-.:A))
print -rl -- ${files#$PWD/}
Though short and convenient, and works whatever character filenames contain, I doubt it would faster than find
+ realpath
.
With the Debian realpath
and GNU tools, you can do:
cd -P .
find -L foo -type f -exec realpath -z {} + |
gawk -v p="$PWD" -v l="${#PWD}" -v RS='\0' -vORS='\0' '
substr($0, 1, l+1) == p "/" {$0 = substr($0, l+2)}; 1' |
xargs -r0 whatever you want to do with them
As I realise now, there's now a realpath
in recent versions of GNU coreutils, which has the exact feature you're looking for, so it's just a matter of
find -L foo -type f -print0 |
xargs -r0 realpath -z --relative-base . |
xargs -r0 whatever you want to do with them
(use --relative-to .
instead of --relative-base .
if you want relative paths even for files whose symlink free path doesn't reside below the current working directory).
Use arrays.
If you don't need to handle the possibility of newlines in your filenames, then you could get away with
mapfile -t ABC_FILES < <(find -L some/dir -name \*.abc | sort)
mapfile -t XYZ_FILES < <(find -L other/dir -name \*.xyz | sort)
then
./program --abc-files "${ABC_FILES[@]}" --xyz-files "${XYZ_FILES[@]}"
If you do need to handle newlines within filenames, and have bash >= 4.4, you can use -print0
and -d ''
to null-terminate the names during array construction:
mapfile -td '' ABC_FILES < <(find -L some/dir -name \*.abc -print0 | sort -z)
(and similarly for the XYZ_FILES
). If you don't have the newer bash, then you could use a null-terminated read loop to append filenames to the arrays e.g.
ABC_FILES=()
while IFS= read -rd '' f; do ABC_FILES+=( "$f" ); done < <(find -L some/dir -name \*.abc -print0 | sort -z)
Best Answer
Let's change the command a bit and see what happens:
I'm not at all sure why it would print the given path in the error message, but apart from that, it's clear that it's the following
-t
that produces the error. That's because the-type
argument is taken as the shorthand for options-t
,-y
,-p
, and-e
, the same way something likels -l -aR
has the three options-l
,-a
, and-R
, and not just the option-l
and filename-aR
. Now,find
doesn't have a-t
option, so it complains.-H
works, as above, and of coursefind -f . -depth
would complain about the option lettere
.You need to end the list of options with
--
, to get the subsequent args taken as non-options. This is the same withfind
as with other tools e.g.:or if you want to list a file called
-aR
:Of course, with other tools, the issue is mostly with things like
ls *.txt
where a filename starting with-
could be an issue. One seldom uses globs withfind
, but the idea is the same.Same as with other tools, one doesn't usually need
--
withfind
, as long as the path given as the first non-option is "nice", e.g..
. An argument not starting with a dash implicitly ends option processing. But you could use--
:Of course you could also use a glob with find, e.g. something like
find -- *.d -name "*.conf"
. Not that the--
helps, since a name starting with a dash (-foo.d
) might now be taken as an invalid predicate.And of course if the expression starts with something that doesn't look like an option (
(
or!
), that also stops option processing, like in the command mentioned in the comments:That also doesn't help if a filename starts with a dash, so that's why they have the
-f
option. Or you could always just prefix the filename with./
, affecting the output similarly. As an added bonus, this would also work with a glob, e.g../*.d
.Note that GNU find is different here, it tries to interpret unknown options as predicates:
but of course that's because it doesn't require explicitly giving a path to search, but instead defaults to
.
if none is given. E.g. these are the same:It also doesn't deal with stacked options, though that's likely fine since one often doesn't use more than one with find (the
-H
,-L
and-P
options all control the treatment of symlinks, and override each other, while -O and -D are for optimization and debugging):It doesn't seem possible to give GNU find a something like
-type
as a filename on the command line. You'll have to use./-type
instead, or [pass the filenames through-files0-from
].