Como dividir uma string em várias strings separadas por pelo menos um espaço no shell bash?


224

Eu tenho uma string contendo muitas palavras com pelo menos um espaço entre cada dois. Como posso dividir a sequência em palavras individuais para que eu possa fazer um loop através delas?

A cadeia é passada como argumento. Por exemplo ${2} == "cat cat file". Como posso percorrer isso?

Além disso, como posso verificar se uma string contém espaços?


1
Que tipo de concha? Bash, cmd.exe, PowerShell ...?
Alexey Sviridov 24/09/09

Você só precisa fazer um loop (por exemplo, executar um comando para cada uma das palavras)? Ou você precisa armazenar uma lista de palavras para uso posterior?
DVK

Respostas:


281

Você tentou passar a variável string para um forloop? O Bash, por exemplo, será dividido automaticamente em espaço em branco.

sentence="This is   a sentence."
for word in $sentence
do
    echo $word
done

 

This
is
a
sentence.

1
@MobRule - a única desvantagem disso é que você não pode capturar facilmente (pelo menos não me lembro de jeito nenhum) a saída para processamento adicional. Veja minha solução "tr" abaixo para algo que envia o material para STDOUT
DVK

4
Você poderia simplesmente anexá-lo a uma variável: A=${A}${word}).
Lucas Jones

1
set $ text [isso vai colocar as palavras em US $ 1, $ 2, $ 3 ... etc]
Rajesh

32
Na verdade, esse truque não é apenas uma solução errada, mas também é extremamente perigoso devido ao globbing da concha. touch NOPE; var='* a *'; for a in $var; do echo "[$a]"; donesaídas em [NOPE] [a] [NOPE]vez do esperado [*] [a] [*](LFs substituídos pelo SPC para facilitar a leitura).
Tino

@mob o que devo fazer se quiser dividir a string com base em alguma string específica? exemplo ".xlsx" separador.

296

Eu gosto da conversão para uma matriz, para poder acessar elementos individuais:

sentence="this is a story"
stringarray=($sentence)

agora você pode acessar elementos individuais diretamente (começa com 0):

echo ${stringarray[0]}

ou converta novamente em string para fazer um loop:

for i in "${stringarray[@]}"
do
  :
  # do whatever on $i
done

É claro que o loop direto da string foi respondido anteriormente, mas essa resposta teve a desvantagem de não acompanhar os elementos individuais para uso posterior:

for i in $sentence
do
  :
  # do whatever on $i
done

Consulte também Referência do array bash .


26
Infelizmente não é perfeito, porque de shell-englobamento: touch NOPE; var='* a *'; arr=($var); set | grep ^arr=saídas arr=([0]="NOPE" [1]="a" [2]="NOPE")em vez do esperadoarr=([0]="*" [1]="a" [2]="*")
Tino

@ Tino: se você não quiser que o globbing interfira, basta desligá-lo. A solução também funcionará bem com caracteres curinga. É a melhor abordagem na minha opinião.
Alexandros

3
@Alexandros Minha abordagem é usar apenas padrões, que são seguros por padrão e funcionam perfeitamente em todos os contextos. Um requisito para mudar a aparência do shell para obter uma solução segura é mais do que apenas um caminho muito perigoso, já é o lado sombrio. Portanto, meu conselho é nunca se acostumar a usar padrões como esse aqui, porque mais cedo ou mais tarde você esquecerá alguns detalhes e alguém explorará seu erro. Você pode encontrar provas de tais explorações na imprensa. Cada. Solteiro. Dia.
Tino

86

Basta usar as conchas "set" embutidas. Por exemplo,

definir $ texto

Depois disso, palavras individuais em $ text estarão em $ 1, $ 2, $ 3, etc. Para maior robustez, geralmente

set - texto $ lixo eletrônico
mudança

para lidar com o caso em que $ text esteja vazio ou comece com um traço. Por exemplo:

text = "Este é um teste"
set - texto $ lixo eletrônico
mudança
por palavra; Faz
  eco "[$ word]"
feito

Isso imprime

[Este]
[é]
[uma]
[teste]

