Bash trang trí chức năng


10

Trong python chúng ta có thể trang trí các hàm với mã được tự động áp dụng và thực thi đối với các hàm.

Có bất kỳ tính năng tương tự trong bash?

Trong tập lệnh tôi hiện đang làm việc, tôi có một số bản tóm tắt kiểm tra các đối số cần thiết và thoát nếu chúng không tồn tại - và hiển thị một số thông báo nếu cờ gỡ lỗi được chỉ định.

Thật không may, tôi phải gắn lại mã này vào mọi chức năng và nếu tôi muốn thay đổi nó, tôi sẽ phải sửa đổi mọi chức năng.

Có cách nào để loại bỏ mã này khỏi từng chức năng và áp dụng nó cho tất cả các chức năng, tương tự như trang trí trong python không?

Câu trả lời:


12

Điều đó sẽ dễ dàng hơn rất nhiều với zshviệc có các hàm ẩn danh và một mảng kết hợp đặc biệt với các mã chức năng. bashTuy nhiên, với bạn có thể làm một cái gì đó như:

decorate() {
  eval "
    _inner_$(typeset -f "$1")
    $1"'() {
      echo >&2 "Calling function '"$1"' with $# arguments"
      _inner_'"$1"' "$@"
      local ret=$?
      echo >&2 "Function '"$1"' returned with exit status $ret"
      return "$ret"
    }'
}

f() {
  echo test
  return 12
}
decorate f
f a b

Mà sẽ xuất:

Calling function f with 2 arguments
test
Function f returned with exit status 12

Bạn không thể gọi trang trí hai lần để trang trí chức năng của bạn hai lần mặc dù.

Với zsh:

decorate()
  functions[$1]='
    echo >&2 "Calling function '$1' with $# arguments"
    () { '$functions[$1]'; } "$@"
    local ret=$?
    echo >&2 "function '$1' returned with status $ret"
    return $ret'

Stephane - có typesetcần thiết không? Nó sẽ không tuyên bố nó khác?
mikeerv

@mikeerv, eval "_inner_$(typeset -f x)"tạo _inner_xnhư một bản sao chính xác của bản gốc x(giống như functions[_inner_x]=$functions[x]trong zsh).
Stéphane Chazelas

Tôi hiểu điều đó - nhưng tại sao bạn cần hai cái?
mikeerv

Bạn cần một bối cảnh khác nhau nếu không bạn sẽ không thể nắm bắt những nội 's return.
Stéphane Chazelas

1
Tôi không theo bạn ở đó. Câu trả lời của tôi là một nỗ lực như một bản đồ gần gũi về những gì tôi hiểu về những người trang trí trăn
Stéphane Chazelas

5

Tôi đã thảo luận về các vấn đề và cách thức các phương pháp dưới đây hoạt động trong một số trường hợp trước đây vì vậy tôi sẽ không làm lại. Cá nhân, yêu thích của riêng tôi về chủ đề là ở đâyở đây .

Nếu bạn không thích đọc nó nhưng vẫn tò mò chỉ cần hiểu rằng các tài liệu ở đây được gắn vào đầu vào của hàm được đánh giá để mở rộng shell trước khi hàm chạy và chúng được tạo lại ở trạng thái khi chúng được xác định hàm mỗi khi hàm được gọi.

KHAI BÁO

Bạn chỉ cần một chức năng khai báo các chức năng khác.

_fn_init() { . /dev/fd/4 ; } 4<<INIT
    ${1}() { $(shift ; printf %s\\n "$@")
     } 4<<-REQ 5<<-\\RESET
            : \${_if_unset?shell will ERR and print this to stderr}
            : \${common_param="REQ/RESET added to all funcs"}
        REQ
            _fn_init $(printf "'%s' " "$@")
        RESET
INIT

CHẠY NÓ

Ở đây tôi gọi _fn_initđể khai báo cho tôi một chức năng được gọi fn.

