ฉันจะใช้อักขระตัวแทนที่ตรงกันข้ามหรือเชิงลบเมื่อจับคู่รูปแบบในเชลล์ unix / linux ได้อย่างไร


325

ว่าฉันต้องการคัดลอกเนื้อหาของไดเรกทอรีที่ไม่รวมไฟล์และโฟลเดอร์ที่มีชื่อมีคำว่า 'Music'

cp [exclude-matches] *Music* /target_directory

สิ่งที่ควรแทนที่ [ไม่รวมการจับคู่] เพื่อทำสิ่งนี้ให้สำเร็จ

คำตอบ:


375

ใน Bash คุณสามารถทำได้โดยการเปิดใช้งานextglobตัวเลือกเช่นนี้ (แทนที่lsด้วยcpและเพิ่มไดเรกทอรีเป้าหมายแน่นอน)

~/foobar> shopt extglob
extglob        off
~/foobar> ls
abar  afoo  bbar  bfoo
~/foobar> ls !(b*)
-bash: !: event not found
~/foobar> shopt -s extglob  # Enables extglob
~/foobar> ls !(b*)
abar  afoo
~/foobar> ls !(a*)
bbar  bfoo
~/foobar> ls !(*foo)
abar  bbar

คุณสามารถปิดใช้งาน extglob ได้ในภายหลัง

shopt -u extglob

