ทำซ้ำรายการไฟล์ที่มีช่องว่าง


201

ฉันต้องการย้ำเหนือรายการไฟล์ รายการนี้เป็นผลลัพธ์ของfindคำสั่งดังนั้นฉันมาด้วย:

getlist() {
  for f in $(find . -iname "foo*")
  do
    echo "File found: $f"
    # do something useful
  done
}

ไม่เป็นไรยกเว้นไฟล์ที่มีช่องว่างในชื่อ:

$ ls
foo_bar_baz.txt
foo bar baz.txt

$ getlist
File found: foo_bar_baz.txt
File found: foo
File found: bar
File found: baz.txt

ฉันจะทำอย่างไรเพื่อหลีกเลี่ยงการแบ่งช่องว่าง


คำตอบ:


253

คุณสามารถแทนที่การทำซ้ำตามคำด้วยหนึ่งบรรทัดตาม:

find . -iname "foo*" | while read f
do
    # ... loop body
done

31
นี่สะอาดมาก และทำให้ฉันรู้สึกดีขึ้นกว่าการเปลี่ยน IFS ควบคู่ไปกับ for loop
Derrick

15
สิ่งนี้จะแบ่งเส้นทางไฟล์เดียวที่มี \ n ตกลงสิ่งเหล่านั้นไม่ควรอยู่ใกล้ แต่สามารถสร้างได้:touch "$(printf "foo\nbar")"
โอลลี่แซนเดอร์

4
เพื่อป้องกันการตีความอินพุต (แบ็กสแลชนำหน้าและต่อท้ายช่องว่าง) ให้ใช้IFS= while read -r fแทน
mklement0

2
คำตอบนี้แสดงให้เห็นถึงการรวมกันที่ปลอดภัยมากขึ้นของfindและห่วงในขณะที่
moi

5
ดูเหมือนว่าชี้ให้เห็นชัดเจน แต่ในเกือบทุกกรณีที่เรียบง่าย, เป็นไปได้สะอาดกว่าห่วงอย่างชัดเจน:-exec find . -iname "foo*" -exec echo "File found: {}" \;นอกจากนี้ในหลายกรณีคุณสามารถแทนที่ไฟล์ล่าสุด\;ด้วย+การใส่ไฟล์จำนวนมากในคำสั่งเดียว
naught101

152

มีหลายวิธีที่ใช้การได้เพื่อให้บรรลุเป้าหมายนี้

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

getlist() {
        IFS=$'\n'
        for file in $(find . -iname 'foo*') ; do
                printf 'File found: %s\n' "$file"
        done
}

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

อย่างไรก็ตามการ messing กับ IFS นั้นไม่จำเป็น นี่คือวิธีที่ฉันต้องการทำ:

getlist() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: %s\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

หากคุณพบว่า< <(command)ไวยากรณ์ที่ไม่คุ้นเคยที่คุณควรอ่านเกี่ยวกับขั้นตอนการเปลี่ยนตัว ข้อดีของการทำเช่นนี้for file in $(find ...)คือการจัดการไฟล์ที่มีช่องว่างการขึ้นบรรทัดใหม่และอักขระอื่น ๆ อย่างถูกต้อง สิ่งนี้ได้ผลเพราะfindด้วย-print0จะใช้null(aka \0) เป็นตัวยุติสำหรับชื่อไฟล์แต่ละชื่อและไม่เหมือนกับบรรทัดใหม่ null ไม่ได้เป็นอักขระตามกฎหมายในชื่อไฟล์

ข้อได้เปรียบนี้มากกว่ารุ่นเกือบเทียบเท่า

getlist() {
        find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
                printf 'File found: %s\n' "$file"
        done
}

คือการกำหนดตัวแปรใด ๆ ในเนื้อความของ while loop นั้นถูกสงวนไว้ นั่นคือถ้าคุณไปwhileที่ด้านบนแล้วเนื้อหาของwhileอยู่ใน subshell ซึ่งอาจไม่เป็นสิ่งที่คุณต้องการ

ข้อได้เปรียบของรุ่นการทดแทนกระบวนการที่find ... -print0 | xargs -0น้อยที่สุด: xargsรุ่นนั้นใช้ได้ถ้าคุณต้องการพิมพ์บรรทัดหรือทำการดำเนินการเดี่ยวบนไฟล์ แต่ถ้าคุณต้องการดำเนินการหลายขั้นตอนเวอร์ชันลูปจะง่ายขึ้น

แก้ไข : นี่คือสคริปต์ทดสอบที่ดีเพื่อให้คุณสามารถรับทราบความแตกต่างระหว่างความพยายามที่แตกต่างกันในการแก้ปัญหานี้

#!/usr/bin/env bash

dir=/tmp/getlist.test/
mkdir -p "$dir"
cd "$dir"

touch       'file not starting foo' foo foobar barfoo 'foo with spaces'\
    'foo with'$'\n'newline 'foo with trailing whitespace      '

