다른 언어와 마찬가지로 인수를 취하는 bash 함수?


17

다음 $PATH과 같이 설정하는 bash 함수가 있습니다.

assign-path()
{
    str=$1
    # if the $PATH is empty, assign it directly.
    if [ -z $PATH ]; then
        PATH=$str;
    # if the $PATH does not contain the substring, append it with ':'.
    elif [[ $PATH != *$str* ]]; then
        PATH=$PATH:$str;
    fi
}

그러나 문제는 내가 다른 변수에 대한 다른 기능을 쓸 필요가있다 (예를 들어, 또 다른 기능 $CLASSPATHassign-classpath()등). bash 함수에 인수를 전달하여 참조로 액세스 할 수있는 방법을 찾을 수 없습니다.

다음과 같은 것이 있다면 더 좋을 것입니다.

assign( bigstr, substr )
{
    if [ -z bigstr ]; then
        bigstr=substr;
    elif [[ bigstr != *str* ]]; then
        bigstr=bigstr:substr;
    fi
}

bash에서 위와 같은 것을 얻는 방법은 무엇입니까?


"다른 언어"는 무엇입니까?
choroba

글쎄, 나는 bash가 c / java와 같이 "참조로 전달"을 허용하는지 의미하려고 노력했다.
ramgorur

1
assign-path /abc추가하지 않습니다 /abc$ PATH에 이미 포함 된 경우 PATH에 /abc/def, /abcd, /def/abc등 특히 당신은 추가 할 수 없습니다 /binPATH에 이미 포함되어있는 경우 /usr/bin.
miracle173

@ miracle173-이것은 사실입니다. 당신이해야 할 일은 $PATH다음과 같은 논쟁에 대해 테스트를 나누고 무효화하는 것 add=/bin dir=/usr/bin ; [ -z "${dir%"$add"}" ] || dir="${dir}:${add}"입니다. 내 대답에서 나는 당신이 사용하는 것만 큼 많은 인수 로이 방법을 수행합니다 IFS=:.
mikeserv

콜론으로 구분 된 목록에 값을 추가하는 특정 (인스턴스) 문제와 관련이 있습니다  $PATH.  과 에 디렉토리를 추가 $PATH이를 이미 없으면 (에 슈퍼 사용자 ).
Scott

답변:


17

에서 bash당신이 사용할 수있는 ${!varname}또 다른의 내용에 의해 참조 변수를 확장 할 수 있습니다. 예 :

$ var=hello
$ foo () { echo "${!1}"; }
$ foo var
hello

매뉴얼 페이지에서 :

${!prefix*}
${!prefix@}
       Names matching prefix.  Expands to the names of variables whose names
       begin with prefix, separated by the first character of the IFS special
       variable.  When @ is used  and the expansion appears within double quotes,
       each variable name expands to a separate word.

또한 내용에서 참조하는 변수를 (위험없이 eval) 설정하려면을 사용할 수 있습니다 declare. 예 :

$ var=target
$ declare "$var=hello"
$ echo "$target"
hello

