Uma função bash que aceita argumentos como outras línguas?

17

Eu tenho uma função bash para definir o $PATHseguinte -

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 tenho que escrever funções diferentes para variáveis ​​diferentes (por exemplo, outra função para $CLASSPATHcurtir assign-classpath()etc.). Não consegui encontrar uma maneira de passar o argumento 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 idéia, como conseguir algo como o acima no bash?

ramgorur
fonte
Quais "outras línguas"?
#
bem, eu tentei dizer, se o bash permite "passar por referência" como em c / java etc
ramgorur
11
assign-path /abcnão irá acrescentar /abcao PATH se $ PATH já contém /abc/def, /abcd, /def/abcetc. Especialmente você não pode adicionar /binse PATH já contém /usr/bin.
precisa saber é o seguinte
@ miracle173 - isso é verdade, o que você precisa fazer é dividir $PATHe teste negate contra seus argumentos como: add=/bin dir=/usr/bin ; [ -z "${dir%"$add"}" ] || dir="${dir}:${add}". Na minha resposta, eu faço dessa maneira com quantos argumentos você quiser usar apenas IFS=:.
precisa saber é o seguinte
Relacionada com a específica (exemplo) emissão de adicionar valores para listas separadas por dois pontos: Como posso limpa adicionar  $PATH?  e Adicionar diretório para $PATHse ele ainda não estiver lá (no Superusuário ).
Scott

Respostas:

17

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

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

Na 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ê pode escrever sua função assim (tome cuidado, porque se você usar declareuma função, deverá fornecer -gou 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

Observe que também corrigi um erro em que, se substrjá é uma substring de um dos membros separados por dois pontos de bigstr, mas não seu próprio membro, ele não seria adicionado. Por exemplo, isso permitiria adicionar /bina uma PATHvariável que já contém /usr/bin. Ele usa os extglobconjuntos para coincidir com o início / fim da string ou com dois pontos e depois com qualquer outra coisa. Sem extglob, a alternativa seria:

[[ $bigstr != $substr && $bigstr != *:$substr &&
   $bigstr != $substr:* && $bigstr != *:$substr:* ]]
Graeme
fonte
-gin declarenão está disponível na versão mais antiga do bash, existe alguma maneira para que eu possa tornar isso compatível com versões anteriores?
Ramgorur
2
@ramgorur, você pode exportcolocá-lo em seu ambiente (com o risco de sobrescrever algo importante) ou eval(vários problemas, incluindo segurança, se você não tomar cuidado). Se estiver usando, evalvocê deve estar bem, se quiser eval "$target=\$substr". Se você esquecer o \ que é, ele potencialmente executará um comando se houver um espaço no conteúdo de substr.
Graeme
9

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

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

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

Isso é impresso hello, world.

derobert
fonte
O único problema com namerefs no Bash é que você não pode ter um nameref (em uma função por exemplo) fazendo referência a uma variável (fora da função) com o mesmo nome que o próprio nameref. Isso pode ser corrigido para a versão 4.5.
Kusalananda
2

Você pode usar evalpara definir um parâmetro. Uma descrição deste comando pode ser encontrada aqui . O seguinte uso de evalestá errado:

errado(){
  avaliação $ 1 = $ 2
}

Com relação à avaliação adicional, evalvocê deve usar

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

Verifique os resultados do uso destas funções:

$ X1 = '$ X2'
$ X2 = '$ X3'
$ X3 = 'xxx'
$ 
$ echo: $ X1:
: $ X2:
$ echo: $ X2:
: $ X3:
$ echo: $ X3:
: xxx:
$ 
$ errado Y $ X1
$ echo: $ Y:
: $ X3:
$ 
$ atribuir Y $ X1
$ echo: $ Y:
: $ X2:
$ 
$ atribuir Y "olá mundo"
$ echo: $ Y:
: olá mundo:
$ # o seguinte pode ser inesperado
$ atribuir Z $ Y
$ echo ": $ Z:"
: olá:
$ # então você deve citar o segundo argumento se for uma variável
$ atribui Z "$ Y"
$ echo ": $ Z:"
: olá mundo:

Mas você pode alcançar seu objetivo sem o uso de eval. Eu prefiro esse caminho que é mais simples.

A função a seguir faz a substituição da maneira correta (espero)

aumentar(){
  CORRENTE local = $ 1
  AUGMENT local = $ 2
  NOVO local
  if [[-z $ CURRENT]]; então
    NEW = $ AUGMENT
  elif [[! (($ CURRENT = $ AUGMENT) || ($ CURRENT = $ AUGMENT: *) || \
    ($ CURRENT = *: $ AUGMENT) || ($ CURRENT = *: $ AUGMENT: *))]]; então
    NOVO = $ ATUAL: $ AUGMENT
  outro
    NOVO = $ ATUAL
    fi
  eco "$ NEW"
}

Verifique a seguinte saída

aumentar / 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

aumento / 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:

aumento / bin: / usr / bin: / bin
/ bin: / usr / bin:

aumento / bin: / bin
/ bin:


aumento: / bin
:: / bin


aumentar "/ usr lib" "/ usr bin"
/ usr lib: / usr bin

aumenta "/ usr lib: / usr bin" "/ usr bin"
/ usr lib: / usr bin

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

PATH = `aumentar PATH / bin`
CLASSPATH = `aumentar CLASSPATH / bin`
LD_LIBRARY_PATH = `aumenta LD_LIBRARY_PATH / usr / lib`
miracle173
fonte
Até a sua declaração de avaliação está errada. Isso, por exemplo: v='echo "OHNO!" ; var' ; l=val ; eval $v='$l' - ecoaria "OHNO! Antes de atribuir var. Você poderia" $ {v ## * [; "$ IFS"]} = '$ l' "para garantir que a cadeia não possa se expandir para algo que não será avaliado com o =.
mikeserv 2/14/14
@ MikeServ obrigado por seu comentário, mas acho que não é um exemplo válido. O primeiro argumento do script de atribuição deve ser um nome de variável ou uma variável que contenha um nome de variável usado no lado esquerdo da =instrução de atribuição. Você pode argumentar que meu script não verifica seu argumento. Isso é verdade. Nem checo se existe algum argumento ou se o número de argumentos é válido. Mas isso foi por intenção. O OP pode adicionar essas verificações, se ele quiser.
precisa saber é o seguinte
@ mikeserv: Eu acho que sua proposta de transformar o primeiro argumento silenciosamente em um nome de variável válido não é uma boa idéia: 1) alguma variável é definida / substituída que não foi planejada pelo usuário. 2) o erro está oculto do usuário da função. Isso nunca é uma boa ideia. Deve-se simplesmente gerar um erro se isso acontecer.
precisa saber é o seguinte
@ mikeserv: É interessante quando se deseja usar sua variável v(melhor seu valor) como segundo argumento da função de atribuição. Portanto, seu valor deve estar no lado direito de uma tarefa. É necessário citar o argumento da função assign. Eu adicionei essa sutileza à minha postagem.
precisa saber é o seguinte
Provavelmente é verdade - e você não está usando eval no seu exemplo final - o que é sensato -, então isso realmente não importa. Mas o que estou dizendo é que qualquer código que use eval e receba a entrada do usuário é inerentemente arriscado - e se você usasse o seu exemplo, eu poderia criar a função projetada para alterar meu caminho e torná-lo mais fácil.
precisa saber é o seguinte
2

