2012-08-26 31 views
35

Tôi phải viết một hàm trong bash. Hàm sẽ lấy khoảng 7 đối số. Tôi biết rằng tôi có thể gọi một chức năng như thế này:Có cách nào để tránh các đối số vị trí trong bash không?

Để gọi một hàm với các tham số:

function_name $arg1 $arg2 

Và tôi có thể tham khảo các thông số của tôi như thế này bên trong hàm:

function_name() { 
    echo "Parameter #1 is $1" 
} 

Câu hỏi của tôi là, có cách nào tốt hơn là tham khảo các tham số bên trong hàm? Tôi có thể tránh các $ 1, $ 2, $ 3, .... điều và chỉ đơn giản là sử dụng $ arg1, $ arg2, ...?

Có phương pháp thích hợp cho điều này hay tôi cần phải gán lại các tham số này cho một số biến khác bên trong hàm? Ví dụ:

function_name() { 
    $ARG1=$1 
    echo "Parameter #1 is $ARG1" 
} 

Ví dụ nào sẽ được đánh giá cao.

+1

Sự khác nhau giữa việc sử dụng '$ 1' so với' $ ARG1' là gì? –

+0

@JonLin: Tôi có thể sử dụng tên trực quan hơn, thay vì $ 1, $ 2. Các con số rất khó hiểu. – Bhushan

+2

Bạn có thực sự yêu cầu bảy tham số bắt buộc không? Một cách tiếp cận phổ biến sẽ là biến càng nhiều càng tốt thành các tùy chọn, và cung cấp một giá trị mặc định, nơi nó có ý nghĩa. Nó làm cho kịch bản của bạn dễ sử dụng hơn nhiều nếu bạn không phải ghi nhớ một thứ tự vị trí cụ thể cho một số lượng lớn các đối số, ngay cả khi hầu hết người dùng sẽ ghi đè các giá trị mặc định hầu hết thời gian. – tripleee

Trả lời

45

Cách phổ biến để làm điều đó được gán các đối số cho các biến cục bộ trong hàm, ví dụ:

copy() { 
    local from=${1} 
    local to=${2} 

    # ... 
} 

Một giải pháp khác có thể là phân tích cú pháp tùy chọn getopt kiểu.

copy() { 
    local arg from to 
    while getopts 'f:t:' arg 
    do 
     case ${arg} in 
      f) from=${OPTARG};; 
      t) to=${OPTARG};; 
      *) return 1 # illegal option 
     esac 
    done 
} 

copy -f /tmp/a -t /tmp/b 

Đáng buồn thay, bash không thể xử lý tùy chọn dài mà sẽ thể dễ đọc hơn, ví dụ:

copy --from /tmp/a --to /tmp/b 

Cho rằng, bạn có cần phải sử dụng các chương trình bên ngoài getopt (mà Tôi nghĩ rằng chỉ hỗ trợ tùy chọn dài trên các hệ thống GNU) hoặc thực hiện trình phân tích cú pháp tùy chọn dài bằng tay, tức là:

copy() { 
    local from to 

    while [[ ${1} ]]; do 
     case "${1}" in 
      --from) 
       from=${2} 
       shift 
       ;; 
      --to) 
       to=${2} 
       shift 
       ;; 
      *) 
       echo "Unknown parameter: ${1}" >&2 
       return 1 
     esac 

     if ! shift; then 
      echo 'Missing parameter argument.' >&2 
      return 1 
     fi 
    done 
} 

copy --from /tmp/a --to /tmp/b 
.210

Xem thêm: using getopts in bash shell script to get long and short command line options


Bạn cũng có thể được lười biếng, và chỉ cần vượt qua 'biến' như các đối số cho hàm, ví dụ::

copy() { 
    local "${@}" 

    # ... 
} 

copy from=/tmp/a to=/tmp/b 

và bạn sẽ có ${from}${to} trong hàm như biến cục bộ.

Chỉ cần lưu ý rằng vấn đề tương tự như dưới đây sẽ áp dụng - nếu một biến cụ thể không được chuyển, nó sẽ được kế thừa từ môi trường mẹ. Bạn có thể muốn thêm một 'dòng an toàn' như:

copy() { 
    local from to # reset first 
    local "${@}" 

    # ... 
} 

