Hiểu về IFS IFS = đọc dòng -r


60

Tôi rõ ràng hiểu rằng người ta có thể thêm giá trị cho biến phân tách trường nội bộ. Ví dụ:

$ IFS=blah
$ echo "$IFS"
blah
$ 

Tôi cũng hiểu rằng read -r linesẽ lưu dữ liệu từ stdinbiến có tên line:

$ read -r line <<< blah
$ echo "$line"
blah
$ 

Tuy nhiên, làm thế nào một lệnh có thể gán giá trị biến? Và hiện nó lưu trữ dữ liệu đầu tiên từ stdinđể biến linevà sau đó cung cấp giá trị lineđến IFS?


Câu trả lời:


104

Một số người có quan niệm sai lầm đó readlà lệnh đọc một dòng. Không phải vậy.

readđọc các từ từ một dòng (có thể là dấu gạch chéo tiếp tục), trong đó các từ được $IFSphân cách và dấu gạch chéo ngược có thể được sử dụng để thoát các dấu phân cách (hoặc tiếp tục dòng).

Cú pháp chung là:

read word1 word2... remaining_words

readđọc stdin một byte tại một thời điểm cho đến khi nó tìm thấy một ký tự xuống dòng unescaped (hoặc end-of-input), chia tách mà theo quy định phức tạp và lưu trữ các kết quả của tách đó vào $word1, $word2... $remaining_words.

Ví dụ trên một đầu vào như:

  <tab> foo bar\ baz   bl\ah   blah\
whatever whatever

và với giá trị mặc định là $IFS, read a b csẽ gán:

  • $afoo
  • $bbar baz
  • $cblah blahwhatever whatever

Bây giờ nếu chỉ thông qua một đối số, điều đó sẽ không trở thành read line. Nó vẫn vậy read remaining_words. Xử lý dấu gạch chéo ngược vẫn được thực hiện, các ký tự khoảng trắng IFS vẫn bị xóa từ đầu và cuối.

Các -rtùy chọn loại bỏ các xử lý dấu chéo ngược. Vì vậy, cùng một lệnh ở trên với -rthay vì gán

  • $afoo
  • $bbar\
  • $cbaz bl\ah blah\

Bây giờ, đối với phần tách, điều quan trọng là phải nhận ra rằng có hai loại ký tự cho $IFS: các ký tự khoảng trắng IFS (cụ thể là khoảng trắng và tab (và dòng mới, mặc dù ở đây không quan trọng trừ khi bạn sử dụng -d), điều này cũng xảy ra ở giá trị mặc định của $IFS) và các giá trị khác. Cách đối xử cho hai lớp nhân vật đó là khác nhau.

Với IFS=:( :là không phải là một nhân vật khoảng trắng IFS), một đầu vào như :foo::bar::sẽ được chia thành "", "foo", "", bar""(và thêm ""với một số hiện thực mặc dù điều đó không thành vấn đề trừ read -a). Trong khi nếu chúng ta thay thế nó :bằng không gian, thì việc chia tách chỉ được thực hiện foobar. Đó là hàng đầu và dấu vết bị bỏ qua, và chuỗi của chúng được coi như một. Có các quy tắc bổ sung khi các ký tự khoảng trắng và không khoảng trắng được kết hợp trong $IFS. Một số triển khai có thể thêm / xóa điều trị đặc biệt bằng cách nhân đôi các ký tự trong IFS ( IFS=::hoặc IFS=' ').

Vì vậy, ở đây, nếu chúng ta không muốn xóa các ký tự khoảng trắng không bị bỏ sót hàng đầu và dấu, chúng ta cần xóa các ký tự khoảng trắng IFS khỏi IFS.

Ngay cả với các ký tự không phải khoảng trắng IFS, nếu dòng đầu vào chứa một (và chỉ một) trong số các ký tự đó và đó là ký tự cuối cùng trong dòng (như IFS=: read -r wordtrên đầu vào như foo:) với các vỏ POSIX (không zshphải một số pdkshphiên bản), đầu vào đó được coi là một footừ vì trong các shell đó, các ký tự $IFSđược coi là dấu kết , vì vậy wordsẽ chứa foo, không foo:.

Vì vậy, cách chuẩn để đọc một dòng đầu vào với readnội dung là:

IFS= read -r line

(lưu ý rằng đối với hầu hết các readtriển khai, chỉ hoạt động đối với các dòng văn bản vì ký tự NUL không được hỗ trợ ngoại trừ trong zsh).

Sử dụng var=value cmdcú pháp đảm bảo IFSchỉ được đặt khác nhau trong khoảng thời gian của cmdlệnh đó .

Ghi chú lịch sử