5
Essa é uma excelente maneira de dividir o var para que partes individuais possam ser acessadas diretamente. +1; resolveu o meu problema #
Cheekysoft 26/07/11

Eu ia sugerir o uso, awkmas seté muito mais fácil. Agora sou um setfanboy. Obrigado @Idelic!
Yzmir Ramirez

22
Por favor, esteja ciente do globbing do shell se você fizer essas coisas: touch NOPE; var='* a *'; set -- $var; for a; do echo "[$a]"; donesaídas em [NOPE] [a] [NOPE]vez do esperado [*] [a] [*]. Use-o apenas se tiver 101% de certeza de que não há metacaracteres SHELL na string dividida!
Tino

4
@Tino: Esse problema se aplica a todos os lugares, não apenas aqui, mas neste caso, você poderia set -fantes set -- $vare set +fdepois desativar o globbing.
Idelic 14/05/19

3
@ Idelic: Boa captura. Com set -fsua solução também é seguro. Mas set +fé o padrão de cada shell, por isso é um detalhe essencial, que deve ser observado, porque outros provavelmente não o conhecem (como eu também).
Tino

81

A maneira provavelmente mais fácil e segura no BASH 3 e acima é:

var="string    to  split"
read -ra arr <<<"$var"

(onde arré a matriz que pega as partes divididas da sequência) ou, se houver novas linhas na entrada e você desejar mais do que apenas a primeira linha:

var="string    to  split"
read -ra arr -d '' <<<"$var"

(observe o espaço -d '', ele não pode ser deixado de fora), mas isso pode fornecer uma nova linha inesperada de <<<"$var"(como isso implicitamente adiciona um LF no final).

Exemplo:

touch NOPE
var="* a  *"
read -ra arr <<<"$var"
for a in "${arr[@]}"; do echo "[$a]"; done

Produz o esperado

[*]
[a]
[*]

como esta solução (em contraste com todas as soluções anteriores aqui) não é propensa a globbing inesperado e muitas vezes incontrolável do shell.

Além disso, isso fornece todo o poder do IFS, como você provavelmente deseja:

Exemplo:

IFS=: read -ra arr < <(grep "^$USER:" /etc/passwd)
for a in "${arr[@]}"; do echo "[$a]"; done

Produz algo como:

[tino]
[x]
[1000]
[1000]
[Valentin Hilbig]
[/home/tino]
[/bin/bash]

Como você pode ver, os espaços também podem ser preservados dessa maneira:

IFS=: read -ra arr <<<' split  :   this    '
for a in "${arr[@]}"; do echo "[$a]"; done

saídas

[ split  ]
[   this    ]

Observe que a manipulação IFSno BASH é um assunto por si só, assim como seus testes, alguns tópicos interessantes sobre isso:

  • unset IFS: Ignora execuções de SPC, TAB, NL e on-line inicia e termina
  • IFS='': Sem separação de campos, apenas lê tudo
  • IFS=' ': Executa o SPC (e somente o SPC)

Algum último exemplo

var=$'\n\nthis is\n\n\na test\n\n'
IFS=$'\n' read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

saídas

1 [this is]
2 [a test]

enquanto

unset IFS
var=$'\n\nthis is\n\n\na test\n\n'
read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

saídas

1 [this]
2 [is]
3 [a]
4 [test]

BTW:

  • Se você não está acostumado a $'ANSI-ESCAPED-STRING'se acostumar com isso, economiza tempo.

  • Se você não incluir -r(como em read -a arr <<<"$var"), a leitura será escapada pela barra invertida. Isso é deixado como exercício para o leitor.


Para a segunda pergunta:

Para testar algo em uma string em que costumo me ater case, já que isso pode verificar vários casos de uma só vez (nota: case apenas executa a primeira correspondência, se você precisar de instruções de uso de multiplicação case), e essa necessidade costuma ser o caso (trocadilho pretendido):

case "$var" in
'')                empty_var;;                # variable is empty
*' '*)             have_space "$var";;        # have SPC
*[[:space:]]*)     have_whitespace "$var";;   # have whitespaces like TAB
*[^-+.,A-Za-z0-9]*) have_nonalnum "$var";;    # non-alphanum-chars found
*[-+.,]*)          have_punctuation "$var";;  # some punctuation chars found
*)                 default_case "$var";;      # if all above does not match
esac

