O que é uma maneira segura e portátil de dividir uma string na programação shell?

4

Ao escrever um script de shell, muitas vezes quero dividir uma string. Aqui está um exemplo muito simples:

for dir in $(echo $PATH | tr : " "); do
    [[ -x "$dir"/"$1" ]] && echo $dir
done

Isso pesquisará cada diretório no $ PATH para um executável com o mesmo nome de $1 . Muito simples, corre bem, mas quebra se um diretório no meu $ PATH contém um espaço em branco em seu nome.

Qual é a maneira recomendada de dividir uma string na ocorrência de um separador recorrente?

Idealmente, a solução seria capaz de rodar shells (razoavelmente) antigas, como o ksh88.

    
por rahmu 08.02.2013 / 12:10

2 respostas

9

Basta definir IFS de acordo com suas necessidades e permitir que o shell execute a divisão de palavras:

IFS=':'
for dir in $PATH; do
    [ -x "$dir"/"$1" ] && echo $dir
done

Isso funciona em bash , dash e ksh , mas testado apenas com as versões mais recentes.

    
por 08.02.2013 / 12:18
9

A solução óbvia seria usar a divisão da palavra shell, mas cuidado com algumas dicas:

IFS=:
set -f
for dir in $PATH; do
    dir=${dir:-.}
    [ -x "${dir%/}/$1" ] && printf "%s\n" "$dir"
done

Você precisa de set -f porque quando uma variável é deixada sem aspas, tanto divisão de palavras como geração de nome de arquivo ( globbing ) são executadas nela e aqui você só quer dividir palavras (por exemplo, no caso improvável de que $PATH contenha /usr/local/*bin* , você quer procurar na pasta /usr/local/*bin* , não em /usr/local/bin e /usr/local/sbin ..., e se PATH contiver /*/*/*/../../../*/*/*/*/../../../*/*/*/* , você não quer que sua máquina caia)

Um componente vazio $PATH significa o diretório atual ( . ), não / . $dir/$1 não estaria correto nesse caso. A solução é escrever $dir${dir:+/}$1 ou alterar $dir para . nesse caso (o que fornece uma saída mais útil quando exibido com printf '%s\n' "$dir" .

//foo não é necessariamente igual a /foo , portanto, se / estiver em $PATH , você não deseja $dir/$1 , que seria //$1 . Daí o ${dir%/} para remover uma barra final.

Depois, há alguns outros problemas:

Para $PATH , ":" é um separador de campo enquanto para $IFS , é um campo terminator (sim, eu sei, S is para S eparator , culpe ksh e POSIX por padronizar o comportamento do ksh).

Portanto, se $PATH for /usr/bin:/bin: (que é uma prática ruim, mas ainda é comumente encontrada), isso significa "/usr/bin" , "/bin" e "" (ou seja, o diretório atual), enquanto a divisão da palavra shell (todas as shells POSIX, exceto zsh ) dividirão isso em /usr/bin e /bin apenas.

Se $PATH estiver definido, mas vazio, isso significa: "procure apenas no diretório atual" . Enquanto os shells (incluindo aqueles que tratam $IFS como separador) irão expandi-lo para uma lista vazia.

Por último, mas não menos importante. Se $PATH não está definido, então isso tem um significado especial que é: procurar na lista de pesquisa padrão do sistema , que infelizmente significa algo diferente dependendo de quem (qual comando) você pergunta.

$ env -u PATH bash -c 'type usbipd'
usbipd is /usr/local/sbin/usbipd
$ env -u PATH ksh -c 'type usbipd'
ksh: whence: usbipd: not found

E, basicamente, no seu script, você teria que adivinhar qual é o caminho de pesquisa padrão no contexto que é importante para você.

Observe que o POSIX deixa o comportamento não especificado quando $PATH não está definido ou está vazio, portanto, isso não o ajudará. Isso também significa que o que eu disse acima pode não se aplicar a alguns sistemas POSIX / Unix passados, atuais ou futuros.

Em suma, analisar $PATH para tentar descobrir de onde um comando seria executado é um assunto complicado.

Existe um comando padrão para isso, que é command :

ls_path=$(command -v ls)

Mas o que se pode perguntar é: por que você quer saber?

Agora para restaurar o IFS ao seu valor padrão:

oldIFS=$IFS
IFS=:
...
IFS=$oldIFS

funcionará na prática na maioria dos casos, mas não é garantido que funcione pelo POSIX.

A razão é que, se $IFS foi anteriormente desfeito, o que significa comportamento de divisão padrão (que está em shells POSIX, divididos em espaço, tabulação ou nova linha), após esses comandos, set mas vazio (o que significa não dividir ).

Outro problema em potencial é se você generalizar essa abordagem e usá-la em muitas funções diferentes, então, se na parte ... acima, você estiver chamando uma função que faz a mesma coisa (faz uma cópia de $IFS em $oldIFS ), você perderá o $oldIFS original e restaurará o $IFS errado.

Em vez disso, você pode usar subshells quando possível:

(
  IFS=:
  ...
)
# only the subshell's IFS was affected, the parent still has its own IFS

Minha abordagem é definir o $ IFS (e ativar ou desativar set -f ) todas as vezes Eu preciso da divisão de palavras (o que é raro) e não restauro o valor anterior. Obviamente, isso não funciona se o script chamar o código de outra pessoa que não segue essa prática e assume um comportamento padrão de divisão de palavras.

    
por 08.02.2013 / 13:18