Làm thế nào để đọc toàn bộ kịch bản shell trước khi thực hiện nó?


35

Thông thường, nếu bạn chỉnh sửa một bản kiểm tra, tất cả các cách sử dụng tập lệnh đều dễ bị lỗi.

Theo như tôi hiểu, bash (các shell khác cũng vậy?) Đọc tập lệnh tăng dần, vì vậy nếu bạn sửa đổi tập tin bên ngoài, nó bắt đầu đọc sai nội dung. Có cách nào để ngăn chặn nó?

Thí dụ:

sleep 20

echo test

Nếu bạn thực thi tập lệnh này, bash sẽ đọc dòng đầu tiên (giả sử 10 byte) và đi ngủ. Khi nó tiếp tục, có thể có các nội dung khác nhau trong tập lệnh bắt đầu từ byte thứ 10. Tôi có thể ở giữa một dòng trong kịch bản mới. Do đó, kịch bản đang chạy sẽ bị hỏng.


Bạn có ý nghĩa gì khi "sửa đổi tập lệnh bên ngoài"?
malawlawns

1
Có lẽ có một cách để bọc tất cả các nội dung trong một chức năng hoặc một cái gì đó, vì vậy trình bao sẽ đọc toàn bộ tập lệnh trước? Nhưng những gì về dòng cuối cùng mà bạn gọi hàm, nó sẽ được đọc cho đến EOF? Có lẽ bỏ qua cuối cùng \nsẽ làm gì? Có lẽ một subshell ()sẽ làm gì? Tôi không có nhiều kinh nghiệm với nó, xin vui lòng giúp đỡ!
VasyaNovikov

@malawlawns nếu tập lệnh có nội dung như thế nào sleep 20 ;\n echo test ;\n sleep 20và tôi bắt đầu chỉnh sửa nó, nó có thể hoạt động sai. Ví dụ, bash có thể đọc 10 byte đầu tiên của tập lệnh, hiểu sleeplệnh và đi ngủ. Sau khi nó tiếp tục, sẽ có các nội dung khác nhau trong tệp bắt đầu từ 10 byte.
VasyaNovikov

1
Vì vậy, những gì bạn đang nói là bạn đang chỉnh sửa một tập lệnh đang thực thi? Dừng tập lệnh trước, thực hiện các chỉnh sửa của bạn và sau đó bắt đầu lại tập lệnh.
malawlawns

@malawlawns vâng, về cơ bản là vậy. Vấn đề là, nó không thuận tiện cho tôi để dừng các kịch bản, và thật khó để luôn nhớ làm điều đó. Có lẽ có một cách để buộc bash đọc toàn bộ kịch bản trước?
VasyaNovikov

Câu trả lời:


43

Có shell, và bashđặc biệt, cẩn thận để đọc từng dòng một tệp, vì vậy nó hoạt động tương tự như khi bạn sử dụng nó một cách tương tác.

Bạn sẽ nhận thấy rằng khi tệp không thể tìm kiếm được (như một đường ống), bashthậm chí đọc từng byte một lần để đảm bảo không đọc qua \nký tự. Khi tệp có thể tìm kiếm, nó sẽ tối ưu hóa bằng cách đọc các khối đầy đủ tại một thời điểm, nhưng tìm kiếm trở lại sau \n.

Điều đó có nghĩa là bạn có thể làm những việc như:

bash << \EOF
read var
var's content
echo "$var"
EOF

Hoặc viết kịch bản tự cập nhật. Điều mà bạn sẽ không thể làm nếu nó không mang lại cho bạn sự đảm bảo đó.

Bây giờ, thật hiếm khi bạn muốn làm những việc như vậy và, như bạn phát hiện ra, tính năng đó có xu hướng trở nên thường xuyên hơn là hữu ích.

Để tránh điều đó, bạn có thể thử và đảm bảo rằng bạn không sửa đổi tệp tại chỗ (ví dụ: sửa đổi một bản sao và di chuyển bản sao tại chỗ (ví dụ như sed -ihoặc perl -pimột số biên tập viên thực hiện)).

Hoặc bạn có thể viết kịch bản của bạn như:

{
  sleep 20
  echo test
}; exit

(lưu ý rằng điều quan trọng là exitphải nằm trên cùng một đường với }; mặc dù bạn cũng có thể đặt nó bên trong niềng răng ngay trước khi đóng).

hoặc là:

main() {
  sleep 20
  echo test
}
main "$@"; exit

Shell sẽ cần đọc kịch bản cho đến exittrước khi bắt đầu làm bất cứ điều gì. Điều đó đảm bảo shell sẽ không đọc lại từ script.

Điều đó có nghĩa là toàn bộ tập lệnh sẽ được lưu trữ trong bộ nhớ.

Điều đó cũng có thể ảnh hưởng đến việc phân tích cú pháp của tập lệnh.

Ví dụ bash: trong :

export LC_ALL=fr_FR.UTF-8
echo $'St\ue9phane'

Sẽ xuất ra rằng U + 00E9 được mã hóa trong UTF-8. Tuy nhiên, nếu bạn thay đổi nó thành:

{
  export LC_ALL=fr_FR.UTF-8
  echo $'St\ue9phane'
}

Các \ue9sẽ được mở rộng trong charset đó là có hiệu lực tại thời điểm mà lệnh đã được phân tích trong trường hợp này là trước khi các exportlệnh được thực thi.

Cũng lưu ý rằng nếu lệnh sourceaka .được sử dụng, với một số shell, bạn sẽ gặp vấn đề tương tự đối với các tệp có nguồn gốc.

Đó không phải là trường hợp bashmặc dù sourcelệnh của bạn đọc tệp đầy đủ trước khi diễn giải nó. Nếu viết bashcụ thể, bạn thực sự có thể sử dụng điều đó, bằng cách thêm vào lúc bắt đầu tập lệnh:

if [[ ! $already_sourced ]]; then
  already_sourced=1
  source "$0"; exit
fi

(Tôi sẽ không dựa vào điều đó mặc dù bạn có thể tưởng tượng các phiên bản trong tương lai bashcó thể thay đổi hành vi đó hiện được coi là hạn chế (bash và AT & T ksh là những vỏ giống như POSIX duy nhất hoạt động như vậy theo như có thể nói) và already_sourcedmẹo này hơi dễ vỡ vì nó giả sử rằng biến đó không có trong môi trường, chưa kể đến việc nó ảnh hưởng đến nội dung của biến BASH_SOURCE)


@VasyaNovikov, dường như có điều gì đó không ổn với SE tại thời điểm này (hoặc ít nhất là đối với tôi). Chỉ có một vài câu trả lời khi tôi thêm câu trả lời của tôi và bình luận của bạn dường như chỉ được bật lên ngay cả khi nó nói rằng nó đã được đăng 16 phút trước (hoặc có thể đó chỉ là tôi làm mất viên bi của tôi). Dù sao, lưu ý thêm "lối ra" cần thiết ở đây để tránh các vấn đề khi kích thước của tệp tăng lên (như đã lưu ý trong nhận xét tôi đã thêm vào câu trả lời của bạn).
Stéphane Chazelas

Stéphane, tôi nghĩ rằng tôi đã tìm thấy một giải pháp khác. Nó là để sử dụng }; exec true. Bằng cách này, không có yêu cầu về dòng mới ở cuối tệp, thân thiện với một số biên tập viên (như emacs). Tất cả các bài kiểm tra mà tôi có thể nghĩ về công việc chính xác với}; exec true
VasyaNovikov

@VasyaNovikov, không chắc ý của bạn là gì. Làm thế nào là tốt hơn }; exit? Bạn cũng đang mất trạng thái thoát.
Stéphane Chazelas

Như đã đề cập ở một câu hỏi khác: thông thường trước tiên phân tích toàn bộ tệp và sau đó thực thi câu lệnh ghép trong trường hợp lệnh dot ( . script) được sử dụng.
schily

@schily, vâng tôi đề cập rằng trong câu trả lời này là một hạn chế của AT & T ksh và bash. Các loại vỏ POSIX khác không có giới hạn đó.
Stéphane Chazelas

12

Bạn chỉ cần xóa tệp (tức là sao chép tệp, xóa tệp, đổi tên bản sao trở lại tên gốc). Trong thực tế, nhiều biên tập viên có thể được cấu hình để làm điều này cho bạn. Khi bạn chỉnh sửa tệp và lưu bộ đệm đã thay đổi vào tệp, thay vì ghi đè tệp, nó sẽ đổi tên tệp cũ, tạo tệp mới và đặt nội dung mới vào tệp mới. Do đó, bất kỳ kịch bản chạy nên tiếp tục mà không có vấn đề.

Bằng cách sử dụng hệ thống kiểm soát phiên bản đơn giản như RCS có sẵn cho vim và emacs, bạn sẽ có được lợi thế kép khi có lịch sử thay đổi và hệ thống kiểm tra nên mặc định xóa tệp hiện tại và tạo lại nó với các chế độ chính xác. (Cẩn thận với các liên kết cứng như vậy tất nhiên).


"xóa" không thực sự là một phần của quá trình. Nếu bạn muốn biến nó thành nguyên tử chính xác, bạn thực hiện đổi tên tệp đích - nếu bạn có bước xóa, sẽ có nguy cơ quá trình của bạn chết sau khi xóa nhưng trước khi đổi tên, không để lại tệp nào ( hoặc người đọc cố gắng truy cập tệp trong cửa sổ đó và không tìm thấy phiên bản cũ cũng như phiên bản mới).
Charles Duffy

11

Giải pháp đơn giản nhất:

{
  ... your code ...

  exit
}

Bằng cách này, bash sẽ đọc toàn bộ {}khối trước khi thực hiện nó vàexit sẽ đảm bảo không có gì được đọc bên ngoài khối mã.

Nếu bạn không muốn "thực thi" tập lệnh, mà là "nguồn" nó, bạn cần một giải pháp khác. Điều này sẽ làm việc sau đó:

{
  ... your code ...

  return 2>/dev/null || exit
}

Hoặc nếu bạn muốn kiểm soát trực tiếp mã thoát:

{
  ... your code ...

  ret="$?";return "$ret" 2>/dev/null || exit "$ret"
}

Võngà! Kịch bản này là an toàn để chỉnh sửa, nguồn và thực hiện. Bạn vẫn phải chắc chắn rằng bạn không sửa đổi nó trong những mili giây đó khi nó ban đầu được đọc.


1
Những gì tôi tìm thấy là nó không thấy EOF và ngừng đọc tệp, nhưng nó bị rối trong quá trình xử lý "luồng đệm" của nó và cuối cùng tìm kiếm ở cuối tệp, đó là lý do tại sao nó trông ổn nếu kích thước của tệp tăng không nhiều, nhưng trông tệ khi bạn tạo tệp lớn hơn gấp đôi so với trước. Tôi sẽ báo cáo lỗi cho những người duy trì bash.
Stéphane Chazelas


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
terdon

5

Bằng chứng của khái niệm. Đây là một kịch bản tự sửa đổi:

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line overwites the on disk copy of the script
cat /tmp/scr2 > /tmp/scr
# this line ends up changed.
echo script content kept
EOF
chmod u+x /tmp/scr
/tmp/scr

chúng tôi thấy bản in đã thay đổi

Điều này là do tải bash giữ một tệp xử lý để mở tập lệnh, vì vậy những thay đổi đối với tệp sẽ được nhìn thấy ngay lập tức.

Nếu bạn không muốn cập nhật bản sao trong bộ nhớ, hãy bỏ liên kết tệp gốc và thay thế nó.

Một cách để làm điều đó là sử dụng sed -i.

sed -i '' filename

bằng chứng của khái niệm

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line unlinks the original and creates a new copy.
sed -i ''  /tmp/scr

# now overwriting it has no immediate effect
cat /tmp/scr2 > /tmp/scr
echo script content kept
EOF

chmod u+x /tmp/scr
/tmp/scr

Nếu bạn đang sử dụng trình chỉnh sửa để thay đổi tập lệnh, bật tính năng "giữ bản sao lưu" có thể là tất cả những gì cần thiết để khiến trình chỉnh sửa ghi phiên bản đã thay đổi thành tệp mới thay vì ghi đè lên tập tin hiện có.


2
Không, bashkhông mở tệp với mmap(). Bạn chỉ cần cẩn thận đọc từng dòng một khi cần thiết, giống như khi nó nhận lệnh từ thiết bị đầu cuối khi tương tác.
Stéphane Chazelas

2

Gói kịch bản của bạn trong một khối {}có thể là tùy chọn tốt nhất nhưng yêu cầu thay đổi tập lệnh của bạn.

F=$(mktemp) && cp test.sh $F && bash $F; rm $F;

sẽ là tùy chọn tốt thứ hai (giả sử tmpfs ) nhược điểm là nó phá vỡ $ 0 nếu tập lệnh của bạn sử dụng điều đó.

sử dụng một cái gì đó như F=test.sh; tail -n $(cat "$F" | wc -l) "$F" | bashlà ít lý tưởng hơn vì nó phải giữ toàn bộ tệp trong bộ nhớ và phá vỡ $ 0.

Nên tránh chạm vào tệp gốc để lần cuối sửa đổi, đọc khóa và liên kết cứng không bị xáo trộn. bằng cách đó, bạn có thể để một trình soạn thảo mở trong khi chạy tệp và rsync sẽ không cần kiểm tra lại tệp để sao lưu và chức năng liên kết cứng như mong đợi.

thay thế tệp khi chỉnh sửa sẽ hoạt động nhưng kém mạnh mẽ hơn vì nó không thể thực thi đối với các tập lệnh / người dùng / hoặc người dùng khác có thể quên. Và một lần nữa nó sẽ phá vỡ các liên kết cứng.


bất cứ điều gì làm cho một bản sao sẽ làm việc. tac test.sh | tac | bash
Jasen
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.