Lösen von "mv: Argumentliste zu lang"?

64

Ich habe einen Ordner mit mehr als einer Million Dateien, die sortiert werden müssen, aber ich kann nichts wirklich tun, weil mvdiese Meldung die ganze Zeit ausgegeben wird

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

Ich verwende diesen Befehl, um Dateien ohne Erweiterung zu verschieben:

mv -- !(*.jpg|*.png|*.bmp) targetdir/
Dominique
quelle
Verwandte Themen
Codeforester

Antworten:

82

xargsist das Werkzeug für den Job. Das oder findmit -exec … {} +. Diese Tools führen einen Befehl mehrmals mit so vielen Argumenten aus, wie auf einmal übergeben werden können.

Beide Methoden sind einfacher durchzuführen, wenn die Liste der variablen Argumente am Ende ist, was hier nicht der Fall ist: Das letzte Argument mvist das Ziel. Bei GNU-Dienstprogrammen (z. B. unter nicht eingebettetem Linux oder Cygwin) ist die -tOption to mvnützlich, um das Ziel zuerst zu übergeben.

Wenn die Dateinamen weder Leerzeichen noch Leerzeichen enthalten \"', können Sie die Dateinamen einfach als Eingabe für angeben xargs(der echoBefehl ist eine integrierte Bash, daher unterliegt er nicht der Begrenzung der Befehlszeilenlänge):

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

Sie können die -0Option verwenden, xargsum eine durch Nullen getrennte Eingabe anstelle des in Anführungszeichen gesetzten Standardformats zu verwenden.

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

Alternativ können Sie die Liste der Dateinamen mit erzeugen find. Verwenden Sie -type d -prune. Da für die aufgelisteten Bilddateien keine Aktion angegeben ist, werden nur die anderen Dateien verschoben.

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

(Dies schließt im Gegensatz zu den Shell-Platzhaltermethoden Punktdateien ein.)

Wenn Sie keine GNU-Dienstprogramme haben, können Sie eine Zwischen-Shell verwenden, um die Argumente in der richtigen Reihenfolge abzurufen. Diese Methode funktioniert auf allen POSIX-Systemen.

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

In zsh können Sie das mveingebaute laden :

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

oder wenn Sie es vorziehen zu lassen mvund andere Namen weiterhin auf die externen Befehle verweisen:

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

oder mit ksh-style globs:

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

Alternativ mit GNU mvund zargs:

autoload -U zargs
setopt extended_glob
zargs -- ./^*.(jpg|png|bmp) -- mv -t targetdir/
Gilles 'SO - hör auf böse zu sein'
quelle
1
Die ersten beiden Befehle haben "-bash:!: Event not found" zurückgegeben und die nächsten beiden haben überhaupt keine Dateien verschoben. Ich bin auf CentOS 6.5, wenn Sie es wissen sollten
Dominique
1
@Dominique Ich habe dieselbe Globbing-Syntax verwendet, die Sie in Ihrer Frage verwendet haben. Sie müssen shopt -s extglobes aktivieren. Ich hatte einen Schritt in den findBefehlen verpasst , ich habe sie behoben.
Gilles 'SO- hör auf böse zu sein'
Ich erhalte dies mit dem find-Befehl "find: ungültiger Ausdruck; Sie haben einen binären Operator '-o' mit nichts davor verwendet." Ich werde jetzt die anderen ausprobieren.
Dominique
@Dominique Die findBefehle, die ich (jetzt) ​​gepostet habe, funktionieren. Sie müssen beim Kopieren und Einfügen einen Teil weggelassen haben.
Gilles 'SO- hör auf böse zu sein'
Gilles, warum nicht den Operator "nicht" für die Suchbefehle verwenden !? Es ist expliziter und einfacher zu verstehen als das ungerade Nachlaufen -o. Zum Beispiel! -name '*.jpg' -a ! -name '*.png' -a ! -name '*.bmp'
CivFan
13

Wenn die Arbeit mit dem Linux-Kernel ausreicht, können Sie dies einfach tun

ulimit -s 100000

Das wird funktionieren, da der Linux-Kernel vor ungefähr 10 Jahren einen Patch enthielt, der das Argument-Limit so änderte, dass es auf der Stack-Größe basierte: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/ commit /? id = b6a2fea39318e43fee84fa7b0b90d68bed92d2ba

Update: Wenn Sie mutig sind, können Sie sagen

ulimit -s unlimited

und Sie werden mit allen Shell-Erweiterungen in Ordnung sein, solange Sie genug RAM haben.

Mikko Rantalainen
quelle
Das ist ein Hack. Woher wissen Sie, auf was Sie das Stack-Limit einstellen sollen? Dies betrifft auch andere Prozesse, die in derselben Sitzung gestartet wurden.
Kusalananda
1
Ja, es ist ein Hack. Meistens handelt es sich bei solchen Hacks um einmalige Vorgänge (wie oft verschieben Sie ohnehin eine große Anzahl von Dateien manuell?). Wenn Sie sicher sind, dass der Prozess nicht Ihren gesamten Arbeitsspeicher beansprucht, können Sie festlegen ulimit -s unlimited, dass er für praktisch unbegrenzte Dateien funktioniert.
Mikko Rantalainen
Bei ulimit -s unlimitedder eigentlichen Kommandozeile liegt das Limit bei 2 ^ 31 oder 2 GB. ( MAX_ARG_STRLENIn Kernel-Quelle.)
Mikko Rantalainen
9