따라서 다음과 같이 함수를 작성할 수 있습니다 (함수 declare에서 사용 하는 -g경우 변수를 제공해야합니다 .

shopt -s extglob

assign()
{
  target=$1
  bigstr=${!1}
  substr=$2

  if [ -z "$bigstr" ]; then
    declare -g -- "$target=$substr"
  elif [[ $bigstr != @(|*:)$substr@(|:*) ]]; then
    declare -g -- "$target=$bigstr:$substr"
  fi
}

그리고 그것을 다음과 같이 사용하십시오 :

assign PATH /path/to/binaries

또한 substr콜론으로 구분 된 멤버 중 하나의 하위 문자열이지만 이미 bigstr자체 멤버가 아닌 하위 버그 인 경우 추가되지 않는 버그를 수정했습니다 . 예를 들어, 이미 포함 /binPATH변수에 추가 할 수 /usr/bin있습니다. extglob문자열의 시작 / 끝 또는 콜론과 일치 하는 집합을 사용합니다 . 이 없으면 extglob대안은 다음과 같습니다.

[[ $bigstr != $substr && $bigstr != *:$substr &&
   $bigstr != $substr:* && $bigstr != *:$substr:* ]]

-gdeclare배쉬의 이전 버전에서 사용할 수 없습니다, 나는이 이전 버전과 호환을 할 수 있도록 모든 방법은 무엇입니까?
ramgorur

2
@ramgorur, 당신은 export당신의 환경 (중요한 것을 덮어 쓸 위험이 있음) 또는 eval(주의하지 않으면 보안을 포함한 다양한 문제 )에 넣을 수 있습니다 . 사용하는 경우 eval당신이하는 것은 해야 괜찮을 당신처럼 그것을 할 경우 eval "$target=\$substr". 그래도 잊어 버리면 \ 의 내용에 공백이 있으면 명령을 실행할 가능성이 substr있습니다.
Graeme

9

bash 4.3의 새로운 기능은 & -n옵션입니다 .declarelocal

func() {
    local -n ref="$1"
    ref="hello, world"
}

var='goodbye world'
func var
echo "$var"

인쇄됩니다 hello, world.


Bash에서 namerefs의 유일한 문제는 nameref 자체와 동일한 이름의 변수 (함수 외부)를 참조하는 nameref (예 : 함수)를 가질 수 없다는 것입니다. 그러나 4.5 버전에서는 수정 될 수 있습니다.
Kusalananda

2

eval매개 변수를 설정하는 데 사용할 수 있습니다 . 이 명령에 대한 설명은 여기 에서 찾을 수 있습니다 . 다음 사용법 eval이 잘못되었습니다.

잘못된(){
  평가 $ 1 = $ 2
}

추가 평가와 관련하여 eval사용해야합니까?

양수인(){
  평가 $ 1 = '$ 2'
}

다음 기능을 사용한 결과를 확인하십시오.

$ X1 = '$ X2'
$ X2 = '$ X3'
$ X3 = 'xxx'
$ 
$ echo : $ X1 :
: $ X2 :
$ echo : $ X2 :
: $ X3 :
$ echo : $ X3 :
:트리플 엑스:
$ 
$ 잘못된 Y $ X1
$ echo : $ Y :
: $ X3 :
$ 
$ 할당 Y $ X1
$ echo : $ Y :
: $ X2 :
$ 
$ "할로 월드"할당
$ echo : $ Y :
: 할로 월드 :
$ # 다음은 예상치 못한 것일 수 있습니다.
$ 할당 Z $ Y
$ echo ": $ Z :"
:어이:
$ # 그래서 변수의 경우 두 번째 인수를 인용해야합니다
$ Z "$ Y"할당
$ echo ": $ Z :"
: 할로 월드 :

그러나를 사용하지 않고도 목표를 달성 할 수 있습니다 eval. 더 간단한 방법을 선호합니다.

다음 함수는 올바른 방식으로 대체를 만듭니다.

기능 보강 () {
  로컬 CURRENT = $ 1
  지역 AUGMENT = $ 2
  지역 NEW
  [[-z $ CURRENT]] 인 경우; 그때
    NEW = $ AUGMENT
  elif [[! (($ CURRENT = $ AUGMENT) || ($ CURRENT = $ AUGMENT : *) || \
    ($ CURRENT = * : $ AUGMENT) || ($ CURRENT = * : $ AUGMENT : *))]] ;; 그때
    NEW = $ 현재 : $ AUGMENT
  그밖에
    NEW = $ 현재
    fi
  에코 "$ NEW"
}

다음 출력을 확인하십시오

확대 / usr / bin / bin
/ usr / bin : / bin

기능 보강 / usr / bin : / bin / bin
/ usr / bin : / bin

기능 보강 / usr / bin : / bin : / usr / local / bin / bin
/ usr / bin : / bin : / usr / local / bin

기능 보강 / bin : / usr / bin / bin
/ bin : / usr / bin

기능 보강 / bin / bin
/큰 상자


기능 보강 / usr / bin : / bin
/ usr / bin :: / bin

기능 보강 / usr / bin : / bin : / bin
/ usr / bin : / bin :

기능 보강 / usr / bin : / bin : / usr / local / bin : / bin
/ usr / bin : / bin : / usr / local / bin :

기능 보강 / bin : / usr / bin : / bin
/ bin : / usr / bin :

기능 보강 / bin : / bin
/큰 상자:


기능 보강 : / bin
::/큰 상자


기능 보강 "/ usr lib" "/ usr bin"
/ usr lib : / usr bin

기능 보강 "/ usr lib : / usr bin" "/ usr bin"
/ usr lib : / usr bin

이제 augment다음 방법으로 함수를 사용하여 변수를 설정할 수 있습니다.

PATH =`확장 경로 / bin`
CLASSPATH =`CLASSPATH / bin` 기능 보강
LD_LIBRARY_PATH =`LD_LIBRARY_PATH / usr / lib를 추가하십시오`

당신의 평가 진술조차 잘못되었습니다. 예를 들면 다음과 같습니다. v='echo "OHNO!" ; var' ; l=val ; eval $v='$l' -var를 할당하기 전에 "OHNO!를 에코합니다. 문자열을 확장 할 수 없도록"$ {v ## * [; "$ IFS"]} = '$ l' "
mikeserv는

@ mikeserv 귀하의 의견에 감사하지만 이것이 유효한 예는 아니라고 생각합니다. assign 스크립트의 첫 번째 인수는 변수 이름 또는 =assignement 문의 왼쪽에 사용되는 변수 이름이 포함 된 변수 여야합니다. 내 스크립이 주장을 확인하지 않는다고 주장 할 수 있습니다. 사실입니다. 나는 인수가 있는지 또는 인수 수가 유효한지조차 확인하지 않습니다. 그러나 이것은 의도적 인 것이었다. 원하는 경우 OP는 그러한 검사를 추가 할 수 있습니다.
miracle173

@ mikeserv : 첫 번째 인수를 자동으로 유효한 변수 이름으로 변환하려는 제안은 좋은 생각이 아니라고 생각합니다. 1) 사용자가 의도하지 않은 일부 변수가 설정 / 덮어졌습니다. 2) 오류는 기능 사용자에게 숨겨져 있습니다. 결코 좋은 생각이 아닙니다. 그런 일이 발생하면 단순히 오류를 제기해야합니다.
miracle173

