ฉันจะรับค่าเฉพาะจากอาร์เรย์ใน Bash ได้อย่างไร


93

ฉันมีเกือบคำถามเดียวกับที่นี่

ฉันมีอาร์เรย์ที่มีaa ab aa ac aa adฯลฯ ตอนนี้ฉันต้องการเลือกองค์ประกอบเฉพาะทั้งหมดจากอาร์เรย์นี้ คิดว่านี่จะเป็นเรื่องง่ายโดยมีsort | uniqหรือsort -uที่พวกเขากล่าวถึงในคำถามอื่น ๆ นั้น แต่ไม่มีอะไรเปลี่ยนแปลงในอาร์เรย์ ... รหัสคือ:

echo `echo "${ids[@]}" | sort | uniq`

ผมทำอะไรผิดหรือเปล่า?

คำตอบ:


131

แฮ็คนิดหน่อย แต่ควรทำ:

echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '

ในการบันทึกผลลัพธ์ที่ไม่ซ้ำกันที่จัดเรียงไว้กลับเข้าไปในอาร์เรย์ให้ทำการกำหนด Array :

sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))

หากเชลล์ของคุณรองรับสตริงที่นี่ ( bashควร) คุณสามารถสำรองechoกระบวนการได้โดยเปลี่ยนเป็น:

tr ' ' '\n' <<< "${ids[@]}" | sort -u | tr '\n' ' '

อินพุต:

ids=(aa ab aa ac aa ad)

เอาท์พุต:

aa ab ac ad

คำอธิบาย:

  • "${ids[@]}"- ไวยากรณ์สำหรับการทำงานกับเชลล์อาร์เรย์ไม่ว่าจะใช้เป็นส่วนหนึ่งของechoหรือต่อท้าย @ส่วนหนึ่งหมายถึง "องค์ประกอบทั้งหมดในอาร์เรย์"
  • tr ' ' '\n'- แปลงช่องว่างทั้งหมดเป็นบรรทัดใหม่ เนื่องจากอาร์เรย์ของคุณถูกมองโดยเชลล์เป็นองค์ประกอบในบรรทัดเดียวคั่นด้วยช่องว่าง และเนื่องจากการจัดเรียงคาดว่าอินพุตจะอยู่ในบรรทัดแยกกัน
  • sort -u - จัดเรียงและรักษาเฉพาะองค์ประกอบที่เป็นเอกลักษณ์
  • tr '\n' ' ' - แปลงบรรทัดใหม่ที่เราเพิ่มไว้ก่อนหน้านี้กลับเป็นช่องว่าง
  • $(...)- การแทนที่คำสั่ง
  • นอกเหนือ: tr ' ' '\n' <<< "${ids[@]}"เป็นวิธีที่มีประสิทธิภาพมากขึ้นในการทำ:echo "${ids[@]}" | tr ' ' '\n'

37
+1. เป็นระเบียบมากขึ้น: เก็บองค์ประกอบ uniq ในอาร์เรย์ใหม่:uniq=($(printf "%s\n" "${ids[@]}" | sort -u)); echo "${uniq[@]}"
glenn jackman

@glennjackman โอ้เรียบร้อย! ฉันไม่รู้ด้วยซ้ำว่าคุณสามารถใช้printfวิธีนั้นได้ (ให้อาร์กิวเมนต์มากกว่าสตริงรูปแบบ)
sampson-chen

4
+1 ผมไม่แน่ใจว่าถ้าเป็นกรณีที่แยก sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))แต่วางรายการที่ไม่ซ้ำกลับเข้ามาในอาร์เรย์ที่จำเป็นในวงเล็บเพิ่มเติมเช่น: หากไม่มีวงเล็บเพิ่มเติมก็จะให้เป็นสตริง
whla

3
หากคุณไม่ต้องการที่จะเปลี่ยนแปลงคำสั่งขององค์ประกอบการใช้งานแทน... | uniq | ... ... | sort -u | ...
Jesse Chisholm