Das Argument-Übergabe-Limit des Betriebssystems gilt nicht für Erweiterungen, die im Shell-Interpreter auftreten. Zusätzlich zur Verwendung von xargsoder findkönnen wir also einfach eine Shell-Schleife verwenden, um die Verarbeitung in einzelne mvBefehle aufzuteilen:

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

Hierbei werden nur die Funktionen und Dienstprogramme der POSIX Shell Command Language verwendet. Dieser Einzeiler wird durch Einrückung klarer, wobei unnötige Semikolons entfernt werden:

for x in *; do
  case "$x" in
    *.jpg|*.png|*.bmp) 
       ;; # nothing
    *) # catch-all case
       mv -- "$x" target
       ;;
  esac
done
Kaz
quelle
Mit mehr als einer Million Dateien werden mehr als eine Million mvProzesse erzeugt, anstatt nur der wenigen, die mit der POSIX- findLösung @Gilles posted benötigt werden . Mit anderen Worten, dies führt zu einer Menge unnötiger CPU-Abwanderung.
CivFan
@CivFan Ein weiteres Problem ist, dass Sie sich davon überzeugen, dass die geänderte Version dem Original entspricht. Es ist leicht zu erkennen, dass die caseAussage über das Ergebnis der *Erweiterung zum Herausfiltern mehrerer Erweiterungen dem ursprünglichen !(*.jpg|*.png|*.bmp)Ausdruck entspricht. Die findAntwort ist in der Tat nicht gleichwertig; es steigt in Unterverzeichnisse ab (ich sehe kein -maxdepthPrädikat).
Kaz
-name . -o -type d -prune -oschützt vor dem Abstieg in Unterverzeichnisse. -maxdepthist anscheinend nicht POSIX-konform, obwohl das in meiner findManpage nicht erwähnt ist .
CivFan
Zurücksetzen auf Revision 1. Die Frage sagt nichts über Quell- oder Zielvariablen aus, daher wird die Antwort unnötig erweitert.
Kaz
5

Um eine aggressivere Lösung als die zuvor angebotenen zu erhalten, rufen Sie Ihre Kernelquelle auf und bearbeiten Sie sie include/linux/binfmts.h

Erhöhen Sie die Größe MAX_ARG_PAGESauf etwas größer als 32. Dies erhöht die Speicherkapazität, die der Kernel für Programmargumente zur Verfügung stellt, wodurch Sie Ihren Befehl mvoder rmfür eine Million Dateien oder was auch immer Sie tun , angeben können . Neu kompilieren, installieren, neu starten.

IN ACHT NEHMEN! Wenn Sie dies für Ihren Systemspeicher zu groß einstellen und dann einen Befehl mit vielen Argumenten ausführen, PASSIEREN SCHLECHTE DINGE! Gehen Sie dabei bei Mehrbenutzersystemen äußerst vorsichtig vor. Dies erleichtert böswilligen Benutzern den Verbrauch Ihres gesamten Arbeitsspeichers!

Wenn Sie nicht wissen, wie Sie Ihren Kernel manuell neu kompilieren und neu installieren können, tun Sie am besten einfach so, als gäbe es diese Antwort vorerst nicht.

Perkins
quelle
5

Eine einfachere Lösung "$origin"/!(*.jpg|*.png|*.bmp)anstelle eines catch-Blocks:

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

Vielen Dank an @Score_Under

Für ein mehrzeiliges Skript können Sie Folgendes tun (beachten Sie, ;bevor das donegelöscht wird):

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

Um eine allgemeinere Lösung zu erstellen, bei der alle Dateien verschoben werden, können Sie den Einzeiler verwenden:

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

Was so aussieht, wenn Sie Einrückungen machen:

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

Dadurch werden alle Dateien im Ursprung einzeln zum Ziel verschoben. Die Anführungszeichen $filesind erforderlich, falls die Dateinamen Leerzeichen oder andere Sonderzeichen enthalten.

Hier ist ein Beispiel für diese Methode, die perfekt funktioniert hat

for file in "/Users/william/Pictures/export_folder_111210/"*.jpg; do
    mv -- "$file" "/Users/william/Desktop/southland/landingphotos/";
done
Weiße Katze
quelle
Sie können so etwas wie das ursprüngliche Glob in der for-Schleife verwenden, um eine genauere Lösung für die Anforderungen zu erhalten.
Score_Under
Was meinst du mit Original Glob?
Whitecat
Sorry , wenn das ein wenig kryptisch war, war ich in der Frage der glob verweisen: !(*.jpg|*.png|*.bmp). Sie können dies zu Ihrer for-Schleife hinzufügen, indem Sie ein Globe einfügen, "$origin"/!(*.jpg|*.png|*.bmp)wodurch der in Kaz 'Antwort verwendete Schalter überflüssig wird und der einfache Körper der for-Schleife erhalten bleibt.
Score_Under
Ehrfürchtiges Ergebnis. Ich habe Ihren Kommentar aufgenommen und meine Antwort aktualisiert.
Whitecat
3

Manchmal ist es am einfachsten, ein kleines Skript zu schreiben, zB in Python:

import glob, shutil

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

Sie können diese Einschränkung umgehen, während Sie sie noch verwenden, mvwenn es Ihnen nichts ausmacht, sie ein paar Mal auszuführen.

Sie können Teile gleichzeitig verschieben. Angenommen, Sie hatten eine lange Liste alphanumerischer Dateinamen.

mv ./subdir/a* ./

Das funktioniert. Dann knock out noch ein großes Stück. Nach ein paar Zügen können Sie einfach wieder verwendenmv ./subdir/* ./

Kristian
quelle
0

Hier sind meine zwei Cent, fügen Sie dies in .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

Verwendungszweck

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