เหตุใดการตัดจึงล้มเหลวด้วยการทุบตีและไม่ zsh


10

ฉันสร้างไฟล์ที่มีฟิลด์คั่นด้วยแท็บ

echo foo$'\t'bar$'\t'baz$'\n'foo$'\t'bar$'\t'baz > input

ฉันมีสคริปต์ชื่อต่อไปนี้ zsh.sh

#!/usr/bin/env zsh
while read line; do
    <<<$line cut -f 2
done < "$1"

ฉันทดสอบมัน

$ ./zsh.sh input
bar
bar

มันใช้งานได้ดี อย่างไรก็ตามเมื่อฉันเปลี่ยนบรรทัดแรกเพื่อเรียกใช้bashแทนจะล้มเหลว

$ ./bash.sh input
foo bar baz
foo bar baz

ทำไมสิ่งนี้ถึงล้มเหลวbashและทำงานกับzsh?

การแก้ไขปัญหาเพิ่มเติม

  • การใช้เส้นทางตรงใน shebang แทนที่จะenvสร้างพฤติกรรมแบบเดียวกัน
  • ไปป์กับechoแทนที่จะใช้ที่นี่สตริง<<<$lineยังสร้างลักษณะการทำงานเดียวกัน echo $line | cut -f 2กล่าวคือ
  • ใช้awkแทนการcut ทำงานสำหรับเปลือกทั้งสอง <<<$line awk '{print $2}'กล่าวคือ

4
โดยวิธีการที่คุณสามารถทำให้ไฟล์ทดสอบของคุณมากขึ้นได้ง่ายๆโดยการทำอย่างใดอย่างหนึ่งต่อไปนี้: echo -e 'foo\tbar\tbaz\n...', echo $'foo\tbar\tbaz\n...'หรือprintf 'foo\tbar\tbaz\n...\n'หรือรูปแบบเหล่านี้ มันช่วยให้คุณไม่ต้องห่อแต่ละแท็บหรือขึ้นบรรทัดใหม่
หยุดชั่วคราวจนกว่าจะมีการแจ้งให้ทราบต่อไป

คำตอบ:


13

สิ่งที่เกิดขึ้นคือbashแทนที่แท็บด้วยช่องว่าง คุณสามารถหลีกเลี่ยงปัญหานี้โดยการพูด"$line"แทนหรือโดยการตัดช่องว่างอย่างชัดเจน


1
มีเหตุผลใดที่ Bash เห็น\tและแทนที่ด้วยช่องว่าง?
user1717828

@ user1717828 ใช่มันเรียกว่าผู้ประกอบการถ่มน้ำลาย + glob มันเกิดอะไรขึ้นเมื่อคุณใช้ตัวแปรที่ไม่ระบุใน bash และ shell ที่คล้ายกัน
terdon

1
@terdon ใน<<< $line, bashไม่แยก แต่ไม่ glob ไม่มีเหตุผลอะไรที่มันจะแยกออกจากที่นี่เพราะ<<<คาดว่าจะมีคำเดียว มันแยกแล้วร่วมในกรณีที่ซึ่งจะทำให้ความรู้สึกเล็ก ๆ น้อย ๆ และเป็นกับทุกการใช้งานเปลือกหอยอื่น ๆ ที่ได้รับการสนับสนุนก่อนหรือหลัง<<< bashIMO มันเป็นข้อผิดพลาด
Stéphane Chazelas

@ StéphaneChazelasยุติธรรมพอปัญหาอยู่ที่การแบ่งส่วนแล้ว
terdon

2
@ StéphaneChazelasไม่มีการแบ่ง (และ glob) บน bash 4.4

17

เพราะในการที่<<< $line, bashไม่แยกคำ ( แต่ไม่ globbing) ใน$lineขณะที่มันไม่ได้ยกมามีแล้วร่วมคำที่เกิดขึ้นกับตัวละครของช่องว่าง (และทำให้ว่าในแฟ้มชั่วคราวตามด้วยอักขระขึ้นบรรทัดใหม่และทำให้ stdin ของcut)

$ a=a,b,,c bash -c 'IFS=","; sed -n l <<< $a'
a b  c$

tabเกิดขึ้นเป็นค่าเริ่มต้นของ$IFS:

$ a=$'a\tb'  bash -c 'sed -n l <<< $a'
a b$

การแก้ปัญหาด้วยbashคือการพูดตัวแปร

$ a=$'a\tb' bash -c 'sed -n l <<< "$a"'
a\tb$

โปรดทราบว่ามันเป็นเพียงเปลือกหอยเท่านั้นที่ทำเช่นนั้น zsh(ที่<<<มาจากแรงบันดาลใจจากพอร์ตของ Unix rc) ksh93, mkshและyashที่ยังสนับสนุน<<<ไม่ได้ทำมัน

