Como posso usar uma variável como uma condição de caso?

17

Eu estou tentando usar uma variável que consiste em diferentes seqüências de caracteres separadas com um |como um caseteste de instrução. Por exemplo:

string="\"foo\"|\"bar\""
read choice
case $choice in
    $string)
        echo "You chose $choice";;
    *)
        echo "Bad choice!";;
esac

Quero poder digitar fooou barexecutar a primeira parte da casedeclaração. No entanto, ambos fooe barme levar para o segundo:

$ foo.sh
foo
Bad choice!
$ foo.sh
bar
Bad choice!

Usar em "$string"vez de $stringnão faz diferença. Nem usa string="foo|bar".

Eu sei que posso fazer assim:

case $choice in
    "foo"|"bar")
        echo "You chose $choice";;
    *)
        echo "Bad choice!";;
esac

Posso pensar em várias soluções alternativas, mas gostaria de saber se é possível usar uma variável como uma casecondição no bash. É possível e, se sim, como?

terdon
fonte
2
Não posso sugerir isso como uma resposta real, mas como ninguém mais o mencionou, você pode agrupar a declaração do caso em uma evalopção $ escapando, $ parens, asensisk, asterisk, ponto-e-vírgula e novas linhas. Feio, mas "funciona".
Jeff Schaller
@ JeffSchaller - não é uma má idéia muitas vezes, e talvez seja apenas o bilhete neste caso. Também considerei recomendar, mas a parte readme parou. na minha opinião, a validação de entrada do usuário, que é isso que parece ser, os casepadrões não devem estar no topo da lista de avaliação e devem ser reduzidos ao *padrão padrão, de modo que os únicos resultados que chegam lá sejam garantidos aceitáveis. Ainda assim, como o problema é de ordem de análise / expansão, uma segunda avaliação pode ser necessária.
mikeserv

Respostas:

13

O manual do bash declara:

maiúsculas e minúsculas na lista [[(] padrão [| padrão] ...) ;; ] ... esac

Cada padrão examinado é expandido usando expansão de til, expansão de parâmetro e variável, substituição aritmética, substituição de comando e substituição de processo.

Nenhuma «expansão de nome de caminho»

Assim: um padrão NÃO é expandido com «expansão de nome de caminho».

Portanto: um padrão NÃO pode conter "|" dentro. Somente: dois padrões podem ser unidos ao "|".

Isso funciona:

s1="foo"; s2="bar"    # or even s1="*foo*"; s2="*bar*"

read choice
case $choice in
    $s1|$s2 )     echo "Two val choice $choice"; ;;  # not "$s1"|"$s2"
    * )           echo "A Bad  choice! $choice"; ;;
esac

Usando «Globbing estendido»

No entanto, wordé compatível com o patternuso de regras de "expansão de nome de caminho".
E «Extended Globbing» aqui , aqui e aqui permite o uso de padrões alternados ("|").

Isso também funciona:

shopt -s extglob

string='@(foo|bar)'

read choice
    case $choice in
        $string )      printf 'String  choice %-20s' "$choice"; ;;&
        $s1|$s2 )      printf 'Two val choice %-20s' "$choice"; ;;
        *)             printf 'A Bad  choice! %-20s' "$choice"; ;;
    esac
echo

Conteúdo da string

O próximo script de teste mostra que o padrão que corresponde a todas as linhas que contêm um fooou barqualquer lugar é '*$(foo|bar)*'ou as duas variáveis $s1=*foo*e$s2=*bar*


Script de teste:

shopt -s extglob    # comment out this line to test unset extglob.
shopt -p extglob

s1="*foo*"; s2="*bar*"

string="*foo*"
string="*foo*|*bar*"
string='@(*foo*|*bar)'
string='*@(foo|bar)*'
printf "%s\n" "$string"

while IFS= read -r choice; do
    case $choice in
        "$s1"|"$s2" )   printf 'A first choice %-20s' "$choice"; ;;&
        $string )   printf 'String  choice %-20s' "$choice"; ;;&
        $s1|$s2 )   printf 'Two val choice %-20s' "$choice"; ;;
        *)      printf 'A Bad  choice! %-20s' "$choice"; ;;
    esac
    echo
done <<-\_several_strings_
f
b
foo
bar
*foo*
*foo*|*bar*
\"foo\"
"foo"
afooline
onebarvalue
now foo with spaces
_several_strings_
Comunidade
fonte
9

Você pode usar a extglobopção:

shopt -s extglob
string='@(foo|bar)'
choroba
fonte
Interessante; o que faz @(foo|bar)especial em comparação com foo|bar? Ambos são padrões válidos que funcionam da mesma maneira quando digitados literalmente.
chepner
4
Ah deixa pra lá. |não faz parte do padrão foo|bar, faz parte da sintaxe da caseinstrução permitir vários padrões em uma cláusula. | faz parte do padrão estendido, no entanto.
Chepner # 6/15
3

Você precisa de duas variáveis caseporque o |canal ou é analisado antes que os padrões sejam expandidos.

v1=foo v2=bar

case foo in ("$v1"|"$v2") echo foo; esac

foo

Os padrões de shell nas variáveis ​​são tratados de maneira diferente quando citados ou não entre aspas:

q=?

case a in
("$q") echo question mark;;
($q)   echo not a question mark
esac

not a question mark
mikeserv
fonte
-1

Se você deseja uma solução alternativa compatível com traços, escreva:

  string="foo|bar"
  read choice
  awk 'BEGIN{
   n=split("'$string'",p,"|");
   for(i=1;i<=n;i++)
    if(system("\
      case \"'$choice'\" in "p[i]")\
       echo \"You chose '$choice'\";\
       exit 1;;\
      esac"))
     exit;
   print "Bad choice"
  }'

Explicação: awk é usado para dividir "string" e testar cada parte separadamente. Se "choice" corresponder à parte atualmente testada p [i], o comando awk será encerrado com "exit" na linha 11. Para o teste, o "case" do shell é usado (dentro de uma chamada de 'sistema') , como solicitado por Terdon. Isso mantém a possibilidade de modificar a "string" de teste pretendida, por exemplo, para "foo * | bar", a fim de corresponder também a "foooo" ("padrão de pesquisa"), como permite o "case" do shell. Se preferir expressões regulares, você pode omitir a chamada "system" e usar "~" ou "match" do awk.

Gerald Schade
fonte
Caro downvoter, eu poderia aprender melhor como evitar candidatos a soluções inúteis se você me dissesse o motivo do seu voto negativo.
Gerald Schade