16

Background

In Linux you can:

  • List a group of files with ls Filename*
  • Remove a group of files with rm Filename*
  • Move a group of files with mv Filename* /New/Directory
  • But you cannot copy a group of files with: cp Filename* *.bak

Change Linux cp command to copy group of files

I have a group of files I'd like to copy without entering the names one by one and using the cp command:

$ ls gmail-meta3*
gmail-meta3                          gmail-meta3-REC-1558392194-26467821
gmail-meta3-LAB-1558392194-26467821  gmail-meta3-YAD-1558392194-26467821

How can I use something like the old DOS command copy gmail-meta3* *.bak?

I don't want to type similar command four times:

cp gmail-meta3-LAB-1558392194-26467821 gmail-meta3-LAB-1558392194-26467821.bak

I'm looking for a script/function/app that accepts parameters for old and new filename group and not something with hard-coded filenames. For example, a user can type:

copy gmail-meta3* *.bak

or they might type:

copy gmail-meta3* save-*

8 Answers8

21

Here is an example of one atypical usage of sed, that is applicable in this case:

sed -i.bak '' file-prefix*

In this way, actually, sed will not change the files, because we do not provide any commands '', but due to the option -i[suffix] it will create a backup copy of each file. I found this approach when I was asking Is there any way to create backup copy of a file, without type its name twice?

pa4080
  • 30,621
16

You can use find:

find . -max-depth 1 -name 'gmail-meta3*' -exec cp "{}" "{}.bak" \;

That will find in the current directory . all files with a name matching the glob pattern (mind the single quotes around the pattern to prevent shell globbing). For each file found, it will exec cp from name to name.bak. The \; at the end ensures it will do each file individually instead of passing all of them at once. The max depth as 1 only searches the cuurent directory instead of recursing down.

cbojar
  • 261
11

You can use a for loop with bash. Normally, I would just type it as a one-liner because this isn't a task I perform often:

for f in test* ; do cp -a "$f" "prefix-${f}.ext" ; done

However, if you need it as a script:

cps() {
   [ $# -lt 2 ] && echo "Usage: cps REGEXP FILES..." && return 1

   PATTERN="$1" ; shift

   for file in "$@" ; do
      file_dirname=`dirname "$file"`
      file_name=`basename "$file"`
      file_newname=`echo "$file_name" | sed "$PATTERN"`

      if [[ -f "$file" ]] && [[ ! -e "${file_dirname}/${file_newname}" ]] ; then
         cp -a "$file" "${file_dirname}/${file_newname}"
      else
         echo "Error: $file -> ${file_dirname}/${file_newname}"
      fi
   done
}

Usage is similar to rename. To test:

pushd /tmp
mkdir tmp2
touch tmp2/test{001..100}     # create test files
ls tmp2
cps 's@^@prefix-@ ; s@$@.bak@' tmp2/test*    # create backups
cps 's@$@.bak@' tmp2/test*    # more backups ... will display errors
ls tmp2
\rm -r tmp2                   # cleanup
popd
xiota
  • 5,038
6

The closest you will likely get to the DOS paradigm is mcp (from the mmv package):

mcp 'gmail-meta3*' 'gmail-meta3#1.bak'

If zsh is available, its contributed zmv module is perhaps a little closer:

autoload -U zmv

zmv -C '(gmail-meta3*)' '$1.bak'

I'd avoid ls regardless - a variant on your own answer that's safe for whitespace (including newlines) would be

printf '%s\0' gmail-meta3* | while IFS= read -r -d '' f; do cp -a -- "$f" "$f.bak"; done

or perhaps

printf '%s\0' gmail-meta3* | xargs -0 -I{} cp -a -- {} {}.bak
steeldriver
  • 142,475
5

rsync only solution

If you just want to backup your files, you can copy them to a new directory

rsync /path/to/dir/Filename* /path/to/backupdirectory

This will copy the Filename files from /path/to/dir/ to /path/to/backupdirectory.


rsync + filerename

If you want your backup files to have a suffix, things get hacky with rsync...

rsync -Iu /path/to/dir/Filename* /path/to/dir/Filename* -b --backup-dir=/path/to/backupdirectory --suffix=.bak

This would overwrite the existing files... with the existing files (-I) but only if they are (-u) newer (which they aren't) and creating a backup, with a suffix.

You can also do that in the same directory. But better exclude existing backups.

rsync -Iu /path/to/dir/Filename* /path/to/dir/Filename* -b --backup-dir=/path/to/backupdirectory --suffix=.bak --exclude '*.bak'

Robert Riedl
  • 4,401
4

I wrote this one-liner into my ~/.bashrc. Much better answers using find can be posted I suppose. Even better answers could be written in in C. Hopefully this Q&A gets the ball rolling for better answers:

cps () {
    # cps "Copy Splat", copy group of files to backup, ie "cps Filename .bak"
    # Copies Filename1 to Filename1.bak, Filename2 to Filename2.bak, etc.
    # If Filename1.bak exists, don't copy it to Filename1.bak.bak
    for f in "$1"*; do [[ ! "$f" == *"$2" ]] && cp -a "$f" "$f$2"; done

    # OLD version comments suggested to remove 
    # ls "$1"* | while read varname; do cp -a "$varname" "$varname$2"; done
}
  • for f in "$1"*; do : $1 is the gmail-meta3 parameter and f is the list of files matching. Combined this means for gmail-meta3, gmail-meta3-LAB-9999, etc. do the following
  • [[ ! "$f" == *"$2" ]] && : $f is the same as f above. $2 is the .bak parameter passed. Combined this means if the filename doesn't end in .bak (because we don't want to copy .bak and create .bak.bak) then do the following
  • cp -a "$f" "$f$2"; copy gmail-meta3 to gmail-meta3.bak, etc.
  • done : loop back and grab next filename in gmail-meta3* list.

cps gmail-meta3 .bak Sample Output

Using the question as an example here is how it looks in action:

───────────────────────────────────────────────────────────────────────────────────────────
rick@alien:~/gmail$ ll gmail-meta3*
-rw-rw-r-- 1 rick rick 26467821 May 20 16:43 gmail-meta3
-rw-rw-r-- 1 rick rick 26467821 May 20 16:43 gmail-meta3.bak
-rw-rw-r-- 1 rick rick      643 May 20 16:43 gmail-meta3-LAB-1558392194-26467821
-rw-rw-r-- 1 rick rick      643 May 20 16:43 gmail-meta3-LAB-1558392194-26467821.bak
-rw-rw-r-- 1 rick rick    49607 May 20 16:44 gmail-meta3-REC-1558392194-26467821
-rw-rw-r-- 1 rick rick    49607 May 20 16:44 gmail-meta3-REC-1558392194-26467821.bak
-rw-rw-r-- 1 rick rick   728954 Jun 27 17:04 gmail-meta3-YAD-1558392194-26467821
-rw-rw-r-- 1 rick rick   728954 Jun 27 05:46 gmail-meta3-YAD-1558392194-26467821.bak
───────────────────────────────────────────────────────────────────────────────────────────
rick@alien:~/gmail$ cps gmail-meta3 .bak
───────────────────────────────────────────────────────────────────────────────────────────
rick@alien:~/gmail$ ll gmail-meta3*
-rw-rw-r-- 1 rick rick 26467821 May 20 16:43 gmail-meta3
-rw-rw-r-- 1 rick rick 26467821 May 20 16:43 gmail-meta3.bak
-rw-rw-r-- 1 rick rick      643 May 20 16:43 gmail-meta3-LAB-1558392194-26467821
-rw-rw-r-- 1 rick rick      643 May 20 16:43 gmail-meta3-LAB-1558392194-26467821.bak
-rw-rw-r-- 1 rick rick    49607 May 20 16:44 gmail-meta3-REC-1558392194-26467821
-rw-rw-r-- 1 rick rick    49607 May 20 16:44 gmail-meta3-REC-1558392194-26467821.bak
-rw-rw-r-- 1 rick rick   728954 Jun 27 17:04 gmail-meta3-YAD-1558392194-26467821
-rw-rw-r-- 1 rick rick   728954 Jun 27 17:04 gmail-meta3-YAD-1558392194-26467821.bak
───────────────────────────────────────────────────────────────────────────────────────────
rick@alien:~/gmail$ 

Note: This uses the -a flag with the cp command to preserve timestamps and give you better grasp of your file backups.

Notice how the file copies have the exact same date and time as the originals. If the -a parameter was omitted they would be given the current date and time and wouldn't look like a true backup except that the file size would be the same.

4

This one should do as requested:

cps(){ p="${@: -1}"; for f in "${@:1:$#-1}"; do cp -ai "$f" "${p//\?/$f}"; done  }

Usage:

cps FILES... pattern
Example 1: cps gmail-meta3* ?.bak
Example 2: cps * save-?
Example 3: cps * bla-?-blubb

I chose ? because # must be quoted when it's the first character of pattern, otherwise it's recognized as the start of a comment.

Test:

$ touch 'test};{bla#?"blubb'
$ cps test* bla-?-blubb
$ ls
test};{bla#?"blubb  bla-test};{bla#?"blubb-blubb


Some earlier versions of the script for adding a suffix:

Similar to @WinEunuuchs2Unix answer, but I think more flexible and not parsing ls:

cps(){ S="$1"; shift; printf '%s\0' "$@" | xargs -0 -I{} cp -abfS "$S" {} {}; }

Put this in your .bashrc.

Usage:

cps SUFFIX FILES...
Example: cps .bak gmail-meta3*

Alternative, with the suffix as last argument (via and via):

cps(){ S="${@: -1}"; printf '%s\0' "${@:1:$#-1}" | xargs -0 -I{} cp -abfS "$S" {} {}; }

Usage:

cps FILES... SUFFIX
Example: cps gmail-meta3* .bak

pLumo
  • 27,991
2

Another method of achieving your requirement is to copy the files to a temporary directory and use the rename command to rename them.

$ mkdir backup
$ cp filename* /tmp/rename-backup/
$ rename 's/(filename.*)/$1.bak/' /tmp/rename-backup/*
$ mv /tmp/rename-backup/* ./

If you need it as a script, you can use it like so

cps () {
    mkdir -p /tmp/rename-backup/
    cp "$1"* /tmp/rename-backup/
    rename "s/($1.*)/\$1.$2/" /tmp/rename-backup/*
    mv "/tmp/rename-backup/$1"*".$2" .
}

And you can use it like so:

cps file bak

This is an example

$ ls -l
total 0
-rw-r--r--  1 danny  danny  0 Jun 26 16:23 file a
-rw-r--r--  1 danny  danny  0 Jun 26 16:23 file ab
-rw-r--r--  1 danny  danny  0 Jun 26 16:23 file ac
-rw-r--r--  1 danny  danny  0 Jun 26 16:05 filename1
-rw-r--r--  1 danny  danny  0 Jun 26 16:05 filename2
-rw-r--r--  1 danny  danny  0 Jun 26 16:05 filename3
-rw-r--r--  1 danny  danny  0 Jun 26 16:05 filename4
$ cps file bak
$ ls -l
total 0
-rw-r--r--  1 danny  danny  0 Jun 26 16:23 file a
-rw-r--r--  1 danny  danny  0 Jun 26 16:41 file a.bak
-rw-r--r--  1 danny  danny  0 Jun 26 16:23 file ab
-rw-r--r--  1 danny  danny  0 Jun 26 16:41 file ab.bak
-rw-r--r--  1 danny  danny  0 Jun 26 16:23 file ac
-rw-r--r--  1 danny  danny  0 Jun 26 16:41 file ac.bak
-rw-r--r--  1 danny  danny  0 Jun 26 16:05 filename1
-rw-r--r--  1 danny  danny  0 Jun 26 16:41 filename1.bak
-rw-r--r--  1 danny  danny  0 Jun 26 16:05 filename2
-rw-r--r--  1 danny  danny  0 Jun 26 16:41 filename2.bak
-rw-r--r--  1 danny  danny  0 Jun 26 16:05 filename3
-rw-r--r--  1 danny  wheel  0 Jun 26 16:41 filename3.bak
-rw-r--r--  1 danny  danny  0 Jun 26 16:05 filename4
-rw-r--r--  1 danny  danny  0 Jun 26 16:41 filename4.bak
Dan
  • 14,180