Đây có phải là một cách tiếp cận lành mạnh để nâng cấp sao lưu lên biến $ IFS không?


18

Tôi luôn luôn do dự khi phải $IFSloay hoay vì điều đó đang gây xáo trộn toàn cầu.

Nhưng thường thì nó làm cho việc tải các chuỗi vào một mảng bash đẹp và súc tích, và đối với kịch bản bash, sự đồng nhất là khó thực hiện.

Vì vậy, tôi cho rằng nó có thể tốt hơn không có gì nếu tôi cố gắng "lưu" nội dung bắt đầu của $IFSmột biến khác và sau đó khôi phục nó ngay lập tức sau khi tôi sử dụng xong $IFScho một cái gì đó.

Đây có phải là thực tế? Hoặc về cơ bản là vô nghĩa và tôi chỉ nên đặt IFSlại trực tiếp bất cứ thứ gì nó cần cho những lần sử dụng tiếp theo?


Tại sao nó không thực tế?
Bratchley

Bởi vì không đặt IFS sẽ làm tốt công việc.
llua

1
Đối với những người nói rằng việc không đặt IFS sẽ hoạt động tốt, hãy nhớ rằng đó là tình huống: stackoverflow.com/questions/39545837/ trên . Theo kinh nghiệm của tôi, tốt nhất là đặt IFS theo cách thủ công thành mặc định cho trình thông dịch shell của bạn, cụ thể là $' \t\n'nếu bạn đang sử dụng bash. unset $IFSchỉ đơn giản là không luôn khôi phục nó về những gì bạn mong muốn là mặc định.
Darrel Holt

Câu trả lời:


9

Bạn có thể lưu và gán cho IFS khi cần. Không có gì sai khi làm như vậy. Không có gì lạ khi lưu giá trị của nó để khôi phục sau khi sửa đổi tạm thời, nhanh chóng, như ví dụ gán mảng của bạn.

Như @llua đề cập trong bình luận của anh ấy cho câu hỏi của bạn, chỉ cần bỏ đặt IFS sẽ khôi phục hành vi mặc định, tương đương với việc gán một dòng không gian-tab-newline.

Thật đáng để xem xét làm thế nào có thể có vấn đề hơn khi không đặt / hủy đặt IFS một cách rõ ràng hơn là làm như vậy.

Từ phiên bản POSIX 2013, 2.5.3 Biến Shell :

Việc triển khai có thể bỏ qua giá trị của IFS trong môi trường hoặc không có IFS khỏi môi trường, tại thời điểm shell được gọi, trong trường hợp đó, shell sẽ đặt IFS thành <space> <tab> <newline> khi nó được gọi .

Một vỏ được gọi, tuân thủ POSIX có thể hoặc không kế thừa IFS từ môi trường của nó. Từ đây:

  • Một tập lệnh di động không thể kế thừa một cách đáng tin cậy IFS thông qua môi trường.
  • Một tập lệnh dự định chỉ sử dụng hành vi phân tách mặc định (hoặc tham gia, trong trường hợp "$*"), nhưng có thể chạy dưới lớp vỏ khởi tạo IFS từ môi trường, phải đặt / hủy đặt IFS một cách rõ ràng để tự bảo vệ chống lại sự xâm nhập của môi trường.

