2008-09-17 38 views

Trả lời

190

Tôi đã viết các kịch bản shell khá phức tạp và đề xuất đầu tiên của tôi là "không". Lý do là khá dễ gây ra một sai lầm nhỏ làm cản trở kịch bản của bạn, hoặc thậm chí làm cho nó nguy hiểm.

Điều đó nói rằng, tôi không có các tài nguyên khác để vượt qua bạn nhưng trải nghiệm cá nhân của tôi. Đây là những gì tôi thường làm, đó là quá mức cần thiết, nhưng có xu hướng vững chắc, mặc dù rất tiết.

Invocation

làm cho kịch bản của bạn chấp nhận tùy chọn dài và ngắn. hãy cẩn thận vì có hai lệnh để phân tích cú pháp các tùy chọn, getopt và getopts. Sử dụng getopt khi bạn gặp ít rắc rối hơn.

CommandLineOptions__config_file="" 
CommandLineOptions__debug_level="" 

getopt_results=`getopt -s bash -o c:d:: --long config_file:,debug_level:: -- "[email protected]"` 

if test $? != 0 
then 
    echo "unrecognized option" 
    exit 1 
fi 

eval set -- "$getopt_results" 

while true 
do 
    case "$1" in 
     --config_file) 
      CommandLineOptions__config_file="$2"; 
      shift 2; 
      ;; 
     --debug_level) 
      CommandLineOptions__debug_level="$2"; 
      shift 2; 
      ;; 
     --) 
      shift 
      break 
      ;; 
     *) 
      echo "$0: unparseable option $1" 
      EXCEPTION=$Main__ParameterException 
      EXCEPTION_MSG="unparseable option $1" 
      exit 1 
      ;; 
    esac 
done 

if test "x$CommandLineOptions__config_file" == "x" 
then 
    echo "$0: missing config_file parameter" 
    EXCEPTION=$Main__ParameterException 
    EXCEPTION_MSG="missing config_file parameter" 
    exit 1 
fi 

Một điểm quan trọng khác là chương trình phải luôn trả về 0 nếu hoàn thành thành công, khác 0 nếu có sự cố.

Chức năng gọi

Bạn có thể gọi chức năng trong bash, chỉ cần nhớ để xác định chúng trước khi cuộc gọi. Các hàm giống như các tập lệnh, chúng chỉ có thể trả về các giá trị số. Điều này có nghĩa là bạn phải phát minh ra một chiến lược khác nhau để trả về các giá trị chuỗi. Chiến lược của tôi là sử dụng một biến gọi là RESULT để lưu trữ kết quả và trả về 0 nếu hàm được hoàn thành một cách rõ ràng. Ngoài ra, bạn có thể tăng ngoại lệ nếu bạn trả về một giá trị khác 0 và sau đó đặt hai biến ngoại lệ (ví dụ: EXCEPTION và EXCEPTION_MSG), loại đầu tiên chứa loại ngoại lệ và thứ hai là thông báo có thể đọc được của con người.

Khi bạn gọi một hàm, các tham số của hàm được gán cho các vars đặc biệt $ 0, $ 1 vv. Tôi khuyên bạn nên đặt chúng vào các tên có ý nghĩa hơn. khai báo các biến bên trong hàm như địa phương:

function foo { 
    local bar="$0" 
} 

Lỗi tình huống dễ bị

Trong bash, trừ khi bạn khai báo bằng cách khác, một biến unset được sử dụng như một chuỗi rỗng. Điều này rất nguy hiểm trong trường hợp lỗi đánh máy, vì biến bị đánh sai sẽ không được báo cáo và biến này sẽ được đánh giá là trống. sử dụng

set -o nounset 

để ngăn điều này xảy ra. Hãy cẩn thận, bởi vì nếu bạn làm điều này, chương trình sẽ hủy bỏ mỗi khi bạn đánh giá một biến không xác định.Vì lý do này, cách duy nhất để kiểm tra xem một biến không được định nghĩa như sau:

