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 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?

muru
  • 72,889
Wang
  • 1,296
  • 4
    Yes, you'll have to use sh or pipe it out. Process substitution takes place before find starts. – muru Dec 25 '14 at 06:03
  • 1
    Is there any more elegant way to do this? – Wang Dec 26 '14 at 03: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 '20 at 15:29
  • an excellent question & answer about this topic: https://unix.stackexchange.com/q/321697/93768 – DJCrashdummy Apr 18 '22 at 17:14

4 Answers4

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.

cuonglm
  • 153,898
  • 1
    I think you're missing a path on that proposed solution. Perhaps it should start find . -iname. Thanks! – neillb Feb 29 '16 at 00:56
9

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)"' \;
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).

an9wer
  • 161
  • 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 '23 at 09: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).