Existe algo como "split ()" do JavaScript no shell?


18

É muito fácil usar o split()JavaScript para quebrar uma string em uma matriz.

E o script de shell?

Digamos que eu queira fazer isso:

$ script.sh var1_var2_var3

Quando o usuário fornece essa string var1_var2_var3para o script.sh, dentro do script ele a converterá em uma matriz como

array=( var1 var2 var3 )
for name in ${array[@]}; do
    # some code
done

11
o que shellvocê está usando, com bashque você pode fazerIFS='_' read -a array <<< "${string}"
gwillie

perltambém pode fazer isso. Não é uma concha "pura", mas é bastante comum.
Sobrique 08/09

@Sobrique Também não conheço a definição técnica de shell "puro", mas existe o node.js.
Emory

I tendem a trabalhar em 'está provavelmente instalado na minha caixa de linux por padrão' e não se preocupe as minúcias :)
Sobrique

Respostas:


24

Os shells Bourne / POSIX têm um operador split + glob e é chamado toda vez que você deixa uma expansão de parâmetro ( $var, $-...), substituição de comando ( $(...)) ou expansão aritmética ( $((...))) sem aspas no contexto da lista.

Na verdade, você invocado por engano quando você fez for name in ${array[@]}em vez de for name in "${array[@]}". (Na verdade, você deve se lembrar de que invocar esse operador assim por engano é fonte de muitos bugs e vulnerabilidades de segurança ).

Esse operador está configurado com o $IFSparâmetro especial (para informar em quais caracteres dividir (embora tenha cuidado com o espaço, a guia e a nova linha receberem um tratamento especial)) e a -fopção de desativar ( set -f) ou ativar ( set +f) a globpeça.

Observe também que, enquanto o Sin $IFSfoi originalmente (no shell Bourne de onde $IFSvem) para Separator, nos shells POSIX, os caracteres em $IFSdevem ser vistos como delimitadores ou terminadores (veja um exemplo abaixo).

Então, para dividir _:

string='var1_var2_var3'
IFS=_ # delimit on _
set -f # disable the glob part
array=($string) # invoke the split+glob operator

for i in "${array[@]}"; do # loop over the array elements.

Para ver a distinção entre separador e delimitador , tente:

string='var1_var2_'

Isso o dividirá em var1e var2somente (nenhum elemento extra vazio).

Portanto, para torná-lo semelhante ao JavaScript split(), você precisará de uma etapa extra:

string='var1_var2_var3'
IFS=_ # delimit on _
set -f # disable the glob part
temp=${string}_ # add an extra delimiter
array=($temp) # invoke the split+glob operator

(observe que ele dividiria um elemento vazio $stringem 1 (não 0 ), como o JavaScript split()).

Para ver a guia de tratamentos especiais, o espaço e a nova linha, compare:

IFS=' '; string=' var1  var2  '

(de onde você chega var1e var2) com

IFS='_'; string='_var1__var2__'

onde você começa: '', var1, '', var2, ''.

Observe que o zshshell não invoca esse operador split + glob implicitamente assim, a menos que seja dentro shou kshemulado. Lá, você deve invocá-lo explicitamente. $=stringpara a parte dividida, $~stringpara a parte global ( $=~stringpara ambos) e também possui um operador de divisão onde é possível especificar o separador:

array=(${(s:_:)string})

ou para preservar os elementos vazios:

array=("${(@s:_:)string}")

Note-se que não sé para a divisão , não delimitando (também com $IFS, um conhecido POSIX não conformidade do zsh). É diferente do JavaScript, split()pois uma string vazia é dividida em 0 (não 1) elemento.

Uma diferença notável com $IFS-splitting é que se ${(s:abc:)string}divide na abcstring, enquanto com IFS=abc, que se dividiria em a, bou c.

Com zshe ksh93, o tratamento especial que o espaço, a guia ou a nova linha recebe pode ser removido dobrando-os $IFS.

Como uma nota histórica, a concha Bourne (o ancestral ou conchas POSIX modernas) sempre retirava os elementos vazios. Ele também teve vários bugs relacionados à divisão e expansão de $ @ com valores não padrão de $IFS. Por exemplo IFS=_; set -f; set -- $@, não seria equivalente a IFS=_; set -f; set -- $1 $2 $3....

Divisão em regexps

Agora, para algo mais próximo do JavaScript split()que possa ser dividido em expressões regulares, você precisará confiar em utilitários externos.