# while with process substitution, null terminated, empty IFS
getlist0() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# while with process substitution, null terminated, default IFS
getlist1() {
    while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# pipe to while, newline terminated
getlist2() {
    find . -iname 'foo*' | while read -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# pipe to while, null terminated
getlist3() {
    find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, default IFS
getlist4() {
    for file in "$(find . -iname 'foo*')" ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, newline IFS
getlist5() {
    IFS=$'\n'
    for file in $(find . -iname 'foo*') ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}


# see how they run
for n in {0..5} ; do
    printf '\n\ngetlist%d:\n' $n
    eval getlist$n
done

rm -rf "$dir"

1
ยอมรับคำตอบของคุณ: สมบูรณ์และน่าสนใจที่สุด - ฉันไม่รู้จัก$IFSและ< <(cmd)วากยสัมพันธ์ ยังมีสิ่งหนึ่งที่ยังคงคลุมเครือกับฉันทำไม$ใน$'\0'? ขอบคุณมาก.
gregseth

2
+1 แต่คุณควรเพิ่ม ... while IFS= read... เพื่อจัดการไฟล์ที่เริ่มต้นหรือลงท้ายด้วย whitespace
Gordon Davisson

1
มีข้อแม้หนึ่งสำหรับโซลูชันการทดแทนกระบวนการ หากคุณมีพรอมต์ใด ๆ อยู่ภายในลูป (หรือกำลังอ่านจาก STDIN ด้วยวิธีอื่นใด) อินพุตจะถูกเติมด้วยข้อมูลที่คุณป้อนเข้าไปในลูป (อาจจะเพิ่มในคำตอบ?)
andsens

2
@uvsmtid: คำถามนี้ถูกแท็กbashดังนั้นฉันรู้สึกปลอดภัยโดยใช้คุณลักษณะเฉพาะของ bash การทดแทนกระบวนการไม่สามารถเคลื่อนย้ายไปยังเชลล์อื่นได้ (ตัวเองไม่น่าจะได้รับการอัพเดตที่สำคัญเช่นนี้)
sorpigal

2
การรวมเข้าIFS=$'\n'กับการforป้องกันการแยกคำภายในบรรทัด แต่ยังคงทำให้บรรทัดผลลัพธ์มีการวนรอบดังนั้นวิธีการนี้จึงไม่สมบูรณ์ (เว้นแต่คุณจะปิดการหมุนก่อน) ในขณะที่read -d $'\0'ผลงานก็จะทำให้เข้าใจผิดเล็กน้อยในการที่จะแสดงให้เห็นว่าคุณสามารถใช้$'\0'เพื่อสร้าง NULs - คุณไม่สามารถก\0ในสตริง ANSI C-อ้างได้อย่างมีประสิทธิภาพยุติสตริงเพื่อให้มีประสิทธิภาพเช่นเดียวกับ-d $'\0' -d ''
mklement0

29

นอกจากนี้ยังมีทางออกที่ง่ายมาก: พึ่งพาการทุบตี bash

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"
$ ls
stupid   file 3  stupid file1     stupid file2
$ for file in *; do echo "file: '${file}'"; done
file: 'stupid   file 3'
file: 'stupid file1'
file: 'stupid file2'

โปรดทราบว่าฉันไม่แน่ใจว่าพฤติกรรมนี้เป็นค่าเริ่มต้น แต่ฉันไม่เห็นการตั้งค่าพิเศษใด ๆ ใน shopt ของฉันดังนั้นฉันจะไปและบอกว่าควรจะ "ปลอดภัย" (ทดสอบบน osx และ ubuntu)


13
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"

6
เป็นหมายเหตุด้านนี้จะทำงานเฉพาะถ้าคุณต้องการรันคำสั่ง ตัวเชลล์ในตัวจะไม่ทำงานเช่นนี้
อเล็กซ์


6

เนื่องจากคุณไม่ได้ทำการกรองประเภทอื่นด้วยfindคุณสามารถใช้สิ่งต่อไปนี้ตั้งแต่bash4.0:

shopt -s globstar
getlist() {
    for f in **/foo*
    do
        echo "File found: $f"
        # do something useful
    done
}

**/จะตรงกับไดเรกทอรีศูนย์หรือมากกว่าดังนั้นรูปแบบเต็มรูปแบบจะตรงกับfoo*ในไดเรกทอรีปัจจุบันหรือไดเรกทอรีย่อยใด ๆ


3

ฉันชอบลูปและทำซ้ำแถวดังนั้นฉันคิดว่าฉันจะเพิ่มคำตอบนี้ในการผสม ...

ฉันยังชอบตัวอย่างไฟล์ที่โง่ของ marchelbling :)

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"

ภายในไดเรกทอรีทดสอบ:

readarray -t arr <<< "`ls -A1`"

สิ่งนี้จะเพิ่มแต่ละรายการไฟล์ลงในอาเรย์ของ bash ที่arrมีการขึ้นบรรทัดใหม่ต่อท้าย

สมมติว่าเราต้องการให้ชื่อไฟล์เหล่านี้ดีกว่า ...

for i in ${!arr[@]}
do 
    newname=`echo "${arr[$i]}" | sed 's/stupid/smarter/; s/  */_/g'`; 
    mv "${arr[$i]}" "$newname"
done

$ {! ARR [@]} ขยายไป 0 1 2 ดังนั้น "$ {ARR [$ i]}" คือฉันTHองค์ประกอบของอาร์เรย์ เครื่องหมายคำพูดรอบตัวแปรมีความสำคัญต่อการรักษาช่องว่าง

ผลลัพธ์คือสามไฟล์ที่ถูกเปลี่ยนชื่อ:

$ ls -1
smarter_file1
smarter_file2
smarter_file_3

2

findมี-execอาร์กิวเมนต์ที่วนรอบผลลัพธ์การค้นหาและดำเนินการคำสั่งโดยพลการ ตัวอย่างเช่น:

find . -iname "foo*" -exec echo "File found: {}" \;

ที่นี่{}แสดงถึงไฟล์ที่พบและการตัดคำใน""อนุญาตให้คำสั่งเชลล์ resultant จัดการกับช่องว่างในชื่อไฟล์

ในหลายกรณีคุณสามารถแทนที่สุดท้าย\;(ซึ่งเริ่มคำสั่งใหม่) ด้วย a \+ซึ่งจะใส่หลายไฟล์ในคำสั่งเดียว (ไม่จำเป็นต้องทั้งหมดในคราวเดียวดูman findรายละเอียดเพิ่มเติม)


0

ในบางกรณีที่นี่หากคุณต้องการคัดลอกหรือย้ายรายการไฟล์คุณสามารถไปที่รายการนั้นเพื่อ awk ได้เช่นกัน
สำคัญ\"" "\"รอบ ๆ ฟิลด์$0(โดยย่อไฟล์ของคุณหนึ่งบรรทัดรายการ = หนึ่งไฟล์)

find . -iname "foo*" | awk '{print "mv \""$0"\" ./MyDir2" | "sh" }'

0

Ok - โพสต์แรกของฉันใน Stack Overflow!

แม้ว่าปัญหาของฉันเกี่ยวกับสิ่งนี้จะอยู่ใน csh เสมอไม่ทุบตีวิธีที่ฉันนำเสนอจะฉันแน่ใจว่าทำงานในทั้ง ปัญหานี้เกิดจากการตีความผลตอบแทนของ "ls" ของเชลล์ เราสามารถลบ "ls" ออกจากปัญหาได้โดยเพียงแค่ใช้การขยายเชลล์ของ*wildcard - แต่นี่จะทำให้เกิดข้อผิดพลาด "ไม่ตรงกัน" หากไม่มีไฟล์ในปัจจุบัน (หรือโฟลเดอร์ที่ระบุ) - เพื่อรับรอบนี้เราก็ขยาย การขยายเพื่อรวมไฟล์ดอทจึง: * .*- สิ่งนี้จะให้ผลลัพธ์เสมอตั้งแต่ไฟล์ และ .. จะมีอยู่เสมอ ดังนั้นใน csh เราสามารถใช้โครงสร้างนี้ ...

foreach file (* .*)
   echo $file
end

ถ้าคุณต้องการกรองไฟล์ดอทมาตรฐานออกมานั่นมันง่ายพอ ...

foreach file (* .*)
   if ("$file" == .) continue
   if ("file" == ..) continue
   echo $file
end

รหัสในกระทู้แรกในกระทู้นี้จะถูกเขียนดังนี้: -

getlist() {
  for f in $(* .*)
  do
    echo "File found: $f"
    # do something useful
  done
}

หวังว่านี่จะช่วยได้!


0

ทางออกสำหรับงานอื่น ...

เป้าหมายคือ:

  • เลือก / กรองชื่อไฟล์ซ้ำในไดเรกทอรี
  • จัดการแต่ละชื่อ (ช่องว่างใด ๆ ในเส้นทาง ... )
#!/bin/bash  -e
## @Trick in order handle File with space in their path...
OLD_IFS=${IFS}
IFS=$'\n'
files=($(find ${INPUT_DIR} -type f -name "*.md"))
for filename in ${files[*]}
do
      # do your stuff
      #  ....
done
IFS=${OLD_IFS}


ขอบคุณสำหรับคำพูดที่สร้างสรรค์ แต่: 1- นี่เป็นปัญหาที่แท้จริง 2- เปลือกอาจมีการพัฒนาในเวลา ... ตามที่ทุกคนฉันถือว่า; 3- ไม่มีคำตอบข้างต้นสามารถตอบสนองความละเอียดโดยตรงของ pb โดยไม่มีการเปลี่ยนแปลงปัญหาหรือ disserting :-)
Vince B
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.