Precisa capturar stdout e modificar a variável por referência da função no bash

5

Eu posso facilmente capturar stdout de uma chamada de função (em subshell) para uma variável com:

val="$(get_value)"

Eu também posso modificar variáveis (por exemplo, uma matriz) em um shell por referência , por assim dizer, no mesmo shell com algo como:

function array.delete_by_index {
    local array_name="$1"
    local key="$2"

    unset "$array_name[$key]"
    eval "$array_name=(\"\${$array_name[@]}\")"
}

array.delete_by_index "array1" 0

Mas o que eu estou lutando para descobrir como fazer é fazer as duas coisas ao mesmo tempo, de uma maneira limpa. Um exemplo de onde eu quero isso é estalar um valor de uma matriz:

function array.pop {
    local array_name="$1"

    local last_index=$(( $(eval "echo \${#$array_name[@]}") - 1 ))
    local tmp="$array_name[\"$last_index\"]"
    echo "${!tmp}"

    # Changes "$array_name" here, but not in caller since this is a sub shell
    array.delete_by_index "$array_name" $last_index
}

val="$(array.pop "array1")"

Parece-me que todas as formas de capturar stdout para uma variável requerem um subshell no bash, e usar um sub shell não me permitirá alterar um valor por referência no contexto do chamador.

Eu estou querendo saber se alguém sabe um basismo mágico para conseguir isso? Eu particularmente não quero uma solução que use qualquer tipo de arquivo / fifo no sistema de arquivos.

A segunda resposta nesta questão parece para sugerir que isso é possível em ksh usando val="${ cmd; }" , já que essa construção aparentemente permite capturar a saída, mas sem usar sub-shells. Então, sim, eu poderia tecnicamente mudar para o ksh, mas gostaria de saber se isso é possível no bash.

    
por krb686 07.01.2017 / 07:14

2 respostas

6

Isso funciona em bash (desde o release 4.3) e ksh93 . Para "embasar", substitua todos os typeset por local nas funções e typeset no escopo global por declare (mantendo todas as opções!). Eu sinceramente não sei porque o Bash tem tantos nomes diferentes para coisas que são apenas variações de typeset .

function stack_push
{
    typeset -n _stack="$1"
    typeset element="$2"

    _stack+=("$element")
}

function stack_pop
{
    typeset -n _stack="$1"
    typeset -n _retvar="$2"

    _retvar="${_stack[-1]}"

    unset _stack[-1]
}

typeset -a stack=()

stack_push stack "hello"
stack_push stack "world"

stack_pop stack value
printf '%s ' "$value"

stack_pop stack value
printf '%s\n' "$value"

Usando um nameref na função, você evita eval (nunca precisei usar eval em nenhum script!). Fornecendo a função stack_pop com um local para armazenar o valor estourado, você evita a subcamada. Evitando o subshell, a função stack_pop pode modificar o valor da variável stack no escopo externo.

O sublinhado nas variáveis locais na função é evitar ter um nameref que tenha o mesmo nome da variável que ele referencia (Bash não gosta, ksh não se importa, veja esta pergunta ).

Em ksh você pode escrever a função stack_pop como

function stack_pop
{
    typeset -n _stack="$1"

    printf '%s' "${_stack[-1]}"

    unset _stack[-1]
}

E, em seguida, chame-o com

printf '%s %s\n' "${ stack_pop stack }" "${ stack_pop stack }"

( ${ ... } é o mesmo que $( ... ) , mas não cria um subshell)

Mas eu não sou um grande fã disso. IMHO, stack_pop não deveria ter que enviar os dados para stdout, e eu não deveria ter que chamá-lo com ${ ... } para obter os dados. Eu poderia estar mais ok com meu original stack_pop e, em seguida, adicionar um stack_pop_print que faz o acima, se necessário.

Para o Bash, você poderia ir com o stack_pop no começo do meu post, e então ter um stack_top_print que apenas imprime o elemento superior da pilha para stdout, sem removê-lo (o que não pode porque provavelmente seria executado em um sub-diretório $( ... ) .

    
por 07.01.2017 / 10:13
2

A única solução que eu conseguia pensar sem usar arquivos ou modificar a função era executar o comando duas vezes, no qual um seria executado em um subshell para captura, e o outro para modificar a variável no shell pai.

val="$(array.pop "array1")"
array.pop array1

Se reescrever a função for uma opção:

function array.pop {
    local array_name="$1";
    local last_index=$(( $(eval "echo \${#$array_name[@]}") - 1 ));
    local tmp="$array_name[\"$last_index\"]";
    echo "${!tmp}"; 

    if [[ $2 ]]; then
        eval "$2=\"${!tmp}\""; # magic bashism here 
    fi

    array.delete_by_index "$array_name" "$last_index"; 
}

Para ser executado como array.pop 'array1' variablename , que também inicializará a variável, mesmo que ela não exista:

$ array1=(one two three)
$ array.pop array1 var
three
$ echo "$var"
three
$ echo "${array1[*]}"
one two
    
por 07.01.2017 / 09:43

Tags