Tại sao cắt không thành công với bash và không zsh?


10

Tôi tạo một tệp với các trường được phân định bằng tab.

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

Tôi có đoạn script sau zsh.sh

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

Tôi kiểm tra nó.

$ ./zsh.sh input
bar
bar

Điều này hoạt động tốt. Tuy nhiên, khi tôi thay đổi dòng đầu tiên để gọi bashthay thế, nó thất bại.

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

Tại sao điều này thất bại với bashvà làm việc với zsh?

Xử lý sự cố bổ sung

  • Sử dụng các đường dẫn trực tiếp trong shebang thay vì envtạo ra hành vi tương tự.
  • Đường ống echothay vì sử dụng chuỗi ở đây <<<$linecũng tạo ra hành vi tương tự. tức echo $line | cut -f 2.
  • Sử dụng awkthay vì cut làm việc cho cả vỏ. tức <<<$line awk '{print $2}'.

4
Bằng cách này, bạn có thể làm cho tập tin thử nghiệm của bạn đơn giản hơn bằng cách thực hiện một trong các: echo -e 'foo\tbar\tbaz\n...', echo $'foo\tbar\tbaz\n...', hoặc printf 'foo\tbar\tbaz\n...\n'hoặc các biến thể trong số này. Nó giúp bạn không phải bọc riêng từng tab hoặc dòng mới.
Tạm dừng cho đến khi có thông báo mới.

Câu trả lời:


13

Điều gì xảy ra là bashthay thế các tab bằng dấu cách. Bạn có thể tránh vấn đề này bằng cách nói "$line"thay vào đó, hoặc bằng cách cắt rõ ràng trên khoảng trắng.


1
Có bất kỳ lý do Bash nhìn thấy \tvà thay thế nó bằng một không gian?
dùng1717828

@ user1717828 có, nó được gọi là toán tử spit + global . Đó là những gì xảy ra khi bạn sử dụng một biến không được trích dẫn trong bash và các shell tương tự.
terdon

1
@terdon, trong <<< $line, bashkhông phân chia nhưng không toàn cầu. Không có lý do gì nó sẽ chia ra ở đây như <<<mong đợi một từ. Nó tách ra và sau đó tham gia trong trường hợp đó, điều này rất ít có ý nghĩa và chống lại tất cả các triển khai hệ vỏ khác đã hỗ trợ <<<trước hoặc sau bash. IMO đó là một lỗi.
Stéphane Chazelas

@ StéphaneChazelas đủ công bằng, vấn đề là với phần tách ra nào.
terdon

2
@ StéphaneChazelas Không xảy ra sự chia tách (cũng như toàn cầu) trên bash 4.4

17

Đó là bởi vì <<< $line, trong bashviệc tách từ, (mặc dù không phải là toàn cầu) $linevì nó không được trích dẫn ở đó và sau đó kết hợp các từ kết quả với ký tự khoảng trắng (và đặt nó trong một tệp tạm thời theo sau là một ký tự dòng mới và tạo thành stdin của cut).

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

tabxảy ra ở giá trị mặc định của $IFS:

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

Giải pháp với bashlà trích dẫn biến.

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

Lưu ý rằng đó là vỏ duy nhất làm điều đó. zsh(nơi <<<xuất phát từ, lấy cảm hứng từ cổng Unix của rc), ksh93, mkshyashđó cũng hỗ trợ <<<không làm điều đó.

Khi nói đến mảng, mksh, yashzshtham gia vào các ký tự đầu tiên của $IFS, bashksh93trên không gian.

$ 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$

Có sự khác biệt giữa zsh/ yashmksh(ít nhất là phiên bản R52) khi $IFStrống:

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

Hành vi phù hợp hơn trên các shell khi bạn sử dụng "${a[*]}"(ngoại trừ việc mkshvẫn có lỗi khi $IFStrống).

Trong echo $line | ...đó, đó là toán tử chia + toàn cầu thông thường trong tất cả các shell giống Bourne nhưng zsh(và các vấn đề thông thường liên quan đến echo).


1
Câu trả lời tuyệt vời! Cảm ơn bạn (+1). Mặc dù vậy, tôi sẽ chấp nhận người hỏi thấp nhất, vì họ trả lời câu hỏi đủ tốt để tiết lộ sự ngu ngốc của tôi.
Sparhawk

10

Vấn đề là bạn không trích dẫn $line. Để điều tra, thay đổi hai tập lệnh để chúng chỉ cần in $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"

Bây giờ, so sánh đầu ra của họ:

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

Như bạn có thể thấy, vì bạn không trích dẫn $line, các tab không được giải thích chính xác bằng bash. Zsh dường như đối phó với điều đó tốt hơn. Bây giờ, cutsử dụng \tlàm dấu phân cách trường theo mặc định. Do đó, do bashtập lệnh của bạn đang ăn các tab (do toán tử split + global), cutchỉ nhìn thấy một trường và hành động tương ứng. Những gì bạn đang thực sự chạy là:

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

Vì vậy, để làm cho tập lệnh của bạn hoạt động như mong đợi trong cả hai trình bao, hãy trích dẫn biến của bạn:

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

Sau đó, cả hai sản xuất cùng một đầu ra:

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

Câu trả lời tuyệt vời! Cảm ơn bạn (+1). Mặc dù vậy, tôi sẽ chấp nhận người hỏi thấp nhất, vì họ trả lời câu hỏi đủ tốt để tiết lộ sự ngu ngốc của tôi.
Sparhawk

^ bỏ phiếu cho câu trả lời duy nhất (chưa) thực sự bao gồm phần sửa lỗibash.sh
lauir

1

Như đã được trả lời, một cách dễ sử dụng hơn để sử dụng một biến là trích dẫn nó:

$ 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$

Có một sự khác biệt của việc thực hiện trong bash, với dòng:

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

Đây là kết quả của hầu hết các shell:

/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$

Chỉ bash phân chia biến ở bên phải <<<khi không được trích dẫn.
Tuy nhiên, điều đó đã được sửa trên bash phiên bản 4.4
Điều đó có nghĩa là giá trị $IFSảnh hưởng đến kết quả của <<<.


Với dòng:

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

Tất cả các shell sử dụng ký tự đầu tiên của IFS để nối các giá trị.

/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$

Với "${l[@]}", một khoảng trắng là cần thiết để phân tách các đối số khác nhau, nhưng một số shell chọn sử dụng giá trị từ IFS (Điều đó có đúng không?).

/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$

Với IFS null, các giá trị sẽ được nối, như với dòng này:

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$

Nhưng cả lksh và mksh đều không làm như vậy.

Nếu chúng ta thay đổi thành một danh sách các đối số:

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$

Cả yash và zsh đều không giữ các đối số tách biệt. Đó có phải là một lỗi không?


Về zsh/ yash"${l[@]}"trong bối cảnh không có trong danh sách, đó là theo thiết kế, nơi "${l[@]}"chỉ đặc biệt trong bối cảnh danh sách. Trong bối cảnh không có trong danh sách, không thể tách rời , bạn cần tham gia các yếu tố bằng cách nào đó. Tham gia với ký tự đầu tiên của $ IFS phù hợp hơn so với tham gia với một nhân vật không gian IMO. dashnó cũng như vậy ( dash -c 'IFS=; a=$@; echo "$a"' x a b). POSIX tuy nhiên đang có ý định thay đổi IIRC đó. Xem cuộc thảo luận (dài) này
Stéphane Chazelas


Trả lời bản thân mình, không, có cái nhìn thứ hai, POSIX sẽ để lại hành vi var=$@không xác định.
Stéphane Chazelas
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.