14
ฉันชอบคุณสมบัตินี้:ls /dir/*/!(base*)
Erick Robertson

6
คุณรวมทุกอย่างไว้อย่างไร ( ) และยกเว้น! (b )
Elijah Lynn

4
คุณจะจับคู่พูดว่าทุกสิ่งที่เริ่มต้นด้วยfยกเว้นได้fooอย่างไร
Noldorin

8
เหตุใดจึงปิดใช้งานโดยค่าเริ่มต้น
weberc2

3
shopt -o -u histexpand หากคุณต้องการค้นหาไฟล์ที่มีเครื่องหมายอัศเจรีย์อยู่ในนั้น - โดยค่าเริ่มต้น extglob จะปิดใช้งานตามค่าเริ่มต้นเพื่อที่จะได้ไม่ยุ่งกับ histexpand ในเอกสารอธิบายว่าทำไมถึงเป็นเช่นนั้น จับคู่ทุกอย่างที่ขึ้นต้นด้วย f ยกเว้น foo: f! (oo) แน่นอน 'อาหาร' จะยังคงตรงกัน (คุณจะต้อง f! (oo *) เพื่อหยุดสิ่งที่เริ่มต้นใน 'foo' หรือถ้าคุณต้องการกำจัด ของบางสิ่งที่ลงท้ายด้วยการใช้ '.foo'! ( .foo), หรือนำหน้า: myprefix! ( .foo) (ตรงกับ myprefixBLAH แต่ไม่ใช่ myprefixBLAH.foo)
osirisgothra

227

extglobตัวเลือกเปลือกจะช่วยให้คุณจับคู่รูปแบบที่มีประสิทธิภาพมากขึ้นในบรรทัดคำสั่ง

คุณเปิดด้วยและปิดด้วยshopt -s extglobshopt -u extglob

ในตัวอย่างของคุณคุณจะเริ่ม:

$ shopt -s extglob
$ cp !(*Music*) /target_directory

ตัวดำเนินการ bing globสิ้นสุดext ที่พร้อมใช้งานเต็มรูปแบบคือ (ตัดตอนมาจาก)man bash

หากมีการเปิดใช้งานตัวเลือกเปลือก extglob โดยใช้ shopt builtin ตัวดำเนินการจับคู่รูปแบบส่วนขยายจำนวนมากจะได้รับการยอมรับ รูปแบบรายการคือรายการของหนึ่งหรือหลายรูปแบบคั่นด้วย | รูปแบบคอมโพสิตอาจเกิดขึ้นโดยใช้หนึ่งในรูปแบบย่อยดังต่อไปนี้:

  • (รายการรูปแบบ)
    จับคู่ศูนย์หรือหนึ่งรูปแบบของรูปแบบที่กำหนด
  • * (รายการรูปแบบ)
    จับคู่เกิดขึ้นเป็นศูนย์ของรูปแบบที่กำหนด
  • + (รายการรูปแบบ)
    ตรงกับหนึ่งหรือหลายรูปแบบของรูปแบบที่กำหนด
  • @ (รายการรูปแบบ)
    ตรงกับหนึ่งในรูปแบบที่กำหนด
  • ! (รายการรูปแบบ)
    จับคู่อะไรก็ได้ยกเว้นรูปแบบที่กำหนดอย่างใดอย่างหนึ่ง

ตัวอย่างเช่นหากคุณต้องการแสดงรายการไฟล์ทั้งหมดในไดเรกทอรีปัจจุบันที่ไม่ใช่.cหรือ.hไฟล์คุณจะทำ:

$ ls -d !(*@(.c|.h))

แน่นอนว่าเปลือกหอยปกติทำงานได้ดีดังนั้นตัวอย่างสุดท้ายอาจเขียนเป็น:

$ ls -d !(*.[ch])

1
อะไรคือสาเหตุของ -d?
Big McLargeHuge

2
@Koveras สำหรับกรณีที่หนึ่ง.cหรือ.hไฟล์เป็นไดเรกทอรี
58

@DaveKennedy มันทุกอย่างที่รายการในไดเรกทอรีปัจจุบันแต่ไม่ได้เนื้อหาของไดเรกทอรีย่อยที่อาจมีอยู่ในไดเรกทอรีD D
spurra

23

ไม่ใช่ในทุบตี (ที่ฉันรู้) แต่:

cp `ls | grep -v Music` /target_directory

ฉันรู้ว่านี่ไม่ใช่สิ่งที่คุณกำลังมองหา แต่มันจะแก้ปัญหาตัวอย่างของคุณ


ค่าเริ่มต้น ls จะใส่หลายไฟล์ต่อบรรทัดซึ่งอาจไม่ได้ผลที่ถูกต้อง
Daniel Bungert

10
เมื่อ stdout เป็นเทอร์มินัลเท่านั้น เมื่อใช้ในไปป์ไลน์ ls จะพิมพ์ชื่อไฟล์หนึ่งชื่อต่อบรรทัด
Adam Rosenfield

ls วางหลายไฟล์ต่อบรรทัดเท่านั้นหากส่งออกไปยังเทอร์มินัล ลองด้วยตัวคุณเอง - "ls | less" จะไม่มีไฟล์หลายไฟล์ต่อบรรทัด
SpoonMeiser

3
มันจะไม่ทำงานกับชื่อไฟล์ที่มีช่องว่าง (หรืออักขระเว้นวรรคสีขาวอื่น ๆ )
tzot

7

หากคุณต้องการหลีกเลี่ยงต้นทุน mem ของการใช้คำสั่ง exec ฉันเชื่อว่าคุณสามารถทำได้ดีกว่าด้วย xargs ฉันคิดว่าสิ่งต่อไปนี้เป็นทางเลือกที่มีประสิทธิภาพมากกว่า

find foo -type f ! -name '*Music*' -exec cp {} bar \; # new proc for each exec



find . -maxdepth 1 -name '*Music*' -prune -o -print0 | xargs -0 -i cp {} dest/

6

ในทุบตีทางเลือกที่จะshopt -s extglobเป็นตัวแปรGLOBIGNORE มันไม่ได้ดีกว่านี้จริงๆ แต่ฉันจำได้ง่ายกว่า

ตัวอย่างที่อาจเป็นสิ่งที่โปสเตอร์ดั้งเดิมต้องการ:

GLOBIGNORE="*techno*"; cp *Music* /only_good_music/

เมื่อเสร็จแล้วunset GLOBIGNOREจะสามารถที่จะrm *techno*อยู่ในไดเรกทอรีแหล่งที่มา


5

นอกจากนี้คุณยังสามารถใช้การforวนรอบง่าย ๆ:

for f in `find . -not -name "*Music*"`
do
    cp $f /target/dir
done

1
นี่เป็นการค้นหาแบบเรียกซ้ำซึ่งเป็นพฤติกรรมที่แตกต่างจากที่ OP ต้องการ
Adam Rosenfield

1
ใช้-maxdepth 1สำหรับการไม่เรียกซ้ำ?
avtomaton

ฉันพบว่านี่เป็นโซลูชั่นที่สะอาดที่สุดโดยไม่ต้องเปิด / ปิดตัวเลือกเชลล์ แนะนำให้ใช้ตัวเลือก -maxdepth ในโพสต์นี้เพื่อให้ได้ผลลัพธ์ตามที่ OP ต้องการ แต่ทั้งหมดขึ้นอยู่กับสิ่งที่คุณพยายามทำ
David Lapointe

การใช้findbackticks จะทำให้แตกในลักษณะที่ไม่พึงประสงค์หากพบชื่อไฟล์ที่ไม่น่าสนใจ
tripleee

5

การตั้งค่าส่วนตัวของฉันคือการใช้ grep และคำสั่ง while สิ่งนี้ทำให้ผู้ใช้สามารถเขียนสคริปต์ที่มีประสิทธิภาพ แต่สามารถอ่านได้เพื่อให้แน่ใจว่าคุณจะทำสิ่งที่คุณต้องการ บวกโดยใช้คำสั่ง echo คุณสามารถดำเนินการแห้งก่อนที่จะดำเนินการจริง ตัวอย่างเช่น:

ls | grep -v "Music" | while read filename
do
echo $filename
done

จะพิมพ์ไฟล์ที่คุณจะทำการคัดลอก หากรายการถูกต้องขั้นตอนต่อไปคือการแทนที่คำสั่ง echo ด้วยคำสั่ง copy ดังนี้:

ls | grep -v "Music" | while read filename
do
cp "$filename" /target_directory
done

1
วิธีนี้จะใช้ได้ตราบใดที่ชื่อไฟล์ของคุณไม่มีแท็บบรรทัดใหม่บรรทัดว่างมากกว่าหนึ่งช่องในหนึ่งแถวหรือแบ็กสแลชใด ๆ ในขณะที่เป็นกรณีทางพยาธิวิทยามันเป็นเรื่องดีที่จะตระหนักถึงความเป็นไป ในbashคุณสามารถใช้while IFS='' read -r filenameแต่แล้วบรรทัดใหม่ยังคงมีปัญหา โดยทั่วไปจะเป็นการดีที่สุดที่จะไม่ใช้lsในการระบุไฟล์ เครื่องมือเช่นfindนั้นเหมาะสมกว่า
Thedward

โดยไม่ต้องใช้เครื่องมือใด ๆ เพิ่มเติม:for file in *; do case ${file} in (*Music*) ;; (*) cp "${file}" /target_directory ; echo ;; esac; done
Thedward

mywiki.wooledge.org/ParsingLsแสดงเหตุผลเพิ่มเติมหลายประการว่าทำไมคุณควรหลีกเลี่ยงสิ่งนี้
tripleee

5

เคล็ดลับที่ฉันไม่ได้เห็นที่นี่เลยที่ไม่ได้ใช้extglob, findหรือgrepคือการรักษาทั้งสองรายชื่อไฟล์เป็นชุดและ"ต่าง"พวกเขาโดยใช้comm:

comm -23 <(ls) <(ls *Music*)

commเป็นที่นิยมมากกว่าdiffเพราะไม่มี cruft พิเศษ

ผลตอบแทนนี้ทุกองค์ประกอบของชุดที่ 1 lsที่มีไม่ได้ยังอยู่ในชุดที่ ls *Music*2 สิ่งนี้ต้องการให้ทั้งสองชุดเรียงลำดับเพื่อทำงานอย่างถูกต้อง ไม่มีปัญหาสำหรับการlsและการขยายตัว glob แต่ถ้าคุณกำลังใช้สิ่งที่ต้องการให้แน่ใจว่าจะก่อให้เกิดfindsort

comm -23 <(find . | sort) <(find . | grep -i '.jpg' | sort)

อาจมีประโยชน์


1
ข้อดีอย่างหนึ่งของการยกเว้นคือไม่สำรวจไดเรกทอรีในตอนแรก วิธีการแก้ปัญหานี้จะทำการสำรวจทั้งสองไดเรกทอรีย่อยหนึ่งรายการด้วยการยกเว้นและอีกหนึ่งรายการที่ไม่มี
Mark Stosberg

จุดที่ดีมาก @ MarkStosberg แม้ว่าข้อดีอย่างหนึ่งของเทคนิคนี้คือคุณสามารถอ่านการยกเว้นจากไฟล์จริงเช่นcomm -23 <(ls) exclude_these.list
James M. Lay

3

ทางออกหนึ่งสำหรับสิ่งนี้สามารถพบได้ด้วยการค้นหา

$ mkdir foo bar
$ touch foo/a.txt foo/Music.txt
$ find foo -type f ! -name '*Music*' -exec cp {} bar \;
$ ls bar
a.txt

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

แก้ไข: อดัมในความคิดเห็นตั้งข้อสังเกตว่านี่เป็นแบบเรียกซ้ำ ค้นหาตัวเลือก mindepth และ maxdepth อาจมีประโยชน์ในการควบคุมสิ่งนี้


สิ่งนี้ทำซ้ำซ้ำซึ่งเป็นพฤติกรรมที่แตกต่างกัน นอกจากนี้ยังวางไข่กระบวนการใหม่สำหรับแต่ละไฟล์ซึ่งอาจไม่มีประสิทธิภาพมากสำหรับไฟล์จำนวนมาก
Adam Rosenfield

ต้นทุนของการวางไข่กระบวนการอยู่ที่ประมาณศูนย์เมื่อเปรียบเทียบกับ IO ทั้งหมดที่คัดลอกแต่ละไฟล์ที่สร้าง ดังนั้นฉันจะบอกว่ามันดีพอสำหรับการใช้งานเป็นครั้งคราว
dland

วิธีแก้ปัญหาบางอย่างสำหรับกระบวนการวางไข่: stackoverflow.com/questions/186099/…
Vinko Vrsalovic

ใช้ "-maxdepth 1" เพื่อหลีกเลี่ยงการเรียกซ้ำ
ejgottl

ใช้ backticks เพื่อรับการขยายตัวของการ์ดเสริมเชลล์: cp find -maxdepth 1 -not -name '*Music*'/ target_directory
ejgottl

2

ผลงานต่อไปนี้แสดงรายการ*.txtไฟล์ทั้งหมดใน dir ปัจจุบันยกเว้นไฟล์ที่ขึ้นต้นด้วยตัวเลข

งานนี้ในbash, dash, zshและทุก POSIX อื่น ๆ เปลือกหอยที่เข้ากันได้

for FILE in /some/dir/*.txt; do    # for each *.txt file
    case "${FILE##*/}" in          #   if file basename...
        [0-9]*) continue ;;        #   starts with digit: skip
    esac
    ## otherwise, do stuff with $FILE here