@ mikeserv : 변수를 v할당 함수의 두 번째 인수 로 사용하려고 할 때 흥미 롭습니다 . 따라서 그 가치는 양도의 오른쪽에 있어야합니다. 함수의 인수를 인용해야합니다 assign. 이 미묘함을 내 게시물에 추가했습니다.
miracle173

아마도 사실이며 최종 예제에서 실제로 eval을 사용하지 않는 것이 현명한 것이므로 실제로 중요하지 않습니다. 그러나 내가 말하는 것은 eval을 사용하고 사용자 입력을 취하는 모든 코드는 본질적으로 위험하다는 것입니다. 예를 사용하면 경로를 거의 노력하지 않고 경로를 변경하도록 설계된 함수를 만들 수 있습니다.
mikeserv

2

몇 가지 트릭을 사용하면 실제로 배열과 함께 명명 된 매개 변수를 함수에 전달할 수 있습니다 (bash 3 및 4에서 테스트 됨).

내가 개발 한 방법을 사용하면 다음과 같은 함수에 전달 된 매개 변수에 액세스 할 수 있습니다

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"

다시 말해, 이름으로 매개 변수를 호출 할 수있을뿐만 아니라 (더 읽기 쉬운 코어를 구성 함) 실제로 배열을 전달할 수 있습니다 (변수에 대한 참조-이 기능은 bash 4.3에서만 작동합니다). 또한 매핑 된 변수는 모두 $ 1 (및 기타)과 같이 로컬 범위에 있습니다.

이 작업을 수행하는 코드는 매우 가볍고 bash 3 및 bash 4에서 모두 작동합니다 (이것은 테스트 한 유일한 버전입니다). bash로 개발하는 것을 훨씬 더 좋고 쉽게 만드는 더 많은 트릭에 관심이 있다면 Bash Infinity Framework를 살펴보십시오. 아래 코드는 그 목적을 위해 개발되었습니다.

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\" \"\$@\"; [[ \$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'

1
assign () 
{ 
    if [ -z ${!1} ]; then
        eval $1=$2
    else
        if [[ ${!1} != *$2* ]]; then
            eval $1=${!1}:$2
        fi
    fi
}