để đảm bảo rằng ${from}${to} sẽ unset khi không được thông qua.


Và nếu có điều gì rất xấu là quan tâm của bạn, bạn cũng có thể gán các đối số là các biến toàn cục khi gọi chức năng, ví dụ:

from=/tmp/a to=/tmp/b copy 

Sau đó, bạn có thể chỉ cần sử dụng ${from}${to} trong hàm copy(). Chỉ cần lưu ý rằng bạn nên luôn luôn chuyển tất cả các thông số. Nếu không, một biến ngẫu nhiên có thể bị rò rỉ vào hàm.

from= to=/tmp/b copy # safe 
to=/tmp/b copy   # unsafe: ${from} may be declared elsewhere 

Nếu bạn có bash 4.1 (tôi nghĩ), bạn cũng có thể thử sử dụng các mảng kết hợp. Nó sẽ cho phép bạn vượt qua các đối số được đặt tên nhưng nó sẽ trở nên xấu xí. Một cái gì đó như:

args=([from]=/tmp/a [to]=/tmp/b) 
copy args 

Và sau đó tại copy(), bạn cần phải grab the array.

+0

Phương pháp 'cục bộ': sao chép từ =/tmp/a đến =/tmp/b; trông rất giống với cách người ta gọi một mục tiêu 'sao chép' và truyền cho nó hai biến môi trường mới (từ và đến). Tôi thực sự thích những điểm giống nhau về cú pháp. –

+0

@claytontstanley: vâng, đó là ý định;). –

+0

Tôi sử dụng cách tiếp cận biến toàn cầu cho các đối số tùy chọn, nhưng tôi đặt tên biến nói sau hàm và xóa nó trong hàm sau khi nó được chọn. Ví dụ, nếu tôi có một dấu nhắc tùy chọn cho 'get_input()', thì tôi sẽ sử dụng 'gi_prompt =" blah blah blah "', và trong hàm, 'unset gi_prompt'. Bằng cách đặt tên tất cả mọi thứ liên quan đến chức năng, điều này ngăn chặn rò rỉ và đặt tên xung đột trong khi cho phép một số linh hoạt. –

0

Các đối số được gửi đến các hàm như một bộ các mục riêng lẻ, vì vậy chúng không có tên như vậy, chỉ các vị trí. điều này cho phép một số khả năng thú vị như dưới đây, nhưng nó có nghĩa là bạn đang mắc kẹt với $ 1. 2 đô la, v.v. về việc có ánh xạ chúng đến các tên tốt hơn hay không, câu hỏi đặt ra là chức năng này lớn đến mức nào, và nó sẽ đọc được bao nhiêu một cách rõ ràng hơn. nếu phức tạp của nó, thì ánh xạ các tên có ý nghĩa ($ BatchID, $ FirstName, $ SourceFilePath) là một ý tưởng hay. cho các công cụ đơn giản, có lẽ không cần thiết. Tôi chắc chắn sẽ không bận tâm nếu bạn đang sử dụng tên như $ arg1.

bây giờ, nếu bạn chỉ muốn echo lại các thông số, bạn có thể lặp qua chúng:

for $arg in "[email protected]" 
do 
    echo "$arg" 
done 

chỉ là một thực tế thú vị; trừ khi bạn đang xử lý một danh sách, bạn có lẽ quan tâm đến somthing hữu ích hơn

3

Chức năng Shell có quyền truy cập đầy đủ vào bất kỳ biến nào có sẵn trong phạm vi gọi, ngoại trừ cho các tên biến được sử dụng làm biến cục bộ bên trong chính hàm đó. Ngoài ra, bất kỳ biến không cục bộ nào được đặt trong một hàm có sẵn ở bên ngoài sau khi hàm được gọi. Hãy xem xét ví dụ sau:

A=aaa 
B=bbb 

echo "A=$A B=$B C=$C" 

example() { 
    echo "example(): A=$A B=$B C=$C" 

    A=AAA 
    local B=BBB 
    C=CCC 

    echo "example(): A=$A B=$B C=$C" 
} 

example 

echo "A=$A B=$B C=$C" 

đoạn này có kết quả như sau:

A=aaa B=bbb C= 
example(): A=aaa B=bbb C= 
example(): A=AAA B=BBB C=CCC 
A=AAA B=bbb C=CCC 

nhiên, nhược điểm của phương pháp này là chức năng không phải là khép kín nữa và thiết lập một biến bên ngoài một hàm có thể có tác dụng phụ ngoài ý muốn. Nó cũng sẽ làm cho mọi thứ khó hơn nếu bạn muốn truyền dữ liệu đến một hàm mà không gán nó cho một biến đầu tiên, vì hàm này không sử dụng các tham số vị trí nữa.

Cách phổ biến nhất để xử lý này là sử dụng biến địa phương cho các đối số và bất kỳ biến tạm thời trong vòng một hàm:

example() { 
    local A="$1" B="$2" C="$3" TMP="/tmp" 

    ... 
} 

Điều này tránh gây ô nhiễm không gian tên shell với các biến chức năng địa phương.

7

Bạn luôn có thể vượt qua mọi thứ thông qua môi trường:

#!/bin/sh 
foo() { 
    echo arg1 = $arg1 
    echo arg2 = $arg2 
} 

arg1=banana arg2=apple foo 
0

đây là một chủ đề lớn tuổi, nhưng vẫn còn tôi muốn chia sẻ các chức năng bên dưới (yêu cầu bash 4). Nó phân tích cú pháp các đối số có tên và đặt các biến trong môi trường kịch bản lệnh. Chỉ cần chắc chắn rằng bạn có giá trị mặc định sane cho tất cả các thông số bạn cần. Câu lệnh xuất khẩu ở cuối cũng có thể chỉ là một eval. Thật tuyệt vời khi kết hợp với sự thay đổi để mở rộng các tập lệnh hiện có đã có một vài tham số vị trí và bạn không muốn thay đổi cú pháp, nhưng vẫn thêm một số tính linh hoạt.

parseOptions() 
{ 
    args=("[email protected]") 
    for opt in "${args[@]}"; do 
    if [[ ! "${opt}" =~ .*=.* ]]; then 
     echo "badly formatted option \"${opt}\" should be: option=value, stopping..." 
     return 1 
    fi 
    local var="${opt%%=*}" 
    local value="${opt#*=}" 
    export ${var}="${value}" 
    done 
    return 0 
} 
1

Tôi nghĩ tôi có giải pháp cho bạn. Với một vài thủ thuật, bạn có thể thực sự chuyển các tham số có tên tới các hàm, cùng với các mảng.

Phương pháp tôi phát triển cho phép bạn truy cập vào các thông số truyền cho hàm như thế này:

testPassingParams() { 

    @var hello 
    l=4 @array anArrayWithFourElements 
    l=2 @array anotherArrayWithTwo 
    @var anotherSingle 
    @reference table # references only work in bash >=4.3 
    @params anArrayOfVariedSize 

    test "$hello" = "$1" && echo correct 
    # 
    test "${anArrayWithFourElements[0]}" = "$2" && echo correct 
    test "${anArrayWithFourElements[1]}" = "$3" && echo correct 
    test "${anArrayWithFourElements[2]}" = "$4" && echo correct 
    # etc... 
    # 
    test "${anotherArrayWithTwo[0]}" = "$6" && echo correct 
    test "${anotherArrayWithTwo[1]}" = "$7" && echo correct 
    # 
    test "$anotherSingle" = "$8" && echo correct 
    # 
    test "${table[test]}" = "works" 
    table[inside]="adding a new value" 
    # 
    # I'm using * just in this example: 
    test "${anArrayOfVariedSize[*]}" = "${*:10}" && echo correct 
} 

fourElements=(a1 a2 "a3 with spaces" a4) 
twoElements=(b1 b2) 
declare -A assocArray 
assocArray[test]="works" 

testPassingParams "first" "${fourElements[@]}" "${twoElements[@]}" "single with spaces" assocArray "and more... " "even more..." 

test "${assocArray[inside]}" = "adding a new value" 

Nói cách khác, không chỉ bạn có thể gọi các thông số của bạn bằng tên của họ (mà làm cho một lõi dễ đọc hơn), bạn thực sự có thể vượt qua mảng (và tham chiếu đến các biến - tính năng này chỉ hoạt động trong bash 4.3)! Ngoài ra, các biến được ánh xạ là tất cả trong phạm vi cục bộ, giống như $ 1 (và các biến khác).

