bash: การใช้โพรซีเดอร์ find-select ที่ปลอดภัยแบบ whitespace ปลอดภัย


12

รับชื่อไฟล์เหล่านี้:

$ ls -1
file
file name
otherfile

bash ตัวเองทำดีกับช่องว่างที่ฝังตัว:

$ for file in *; do echo "$file"; done
file
file name
otherfile
$ select file in *; do echo "$file"; done
1) file
2) file name
3) otherfile
#?

อย่างไรก็ตามบางครั้งฉันอาจไม่ต้องการทำงานกับไฟล์ทุกไฟล์หรือแม้กระทั่งในที่เข้มงวด$PWDซึ่งเป็นที่findมาซึ่งยังจัดการกับช่องว่างในนาม:

$ find -type f -name file\*
./file
./file name
./directory/file
./directory/file name

ฉันพยายามที่จะกุเรื่องรุ่น whispace ปลอดภัยของนี้คริปตซึ่งจะใช้การส่งออกของfindและนำเสนอเข้าสู่select:

$ select file in $(find -type f -name file); do echo $file; break; done
1) ./file
2) ./directory/file

อย่างไรก็ตามการทำเช่นนี้จะทำให้เกิดช่องว่างในชื่อไฟล์:

$ select file in $(find -type f -name file\*); do echo $file; break; done
1) ./file        3) name          5) ./directory/file
2) ./file        4) ./directory/file  6) name

ปกติผมจะได้รับรอบนี้โดย messing IFSรอบด้วย อย่างไรก็ตาม:

$ IFS=$'\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'
$ IFS='\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'

อะไรคือทางออกของปัญหานี้?



1
หากคุณกำลังเพียงใช้findสำหรับความสามารถเพื่อให้ตรงกับชื่อไฟล์โดยเฉพาะอย่างยิ่งคุณสามารถใช้select file in **/file*(หลังจากการตั้งค่าshopt -s globstar) ในbash4 หรือในภายหลัง
chepner

คำตอบ:


14

หากคุณต้องการจัดการช่องว่างและแท็บ (ไม่ใช่การขึ้นบรรทัดใหม่) คุณสามารถใช้mapfile(หรือคำพ้องความหมายreadarray) เพื่ออ่านในอาเรย์เช่นที่กำหนด

$ ls -1
file
other file
somefile

แล้วก็

$ IFS= mapfile -t files < <(find . -type f)
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
#? 3
./other file

ถ้าคุณทำจำเป็นที่จะต้องขึ้นบรรทัดใหม่จับของคุณและbashรุ่นให้ null คั่นด้วยmapfile1IFS= mapfile -t -d '' files < <(find . -type f -print0)แล้วคุณสามารถปรับเปลี่ยนที่ มิฉะนั้นให้รวบรวมอาร์เรย์ที่เทียบเท่าจากfindเอาต์พุตที่มีการคั่นด้วย null โดยใช้readลูป:

$ touch $'filename\nwith\nnewlines'
$ 
$ files=()
$ while IFS= read -r -d '' f; do files+=("$f"); done < <(find . -type f -print0)
$ 
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
4) ./filename
with
newlines
#? 4
./filename?with?newlines

1-dตัวเลือกที่ถูกเพิ่มเข้ามาmapfileในbashรุ่น 4.4 iirc


2
+1 สำหรับคำกริยาอื่นที่ฉันไม่เคยใช้มาก่อน
roaima

อันที่จริงmapfileก็เป็นของใหม่สำหรับฉันด้วย ความรุ่งโรจน์
DopeGhoti

while IFS= readรุ่นทำงานกลับมาอยู่ในทุบตี v3 (ซึ่งเป็นสิ่งสำคัญสำหรับพวกเราใช้ MacOS)
Gordon Davisson

3
+1 สำหรับfind -print0ตัวแปร บ่นว่าวางไว้หลังจากเวอร์ชั่นที่ไม่ถูกต้องและอธิบายว่ามันใช้งานได้ก็ต่อเมื่อมีใครรู้ว่าพวกเขาต้องจัดการกับการขึ้นบรรทัดใหม่ หากมีใครจัดการกับสิ่งที่คาดไม่ถึงในสถานที่ซึ่งคาดว่าจะมีคนจัดการไม่คาดคิดเลย
Charles Duffy

8