$ echo =$x=
==
$ assign x y
$ echo =$x=
=y=
$ assign x y
$ echo =$x=
=y=
$ assign x z
$ echo =$x=
=y:z=

맞습니까?


안녕하세요, 나는 당신처럼 그것을 시도했지만 작동하지 않습니다. 초보자가되어 죄송합니다. 이 스크립트에 어떤 문제가 있는지 말씀해 주 시겠습니까?
ramgorur

1
귀하의 사용은 eval임의의 명령 실행에 영향을 받기 쉽다.
Chris Down

zhe eval라인을 더 안전하게 만드는 아이디어가 있습니까? 일반적으로 eval이 필요하다고 생각되면 * sh를 사용하지 않고 다른 언어로 전환하기로 결정합니다. 반면, scrips에서 이것을 사용하여 일부 PATH와 같은 변수에 항목을 추가하면 사용자 입력이 아닌 문자열 상수로 실행됩니다.

1
eval안전하게 할 수는 있지만 많은 생각이 필요합니다. 매개 변수를 참조하려는 경우 다음과 같은 작업을 수행하려고합니다 eval "$1=\"\$2\"". eval's첫 번째 패스에서는 $ 1 만 평가하고 두 번째는 value = "$ 2"입니다. 그러나 다른 것을해야합니다. 여기서는 필요하지 않습니다.
mikeserv

사실, 위의 의견도 잘못되었습니다. 해야 할 일 "${1##*[;"$IFS"]}=\"\$2\""이 있으며 보증조차 제공되지 않습니다. 또는 eval "$(set -- $1 ; shift $(($#-1)) ; echo $1)=\"\$2\"". 쉽지 않습니다.
mikeserv

1

명명 된 인수는 단순히 Bash의 구문이 설계된 방식이 아닙니다. Bash는 Bourne 쉘을 반복적으로 개선하도록 설계되었습니다. 따라서 가능한 한 두 쉘 사이에서 특정 작업을 수행해야합니다. 가되지 않도록 의미 전반적인와 스크립트 쉽게로, 당신이 넘어서하는 본 환경에서 스크립트를 가지고가는 경우에 보장하면서 Bourne 씨보다 더 될 운명이야 bash는 가능한 한 쉽게합니다. 많은 껍질이 여전히 Bourne을 사실상의 표준으로 취급하기 때문에 사소한 것은 아닙니다. 사람들이 스크립트를 Bourne과 호환되도록 작성하기 때문에 (이 이식성을 위해) 요구는 여전히 유효하며 변경되지 않을 것입니다.

python가능하다면 다른 쉘 스크립트 (예 : 비슷한 것)를 완전히 보는 것이 좋습니다. 언어의 한계에 부딪 치면 새로운 언어를 사용해야합니다.


