Résoudre «mv: liste d’arguments trop longue»?

64

J'ai un dossier avec plus d'un million de fichiers qui doit être trié, mais je ne peux rien faire car mvce message est affiché 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/
Dominique
la source

Réponses:

82

xargsest l'outil pour le travail. Que, ou findavec -exec … {} +. Ces outils exécutent une commande plusieurs fois, avec autant d'arguments que possible en une 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 mvest la destination. Avec les utilitaires GNU (c’est-à-dire sur Linux non intégré ou Cygwin), l’ -toption to mvest utile, pour transmettre la destination en premier.

Si les noms de fichier ne comportent ni espace ni espace \"', vous pouvez simplement fournir les noms de fichier comme entrée xargs(la echocommande est une commande intégrée de 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 l' -0option pour xargsutiliser une entrée délimitée par des valeurs NULL au lieu du format cité 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 rentrer dans les sous-répertoires, utilisez -type d -prune. Etant 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 de points, contrairement aux méthodes génériques du shell.)

Si vous n'avez pas d'utilitaire 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 mvpaquet intégré :

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

ou si vous préférez laisser mvet d'autres noms continuer à faire référence aux commandes externes:

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

ou avec des globs de style ksh:

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

Sinon, utilisez GNU mvet zargs:

autoload -U zargs
setopt extended_glob
zargs -- ./^*.(jpg|png|bmp) -- mv -t targetdir/
Gilles, arrête de faire le mal
la source
1
Les deux premières commandes ont renvoyé "-bash:!: Event not found" et les deux suivantes n'ont déplacé aucun fichier. Si vous êtes au courant, je suis sur CentOS 6.5
Dominique,
1
@ Dominique j'ai utilisé la même syntaxe de déplacement que celle que vous avez utilisée dans votre question. Vous devrez l' shopt -s extglobactiver. J'avais raté une étape dans les findcommandes, je les ai corrigées.
Gilles, arrête de faire le mal
Je reçois ceci avec la commande find "find: expression invalide; vous avez utilisé un opérateur binaire '-o' sans rien avant." Je vais maintenant essayer les autres.
Dominique
@Dominique Les findcommandes que j'ai postées (maintenant) fonctionnent. Vous devez avoir omis une partie lors du copier-coller.
Gilles 'SO- arrête d'être méchant'
Gilles, pour les commandes de trouver, pourquoi ne pas utiliser l'opérateur « non », !? C’est plus explicite et plus facile à comprendre que l’étrange fuite -o. Par exemple,! -name '*.jpg' -a ! -name '*.png' -a ! -name '*.bmp'
CivFan
13

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

ulimit -s 100000

cela fonctionnera car le noyau Linux a inclus un correctif il y a environ 10 ans qui modifiait la limite d'arguments 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

Mise à jour: Si vous vous sentez courageux, vous pouvez dire

ulimit -s unlimited

et toutes les extensions de shell vous conviendront aussi longtemps que vous aurez assez de RAM.

Mikko Rantalainen
la source
C'est un bidouillage. Comment sauriez-vous à quoi définir la limite de pile? Cela affecte également les autres processus démarrés dans la même session.
Kusalananda
1
Oui, c'est un bidouillage. La plupart du temps, ce type de piratage est ponctuel (combien de fois vous déplacez-vous manuellement une quantité de fichiers considérable?). Si vous êtes certain que le processus ne consommera pas toute votre mémoire vive, vous pouvez le définir ulimit -s unlimitedet cela fonctionnera pour des fichiers pratiquement illimités.
Mikko Rantalainen
La ulimit -s unlimitedlimite de ligne de commande réelle est de 2 ^ 31 ou 2 Go. ( MAX_ARG_STRLENsource du noyau.)
Mikko Rantalainen Le
9

La limite de dépassement d'argument du système d'exploitation ne s'applique pas aux extensions qui se produisent dans l'interpréteur de shell. Donc, en plus d'utiliser xargsou find, nous pouvons simplement utiliser une boucle shell pour décomposer le traitement en mvcommandes individuelles :

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

Ceci utilise uniquement les fonctionnalités et les utilitaires POSIX Shell Command Language. Cette ligne est plus claire avec l'indentation, avec les points-virgules inutiles supprimés:

for x in *; do
  case "$x" in
    *.jpg|*.png|*.bmp) 
       ;; # nothing
    *) # catch-all case
       mv -- "$x" target
       ;;
  esac
done
Kaz
la source
Avec plus d'un million de fichiers, cela générera plus d'un million de mvprocessus, au lieu des quelques nécessaires à l'aide de la findsolution POSIX @Gilles affichée. En d'autres termes, cette méthode génère beaucoup de temps processeur inutile.
CivFan
@CivFan Un autre problème est de vous convaincre que la version modifiée est équivalente à la version originale. Il est facile de voir que l' caseinstruction sur le résultat de l' *extension permettant de filtrer plusieurs extensions est équivalente à l' !(*.jpg|*.png|*.bmp)expression d' origine . La findréponse n’est en fait pas équivalente; il descend dans les sous-répertoires (je ne vois pas de -maxdepthprédicat).
Kaz
-name . -o -type d -prune -oprotège de la descente dans les sous-répertoires. -maxdepthn'est apparemment pas conforme à POSIX, bien que ce ne soit pas mentionné dans ma findpage de manuel.
CivFan
Rétrogradé à la révision 1. La question ne dit rien sur les variables source ou de destination, cela ajoute donc une cruauté inutile à la réponse.
Kaz
5

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

Augmentez la taille de MAX_ARG_PAGESquelque 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 mvou rmpour un million de fichiers ou quoi que vous fassiez. Recompiler, installer, redémarrer.

IL FAUT SE MÉFIER! Si vous définissez une taille trop grande pour la mémoire de votre système, exécutez une commande contenant de nombreux arguments Soyez extrêmement prudent lorsque vous utilisez des systèmes multi-utilisateurs, cela permettra 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 le moment.

Perkins
la source
5

Une solution plus simple utilisant à la "$origin"/!(*.jpg|*.png|*.bmp)place 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 ce qui ;précède avant que le donemessage ne 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

Ce qui ressemble à ceci si vous faites une indentation:

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

Cela prend chaque fichier de l’origine et les déplace un par un vers la destination. Les guillemets $filesont nécessaires au cas où des noms de fichiers contiennent des espaces ou d’autres caractères spéciaux.

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
Chat blanc
la source
Vous pouvez utiliser quelque chose comme le glob d'origine dans la boucle for pour obtenir une solution plus proche de ce qui est demandé.
Score_Under
Que veux-tu dire par Glob original?
Whitecat
Désolé si cela était un peu cryptique, je faisais référence à la glob dans la question: !(*.jpg|*.png|*.bmp). Vous pouvez ajouter cela à votre boucle for-line en effectuant une suppression "$origin"/!(*.jpg|*.png|*.bmp)pour éviter le recours au commutateur utilisé dans la réponse de Kaz et conserver le corps simple de la boucle-for.
Score_Under
Score génial. J'ai incorporé votre commentaire et mis à jour ma réponse.
Whitecat
3

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

import glob, shutil

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

Vous pouvez contourner cette restriction tout en continuant à l'utiliser mvsi 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 avez une longue liste de noms de fichiers alphanumériques.

mv ./subdir/a* ./

Ça marche. Puis assommer un autre gros morceau. Après quelques mouvements, vous pouvez simplement revenir à utilisermv ./subdir/* ./

Kristian
la source
0

Voici mes deux cents, ajouter 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/
Ako
la source