131

I have a folder with more than a million files that needs sorting, but I can't really do anything because mv outputs this message all the time

-bash: /bin/mv: Argument list too long

I'm using this command to move extension-less files:

mv -- !(*.jpg|*.png|*.bmp) targetdir/
2

9 Answers 9

137

xargs is the tool for the job. That, or find with -exec … {} +. These tools run a command several times, with as many arguments as can be passed in one go.

Both methods are easier to carry out when the variable argument list is at the end, which isn't the case here: the final argument to mv is the destination. With GNU utilities (i.e. on non-embedded Linux or Cygwin), the -t option to mv is useful, to pass the destination first.

If the file names have no whitespace nor any of \"' and don't start with -¹, then you can simply provide the file names as input to xargs (the echo command is a bash builtin, so it isn't subject to the command line length limit; if you see !: event not found, you need to enable globbing syntax with shopt -s extglob):

echo !(*.jpg|*.png|*.bmp) | xargs mv -t targetdir --

You can use the -0 option to xargs to use null-delimited input instead of the default quoted format.

printf '%s\0' !(*.jpg|*.png|*.bmp) | xargs -0 mv -t targetdir --

Alternatively, you can generate the list of file names with find. To avoid recursing into subdirectories, use -type d -prune. Since no action is specified for the listed image files, only the other files are moved.

find . -name . -o -type d -prune -o \
       -name '*.jpg' -o -name '*.png' -o -name '*.bmp' -o \
       -exec mv -t targetdir/ {} +

(This includes dot files, unlike the shell wildcard methods.)

If you don't have GNU utilities, you can use an intermediate shell to get the arguments in the right order. This method works on all POSIX systems.

find . -name . -o -type d -prune -o \
       -name '*.jpg' -o -name '*.png' -o -name '*.bmp' -o \
       -exec sh -c 'mv "$@" "$0"' targetdir/ {} +

In zsh, you can load the mv builtin:

setopt extended_glob
zmodload zsh/files
mv -- ^*.(jpg|png|bmp) targetdir/

or if you prefer to let mv and other names keep referring to the external commands:

setopt extended_glob
zmodload -Fm zsh/files b:zf_\*
zf_mv -- ^*.(jpg|png|bmp) targetdir/

or with ksh-style globs:

setopt ksh_glob
zmodload -Fm zsh/files b:zf_\*
zf_mv -- !(*.jpg|*.png|*.bmp) targetdir/

Alternatively, using GNU mv and zargs:

autoload -U zargs
setopt extended_glob
zargs -- ./^*.(jpg|png|bmp) -- mv -t targetdir/ --

¹ with some xargs implementations, file names must also be valid text in the current locale. Some would also consider a file named _ as indicating the end of input (can be avoided with -E '')

13
  • 2
    The first two commands returned "-bash: !: event not found" and the next two did not move any files at all. I'm on CentOS 6.5 if you should know
    – Dominique
    Commented May 9, 2014 at 1:44
  • 1
    @Dominique I used the same globbing syntax that you used in your question. You'll need shopt -s extglob to enable it. I'd missed a step in the find commands, I've fixed them. Commented May 9, 2014 at 1:50
  • I'm getting this with the find command "find: invalid expression; you have used a binary operator '-o' with nothing before it." I will now try the other ones.
    – Dominique
    Commented May 9, 2014 at 1:56
  • @Dominique The find commands I've posted (now) work. You must have left off a part when copy-pasting. Commented May 9, 2014 at 1:58
  • Gilles, for the find commands, why not use the "not" operator, !? It's more explicit and easier to understand than the odd trailing -o. For example, ! -name '*.jpg' -a ! -name '*.png' -a ! -name '*.bmp'
    – CivFan
    Commented Oct 23, 2015 at 21:15
52

If working with Linux kernel is enough you can simply do

ulimit -S -s unlimited

That will work because Linux kernel included a patch around 10 years ago that changed argument limit to be based on stack size: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=b6a2fea39318e43fee84fa7b0b90d68bed92d2ba

If you don't want unlimited stack space, you can say e.g.

ulimit -S -s 100000

to limit the stack to 100MB. Note that you need to set stack space to normal stack usage (usually 8 MB) plus the size of the command line you would want to use.

You can query actual limit as follows:

getconf ARG_MAX

that will output the maximum command line length in bytes. For example, Ubuntu defaults set this to 2097152 which means roughly 2 MB. If I run with unlimited stack I get 4611686018427387903 which is exactly 2^62 or about 46000 TB. If your command line exceeds that, I expect you to be able to workaround the issue by yourself.

Note that if you use sudo as in sudo mv *.dat somewhere/. running ulimit cannot fix that issue because sudo resets the stack size before executing the mv for real. To workaround that, you have to start root shell with sudo -s, then run ulimit -S -s unlimited and finally run the command without sudo in that root shell.

12
  • 1
    That's a hack. How would you know what to set the stack limit to? This also affects other processes started in the same session.
    – Kusalananda
    Commented Nov 1, 2017 at 9:38
  • 3
    Yeah, it's a hack. Most of the time this kind of hacks are one-off (how often you manually move huge amount of files anyway?). If you are sure that the process is not going to eat all your RAM, you can set ulimit -s unlimited and it will work for practically unlimited files. Commented Nov 3, 2017 at 13:00
  • 1
    With ulimit -s unlimited the actual command line limit is 2^31 or 2 GB. (MAX_ARG_STRLEN in kernel source.) Commented Jun 8, 2018 at 7:01
  • 1
    I have to add that I don't understand why Linux even limits the command line length by default. There's zero technical reason to do because the user process can still waste all the same memory at will so this is not suitable for resource usage limitations either. Commented Feb 5, 2021 at 13:34
  • 1
    @Kusalananda not necessarily. You can set ulimit just for a subshell: (ulimit -S -s unlimited; mv ...). MAX_ARG_STRLEN refers to the maximum length of a single command line argument, and it's much smaller than 2^31 (it's actually 32 pages => 128kb - 2Mb)
    – user313992
    Commented Nov 1, 2021 at 18:43
19

Sometimes it's easiest to just write a little script, e.g. in Python:

import glob, shutil

for i in glob.glob('*.jpg'):
  shutil.move(i, 'new_dir/' + i)
2
  • 1
    after accidentally deleting 10% of the files with a botched shell cmd (the find -exec mv method), this method was the best because I could easily insert some logic and print() statements to make sure it's doing what you want. For example, print each file it's moving, and stop after 10 files as a sanity check, before committing the whole dir.
    – Demis
    Commented Oct 19, 2022 at 6:17
  • I prefer using pathlib's iterdir to specify an input directory, and then iterdir returns Path objects so you can conveniently access file.name or file.stem. Commented Aug 17, 2023 at 0:28
16

Try this:

find currentdir -name '*.*' -exec mv {} targetdir \;
  • find: search a folder
  • -name: match a desired criteria
  • -exec: run the command that follows
  • {}: insert the filename found
  • \;: mark the end of the exec command
5
  • 5
    Adding -maxdepth 1 before -exec helped with moving files to a subdirectory, to avoid matching them again once they'd been moved.
    – Alf Eaton
    Commented Jun 16, 2020 at 21:20
  • This answer format is very considerate: the example is followed with one-liner explanations of the find command arguments. This format accelerates the learning process
    – gatorback
    Commented Jul 2, 2022 at 13:06
  • Turns out if the targetdir doesn't exist (was in wrong cd), it'll rename the file targetdir, then move to the next file and rename that file targetdir - apparently overwriting the previous file. In this way it sequentially overwrote and deleted each file...
    – Demis
    Commented Oct 19, 2022 at 5:59
  • find: -exec: no terminating ";" or "+"
    – geoidesic
    Commented Jan 4, 2023 at 17:48
  • Here it is with Alf's suggestion: find currentdir -name '*.*' -maxdepth 1 -exec mv {} targetdir \;
    – pfrank
    Commented Jan 12, 2023 at 1:34
12

The operating system's argument passing limit does not apply to expansions which happen within the shell interpreter. So in addition to using xargs or find, we can simply use a shell loop to break up the processing into individual mv commands:

for x in *; do case "$x" in *.jpg|*.png|*.bmp) ;; *) mv -- "$x" target ;; esac ; done

This uses only POSIX Shell Command Language features and utilities. This one-liner is clearer with indentation, with unnecessary semicolons removed:

for x in *; do
  case "$x" in
    *.jpg|*.png|*.bmp) 
       ;; # nothing
    *) # catch-all case
       mv -- "$x" target
       ;;
  esac
