Como dividir uma string em um array no bash

6

Eu tenho um problema com a saída de um programa. Eu preciso lançar um comando no bash e pegar sua saída (uma string) e dividi-la para adicionar novas linhas em determinados lugares. A string é assim:

battery.charge: 90 battery.charge.low: 30 battery.runtime: 3690 battery.voltage: 230.0 device.mfr: MGE UPS SYSTEMS device.model: Pulsar Evolution 500

basicamente, é um valor xxx.yy.zz: , mas o valor pode conter espaços. Aqui está a saída que eu gostaria de obter

battery.charge: 90
battery.charge.low: 30
battery.runtime: 3690
battery.voltage: 230.0
device.mfr: MGE UPS SYSTEMS
device.model: Pulsar Evolution 500 

Eu tenho uma idéia para procurar primeiro ponto e, em seguida, olhar para trás a partir dessa posição de espaço para colocar uma nova linha lá, mas não sei como conseguir isso no Bash. Eu ainda sou um iniciante.

    
por user67524 13.05.2014 / 13:33

6 respostas

2

Solução bash pura, sem ferramentas externas usadas para processar as strings, apenas expansão de parâmetros:

#! /bin/bash
str='battery.charge: 90 battery.charge.low: 30 battery.runtime: 3690 battery.voltage: 230.0 device.mfr: MGE UPS SYSTEMS device.model: Pulsar Evolution 500'

IFS=: read -a fields <<< "$str"

