ฉันจะเก็บผลลัพธ์คำสั่ง“ find” เป็นอาร์เรย์ใน Bash ได้อย่างไร


93

ฉันพยายามบันทึกผลลัพธ์จากfindas arrays นี่คือรหัสของฉัน:

#!/bin/bash

echo "input : "
read input

echo "searching file with this pattern '${input}' under present directory"
array=`find . -name ${input}`

len=${#array[*]}
echo "found : ${len}"

i=0

while [ $i -lt $len ]
do
echo ${array[$i]}
let i++
done

ฉันได้รับ 2 ไฟล์. txt ภายใต้ไดเร็กทอรีปัจจุบัน ดังนั้นผมจึงคาดหวังที่ '2' ${len}เป็นผลมาจาก อย่างไรก็ตามมันพิมพ์ 1 เหตุผลก็คือมันใช้ผลลัพธ์ทั้งหมดfindเป็นองค์ประกอบเดียว ฉันจะแก้ไขปัญหานี้ได้อย่างไร?

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

คำตอบ:


134

อัปเดต 2020 สำหรับผู้ใช้ Linux:

หากคุณมีรุ่นขึ้นไปวันที่ทุบตี (4.4-alpha หรือดีกว่า) ในขณะที่คุณอาจจะทำอย่างไรถ้าคุณอยู่ในลินุกซ์แล้วคุณควรจะใช้คำตอบของเบนจามินดับบลิวของ

หากคุณใช้ Mac OS ซึ่ง - ฉันตรวจสอบครั้งล่าสุด - ยังคงใช้ bash 3.2 หรือใช้ bash รุ่นเก่าอยู่ให้ไปยังส่วนถัดไป

คำตอบสำหรับ bash 4.3 หรือก่อนหน้า

นี่คือทางออกหนึ่งในการรับเอาต์พุตfindลงในbashอาร์เรย์:

array=()
while IFS=  read -r -d $'\0'; do
    array+=("$REPLY")
done < <(find . -name "${input}" -print0)

นี่เป็นเรื่องยุ่งยากเนื่องจากโดยทั่วไปชื่อไฟล์อาจมีช่องว่างบรรทัดใหม่และอักขระที่เป็นศัตรูกับสคริปต์อื่น ๆ วิธีเดียวที่จะใช้findและแยกชื่อไฟล์ออกจากกันอย่างปลอดภัยคือการใช้-print0ซึ่งพิมพ์ชื่อไฟล์ที่คั่นด้วยอักขระ null สิ่งนี้จะไม่สะดวกมากนักหาก bash readarray/ mapfileฟังก์ชั่นรองรับสตริงที่คั่นด้วย null แต่ไม่เป็นเช่นนั้น Bash readทำและนั่นนำเราไปสู่ลูปด้านบน

[คำตอบนี้เขียนขึ้นครั้งแรกในปี 2014 หากคุณมี bash เวอร์ชันล่าสุดโปรดดูการอัปเดตด้านล่าง]

มันทำงานอย่างไร

  1. บรรทัดแรกสร้างอาร์เรย์ว่าง: array=()

  2. ทุกครั้งที่ดำเนินการreadคำสั่งชื่อไฟล์ที่คั่นด้วย null จะถูกอ่านจากอินพุตมาตรฐาน -rตัวเลือกที่จะบอกreadให้ออกไปจากตัวอักษรทับขวาเพียงอย่างเดียว -d $'\0'บอกreadว่าการป้อนข้อมูลจะเป็นโมฆะคั่น เนื่องจากเราละเว้นชื่อเปลือกทำให้การป้อนข้อมูลลงในชื่อเริ่มต้น:readREPLY

  3. งบผนวกชื่อไฟล์ใหม่อาร์เรย์array+=("$REPLY")array

  4. บรรทัดสุดท้ายรวมการเปลี่ยนเส้นทางและการแทนที่คำสั่งเพื่อจัดเตรียมเอาต์พุตfindไปยังอินพุตมาตรฐานของwhileลูป

เหตุใดจึงต้องใช้การทดแทนกระบวนการ

หากเราไม่ได้ใช้การทดแทนกระบวนการลูปสามารถเขียนเป็น:

array=()
find . -name "${input}" -print0 >tmpfile
while IFS=  read -r -d $'\0'; do
    array+=("$REPLY")
done <tmpfile
rm -f tmpfile

ในด้านบนผลลัพธ์ของfindจะถูกเก็บไว้ในไฟล์ชั่วคราวและไฟล์นั้นถูกใช้เป็นอินพุตมาตรฐานของลูป while แนวคิดในการทดแทนกระบวนการคือการทำให้ไฟล์ชั่วคราวดังกล่าวไม่จำเป็น ดังนั้นแทนที่จะให้whileลูปได้รับ stdin จากtmpfileเราสามารถให้มันรับ stdin <(find . -name ${input} -print0)ได้

การทดแทนกระบวนการมีประโยชน์อย่างกว้างขวาง ในหลาย ๆ ที่ที่คำสั่งต้องการอ่านจากไฟล์คุณสามารถระบุการทดแทนกระบวนการแทน<(...)ชื่อไฟล์ มีรูปแบบที่คล้ายคลึงกัน>(...)ซึ่งสามารถใช้แทนชื่อไฟล์ที่คำสั่งต้องการเขียนไปยังไฟล์

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

ทางเลือก: lastpipe

หากต้องการlastpipeสามารถใช้แทนการทดแทนกระบวนการ (ปลายหมวก: ซีซาร์ ):

set +m
shopt -s lastpipe
array=()
find . -name "${input}" -print0 | while IFS=  read -r -d $'\0'; do array+=("$REPLY"); done; declare -p array

shopt -s lastpipeบอกให้ bash รันคำสั่งสุดท้ายในไปป์ไลน์ในเชลล์ปัจจุบัน (ไม่ใช่พื้นหลัง) ด้วยวิธีนี้สิ่งที่arrayเหลืออยู่หลังจากท่อส่งเสร็จสมบูรณ์ เพราะจะมีผลหากการควบคุมงานถูกปิดเราทำงานlastpipe set +m(ในสคริปต์ซึ่งตรงข้ามกับบรรทัดคำสั่งการควบคุมงานจะปิดโดยค่าเริ่มต้น)

หมายเหตุเพิ่มเติม

คำสั่งต่อไปนี้สร้างตัวแปรเชลล์ไม่ใช่เชลล์อาร์เรย์:

array=`find . -name "${input}"`

หากคุณต้องการสร้างอาร์เรย์คุณจะต้องใส่ parens รอบ ๆ ผลลัพธ์ของ find อย่างไร้เดียงสาเราสามารถ:

array=(`find . -name "${input}"`)  # don't do this

ปัญหาคือเชลล์ทำการแยกคำกับผลลัพธ์findเพื่อไม่ให้องค์ประกอบของอาร์เรย์เป็นสิ่งที่คุณต้องการ

อัปเดต 2019

ตั้งแต่เวอร์ชัน 4.4-alpha ตอนนี้ bash สนับสนุน-dตัวเลือกเพื่อให้ไม่จำเป็นต้องใช้ลูปด้านบนอีกต่อไป สามารถใช้:

mapfile -d $'\0' array < <(find . -name "${input}" -print0)

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้โปรดดู (และ upvote) คำตอบของเบนจามินดับบลิวของ


1
@JuneyoungOh ดีใจที่มันช่วย ฉันเพิ่มส่วนของการทดแทนกระบวนการ
John1024

3
@ Rockallite นั่นเป็นข้อสังเกตที่ดี แต่ไม่สมบูรณ์ แม้ว่าจะเป็นความจริงที่เราไม่ได้แยกเป็นหลายคำ แต่เราก็ยังต้องIFS=หลีกเลี่ยงการเว้นช่องว่างจากจุดเริ่มต้นหรือจุดสิ้นสุดของบรรทัดอินพุต คุณสามารถทดสอบสิ่งนี้ได้อย่างง่ายดายโดยเปรียบเทียบผลลัพธ์ของ read var <<<' abc '; echo ">$var<"กับผลลัพธ์ของIFS= read var <<<' abc '; echo ">$var<". ในกรณีเดิมช่องว่างก่อนและหลังabcจะถูกลบออก อย่างหลังพวกเขาไม่ ชื่อไฟล์ที่ขึ้นต้นหรือลงท้ายด้วยช่องว่างอาจผิดปกติ แต่มีอยู่เราต้องการให้ประมวลผลอย่างถูกต้อง
John1024

1
สวัสดีหลังจากที่ฉันรันโค้ดของคุณแล้วฉันได้รับข้อผิดพลาดทางไวยากรณ์ของข้อความใกล้กับโทเค็นที่ไม่คาดคิด<' เสร็จสิ้น <(ค้นหา aaa / -not -newermt "$ last_build_timestamp_v" -type f -print0) '
Przemysław Sienkiewicz

1
หมายเหตุ: ''สามารถใช้วิธีที่ง่ายกว่าแทน$'\0':n=0; while IFS= read -r -d '' line || [ "$line" ]; do echo "$((++n)):$line"; done < <(printf 'first\nstill first\0second\0third')
glenn jackman

1
@theeagle BLAH=$(find . -name '*.php')ฉันคิดว่าคุณตั้งใจที่จะเขียน ตามที่กล่าวไว้ในคำตอบแนวทางดังกล่าวจะใช้ได้ในบางกรณี แต่จะใช้ไม่ได้โดยทั่วไปกับชื่อไฟล์ทั้งหมดและไม่ได้สร้างอาร์เรย์ตามที่ OP คาดไว้
John1024

35

Bash 4.4 แนะนำ-dตัวเลือกสำหรับreadarray/ mapfileดังนั้นตอนนี้จึงสามารถแก้ไขได้ด้วย

readarray -d '' array < <(find . -name "$input" -print0)

สำหรับวิธีการที่ใช้งานได้กับชื่อไฟล์ที่กำหนดเองซึ่งรวมถึงช่องว่างบรรทัดใหม่และอักขระ globbing สิ่งนี้ต้องการการfindสนับสนุนของคุณ-print0เช่น GNU find ทำ

จากคู่มือ (ละเว้นตัวเลือกอื่น ๆ ):

mapfile [-d delim] [array]

-d
อักขระตัวแรกของdelimถูกใช้เพื่อยุติบรรทัดอินพุตแต่ละบรรทัดแทนที่จะขึ้นบรรทัดใหม่ ถ้าdelimเป็นสตริงว่างmapfileจะยุติบรรทัดเมื่ออ่านอักขระ NUL

และเป็นเพียงคำพ้องความหมายของreadarraymapfile


18

หากคุณใช้bash4 หรือใหม่กว่าคุณสามารถแทนที่การใช้งานfindด้วย

shopt -s globstar nullglob
array=( **/*"$input"* )

**รูปแบบการใช้งานโดยglobstarการแข่งขัน 0 หรือไดเรกทอรีมากขึ้นช่วยให้รูปแบบเพื่อให้ตรงกับความลึกโดยพลการในไดเรกทอรีปัจจุบัน หากไม่มีnullglobตัวเลือกรูปแบบ (หลังจากการขยายพารามิเตอร์) จะได้รับการปฏิบัติตามตัวอักษรดังนั้นหากไม่มีการจับคู่คุณจะมีอาร์เรย์ที่มีสตริงเดียวแทนที่จะเป็นอาร์เรย์ว่างเปล่า

เพิ่มdotglobตัวเลือกในบรรทัดแรกเช่นกันหากคุณต้องการสำรวจไดเรกทอรีที่ซ่อนอยู่ (เช่น.ssh) และจับคู่ไฟล์ที่ซ่อนอยู่ (เช่น.bashrc) ด้วย


4
อาจจะnullglobเกินไป…
kojiro

1
ใช่ฉันมักจะลืมสิ่งนั้น
chepner

5
โปรดทราบว่าสิ่งนี้จะไม่รวมไฟล์และไดเร็กทอรีที่ซ่อนอยู่เว้นแต่dotglobจะถูกตั้งค่าไว้ (อาจเป็นหรือไม่ต้องการก็ได้ แต่ก็ควรกล่าวถึงเช่นกัน)
gniourf_gniourf

10

คุณสามารถลองสิ่งที่ชอบ

array=(`find . -type f | sort -r | head -2`)
และในการพิมพ์ค่าอาร์เรย์คุณสามารถลองใช้บางอย่างเช่น echo "${array[*]}"


8
หยุดพักหากมีชื่อไฟล์ที่มีช่องว่างหรืออักขระลูกโลก
gniourf_gniourf

1

สิ่งต่อไปนี้ดูเหมือนจะใช้ได้กับทั้ง Bash และ Z Shell บน macOS

#! /bin/sh

IFS=$'\n'
paths=($(find . -name "foo"))
unset IFS

printf "%s\n" "${paths[@]}"

สิ่งนี้ใช้ได้กับไฟล์ที่มีช่องว่างและอักขระพิเศษอื่น ๆ ล้มเหลวด้วยกรณีของไฟล์ที่มี linebreak ในชื่อ คุณสามารถสร้างเพื่อทดสอบกับprintf "%b" "file name with spaces, a star * ...\012and a second line\0" | xargs -0 touch
Stéphane Gourichon

-1

ใน bash $(<any_shell_cmd>)ช่วยในการรันคำสั่งและจับเอาต์พุต การส่งผ่านสิ่งนี้ไปIFSด้วย\nตัวคั่นช่วยในการแปลงเป็นอาร์เรย์

IFS='\n' read -r -a txt_files <<< $(find /path/to/dir -name "*.txt")

4
สิ่งนี้จะได้รับเฉพาะไฟล์แรกของผลลัพธ์findในอาร์เรย์
Benjamin W.

-2

คุณสามารถทำได้ดังนี้:

#!/bin/bash
echo "input : "
read input

echo "searching file with this pattern '${input}' under present directory"
array=(`find . -name '*'${input}'*'`)

for i in "${array[@]}"
do :
    echo $i
done

1
ขอบคุณ. มาก. แต่อย่างที่ @anishsane ชี้ไปควรพิจารณาช่องว่างในชื่อไฟล์ในโปรแกรมของฉัน ขอบคุณมาก!
Juneyoung Oh

-3

สำหรับฉันสิ่งนี้ใช้ได้ดีกับ cygwin:

declare -a names=$(echo "("; find <path> <other options> -printf '"%p" '; echo ")")
for nm in "${names[@]}"
do
    echo "$nm"
done

สิ่งนี้ใช้ได้กับช่องว่าง แต่ใช้กับเครื่องหมายอัญประกาศคู่ (") ในชื่อไดเร็กทอรีไม่ได้ (ซึ่งไม่ได้รับอนุญาตในสภาพแวดล้อม Windows อยู่ดี)

ระวังช่องว่างในตัวเลือก -printf


3
เสียและเป็นอันตราย : จะไม่จัดการคำพูดและอาจมีการแทรกรหัสโดยอำเภอใจ ไม่ได้ใช้.
gniourf_gniourf

2
ดูเหมือนว่าจะมีคนแจ้งว่าโพสต์นี้ถูกลบ "มันไม่ถูกต้อง" ไม่ใช่เหตุผลในการลบ SO ผู้ใช้พยายามตอบโดยอยู่ในหัวข้อและตรงตามเกณฑ์สำหรับคำตอบ ปุ่มลงคะแนนใช้เพื่อวัดประโยชน์และความถูกต้องไม่ใช่ปุ่มลบ
Frambot

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