Existe algo como o “split ()” do JavaScript no shell?

19

É muito fácil usar split() em JavaScript para dividir 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 fornecer essa string var1_var2_var3 para o script.sh, dentro do script ele converterá a string em uma matriz como

array=( var1 var2 var3 )
for name in ${array[@]}; do
    # some code
done
    
por AGamePlayer 08.09.2015 / 12:01

2 respostas

25

Shells parecidos com Bourne / POSIX têm um operador split + glob e são invocados 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ê invocou por engano quando criou for name in ${array[@]} em vez de for name in "${array[@]}" . (Na verdade, você deve tomar cuidado para invocar esse operador por engano como fonte de muitos bugs e vulnerabilidades de segurança ).

Esse operador é configurado com o parâmetro $IFS special (para informar em quais caracteres dividir (embora tenha cuidado com o espaço, a guia e a nova linha recebam um tratamento especial)) e a opção -f para desativar ( set -f ) ou habilite ( set +f ) a glob part.

Observe também que, embora o S em $IFS tenha sido originalmente (no shell Bourne de onde vem $IFS ) para o eparator S , em shells POSIX, os caracteres em $IFS deve ser visto como delimitadores ou terminadores (veja abaixo um exemplo).

Então, para dividir em _ :

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 , experimente:

string='var1_var2_'

Isso dividirá em var1 e var2 apenas (sem elemento extra vazio).

Assim, para torná-lo semelhante ao split() do JavaScript, você precisaria 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 $string em 1 (não 0 ), como split() do JavaScript).

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

IFS=' '; string=' var1  var2  '

(onde você obtém var1 e var2 ) com

IFS='_'; string='_var1__var2__'

onde você obtém: '' , var1 , '' , var2 , '' .

Observe que o shell zsh não invoca implicitamente o operador split + glob a menos que em sh ou ksh emulação. Lá, você tem que invocá-lo explicitamente. $=string para a parte dividida, $~string para a parte glob ( $=~string para ambos) e também tem um operador split onde você pode especificar o separador:

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

ou para preservar os elementos vazios:

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

Observe que há s para divisão , não delimitando (também com $IFS , uma não conformidade conhecida POSIX de zsh ). É diferente do split() do JavaScript em que uma string vazia é dividida em 0 (não 1).

Uma diferença notável com $IFS -split é que ${(s:abc:)string} divide na string abc , enquanto com IFS=abc , que seria dividido em a , b ou c .

Com zsh e ksh93 , o tratamento especial que o espaço, a guia ou a nova linha receber pode ser removido, dobrando-os em $IFS .

Como uma nota histórica, o shell Bourne (o ancestral ou as atuais estruturas POSIX) sempre despojou os elementos vazios. Ele também tinha 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 split() do JavaScript que pode ser dividido em expressões regulares, você precisaria confiar em utilitários externos.

Na caixa de ferramentas POSIX, awk tem um operador split que pode dividir em expressões regulares estendidas (elas são mais ou menos um subconjunto das expressões regulares semelhantes a Perl suportadas por 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 shell zsh tem suporte embutido para expressões regulares compatíveis com Perl (em seu zsh/pcre module), mas usá-lo para dividir uma string, embora possível, é relativamente incômodo.

    
por 08.09.2015 / 12:15
7

Sim, use IFS e defina como _ . Em seguida, use read -a para armazenar em uma matriz ( -r desativa a expansão de barra invertida). Note que isto é específico para o bash; O ksh e o zsh têm características similares com uma sintaxe ligeiramente diferente, e o sh simples não tem 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

De man bash :

read

-a aname

The words are assigned to sequential indices of the array variable aname, starting at 0. aname is unset before any new values are assigned. Other name arguments are ignored.

IFS

The Internal Field Separator that is used for word splitting after expansion and to split lines into words with the read builtin command. The default value is ''''.

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

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