Solving "mv: Argument list too long"?

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 \"', 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/

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.


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