if test "x${foo:-notset}" == "xnotset" 
then 
    echo "foo not set" 
fi 

Bạn có thể khai báo các biến như readonly:

readonly readonly_var="foo" 

Mô-đun hóa

Bạn có thể đạt được mô đun "python like" nếu bạn sử dụng mã sau:

set -o nounset 
function getScriptAbsoluteDir { 
    # @description used to get the script path 
    # @param $1 the script $0 parameter 
    local script_invoke_path="$1" 
    local cwd=`pwd` 

    # absolute path ? if so, the first character is a/
    if test "x${script_invoke_path:0:1}" = 'x/' 
    then 
     RESULT=`dirname "$script_invoke_path"` 
    else 
     RESULT=`dirname "$cwd/$script_invoke_path"` 
    fi 
} 

script_invoke_path="$0" 
script_name=`basename "$0"` 
getScriptAbsoluteDir "$script_invoke_path" 
script_absolute_dir=$RESULT 

function import() { 
    # @description importer routine to get external functionality. 
    # @description the first location searched is the script directory. 
    # @description if not found, search the module in the paths contained in $SHELL_LIBRARY_PATH environment variable 
    # @param $1 the .shinc file to import, without .shinc extension 
    module=$1 

    if test "x$module" == "x" 
    then 
     echo "$script_name : Unable to import unspecified module. Dying." 
     exit 1 
    fi 

    if test "x${script_absolute_dir:-notset}" == "xnotset" 
    then 
     echo "$script_name : Undefined script absolute dir. Did you remove getScriptAbsoluteDir? Dying." 
     exit 1 
    fi 

    if test "x$script_absolute_dir" == "x" 
    then 
     echo "$script_name : empty script path. Dying." 
     exit 1 
    fi 

    if test -e "$script_absolute_dir/$module.shinc" 
    then 
     # import from script directory 
     . "$script_absolute_dir/$module.shinc" 
    elif test "x${SHELL_LIBRARY_PATH:-notset}" != "xnotset" 
    then 
     # import from the shell script library path 
     # save the separator and use the ':' instead 
     local saved_IFS="$IFS" 
     IFS=':' 
     for path in $SHELL_LIBRARY_PATH 
     do 
      if test -e "$path/$module.shinc" 
      then 
       . "$path/$module.shinc" 
       return 
      fi 
     done 
     # restore the standard separator 
     IFS="$saved_IFS" 
    fi 
    echo "$script_name : Unable to find module $module." 
    exit 1 
} 

sau đó bạn có thể import các file có phần mở rộng .shinc với cú pháp sau

nhập khẩu "AModule/ModuleFile"

nào sẽ được tìm kiếm trong SHELL_LIBRARY_PATH. Khi bạn luôn nhập vào không gian tên chung, hãy nhớ tiền tố tất cả các hàm và biến của bạn với một tiền tố thích hợp, nếu không bạn có nguy cơ bị xung đột tên. Tôi sử dụng dấu gạch dưới kép như dấu chấm python.

Ngoài ra, đặt này là điều đầu tiên trong module của bạn

# avoid double inclusion 
if test "${BashInclude__imported+defined}" == "defined" 
then 
    return 0 
fi 
BashInclude__imported=1 

Object lập trình hướng

Trong bash, bạn không thể làm lập trình hướng đối tượng, trừ khi bạn xây dựng một hệ thống khá phức tạp của phân bổ đối tượng (tôi đã nghĩ về điều đó. Nó khả thi, nhưng điên rồ). Trong thực tế, bạn có thể thực hiện "Lập trình theo định hướng Singleton": bạn có một cá thể của mỗi đối tượng và chỉ một đối tượng.

Điều tôi làm là: tôi định nghĩa một đối tượng thành một mô-đun (xem mục nhập mô-đun hóa). Sau đó, tôi xác định vars trống (tương tự như các biến thành viên) một hàm init (constructor) và hàm thành viên, giống như trong mã ví dụ này