Com alguns truques, você pode realmente passar parâmetros nomeados para funções, juntamente com matrizes (testadas nos bash 3 e 4).

O método que eu desenvolvi permite acessar 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 nomes (o que compõe um núcleo mais legível), como também pode passar matrizes (e referências a variáveis ​​- esse recurso funciona apenas no bash 4.3)! Além disso, as variáveis ​​mapeadas estão todas no escopo local, assim como $ 1 (e outras).

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

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'
niieani
fonte
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?


fonte
oi, tentei fazê-lo como o seu, mas não está funcionando, desculpe por ser um novato. Você poderia me dizer o que há de errado com este script ?
ramgorur
11
Seu uso evalé suscetível à execução arbitrária de comandos.
Chris Baixo
Você tem uma idéia para tornar as evallinhas zhe mais seguras? Normalmente, quando acho que preciso avaliar, decido não usar * sh e mudar para um idioma diferente. Por outro lado, usando isso em scrips anexar entradas para alguns PATH-like variáveis, ele será executado com constantes de cadeia e não a entrada do usuário ...
11
você pode fazer evalcom segurança - mas é preciso muito pensamento. Se você está apenas tentando fazer referência a um parâmetro, gostaria de fazer algo assim: eval "$1=\"\$2\""dessa maneira, na eval'sprimeira passagem, ele avalia apenas $ 1 e, no segundo, valor = "$ 2". Mas você precisa fazer outra coisa - isso é desnecessário aqui.
mikeserv
Na verdade, meu comentário acima também está errado. Você tem que fazer "${1##*[;"$IFS"]}=\"\$2\""- e mesmo isso vem sem garantia. Or eval "$(set -- $1 ; shift $(($#-1)) ; echo $1)=\"\$2\"". Não é fácil.
precisa
1

Argumentos nomeados simplesmente não são como a sintaxe de Bash foi projetada. O Bash foi projetado para ser uma melhoria iterativa do shell Bourne. Como tal, ele deve garantir que certas coisas funcionem entre as duas conchas o máximo possível. Portanto, não é destinado para ser mais fácil de script com No geral, é apenas pretende ser melhor do que Bourne, assegurando que se você tomar um script a partir de um ambiente de Bourne sobre a bashé tão fácil quanto possível. Isso não é trivial, pois muitas conchas 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 alguma vez mude.

Você provavelmente está melhor vendo um shell script diferente (como pythonalgo assim) inteiramente, se for possível. Se você estiver enfrentando as limitações de um idioma, precisará começar a usar um novo idioma.

Bratchley
fonte
Talvez no início da bashvida isso fosse verdade. Mas agora são tomadas disposições específicas. Variáveis ​​de referência completas agora estão disponíveis em bash 4.3- veja a resposta de derobert .
Graeme
E se você olhar aqui, você vai ver que você pode fazer esse tipo de coisa realmente facilmente mesmo com apenas código portátil POSIX: unix.stackexchange.com/a/120531/52934
mikeserv
1

Com a shsintaxe padrão (funcionaria bash, e não apenas bash), você poderia:

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

Como por soluções usando bash's declare, é seguro , desde que $1contém um nome de variável válido.

Stéphane Chazelas
fonte
0

SOBRE ARGOS NOMEADOS:

Isso é feito com muita simplicidade e bashnão é necessário - este é o comportamento básico especificado da atribuição do POSIX via expansão de parâmetro:

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

Para fazer uma demonstração semelhante à @Graeme, mas de maneira portátil:

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

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

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

SOLUÇÃO:

Para o seu problema específico, não acredito que os argumentos nomeados sejam necessários, embora certamente sejam possíveis. Use em $IFSvez 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
}

Aqui está 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 apenas adicionou os argumentos que ainda não estavam presentes $PATHou que vieram antes? Ou mesmo que foram necessários mais de um argumento? $IFSé útil.

mikeserv
fonte
oi, eu não segui, então você poderia elaborar um pouco mais? obrigado.
ramgorur
Eu já estou fazendo isso ... apenas mais alguns momentos por favor ...
mikeserv
@ramgorur Melhor? Desculpe, mas a vida real se intrometeu e demorei um pouco mais do que eu esperava para terminar a redação.
precisa saber é o seguinte
mesmo aqui também, sucumbiu à vida real. Parece que existem várias abordagens diferentes para codificar essa coisa, deixe-me dar um tempo para escolher a melhor.
Ramgorur
@ramgorur - é claro, só queria ter certeza de que não o deixei pendurado. Sobre isso - você escolhe o que quiser, cara. Vou dizer que nenhuma das outras respostas que eu vejo oferecem uma solução concisa, portátil ou tão robusta quanto assignaqui. Se você tiver dúvidas sobre como funciona, ficarei feliz em responder. E, a propósito, se você realmente quiser argumentos nomeados, talvez queira examinar esta outra resposta minha, na qual mostro como declarar uma função nomeada para os argumentos de outra função: unix.stackexchange.com/a/120531/52934
mikeserv
-2

Não consigo encontrar nada como ruby, python, etc., mas isso parece mais próximo de mim

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

A legibilidade é melhor na minha opinião, 4 linhas são excessivas para declarar os nomes dos parâmetros. Parece mais próximo das línguas modernas também.

Francis Bongiovanni
fonte
(1) A questão é sobre funções de shell. Sua resposta apresenta uma função de esqueleto. Além disso, sua resposta não tem nada a ver com a pergunta. (2) Você acha que aumenta a legibilidade para pegar linhas separadas e concatená-las em uma linha, com ponto e vírgula como separadores? Eu acredito que você entendeu de trás para frente; que seu estilo de uma linha é  menos legível que o estilo de várias linhas.
Scott
Se a redução de peças desnecessárias é o ponto, então por que as aspas e ponto-e-vírgula? a=$1 b=$2 ...funciona tão bem.
ilkkachu 12/07/19