Uma função bash que aceita argumentos como outras linguagens?

14

Eu tenho uma função bash para definir o $PATH como este -

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
}

Mas o problema é que eu tenho que escrever uma função diferente para variáveis diferentes (por exemplo, outra função para $CLASSPATH like assign-classpath() etc.). Eu não consegui encontrar uma maneira de passar argumentos para a função bash para que eu possa acessá-lo por referência.

Seria melhor se eu tivesse algo como -

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

Alguma ideia, como conseguir algo como acima no bash?

    
por ramgorur 01.04.2014 / 23:26

9 respostas

15

Em bash , você pode usar ${!varname} para expandir a variável referenciada pelo conteúdo de outra. Por exemplo:

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

Da página do manual:

${!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.

Além disso, para definir uma variável referenciada pelo conteúdo (sem os perigos de eval ), você pode usar declare . Por exemplo:

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

Assim, você poderia escrever sua função assim (tome cuidado, porque se você usa declare em uma função, você deve dar -g ou a variável será local):

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
}

E use-o como:

assign PATH /path/to/binaries

Note que eu também corrigi um erro onde se substr já é uma substring de um dos membros separados por dois pontos de bigstr , mas não seu próprio membro, então não seria adicionado. Por exemplo, isso permitiria a adição de /bin a uma variável PATH contendo /usr/bin . Ele usa os conjuntos extglob para corresponder ao início / fim da sequência ou dois-pontos e depois a qualquer outra coisa. Sem extglob , a alternativa seria:

[[ $bigstr != $substr && $bigstr != *:$substr &&
   $bigstr != $substr:* && $bigstr != *:$substr:* ]]
    
por 01.04.2014 / 23:53
8

Novo no bash 4.3, é a opção -n para declare & local :

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

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

Isso imprime hello, world .

    
por 02.04.2014 / 00:00
2

Você pode usar eval para definir um parâmetro. Uma descrição desse comando pode ser encontrada aqui . O uso a seguir de eval está errado:

wrong(){
  eval $1=$2
}

Em relação à avaliação adicional eval você deve usar

assign(){
  eval $1='$2'
}

Verifique os resultados da utilização destas funções:

$ X1='$X2'
$ X2='$X3'
$ X3='xxx'
$ 
$ echo :$X1:
:$X2:
$ echo :$X2:
:$X3:
$ echo :$X3:
:xxx:
$ 
$ wrong Y $X1
$ echo :$Y:
:$X3:
$ 
$ assign Y $X1
$ echo :$Y:
:$X2:
$ 
$ assign Y "hallo world"
$echo :$Y:
:hallo world:
$ # the following may be unexpected
$ assign Z $Y
$ echo ":$Z:"
:hallo:
$ # so you have to quote the second argument if its a variable
$ assign Z "$Y"
$ echo ":$Z:"
:hallo world:

Mas você pode alcançar sua meta sem o uso de eval . Eu prefiro assim, é mais simples.

A seguinte função faz a substituição no caminho certo (espero)

augment(){
  local CURRENT=$1
  local AUGMENT=$2
  local NEW
  if [[ -z $CURRENT ]]; then
    NEW=$AUGMENT
  elif [[ ! ( ( $CURRENT = $AUGMENT ) || ( $CURRENT = $AUGMENT:* ) || \
    ( $CURRENT = *:$AUGMENT ) || ( $CURRENT = *:$AUGMENT:* ) ) ]]; then
    NEW=$CURRENT:$AUGMENT
  else
    NEW=$CURRENT
    fi
  echo "$NEW"
}

Verifique a seguinte saída

augment /usr/bin /bin
/usr/bin:/bin

augment /usr/bin:/bin /bin
/usr/bin:/bin

augment /usr/bin:/bin:/usr/local/bin /bin
/usr/bin:/bin:/usr/local/bin

augment /bin:/usr/bin /bin
/bin:/usr/bin

augment /bin /bin
/bin


augment /usr/bin: /bin
/usr/bin::/bin

augment /usr/bin:/bin: /bin
/usr/bin:/bin:

augment /usr/bin:/bin:/usr/local/bin: /bin
/usr/bin:/bin:/usr/local/bin:

augment /bin:/usr/bin: /bin
/bin:/usr/bin:

augment /bin: /bin
/bin:


augment : /bin
::/bin


augment "/usr lib" "/usr bin"
/usr lib:/usr bin

augment "/usr lib:/usr bin" "/usr bin"
/usr lib:/usr bin

Agora você pode usar a função augment da seguinte maneira para definir uma variável:

PATH='augment PATH /bin'
CLASSPATH='augment CLASSPATH /bin'
LD_LIBRARY_PATH='augment LD_LIBRARY_PATH /usr/lib'
    
por 02.04.2014 / 09:12
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=

Isso se encaixa?

    
por 01.04.2014 / 23:50
1

Argumentos nomeados simplesmente não são como a sintaxe do Bash foi projetada. Bash foi projetado para ser um aprimoramento iterativo no shell Bourne. Como tal, ele precisa garantir que certas coisas funcionem entre as duas conchas, tanto quanto possível. Portanto, não é significado que seja mais fácil criar scripts no geral, é apenas para ser melhor do que o Bourne, garantindo que, se você levar um script de um ambiente Bourne para bash , será tão fácil quanto possível. Isso não é trivial, já que muitos shells ainda tratam Bourne como um padrão de fato. Como as pessoas escrevem seus scripts para serem compatíveis com Bourne (para essa portabilidade), a necessidade permanece em vigor e é improvável que ela mude.

Provavelmente, é melhor você olhar um script de shell diferente (como python ou algo assim) inteiramente, se for possível. Se você está enfrentando as limitações de um idioma, precisa começar a usar um novo idioma.

    
por 02.04.2014 / 00:00
1

ON ARGS NOME:

Isto é feito de forma muito simples e bash não é necessário - este é o comportamento POSIX básico especificado da atribuição via expansão de parâmetro:

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

Para demonstrar de forma semelhante ao @Graeme, mas de forma portátil:

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

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

E lá só faço str= para garantir que ele tenha um valor nulo, porque a expansão de parâmetro tem a proteção incorporada contra a reatribuição do ambiente shell inline se já estiver definido.

SOLUÇÃO:

Para o seu problema específico, não acredito que argumentos nomeados sejam necessários, embora certamente sejam possíveis. Use $IFS em vez disso:

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
}

Veja o que recebo quando o executo:

% 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

Observe que ele adicionou apenas os argumentos que não estavam em $PATH ou que vieram antes? Ou mesmo que demorou mais de um argumento? $IFS é útil.

    
por 02.04.2014 / 00:11
1

Com alguns truques, você pode realmente passar parâmetros nomeados para funções, junto com arrays (testados no bash 3 e 4).

O método que desenvolvi permite que você acesse parâmetros passados para uma função como esta:

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"

Em outras palavras, não apenas você pode chamar seus parâmetros pelos seus nomes (o que compõe um núcleo mais legível), você pode realmente passar matrizes (e referências a variáveis - este recurso funciona apenas no bash 4.3)! Além disso, as variáveis mapeadas estão todas no escopo local, exatamente como $ 1 (e outras).

O código que faz este trabalho é bem leve e funciona tanto no bash 3 quanto no bash 4 (estas são as únicas versões com as quais testei). Se você está interessado em mais truques como este que tornam o desenvolvimento com o bash muito mais agradável e mais fácil, você pode dar uma olhada no meu Bash Infinity Framework , o código abaixo foi desenvolvido para esse propósito.

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'
    
por 04.05.2015 / 17:40
0

Ou ... use backsticks

function foo(){
     # $1 is first param
     # $2 is second param ...
 }

Chamando:

 result='foo "param1" "param2" ...'
 # they will go the right way.
    
por 19.01.2017 / 09:51
0

Com a sintaxe sh padrão (funcionaria em bash e não apenas em bash ), você poderia fazer:

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

Como para soluções que usam bash ' declare , é seguro desde que $1 contenha um nome de variável válido.

    
por 19.01.2017 / 14:50