17

I have a lot of files in a folder, named like 00802_Bla_Aquarium_XXXXX.jpg. Now I need to copy every 4th file to a subfolder, saying in selected/.

00802_Bla_Aquarium_00020.jpg <= this one
00802_Bla_Aquarium_00021.jpg
00802_Bla_Aquarium_00022.jpg
00802_Bla_Aquarium_00023.jpg
00802_Bla_Aquarium_00024.jpg <= this one
00802_Bla_Aquarium_00025.jpg
00802_Bla_Aquarium_00026.jpg
00802_Bla_Aquarium_00027.jpg
00802_Bla_Aquarium_00028.jpg <= this one
00802_Bla_Aquarium_00029.jpg

How do I do this?

2
  • You might be able to do like the solutions to superuser.com/q/396536/87552 on some output of ls. See also stackoverflow.com/q/4553751/789593 which asks about every second line.
    – N.N.
    Dec 21, 2012 at 10:33
  • If they're numbered and you know the last number, you might consider a variation of for n in $(seq -w 20 4 200); do cp "00802_..._00${n}" ...; done Dec 21, 2012 at 11:25

8 Answers 8

13

With zsh, you could do:

n=0; cp 00802_Bla_Aquarium_?????.jpg(^e:'((n++%4))':) /some/place

POSIXly, same idea, just a bit more verbose:

# put the file list in the positional parameters ($1, $2...).
# the files are sorted in alphanumeric order by the shell globbing
set -- 00802_Bla_Aquarium_?????.jpg

n=0
# loop through the files, increasing a counter at each iteration.
for i do
  # every 4th iteration, append the current file to the end of the list
  [ "$(($n % 4))" -eq 0 ] && set -- "$@" "$i"

  # and pop the current file from the head of the list
  shift
  n=$(($n + 1))
done

# now "$@" contains the files that have been appended.
cp -- "$@" /some/place

Since those filenames don't contain any blank or wildcard characters, you could also do:

cp $(printf '%s\n' 00802_Bla_Aquarium_?????.jpg | awk 'NR%4 == 1') /some/place
2
  • Can you please put comment also in posix code Dec 21, 2012 at 10:47
  • @ChrisDown, for i do is standard and actually more portable than for i; do and is the canonical way to loop over the positional parameters. Dec 21, 2012 at 17:34
8

In bash, a funny possibility, that will work rather well here:

cp 00802_Bla_Aquarium_*{00..99..4}.jpg selected

That's definitely the shortest and most efficient answer: no subshell, no loop, no pipe, no awkward external process; just one fork to cp (that you can't avoid anyway) and one bash brace expansion and glob (that you can get rid of altogether since you know how many files you have).

3
  • 4
    … and 2 assumptions: files are numbered continuously and the first one is divisible with 4.
    – manatwork
    Dec 21, 2012 at 17:23
  • @manatwork no problem if first one is not divisible by 4: e.g., cp 00802_Bla_Aquarium_*{03..99..4}.jpg selected if you want 3 mod 4. (I'm using the wonderful fact that 100 is divisible by 4). Now, about continuously numbered files, all the other answers assume the same. Dec 21, 2012 at 17:26
  • Which other answers assume continuous numbering? I found only Ulrich Schwarz's comment and no such answer.
    – manatwork
    Dec 21, 2012 at 17:38
7

Simply with bash, you can do:

n=0
for file in ./*.jpg; do
   test $n -eq 0 && cp "$file" selected/
   n=$((n+1))
   n=$((n%4))
done

The pattern ./*.jpg will be replaced by an alphabetically sorted list of file names as stated by the bash man, so it should fit your purpose.

1
  • In bash, your three line inside the for loop can be replaced with just this line: (((++n)%4)) || cp "$file" selected/. ;-). The problem though is that you're forking on a cp for each file which is not really efficient. Dec 21, 2012 at 17:33
1

If you have ruby installed you can use the following one-liner. Note that it assumes that the directory selected exists.

ruby -rfileutils -e 'files = Dir.glob("*.jpg").each_slice(4) { |file| FileUtils.cp(file.first, "selected/" + file.first) }'

It takes a list of all files with the extension .jpg in the current directory and slices it into lists of four elements and copies the first element from each such list to the directory selected in the current directory.

1

If you know you will not have newlines in your file names, you could use:

find . -maxdepth 1 -name "*.jpg" | sort | while IFS= read -r file; do
  cp "$file" selected/
  IFS= read -r; IFS= read -r; IFS= read -r
done
1
  • @Gilles: thanks for the edit. I removed the file variable in the 3 read though, as I want to point out that they are not needed.
    – jfg956
    Dec 22, 2012 at 11:11
1

You can use a GUI file manager and resize the border of the window until every 5 files occupy a row, and then use mouse to select the first column...

0

This solution relies on text utils, but should work in less advanced shells too:

cp `ls * | awk 'NR % 4 == 0'` selected

Note that it may break if the number of files is too big. In that case I would go with slower xargs way:

ls * | awk 'NR % 4 == 0' | xargs -i cp {} selected
1
-1

Looking at stackoverflow.com/q/4553751/789593 which offers a solution to copy every second one as @N.N. mentioned, a very simple solution would be:

  • Remove the first file from the starting folder
  • Copy every second one from the starting folder to an intermediate folder
  • Copy every seconde one from the intermediate folder into the final folder
  • Add the first file manually

It may not be very efficient as it requires an extra copy step bit it should be very easy to do.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .