Các mẫu thiết kế hoặc thực tiễn tốt nhất cho các kịch bản shell [đã đóng]


167

Có ai biết bất kỳ tài nguyên nào nói về thực tiễn tốt nhất hoặc các mẫu thiết kế cho các kịch bản shell (sh, bash, v.v.) không?


2
Tôi vừa viết lên một bài viết nhỏ về mẫu mẫu trong BASH tối qua. Xem những gì bạn nghĩ.
quickshiftin

Câu trả lời:


222

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

Điều đó nói rằng, tôi không có tài nguyên khác để vượt qua bạn nhưng kinh 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 dài dòng.

Cầu nguyện

làm cho kịch bản của bạn chấp nhận các 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ác tùy chọn, getopt và getopts. Sử dụng getopt khi bạn đối mặt với í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:: -- "$@"`

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 sẽ luôn trả về 0 nếu hoàn thành thành công, khác không nếu có sự cố.

Chức năng gọi

Bạn có thể gọi các hàm trong bash, chỉ cần nhớ xác định chúng trước khi 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 để trả về các giá trị chuỗi. Chiến lược của tôi là sử dụng một biến có tên là KẾT QUẢ để lưu trữ kết quả và trả về 0 nếu chức năng hoàn thành sạch sẽ. Ngoài ra, bạn có thể đưa ra các ngoại lệ nếu bạn trả về một giá trị khác 0, sau đó đặt hai "biến ngoại lệ" (của tôi: EXCEPTION và EXCEPTION_MSG), loại đầu tiên chứa loại ngoại lệ và thứ hai là thông điệp 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, v.v. 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 là local:

function foo {
   local bar="$0"
}

Tình huống dễ bị lỗi

Trong bash, trừ khi bạn khai báo 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 đánh máy, vì biến được gõ sai sẽ không được báo cáo và nó sẽ được đánh giá là trống. sử dụng

set -o nounset

để ngăn chặn điều này xảy ra. Mặc dù vậy, 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 nếu một biến không được xác định là 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 là chỉ đọc:

readonly readonly_var="foo"

Mô đun hóa

Bạn có thể đạt được mô đun hóa "python like" nếu bạn sử dụng đoạn 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ể nhập tệp với phần mở rộng .shinc với cú pháp sau

nhập "AModule / ModuleFile"

Mà sẽ được tìm kiếm trong SHELL_LIBRARY_PATH. Khi bạn luôn nhập trong không gian tên toàn cục, 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ơ đụng độ tên. Tôi sử dụng dấu gạch dưới kép như dấu chấm trăn.

Ngoài ra, đặt điều này như là điều đầu tiên trong mô-đun của bạn

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

Lập trình hướng đối tượng

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

Những gì tôi làm là: tôi xác định một đối tượng vào một mô-đun (xem mục nhập mô-đun). Sau đó, tôi định nghĩa các vars trống (tương tự với các biến thành viên) một hàm init (hàm tạo) và các hàm thành viên, 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 hữu ích để nắm bắt và xử lý các trường hợp 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 "$@"

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

Nếu một số thứ không hoạt động vì một số lý do, hãy thử sắp xếp lại mã. Thứ tự 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ợ các chức năng và nói chung nó thật kinh khủng.

Hy vọng nó sẽ giúp, 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 để có thể 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.


7
Wow, và tôi nghĩ rằng tôi sẽ sử dụng quá mức trong bash ... Tôi có xu hướng sử dụng các chức năng bị cô lập và lạm dụng các mạng con (do đó tôi phải chịu đựng khi tốc độ có liên quan). Không có biến toàn cầu bao giờ, không vào hoặc ra (để giữ gìn sự tỉnh táo). Tất cả trả về thông qua thiết bị xuất chuẩn hoặc xuất ra tệp. set -u / set -e (set quá tệ -e trở nên vô dụng ngay khi lần đầu tiên và phần lớn mã của tôi thường ở đó). Các đối số chức năng đượ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 tập lệnh bash 3000 dòng, tôi có xu hướng viết các tập lệnh nhỏ nhất theo kiểu này ...
Eugene

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

"yếu tố con người" là những yếu tố tồi tệ nhất. Máy móc không chiến đấu với bạn khi bạn cung cấp cho họ một cái gì đó tốt hơn.
jeremyjjbrown

1
Tại sao lại getoptvs getopts? getoptsdễ mang theo hơn và hoạt động trong mọi vỏ POSIX. Đặc biệt vì câu hỏi là các thực tiễn tốt nhất về vỏ thay vì các thực tiễn tốt nhất cụ thể, tôi sẽ hỗ trợ tuân thủ POSIX để hỗ trợ nhiều hệ vỏ khi có thể.
Wimateeka

1
cảm ơn vì đã cung cấp tất cả lời khuyên cho shell scripting mặc dù bạn trung thực: "Hy vọng nó có ích, mặc dù xin lưu ý. Nếu bạn phải sử dụng loại điều 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 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. "
dieHellste

25

Hãy xem Hướng dẫn về Bash-Scripting nâng cao để có nhiều sự khôn ngoan về kịch bản shell - cũng không chỉ Bash.

Đừng nghe những người bảo bạn nhìn vào những ngôn ngữ khác, phức tạp hơn. Nếu shell script đáp ứng nhu cầu của bạn, hãy sử dụng nó. Bạn muốn chức năng, 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 sơ yếu 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 phải hoàn thành và bạn đã biết trình bao.

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


9
Lưu ý rằng đối với các tập lệnh thậm chí hơi phức tạp, đó KHÔNG phải là cách thực hành tốt nhất. Viết mã không chỉ là làm cho một cái gì đó hoạt động. Đó là về việc xây dựng nó một cách nhanh chóng, dễ dàng, và nó đáng tin cậy, có thể tái sử dụng, và dễ đọc và bảo trì (đặc biệt đối với những người khác). Shell script không quy mô tốt đến bất kỳ cấp độ. Các ngôn ngữ mạnh mẽ hơn đơn giản hơn nhiều cho các dự án với bất kỳ logic nào.
vật bị trôi giạt

20

shell script là ngôn ngữ được thiết kế để thao tác với các tệp và quy trình. Mặc dù nó rất tuyệt, nhưng nó không phải là ngôn ngữ có mục đích chung, vì vậy, luôn cố gắng dán logic từ các tiện ích hiện có thay vì tái tạo logic mới trong tập lệnh shell.

Ngoài nguyên tắc chung đó, tôi đã thu thập một số lỗi script shell phổ biến .



11

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

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 lớn, 4000 dòng bash và đang phát triển ... và bạn không thể thoát khỏi 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 ở 40 dòng bash đẹp.


9

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


7
bạn tự mâu thuẫn bằng cách nói "không cần phải phức tạp hóa" và sau đó liệt kê những phức tạp khác nhau mà bạn nghĩ là giá trị gia tăng, trong khi trong hầu hết các trường hợp bị lạm dụng thành những con quái vật xấu xí thay vì sử dụng để đơn giản hóa các vấn đề và thực hiện.
Evgeny

3
điều này hàm ý một nhược điểm lớn, tập lệnh của bạn sẽ không thể di động trên các hệ thống không có trăn
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 kỳ ai không nên quay lưng lại với các ngôn ngữ như Python hoặc Ruby vì nhiều khả năng nó có sẵn và nếu không, đó là một lệnh (hoặc vài lần nhấp) không được cài đặt . Nếu bạn cần thêm tính di động, hãy nghĩ về 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ó sẵn JVM.
Wil Moore III

@astropanic gần như tất cả các cổng Linux với Python hiện nay
Pithikos

@Pithikos, chắc chắn, và thích thú với sự rắc rối của python2 vs python3. Ngày nay tôi viết tất cả các công cụ của mình bằng cách sử dụng và không thể hạnh phúc hơn.
astropanic

9

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


7

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

Hầu hết trong số chúng không có "bash-isms" và có sự phân tách tốt giữa cài đặt cấu hình, tệp thư viện và định dạng nguồn.

Phong cách cá nhân của tôi là viết một bản tổng thể xác định một số biến mặc định và sau đó thử tải ("nguồn") một 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 kịch bản phức tạp hơn. (Perl được tạo ra cho mục đích đó.)

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


-1

Hoặc trích dẫn 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 mất người nói rằng.

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

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.