No baú da ferramenta POSIX, awkexiste um splitoperador que pode ser dividido em expressões regulares estendidas (essas são mais ou menos um subconjunto das expressões regulares do tipo Perl suportadas pelo JavaScript).

split() {
  awk -v q="'" '
    function quote(s) {
      gsub(q, q "\\" q q, s)
      return q s q
    }
    BEGIN {
      n = split(ARGV[1], a, ARGV[2])
      for (i = 1; i <= n; i++) printf " %s", quote(a[i])
      exit
    }' "$@"
}
string=a__b_+c
eval "array=($(split "$string" '[_+]+'))"

O zshshell possui suporte interno para expressões regulares compatíveis com Perl (em seu zsh/pcremódulo), mas usá-lo para dividir uma string, embora possível seja relativamente complicado.


Existe alguma razão para tratamentos especiais com guia, espaço e nova linha?
cuonglm

11
@cuonglm, geralmente você quer dividir em palavras quando os delimitadores são espaços em branco, no caso de delimitadores não em branco (como a divisão $PATHem :) ao contrário, você geralmente querem preservar os elementos vazios. Observe que no shell Bourne, todos os personagens estavam recebendo o tratamento especial, kshalterado para ter apenas os espaços em branco (apenas espaço, guia e nova linha) tratados especialmente.
Stéphane Chazelas

Bem, a recente nota de Bourne adicionada me surpreendeu. E para concluir, você deve adicionar a nota para o zshtratamento com string contendo 2 ou mais caracteres ${(s:string:)var}? Se adicionado, eu posso apagar a minha resposta :)
cuonglm

11
O que você quer dizer com "Observe também que o S em $ IFS é para Delimitador, não para Separador". Eu entendo a mecânica e que ela ignora os separadores à direita, mas Srepresenta o separador , não o delimitador . Pelo menos, é o que diz o manual do meu bash.
terdon

@terdon, $IFSvem do shell Bourne onde era separador , o ksh mudou o comportamento sem alterar o nome. Menciono isso para enfatizar que split+glob(exceto em zsh ou pdksh) simplesmente não se divide mais.
Stéphane Chazelas

7

Sim, use IFSe configure-o para _. Em seguida, use read -apara armazenar em uma matriz ( -rdesativa a expansão da barra invertida). Observe que isso é específico ao bash; O ksh e o zsh possuem recursos semelhantes com sintaxe ligeiramente diferente, e o sh simples não possui variáveis ​​de matriz.

$ r="var1_var2_var3"
$ IFS='_' read -r -a array <<< "$r"
$ for name in "${array[@]}"; do echo "+ $name"; done
+ var1
+ var2
+ var3

A partir de man bash :

ler

-uma aname

As palavras são atribuídas aos índices seqüenciais da variável da matriz aname, iniciando em 0. aname é desabilitado antes que novos valores sejam atribuídos. Outros argumentos de nome são ignorados.

IFS

O separador de campo interno usado para dividir palavras após a expansão e dividir linhas em palavras com o comando read builtin. O valor padrão é `` ''.

Observe que readpara na primeira nova linha. Passe -d ''para readevitar isso, mas nesse caso, haverá uma nova linha extra no final devido ao <<<operador. Você pode removê-lo manualmente:

IFS='_' read -r -d '' -a array <<< "$r"
array[$((${#array[@]}-1))]=${array[$((${#array[@]}-1))]%?}

Isso pressupõe $rque não contém caracteres de nova linha ou barras invertidas. Observe também que ele funcionará apenas nas versões recentes do bashshell.
Stéphane Chazelas

@ StéphaneChazelas bom ponto. Sim, este é o caso "básico" de uma sequência. De resto, todos devem procurar sua resposta abrangente. Em relação às versões do bash, read -afoi introduzido no bash 4, certo?
fedorqui 8/09/2015

11
desculpe meu mal, eu pensei que <<<foi adicionado recentemente, bashmas parece que está lá desde 2.05b (2002). read -aé ainda mais velho que isso. <<<vem zshe é suportado por ksh93(e mksh e yash) também, mas read -aé específico do bash (está -Ano ksh93, yash e zsh).
Stéphane Chazelas

@ StéphaneChazelas existe alguma maneira "fácil" de encontrar quando essas mudanças aconteceram? Eu digo "fácil" para não cavar os arquivos de lançamento, talvez uma página mostrando todos eles.
fedorqui 8/09/2015

11
Eu olho para os logs de alterações para isso. O zsh também possui um repositório git com histórico já em 3.1.5 e sua lista de discussão também é usada para rastrear alterações.
Stéphane Chazelas
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.