So there's this rename(1) Perl thing. It suits my task precisely, except that I need it to basically cp files instead of mv.

How to achieve that? I have quite a few rules of renaming, all expressed compactly in s|/foodir/|/|;s|/bardir/|/| form, and then a few lines of file patterns which I need to m̶o̶v̶e̶ copy.

It looks somewhat like this:

rename -v 's|/pars/|/|; s|/fts/|/|; s|innobase/include|include|' \
    storage/{innobase,xtradb}/pars/{pars0grm.cc,pars0grm.y,pars0lex.l,lexyy.cc} \
    storage/{innobase,xtradb}/fts/{fts0blex.cc,fts0blex.l,fts0pars.cc,fts0pars.y,fts0tlex.cc,fts0tlex.l} \

That's a job for pax. pax is a standard POSIX command; some Linux distributions omit it from the default installation, so you may need to install the package explicitly. You don't get the full power of Perl, just basic sed regex replacement, but that's good enough for your use case.

pax -rw -pe -s'|/pars/|/|' -s'|/fts/|/|' -s'|innobase/include|include|' …

If you want something more powerful, there's zsh's zmv. There are many expamples on this site, e.g. Traverse, copy & transform file names, how to rename files while copying?


Like you, I don't want a different tool or syntax, I want copy with exactly the same syntax as rename! I just answered this over here. Posting here for convenience:

I really like the regex syntax of the rename perl script (by Robin Barker and Larry Wall), e.g.:

rename "s/OldFile/NewFile/" OldFile*

OldFile.c and OldFile.h are renamed to NewFile.c and NewFile.h, respectively

I simply wanted the exact same thing with a copy command:

copy "s/OldFile/NewFile/" OldFile*

So I duplicated that script and changed the rename statement to copy via File::Copy. Et voila! A copy command with perl-regex syntax:


  • So... while trying to avoid more new tools, you basically produced a new tool? :) – ulidtko Apr 8 '18 at 8:28
  • Ha ha. The point is "same syntax means I don't have to remember anything else". Not pax, not sed, not bash a for loop. – Jeff Ward Apr 8 '18 at 13:37

OK, so I managed to get away with plain old sed like this.

ls -1 \
    storage/{innobase,xtradb}/pars/{pars0grm.cc,pars0grm.y,pars0lex.l,lexyy.cc} \
    storage/{innobase,xtradb}/fts/{fts0blex.cc,fts0blex.l,fts0pars.cc,fts0pars.y,fts0tlex.cc,fts0tlex.l} \
    storage/innobase/include/fts0[bt]lex.h \
    | sed -re 'h; s|/pars/|/|; s|/fts/|/|; s|innobase/include|include|; H; x; s|\n| |' \
    | xargs -L1 cp -v \

The sed script is not so trivial, so let me explain step-by-step (for future me):

  • h copies the current line ("pattern space") into a register ("hold space");
  • each s edits the pattern space as usual, hold space doesn't change;
  • H appends the now edited line to the hold register — which ends up holding two lines, the old filename and the new one, with embedded \n characters of course;
  • x exchanges the hold space with pattern space, effectively loading the old-new pair from the hold register;
  • s|\n| | flattens the two lines into one, separating them with space;
  • and finally, an implicit p (from calling sed without -n) prints out the result.

Altogether, the pipeline works as follows:

  • ls -1 expands all the globs into filenames of real existing files, and prints them one-per-line;
  • sed turns that into "$old_filename $new_filename", again one-file-per-line;
  • xargs -L1 calls cp -v $old_filename $new_filename on every line.

That's it!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.