set -vx
_fn_init fn \
    'echo "this would be command 1"' \
    'echo "$common_param"'

#OUTPUT#
+ _fn_init fn 'echo "this would be command 1"' 'echo "$common_param"'
shift ; printf %s\\n "$@"
++ shift
++ printf '%s\n' 'echo "this would be command 1"' 'echo "$common_param"'
printf "'%s' " "$@"
++ printf ''\''%s'\'' ' fn 'echo "this would be command 1"' 'echo "$common_param"'
#ALL OF THE ABOVE OCCURS BEFORE _fn_init RUNS#
#FIRST AND ONLY COMMAND ACTUALLY IN FUNCTION BODY BELOW#
+ . /dev/fd/4

    #fn AFTER _fn_init .dot SOURCES IT#
    fn() { echo "this would be command 1"
        echo "$common_param"
    } 4<<-REQ 5<<-\RESET
            : ${_if_unset?shell will ERR and print this to stderr}
            : ${common_param="REQ/RESET added to all funcs"}
        REQ
            _fn_init 'fn' \
               'echo "this would be command 1"' \
               'echo "$common_param"'
        RESET

CẦN THIẾT

Nếu tôi muốn gọi hàm này, nó sẽ chết trừ khi biến môi trường _if_unsetđược đặt.

fn

#OUTPUT#
+ fn
/dev/fd/4: line 1: _if_unset: shell will ERR and print this to stderr

Xin lưu ý thứ tự của dấu vết vỏ - không chỉ fnthất bại khi được gọi khi _if_unsetkhông được đặt, mà nó không bao giờ chạy ở vị trí đầu tiên . Đây là yếu tố quan trọng nhất để hiểu khi làm việc với các tài liệu mở rộng ở đây - chúng phải luôn luôn xảy ra trước tiên vì chúng là <<inputsau tất cả.

Lỗi xuất phát từ /dev/fd/4vì trình bao cha mẹ đang đánh giá đầu vào đó trước khi chuyển nó cho hàm. Đó là cách đơn giản nhất, hiệu quả nhất để kiểm tra môi trường cần thiết.

Dù sao, sự thất bại dễ dàng được khắc phục.

_if_unset=set fn

#OUTPUT#
+ _if_unset=set
+ fn
+ echo 'this would be command 1'
this would be command 1
+ echo 'REQ/RESET added to all funcs'
REQ/RESET added to all funcs

LINH HOẠT

Biến common_paramđược ước tính thành một giá trị mặc định trên đầu vào cho mọi hàm được khai báo bởi _fn_init. Nhưng giá trị đó cũng có thể thay đổi đối với bất kỳ giá trị nào khác cũng sẽ được tôn vinh bởi mọi chức năng được khai báo tương tự. Bây giờ tôi sẽ để lại dấu vết vỏ - chúng tôi sẽ không đi vào bất kỳ lãnh thổ nào chưa được khám phá ở đây hoặc bất cứ điều gì.

set +vx
_fn_init 'fn' \
               'echo "Hi! I am the first function."' \
               'echo "$common_param"'
_fn_init 'fn2' \
               'echo "This is another function."' \
               'echo "$common_param"'
_if_unset=set ;

Ở trên tôi khai báo hai hàm và đặt _if_unset. Bây giờ, trước khi gọi một trong hai chức năng, tôi sẽ không đặt common_paramđể bạn có thể thấy họ sẽ tự đặt nó khi tôi gọi cho họ.

unset common_param ; echo
fn ; echo
fn2 ; echo

#OUTPUT#
Hi! I am the first function.
REQ/RESET added to all funcs

This is another function.
REQ/RESET added to all funcs

Và bây giờ từ phạm vi của người gọi:

echo $common_param

#OUTPUT#
REQ/RESET added to all funcs

Nhưng bây giờ tôi muốn nó là một cái gì đó hoàn toàn khác:

common_param="Our common parameter is now something else entirely."
fn ; echo 
fn2 ; echo

#OUTPUT#
Hi! I am the first function.
Our common parameter is now something else entirely.

This is another function.
Our common parameter is now something else entirely.

Và nếu tôi không đặt _if_unset?

unset _if_unset ; echo
echo "fn:"
fn ; echo
echo "fn2:"
fn2 ; echo

#OUTPUT#
fn:
dash: 1: _if_unset: shell will ERR and print this to stderr

fn2:
dash: 1: _if_unset: shell will ERR and print this to stderr

CÀI LẠI

Nếu bạn cần thiết lập lại trạng thái của chức năng bất cứ lúc nào nó có thể dễ dàng thực hiện. Bạn chỉ cần làm (từ trong hàm):

. /dev/fd/5

Tôi đã lưu các đối số được sử dụng để ban đầu khai báo hàm trong bộ 5<<\RESETmô tả tệp đầu vào. Vì vậy, .dottìm nguồn cung ứng trong shell bất cứ lúc nào sẽ lặp lại quá trình thiết lập nó ở nơi đầu tiên. Tất cả đều khá dễ dàng, thực sự và khá dễ mang theo nếu bạn sẵn sàng bỏ qua thực tế là POSIX không thực sự chỉ định đường dẫn nút thiết bị mô tả tệp (cần thiết cho trình bao .dot).

Bạn có thể dễ dàng mở rộng hành vi này và định cấu hình các trạng thái khác nhau cho chức năng của mình.

HƠN?

Bằng cách này, hầu như không làm trầy xước bề mặt. Tôi thường sử dụng các kỹ thuật này để nhúng các hàm trợ giúp nhỏ có thể khai báo bất cứ lúc nào vào đầu vào của hàm chính - ví dụ, cho các $@mảng vị trí bổ sung khi cần. Trong thực tế - như tôi tin, nó phải là một cái gì đó rất gần với điều này mà các vỏ đạn bậc cao hơn vẫn làm. Bạn có thể thấy chúng rất dễ được đặt tên theo chương trình.

Tôi cũng muốn khai báo một hàm tạo chấp nhận một loại tham số giới hạn và sau đó định nghĩa một hàm đầu đốt sử dụng một lần hoặc theo cách khác phạm vi dọc theo các dòng của lambda - hoặc một hàm nội tuyến - chỉ đơn giản unset -flà chính nó khi xuyên qua. Bạn có thể vượt qua một hàm shell xung quanh.


Lợi thế của sự phức tạp thêm đó với việc mô tả tập tin so với việc sử dụng là evalgì?
Stéphane Chazelas

@StephaneChazelas Không có sự phức tạp thêm vào từ quan điểm của tôi. Trong thực tế, tôi thấy nó theo cách khác. Ngoài ra, việc trích dẫn dễ dàng hơn nhiều và .dothoạt động với các tệp và luồng để bạn không gặp phải các vấn đề trong danh sách đối số tương tự mà bạn có thể gặp phải. Tuy nhiên, đây có lẽ là vấn đề ưu tiên. Tôi chắc chắn nghĩ rằng nó sạch hơn - đặc biệt là khi bạn bắt đầu trốn tránh - đó là một cơn ác mộng từ nơi tôi ngồi.
mikeerv

@StephaneChazelas Mặc dù có một lợi thế - và đó là một lợi thế khá tốt. Eval ban đầu và eval thứ hai không cần phải quay lại với phương thức này. Di sản được đánh giá trên đầu vào, nhưng bạn không phải tìm .dotnguồn cho đến khi bạn tốt và sẵn sàng - hoặc bao giờ. Điều này cho phép bạn tự do hơn một chút trong việc kiểm tra các đánh giá của nó. Và nó cung cấp sự linh hoạt của trạng thái trên đầu vào - có thể được xử lý theo các cách khác - nhưng nó ít nguy hiểm hơn từ quan điểm đó so với eval.
mikeerv

2

Tôi nghĩ một cách để in thông tin về chức năng, khi bạn

kiểm tra các đối số cần thiết và thoát nếu chúng không tồn tại - và hiển thị một số thông báo

là để thay đổi bash dựng sẵn returnvà / hoặc exitvào đầu mỗi tập lệnh (hoặc trong một số tệp, mà bạn nguồn mỗi lần trước khi thực hiện chương trình). Vì vậy, bạn gõ

   #!/bin/bash
   return () {
       if [ -z $1 ] ; then
           builtin return
       else
           if [ $1 -gt 0 ] ; then
                echo function ${FUNCNAME[1]} returns status $1 
                builtin return $1
           else
                builtin return 0
           fi
       fi
   }
   foo () {
       [ 1 != 2 ] && return 1
   }
   foo

Nếu bạn chạy nó, bạn sẽ nhận được:

   function foo returns status 1

Điều đó có thể dễ dàng cập nhật với cờ gỡ lỗi nếu bạn cần, hơi giống như thế này:

   #!/bin/bash
   VERBOSE=1
   return () {
       if [ -z $1 ] ; then
           builtin return
       else
           if [ $1 -gt 0 ] ; then
               [ ! -z $VERBOSE ] && [ $VERBOSE -gt 0 ] && echo function ${FUNCNAME[1]} returns status $1  
               builtin return $1
           else
               builtin return 0
           fi
       fi
    }    

Câu lệnh theo cách này sẽ chỉ được thực thi khi đặt ĐỘNG TỪ biến (ít nhất đó là cách tôi sử dụng dài dòng trong tập lệnh của mình). Nó chắc chắn không giải quyết được vấn đề về chức năng trang trí, nhưng nó có thể hiển thị các thông báo trong trường hợp hàm trả về trạng thái khác không.

Tương tự, bạn có thể xác định lại exit, bằng cách thay thế tất cả các trường hợp return, nếu bạn muốn thoát khỏi tập lệnh.

EDIT: Tôi muốn thêm vào đây cách tôi sử dụng để trang trí các chức năng trong bash, nếu tôi có rất nhiều trong số chúng và cũng được lồng vào nhau. Khi tôi viết kịch bản này:

#!/bin/bash 
outer () { _
    inner1 () { _
        print "inner 1 command"
    }   
    inner2 () { _
        double_inner2 () { _
            print "double_inner1 command"
        } 
        double_inner2
        print "inner 2 command"
    } 
    inner1
    inner2
    inner1
    print "just command in outer"
}
foo_with_args () { _ $@
    print "command in foo with args"
}
echo command in body of script
outer
foo_with_args

Và đối với đầu ra tôi có thể nhận được điều này:

command in body of script
    outer: 
        inner1: 
            inner 1 command
        inner2: 
            double_inner2: 
                double_inner1 command
            inner 2 command
        inner1: 
            inner 1 command
        just command in outer
    foo_with_args: 1 2 3
        command in foo with args

Nó có thể hữu ích cho ai đó có chức năng và muốn gỡ lỗi chúng, để xem lỗi nào xảy ra. Nó dựa trên ba chức năng, có thể được mô tả dưới đây:

#!/bin/bash 
set_indentation_for_print_function () {
    default_number_of_indentation_spaces="4"
    #                            number_of_spaces_of_current_function is set to (max number of inner function - 3) * default_number_of_indentation_spaces 
    #                            -3 is because we dont consider main function in FUNCNAME array - which is if your run bash decoration from any script,
    #                            decoration_function "_" itself and set_indentation_for_print_function.
    number_of_spaces_of_current_function=`echo ${#FUNCNAME[@]} | awk \
        -v default_number_of_indentation_spaces="$default_number_of_indentation_spaces" '
        { print ($1-3)*default_number_of_indentation_spaces}
        '`
    #                            actual indent is sum of default_number_of_indentation_spaces + number_of_spaces_of_current_function
    let INDENT=$number_of_spaces_of_current_function+$default_number_of_indentation_spaces
}
print () { # print anything inside function with proper indent
    set_indentation_for_print_function
    awk -v l="${INDENT:=0}" 'BEGIN {for(i=1;i<=l;i++) printf(" ")}' # print INDENT spaces before echo
    echo $@
}
_ () { # decorator itself, prints funcname: args
    set_indentation_for_print_function
    let INDENT=$INDENT-$default_number_of_indentation_spaces # we remove def_number here, because function has to be right from usual print
    awk -v l="${INDENT:=0}" 'BEGIN {for(i=1;i<=l;i++) printf(" ")}' # print INDENT spaces before echo
    #tput setaf 0 && tput bold # uncomment this for grey color of decorator
    [ $INDENT -ne 0 ] && echo "${FUNCNAME[1]}: $@" # here we avoid situation where decorator is used inside the body of script and not in the function
    #tput sgr0 # resets grey color
}