done
  1. ในบรรทัดที่หนึ่งรูปแบบ/some/dir/*.txtจะทำให้forห่วงย้ำกว่าไฟล์ทั้งหมดในท้ายที่สุดของชื่อด้วย/some/dir.txt

  2. ในบรรทัดที่สองจะใช้คำสั่ง case เพื่อแยกไฟล์ที่ไม่ต้องการออก - ${FILE##*/}นิพจน์ตัดองค์ประกอบของชื่อ dir ชั้นนำออกจากชื่อไฟล์ (ที่นี่/some/dir/) เพื่อให้ patters สามารถจับคู่กับชื่อไฟล์พื้นฐานของไฟล์เท่านั้น (หากคุณกำจัดชื่อไฟล์โดยอิงตามคำต่อท้ายคุณสามารถย่อให้เหลือ$FILEแทน)

  3. ในบรรทัดที่สามไฟล์ทั้งหมดที่ตรงกับcaseรูปแบบ[0-9]*) จะถูกข้ามไป ( continueคำสั่งจะข้ามไปที่การวนซ้ำครั้งถัดไปของforลูป) - หากคุณต้องการทำสิ่งที่น่าสนใจมากขึ้นที่นี่เช่นการข้ามไฟล์ทั้งหมดที่ไม่ได้ขึ้นต้นด้วยตัวอักษร (a – z) โดยใช้[!a-z]*หรือคุณสามารถใช้รูปแบบหลายรูปแบบเพื่อข้ามชื่อไฟล์หลายประเภทเช่น[0-9]*|*.bakการข้ามไฟล์ทั้งสอง.bakไฟล์ และไฟล์ที่ไม่ได้ขึ้นต้นด้วยตัวเลข


Doh! มีข้อผิดพลาด (ฉันจับคู่กับ*.txtแทนที่จะเป็นเพียงแค่*) แก้ไขแล้ว
zrajm

0

สิ่งนี้จะทำได้ยกเว้น 'เพลง'

cp -a ^'Music' /target

สิ่งนี้และสำหรับการยกเว้นสิ่งต่าง ๆ เช่น Music? * หรือ *? Music

cp -a ^\*?'complete' /target
cp -a ^'complete'?\* /target

cpหน้าคู่มือใน MacOS มี-aตัวเลือก แต่ก็ทำอะไรบางอย่างที่แตกต่างกันอย่างสิ้นเชิง แพลตฟอร์มใดรองรับสิ่งนี้
tripleee
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.