2
@Jesse, uniqเพียงลบติดต่อกันซ้ำกัน ในตัวอย่างในคำตอบนี้จะจบลงเหมือนเดิมsorted_unique_ids เพื่อรักษาเพื่อลองids ... | awk '!seen[$0]++'ดูstackoverflow.com/questions/1444406/…ด้วย
Rob Kennedy

29

หากคุณใช้ Bash เวอร์ชัน 4 ขึ้นไป (ซึ่งควรเป็นเช่นนั้นใน Linux เวอร์ชันใหม่ ๆ ) คุณสามารถรับค่าอาร์เรย์ที่ไม่ซ้ำกันใน bash ได้โดยการสร้างอาร์เรย์ที่เชื่อมโยงใหม่ซึ่งมีค่าแต่ละค่าของอาร์เรย์เดิม สิ่งนี้:

$ a=(aa ac aa ad "ac ad")
$ declare -A b
$ for i in "${a[@]}"; do b["$i"]=1; done
$ printf '%s\n' "${!b[@]}"
ac ad
ac
aa
ad

สิ่งนี้ใช้ได้ผลเพราะในอาร์เรย์ใด ๆ (เชื่อมโยงหรือดั้งเดิมในภาษาใดก็ได้) แต่ละคีย์จะปรากฏได้เพียงครั้งเดียว เมื่อforวงมาถึงที่คุ้มค่าที่สองของaaในa[2]ก็เขียนทับซึ่งเป็นชุดเดิมสำหรับb[aa]a[0]

การทำสิ่งต่างๆด้วยการทุบตีแบบดั้งเดิมทำได้เร็วกว่าการใช้ไปป์และเครื่องมือภายนอกเช่นsortและuniqแม้ว่าสำหรับชุดข้อมูลขนาดใหญ่คุณจะเห็นประสิทธิภาพที่ดีขึ้นหากคุณใช้ภาษาที่มีประสิทธิภาพมากขึ้นเช่น awk, python เป็นต้น

ถ้าคุณรู้สึกมั่นใจคุณสามารถหลีกเลี่ยงforวงโดยใช้ความสามารถที่จะนำรูปแบบสำหรับอาร์กิวเมนต์หลายแม้เรื่องนี้ดูเหมือนว่าจะต้องprintf eval(หยุดอ่านตอนนี้ถ้าคุณสบายดี)

$ eval b=( $(printf ' ["%s"]=1' "${a[@]}") )
$ declare -p b
declare -A b=(["ac ad"]="1" [ac]="1" [aa]="1" [ad]="1" )

เหตุผลที่โซลูชันนี้ต้องการevalคือค่าอาร์เรย์จะถูกกำหนดก่อนการแยกคำ นั่นหมายความว่าเอาต์พุตของการแทนที่คำสั่งถือเป็นคำเดี่ยวแทนที่จะเป็นชุดของคู่คีย์ = ค่า

แม้ว่าจะใช้ subshell แต่ก็ใช้ bash builtins เท่านั้นในการประมวลผลค่าอาร์เรย์ อย่าลืมประเมินการใช้งานของคุณevalด้วยสายตาที่สำคัญ หากคุณไม่มั่นใจ 100% ว่า chepner หรือ glenn jackman หรือ greycat จะไม่พบข้อผิดพลาดกับรหัสของคุณให้ใช้ for loop แทน


สร้างข้อผิดพลาด: เกินระดับการเรียกซ้ำของนิพจน์
Benubird

1
@ Benubird - คุณสามารถวางเนื้อหาเทอร์มินัลของคุณได้หรือไม่? มันทำงานได้อย่างสมบูรณ์แบบสำหรับฉันดังนั้นการคาดเดาที่ดีที่สุดของฉันคือคุณมี (1) การพิมพ์ผิด (2) bash เวอร์ชันเก่า (อาร์เรย์ที่เชื่อมโยงถูกเพิ่มลงใน v4) หรือ (3) การไหลบ่าเข้ามาของพื้นหลังจักรวาลจำนวนมากอย่างน่าขัน การแผ่รังสีที่เกิดจากหลุมดำควอนตัมในห้องใต้ดินของเพื่อนบ้านทำให้เกิดการรบกวนสัญญาณภายในคอมพิวเตอร์ของคุณ
ghoti

