Làm cách nào tôi có thể làm cứng các tập lệnh bash chống lại tác hại khi thay đổi trong tương lai?


46

Vì vậy, tôi đã xóa thư mục nhà của mình (hoặc chính xác hơn là tất cả các tệp tôi có quyền truy cập ghi). Điều gì đã xảy ra là tôi đã có

build="build"
...
rm -rf "${build}/"*
...
<do other things with $build>

trong một tập lệnh bash và sau khi không còn cần thiết nữa $build, hãy xóa phần khai báo và tất cả các công dụng của nó - nhưng rm. Bash vui vẻ mở rộng ra rm -rf /*. Vâng

Tôi cảm thấy ngu ngốc, cài đặt bản sao lưu, làm lại công việc tôi đã mất. Cố gắng vượt qua sự xấu hổ.

Bây giờ, tôi tự hỏi: các kỹ thuật để viết các tập lệnh bash là gì để những lỗi như vậy không thể xảy ra, hoặc ít nhất là ít có khả năng? Chẳng hạn, tôi đã viết

FileUtils.rm_rf("#{build}/*")

trong một kịch bản Ruby, thông dịch viên sẽ phàn nàn về việc buildkhông được khai báo, vì vậy có ngôn ngữ bảo vệ tôi.

Những gì tôi đã xem xét trong bash, bên cạnh corraling rm(mà, như nhiều câu trả lời trong các câu hỏi liên quan đề cập, không phải là không có vấn đề):

  1. rm -rf "./${build}/"*
    Điều đó sẽ giết chết công việc hiện tại của tôi (một repo Git) nhưng không có gì khác.
  2. Một biến thể / tham số hóa rmyêu cầu tương tác khi hoạt động bên ngoài thư mục hiện tại. (Không thể tìm thấy bất kỳ.) Hiệu ứng tương tự.

Là nó, hoặc có những cách khác để viết các tập lệnh bash "mạnh mẽ" theo nghĩa này?


3
Không có lý do cho rm -rf "${build}/*dù dấu ngoặc kép đi đâu. rm -rf "${build} sẽ làm điều tương tự vì f.
Monty Harder

1
Kiểm tra tĩnh với shellcheck.net là một nơi rất chắc chắn để bắt đầu. Có sẵn tích hợp trình chỉnh sửa, vì vậy bạn có thể nhận được cảnh báo từ các công cụ của mình ngay khi bạn xóa định nghĩa về thứ gì đó vẫn được sử dụng.
Charles Duffy

@CharlesDuffy Để add để xấu hổ của tôi, tôi đã viết kịch bản trong một IDE IDEA-phong cách với BashSupport cài đặt, mà không cảnh báo trong trường hợp đó. Vì vậy, có, điểm hợp lệ, nhưng tôi thực sự cần hủy bỏ khó khăn.
Raphael

2
Gotcha. Hãy chắc chắn lưu ý các cảnh báo trong BashFAQ # 112 . set -ukhông phải là gần như cau mày như vậy set -e, nhưng nó vẫn có vấn đề.
Charles Duffy

2
câu trả lời đùa: Chỉ cần đặt #! /usr/bin/env rubyở đầu của mọi kịch bản shell và quên bash;)
Pod

Câu trả lời:


75
set -u

hoặc là

set -o nounset

Điều này sẽ làm cho trình bao hiện tại xử lý việc mở rộng các biến không đặt là lỗi:

$ unset build
$ set -u
$ rm -rf "$build"/*
bash: build: unbound variable

set -uset -o nounsetcác tùy chọn vỏ POSIX .

Một giá trị trống sẽ không gây ra lỗi mặc dù.

Cho rằng, sử dụng

$ rm -rf "${build:?Error, variable is empty or unset}"/*
bash: build: Error, variable is empty or unset

Việc mở rộng ${variable:?word}sẽ mở rộng thành giá trị variabletrừ khi nó trống hoặc không được đặt. Nếu nó trống hoặc không được đặt, phần tử wordsẽ được hiển thị trên lỗi tiêu chuẩn và trình bao sẽ coi việc mở rộng là một lỗi (lệnh sẽ không được thực thi và nếu chạy trong trình bao không tương tác, điều này sẽ chấm dứt). Rời :khỏi sẽ chỉ gây ra lỗi cho một giá trị chưa được đặt, giống như dưới set -u.

${variable:?word}là một mở rộng tham số POSIX .

Cả hai điều này sẽ khiến một lớp vỏ tương tác chấm dứt trừ khi set -e(hoặc set -o errexit) cũng có hiệu lực. ${variable:?word}làm cho các script thoát ra nếu biến trống hoặc không được đặt. set -usẽ khiến một đoạn script thoát ra nếu được sử dụng cùng với set -e.


Đối với câu hỏi thứ hai của bạn. Không có cách nào để giới hạn rmkhông hoạt động bên ngoài thư mục hiện tại.

Việc triển khai GNU rmcó một --one-file-systemtùy chọn ngăn không cho đệ quy xóa các hệ thống tệp được gắn kết, nhưng nó gần như tôi tin rằng chúng ta có thể nhận được mà không cần thực hiện rmcuộc gọi trong một hàm thực sự kiểm tra các đối số.


Như một lưu ý phụ: ${build}hoàn toàn tương đương với $buildtrừ khi việc mở rộng xảy ra như một phần của chuỗi trong đó ký tự ngay sau là ký tự hợp lệ trong tên biến, chẳng hạn như trong "${build}x".


Tốt lắm, cảm ơn nhé! 1) Là nó ${build:?msg}hay ${build?msg}? 2) Trong bối cảnh của một cái gì đó như xây dựng tập lệnh, tôi nghĩ rằng sử dụng một công cụ khác với rm để xóa an toàn hơn sẽ ổn: chúng tôi biết rằng chúng tôi không muốn làm việc bên ngoài thư mục hiện tại, vì vậy chúng tôi làm rõ điều đó bằng cách sử dụng hạn chế chỉ huy. Nói chung không cần phải làm cho rm an toàn hơn.
Raphael

1
@Raphael Xin lỗi, nên với :. Tôi sẽ sửa lỗi đánh máy đó ngay bây giờ và tôi sẽ đề cập đến ý nghĩa của nó sau này khi tôi quay lại máy tính của mình.
Kusalananda

2
@Raphael Ok, tôi đã thêm một câu ngắn về những gì xảy ra mà không có :(nó sẽ chỉ gây ra lỗi cho các biến không đặt ). Tôi không dám viết một công cụ có thể đối phó với việc xóa các tệp theo một đường dẫn cụ thể, trong mọi trường hợp. Đường dẫn phân tích cú pháp và quan tâm / không quan tâm đến các liên kết tượng trưng, ​​vv là một chút quá khó khăn với tôi tại thời điểm này.
Kusalananda

1
Cảm ơn, lời giải thích giúp! Về "rm địa phương": Tôi chủ yếu suy ngẫm, có thể câu cá cho "chắc chắn, đó là <tên công cụ>" - nhưng chắc chắn không cố gắng giúp ma cà rồng bạn viết nó! OO Tốt, bạn đã giúp đủ! :)
Raphael

2
@MateuszKonieczny Tôi không nghĩ là tôi sẽ xin lỗi. Tôi tin rằng các biện pháp an toàn như thế này nên được sử dụng khi cần thiết . Như với tất cả mọi thứ làm cho một môi trường an toàn, cuối cùng nó sẽ khiến mọi người ngày càng bất cẩn và phụ thuộc vào các biện pháp an toàn. Tốt hơn là nên biết những gì mỗi biện pháp an toàn làm và sau đó áp dụng chúng một cách có chọn lọc khi cần thiết.
Kusalananda

12

Tôi sẽ đề nghị kiểm tra xác thực bình thường bằng cách sử dụng test/[ ]

Bạn sẽ an toàn nếu bạn viết kịch bản của mình như vậy:

build="build"
...
[ -n "${build}" ] || exit 1
rm -rf "${build}/"*
...

Các [ -n "${build}" ]kiểm tra đó "${build}"là một chuỗi có độ dài khác không .

Các ||là toán tử logic OR trong bash. Nó khiến một lệnh khác được chạy nếu lệnh đầu tiên thất bại.

Theo cách này, đã ${build}trống / không xác định / vv. tập lệnh sẽ thoát (với mã trả về là 1, đây là lỗi chung).

Điều này cũng sẽ bảo vệ bạn trong trường hợp bạn loại bỏ tất cả các sử dụng ${build}[ -n "" ]sẽ luôn sai.


Ưu điểm của việc sử dụng test/ [ ]là có nhiều kiểm tra có ý nghĩa khác mà nó cũng có thể sử dụng.

Ví dụ:

[ -f FILE ] True if FILE exists and is a regular file.
[ -d FILE ] True if FILE exists and is a directory.
[ -O FILE ] True if FILE exists and is owned by the effective user ID.

Công bằng, nhưng một chút khó khăn. Tôi có đang thiếu thứ gì đó không, hay nó có làm tương tự như ${variable:?word}@Kusalananda đề xuất không?
Raphael

@Raphael nó hoạt động tương tự trong trường hợp "chuỗi có giá trị không" nhưng test(nghĩa là [) có nhiều kiểm tra khác có liên quan, chẳng hạn như -d(đối số là thư mục), -f(đối số là tệp), -O(tệp được sở hữu bởi người dùng hiện tại) và như vậy.
Centimane

1
@Raphael cũng vậy, xác nhận phải là một phần bình thường của bất kỳ mã / tập lệnh nào. Cũng lưu ý, nếu bạn đã xóa tất cả các phiên bản của bản dựng, bạn cũng sẽ không xóa ${build:?word}cùng với nó chứ? Các ${variable:?word}định dạng không bảo vệ bạn nếu bạn loại bỏ tất cả các trường của biến.
Centimane

1
1) Tôi nghĩ rằng có một sự khác biệt giữa việc đảm bảo các giả định được giữ (các tệp tồn tại, v.v.) và kiểm tra xem các biến có được đặt không (công việc imho của trình biên dịch / trình thông dịch). Nếu tôi phải làm điều đó bằng tay, tốt hơn hết là có cú pháp cực kỳ hấp dẫn cho nó - điều mà ifkhông phải là toàn diện . 2) "bạn cũng sẽ không xóa $ {build :? Word} cùng với nó" - kịch bản là tôi đã bỏ lỡ việc sử dụng biến. Sử dụng ${v:?w}sẽ bảo vệ tôi khỏi thiệt hại. Tôi đã loại bỏ tất cả các cách sử dụng, thậm chí truy cập đơn giản sẽ không có hại, rõ ràng.
Raphael

Tất cả đã nói, câu trả lời của bạn là một câu trả lời công bằng và hữu ích cho câu hỏi chung chung: đảm bảo các giả định được giữ quan trọng đối với các kịch bản tồn tại xung quanh. Vấn đề cụ thể trong cơ thể câu hỏi là, imho, được trả lời tốt hơn bởi Kusalananda. Cảm ơn!
Raphael

4

Trong trường hợp cụ thể của bạn, trước đây tôi đã làm lại 'xóa' để di chuyển tệp / thư mục (giả sử / tmp nằm trên cùng phân vùng với thư mục của bạn):

# mktemp -d is also a good, reliable choice
trashdir=/tmp/.trash-$USER/trash-`date`
mkdir -p "$trashdir"
...
mv "${build}"/* "$trashdir"
...

Đằng sau hậu trường, điều này di chuyển các tham chiếu tệp / thư mục toplevel từ nguồn sang $trashdircấu trúc thư mục đích trên cùng một phân vùng và không mất thời gian đi bộ cấu trúc thư mục và giải phóng các khối đĩa trên mỗi tệp ngay lúc đó. Điều này tạo ra việc dọn dẹp nhanh hơn nhiều trong khi hệ thống đang hoạt động, đổi lại việc khởi động lại chậm hơn một chút (/ tmp được làm sạch khi khởi động lại).

Ngoài ra, một mục cron để dọn dẹp định kỳ /tmp/.trash-$USER sẽ giữ / tmp không bị đầy, đối với các quy trình (ví dụ: bản dựng) tiêu tốn nhiều dung lượng đĩa. Nếu thư mục của bạn nằm trên một phân vùng khác là / tmp, bạn có thể tạo một thư mục tương tự / tmp trên phân vùng của mình và thay vào đó là cron dọn dẹp.

Mặc dù vậy, quan trọng nhất, nếu bạn làm hỏng các biến theo bất kỳ cách nào, bạn có thể khôi phục nội dung trước khi dọn dẹp xảy ra.


2
"Điều này tạo ra việc dọn dẹp nhanh hơn nhiều trong khi hệ thống đang hoạt động" - phải không? Tôi đã nghĩ cả hai chỉ về việc thay đổi các nút bị ảnh hưởng. Nó chắc chắn không nhanh hơn nếu /tmpnằm trên một phân vùng khác (tôi nghĩ nó luôn dành cho tôi, ít nhất là đối với các tập lệnh chạy trong không gian người dùng); sau đó, thư mục thùng rác cần được thay đổi (và sẽ không thu được lợi nhuận từ việc xử lý hệ điều hành /tmp).
Raphael

1
Bạn có thể sử dụng mktemp -dđể có được thư mục tạm thời đó mà không giẫm lên ngón chân của quá trình khác (và để tôn vinh chính xác $TMPDIR).
Toby Speight

Đã thêm ghi chú chính xác và hữu ích của bạn từ cả hai bạn, cảm ơn. Tôi nghĩ rằng ban đầu tôi đã đăng bài này quá nhanh mà không xem xét các điểm bạn đã đưa lên.
dùng117529

2

Sử dụng thay thế paramater bash để gán mặc định khi biến không được khởi tạo, ví dụ:

rm -rf $ {biến: - "/ nonexistent"}


1

Tôi luôn cố gắng bắt đầu các tập lệnh Bash của mình bằng một dòng #!/bin/bash -ue.

-ecó nghĩa là "thất bại trên e rror đầu tiên ";

-ucó nghĩa là "thất bại trong lần sử dụng đầu tiên của biến u ndeclared".

Tìm thêm chi tiết trong bài viết tuyệt vời Sử dụng Chế độ nghiêm ngặt Bash không chính thức (Trừ khi bạn gỡ lỗi) . Ngoài ra tác giả khuyên bạn nên sử dụng set -o pipefail; IFS=$'\n\t'nhưng với mục đích của tôi thì điều này là quá mức cần thiết.


1

Lời khuyên chung để kiểm tra xem biến của bạn có được đặt hay không, là một công cụ hữu ích để ngăn chặn loại vấn đề này. Nhưng trong trường hợp này đã có một giải pháp đơn giản hơn.

Rất có thể không cần phải toàn cầu hóa nội dung của $buildthư mục để xóa chúng mà không phải là $buildthư mục thực sự . Vì vậy, nếu bạn bỏ qua phần không liên quan *thì một giá trị unset sẽ biến thành rm -rf /mặc định, hầu hết các triển khai rm trong thập kỷ trước sẽ từ chối thực hiện (trừ khi bạn vô hiệu hóa bảo vệ này với --no-preserve-roottùy chọn của GNU rm ).

Bỏ qua dấu vết /cũng sẽ dẫn đến rm ''thông báo lỗi:

rm: can't remove '': No such file or directory

Điều này hoạt động ngay cả khi lệnh rm của bạn không thực hiện bảo vệ cho /.


-2

Bạn đang suy nghĩ về các thuật ngữ ngôn ngữ lập trình, nhưng bash là một ngôn ngữ kịch bản :-) Vì vậy, hãy sử dụng một mô hình hướng dẫn hoàn toàn khác nhau cho một mô hình ngôn ngữ hoàn toàn khác .

Trong trường hợp này:

rmdir ${build}

rmdirsẽ từ chối xóa một thư mục không trống, trước tiên bạn cần xóa các thành viên. Bạn biết những thành viên đó là gì, phải không? Chúng có thể là các tham số cho tập lệnh của bạn hoặc xuất phát từ một tham số, vì vậy:

rm -rf ${build}/${parameter}
rmdir ${build}

Bây giờ, nếu bạn đặt một số tệp hoặc thư mục khác vào đó như tệp tạm thời hoặc thứ gì đó mà bạn không nên có, rmdirsẽ gây ra lỗi. Xử lý nó đúng cách, sau đó:

rmdir ${build} || build_dir_not_empty "${build}"

Cách suy nghĩ này đã phục vụ tôi rất tốt bởi vì ... vâng, đã ở đó, đã làm điều đó.


6
Cảm ơn cho nỗ lực của bạn, nhưng điều này là hoàn toàn không đúng. Giả định "bạn biết những thành viên đó là gì" là không chính xác; nghĩ về đầu ra trình biên dịch. make cleansẽ sử dụng một số ký tự đại diện (trừ khi trình biên dịch rất siêng năng tạo danh sách mọi tệp mà nó tạo). Ngoài ra, đối với tôi, dường như rm -rf ${build}/${parameter}chỉ có vấn đề đi xuống một chút.
Raphael

Hừm. Nhìn thế nào mà đọc. "Cảm ơn bạn, nhưng đây là điều mà tôi đã không tiết lộ trong câu hỏi ban đầu vì vậy tôi sẽ không chỉ từ chối câu trả lời của bạn mà còn đánh giá thấp nó, mặc dù nó được áp dụng nhiều hơn cho trường hợp chung." Ồ
Giàu

"Hãy để tôi đưa ra các giả định bổ sung ngoài những gì bạn đã viết trong câu hỏi và sau đó viết một câu trả lời chuyên ngành." * nhún vai * (FYI, không phải là nó bất kỳ doanh nghiệp của bạn: Tôi không downvote Có thể cho rằng tôi nên có, bởi vì câu trả lời là không hữu ích (với tôi, ít nhất)..)
Raphael

Hừm. Tôi không đưa ra giả định, và viết một câu trả lời chung chung . Không có gì về makecâu hỏi. Viết một câu trả lời chỉ áp dụng được makesẽ là một giả định rất cụ thể và đáng ngạc nhiên. Câu trả lời của tôi hoạt động cho các trường hợp khác make, ngoài phát triển phần mềm, ngoài vấn đề người mới cụ thể mà bạn gặp phải bằng cách xóa nội dung của mình.
Giàu

1
Kusalananda dạy tôi cách câu cá. Bạn đi cùng và nói, "Vì bạn sống ở đồng bằng, tại sao bạn không ăn thịt bò thay thế?"
Raphael
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.