คำตอบนี้มีคำตอบสำหรับไฟล์ทุกประเภท ด้วยการขึ้นบรรทัดใหม่หรือช่องว่าง
มีวิธีแก้ปัญหาสำหรับการทุบตีเมื่อเร็ว ๆ นี้เช่นเดียวกับทุบตีโบราณและแม้กระทั่งเปลือกหอย POSIX เก่า

ต้นไม้ที่แสดงด้านล่างในคำตอบนี้[1]ใช้สำหรับการทดสอบ

เลือก

selectการทำงานกับอาร์เรย์ทำได้ง่าย:

$ dir='deep/inside/a/dir'
$ arr=( "$dir"/* )
$ select var in "${arr[@]}"; do echo "$var"; break; done

หรือด้วยพารามิเตอร์ตำแหน่ง:

$ set -- "$dir"/*
$ select var; do echo "$var"; break; done

ดังนั้นปัญหาที่แท้จริงคือการได้รับ "รายการไฟล์" (คั่นอย่างถูกต้อง) ภายในอาเรย์หรือภายในพารามิเตอร์ตำแหน่ง อ่านต่อไป

ทุบตี

ฉันไม่เห็นปัญหาที่คุณรายงานด้วยการทุบตี Bash สามารถค้นหาภายในไดเรกทอรีที่กำหนด:

$ dir='deep/inside/a/dir'
$ printf '<%s>\n' "$dir"/*
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

หรือถ้าคุณชอบวง:

$ set -- "$dir"/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

โปรดทราบว่าไวยากรณ์ข้างต้นจะทำงานอย่างถูกต้องกับเชลล์ (สมเหตุสมผล) ใด ๆ (ไม่ใช่ csh อย่างน้อย)

ข้อ จำกัด เพียงอย่างเดียวที่ไวยากรณ์ข้างต้นมีเพื่อลงไปยังไดเรกทอรีอื่น
แต่ทุบตีสามารถทำได้:

$ shopt -s globstar
$ set -- "$dir"/**/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

ในการเลือกไฟล์บางไฟล์เท่านั้น (เช่นไฟล์ที่ลงท้ายด้วยไฟล์) ให้แทนที่ *:

$ set -- "$dir"/**/*file
$ printf '<%s>\n' "$@"
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/zz last file>

แข็งแรง

เมื่อคุณวาง "พื้นที่ปลอดภัย " ในชื่อเรื่องฉันจะถือว่าสิ่งที่คุณหมายถึงคือ " แข็งแกร่ง "

วิธีที่ง่ายที่สุดในการเพิ่มความแข็งแกร่งให้กับช่องว่าง (หรือการขึ้นบรรทัดใหม่) คือการปฏิเสธการประมวลผลอินพุตที่มีช่องว่าง (หรือการขึ้นบรรทัดใหม่) วิธีที่ง่ายมากในการทำเช่นนี้ในเชลล์คือการออกโดยมีข้อผิดพลาดหากชื่อไฟล์ใด ๆ ขยายด้วยช่องว่าง มีหลายวิธีในการทำเช่นนี้ แต่คอมแพคที่สุด (และ posix) (แต่ จำกัด เฉพาะเนื้อหาไดเรกทอรีเดียวรวมถึงชื่อ suddirectories และหลีกเลี่ยงจุดไฟล์) คือ:

$ set -- "$dir"/file*                            # read the directory
$ a="$(printf '%s' "$@" x)"                      # make it a long string
$ [ "$a" = "${a%% *}" ] || echo "exit on space"  # if $a has an space.
$ nl='
'                    # define a new line in the usual posix way.  

$ [ "$a" = "${a%%"$nl"*}" ] || echo "exit on newline"  # if $a has a newline.

หากโซลูชันที่ใช้มีความทนทานในรายการเหล่านี้ให้ลบการทดสอบ

ในทุบตีไดเรกทอรีย่อยสามารถทดสอบได้ทันทีพร้อมกับ ** อธิบายข้างต้น

มีสองวิธีในการรวมไฟล์ดอทโซลูชัน Posix คือ:

set -- "$dir"/* "$dir"/.[!.]* "$dir"/..?*

หา

หากการค้นหาต้องใช้ด้วยเหตุผลบางอย่างให้แทนที่ตัวคั่นด้วย NUL (0x00)

ทุบตี 4.4+

$ readarray -t -d '' arr < <(find "$dir" -type f -name file\* -print0)
$ printf '<%s>\n' "${arr[@]}"
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/file>

bash 2.05+

i=1  # lets start on 1 so it works also in zsh.
while IFS='' read -d '' val; do 
    arr[i++]="$val";
done < <(find "$dir" -type f -name \*file -print0)
printf '<%s>\n' "${arr[@]}"

POSIXLY

ในการสร้างโซลูชัน POSIX ที่ถูกต้องซึ่งการค้นหาไม่มีตัวคั่น NUL และไม่มี-d(หรือ-a) สำหรับการอ่านเราจำเป็นต้องมีโปรแกรม aproach entirelly ที่แตกต่างกัน

เราจำเป็นต้องใช้คอมเพล็กซ์-execจาก find ด้วยการเรียกไปยัง shell:

find "$dir" -type f -exec sh -c '
    for f do
        echo "<$f>"
    done
    ' sh {} +

หรือหากสิ่งที่จำเป็นคือการเลือก (select เป็นส่วนหนึ่งของ bash ไม่ใช่ sh):

$ find "$dir" -type f -exec bash -c '
      select f; do echo "<$f>"; break; done ' bash {} +

1) deep/inside/a/dir/file name
2) deep/inside/a/dir/zz last file
3) deep/inside/a/dir/file with a
newline
4) deep/inside/a/dir/directory/file name
5) deep/inside/a/dir/directory/zz last file
6) deep/inside/a/dir/directory/file with a
newline
7) deep/inside/a/dir/directory/file
8) deep/inside/a/dir/file
#? 3
<deep/inside/a/dir/file with a
newline>

[1]ต้นไม้นี้ (\ 012 เป็นบรรทัดใหม่):

$ tree
.
└── deep
    └── inside
        └── a
            └── dir
                ├── directory
                   ├── file
                   ├── file name
                   └── file with a \012newline
                ├── file
                ├── file name
                ├── otherfile
                ├── with a\012newline
                └── zz last file

สามารถสร้างได้ด้วยสองคำสั่งนี้:

$ mkdir -p deep/inside/a/dir/directory/
$ touch deep/inside/a/dir/{,directory/}{file{,\ {name,with\ a$'\n'newline}},zz\ last\ file}

6

คุณไม่สามารถตั้งค่าตัวแปรที่อยู่ด้านหน้าของโครงสร้างลูป แต่คุณสามารถตั้งค่าไว้ด้านหน้าเงื่อนไข นี่คือส่วนจากหน้าคน:

สภาพแวดล้อมสำหรับคำสั่งหรือฟังก์ชั่นที่เรียบง่ายใดอาจถูกเพิ่มชั่วคราวโดยนำหน้าด้วยการกำหนดพารามิเตอร์ตามที่อธิบายไว้ข้างต้นในพารามิเตอร์

(การวนซ้ำไม่ใช่คำสั่งง่ายๆ )

นี่คือโครงสร้างที่ใช้กันทั่วไปซึ่งแสดงให้เห็นถึงความล้มเหลวและสถานการณ์ความสำเร็จ:

IFS=$'\n' while read -r x; do ...; done </tmp/file     # Failure
while IFS=$'\n' read -r x; do ...; done </tmp/file     # Success

แต่น่าเสียดายที่ผมไม่สามารถดูวิธีการฝังเปลี่ยนIFSเข้าสู่โครงสร้างในขณะที่มีมันส่งผลกระทบต่อการประมวลผลของการที่เกี่ยวข้องselect $(...)อย่างไรก็ตามไม่มีอะไรที่จะป้องกันไม่ให้IFSถูกตั้งค่าไว้นอกวง:

IFS=$'\n'; while read -r x; do ...; done </tmp/file    # Also success

และนี่คือโครงสร้างที่ฉันสามารถดูได้ด้วยselect:

IFS=$'\n'; select file in $(find -type f -name 'file*'); do echo "$file"; break; done

เมื่อเขียนรหัสป้องกันฉันขอแนะนำให้ใช้ประโยคใน subshell หรือIFSและSHELLOPTSบันทึกและเรียกคืนรอบ ๆ บล็อก:

OIFS="$IFS" IFS=$'\n'                     # Split on newline only
OSHELLOPTS="$SHELLOPTS"; set -o noglob    # Wildcards must not expand twice

select file in $(find -type f -name 'file*'); do echo $file; break; done

IFS="$OIFS"
[[ "$OSHELLOPTS" !~ noglob ]] && set +o noglob

5
สมมติว่าIFS=$'\n'ปลอดภัยไม่มีมูลความจริง ชื่อไฟล์สามารถประกอบด้วยตัวอักษรขึ้นบรรทัดใหม่ได้อย่างสมบูรณ์แบบ
Charles Duffy

4
ฉันลังเลที่จะยอมรับการยืนยันดังกล่าวเกี่ยวกับชุดข้อมูลที่เป็นไปได้ของบุคคลที่มีมูลค่าแม้ว่าจะมีอยู่ก็ตาม เหตุการณ์การสูญเสียข้อมูลที่เลวร้ายที่สุดที่ฉันเคยพบคือกรณีที่สคริปต์การบำรุงรักษาที่รับผิดชอบในการล้างข้อมูลสำรองเก่าพยายามลบไฟล์ที่สร้างขึ้นโดยสคริปต์ Python โดยใช้โมดูล C ที่มีตัวชี้ที่ไม่ดีซึ่งทิ้งขยะแบบสุ่ม - รวมถึงตัวแทนที่คั่นด้วยช่องว่าง - ลงในชื่อ
Charles Duffy

2
คนอาคารเชลล์สคริปต์ทำล้างไฟล์เหล่านั้นไม่ได้รำคาญที่จะพูดเพราะชื่อ "ไม่อาจ" [0-9a-f]{24}ล้มเหลวเพื่อให้ตรงกับ การสำรองข้อมูลที่ใช้เพื่อสนับสนุนการเรียกเก็บเงินลูกค้าหายไป
Charles Duffy

4
เห็นด้วยกับ @CharlesDuffy อย่างสมบูรณ์ การจัดการกับเคสขอบไม่ได้ผลเมื่อคุณทำงานแบบโต้ตอบและสามารถเห็นสิ่งที่คุณทำ selectโดยการออกแบบของมันมีไว้สำหรับโซลูชันที่เขียนสคริปต์ดังนั้นจึงควรได้รับการออกแบบมาเพื่อจัดการกับขอบเคส
Wildcard

2
@ilkkachu แน่นอน - คุณไม่เคยโทรselectจากเชลล์ที่คุณพิมพ์คำสั่งให้เรียกใช้ แต่เฉพาะสคริปต์ที่คุณตอบรับคำแจ้งจากสคริปต์นั้นและสคริปต์นั้นอยู่ที่ไหน ดำเนินการตรรกะที่กำหนดไว้ล่วงหน้า (สร้างโดยไม่มีความรู้เกี่ยวกับชื่อไฟล์ที่กำลังดำเนินการอยู่) ตามข้อมูลนั้น
Charles Duffy

4

ฉันอาจอยู่นอกเขตอำนาจของฉันที่นี่ แต่บางทีคุณอาจเริ่มต้นด้วยสิ่งนี้อย่างน้อยก็ไม่ได้มีปัญหากับช่องว่าง:

find -maxdepth 1 -type f -printf '%f\000' | {
    while read -d $'\000'; do
            echo "$REPLY"
            echo
    done
}

เพื่อหลีกเลี่ยงการสันนิษฐานเท็จที่อาจเกิดขึ้นดังที่ระบุไว้ในความคิดเห็นโปรดทราบว่ารหัสข้างต้นเทียบเท่ากับ:

   find -maxdepth 1 -type f -printf '%f\0' | {
        while read -d ''; do
                echo "$REPLY"
                echo
        done
    }

read -dเป็นทางออกที่ฉลาด ขอบคุณสำหรับสิ่งนี้.
DopeGhoti

2
read -d $'\000'คือว่าเหมือนกันread -d ''แต่สำหรับคนที่ทำให้เข้าใจผิดเกี่ยวกับความสามารถของทุบตี (นัยว่าไม่ถูกต้องว่ามันเป็นความสามารถที่จะเป็นตัวแทนของ NULs อักษรภายในสตริง) เรียกใช้s1=$'foo\000bar'; s2='foo'แล้วลองค้นหาวิธีแยกความแตกต่างระหว่างค่าทั้งสอง (เวอร์ชันในอนาคตอาจกลับสู่สภาวะปกติด้วยพฤติกรรมการแทนที่คำสั่งด้วยการทำให้ค่าที่เก็บไว้เทียบเท่าfoobarแต่ไม่ใช่ในวันนี้)
Charles Duffy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.