1
ไม่ได้ไม่ได้เก็บสิ่งที่ไม่ได้ผล แต่ตอนนี้ฉันลองวิ่งของคุณแล้วมันก็ได้ผลดังนั้นอาจจะเป็นรังสีคอสมิก
Benubird

เดาว่าคำตอบนี้ใช้ bash v4 (Associative arrays) และถ้ามีคนลอง bash v3 มันจะไม่ได้ผล (อาจไม่ใช่สิ่งที่ @Benubird เห็น) Bash v3 ยังคงเป็นค่าเริ่มต้นในหลาย ๆ
สภาพแวดล้อม

1
@nhed จุดที่ถ่าย ฉันเห็นว่า Yosemite Macbook เวอร์ชันล่าสุดของฉันมีเวอร์ชันพื้นฐานเหมือนกันแม้ว่าฉันจะติดตั้ง v4 จาก macports แล้วก็ตาม คำถามนี้ติดแท็ก "linux" แต่ฉันได้อัปเดตคำตอบเพื่อชี้ให้เห็นข้อกำหนด
ghoti

18

ฉันรู้ว่าคำตอบนี้ได้รับคำตอบแล้ว แต่ก็มีผลการค้นหาค่อนข้างสูงและอาจช่วยใครบางคนได้

printf "%s\n" "${IDS[@]}" | sort -u

ตัวอย่าง:

~> IDS=( "aa" "ab" "aa" "ac" "aa" "ad" )
~> echo  "${IDS[@]}"
aa ab aa ac aa ad
~>
~> printf "%s\n" "${IDS[@]}" | sort -u
aa
ab
ac
ad
~> UNIQ_IDS=($(printf "%s\n" "${IDS[@]}" | sort -u))
~> echo "${UNIQ_IDS[@]}"
aa ab ac ad
~>

1
เพื่อแก้ไขอาร์เรย์ฉันถูกบังคับให้ทำสิ่งนี้: ids=(ab "a a" ac aa ad ac aa);IFS=$'\n' ids2=(`printf "%s\n" "${ids[@]}" |sort -u`)ดังนั้นฉันจึงIFS=$'\n'แนะนำโดย @gniourf_gniourf
Aquarius Power

ฉันต้องสำรองข้อมูลและหลังจากคำสั่งเรียกคืนค่า IFS! หรือมันไปยุ่งเรื่องอื่น ..
Aquarius Power

@Jetse นี่ควรเป็นคำตอบที่ยอมรับเนื่องจากใช้เพียงสองคำสั่งไม่มีลูปไม่มีการประเมินและเป็นเวอร์ชันที่กะทัดรัดที่สุด
mgutt

1
@AquariusPower ระวังโดยพื้นฐานแล้วคุณกำลังทำ: IFS=$'\n'; ids2=(...)เนื่องจากการมอบหมายชั่วคราวก่อนการกำหนดตัวแปรเป็นไปไม่ได้ ใช้โครงสร้างนี้แทน: IFS=$'\n' read -r -a ids2 <<<"$(printf "%s\n" "${ids[@]}" | sort -u)".
เยติ

13

หากองค์ประกอบอาร์เรย์ของคุณมีช่องว่างสีขาวหรืออักขระพิเศษของเชลล์อื่น ๆ (และคุณแน่ใจได้หรือไม่ว่าไม่มี) ให้จับสิ่งเหล่านั้นก่อนอื่น (และคุณควรทำสิ่งนี้เสมอ) แสดงอาร์เรย์ของคุณด้วยเครื่องหมายคำพูดคู่! เช่น"${a[@]}". Bash จะแปลสิ่งนี้ตามตัวอักษรว่า "แต่ละองค์ประกอบอาร์เรย์ในอาร์กิวเมนต์แยกต่างหาก" ภายในทุบตีสิ่งนี้ก็ใช้ได้ผลเสมอ

