1

I would like to evaluate a basename expression given an argument from xargs.

I tested:

find . -name '*.txt' | xargs -I f cp f DIR_OUT/copied_$(basename f)

which gives no file or directory because $(basename f) was not evaluated correctly.

I may split it two steps: copying and changing the filename, but I would like to learn how to evaluate an expression with an argument from xargs.

  • When you press enter, the shell parses the line, see the $(), evaluates it, (resulting in f), start up the find and xargs with the resulting parameters. Quoting is needed to prevent that... But xargs is not a shell and does not understand things like $() on its own... – Gert van den Berg Jun 13 '19 at 06:37

3 Answers3

1

The $(basename f) command substitution that you use in your command would be evaluated exactly once (yielding the string f), before the xargs command is even started. This is why your code fails to copy the files to the correct names.

The "No such file or directory" error that you get is due to the f in DIR_OUT/copied_f being replaced by the pathnames of the files found by find.


A more robust way of doing the same thing,

find . -type f -name '*.txt' -exec sh -c '
    for pathname do
        cp "$pathname" "DIR_OUT/copied_${pathname##*/}"
    done' sh {} +

Or, with xargs while taking care to pass the found pathnames between find and xargs as nul-terminated strings:

find . -type f -name '*.txt' -print0 |
xargs -0 sh -c '
    for pathname do
        cp "$pathname" "DIR_OUT/copied_${pathname##*/}"
    done' sh

In both of these variations, we call a small in-line shell script that loops over the given pathnames and copies them to new names under the directory DIR_OUT. The ${pathname##*/} may be replaced by $( basename "$pathname" ).

Note that -print0 (in find) and -0 (in xargs) are non-standard, but commonly implemented.

Related:

Kusalananda
  • 333,661
  • The for without in ...; is interesting... (It seems supported in bash and dash). OpenSolaris (k)sh seems to need a semicolon (or newline) (according to the man page). (The POSIX docs does not seem to require it) – Gert van den Berg Jun 13 '19 at 07:43
  • @GertvandenBerg The syntax is standard, but "relatively recent". Very old shells would not understand it. All shells that I have access to groks it correctly (bash, dash, zsh, yash, pdksh, ksh93). Yes, an older shell may well need a ; before do (but would probably not require the full for ... in "$@"; do). – Kusalananda Jun 13 '19 at 07:46
  • Even Solaris 10's sh (which is an actual Bourne shell, seems to support it). I'm not sure about the semi-colon. It is nice to know about these (relatively obscure) shorthand versions... – Gert van den Berg Jun 13 '19 at 08:04
1

You need a small modification in your original command

find . -name '*.txt' | xargs -I f sh -c 'cp f DIR_OUT/copied_$(basename f)'
k_vishwanath
  • 198
  • 1
  • 7
0

To interpret the $(), you need to run a shell from xargs.

See here

You should also be using null-deliminated output from find (when on a system that allows it), to prevent issues with problematic characters in filenames.

This should do what you want: (If somewhere without the null support, remove -0 from xargs and replace -print0 with -print)

find . -type f -name '*.txt' -print0 | xargs -n1 -r0 /bin/sh -c 'cp "$1" "DIR_OUT/copied_$(basename "$1")"' ''

-type f ensures that find only give file results -print0 make find print null terminated output -n1 ensures that xargs pass only one result to a process -r tells xargs not to run stuff if there is no results -0 makes xargs accept null-terminated input

The command that is run is a shell with an inline script and one blank parameter (since that is how parameters are passed to -c scripts)

xargs pass the parameters to the sh -c 'commands' '', which results in the first parameter being $1 inside the -c. The single quotes bevents the calling shell from dealing with the $() and $1.