การแก้“ mv: รายการอาร์กิวเมนต์ยาวเกินไป” หรือไม่


64

ฉันมีโฟลเดอร์ที่มีไฟล์มากกว่าล้านไฟล์ที่ต้องการเรียงลำดับ แต่ฉันไม่สามารถทำอะไรได้เลยเพราะmvแสดงข้อความนี้ตลอดเวลา

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

ฉันใช้คำสั่งนี้เพื่อย้ายไฟล์ส่วนขยายที่น้อยกว่า:

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

คำตอบ:


82

xargsเป็นเครื่องมือสำหรับงาน นั้นหรือกับfind -exec … {} +เครื่องมือเหล่านี้เรียกใช้คำสั่งหลายครั้งโดยมีอาร์กิวเมนต์มากที่สุดเท่าที่สามารถส่งผ่านได้ในครั้งเดียว

วิธีการทั้งสองจะง่ายต่อการดำเนินการเมื่อรายการอาร์กิวเมนต์ตัวแปรอยู่ที่ท้ายซึ่งไม่ใช่กรณีที่นี่: อาร์กิวเมนต์สุดท้ายที่mvเป็นปลายทาง ด้วยโปรแกรมอรรถประโยชน์ของ GNU (เช่นบน Linux หรือ Cygwin ที่ไม่ได้ฝังตัว) -tตัวเลือกที่mvมีประโยชน์คือการส่งผ่านปลายทางก่อน

หากชื่อไฟล์ไม่มีช่องว่างหรือส่วนใด ๆ\"'คุณสามารถระบุชื่อไฟล์เป็นอินพุตxargs( echoคำสั่งคือ bash builtin ดังนั้นจึงไม่ขึ้นอยู่กับขีดจำกัดความยาวบรรทัดคำสั่ง):

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

คุณสามารถใช้-0ตัวเลือกเพื่อxargsใช้อินพุตที่คั่นด้วย null แทนรูปแบบที่ยกมาเป็นค่าเริ่มต้น

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

findหรือคุณสามารถสร้างรายชื่อไฟล์ที่มี เพื่อหลีกเลี่ยงการเข้าไปในไดเรกทอรีย่อย recursing -type d -pruneใช้ เนื่องจากไม่ได้ระบุการดำเนินการสำหรับไฟล์รูปภาพที่แสดงรายการเฉพาะไฟล์อื่นเท่านั้นที่จะถูกย้าย

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

(ซึ่งรวมถึงไฟล์ dot ซึ่งไม่เหมือนวิธีเชลล์ wildcard)

หากคุณไม่มียูทิลิตี้ GNU คุณสามารถใช้เชลล์ระดับกลางเพื่อรับอาร์กิวเมนต์ตามลำดับที่ถูกต้อง วิธีนี้ใช้ได้กับทุกระบบ POSIX

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

ใน zsh คุณสามารถโหลดmvbuiltin ได้ :

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

หรือถ้าคุณต้องการที่จะให้mvและชื่ออื่น ๆ ยังคงอ้างอิงถึงคำสั่งภายนอก:

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

หรือกับ ksh- สไตล์ globs:

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

อีกทางเลือกหนึ่งโดยใช้ GNU mvและzargs:

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

1
คำสั่งสองคำสั่งแรกส่งคืน "-bash:!: event not found" และคำสั่งสองรายการถัดไปไม่ได้ย้ายไฟล์ใด ๆ เลย ฉันใช้ CentOS 6.5 ถ้าคุณควรรู้
Dominique

1
@Dominique ฉันใช้ไวยากรณ์ globbing เดียวกับที่คุณใช้ในคำถามของคุณ คุณจะต้องshopt -s extglobเปิดใช้งาน ฉันพลาดขั้นตอนในfindคำสั่งฉันได้แก้ไขแล้ว
Gilles

ฉันได้รับสิ่งนี้ด้วยคำสั่ง find "find: expression ที่ไม่ถูกต้อง; คุณใช้ตัวดำเนินการไบนารี '-o' โดยไม่มีอะไรมาก่อน ฉันจะลองอีกอัน
Dominique

@Dominique findคำสั่งที่ฉันโพสต์ (ตอนนี้) ทำงาน คุณต้องออกไปเป็นส่วนหนึ่งเมื่อคัดลอกวาง
Gilles

Gilles สำหรับคำสั่ง find ทำไมไม่ใช้โอเปอเรเตอร์ "not" !? -oมันมากขึ้นอย่างชัดเจนและง่ายต่อการเข้าใจกว่าท้ายแปลก ตัวอย่างเช่น! -name '*.jpg' -a ! -name '*.png' -a ! -name '*.bmp'
CivFan