จากนั้นเพื่อให้ได้อาร์เรย์ที่จัดเรียง (และไม่ซ้ำกัน) เราต้องแปลงเป็นอาร์เรย์รูปแบบที่เข้าใจและสามารถแปลงกลับเป็นองค์ประกอบ bash array นี่คือสิ่งที่ดีที่สุดที่ฉันคิดขึ้นมา:

eval a=($(printf "%q\n" "${a[@]}" | sort -u))

น่าเสียดายที่สิ่งนี้ล้มเหลวในกรณีพิเศษของอาร์เรย์ว่างโดยเปลี่ยนอาร์เรย์ว่างให้เป็นอาร์เรย์ขององค์ประกอบว่าง 1 รายการ (เนื่องจาก printf มีอาร์กิวเมนต์ 0 แต่ยังคงพิมพ์ออกมาราวกับว่ามีอาร์กิวเมนต์ว่างหนึ่งรายการ - ดูคำอธิบาย) ดังนั้นคุณต้องจับสิ่งนั้นใน if หรือบางอย่าง

คำอธิบาย: รูปแบบ% q สำหรับ printf "เชลล์หลบหนี" อาร์กิวเมนต์ที่พิมพ์ในลักษณะที่ bash สามารถกู้คืนได้ในบางสิ่งเช่น eval! เนื่องจากแต่ละองค์ประกอบถูกพิมพ์เชลล์ที่ใช้ Escape ในบรรทัดของตัวเองตัวคั่นเดียวระหว่างองค์ประกอบคือขึ้นบรรทัดใหม่และการกำหนดอาร์เรย์จะใช้แต่ละบรรทัดเป็นองค์ประกอบโดยแยกวิเคราะห์ค่าที่ใช้ Escape เป็นข้อความตามตัวอักษร

เช่น

> a=("foo bar" baz)
> printf "%q\n" "${a[@]}"
'foo bar'
baz
> printf "%q\n"
''

eval เป็นสิ่งที่จำเป็นในการดึงค่า Escape ออกจากแต่ละค่ากลับเข้าไปในอาร์เรย์


นี่เป็นรหัสเดียวที่ใช้ได้สำหรับฉันเพราะอาร์เรย์ของสตริงมีช่องว่าง % q คือสิ่งที่ทำเคล็ดลับ ขอบคุณ :)
Somaiah Kumbera

และถ้าคุณไม่ต้องการที่จะเปลี่ยนแปลงคำสั่งขององค์ประกอบที่ใช้แทนuniq sort -u
Jesse Chisholm

โปรดทราบว่าuniqไม่สามารถทำงานได้อย่างถูกต้องในรายการที่ไม่ได้เรียงลำดับดังนั้นจึงต้องใช้ร่วมกับsortไฟล์.
Jean Paul

UNIQ ในรายการที่ไม่ได้เรียงลำดับจะลบติดต่อกันซ้ำกัน จะไม่ลบองค์ประกอบรายการที่เหมือนกันคั่นด้วยสิ่งอื่นในระหว่าง uniq อาจมีประโยชน์เพียงพอขึ้นอยู่กับข้อมูลที่คาดหวังและความปรารถนาที่จะรักษาคำสั่งเดิม
vontrapp

10

'sort' สามารถใช้เพื่อสั่งซื้อผลลัพธ์ของ for-loop:

for i in ${ids[@]}; do echo $i; done | sort

และกำจัดรายการที่ซ้ำกันด้วย "-u":

for i in ${ids[@]}; do echo $i; done | sort -u

ในที่สุดคุณก็สามารถเขียนทับอาร์เรย์ของคุณด้วยองค์ประกอบเฉพาะ:

ids=( `for i in ${ids[@]}; do echo $i; done | sort -u` )

และถ้าคุณไม่ต้องการเปลี่ยนลำดับของสิ่งที่เหลือคุณไม่จำเป็นต้อง:ids=( `for i in ${ids[@]}; do echo $i; done | uniq` )
Jesse Chisholm

3

สิ่งนี้จะรักษาคำสั่งซื้อ:

echo ${ARRAY[@]} | tr [:space:] '\n' | awk '!a[$0]++'

และแก้ไขอาร์เรย์เดิมด้วยค่าเฉพาะ:

ARRAY=($(echo ${ARRAY[@]} | tr [:space:] '\n' | awk '!a[$0]++'))

อย่าใช้uniq. มันต้องการการเรียงลำดับโดยที่ awk ไม่ได้และจุดประสงค์ของคำตอบนี้คือเพื่อรักษาการจัดลำดับเมื่ออินพุตไม่ถูกเรียงลำดับ
bukzor

2

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

ลบรายการที่ซ้ำกัน (ด้วยการเรียงลำดับ)

readarray -t NewArray < <(printf '%s\n' "${OriginalArray[@]}" | sort -u)

ลบรายการที่ซ้ำกัน (โดยไม่ต้องเรียงลำดับ)

readarray -t NewArray < <(printf '%s\n' "${OriginalArray[@]}" | awk '!x[$0]++')

คำเตือน: NewArray=( $(printf '%s\n' "${OriginalArray[@]}" | sort -u) )อย่าพยายามที่จะทำสิ่งที่ชอบ มันจะแตกบนช่องว่าง


ลบรายการที่ซ้ำกัน (ไม่เรียงลำดับ) เป็นเช่นเดียวกับ (ด้วยการเรียงลำดับ) ยกเว้นการเปลี่ยนแปลงที่จะเป็นsort -u uniq
Jesse Chisholm

@JesseChisholm เพียงผสานเส้นที่ซ้ำกันที่อยู่ติดดังนั้นจึงไม่เหมือนกันuniq awk '!x[$0]++'
หก

@JesseChisholm กรุณาลบความคิดเห็นที่ทำให้เข้าใจผิด
bukzor

2

cat number.txt

1 2 3 4 4 3 2 5 6

พิมพ์บรรทัดลงในคอลัมน์: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}'

1
2
3
4
4
3
2
5
6

ค้นหาระเบียนที่ซ้ำกัน: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk 'x[$0]++'

4
3
2

แทนที่ระเบียนที่ซ้ำกัน: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk '!x[$0]++'

1
2
3
4
5
6

ค้นหาเฉพาะระเบียน Uniq: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i|"sort|uniq -u"}

1
5
6


1

หากคุณต้องการโซลูชันที่ใช้เฉพาะ bash ภายในคุณสามารถตั้งค่าเป็นคีย์ในอาร์เรย์ที่เชื่อมโยงจากนั้นแยกคีย์:

declare -A uniqs
list=(foo bar bar "bar none")
for f in "${list[@]}"; do 
  uniqs["${f}"]=""
done

for thing in "${!uniqs[@]}"; do
  echo "${thing}"
done

สิ่งนี้จะส่งออก

bar
foo
bar none

ฉันเพิ่งสังเกตว่านี่เหมือนกับคำตอบของ @ghotis ข้างต้นยกเว้นวิธีการแก้ปัญหาของเขาไม่ได้คำนึงถึงรายการที่มีช่องว่าง
rln

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

1

อีกทางเลือกหนึ่งสำหรับการจัดการกับช่องว่างที่ฝังไว้คือการคั่นด้วยค่าว่างprintfทำให้แตกต่างsortจากนั้นใช้ลูปเพื่อบรรจุกลับเข้าไปในอาร์เรย์:

input=(a b c "$(printf "d\ne")" b c "$(printf "d\ne")")
output=()

while read -rd $'' element
do 
  output+=("$element")
done < <(printf "%s\0" "${input[@]}" | sort -uz)

ในตอนท้ายของสิ่งนี้inputและoutputมีค่าที่ต้องการ (ลำดับที่ระบุไม่สำคัญ):

$ printf "%q\n" "${input[@]}"
a
b
c
$'d\ne'
b
c
$'d\ne'

$ printf "%q\n" "${output[@]}"
a
b
c
$'d\ne'



-3
# Read a file into variable
lines=$(cat /path/to/my/file)

# Go through each line the file put in the variable, and assign it a variable called $line
for line in $lines; do
  # Print the line
  echo $line
# End the loop, then sort it (add -u to have unique lines)
done | sort -u
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.