for (( i=0 ; i < ${#fields[@]} ; i++ )) ; do
    f=${fields[i]}

    notfirst=$(( i>0 ))
    last=$(( i+1 == ${#fields[@]} ))

    (( notfirst )) && echo -n ${f% *}

    start=('' $'\n' ' ')
    colon=('' ': ')
    echo -n "${start[notfirst + last]}${f##* }${colon[!last]}"
done
echo

Explicação: $notfirst e $last são booleanos. A parte antes do último espaço ${f% *} não é impressa para o primeiro campo, pois não existe tal coisa. $start e $colon possuem várias strings que separam os campos: no primeiro item, notfirst + last é 0, então nada é prefixado, para o resto das linhas, $notfirst é 1, então uma nova linha é impressa, e para a última linha, a adição dá 2, então um espaço é impresso. Em seguida, a parte após o último espaço é impressa em ${f##* } . O cólon é impresso para todas as linhas, exceto a última.

    
por 13.05.2014 / 13:58
3

Uma solução perl :

$ perl -pe 's{\S+:}{$seen++ ? "\n$&" : "$&"}ge' file
battery.charge: 90 
battery.charge.low: 30 
battery.runtime: 3690 
battery.voltage: 230.0 
device.mfr: MGE UPS SYSTEMS 
device.model: Pulsar Evolution 500

Explicação

  • \S+: corresponde ao final da string com : .
  • Com todas as strings correspondidas, inserimos a nova linha antes delas ("\n$&") , exceto a primeira ($seen++) .
por 13.05.2014 / 16:16
3

Com o GNU sed, você pode combinar cada sequência contígua (ou seja, sem espaço em branco) terminada por : e, em seguida, colocar uma nova linha antes de todas, exceto a primeira:

sed 's/[^[:space:]]\+:/\n&/g2'

Se a sua versão do sed não suportar a extensão gn , você poderá usar um modificador g simples

sed 's/[^[:space:]]\{1,\}:/\
&/g'

que funcionará da mesma forma, exceto pela impressão de uma nova linha adicional antes da primeira chave. Você poderia usar perl -pe 's/\S+:/\n$&/g' com a mesma condição (pode haver um equivalente em perl do sed g2 do GNU, mas eu não sei).

    
por 13.05.2014 / 14:35
2

É mais fácil usar uma ferramenta que suporta lookarounds:

$ s="battery.charge: 90 battery.charge.low: 30 battery.runtime: 3690 battery.voltage: 230.0 device.mfr: MGE UPS SYSTEMS device.model: Pulsar Evolution 500"
$ grep -oP '\S+:\s+.*?(?=\s+\S+:|$)' <<< "$s"
battery.charge: 90
battery.charge.low: 30
battery.runtime: 3690
battery.voltage: 230.0
device.mfr: MGE UPS SYSTEMS
device.model: Pulsar Evolution 500

Se você quisesse o resultado em uma matriz:

$ IFS=$'\n' foo=($(grep -oP '\S+:\s+.*?(?=\s+\S+:|$)' <<< "$s"))
$ for i in "${!foo[@]}"; do echo "$i<==>${foo[i]}"; done
0<==>battery.charge: 90
1<==>battery.charge.low: 30
2<==>battery.runtime: 3690
3<==>battery.voltage: 230.0
4<==>device.mfr: MGE UPS SYSTEMS
5<==>device.model: Pulsar Evolution 500

EDIT: Explicação do regex:

'\S+:\s+.*?(?=\s+\S+:|$)'
  • \S+ corresponde a um ou mais caracteres que não são espaços em branco
  • : corresponde a :
  • \s+ corresponde a um ou mais espaços após o :
  • .*? denota uma correspondência não gananciosa
  • (?=\s+\S+:|$) é uma afirmação antecipada para determinar se existe:
    • um ou mais espaços seguido por string (charaters que não são espaços em branco) e dois-pontos ou
    • fim da string

Portanto, a string é dividida em partes como battery.charge: 90 , ... device.mfr: MGE UPS SYSTEMS , ...

Abaixo estão os links para alguns analisadores de expressões regulares on-line:

por 13.05.2014 / 14:38
1

Aqui está uma abordagem ingênua que deve funcionar supondo que você não se importa com as abas e as novas linhas na entrada (se houver) convertidas em espaços simples.

A ideia é simples: dividir a entrada no espaço em branco e imprimir todos os tokens, exceto que você prefixa os tokens que terminam com : com uma nova linha (e adiciona novamente um espaço à frente dos outros). A variável $count e if relacionados são úteis apenas para evitar uma linha vazia inicial. Poderia ser removido se isso não é um problema. (O script assume que a entrada está em um arquivo chamado intput no diretório atual.)

#! /bin/bash

count=0
for i in $(<input) ; do
   fmt=
   if [[ $i =~ :$ ]] ; then
       if [[ $count -gt 0 ]] ; then
           fmt="\n%s"
       else
           fmt="%s"
       fi
       ((count++))
   else
       fmt=" %s"
   fi
   printf "$fmt" "$i"
done
echo
echo "Num items: $count"

Espero que alguém possa encontrar uma alternativa melhor.

$ cat input
battery.charge: 90 battery.charge.low: 30 battery.runtime: 3690 battery.voltage: 230.0 device.mfr: MGE UPS SYSTEMS device.model: Pulsar Evolution 500
$ ./t.sh
battery.charge: 90
battery.charge.low: 30
battery.runtime: 3690
battery.voltage: 230.0
device.mfr: MGE UPS SYSTEMS
device.model: Pulsar Evolution 500
Num items: 6
    
por 13.05.2014 / 14:28
0

Você pode usar o awk (1) com o seguinte script split.awk:

BEGIN { RS=" "; first=1; }
first { first=0; printf "%s", $1; next; }
/[a-z]+\.[^:]+:/ { printf "\n%s", $1; next; }
{ printf " %s", $1 }
END { printf "\n" }

Quando você corre

awk -f split.awk input.dat

você receberá

battery.charge: 90
battery.charge.low: 30
battery.runtime: 3690
battery.voltage: 230.0
device.mfr: MGE UPS SYSTEMS
device.model: Pulsar Evolution 500

A idéia é deixar o awk dividir a entrada quando ele vê um espaço (configurando o separador de registro RS na linha 1). Em seguida, corresponde aos valores de xxx.yy.zz: nas linhas 2 e 3 (distinguindo a primeira correspondência das subsequentes), enquanto a linha 4 corresponde sempre que a linha 2 e 3 não coincidem. A linha 5 apenas imprime a última nova linha.

    
por 13.05.2014 / 14:39