Portanto, você pode definir o valor de retorno para verificar o SPC assim:

case "$var" in (*' '*) true;; (*) false;; esac

Por que case? Como geralmente é um pouco mais legível que as sequências de expressões regulares, e graças aos metacaracteres da Shell, ele lida com 99% de todas as necessidades muito bem.


2
Essa resposta merece mais upvotes, devido às questões de englobamento destacado, e sua abrangência
Brian Agnew

@brian Obrigado. Observe que você pode usar set -fou set -o noglobalternar entre globbing, de modo que os metacaracteres do shell não causem mais danos nesse contexto. Mas eu realmente não sou amigo disso, pois isso deixa muito poder do shell / é muito propenso a erros para alternar essa configuração.
Tino

2
Resposta maravilhosa, de fato merece mais votos positivos. Nota lateral sobre a queda do case - você pode usar para ;&conseguir isso. Não tenho muita certeza de qual versão do bash apareceu. Eu sou um usuário 4.3
Sergiy Kolodyazhnyy 11/01

2
@Erg Obrigado por notar, pois eu ainda não sabia disso! Então eu procurei, apareceu no Bash4 . ;&é o avanço forçado sem verificação de padrão, como em C. E também existe o ;;&que continua a fazer as verificações adicionais de padrão. Assim ;;é como if ..; then ..; else if ..e ;;&é como if ..; then ..; fi; if .., onde ;&é como m=false; if ..; then ..; m=:; fi; if $m || ..; then ..- nunca se para de aprender (dos outros);)
Tino

@Tino Isso é absolutamente verdade - o aprendizado é um processo contínuo. Na verdade, eu não sabia ;;&antes de você comentar: D Obrigado, e que a concha esteja com você;) #
Sergiy Kolodyazhnyy

43
$ echo "This is   a sentence." | tr -s " " "\012"
This
is
a
sentence.

Para verificar espaços, use grep:

$ echo "This is   a sentence." | grep " " > /dev/null
$ echo $?
0
$ echo "Thisisasentence." | grep " " > /dev/null     
$ echo $?
1

1
Em BASH echo "X" |geralmente pode ser substituído por <<<"X", como este: grep -s " " <<<"This contains SPC". Você pode identificar a diferença se fizer algo semelhante echo X | read varao contrário read var <<< X. Somente o último importa variável varpara o shell atual, enquanto para acessá-lo na primeira variante você deve agrupar assim:echo X | { read var; handle "$var"; }
Tino

17

(A) Para dividir uma frase em suas palavras (separadas por espaço), você pode simplesmente usar o IFS padrão usando

array=( $string )


Exemplo executando o seguinte trecho

#!/bin/bash

sentence="this is the \"sentence\"   'you' want to split"
words=( $sentence )

len="${#words[@]}"
echo "words counted: $len"

printf "%s\n" "${words[@]}" ## print array

irá produzir

words counted: 8
this
is
the
"sentence"
'you'
want
to
split

Como você pode ver, você pode usar aspas simples ou duplas também sem problemas.

Notas:
- isso é basicamente o mesmo da resposta do mob , mas dessa forma você armazena o array para qualquer necessidade adicional. Se você precisar apenas de um loop único, poderá usar a resposta dele, que é uma linha mais curta :)
- consulte esta pergunta para métodos alternativos para dividir uma string com base no delimitador.


(B) Para procurar um caractere em uma string, você também pode usar uma correspondência de expressão regular.
Exemplo para verificar a presença de um caractere de espaço que você pode usar:

regex='\s{1,}'
if [[ "$sentence" =~ $regex ]]
    then
        echo "Space here!";
fi

Para dica de expressão regular (B), a +1, mas -1 para solução incorreta (A), pois isso é propenso a erros de globbing do shell. ;)
Tino

6

Para verificar espaços apenas com o bash:

[[ "$str" = "${str% *}" ]] && echo "no spaces" || echo "has spaces"

1
echo $WORDS | xargs -n1 echo

Isso gera cada palavra; você pode processar essa lista como desejar depois.

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.