Nội dung readđược giới thiệu bởi trình bao Bourne và đã đọc các từ , không phải các dòng. Có một vài khác biệt quan trọng với đạn POSIX hiện đại.

Các vỏ Bourne readkhông hỗ trợ một -rtùy chọn (được giới thiệu bởi vỏ Korn), vì vậy không có cách nào để vô hiệu hóa xử lý dấu gạch chéo ngoài việc xử lý trước đầu vào với một cái gì đó giống như sed 's/\\/&&/g'ở đó.

Shell Bourne không có khái niệm về hai lớp nhân vật (một lần nữa được giới thiệu bởi ksh). Trong trình bao Bourne, tất cả các ký tự trải qua xử lý giống như các ký tự khoảng trắng IFS thực hiện trong ksh, đó là IFS=: read a b ctrên một đầu vào như foo::barsẽ gán barcho $b, chứ không phải chuỗi rỗng.

Trong vỏ Bourne, với:

var=value cmd

Nếu cmdlà một tích hợp (như readlà), varvẫn được đặt thành valuesau khi cmdđã kết thúc. Điều đó đặc biệt quan trọng $IFSbởi vì trong vỏ Bourne, $IFSđược sử dụng để phân chia mọi thứ, không chỉ các bản mở rộng. Ngoài ra, nếu bạn loại bỏ ký tự khoảng $IFStrắng trong shell Bourne, sẽ "$@"không còn hoạt động.

Trong shell Bourne, việc chuyển hướng một lệnh ghép khiến nó chạy trong một lớp con (trong các phiên bản đầu tiên, ngay cả những thứ như read var < filehoặc exec 3< file; read var <&3không hoạt động), do đó, hiếm khi sử dụng shell Bourne readcho mọi thứ trừ đầu vào của người dùng trên thiết bị đầu cuối (trong đó xử lý tiếp tục dòng có ý nghĩa)

Một số Unice (như HP / UX, cũng có một trong util-linux) vẫn có linelệnh đọc một dòng đầu vào (trước đây là lệnh UNIX tiêu chuẩn cho đến phiên bản Đặc tả UNIX đơn 2 ).

Điều đó về cơ bản giống như head -n 1ngoại trừ việc nó đọc một byte mỗi lần để đảm bảo rằng nó không đọc nhiều hơn một dòng. Trên các hệ thống đó, bạn có thể làm:

line=`line`

Tất nhiên, điều đó có nghĩa là sinh ra một quy trình mới, thực hiện một lệnh và đọc đầu ra của nó thông qua một đường ống, do đó kém hiệu quả hơn nhiều so với ksh IFS= read -r line, nhưng vẫn trực quan hơn rất nhiều.


3
+1 Cảm ơn về một số hiểu biết hữu ích về các phương pháp điều trị khác nhau trên không gian / tab so với "những người khác" trong IFS trong bash ... Tôi biết rằng chúng được đối xử khác nhau, nhưng cách giải thích này đơn giản hóa tất cả. (Và cái nhìn sâu sắc giữa bash (và các vỏ posix khác) và sự shkhác biệt thường xuyên cũng rất hữu ích để viết các tập lệnh di động!)
Olivier Dulac

Ít nhất là cho bash-4.4.19, while read -r; do echo "'$REPLY'"; donehoạt động như while IFS= read -r line; do echo "'$line'"; done.
x-yuri

Điều này: "... quan niệm sai lầm rằng đọc là lệnh đọc một dòng ..." khiến tôi nghĩ rằng, nếu sử dụng readđể đọc một dòng là sai, thì phải có một thứ khác. Khái niệm không sai lầm đó có thể là gì? Hoặc là tuyên bố đầu tiên đúng về mặt kỹ thuật, nhưng trong thực tế, khái niệm không sai lầm là: "đọc là lệnh để đọc các từ trong một dòng. Vì nó rất mạnh mẽ, bạn có thể sử dụng nó để đọc các dòng từ một tệp bằng cách thực hiện: IFS= read -r line"
Mike S

8

Học thuyết

Có hai khái niệm đang được chơi ở đây:

  • IFSlà Dấu tách trường đầu vào, có nghĩa là chuỗi đọc sẽ được phân chia dựa trên các ký tự trong IFS. Trên một dòng lệnh, IFSthông thường là bất kỳ ký tự khoảng trắng nào, đó là lý do tại sao dòng lệnh phân tách tại các khoảng trắng.
  • Làm một cái gì đó như VAR=value commandcó nghĩa là "sửa đổi môi trường của lệnh để VARcó giá trị value". Về cơ bản, lệnh commandsẽ xem VARlà có giá trị value, nhưng bất kỳ lệnh nào được thực thi sau đó vẫn sẽ thấy VARcó giá trị trước đó. Nói cách khác, biến đó sẽ chỉ được sửa đổi cho câu lệnh đó.