13

หากทำงานกับเคอร์เนลของ Linux ก็เพียงพอแล้วที่คุณจะทำได้

ulimit -s 100000

ที่จะทำงานได้เพราะเคอร์เนล Linux รวมแพตช์ประมาณ 10 ปีที่แล้วว่าข้อ จำกัด การเปลี่ยนแปลงจะขึ้นอยู่กับขนาดสแต็ก: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/ กระทำ /? id = b6a2fea39318e43fee84fa7b0b90d68bed92d2ba

อัปเดต: หากคุณรู้สึกกล้าคุณสามารถพูดได้

ulimit -s unlimited

และคุณจะใช้ได้กับการขยายเชลล์ใด ๆ ตราบใดที่คุณมี RAM เพียงพอ


นั่นคือแฮ็ค คุณจะรู้ได้อย่างไรว่าจะตั้งค่าขีด จำกัด ของสแต็กเป็นอย่างไร สิ่งนี้ยังส่งผลต่อกระบวนการอื่น ๆ ที่เริ่มในเซสชันเดียวกัน
Kusalananda

1
ใช่มันแฮ็ค เวลาส่วนใหญ่ของแฮ็กนี้เป็นแบบครั้งเดียว (คุณจะย้ายไฟล์จำนวนมากด้วยตนเองได้อย่างไร?) หากคุณแน่ใจว่ากระบวนการไม่กิน RAM ทั้งหมดของคุณคุณสามารถตั้งค่าulimit -s unlimitedและมันจะทำงานกับไฟล์ไม่ จำกัด จริง
Mikko Rantalainen

ด้วยulimit -s unlimitedขีด จำกัด บรรทัดคำสั่งจริงคือ 2 ^ 31 หรือ 2 GB ( MAX_ARG_STRLENในเคอร์เนลซอร์ส)
Mikko Rantalainen

9

ข้อ จำกัด การส่งผ่านอาร์กิวเมนต์ของระบบปฏิบัติการใช้ไม่ได้กับการขยายที่เกิดขึ้นภายในตัวแปลเชลล์ ดังนั้นนอกเหนือจากการใช้xargsหรือfindเราสามารถใช้ shell loop เพื่อแยกการประมวลผลออกเป็นแต่ละmvคำสั่ง:

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

สิ่งนี้ใช้คุณสมบัติและยูทิลิตีภาษาคำสั่ง POSIX Shell เท่านั้น ซับเดี่ยวนี้ชัดเจนยิ่งขึ้นพร้อมการเยื้องโดยลบเครื่องหมายอัฒภาคที่ไม่จำเป็นออก:

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

ด้วยไฟล์มากกว่าล้านไฟล์สิ่งนี้จะเกิดขึ้นมากกว่าหนึ่งล้านmvโพรเซสแทนที่จะเป็นเพียงส่วนน้อยที่จำเป็นโดยใช้findโซลูชันPOSIX @Gilles ที่โพสต์ กล่าวอีกนัยหนึ่งวิธีนี้ส่งผลให้เกิดการสับ CPU ที่ไม่จำเป็นจำนวนมาก
CivFan

@CivFan อีกปัญหาหนึ่งคือการโน้มน้าวตัวเองว่ารุ่นที่แก้ไขนั้นเทียบเท่ากับของจริง มันง่ายที่จะเห็นว่าcaseคำแถลงเกี่ยวกับผลของ*การขยายตัวเพื่อกรองส่วนขยายต่าง ๆ นั้นเทียบเท่ากับ!(*.jpg|*.png|*.bmp)นิพจน์ดั้งเดิม findคำตอบคือในความเป็นจริงไม่ได้เทียบเท่า มันลงไปในไดเรกทอรีย่อย (ฉันไม่เห็น-maxdepthคำกริยา)
Kaz

-name . -o -type d -prune -oปกป้องจากมากไปน้อยลงในไดเรกทอรีย่อย -maxdepthเห็นได้ชัดว่าไม่ได้เป็นไปตาม POSIX แต่ที่ไม่ได้กล่าวถึงในfindหน้าคนของฉัน
CivFan

ย้อนกลับไปสู่การแก้ไข 1 คำถามไม่ได้พูดอะไรเกี่ยวกับตัวแปรต้นทางหรือปลายทางดังนั้นสิ่งนี้จะเพิ่ม cruft ที่ไม่จำเป็นให้กับคำตอบ
Kaz

5

สำหรับวิธีการแก้ปัญหาที่ก้าวร้าวมากกว่าที่เสนอไว้ก่อนหน้าให้ดึงซอร์สเคอร์เนลและแก้ไข include/linux/binfmts.h

