Hiểu IFS


71

Một số chủ đề sau trên trang web này và StackOverflow rất hữu ích để hiểu cách thức IFShoạt động:

Nhưng tôi vẫn có một số câu hỏi ngắn. Tôi quyết định hỏi họ trong cùng một bài vì tôi nghĩ nó có thể giúp độc giả tương lai tốt hơn:

Q1. IFSthường được thảo luận trong bối cảnh "chia tách trường". Là trường tách giống như chia từ ?

Q2: Đặc tả POSIX cho biết :

Nếu giá trị của IFS là null, sẽ không thực hiện phân tách trường.

Là thiết lập IFS=giống như cài đặt IFSthành null? Đây có phải là những gì có nghĩa là bằng cách đặt nó empty stringquá?

Câu 3: Trong đặc tả POSIX, tôi đọc phần sau:

Nếu IFS không được đặt, shell sẽ hoạt động như thể giá trị của IFS là <space>, <tab> and <newline>

Nói rằng tôi muốn khôi phục giá trị mặc định của IFS. Làm thế nào để làm điều đó? (cụ thể hơn, làm thế nào để tôi tham khảo <tab><newline>?)

Q4: Cuối cùng, mã này sẽ như thế nào:

while IFS= read -r line
do    
    echo $line
done < /path_to_text_file

cư xử nếu chúng ta thay đổi dòng đầu tiên thành

while read -r line # Use the default IFS value

hoặc để:

while IFS=' ' read -r line

Câu trả lời:


28
  1. Vâng, Chúng giống nhau.
  2. Đúng.
  3. Trong bash, và các shell tương tự, bạn có thể làm một cái gì đó như IFS=$' \t\n'. Nếu không, bạn có thể chèn mã kiểm soát bằng chữ bằng cách sử dụng [space] CTRL+V [tab] CTRL+V [enter]. Tuy nhiên, nếu bạn dự định làm điều này, tốt hơn là sử dụng một biến khác để tạm thời lưu trữ IFSgiá trị cũ , sau đó khôi phục nó sau đó (hoặc tạm thời ghi đè lên nó cho một lệnh bằng cách sử dụng var=foo commandcú pháp).
    • Đoạn mã đầu tiên sẽ đặt toàn bộ dòng đọc, nguyên văn, vào $line, vì không có dấu tách trường để thực hiện phân tách từ cho. Tuy nhiên, hãy nhớ rằng vì nhiều shell sử dụng cstrings để lưu trữ chuỗi, trường hợp đầu tiên của NUL vẫn có thể khiến cho sự xuất hiện của nó bị chấm dứt sớm.
    • Đoạn mã thứ hai có thể không đặt một bản sao chính xác của đầu vào $line. Ví dụ: nếu có nhiều dấu tách trường liên tiếp, chúng sẽ được tạo thành một thể hiện duy nhất của phần tử đầu tiên. Điều này thường được công nhận là mất khoảng trắng xung quanh.
    • Đoạn mã thứ ba sẽ làm tương tự như đoạn thứ hai, ngoại trừ nó sẽ chỉ phân tách trên một khoảng trắng (không phải là khoảng trắng, tab hoặc dòng mới thông thường).

3
Câu trả lời cho Q2 là sai: trống IFSvà không đặt IFSlà rất khác nhau. Câu trả lời cho Q4 là một phần sai: các dải phân cách bên trong không được chạm vào đây, chỉ có các dải dẫn và dấu.
Gilles

3
@Gilles: Trong quý 2, không có mệnh giá nào trong ba mệnh giá nhất định đề cập đến một unset IFS, tất cả đều có nghĩa IFS=.
Stéphane Gimenez

@Gilles Trong quý 2, tôi chưa bao giờ nói họ giống nhau. Và các dấu phân cách bên trong được chạm vào, như được hiển thị ở đây : IFS=' ' ; foo=( bar baz qux ) ; echo "${#foo[@]}". (Er, cái gì? Nên có nhiều dấu phân cách không gian trong đó, công cụ SO tiếp tục tước chúng).
Chris Xuống