# avoid double inclusion 
if test "${Table__imported+defined}" == "defined" 
then 
    return 0 
fi 
Table__imported=1 

readonly Table__NoException="" 
readonly Table__ParameterException="Table__ParameterException" 
readonly Table__MySqlException="Table__MySqlException" 
readonly Table__NotInitializedException="Table__NotInitializedException" 
readonly Table__AlreadyInitializedException="Table__AlreadyInitializedException" 

# an example for module enum constants, used in the mysql table, in this case 
readonly Table__GENDER_MALE="GENDER_MALE" 
readonly Table__GENDER_FEMALE="GENDER_FEMALE" 

# private: prefixed with p_ (a bash variable cannot start with _) 
p_Table__mysql_exec="" # will contain the executed mysql command 

p_Table__initialized=0 

function Table__init { 
    # @description init the module with the database parameters 
    # @param $1 the mysql config file 
    # @exception Table__NoException, Table__ParameterException 

    EXCEPTION="" 
    EXCEPTION_MSG="" 
    EXCEPTION_FUNC="" 
    RESULT="" 

    if test $p_Table__initialized -ne 0 
    then 
     EXCEPTION=$Table__AlreadyInitializedException 
     EXCEPTION_MSG="module already initialized" 
     EXCEPTION_FUNC="$FUNCNAME" 
     return 1 
    fi 


    local config_file="$1" 

     # yes, I am aware that I could put default parameters and other niceties, but I am lazy today 
     if test "x$config_file" = "x"; then 
      EXCEPTION=$Table__ParameterException 
      EXCEPTION_MSG="missing parameter config file" 
      EXCEPTION_FUNC="$FUNCNAME" 
      return 1 
     fi 


    p_Table__mysql_exec="mysql --defaults-file=$config_file --silent --skip-column-names -e " 

    # mark the module as initialized 
    p_Table__initialized=1 

    EXCEPTION=$Table__NoException 
    EXCEPTION_MSG="" 
    EXCEPTION_FUNC="" 
    return 0 

} 

function Table__getName() { 
    # @description gets the name of the person 
    # @param $1 the row identifier 
    # @result the name 

    EXCEPTION="" 
    EXCEPTION_MSG="" 
    EXCEPTION_FUNC="" 
    RESULT="" 

    if test $p_Table__initialized -eq 0 
    then 
     EXCEPTION=$Table__NotInitializedException 
     EXCEPTION_MSG="module not initialized" 
     EXCEPTION_FUNC="$FUNCNAME" 
     return 1 
    fi 

    id=$1 

     if test "x$id" = "x"; then 
      EXCEPTION=$Table__ParameterException 
      EXCEPTION_MSG="missing parameter identifier" 
      EXCEPTION_FUNC="$FUNCNAME" 
      return 1 
     fi 

    local name=`$p_Table__mysql_exec "SELECT name FROM table WHERE id = '$id'"` 
     if test $? != 0 ; then 
     EXCEPTION=$Table__MySqlException 
     EXCEPTION_MSG="unable to perform select" 
     EXCEPTION_FUNC="$FUNCNAME" 
     return 1 
     fi 

    RESULT=$name 
    EXCEPTION=$Table__NoException 
    EXCEPTION_MSG="" 
    EXCEPTION_FUNC="" 
    return 0 
} 

Bẫy và xử lý tín hiệu

Tôi thấy điều này rất hữu ích để bắt và xử lý ngoại lệ.

function Main__interruptHandler() { 
    # @description signal handler for SIGINT 
    echo "SIGINT caught" 
    exit 
} 
function Main__terminationHandler() { 
    # @description signal handler for SIGTERM 
    echo "SIGTERM caught" 
    exit 
} 
function Main__exitHandler() { 
    # @description signal handler for end of the program (clean or unclean). 
    # probably redundant call, we already call the cleanup in main. 
    exit 
} 

trap Main__interruptHandler INT 
trap Main__terminationHandler TERM 
trap Main__exitHandler EXIT 

function Main__main() { 
    # body 
} 

# catch signals and exit 
trap exit INT TERM EXIT 

Main__main "[email protected]" 

gợi ý và lời khuyên

Nếu có gì không làm việc cho một số lý do, hãy cố gắng sắp xếp lại các mã. Đặt hàng là quan trọng và không phải lúc nào cũng trực quan.

thậm chí không xem xét làm việc với tcsh. nó không hỗ trợ chức năng, và nó nói chung là khủng khiếp.

Hy vọng điều đó sẽ hữu ích, mặc dù xin lưu ý. Nếu bạn phải sử dụng những thứ tôi đã viết ở đây, điều đó có nghĩa là vấn đề của bạn quá phức tạp để giải quyết bằng shell. sử dụng ngôn ngữ khác. Tôi đã phải sử dụng nó do yếu tố con người và di sản.

+6

Wow, và tôi nghĩ rằng tôi sẽ cho overkill trong bash ... Tôi có xu hướng sử dụng các chức năng bị cô lập và subshells lạm dụng (do đó tôi bị ảnh hưởng khi tốc độ là trong bất kỳ cách nào có liên quan). Không có biến toàn cục nào, không trong và ngoài (để giữ gìn sự tĩnh lặng). Tất cả trở lại thông qua đầu ra hoặc đầu ra tệp. set -u/set -e (quá xấu thiết lập -e trở thành vô ích ngay sau khi đầu tiên nếu, và hầu hết các mã của tôi thường là ở đó). Đối số hàm được thực hiện với [local something = "$ 1"; shift] (cho phép sắp xếp lại dễ dàng khi tái cấu trúc). Sau một 3000 dòng bash script tôi có xu hướng viết kịch bản nhỏ nhất trong thời trang này ... – Eugene

+5

ur một nhà khoa học điên – Prospero

+0

sửa chữa nhỏ cho mô-đun hóa: 1 bạn cần trở lại sau . "$ script_absolute_dir/$ module.shinc" để tránh bị thiếu cảnh báo. 2 bạn phải đặt IFS = "$ saved_IFS" trước khi bạn quay trở lại tìm mô-đun trong $ SHELL_LIBRARY_PATH – Duff

8

Dễ dàng: sử dụng python thay vì tập lệnh shell. Bạn có thể tăng gấp 100 lần khả năng đọc, mà không phải làm phức tạp bất cứ thứ gì bạn không cần, và bảo toàn khả năng phát triển các phần của tập lệnh thành các hàm, đối tượng, đối tượng liên tục (zodb), các đối tượng phân tán (pyro) gần bất kỳ mã bổ sung nào.

+7

bạn mâu thuẫn với chính mình bằng cách nói "không cần phải phức tạp" và liệt kê các phức tạp khác nhau mà bạn cho là thêm giá trị, trong khi hầu hết các trường hợp đều bị lạm dụng vào quái vật xấu xí thay vì được sử dụng để đơn giản hóa các vấn đề và triển khai. – Evgeny

+3

điều này ngụ ý một nhược điểm lớn, kịch bản của bạn sẽ không được di chuyển trên các hệ thống mà python không có mặt – astropanic

+1

Tôi nhận ra điều này đã được trả lời trong '08 (bây giờ là hai ngày trước '12); tuy nhiên, đối với những người nhìn vào những năm sau này, tôi sẽ cảnh báo bất cứ ai chống lại các ngôn ngữ như Python hay Ruby vì nó có nhiều khả năng hơn và nếu không, đó là lệnh (hoặc vài lần nhấp chuột) không được cài đặt . Nếu bạn cần thêm tính di động, hãy nghĩ đến việc viết chương trình của bạn bằng Java vì bạn sẽ khó có thể tìm thấy một máy không có JVM. –

-1

Hoặc quote cũ tương tự như những gì Joao nói: ". Sử dụng perl Bạn sẽ muốn biết bash nhưng không sử dụng nó"

Đáng buồn là tôi đã quên người đã nói điều đó.