Tôi đã cố gắng đưa càng nhiều càng tốt trong các bình luận, nhưng đây cũng là mô tả: Tôi sử dụng _ ()chức năng như trang trí, cái tôi đặt sau khi khai báo mọi chức năng : foo () { _. Hàm này in tên hàm với thụt lề thích hợp, tùy thuộc vào mức độ sâu của hàm trong hàm khác (như một thụt đầu dòng mặc định tôi sử dụng 4 số khoảng trắng). Tôi thường in cái này bằng màu xám, để tách cái này ra khỏi bản in thông thường. Nếu chức năng là cần thiết để được trang trí bằng các đối số, hoặc không có, người ta có thể sửa đổi dòng trước cuối trong chức năng trang trí.

Để in một cái gì đó bên trong chức năng, tôi đã giới thiệu print ()chức năng in mọi thứ được truyền cho nó với thụt lề thích hợp.

Hàm set_indentation_for_print_functionthực hiện chính xác những gì nó đại diện cho, tính toán thụt lề từ ${FUNCNAME[@]}mảng.

Cách này có một số sai sót, ví dụ người ta không thể vượt qua các tùy chọn để printthích echo, ví dụ -nhoặc -e, và nếu hàm trả về 1, nó không được trang trí. Và cũng đối với các đối số, được chuyển đến printnhiều hơn độ rộng của thiết bị đầu cuối, sẽ được bao bọc trên màn hình, người ta sẽ không thấy vết lõm cho đường bao bọc.

Cách tuyệt vời để sử dụng các trình trang trí này là đặt chúng vào tệp riêng biệt và trong mỗi tập lệnh mới để lấy nguồn tệp này source ~/script/hand_made_bash_functions.sh.

Tôi nghĩ rằng cách tốt nhất để kết hợp chức năng trang trí trong bash, là viết trang trí trong cơ thể của từng chức năng. Tôi nghĩ rằng việc viết hàm bên trong hàm trong bash sẽ dễ dàng hơn nhiều, bởi vì nó có tùy chọn để đặt tất cả các biến toàn cục, không giống như trong các Ngôn ngữ hướng đối tượng tiêu chuẩn. Điều đó làm cho nó giống như bạn đang đặt nhãn xung quanh mã của bạn trong bash. Ít nhất điều đó đã giúp tôi cho một kịch bản gỡ lỗi.



0

Đối với tôi điều này cảm thấy giống như cách đơn giản nhất để thực hiện một mô hình trang trí bên trong bash.

#!/bin/bash

function decorator {
    if [ "${FUNCNAME[0]}" != "${FUNCNAME[2]}" ] ; then
        echo "Turn stuff on"
        #shellcheck disable=2068
        ${@}
        echo "Turn stuff off"
        return 0
    fi
    return 1
}

function highly_decorated {
    echo 'Inside highly decorated, calling decorator function'
    decorator "${FUNCNAME[0]}" "${@}" && return
    echo 'Done calling decorator, do other stuff'
    echo 'other stuff'
}

echo 'Running highly decorated'
# shellcheck disable=SC2119
highly_decorated
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.