เมื่อมาถึงอาร์เรย์mksh, yashและzshเข้าร่วมในอักษรตัวแรกของ$IFS, bashและksh93พื้นที่

$ mksh -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1:2$
$ yash -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1:2$
$ ksh -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1 2$
$ zsh -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1:2$
$ bash -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1 2$

มีความแตกต่างระหว่างzsh/ yashและmksh(รุ่น R52 อย่างน้อย) เมื่อ$IFSว่างเปล่า:

$ mksh -c 'a=(1 2); IFS=; sed -n l <<< "${a[@]}"'
1 2$
$ zsh -c 'a=(1 2); IFS=; sed -n l <<< "${a[@]}"'
12$

พฤติกรรมนี้สอดคล้องกันมากขึ้นในทุก ๆ เชลล์เมื่อคุณใช้"${a[*]}"(ยกเว้นว่าmkshยังมีข้อผิดพลาดเมื่อ$IFSว่างเปล่า)

ในecho $line | ...นั้นคือตัวดำเนินการแยก + glob ปกติในเชลล์เหมือน Bourne ทั้งหมด แต่zsh(และปัญหาปกติที่เกี่ยวข้องกับecho)


1
คำตอบที่ยอดเยี่ยม! ขอบคุณ (+1) ฉันจะยอมรับผู้ถามตอบต่ำที่สุดเนื่องจากพวกเขาตอบคำถามได้ดีพอที่จะเปิดเผยความโง่เขลาของฉัน
Sparhawk

10

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

#!/usr/bin/env bash
while read line; do
    echo $line
done < "$1"

และ

#!/usr/bin/env zsh
while read line; do
    echo $line
done < "$1"

ตอนนี้เปรียบเทียบผลลัพธ์ของพวกเขา

$ bash.sh input 
foo bar baz
foo bar baz
$ zsh.sh input 
foo    bar    baz
foo    bar    baz

อย่างที่คุณเห็นเพราะคุณไม่ได้อ้าง$lineถึงแท็บจะตีความไม่ถูกต้องโดยการทุบตี Zsh ดูเหมือนจะจัดการกับสิ่งที่ดีกว่า ตอนนี้cutใช้\tเป็นตัวคั่นฟิลด์ตามค่าเริ่มต้น ดังนั้นเนื่องจากbashสคริปต์ของคุณกำลังกินแท็บ (เนื่องจากตัวแยก + + ตัวดำเนินการ glob) cutจะเห็นเพียงหนึ่งฟิลด์และทำงานตามนั้น สิ่งที่คุณกำลังทำอยู่จริงๆคือ:

$ echo "foo bar baz" | cut -f 2
foo bar baz

ดังนั้นเพื่อให้สคริปต์ทำงานตามที่คาดไว้ในทั้งสองเชลล์ให้อ้างอิงตัวแปรของคุณ:

while read line; do
    <<<"$line" cut -f 2
done < "$1"

จากนั้นทั้งสองเอาต์พุตเดียวกัน:

$ bash.sh input 
bar
bar
$ zsh.sh input 
bar
bar

คำตอบที่ยอดเยี่ยม! ขอบคุณ (+1) ฉันจะยอมรับผู้ถามตอบต่ำที่สุดเนื่องจากพวกเขาตอบคำถามได้ดีพอที่จะเปิดเผยความโง่เขลาของฉัน
Sparhawk

^ โหวตให้เป็นคำตอบเดียวที่ (ยัง) จริงรวมถึงการแก้ไขbash.sh
lauir

1

ดังที่ได้รับการตอบแล้ววิธีพกพาที่จะใช้ตัวแปรคือการอ้างอิงมัน:

$ printf '%s\t%s\t%s\n' foo bar baz
foo    bar    baz
$ l="$(printf '%s\t%s\t%s\n' foo bar baz)"
$ <<<$l     sed -n l
foo bar baz$

$ <<<"$l"   sed -n l
foo\tbar\tbaz$

มีความแตกต่างของการใช้งานในทุบตีกับสาย:

l="$(printf '%s\t%s\t%s\n' foo bar baz)"; <<<$l  sed -n l

นี่คือผลลัพธ์ของเชลล์ส่วนใหญ่:

