yorgo
yorgo

Reputation: 507

How to pass command output as multiple arguments to another command

I want to pass each output from a command as multiple argument to a second command, e.g.:

grep "pattern" input

returns:

file1
file2
file3

and I want to copy these outputs, e.g:

cp file1  file1.bac
cp file2  file2.bac
cp file3  file3.bac

How can I do that in one go? Something like:

grep "pattern" input | cp $1  $1.bac

Upvotes: 26

Views: 15566

Answers (5)

tripleee
tripleee

Reputation: 189936

For completeness, I'll also mention command substitution and explain why this is not recommended:

cp $(grep -l "pattern" input) directory/

(The backtick syntax cp `grep -l "pattern" input` directory/ is roughly equivalent, but it is obsolete and unwieldy; don't use that.)

This will fail if the output from grep produces a file name which contains whitespace or a shell metacharacter.

Of course, it's fine to use this if you know exactly which file names the grep can produce, and have verified that none of them are problematic. But for a production script, don't use this.

Anyway, for the OP's scenario, where you need to refer to each match individually and add an extension to it, the xargs or while read alternatives are superior anyway.

In the worst case (meaning problematic or unspecified file names), pass the matches to a subshell via xargs:

grep -l "pattern" input |
xargs -r sh -c 'for f; do cp "$f" "$f.bac"; done' _

... where obviously the script inside the for loop could be arbitrarily complex.

In the ideal case, the command you want to run is simple (or versatile) enough that you can simply pass it an arbitrarily long list of file names. For example, GNU cp has a -t option to facilitate this use of xargs (the -t option allows you to put the destination directory first on the command line, so you can put as many files as you like at the end of the command):

grep -l "pattern" input | xargs cp -t destdir

which will expand into

cp -t destdir file1 file2 file3 file4 ...

for as many matches as xargs can fit onto the command line of cp, repeated as many times as it takes to pass all the files to cp. (Unfortunately, this doesn't match the OP's scenario; if you need to rename every file while copying, you need to pass in just two arguments per cp invocation: the source file name and the destination file name to copy it to.)

So in other words, if you use the command substitution syntax and grep produces a really long list of matches, you risk bumping into ARG_MAX and "Argument list too long" errors; but xargs will specifically avoid this by instead copying only as many arguments as it can safely pass to cp at a time, and running cp multiple times if necessary instead.

The above will still work incorrectly if you have file names which contain newlines. Perhaps see also https://mywiki.wooledge.org/BashFAQ/020

Upvotes: 6

Jetchisel
Jetchisel

Reputation: 7831

#!/bin/bash

for f in files; do
  if grep -q PATTERN "$f"; then
    echo cp -v "$f" "${f}.bac"
  fi
done

files can be *.txt or *.text which basically means files ending in *.txt or *text or replace with something that you want/need, of course replace PATTERN with yours. Remove echo if you're satisfied with the output. For a recursive solution take a look at the bash shell option globstar

Upvotes: 1

choroba
choroba

Reputation: 242373

You can use xargs:

grep 'pattern' input | xargs -I% cp "%" "%.bac"

Upvotes: 38

fedorqui
fedorqui

Reputation: 290455

In addition to Chris Jester-Young good answer, I would say that xargs is also a good solution for these situations:

grep ... `ls -lad ... | awk '{ print $9 }'` | xargs kill -9

will make it. All together:

grep -hP '^\d+$' `ls -lad /dir/*/pid | grep -P '/dir/\d+/pid' | awk '{ print $9 }'` | xargs kill -9

Upvotes: 5

C. K. Young
C. K. Young

Reputation: 223193

You can use $() to interpolate the output of a command. So, you could use kill -9 $(grep -hP '^\d+$' $(ls -lad /dir/*/pid | grep -P '/dir/\d+/pid' | awk '{ print $9 }')) if you wanted to.

Upvotes: 11

Related Questions