use basename in find -exec?

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?

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

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.

Method 2

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).

Method 3

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

Method 4

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).


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x