Và có những ngày này tôi muốn giới thiệu python over perl.

8

sử dụng set -e để bạn không cày về phía trước sau khi lỗi. Hãy thử làm cho nó sh tương thích mà không dựa vào bash nếu bạn muốn nó chạy trên không-linux.

20

Hãy xem Advanced Bash-Scripting Guide để có nhiều sự khôn ngoan về kịch bản lệnh shell - không chỉ Bash.

Đừng nghe mọi người nói với bạn để xem các ngôn ngữ khác, được cho là phức tạp hơn. Nếu shell scripting đáp ứng nhu cầu của bạn, hãy sử dụng nó. Bạn muốn có chức năng, chứ không phải fanciness. Các ngôn ngữ mới cung cấp các kỹ năng mới có giá trị cho bản lý lịch của bạn, nhưng điều đó không giúp ích gì nếu bạn có công việc cần được thực hiện và bạn đã biết trình bao.

Như đã nêu, không có nhiều "phương pháp hay nhất" hoặc "mẫu thiết kế" cho kịch bản lệnh shell. Các cách sử dụng khác nhau có các nguyên tắc và xu hướng khác nhau - giống như bất kỳ ngôn ngữ lập trình nào khác.

+7

Lưu ý rằng đối với các tập lệnh có độ phức tạp nhỏ, đó không phải là cách thực hành tốt nhất. Mã hóa không phải là về việc nhận được một cái gì đó để làm việc. Đó là về xây dựng nó một cách nhanh chóng, dễ dàng, và nó là đáng tin cậy, tái sử dụng, và dễ đọc và duy trì (đặc biệt là cho những người khác). Các kịch bản lệnh shell không mở rộng tốt ở mọi cấp độ. Các ngôn ngữ mạnh mẽ hơn đơn giản hơn nhiều đối với các dự án có bất kỳ logic nào. – drifter

8

Biết khi nào nên sử dụng nó. Đối với các lệnh dán nhanh và bẩn cùng nhau thì không sao. Nếu bạn cần thực hiện nhiều hơn một vài quyết định không tầm thường, các vòng lặp, bất cứ điều gì, hãy sử dụng Python, Perl và modularize.

Vấn đề lớn nhất với vỏ thường là kết quả cuối cùng trông giống như một quả bóng bùn lớn, 4000 dòng bash và đang phát triển ... và bạn không thể loại bỏ nó vì bây giờ toàn bộ dự án của bạn phụ thuộc vào nó. Tất nhiên, nó bắt đầu tại 40 dòng của bash đẹp.

6

Để tìm một số "thực hành tốt nhất", hãy tìm cách Linux distro (ví dụ Debian) viết của họ init-script (thường được tìm thấy trong /etc/init.d)

Hầu hết trong số đó là không có "bash-ISMS" và có sự tách biệt tốt các cài đặt cấu hình, thư viện và định dạng nguồn.

Kiểu cá nhân của tôi là viết một bản mô tả chính xác định một số biến mặc định và sau đó cố gắng tải ("nguồn") tệp cấu hình có thể chứa các giá trị mới.

Tôi cố gắng tránh các chức năng vì chúng có xu hướng làm cho tập lệnh phức tạp hơn. (Perl được tạo ra cho mục đích đó.)

Để đảm bảo tập lệnh là di động, hãy kiểm tra không chỉ bằng #!/Bin/sh, mà còn sử dụng #!/Bin/ash, #!/Bin/dash, v.v. .Bạn sẽ phát hiện mã cụ thể Bash sớm đủ.

17

tập lệnh shell là ngôn ngữ được thiết kế để thao tác các tệp và quy trình. Trong khi nó tuyệt vời cho điều đó, nó không phải là một ngôn ngữ mục đích chung, vì vậy luôn luôn cố gắng để keo logic từ tiện ích hiện có hơn là tái tạo logic mới trong kịch bản shell.

Khác với nguyên tắc chung đó tôi đã thu thập một số common shell script mistakes.

Các vấn đề liên quan