2
@ StéphaneGimenez, Chris: Ồ, đúng rồi, xin lỗi về Q2, tôi đã đọc sai câu hỏi. Trong Q4, chúng ta đang nói về read; biến cuối cùng lấy tất cả những gì còn lại ngoại trừ dấu phân cách cuối cùng và để lại các dấu phân cách bên trong.
Gilles

1
Gilles đúng một phần về các khoảng trắng không bị xóa bằng cách đọc. Đọc câu trả lời của tôi để biết chi tiết.

22

Q1: Có. Sự chia tách lĩnh vực của người Viking và người chia tách từ ngữ là hai thuật ngữ cho cùng một khái niệm.

Câu 2: Có. Nếu IFSkhông được đặt (tức là sau unset IFS), nó tương đương IFSđược đặt thành $' \t\n'(khoảng trắng, tab và dòng mới). Nếu IFSđược đặt thành một giá trị trống (đó là ý nghĩa của null null, ở đây) (nghĩa là sau IFS=hoặc IFS=''hoặc IFS=""), không có sự phân tách trường nào được thực hiện (và $*, thường sử dụng ký tự đầu tiên của $IFS, sử dụng ký tự khoảng trắng).

Câu 3: Nếu bạn muốn có IFShành vi mặc định , bạn có thể sử dụng unset IFS. Nếu bạn muốn đặt IFSrõ ràng thành giá trị mặc định này, bạn có thể đặt không gian ký tự bằng chữ, tab, dòng mới trong dấu ngoặc đơn. Trong ksh93, bash hoặc zsh, bạn có thể sử dụng IFS=$' \t\n'. Có thể, nếu bạn muốn tránh có một ký tự tab bằng chữ trong tệp nguồn của mình, bạn có thể sử dụng

IFS=" $(echo t | tr t \\t)
"

Q4: Với IFSgiá trị được đặt thành một giá trị trống, hãy read -r lineđặt linethành toàn bộ dòng ngoại trừ dòng mới kết thúc của nó. Với IFS=" ", khoảng trắng ở đầu và cuối dòng được cắt bớt. Với giá trị mặc định là IFS, các tab và khoảng trắng được cắt bớt.


2
Quý 2 có phần sai. Nếu IFS trống, "$ *" được nối mà không có dấu phân cách. (đối với $@, có một số biến thể giữa các shell trong bối cảnh không phải là danh sách như IFS=; var=$@). Cần lưu ý rằng khi IFS trống, không có phân tách từ nào được thực hiện nhưng $ var vẫn mở rộng thành không có đối số thay vì đối số trống khi $ var trống và tiếp tục áp dụng, vì vậy bạn vẫn cần trích dẫn các biến (ngay cả khi bạn vô hiệu hóa globalbing)
Stéphane Chazelas

13

Q1. Trường tách.

Là trường tách giống như chia từ?

Vâng, cả hai đều chỉ đến cùng một ý tưởng.

Câu 2: Khi nào IFS null ?

Có phải thiết lập IFS=''giống như null, cũng giống như một chuỗi rỗng?

Có, cả ba đều có nghĩa giống nhau: Không được tách trường / từ. Ngoài ra, điều này ảnh hưởng đến các trường in (như với echo "$*") tất cả các trường sẽ được nối với nhau không có khoảng trống.

Câu 3: (phần a) Bỏ đặt IFS.

Trong đặc tả POSIX, tôi đọc như sau :

Nếu IFS không được đặt, shell sẽ hoạt động như thể giá trị của IFS là <dấu cách> <tab> <newline> .

Điều này hoàn toàn tương đương với:

Với một unset IFS, shell sẽ hoạt động như thể IFS là mặc định.

Điều đó có nghĩa là 'Chia tách trường' sẽ hoàn toàn giống với giá trị IFS mặc định hoặc không được đặt.
Điều đó KHÔNG có nghĩa là IFS sẽ hoạt động theo cùng một cách trong mọi điều kiện. Cụ thể hơn, việc thực thi OldIFS=$IFSsẽ đặt var OldIFSthành null , không phải mặc định. Và cố gắng đặt lại IFS, vì điều này, IFS=OldIFSsẽ đặt IFS thành null, không giữ cho nó không được đặt như trước đây. Cẩn thận !!.

Câu 3: (phần b) Khôi phục IFS.

Làm thế nào tôi có thể khôi phục giá trị của IFS về mặc định. Nói rằng tôi muốn khôi phục giá trị mặc định của IFS. Làm thế nào để làm điều đó? (cụ thể hơn, làm cách nào để tôi tham khảo <tab><newline> ?)

Đối với zsh, ksh và bash (AFAIK), IFS có thể được đặt thành giá trị mặc định là:

IFS=$' \t\n'        # works with zsh, ksh, bash.

Xong, bạn không cần đọc gì nữa.

Nhưng nếu bạn cần thiết lập lại IFS cho sh, nó có thể trở nên phức tạp.

Chúng ta hãy xem từ dễ nhất để hoàn thành mà không có nhược điểm (ngoại trừ độ phức tạp).

1.- Bỏ đặt IFS.

Chúng ta chỉ có thể unset IFS(Đọc phần 3 phần a, ở trên.).

2.- Hoán đổi ký tự.

Như một giải pháp thay thế, việc hoán đổi giá trị của tab và dòng mới giúp việc đặt giá trị của IFS đơn giản hơn và sau đó nó hoạt động theo cách tương đương.

Đặt IFS thành <dấu cách> <dòng mới> <tab> :

sh -c 'IFS=$(echo " \n\t"); printf "%s" "$IFS"|xxd'      # Works.

3.- Đơn giản? giải pháp:

Nếu có các tập lệnh con cần IFS được đặt chính xác, bạn luôn có thể viết thủ công:

IFS = '   
'

Trường hợp trình tự được nhập thủ công là : IFS='spacetabnewline', trình tự đã được nhập chính xác ở trên (Nếu bạn cần xác nhận, hãy chỉnh sửa câu trả lời này). Nhưng một bản sao / dán từ trình duyệt của bạn sẽ bị hỏng vì trình duyệt sẽ ép / ẩn khoảng trắng. Nó gây khó khăn cho việc chia sẻ mã như được viết ở trên.

4.- Giải pháp hoàn chỉnh.

Để viết mã có thể được sao chép một cách an toàn thường liên quan đến các lối thoát có thể in rõ ràng.

Chúng ta cần một số mã "tạo ra" giá trị mong đợi. Nhưng, ngay cả khi đúng về mặt khái niệm, mã này sẽ KHÔNG đặt dấu vết \n:

sh -c 'IFS=$(echo " \t\n"); printf "%s" "$IFS"|xxd'      # wrong.

Điều đó xảy ra bởi vì, trong hầu hết các hệ vỏ, tất cả các dòng mới thay thế $(...)hoặc `...`thay thế lệnh được loại bỏ khi mở rộng.

Chúng ta cần sử dụng một mẹo cho sh:

sh -c 'IFS="$(printf " \t\nx")"; IFS="${IFS%x}"; printf "$IFS"|xxd'  # Correct.

Một cách khác có thể là đặt IFS làm giá trị môi trường từ bash (ví dụ) và sau đó gọi sh (các phiên bản của nó chấp nhận IFS được đặt qua môi trường), như sau:

env IFS=$' \t\n' sh -c 'printf "%s" "$IFS"|xxd'

Nói tóm lại, sh làm cho việc đặt lại IFS để mặc định là một cuộc phiêu lưu kỳ quặc.

Q4: Trong mã thực tế:

Cuối cùng, làm thế nào mã này:

while IFS= read -r line
do
    echo $line
done < /path_to_text_file

cư xử nếu chúng ta thay đổi dòng đầu tiên thành

while read -r line # Use the default IFS value

hoặc để:

while IFS=' ' read -r line

Đầu tiên: Tôi không biết liệu echo $line(với var KHÔNG được trích dẫn) có ở trên porpouse hay không. Nó giới thiệu một mức độ thứ hai của 'chia tách trường' mà đọc không có. Vì vậy, tôi sẽ trả lời cả hai. :)

Với mã này (để bạn có thể xác nhận). Bạn sẽ cần xxd hữu ích :

#!/bin/ksh
# Correctly set IFS as described above.
defIFS="$(printf " \t\nx")"; defIFS="${defIFS%x}";
IFS="$defIFS"
printf "IFS value: "
printf "%s" "$IFS"| xxd -p

a='   bar   baz   quz   '; l="${#a}"
printf "var value          : %${l}s-" "$a" ; printf "%s\n" "$a" | xxd -p

printf "%s\n" "$a" | while IFS='x' read -r line; do
    printf "IFS --x--          : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf 'Values      quoted :\n' ""  # With values quoted:
printf "%s\n" "$a" | while IFS='' read -r line; do
    printf "IFS null    quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf "%s\n" "$a" | while IFS="$defIFS" read -r line; do
    printf "IFS default quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

unset IFS; printf "%s\n" "$a" | while read -r line; do
    printf "IFS unset   quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;
    IFS="$defIFS"   # set IFS back to default.

printf "%s\n" "$a" | while IFS=' ' read -r line; do
    printf "IFS space   quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf '%s\n' "Values unquoted :"   # Now with values unquoted:
printf "%s\n" "$a" | while IFS='x' read -r line; do
    printf "IFS --x-- unquoted : "
    printf "%s, " $line; printf "%s," $line |xxd -p; done

printf "%s\n" "$a" | while IFS='' read -r line; do
    printf "IFS null  unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

printf "%s\n" "$a" | while IFS="$defIFS" read -r line; do
    printf "IFS defau unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

unset IFS; printf "%s\n" "$a" | while read -r line; do
    printf "IFS unset unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done
    IFS="$defIFS"   # set IFS back to default.

printf "%s\n" "$a" | while IFS=' ' read -r line; do
    printf "IFS space unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

Tôi có:

$ ./stackexchange-Understanding-IFS.sh
IFS value: 20090a
var value          :    bar   baz   quz   -20202062617220202062617a20202071757a2020200a
IFS --x--          :    bar   baz   quz   -20202062617220202062617a20202071757a202020
Values      quoted :
IFS null    quoted :    bar   baz   quz   -20202062617220202062617a20202071757a202020
IFS default quoted :       bar   baz   quz-62617220202062617a20202071757a
IFS unset   quoted :       bar   baz   quz-62617220202062617a20202071757a
IFS space   quoted :       bar   baz   quz-62617220202062617a20202071757a
Values unquoted :
IFS --x-- unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS null  unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS defau unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS unset unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS space unquoted : bar, baz, quz, 6261722c62617a2c71757a2c

Giá trị đầu tiên chỉ là giá trị chính xác của IFS='spacetabnewline'

Dòng tiếp theo là tất cả các giá trị hex mà var $acó và dòng mới '0a' ở cuối vì nó sẽ được trao cho mỗi lệnh đọc.

Dòng tiếp theo, mà IFS là null, không thực hiện bất kỳ 'chia tách trường' nào, nhưng dòng mới bị xóa (như mong đợi).

Ba dòng tiếp theo, vì IFS chứa một khoảng trắng, xóa các khoảng trắng ban đầu và đặt dòng var thành số dư còn lại.

Bốn dòng cuối cùng cho thấy một biến không được trích dẫn sẽ làm gì. Các giá trị sẽ được phân chia trên (một số) khoảng trắng và sẽ được in dưới dạng:bar,baz,qux,


4

unset IFS không xóa IFS, ngay cả khi IFS sau đó được coi là "\ t \ n":

$ echo "'$IFS'"
'   
'
$ IFS=""
$ echo "'$IFS'"
''
$ unset IFS
$ echo "'$IFS'"
''
$ IFS=$' \t\n'
$ echo "'$IFS'"
'   
'
$

Đã thử nghiệm trên các phiên bản bash 4.2.45 và 3.2.25 với cùng một hành vi.


Các câu hỏi và các tài liệu liên quan không nói về unsetcủa IFS, như được giải thích trong các ý kiến của câu trả lời được chấp nhận ở đây.
ILMostro_7
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.