Mã làm cho tác phẩm này khá sáng và hoạt động cả trong bash 3 và bash 4 (đây là những phiên bản duy nhất tôi đã thử nghiệm). Nếu bạn quan tâm đến các thủ thuật khác như thế này khiến việc phát triển với bash đẹp hơn và dễ dàng hơn nhiều, bạn có thể xem Bash Infinity Framework của tôi, mã bên dưới được phát triển cho mục đích đó.

Function.AssignParamLocally() { 
    local commandWithArgs=($1) 
    local command="${commandWithArgs[0]}" 

    shift 

    if [[ "$command" == "trap" || "$command" == "l="* || "$command" == "_type="* ]] 
    then 
     paramNo+=-1 
     return 0 
    fi 

    if [[ "$command" != "local" ]] 
    then 
     assignNormalCodeStarted=true 
    fi 

    local varDeclaration="${commandWithArgs[1]}" 
    if [[ $varDeclaration == '-n' ]] 
    then 
     varDeclaration="${commandWithArgs[2]}" 
    fi 
    local varName="${varDeclaration%%=*}" 

    # var value is only important if making an object later on from it 
    local varValue="${varDeclaration#*=}" 

    if [[ ! -z $assignVarType ]] 
    then 
     local previousParamNo=$(expr $paramNo - 1) 

     if [[ "$assignVarType" == "array" ]] 
     then 
      # passing array: 
      execute="$assignVarName=(\"\${@:$previousParamNo:$assignArrLength}\")" 
      eval "$execute" 
      paramNo+=$(expr $assignArrLength - 1) 

      unset assignArrLength 
     elif [[ "$assignVarType" == "params" ]] 
     then 
      execute="$assignVarName=(\"\${@:$previousParamNo}\")" 
      eval "$execute" 
     elif [[ "$assignVarType" == "reference" ]] 
     then 
      execute="$assignVarName=\"\$$previousParamNo\"" 
      eval "$execute" 
     elif [[ ! -z "${!previousParamNo}" ]] 
     then 
      execute="$assignVarName=\"\$$previousParamNo\"" 
      eval "$execute" 
     fi 
    fi 

    assignVarType="$__capture_type" 
    assignVarName="$varName" 
    assignArrLength="$__capture_arrLength" 
} 

Function.CaptureParams() { 
    __capture_type="$_type" 
    __capture_arrLength="$l" 
} 

alias @trapAssign='Function.CaptureParams; trap "declare -i \"paramNo+=1\"; Function.AssignParamLocally \"\$BASH_COMMAND\" \"\[email protected]\"; [[ \$assignNormalCodeStarted = true ]] && trap - DEBUG && unset assignVarType && unset assignVarName && unset assignNormalCodeStarted && unset paramNo" DEBUG; ' 
alias @param='@trapAssign local' 
alias @reference='_type=reference @trapAssign local -n' 
alias @var='_type=var @param' 
alias @params='_type=params @param' 
alias @array='_type=array @param' 
+0

Cảm ơn. Tôi sẽ thử nó. – Bhushan

+0

@Bhushan Tôi vừa cập nhật mã với một phần bổ sung khác - tham chiếu đi qua, được giới thiệu trong bash 4.3. Trong số những thứ khác nó làm cho nó có thể vượt qua mảng kết hợp thành các hàm. – niieani

1

Tôi đã đích thân hy vọng sẽ nhìn thấy một số loại cú pháp như

func(a b){ 
    echo $a 
    echo $b 
} 

Nhưng vì đó không phải là một chuyện, và tôi thấy khá một vài tài liệu tham khảo để biến toàn cục (không phải không có sự báo trước của Phạm vi và đặt tên xung đột), tôi sẽ chia sẻ cách tiếp cận của mình.

Sử dụng copy chức năng từ Michal's answer:

copy(){ 
    cp $from $to 
} 
from=/tmp/a 
to=/tmp/b 
copy 

này là xấu, vì fromto là những từ rộng như vậy mà bất kỳ số lượng các chức năng có thể sử dụng này. Bạn có thể nhanh chóng kết thúc với một cuộc xung đột đặt tên hoặc một "rò rỉ" trên tay của bạn.