Trong trường hợp này

Vì vậy, khi thực hiện IFS= read -r line, những gì bạn đang làm là đặt IFSthành một chuỗi trống (sẽ không có ký tự nào được sử dụng để phân tách, do đó sẽ không xảy ra phân tách) để readđọc toàn bộ dòng và xem đó là một từ sẽ được gán cho linebiến. Các thay đổi IFSchỉ ảnh hưởng đến câu lệnh đó, do đó, bất kỳ lệnh nào sau đây sẽ không bị ảnh hưởng bởi thay đổi.

Như một lưu ý phụ

Trong khi lệnh là đúng và sẽ làm việc như dự định, thiết lập IFStrong trường hợp này không phải là sức mạnh 1 không cần thiết. Như được viết trong bashtrang man trong phần readdựng sẵn:

Một dòng được đọc từ đầu vào tiêu chuẩn [...] và từ đầu tiên được gán cho tên đầu tiên, từ thứ hai cho tên thứ hai, v.v., với các từ còn lại và dấu phân cách can thiệp của chúng được gán cho tên cuối cùng . Nếu có ít từ được đọc từ luồng đầu vào hơn tên, các tên còn lại được gán giá trị trống. Các ký tự trong IFSđược sử dụng để phân chia dòng thành các từ. [...]

Vì bạn chỉ có linebiến, nên mọi từ sẽ được gán cho nó, vì vậy nếu bạn không cần bất kỳ ký tự khoảng trắng trước và dấu nào trước 1, bạn có thể viết read -r linevà hoàn thành nó.

[1] Giống như một ví dụ về cách một giá trị unsetmặc định hoặc $IFSsẽ gây ra readliên quan đến khoảng trắng IFS hàng đầu / theo dõi , bạn có thể thử:

echo ' where are my spaces? ' | { 
    unset IFS
    read -r line
    printf %s\\n "$line"
} | sed -n l

Chạy nó và bạn sẽ thấy rằng các ký tự trước và dấu sẽ không tồn tại nếu IFSkhông được đặt. Hơn nữa, một số điều kỳ lạ có thể xảy ra nếu $IFSđược sửa đổi ở đâu đó sớm hơn trong kịch bản.


5

Bạn nên đọc câu lệnh đó thành hai phần, phần thứ nhất xóa giá trị của biến IFS, nghĩa là tương đương với phần dễ đọc hơn IFS="", phần thứ hai đang đọc linebiến từ stdin , read -r line.

Điều cụ thể trong cú pháp này là ảnh hưởng của IFS là nhất thời và chỉ hợp lệ cho readlệnh.

Trừ khi tôi thiếu một cái gì đó, trong trường hợp cụ thể đó, việc xóa thanh toán IFSkhông có tác dụng mặc dù mọi thứ IFSđược đặt thành, toàn bộ dòng sẽ được đọc trong linebiến. Sẽ có một sự thay đổi trong hành vi chỉ trong trường hợp có nhiều hơn một biến được truyền dưới dạng tham số cho readlệnh.

Biên tập:

-rở đó để cho phép kết thúc đầu vào mà \không được xử lý đặc biệt, nghĩa là cho dấu gạch chéo ngược được bao gồm trong linebiến và không phải là ký tự tiếp tục để cho phép đầu vào nhiều dòng.

$ read line; echo "[$line]"   
abc\
> def
[abcdef]
$ read -r line; echo "[$line]"  
abc\
[abc\]

Xóa IFS có tác dụng phụ là ngăn chặn việc đọc để cắt bớt các ký tự không gian hoặc dấu cách hàng đầu và dấu cách tiềm năng, ví dụ:

$ echo "   a b c   " | { IFS= read -r line; echo "[$line]" ; }   
[   a b c   ]
$ echo "   a b c   " | { read -r line; echo "[$line]" ; }     
[a b c]

Cảm ơn rici đã chỉ ra sự khác biệt đó.


Điều bạn còn thiếu là nếu IFS không được thay đổi, read -r linesẽ cắt bớt khoảng trắng hàng đầu và dấu trước khi gán đầu vào cho linebiến.
rici

@rici Tôi đã nghi ngờ điều gì đó tương tự nhưng chỉ kiểm tra các ký tự IFS giữa các từ, không dẫn đầu / theo dõi. Cảm ơn đã chỉ ra thực tế đó!
jlliagre

xóa IFS cũng sẽ ngăn việc gán nhiều biến (tác dụng phụ). IFS= read a b <<< 'aa bb' ; echo "-$a-$b-"sẽ hiển thị-aa bb--
kyodev
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.