14

If I just use basename {} .txt, it will work:

find . -iname "*.txt" -exec basename {} .txt \;

It will just print xxx instead of ./xxx.txt

if I want use $(basename {} .txt) in the -exec option, it will fail:

find . -iname "*.txt" -exec echo "$(basename {} .txt)" \;

It will just print ./xxx.txt

How can I solve this problem? I hope I can use $(basename {} .txt) as parameters for other cmd. Do I have to do sh -c or pipe -exec basename {} \; with xargs?

4
  • 4
    Yes, you'll have to use sh or pipe it out. Process substitution takes place before find starts.
    – muru
    Dec 25, 2014 at 6:03
  • 1
    Is there any more elegant way to do this?
    – Wang
    Dec 26, 2014 at 3:03
  • 2
    If other reading this don't need the suffix-stripping capabilities of basename, you can use -execdir instead of -exec as that holds the current file (not path) in {} (source)
    – Sam
    May 18, 2020 at 15:29
  • an excellent question & answer about this topic: unix.stackexchange.com/q/321697/93768 Apr 18, 2022 at 17:14

4 Answers 4

16

Try:

find -iname "*.txt" -exec sh -c 'for f do basename -- "$f" .txt;done' sh {} +

Your first command failed, because $(...) run in subshell, which treat {} as literal. so basename {} .txt return {}, your find became:

find . -iname "*.txt" -exec echo {} \;

which print file name matched.

1
  • 1
    I think you're missing a path on that proposed solution. Perhaps it should start find . -iname. Thanks!
    – neillb
    Feb 29, 2016 at 0:56
8

You can still use $() for command substitution if you use sh -c and single quotes:

find . -iname "*.txt" -exec sh -c 'echo "$(basename {} .txt)"' \;

The single quotes prevent the main shell from executing the sub-command inside $() so it can instead be executed by sh -c after the find command has replaced {} with the file name. See this answer on Stack Overflow for a more thorough explanation.

Note that you can also add double quotes inside the $() to handle spaces in file names:

find . -iname "*.txt" -exec sh -c 'echo "$(basename "{}" .txt)"' \;
0
6

Instead, there is another way by using bash pipelines and xargs command:

    find . -iname '*.txt' | xargs -L1 -I{} basename "{}"

In the xargs command above:

The -L1 argument means executing the output of the find command line by line.

The -I{} argument is intended to wrap the output of the find command with double quotes to avoid bash word splitting (when filenames contain whitespace).

1
  • I got this warning: xargs: warning: options --max-lines and --replace/-I/-i are mutually exclusive, ignoring previous --max-lines value. How to fix?
    – pmor
    Aug 20 at 9:08
1

Agree with @an9wer, as a pipeline is a lot easier (to type, to remember, to read, etc) though I'd try to adhere to posix xargs and use nil character as separator; xargs on alpine, for example, doesn't have the -L1 flag, which means that you must explicitly flag for nil in both the find and xargs command. If you're talking about running adhoc commands, then ignore this, but if this is a script that is developed on something that is not your production environment, and assuming your production environment never changes, then this is a really big deal.

Same as @an9swer, but with nil character as delimiter, using -n flag to feed one result, at a time, as an argument of basename. We also don't need the template flag -I, since its inferred and the default behavior.

find . -iname '*.txt' -print0 | xargs -0 -n1 -- basename

An additional advantage to the above, is that xargs allows for executing the passed statement, in parallel, which means you could run the above in parallel, up to the number of cores that you have, to feed to some downstream process. If you are dealing with a number-approaching-infinity files or if workflow optimization is important, this is a big, low-hanging-fruit win

And yes, I am showing off a little, but its ok, because I am fine with being petty (at times).

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .