12

I've got a directory with numbered files, e.g. 1_foo.txt 2_bar.asc 13_test.png, and want to move them into individual directories (e.g. 1, 2 and 13) using as simple a bash command as possible.

Creating the numbered directories was easy:

mkdir $(seq 1 15)

I've also come up with a command to copy the files into their respective directories:

seq 15 -1 1 | xargs -I@ mv @_* @

That doesn't work, though, as the * is interpreted as a normal character when used with xargs, giving me errors like "mv: File '15_*' not found.".

Is there any easy way to use * as a wildcard in a command called by xargs?

2 Answers 2

30

You were very close.

seq 15 -1 1 | xargs -I@ sh -c 'mv @_* @'

You need to delay the interpretation (expansion) of the * until after the @ substitution has occurred.  (But you already understood that that was the problem, right?)

I’ve been advised never to embed an unknown filename (or other substitution string) directly into a shell command line.  The above example is probably fairly safe, because you know what the strings are going to be — 15, 14, …, 3, 2 and 1.  But using the above example as a template for more complex commands can be dangerous.  A safer arrangement would be

seq 15 -1 1 | xargs -I@ sh -c 'mv -- "$1"_* "$1"' x-sh @

where x-sh is a semi-arbitrary string that will be used to label any error messages issued by the invoked shell.  This is equivalent to my first example, except, rather than embedding the strings (represented by @) directly into the shell command, it injects them by supplying the @ as an argument to the shell, and then referencing them as "$1".


P.S. You suggested running the seq command in reverse (seq 15 -1 1, which generates 1514, …, 3, 2, 1 rather than 1, 2, 3, …, 14, 15) and nobody mentioned it.  This would be an important part of the answer if your filenames were like 1foo.txt, 2bar.asc, and 13test.png, etc. (with various characters appearing after the number, rather than always _).  In that case, the command would be mv "$1"* "$1" (without the _), and, if you did 1 first, then the command mv 1* 1 would sweep up all the 10quick*, 11brown*, 12fox*, etc., files, along with the 1foo* files.  But

seq 1 15 | xargs -I@ sh -c 'mv -- "$1"_* "$1"' x-sh @

should be safe.

P.P.S. The seq command is not specified by POSIX.  Neither is brace expansion in the shell.  A POSIX-compliant solution can be constructed by combining grawity’s answer to this question with this other answer by Adam Katz:

i=1
while [ "$i" -le 15 ]
do
    mv -- "${i}"_* "$i"
    i=$((i+1))
done

P.P.P.S. It’s not critical when you know that the file names begin with alphanumeric characters (i.e., letters and digits), but, in more general cases, you should use -- between the command name and the arguments.  This improves the handling of filenames that begin with a dash.  The -- tells the command to treat the argument (the file name) as an argument.  Without it, such an argument might be treated as an option string.

2
  • 1
    Thanks, that worked perfectly. I'll vote your answer up as soon as I've got enough reputation to do that.
    – n.st
    Commented Dec 13, 2012 at 19:48
  • +1 for an answer that goes further and considers not only number sequences, like presented by the OP. I've got a similar problem: however, I have common names between dirs and files (with several different extensions). Smart solution! Commented Jul 11, 2020 at 10:50
7

Just don't use xargs for that. Use a for loop:

for i in $(seq 1 15); do
    mv ${i}_* $i
done

Even better is to use brace expansion instead of seq:

mkdir {1..15}

for i in {1..15}; do
    mv ${i}_* $i
done
1
  • You're right, that's definitely more elegant than using xargs. Thanks for telling me about brace expansion.
    – n.st
    Commented Dec 13, 2012 at 19:50

You must log in to answer this question.

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