web-dev-qa-db-fra.com

Résoudre "mv: Liste d'arguments trop longue"?

J'ai un dossier contenant plus d'un million de fichiers qui doit être trié, mais je ne peux rien faire car mv affiche ce message tout le temps

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

J'utilise cette commande pour déplacer des fichiers sans extension:

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

xargs est l'outil pour le travail. Cela, ou find avec -exec … {} +. Ces outils exécutent une commande plusieurs fois, avec autant d'arguments que possible en une seule fois.

Les deux méthodes sont plus faciles à exécuter lorsque la liste des arguments variables est à la fin, ce qui n'est pas le cas ici: l'argument final de mv est la destination. Avec GNU utilitaires (c'est-à-dire sur Linux non intégré ou Cygwin), le -t option à mv est utile, pour passer la destination en premier.

Si les noms de fichier ne contiennent ni espace ni \"', vous pouvez simplement fournir les noms de fichiers en entrée dans xargs (la commande echo est une commande bash, elle n'est donc pas soumise à la limite de longueur de ligne de commande):

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

Vous pouvez utiliser le -0 option à xargs pour utiliser une entrée délimitée par des valeurs nulles au lieu du format entre guillemets par défaut.

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

Alternativement, vous pouvez générer la liste des noms de fichiers avec find. Pour éviter de revenir dans des sous-répertoires, utilisez -type d -Prune. Étant donné qu'aucune action n'est spécifiée pour les fichiers image répertoriés, seuls les autres fichiers sont déplacés.

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

(Cela inclut les fichiers à points, contrairement aux méthodes de caractères génériques Shell.)

Si vous ne disposez pas des utilitaires GNU, vous pouvez utiliser un shell intermédiaire pour obtenir les arguments dans le bon ordre. Cette méthode fonctionne sur tous les systèmes POSIX.

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

Dans zsh, vous pouvez charger le mv builtin :

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

ou si vous préférez laisser mv et d'autres noms continuer à se référer aux commandes externes:

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

ou avec des globes de style ksh:

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

Alternativement, en utilisant GNU mv et zargs :

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

Si travailler avec le noyau Linux est suffisant, vous pouvez simplement le faire

ulimit -S -s unlimited

Cela fonctionnera car le noyau Linux a inclus il y a environ 10 ans un correctif qui a modifié la limite d'argument pour qu'elle soit basée sur la taille de la pile: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/ linux.git/commit /? id = b6a2fea39318e43fee84fa7b0b90d68bed92d2ba

Si vous ne voulez pas d'espace de pile illimité, vous pouvez dire par exemple.

ulimit -S -s 100000

pour limiter la pile à 100 Mo. Notez que vous devez définir l'espace de pile sur une utilisation normale de la pile (généralement 8 Mo) plus la taille de la ligne de commande que vous souhaitez utiliser.

Vous pouvez interroger la limite réelle comme suit:

getconf ARG_MAX

qui affichera la longueur maximale de la ligne de commande en octets. Par exemple, les paramètres par défaut d'Ubuntu définissent ce paramètre sur 2097152 ce qui signifie environ 2 Mo. Si je cours avec une pile illimitée, j'obtiens 4611686018427387903 qui est exactement 2 ^ 62 ou environ 46000 To. Si votre ligne de commande dépasse , je m'attends à ce que vous puissiez contourner le problème par vous-même.

16

La limite de passage d'arguments du système d'exploitation ne s'applique pas aux extensions qui se produisent dans l'interpréteur Shell. Ainsi, en plus d'utiliser xargs ou find, nous pouvons simplement utiliser une boucle Shell pour diviser le traitement en commandes individuelles mv:

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

Cela utilise uniquement les fonctionnalités et utilitaires du langage de commande POSIX Shell. Cette doublure est plus claire avec une indentation, avec des points-virgules inutiles supprimés:

for x in *; do
  case "$x" in
    *.jpg|*.png|*.bmp) 
       ;; # nothing
    *) # catch-all case
       mv -- "$x" target
       ;;
  esac
done
9
Kaz

Parfois, il est plus facile d'écrire simplement un petit script, par exemple en Python:

import glob, shutil

for i in glob.glob('*.jpg'):
  shutil.move(i, 'new_dir/' + i)
5
duhaime

Pour une solution plus agressive que celles précédemment proposées, extrayez la source de votre noyau et modifiez include/linux/binfmts.h

Augmentez la taille de MAX_ARG_PAGES à quelque chose de plus grand que 32. Cela augmente la quantité de mémoire que le noyau autorisera pour les arguments du programme, vous permettant ainsi de spécifier votre commande mv ou rm pour un million de fichiers ou tout ce que vous voulez ' re faisant. Recompilez, installez, redémarrez.

IL FAUT SE MÉFIER! Si vous définissez cette valeur trop grande pour votre mémoire système, puis exécutez une commande avec beaucoup d'arguments, MAUVAIS CHOSES ARRIVERONT! Soyez extrêmement prudent en faisant cela sur des systèmes multi-utilisateurs, cela permet aux utilisateurs malveillants d'utiliser plus facilement toute votre mémoire!

Si vous ne savez pas comment recompiler et réinstaller votre noyau manuellement, il est probablement préférable de prétendre que cette réponse n'existe pas pour l'instant.

5
Perkins

Une solution plus simple utilisant "$Origin"/!(*.jpg|*.png|*.bmp) au lieu d'un bloc catch:

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

Merci à @Score_Under

Pour un script multiligne, vous pouvez effectuer les opérations suivantes (notez le ; avant que le done soit supprimé):

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

Pour faire une solution plus généralisée qui déplace tous les fichiers, vous pouvez faire le one-liner:

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

Qui ressemble à ceci si vous faites une indentation:

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

Cela prend chaque fichier dans l'origine et les déplace un par un vers la destination. Les citations autour de $file sont nécessaires en cas d'espaces ou d'autres caractères spéciaux dans les noms de fichiers.

Voici un exemple de cette méthode qui a parfaitement fonctionné

for file in "/Users/william/Pictures/export_folder_111210/"*.jpg; do
    mv -- "$file" "/Users/william/Desktop/southland/landingphotos/";
done
5
Whitecat

Vous pouvez contourner cette restriction tout en utilisant mv si cela ne vous dérange pas de l'exécuter plusieurs fois.

Vous pouvez déplacer des portions à la fois. Disons par exemple que vous aviez une longue liste de noms de fichiers alphanumériques.

mv ./subdir/a* ./

Ça marche. Puis éliminez un autre gros morceau. Après quelques mouvements, vous pouvez simplement recommencer à utiliser mv ./subdir/* ./

1
Kristian

Voici mes deux cents, ajoutez ceci dans .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/
0
Ako