NB Điều quan trọng là phải hiểu rằng đối với cuộc thảo luận này, từ "được viện dẫn" có một ý nghĩa đặc biệt. Một shell chỉ được gọi khi nó được gọi rõ ràng bằng cách sử dụng tên của nó (bao gồm cả một #!/path/to/shellshebang). Một lớp con - chẳng hạn như có thể được tạo bởi $(...)hoặc cmd1 || cmd2 &- không phải là lớp vỏ được gọi và IFS của nó (cùng với hầu hết môi trường thực thi của nó) giống hệt với lớp mẹ của nó. Một shell được gọi sẽ đặt giá trị của $pid của nó, trong khi các lớp con kế thừa nó.


Đây không chỉ đơn thuần là một sự bác bỏ phạm vi; có sự khác biệt thực tế trong lĩnh vực này. Đây là một đoạn script ngắn kiểm tra kịch bản sử dụng một số shell khác nhau. Nó xuất một IFS đã sửa đổi (được đặt thành :) sang một vỏ được gọi và sau đó in IFS mặc định của nó.

$ cat export-IFS.sh
export IFS=:
for sh in bash ksh93 mksh dash busybox:sh; do
    printf '\n%s\n' "$sh"
    $sh -c 'printf %s "$IFS"' | hexdump -C
done

IFS thường không được đánh dấu để xuất, nhưng, nếu có, hãy lưu ý cách bash, ksh93 và mksh bỏ qua môi trường của chúng IFS=:, trong khi dash và busybox tôn vinh nó.

$ sh export-IFS.sh

bash
00000000  20 09 0a                                          | ..|
00000003

ksh93
00000000  20 09 0a                                          | ..|
00000003

mksh
00000000  20 09 0a                                          | ..|
00000003

dash
00000000  3a                                                |:|
00000001

busybox:sh
00000000  3a                                                |:|
00000001

Một số thông tin phiên bản:

bash: GNU bash, version 4.3.11(1)-release
ksh93: sh (AT&T Research) 93u+ 2012-08-01
mksh: KSH_VERSION='@(#)MIRBSD KSH R46 2013/05/02'
dash: 0.5.7
busybox: BusyBox v1.21.1

Mặc dù bash, ksh93 và mksh không khởi tạo IFS từ môi trường, chúng vẫn tái xuất IFS đã sửa đổi của chúng.

Nếu vì bất kỳ lý do gì bạn cần chuyển IFS qua môi trường một cách hợp lý, bạn không thể làm như vậy bằng chính IFS; bạn sẽ cần gán giá trị cho một biến khác và đánh dấu biến đó để xuất. Trẻ em sau đó sẽ cần gán giá trị đó một cách rõ ràng cho IFS của chúng.


Tôi thấy, vì vậy nếu tôi có thể diễn giải, có thể dễ dàng di chuyển hơn để chỉ định rõ ràng IFSgiá trị trong hầu hết các tình huống sẽ sử dụng, và do đó, nó thường không hiệu quả khủng khiếp để thậm chí cố gắng "bảo tồn" giá trị ban đầu của nó.
Steven Lu

1
Vấn đề tối quan trọng là nếu tập lệnh của bạn sử dụng IFS, thì nó nên đặt / hủy đặt IFS một cách rõ ràng để đảm bảo rằng giá trị của nó là giá trị mà bạn muốn. Thông thường, hành vi của tập lệnh của bạn phụ thuộc vào IFS nếu có bất kỳ mở rộng tham số không được trích dẫn, thay thế lệnh không được trích dẫn, mở rộng số học không được trích dẫn, reads hoặc tham chiếu trích dẫn kép $*. Danh sách đó nằm ngoài đỉnh đầu của tôi, vì vậy nó có thể không đầy đủ (đặc biệt là khi xem xét các phần mở rộng POSIX của đạn pháo hiện đại).
Bare chân IO

10

Nói chung, đó là một thực hành tốt để trả lại các điều kiện về mặc định.

Tuy nhiên, trong trường hợp này, không quá nhiều.

Tại sao?:

Ngoài ra, lưu trữ giá trị IFS có vấn đề.
Nếu IFS ban đầu không được đặt, mã IFS="$OldIFS"sẽ đặt IFS thành "", không bỏ đặt nó.

Để thực sự giữ giá trị của IFS (ngay cả khi chưa đặt), hãy sử dụng:

${IFS+"false"} && unset oldifs || oldifs="$IFS"    # correctly store IFS.

IFS="error"                 ### change and use IFS as needed.

${oldifs+"false"} && unset IFS || IFS="$oldifs"    # restore IFS.

IFS thực sự không thể không được đặt. Nếu bạn bỏ đặt nó, shell sẽ hoàn nguyên giá trị mặc định. Vì vậy, bạn không thực sự cần phải kiểm tra điều đó khi lưu nó.
filbranden

Coi chừng rằng bash, unset IFSkhông thể hủy đặt IFS nếu nó đã được khai báo cục bộ trong ngữ cảnh cha (bối cảnh hàm) và không trong bối cảnh hiện tại.
Stéphane Chazelas

5

Bạn có quyền do dự về việc đóng băng toàn cầu. Đừng sợ, có thể viết mã làm việc sạch mà không bao giờ sửa đổi toàn cầu thực tế IFS, hoặc thực hiện một điệu nhảy lưu / khôi phục rườm rà và dễ bị lỗi.

Bạn có thể:

  • đặt IFS cho một lệnh gọi duy nhất:

    IFS=value command_or_function

    hoặc là

  • đặt IFS bên trong một khung con:

    (IFS=value; statement)
    $(IFS=value; statement)

Ví dụ

  • Để có được một chuỗi được phân cách bằng dấu phẩy từ một mảng:

    str="$(IFS=, ; echo "${array[*]-}")"

    Lưu ý: -Chỉ để bảo vệ một mảng trống set -ubằng cách cung cấp một giá trị mặc định khi bỏ đặt (giá trị đó là chuỗi trống trong trường hợp này) .

    Việc IFSsửa đổi chỉ được áp dụng bên trong lớp vỏ được sinh ra bằng cách $() thay thế lệnh . Điều này là do các lớp con có các bản sao của các biến gọi của shell và do đó có thể đọc các giá trị của chúng, nhưng bất kỳ sửa đổi nào được thực hiện bởi lớp con chỉ ảnh hưởng đến bản sao của lớp con chứ không phải biến của cha mẹ.

    Bạn cũng có thể nghĩ: tại sao không bỏ qua subshell và chỉ làm điều này:

    IFS=, str="${array[*]-}"  # Don't do this!

    Không có lệnh gọi nào ở đây và dòng này thay vào đó được hiểu là hai phép gán biến độc lập tiếp theo, như thể nó là:

    IFS=,                     # Oops, global IFS was modified
    str="${array[*]-}"

    Cuối cùng, hãy giải thích tại sao biến thể này không hoạt động:

    # Notice missing ';' before echo
    str="$(IFS=, echo "${array[*]-}")" # Don't do this! 

    Các echolệnh thực sự sẽ được gọi với nó IFSthiết lập biến ,, nhưng echokhông quan tâm hoặc sử dụng IFS. Phép thuật mở rộng "${array[*]}"thành một chuỗi được thực hiện bởi chính (phụ) trước đó echothậm chí còn được gọi.

  • Để đọc trong toàn bộ tệp (không chứa NULLbyte) vào một biến có tên VAR:

    IFS= read -r -d '' VAR < "${filepath}"

    Lưu ý: IFS=giống như IFS=""IFS='', tất cả đều đặt IFS thành chuỗi rỗng, rất khác với unset IFS: nếu IFSkhông được đặt, hành vi của tất cả các hàm bash sử dụng bên IFStrong giống hệt như IFScó giá trị mặc định $' \t\n'.

    Đặt IFSthành chuỗi trống đảm bảo khoảng trắng đầu và cuối được giữ nguyên.

    Việc đọc -d ''hoặc chỉ -d ""đọc dừng lệnh gọi hiện tại của nó trên một NULLbyte, thay vì dòng mới thông thường.

  • Để phân chia $PATHtheo :dấu phân cách của nó :

    IFS=":" read -r -d '' -a paths <<< "$PATH"

    Ví dụ này hoàn toàn mang tính minh họa. Trong trường hợp chung khi bạn đang phân tách dọc theo một dấu phân cách, có thể các trường riêng lẻ có thể chứa (một phiên bản thoát của) dấu phân cách đó. Hãy suy nghĩ về việc cố gắng đọc - trong một hàng của .csvtệp có các cột có thể chứa dấu phẩy (thoát hoặc trích dẫn theo một cách nào đó). Đoạn mã trên sẽ không hoạt động như dự định cho các trường hợp như vậy.

    Điều đó nói rằng, bạn không có khả năng gặp phải những :con đường liên quan như vậy bên trong $PATH. Mặc dù tên đường dẫn UNIX / Linux được phép chứa a :, nhưng dường như bash sẽ không thể xử lý các đường dẫn như vậy nếu bạn cố gắng thêm chúng vào $PATHvà lưu trữ các tệp thực thi trong chúng, vì không có mã để phân tích cú pháp thoát / trích dẫn : mã nguồn của bash 4.4 .

    Cuối cùng, lưu ý rằng đoạn trích nối thêm một dòng mới vào phần tử cuối cùng của mảng kết quả (như được gọi bởi @ StéphaneChazelas trong các nhận xét hiện đã bị xóa) và nếu đầu vào là chuỗi trống, đầu ra sẽ là một phần tử đơn mảng, trong đó phần tử sẽ bao gồm một dòng mới ( $'\n').

Động lực

Cách old_IFS="${IFS}"; command; IFS="${old_IFS}"tiếp cận cơ bản chạm đến toàn cầu IFSsẽ hoạt động như mong đợi đối với các tập lệnh đơn giản nhất. Tuy nhiên, ngay khi bạn thêm bất kỳ sự phức tạp nào, nó có thể dễ dàng phá vỡ và gây ra các vấn đề tinh tế:

  • Nếu commandlà một hàm bash cũng sửa đổi toàn cục IFS(trực tiếp hoặc, ẩn khỏi chế độ xem, bên trong một chức năng khác mà nó gọi) và trong khi thực hiện nhầm sử dụng cùng một old_IFSbiến toàn cục để thực hiện lưu / khôi phục, bạn sẽ gặp lỗi.
  • Như đã chỉ ra trong nhận xét này bởi @Gilles , nếu tình trạng ban đầu của IFSlà unset, ngây thơ tiết kiệm-và-phục hồi sẽ không làm việc, và thậm chí sẽ dẫn đến thất bại hoàn toàn nếu (mis-) được sử dụng phổ biến set -u(aka set -o nounset) tùy chọn vỏ có hiệu lực.
  • Có thể một số mã shell thực thi không đồng bộ với luồng thực thi chính, chẳng hạn như với các trình xử lý tín hiệu (xem help trap). Nếu mã đó cũng sửa đổi toàn cục IFShoặc giả sử nó có một giá trị cụ thể, bạn có thể gặp các lỗi tinh vi.

Bạn có thể tạo ra một chuỗi lưu / khôi phục mạnh mẽ hơn (như câu được đề xuất trong câu trả lời khác này để tránh một số hoặc tất cả các vấn đề này. Tuy nhiên, bạn sẽ phải lặp lại đoạn mã nồi hơi ồn ào đó bất cứ khi nào bạn cần tạm thời tùy chỉnh IFS. giảm khả năng đọc và bảo trì mã.

Xem xét bổ sung cho các kịch bản giống như thư viện

IFSđặc biệt là mối quan tâm đối với các tác giả của các thư viện hàm shell, những người cần đảm bảo mã của họ hoạt động mạnh mẽ bất kể trạng thái toàn cầu ( IFS, tùy chọn shell, ...) được áp đặt bởi những kẻ xâm lược của họ, và cũng không làm phiền trạng thái đó (những kẻ xâm lược có thể dựa vào trên đó để luôn luôn tĩnh).

Khi viết mã thư viện, bạn không thể dựa vào IFSviệc có bất kỳ giá trị cụ thể nào (thậm chí không phải là giá trị mặc định) hoặc thậm chí được đặt ở tất cả. Thay vào đó, bạn cần đặt rõ ràng IFScho bất kỳ đoạn mã nào có hành vi phụ thuộc vào IFS.

Nếu IFSđược đặt rõ ràng thành giá trị cần thiết (ngay cả khi đó là giá trị mặc định) trong mỗi dòng mã trong đó giá trị sử dụng bất kỳ cơ chế nào trong hai cơ chế được mô tả trong câu trả lời này là phù hợp để bản địa hóa hiệu ứng, thì cả hai mã đều độc lập với nhà nước toàn cầu và tránh hoàn toàn việc đóng băng nó. Cách tiếp cận này có thêm lợi ích là làm cho nó rất rõ ràng đối với một người đọc tập lệnh IFSquan trọng đối với chính xác một lệnh / mở rộng này với chi phí văn bản tối thiểu (so với ngay cả lưu / khôi phục cơ bản nhất).

Mã nào bị ảnh hưởng bởi IFSanyway?

May mắn thay, không có nhiều kịch bản có IFSvấn đề (giả sử bạn luôn trích dẫn phần mở rộng của mình ):

  • "$*""${array[*]}"mở rộng
  • các lệnh của readmục tiêu tích hợp nhiều biến ( read VAR1 VAR2 VAR3) hoặc biến mảng ( read -a ARRAY_VAR_NAME)
  • yêu cầu readnhắm mục tiêu một biến duy nhất khi nói đến các ký tự khoảng trắng đầu hoặc cuối hoặc các ký tự không khoảng trắng xuất hiện trong IFS.
  • tách từ (chẳng hạn như mở rộng không được trích dẫn, mà bạn có thể muốn tránh như bệnh dịch hạch )
  • một số tình huống ít phổ biến khác (Xem: IFS @ Greg's Wiki )

Tôi không thể nói rằng tôi hiểu cách chia $ PATH cùng với nó: các dấu phân cách giả sử không có thành phần nào chứa câu : mình . Làm thế nào các thành phần có thể chứa :khi :là dấu phân cách?
Stéphane Chazelas

@ StéphaneChazelas Vâng, :là một ký tự hợp lệ để sử dụng trong tên tệp trên hầu hết các hệ thống tệp UNIX / Linux, vì vậy hoàn toàn có thể có một thư mục chứa tên :. Có lẽ một số shell có một điều khoản để thoát :trong PATH bằng cách sử dụng một cái gì đó như \:, và sau đó bạn sẽ thấy các cột xuất hiện không phải là dấu phân cách thực tế (Có vẻ như bash không cho phép thoát như vậy. Hàm cấp thấp được sử dụng khi lặp qua $PATHchỉ tìm kiếm :trong một chuỗi C: git.savannah.gnu.org/cgit/bash.git/tree/general.c#n891 ).
sls

Tôi đã sửa đổi câu trả lời để hy vọng làm cho $PATHví dụ chia tách :rõ ràng hơn.
sls

1
Chào mừng đến với SO! Cảm ơn vì câu trả lời sâu sắc như vậy :)
Steven Lu

1

Đây có phải là thực tế? Hoặc về cơ bản là vô nghĩa và tôi chỉ nên đặt IFS trực tiếp trở lại bất cứ thứ gì nó cần cho những lần sử dụng tiếp theo?

Tại sao phải mạo hiểm cài đặt lỗi chính tả cho IFS $' \t\n'khi tất cả những gì bạn phải làm là

OIFS=$IFS
do_your_thing
IFS=$OIFS

Ngoài ra, bạn có thể gọi một mạng con nếu bạn không cần bất kỳ biến nào được đặt / sửa đổi trong:

( IFS=:; do_your_thing; )

Điều này là nguy hiểm vì nó không hoạt động nếu IFSban đầu không được đặt.
Gilles 'SO- ngừng trở nên xấu xa'
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.