letter(){ 
    echo "From: $from" 
    echo "To: $to" 
    echo 
    echo "$1" 
} 

to=Emily 
letter "Hello Emily, you're fired for missing two days of work." 

# Result: 
# From: /tmp/a 
# To: Emily 

# Hello Emily, you're fired for missing two days of work. 

Vì vậy, cách tiếp cận của tôi là "không gian tên". Tôi đặt tên biến sau hàm và xóa nó sau khi hàm được thực hiện với hàm đó. Tất nhiên, tôi chỉ sử dụng nó cho các giá trị tùy chọn có giá trị mặc định. Nếu không, tôi chỉ sử dụng arg vị trí.

copy(){ 
    if [[ $copy_from ]] && [[ $copy_to ]]; then 
     cp $copy_from $copy_to 
     unset copy_from copy_to 
    fi 
} 
copy_from=/tmp/a 
copy_to=/tmp/b 
copy # Copies /tmp/a to /tmp/b 
copy # Does nothing, as it ought to 
letter "Emily, you're 'not' re-hired for the 'not' bribe ;)" 
# From: (no /tmp/a here!) 
# To: 

# Emily, you're 'not' re-hired for the 'not' bribe ;) 

tôi sẽ làm một ông chủ khủng khiếp ...


Trên thực tế, tên hàm của tôi có nhiều công phu hơn là "bản sao" hay "chữ".

Ví dụ gần đây nhất cho bộ nhớ của tôi là get_input(), có gi_no_sortgi_prompt.

  • gi_no_sort là giá trị đúng/sai xác định xem đề xuất hoàn thành có được sắp xếp hay không. Mặc định là đúng
  • gi_prompt là chuỗi là ... tốt, đó là tự giải thích. Mặc định là "".

Những lập luận thực tế chức năng mất là nguồn gốc của những 'lời đề nghị kết thúc' nói trên để nhắc đầu vào, và như đã nói danh sách được lấy từ [email protected] trong chức năng, các "tên args" là không bắt buộc [1] và không có cách nào rõ ràng để phân biệt giữa chuỗi có nghĩa là hoàn thành và thông báo boolean/prompt hoặc bất kỳ khoảng trắng nào được phân cách bằng dấu gạch ngang, cho vấn đề đó [2]; giải pháp trên đã kết thúc giúp tôi tiết kiệm lô số sự cố.

ghi chú:

  1. Vì vậy, một mã hóa cứng shift$1, $2, vv là ra câu hỏi.

  2. Ví dụ: là "0 Enter a command: {1..9} $(ls)" giá trị là 0, "Enter a command:" và một bộ 1 2 3 4 5 6 7 8 9 <directory contents>? Hoặc là "0", "Enter", "a""command:" một phần của bộ đó? Bash sẽ giả sử cái sau cho dù bạn có thích hay không.

0

Tất cả những gì bạn phải làm là biến tên trên đường đến cuộc gọi hàm.

function test() { 
    echo $a 
} 

a='hello world' test 
#prove variable didnt leak 
echo $a . 

enter image description here

Đây không chỉ là một tính năng của chức năng, bạn có thể có chức năng đó trong kịch bản riêng của nó và gọi a='hello world' test.sh và nó sẽ chỉ làm việc cùng


Như một chút thú vị, bạn có thể kết hợp phương thức này với các đối số vị trí (giả sử bạn đang tạo một tập lệnh và một số người dùng có thể không biết tên biến).
Heck, tại sao không để cho nó có mặc định cho những đối số quá? Vâng chắc chắn, dễ dàng peasy!

function test2() { 
    [[ -n "$1" ]] && local a="$1"; [[ -z "$a" ]] && local a='hi' 
    [[ -n "$2" ]] && local b="$2"; [[ -z "$b" ]] && local b='bye' 
    echo $a $b 
} 

#see the defaults 
test2 

#use positional as usual 
test2 '' there 
#use named parameter 
a=well test2 
#mix it up 
b=one test2 nice 

#prove variables didnt leak 
echo $a $b . 

enter image description here

Lưu ý rằng nếu test là kịch bản của riêng mình, bạn không cần phải sử dụng các từ khóa local.

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