เพิ่มขนาดของMAX_ARG_PAGESสิ่งที่มีขนาดใหญ่กว่า 32 ซึ่งจะเป็นการเพิ่มจำนวนหน่วยความจำที่เคอร์เนลจะอนุญาตให้มีการขัดแย้งกับโปรแกรมดังนั้นช่วยให้คุณสามารถระบุmvหรือrmคำสั่งของคุณสำหรับไฟล์นับล้านหรือสิ่งที่คุณกำลังทำ คอมไพล์ติดตั้งรีบูต

ระวัง! หากคุณตั้งค่านี้มีขนาดใหญ่เกินไปสำหรับหน่วยความจำระบบของคุณแล้วเรียกใช้คำสั่งที่มีอาร์กิวเมนต์มากมายสิ่งที่ไม่ดีจะเกิดขึ้น! ใช้ความระมัดระวังอย่างมากกับระบบที่มีผู้ใช้หลายคนทำให้ผู้ใช้ที่เป็นอันตรายสามารถใช้หน่วยความจำทั้งหมดของคุณได้ง่ายขึ้น!

หากคุณไม่ทราบวิธีการคอมไพล์ใหม่และติดตั้งเคอร์เนลใหม่ด้วยตนเองอาจเป็นไปได้ว่าคุณอาจต้องตอบคำถามนี้ในตอนนี้


5

วิธีแก้ปัญหาที่ง่ายขึ้นที่ใช้"$origin"/!(*.jpg|*.png|*.bmp)แทน catch catch:

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

ขอบคุณ @Score_Under

สำหรับสคริปต์ที่มีหลายบรรทัดคุณสามารถทำสิ่งต่อไปนี้ (สังเกต;ก่อนที่doneจะดร็อป):

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

หากต้องการแก้ปัญหาที่ทำให้เป็นลักษณะทั่วไปมากขึ้นที่จะย้ายไฟล์ทั้งหมดคุณสามารถทำสิ่งต่อไปนี้:

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

ซึ่งมีลักษณะเช่นนี้หากคุณเยื้อง:

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

การดำเนินการนี้จะนำทุกไฟล์มาและย้ายไปทีละไฟล์ $fileจำเป็นต้องใช้เครื่องหมายอัญประกาศล้อมรอบในกรณีที่มีช่องว่างหรืออักขระพิเศษอื่น ๆ ในชื่อไฟล์

นี่คือตัวอย่างของวิธีการนี้ที่ทำงานได้อย่างสมบูรณ์

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

คุณสามารถใช้บางอย่างเช่น glob เดิมใน for-loop เพื่อให้ได้คำตอบที่ใกล้เคียงกับสิ่งที่ถูกถาม
คะแนน _ ต่ำ

คุณหมายถึง glob ดั้งเดิมอะไร
Whitecat

ขออภัยถ้าเป็นความลับเล็ก ๆ น้อย ๆ ผมหมายถึง glob !(*.jpg|*.png|*.bmp)ในคำถาม: คุณสามารถเพิ่มสิ่งนั้นลงใน for for loop ของคุณโดยการวนรอบ"$origin"/!(*.jpg|*.png|*.bmp)ซึ่งจะหลีกเลี่ยงความต้องการสวิตช์ที่ใช้ในคำตอบของ Kaz และรักษาร่างกายที่เรียบง่ายของ for-loop
คะแนน _ ต่ำกว่า

คะแนนที่ยอดเยี่ยม ฉันรวมความคิดเห็นของคุณและปรับปรุงคำตอบของฉัน
Whitecat

3

บางครั้งการเขียนสคริปต์เพียงเล็กน้อยง่ายที่สุดเช่นใน Python:

import glob, shutil

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

1

คุณสามารถหลีกเลี่ยงข้อ จำกัด ดังกล่าวได้ในขณะที่ยังใช้อยู่mvหากคุณไม่คิดที่จะเรียกใช้สองสามครั้ง

คุณสามารถย้ายบางส่วนได้ตลอดเวลา สมมติว่าคุณมีรายชื่อไฟล์ตัวอักษรและตัวเลขที่มีความยาว

mv ./subdir/a* ./

ที่ได้ผล จากนั้นเคาะก้อนใหญ่อีกก้อน หลังจากสองสามย้ายคุณสามารถกลับไปใช้mv ./subdir/* ./


0

นี่คือสองเซ็นต์ของฉันต่อท้ายนี้ .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

การใช้

mv '*.jpg' ./destination/
mv '/path/*' ./destination/
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.