Como uma função bash pode retornar vários valores?

5

Qual é a melhor prática para retornar muitos valores de uma função bash?

Exemplo1:

Script de função:

function mysqlquery {
    local dbserver='localhost'
    local dbuser='user'
    local dbpass='pass'
    local db='mydb'
    mysql -h "$dbserver" -u "$dbuser" -p "$dbpass" --skip-column-names --raw -e "$*" "$db"
    if [ $? -ne 0 ]; then
        return 1
    fi
}

Script de origem:

for XY in $(mysqlquery "select XY from ABC where DEF = 123" 2>/dev/null);do
    dosomethingwith $XY
done
if mysqlquery "select XY from ABC where DEF = 123" 2>/dev/null; then
    echo true
fi

Exemplo2:

Script de função:

function mysqlquery {
    local dbserver='localhost'
    local dbuser='user'
    local dbpass='pass'
    local db='mydb'
    result=$(mysql -h "$dbserver" -u "$dbuser" -p "$dbpass" -e "$*" "$db" 2>/dev/null)
    if [ $? -ne 0 -o -z "$result" ]; then
        return 1
    fi
}

Script de origem:

result=$(mysqlquery "select XY from ABC where DEF = 123" 2>/dev/null)
for XY in $result;do
    dosomethingwith $XY
done
if mysqlquery "select XY from ABC where DEF = 123" 2>/dev/null; then
    echo true
fi

Ou existem mais abordagens para retornar várias informações (muito mais do que um único valor int)?

    
por FaxMax 03.12.2017 / 01:12

2 respostas

5

Sim, bash return só pode retornar números e apenas inteiros entre 0 e 255.

Para um shell que pode retornar alguma coisa (listas de coisas), você pode ver es :

$ es -c "fn f {return (a 'b c' d \$*)}; printf '%s\n' <={f x y}"
a
b c
d
x
y

Agora, em shells semelhantes a Korn, como bash , você sempre pode retornar os dados em uma variável pré-acordada. E essa variável pode ser em qualquer tipo suportado pelo shell.

Para bash , isso pode ser matrizes escalares e esparsas (matrizes associativas com chaves restritas a inteiros positivos) ou matrizes associativas com chaves não vazias (nem chaves nem valores podem conter caracteres NUL).

Veja também zsh com matrizes normais e matrizes associativas sem essas restrições.

O equivalente da função f es acima pode ser feito com:

f() {
  reply=(a 'b c' d "$@")
}
f
printf '%s\n' "${reply[@]}"

Agora, mysql consultas geralmente retornam tabelas, ou seja, matrizes bidimensionais. O único shell que eu sei que possui arrays multidimensionais é ksh93 (como bash ele não suporta caracteres NUL em suas variáveis).

ksh também suporta variáveis composto que seriam úteis para retornar tabelas com seus cabeçalhos.

Também suporta passar variáveis por referência.

Então, você pode fazer:

function f {
  typeset -n var=$1
  var=(
    (foo bar baz)
    (1 2 3)
  }
}
f reply
printf '%s\n' "${reply[0][1]}" "${reply[1][2]}"

Ou:

function f {
  typeset -n var=$1
  var=(
    (firstname=John lastname=Smith)
    (firstname=Alice lastname=Doe)
  )
}

f reply
printf '%s\n' "${reply[0].lastname}"

Agora, para obter a saída de mysql e armazenar isso em algumas variáveis, precisamos analisar essa saída que é texto com colunas da tabela separadas por caracteres e linhas TAB separados por NL e alguma codificação para os valores a serem permitir que eles contenham NL e TAB.

Sem --raw , mysql produziria um NL como \n , um TAB como \t , uma barra invertida como \ e um NUL como ksh93 .

read -C também tem eval que pode ler texto formatado como uma definição de variável (não muito diferente de usar firstname ), então você pode fazer:

function mysql_to_narray {
  awk -F '\t' -v q="'" '
    function quote(s) {
      gsub(/\n/, "\n", s)
      gsub(/\t/, "\t", s)
      gsub(/\\/, "\", s)
      gsub(q, q "\" q q, s)
      return q s q
    }
    BEGIN{print "("}
    {
      print "("
      for (i = 1; i <= NF; i++)
        print " " quote($i)
      print ")"
    }
    END {print ")"}'
}

function query {
  typeset -n var=$1
  typeset db=$2
  shift 2

  typeset -i n=0
  typeset IFS=' '
  typeset credentials=/path/to/file.my # not password on the command line!
  set -o pipefail

  mysql --defaults-extra-file="$credentials" --batch \
        --skip-column-names -e "$*" "$db" |
    mysql_to_narray |
    read -C var
}

Para ser usado como

query myvar mydb 'select * from mytable' || exit
printf '%s\n' "${myvar[0][0]}"...

Ou para uma variável composta:

function mysql_to_array_of_compounds {
  awk -F '\t' -v q="'" '
    function quote(s) {
      gsub(/\n/, "\n", s)
      gsub(/\t/, "\t", s)
      gsub(/\\/, "\", s)
      gsub(q, q "\" q q, s)
      return q s q
    }
    BEGIN{print "("}
    NR == 1 {
      for (i = 1; i<= NF; i++) header[i] = $i
      next
    }
    {
      print "("
      for (i = 1; i <= NF; i++)
        print " " header[i] "=" quote($i)
      print ")"
    }
    END {print ")"}'
}

function query {
  typeset -n var=$1
  typeset db=$2
  shift 2

  typeset -i n=0
  typeset IFS=' '
  typeset credentials=/path/to/file.my # not password on the command line!
  set -o pipefail

  mysql --defaults-extra-file="$credentials" --batch \
        -e "$*" "$db" |
    mysql_to_array_of_compounds |
    read -C var
}

Para ser usado como:

query myvar mydb 'select "First Name" as firstname, 
                         "Last Name" as lastname from mytable' || exit

printf '%s\n' "${myvar[0].firstname"

Observe que os nomes dos cabeçalhos ( lastname , bash acima) precisam ser identificadores de shell válidos.

Em zsh ou yash ou zsh (embora os índices das matrizes estejam em primeiro lugar em zsh e yash e somente awk pode armazenar caracteres NUL), você pode sempre retornar uma matriz por coluna, tendo set -o localoptions gera o código para defini-los:

query() {
  typeset db="$1"
  shift

  typeset IFS=' '
  typeset credentials=/path/to/file.my # not password on the command line!
  set -o pipefail

  typeset output
  output=$(
    mysql --defaults-extra-file="$credentials" --batch \
          -e "$*" "$db" |
      awk -F '\t' -v q="'" '
        function quote(s) {
          gsub(/\n/, "\n", s)
          gsub(/\t/, "\t", s)
          gsub(/\\/, "\", s)
          gsub(q, q "\" q q, s)
          return q s q
        }
        NR == 1 {
          for (n = 1; n<= NF; n++) column[n] = $n "=("
          next
        }
        {
          for (i = 1; i < n; i++)
            column[i] = column[i] " " quote($i)
        }
        END {
          for (i = 1; i < n; i++)
            print column[i] ") "
        }'
  ) || return
  eval "$output"
}

Para ser usado como:

query mydb 'select "First Name" as firstname, 
                         "Last Name" as lastname from mytable' || exit

printf '%s\n' "${firstname[1]}"

Adicione um zsh com local - ou set -o pipefail com bash4.4 + antes do ksh93 para a configuração dessa opção ser local para a função, como com a abordagem bash .

Observe que, em todos os itens acima, não estamos convertendo de volta o ksh93 s para NULs reais, pois zsh ou gsub(/\0/, "awk", s) se sufocariam neles. Você pode fazer isso usando %code% para poder trabalhar com BLOBs, mas observe que %code% não funcionaria com todas as implementações %code% .

De qualquer forma, aqui, eu usaria linguagens mais avançadas do que um shell como perl ou python para fazer esse tipo de coisa.

    
por 03.12.2017 / 19:27
1

Bem, isso depende do tipo de formato de saída que você deseja / precisa. A maneira mais fácil é provavelmente apenas imprimir a saída da função, assim a função se comporta como qualquer outro comando. Outra maneira seria definir alguma variável (possivelmente uma matriz associativa) dentro da função. Isso tem o benefício de que itens diferentes são separados de forma limpa, mas você pode precisar codificar algumas variáveis.

A função em seu primeiro exemplo implementa a primeira: qualquer que seja o cliente mysql que imprima da função, vai para a saída padrão da função. Dado que os dados já vêm como um fluxo de bytes, mantê-lo como tal é bom.

Embora aqui a questão seja o que fazer com a saída. for x in $(somecmd) ... não é bom, pois a saída de somecmd é dividida em palavras e processada para globs de nome de arquivo. Geralmente é melhor usar while read ... , consulte Como posso ler um arquivo (fluxo de dados, variável) linha por linha (e / ou ou campo a campo)?

Para ler a saída de mysql linha por linha, você pode fazer

mysql -h "$dbserver" etc. etc. | while read -r line ; do
    dosomethingwith "$line"
done

ou com uma função

mysqlquery() {
    ...
    mysql -h "$dbserver" etc. etc. 2>/dev/null
}
mysqlquery | while read -r line ; do ...

Note que você não precisa do if [ $? -ne 0 ]; then return 1 : o valor de retorno da função é o mesmo que o do último comando. Não é fácil olhar para o valor de retorno se você estiver usando a função para alimentar um pipe.

    
por 03.12.2017 / 17:18