아마도 bash인생 초기에 이것은 사실이었습니다. 그러나 이제 구체적인 규정이 마련되었습니다. 이제 전체 참조 변수를 사용할 수 있습니다 ( derobert 's answerbash 4.3 참조) .
Graeme

그리고 여기를 보면 POSIX 포터블 코드만으로도 이런 종류의 작업을 쉽게 수행 할 수 있습니다 : unix.stackexchange.com/a/120531/52934
mikeserv

1

표준 sh구문을 사용하면 (에서뿐만 bash아니라에서 작동 bash할 수 있음) 다음을 수행 할 수 있습니다.

assign() {
  eval '
    case :${'"$1"'}: in
      (::) '"$1"'=$2;;   # was empty, copy
      (*:"$2":*) ;;      # already there, do nothing
      (*) '"$1"'=$1:$2;; # otherwise, append with a :
    esac'
}

솔루션은 사용처럼 bash의를 declare, 그것의 안전 만큼으로 $1유효한 변수의 이름이 포함되어 있습니다.


0

명명 된 아치에서 :

이것은 매우 간단하게 수행되며 bash전혀 필요하지는 않습니다. 이것은 매개 변수 확장을 통한 기본 POSIX 지정 할당 동작입니다.

: ${PATH:=this is only assigned to \$PATH if \$PATH is null or unset}

@Graeme과 비슷한 맥락에서 휴대용 방식으로 데모하려면 :

_fn() { echo "$1 ${2:-"$1"} $str" ; }

% str= ; _fn "${str:=hello}"
> hello hello hello

그리고 str=매개 변수 확장에는 이미 설정된 쉘 환경을 인라인으로 재할 당하지 못하도록 내장 된 보호 기능이 있기 때문에 null 값을 갖도록하기 만합니다.

해결책:

귀하의 특정 문제에 대해서는 분명히 가능하지만 명명 된 인수가 필요하다고 생각하지 않습니다. $IFS대신 사용하십시오 :

assign() { oFS=$IFS ; IFS=: ; add=$* 
    set -- $PATH ; for p in $add ; do { 
        for d ; do [ -z "${d%"$p"}" ] && break 
        done ; } || set -- $* $p ; done
    PATH= ; echo "${PATH:="$*"}" ; IFS=$oFS
}

내가 그것을 실행할 때 얻는 것은 다음과 같습니다.

% PATH=/usr/bin:/usr/yes/bin
% assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/nope/bin \
    /usr/bin \
    /nope/usr/bin \
    /usr/nope/bin

> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

% echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

% dir="/some crazy/dir"
% p=`assign /usr/bin /usr/bin/new "$dir"`
% echo "$p" ; echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin:/some crazy/dir:/usr/bin/new
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin:/some crazy/dir:/usr/bin/new

아직 없었 $PATH거나 이전에 나온 주장 만 추가 했습니까? 아니면 심지어 하나 이상의 논쟁이 필요 했습니까? $IFS편리합니다.


안녕하세요, 나는 따라하지 못했습니다, 좀 더 자세히 설명해 주시겠습니까? 감사.
ramgorur

나는 이미 그렇게하고있다 ... 잠시만 더 기다려주세요 ...
mikeserv

@ramgorur 더 나은가요? 죄송하지만 실제 생활이 침범되어 글쓰기를 마치는 데 예상보다 시간이 조금 더 걸렸습니다.
mikeserv

실생활에도 굴복했습니다. 이 코드를 작성하는 많은 다른 접근법처럼 보입니다. 언제나 최선의 방법을 결정하도록하겠습니다.
ramgorur

@ramgorur-물론, 나는 당신이 교수형에 빠지지 않도록하고 싶었습니다. 이것에 대해-원하는 것을 선택하십시오. 나는 내가 볼 수있는 다른 대답들 중 어느 것도 여기에서와 같이 간결하고 휴대 가능하거나 강력한 솔루션으로 제공 assign하지 않을 것이라고 말할 것 입니다. 작동 방식에 대해 궁금한 점이 있으면 기꺼이 답변 해 드리겠습니다. 그리고 실제로 명명 된 인수를 원한다면 다른 함수의 인수에 대해 명명 된 함수를 선언하는 방법을 보여주는이 다른 답변을보고 싶을 것입니다 : unix.stackexchange.com/a/120531/52934
mikeserv

-2

루비, 파이썬 등과 같은 것을 찾을 수 없지만 이것은 나에게 더 가깝습니다.

foo() {
  BAR="$1"; BAZ="$2"; QUUX="$3"; CORGE="$4"
  ...
}

내 의견으로는 가독성이 더 좋으며 매개 변수 이름을 선언하는 데 4 줄이 과도합니다. 현대 언어에 더 가깝게 보입니다.


(1) 질문은 쉘 함수에 관한 것입니다. 당신의 대답은 골격 쉘 기능을 제시합니다. 그 외에도 귀하의 답변은 질문과 관련이 없습니다. (2) 세미콜론을 구분 기호로 사용하여 별도의 줄을 가져 와서 한 줄로 연결하는 것이 가독성을 높이는 것으로 생각 하십니까? 나는 당신이 그것을 거꾸로 가지고 있다고 믿습니다. 한 줄 스타일은  여러 줄 스타일 보다 읽기 어렵습니다.
Scott

불필요한 부분을 줄이는 것이 요점이라면 왜 따옴표와 세미콜론입니까? a=$1 b=$2 ...잘 작동합니다.
ilkkachu
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.