/bin/sh         : foo bar baz$
/bin/b43sh      : foo bar baz$
/bin/bash       : foo bar baz$
/bin/b44sh      : foo\tbar\tbaz$
/bin/y2sh       : foo\tbar\tbaz$
/bin/ksh        : foo\tbar\tbaz$
/bin/ksh93      : foo\tbar\tbaz$
/bin/lksh       : foo\tbar\tbaz$
/bin/mksh       : foo\tbar\tbaz$
/bin/mksh-static: foo\tbar\tbaz$
/usr/bin/ksh    : foo\tbar\tbaz$
/bin/zsh        : foo\tbar\tbaz$
/bin/zsh4       : foo\tbar\tbaz$

เฉพาะทุบตีแบ่งตัวแปรทางด้านขวาของ<<<เมื่อไม่ได้ยกมา
แต่ที่ได้รับการแก้ไขในทุบตีรุ่น 4.4
นั่นหมายความว่าค่าของผลกระทบต่อผลการ$IFS<<<


ด้วยสาย:

l=(1 2 3); IFS=:; sed -n l <<<"${l[*]}"

เชลล์ทั้งหมดใช้อักขระตัวแรกของ IFS เพื่อเข้าร่วมค่า

/bin/y2sh       : 1:2:3$
/bin/sh         : 1:2:3$
/bin/b43sh      : 1:2:3$
/bin/b44sh      : 1:2:3$
/bin/bash       : 1:2:3$
/bin/ksh        : 1:2:3$
/bin/ksh93      : 1:2:3$
/bin/lksh       : 1:2:3$
/bin/mksh       : 1:2:3$
/bin/zsh        : 1:2:3$
/bin/zsh4       : 1:2:3$

ด้วย"${l[@]}"ต้องการพื้นที่เพื่อแยกอาร์กิวเมนต์ที่แตกต่างกัน แต่เชลล์บางตัวเลือกที่จะใช้ค่าจาก IFS (ถูกต้องหรือไม่)

/bin/y2sh       : 1:2:3$
/bin/sh         : 1 2 3$
/bin/b43sh      : 1 2 3$
/bin/b44sh      : 1 2 3$
/bin/bash       : 1 2 3$
/bin/ksh        : 1 2 3$
/bin/ksh93      : 1 2 3$
/bin/lksh       : 1:2:3$
/bin/mksh       : 1:2:3$
/bin/zsh        : 1:2:3$
/bin/zsh4       : 1:2:3$

ด้วย IFS ที่ว่างเปล่าค่าควรเข้าร่วมเช่นเดียวกับบรรทัดนี้:

a=(1 2 3); IFS=''; sed -n l <<<"${a[*]}"

/bin/y2sh       : 123$
/bin/sh         : 123$
/bin/b43sh      : 123$
/bin/b44sh      : 123$
/bin/bash       : 123$
/bin/ksh        : 123$
/bin/ksh93      : 123$
/bin/lksh       : 1 2 3$
/bin/mksh       : 1 2 3$
/bin/zsh        : 123$
/bin/zsh4       : 123$

แต่ทั้ง lksh และ mksh ไม่สามารถทำได้

หากเราเปลี่ยนเป็นรายการข้อโต้แย้ง:

l=(1 2 3); IFS=''; sed -n l <<<"${l[@]}"

/bin/y2sh       : 123$
/bin/sh         : 1 2 3$
/bin/b43sh      : 1 2 3$
/bin/b44sh      : 1 2 3$
/bin/bash       : 1 2 3$
/bin/ksh        : 1 2 3$
/bin/ksh93      : 1 2 3$
/bin/lksh       : 1 2 3$
/bin/mksh       : 1 2 3$
/bin/zsh        : 123$
/bin/zsh4       : 123$

yash และ zsh ไม่สามารถแยกอาร์กิวเมนต์ได้ นั่นเป็นข้อบกพร่องหรือไม่?


เกี่ยวกับzsh/ yashและ"${l[@]}"ในบริบทที่ไม่ใช่รายการนั่นคือโดยการออกแบบที่"${l[@]}"มีความพิเศษเฉพาะในบริบทรายการ ในบริบทที่ไม่ใช่รายการไม่มีการแยกคุณสามารถเข้าร่วมองค์ประกอบได้ การเข้าร่วมกับอักขระตัวแรกของ $ IFS นั้นมีความสอดคล้องมากกว่าการเข้าร่วมกับ IMO เว้นวรรค dashทำมันได้เช่นกัน ( dash -c 'IFS=; a=$@; echo "$a"' x a b) POSIX อย่างไรก็ตามมีเจตนาที่จะเปลี่ยนแปลง IIRC นั้น ดูการสนทนา (ยาว) นี้
Stéphane Chazelas


การตอบกลับด้วยตัวเองไม่ใช่มีลักษณะที่สอง POSIX จะออกจากการทำงานที่var=$@ไม่ได้ระบุ
Stéphane Chazelas
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.