Shell: selecionando um programa disponível

3

No bash / ksh / zsh, há um bom idioma para definir uma variável como a primeira de uma lista de programas alternativos que podem ser encontrados em $PATH (ou de outra forma solicitada pelo shell)?

Por exemplo, se eu tiver um script que precise funcionar se o realpath (1) estiver instalado em realpath ou grealpath , eu poderia percorrer o caminho mais longo:

 if type "grealpath" > /dev/null; then
    realpath_exec=grealpath
  elif type "realpath" > /dev/null; then
    realpath_exec=realpath
  else
    echo "$0: No realpath found in PATH" >&2
    exit 1
  fi

A maioria das maneiras que posso pensar em criar uma expressão com || e && precisam de subpomes aninhados (não um problema enorme, apenas irritante e ruim para desempenho em uma função freqüentemente chamada) e / ou redirecionamentos complicados ou feio grep para lidar com as várias saídas. (Note que a saída de type ou which não é diretamente utilizável se o "programa" instalado for uma função ou um alias; portanto, se eu apenas quiser supor que algo disponível para mim chamado realpath pode fazer o trabalho, devo ligar valor de retorno em vez de saída ou fazer um grep.

O que precede é bom, é apenas muito prolixo e se você tem várias opções de programas como esta, é dolorosamente assim. Existe uma maneira mais elegante que eu possa escolher o primeiro programa disponível de uma lista e atribuí-lo a uma variável?

    
por Trey 12.12.2015 / 21:03

3 respostas

2

A maneira mais direta e compreensível seria apenas um loop for com um único if s dentro dele, que break s fora do loop quando encontrar uma correspondência.

Se você não quiser fazer isso, no Bash, type aceita vários argumentos para pesquisar por:

type grealpath realpath ...

e a primeira palavra da linha de saída para uma entrada correspondente é sempre 1 o nome do comando / função / alias. Você pode capturar a saída padrão em uma matriz e indexá-la para evitar um subprocesso adicional:

words=($(type realpath grealpath foo make 2>/dev/null))
realpath_exec=${words[0]}
if ! [ "$realpath_exec" ] ...

De um certo ponto de vista, isso é elegante, mas é decididamente mais difícil de entender quando você se depara com isso.

A divisão de palavras em inicializadores de matriz depende do valor de IFS . Se IFS tiver sido alterado ou se algum dos seus comandos contiver um espaço, isso não funcionará.

1 Experimentalmente, este parece ser o caso em mais localidades, mesmo onde essa não é a ordem natural das palavras, e parece ser sempre uma U + 0020 ASCII espaço depois, mas o formato de saída não é mais especificado. Em algumas localizações, isso não funcionará; você precisa considerar se isso será um problema para você. Você pode usar LC_ALL=C type ... para (mais ou menos) garantir um formato de saída adequado.

Em zsh , se você se importa apenas com executáveis, e não com funções ou aliases, você pode usar $commands :

realpath_exec=${commands[grealpath]:-${commands[realpath]:-${commands[another]:?No compatible command found}}}

A expansão ${param:-...} fornece o valor de param , se não for nulo, e ... , caso contrário; ${param:?...} erros com ... se param falhar. Isso continua bastante feio.

Se você não se importa com a ordenação entre a seleção de comandos, você pode usar uma versão mais simples com o (i) sinalizador subscrito :

realpath_exec=${commands[(i)realpath|grealpath|another]}

Para incluir funções e aliases também em qualquer um desses, você pode usar $functions e $aliases , mas será repetitivo.

    
por 12.12.2015 / 21:32
2

Você poderia refatorar, por exemplo, em:

isExecutable(){ type "$1" >/dev/null 2>&1 && printf "%s\n" "$1"; }

realpath_exe='isExecutable grealpath || isExecutable realpath'  
[ -n "$realpath_exe" ] || {
   echo "$0: No realpath found in PATH" >&2
   exit 1
}

mas acho que sua versão é boa e legível. Eu não me preocuparia com o seu comprimento.

Por favor, note que você não pode ter cifrões no lado esquerdo das atribuições de variáveis e também que type também procura funções e aliases além dos arquivos executáveis no PATH.

    
por 12.12.2015 / 21:32
2

Você está certo sobre a saída de type e which : então você não deve usá-los. Você deve usar command .

pathx()
    for   cmd
    do    set "" "${PATH:?is NULL!}:"
          while  "${2:+set}" -- "${2%%:*}" "${2#*:}" 2>&3
          do      command -v -- "${1:-.}/$cmd"
    done; done    3>/dev/null

Isso só faz command -v $PATH_component/$cmd e assim nunca listará aliases ou builtins ou qualquer um dos outros - ele irá procurar recursivamente cada componente de sua variável de ambiente $PATH para cada um de seus argumentos e imprimir em seu stdout qualquer que encontrar.

Se você adicionar um && break após command -v ... , ele abortará sua $PATH pesquisa na primeira vez que localizar com êxito um executável $PATH 'd nomeado para um de seus argumentos.

Foi a ideia de Michael Homer - e é realmente a melhor.

Funciona aninhando o loop while no loop for . for cada iteração do loop for , o loop while itera sobre todos os componentes em $PATH , testando a string mais curta : colon-separated ele pode dividir ${2%%:*} com command -v $slice/$cmd e salvar a maior string para a próxima iteração ${2#*:} . É somente quando o $PATH foi completamente testado que o loop while tenta executar uma string "${2:-NUL}" e falha ao concluir e a próxima iteração for começa.

cp /bin/cat /tmp
(PATH=$PATH:/tmp pathx cat dd dummy doesntexist read echo)
/usr/bin/cat
/tmp/cat
/usr/bin/dd
/usr/bin/echo

Aparentemente, você quer aliases e outras coisas. Bem, isso é factível:

shellx()
    for  cmd
    do  "set"  --   "$cmd";"unset" cmd
         for   type  in     alias  exe
         do    case  $type  in
               (a*)        "alias"  "${1%%*=*}"     ;;
               (e*)  PATH= "command" -v -- "$1"     &&
                            type=function::builtin  ||
                           "command" -v -- "$1"     ;;
               esac  >&2&& "command" -V -- "$1" >&3 &&
                     cmd=$("command" -v -- "$1")    && return
    done;done  3>&2 2>/dev/null

Isso foi mais difícil do que eu me lembro, mas termina assim que um de seus argumentos é considerado executável. Ele coloca seu tipo - um dos alias , função :: builtin , ou exe em $type , e o comando entra em $cmd . Os aliases obtêm a definição escrita em $cmd - que em zsh , bash e yash se parece com ...

alias x='something or other'

... em ksh93 é apenas ...

something or other

... e em dash é ...

x='something or other'

... mas, novamente, todos recebem a variável $type atribuída.

Se você passar um argumento de que é um alias de shell e de alguma forma foi definido mesmo que seu nome contenha = , então, bem, isso não o encontrará. Se você precisar dessa funcionalidade, será necessário usar a saída alias ao invés de testar seu retorno.

Se um executável for encontrado, a saída command -V será gravada no erro padrão imediatamente antes da função retornar. Ele retorna false se nenhum executável for encontrado.

    
por 13.12.2015 / 07:03