done
4
  • 3
    With more than a million files, this will in turn spawn more than a million mv processes, instead of just the few needed using the POSIX find solution @Gilles posted. In other words, this way results in lots of unnecessary CPU churn.
    – CivFan
    Commented Oct 23, 2015 at 20:48
  • @CivFan Another problem is convincing yourself that the modified version is equivalent to the original. It's easy to see that the case statement on the result of * expansion to filter out several extensions is equivalent to the original !(*.jpg|*.png|*.bmp) expression. The find answer is in fact not equivalent; it descends into subdirectories (I don't see a -maxdepth predicate).
    – Kaz
    Commented Oct 23, 2015 at 20:51
  • -name . -o -type d -prune -o protects from descending into sub-directories. -maxdepth is apparently not POSIX compliant, though that's not mentioned in my find man page.
    – CivFan
    Commented Oct 23, 2015 at 21:33
  • Rolled back to revision 1. The question doesn't say anything about source or destination variables, so this adds unnecessary cruft to the answer.
    – Kaz
    Commented Oct 24, 2015 at 0:29
8

A more simple solution using "$origin"/!(*.jpg|*.png|*.bmp) instead of a catch block:

for file in "$origin"/!(*.jpg|*.png|*.bmp); do mv -- "$file" "$destination" ; done

Thanks to @Score_Under

For a multi-line script you can do the following (notice the ; before the done is dropped):

for file in "$origin"/!(*.jpg|*.png|*.bmp); do        # don't copy types *.jpg|*.png|*.bmp
    mv -- "$file" "$destination" 
done 

To do a more generalized solution that moves all files, you can do the one-liner:

for file in "$origin"/*; do mv -- "$file" "$destination" ; done

Which looks like this if you do indentation:

for file in "$origin"/*; do
    mv -- "$file" "$destination"
done 

This takes every file in the origin and moves them one by one to the destination. The quotes around $file are necessary in case there are spaces or other special characters in the filenames.

Here is an example of this method that worked perfectly

for file in "/Users/william/Pictures/export_folder_111210/"*.jpg; do
    mv -- "$file" "/Users/william/Desktop/southland/landingphotos/";
done
4
  • You could use something like the original glob in the for-loop to get a closer solution to what's being asked for. Commented Jun 20, 2015 at 0:34
  • What do you mean original glob?
    – Whitecat
    Commented Jun 20, 2015 at 0:38
  • Sorry if that was a little cryptic, I was referring to the glob in the question: !(*.jpg|*.png|*.bmp). You could add that to your for-loop by globbing "$origin"/!(*.jpg|*.png|*.bmp) which would avoid the need for the switch used in Kaz's answer and keep the simple body of the for-loop. Commented Jun 21, 2015 at 3:52
  • Awesome Score. I incorporated your comment and updated my answer.
    – Whitecat
    Commented Jun 27, 2015 at 22:55
5

For a more aggressive solution than those previously offered, pull up your kernel source and edit include/linux/binfmts.h

Increase the size of MAX_ARG_PAGES to something larger than 32. This increases the amount of memory the kernel will allow for program arguments, thereby allowing you to specify your mv or rm command for a million files or whatever you're doing. Recompile, install, reboot.

BEWARE! If you set this too large for your system memory, and then run a command with a lot of arguments BAD THINGS WILL HAPPEN! Be extremely cautious doing this to multi-user systems, it makes it easier for malicious users to use up all your memory!

If you don't know how to recompile and reinstall your kernel manually, it's probably best that you just pretend this answer doesn't exist for now.

1

You can get around that restriction while still using mv if you don't mind running it a couple times.

You can move portions at a time. Let's say for example you had a long list of alphanumeric file names.

mv ./subdir/a* ./

That works. Then knock out another big chunk. After a couple moves, you can just go back to using mv ./subdir/* ./

0

Here is my two cents, append this into .bash_profile

mv() {
  if [[ -d $1 ]]; then #directory mv
    /bin/mv $1 $2
  elif [[ -f $1 ]]; then #file mv
    /bin/mv $1 $2
  else
    for f in $1
    do
      source_path=$f
      #echo $source_path
      source_file=${source_path##*/}
      #echo $source_file
      destination_path=${2%/} #get rid of trailing forward slash

      echo "Moving $f to $destination_path/$source_file"

      /bin/mv $f $destination_path/$source_file
    done
  fi
}
export -f mv

Usage

mv '*.jpg' ./destination/
mv '/path/*' ./destination/

You must log in to answer this question.

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