Dividir lista separada por vírgula ignorando vírgulas dentro de {} correspondência

1

Eu quero dividir um csv, mas ignorar qualquer vírgula dentro de grupos de chaves correspondentes e percorrer cada um dos membros da lista. O código abaixo funciona muito bem, mas não considera vírgulas nos grupos de chaves.

Suposições:

  • Sempre haverá pares de chaves. Ou seja, entradas como {{ {a,b,c}, x não ocorrerão.

Saída esperada:

Word='{0,1}'
Word='alpha'
Word='{(x,y,z)}'
Word='{{1,2,3}, {a,b,c}}'

Referências:

Código:

#!/bin/bash

#TEST_STRING="alpha, beta, gamma" ## <--- works great for simple case
TEST_STRING="{0,1}, alpha, {(x,y,z)}, {{1,2,3}, {a,b,c}}"

echo "${TEST_STRING}" | sed -n 1'p' | tr ',' '\n' | while read Extracted_Word; do
    printf "Word='%s'\n" "${Extracted_Word}"
done

Eu tentei adaptar a solução do 123 (agora excluída):

#!/bin/bash

#TEST_STRING="alpha, beta, gamma" ## <--- works great for simple case
TEST_STRING="{0,1}, alpha, {(x,y,z)}, {{1,2,3}, {a,b,c}}"

echo "${TEST_STRING}" \
    | sed -n 1'p' \
    | sed 's/\({[^}]*\({[^}]*}[^}]*\)*} *\)\(,\|$\) */\n/g;:1;s/\(\n[^{}]*\), */\n/;t1' \
    | tr ',' '\n' \
    | while read Extracted_Word; do
    printf "Word='%s'\n" "${Extracted_Word}"
done

mas isso produz a seguinte mensagem de erro para mim:

./testcsv.sh
sed: 1: "s/\({[^}]*\({[^}]*}[^}] ...": bad flag in substitute command: ':'
./testcsv.sh: line 18: {{ {a,b,c}, x: command not found
    
por Peter Grill 02.02.2016 / 08:39

4 respostas

3

Experimente uma bash

pura
#!/bin/bash
TEST_STRING="{0,1}, alpha, {(x,y,z)}, {{1,2,3}, {a,b,c}}"
TEST_STRING="$TEST_STRING"","
count=0
newword=''
while [ "${TEST_STRING::1}" ] ; do 
    l="${TEST_STRING::1}"
    TEST_STRING=${TEST_STRING:1}
    [ "$l" = '{' ] && ((count++))
    [ "$l" = '}' ] && ((count--))
    if [ "$l" = ',' ] && ! ((count)) ; then
        echo "Word='$newword'"
        newword=''
    else
        if [ "$newword" ] || [ "$l" != " " ] ; then
            newword="$newword""$l"
        fi
    fi
done
    
por 02.02.2016 / 09:47
2

Aqui está um script sed que dividirá seu exemplo:

#!/bin/sed -Ef

# replace all commas with newlines
s/,/\
/g

# Do we need to re-join any lines?
:loop
# Unmatched brace containing possibly another (matched) level of
# braces:
s/(\{([^{}]|\{[^{}]*\})*)\
/,/
tloop

# remove any leading space
s/\n */\
/g

# At first line, print result, then exit.
1q

Advertência: ele só irá lidar com dois níveis de chaves (de acordo com os comentários da pergunta).

Testes:

$ ./259252.sed <<<'{0,1}, alpha, {(x,y,z)}, {{1,2,3}, {a,b,c}}'
{0,1}
alpha
{(x,y,z)}
{{1,2,3}, {a,b,c}}

E para mostrar que sai depois que a primeira linha é processada:

$ ./259252.sed <<<$'a,b,c\nd,e,f'
a
b
c

Eu estou rodando isso no Linux, e usando as respostas para Diferenças entre sed no Mac OSX e outro “padrão” sed para portá-lo para o MacOS . Se isso não funcionar, então esta resposta sugere que você pode instalar o GNU sed com brew install gnu-sed e usar gsed em vez de sed para invocá-lo.

Em uso:

#!/bin/bash

TEST_STRING="{0,1}, alpha, {(x,y,z)}, {{1,2,3}, {a,b,c}}"

echo "${TEST_STRING}" | sed -E -f 259252.sed | while read Extracted_Word; do
    printf "Word='%s'\n" "${Extracted_Word}"
done

que dá:

Word='{0,1}'
Word='alpha'
Word='{(x,y,z)}'
Word='{{1,2,3}, {a,b,c}}'
    
por 02.02.2016 / 11:00
1
str='{0,1},alpha,{(x,y,z)},{{1,2,3},{a,b,c}}'
OPTIND=1 l=0 r=0; set ""
while   getopts : na -"$str"
do      [ "$l" -gt "$r" ]
        case    $?$OPTARG  in
        (1,)  ! l=0 r=0    ;;
        (0})    r=$((r+1)) ;;
        (?{)    l=$((l+1)) ;;
        esac    &&
        set -- "$@$OPTARG" ||
        set -- "$@" ""
done;   printf  %s\n "$@"

dash tem um bug que requer algo como:

set -- "$@" ""; str=${str#?}

... mas além disso, o que foi dito acima deve ser bem rápido, e funcionar basicamente em qualquer shell POSIX, além de ser bastante simples. Ele também deve lidar com pares incompatíveis (mesmo que você não precise) , deixando de reconhecer especialmente um } que ocorre antes de um { .

{0,1}
alpha
{(x,y,z)}
{{1,2,3},{a,b,c}}

Para obter sua string prefixada e as aspas ao redor, você pode substituir o seguinte ...

printf "Word='%s'\n" "$@"

... para o printf %s\n "$@" usado acima. Dado o valor de exemplo de $str aqui, seria impresso:

Word='{0,1}'
Word='alpha'
Word='{(x,y,z)}'
Word='{{1,2,3},{a,b,c}}'

Mais solidamente você pode fazer ...

for W do alias "Word=$W" Word; done

... que renderizaria ...

Word='{0,1}'
Word=alpha
Word='{(x,y,z)}'
Word='{{1,2,3},{a,b,c}}'

... citado conforme necessário, e citaria corretamente aspas fixas bem (embora, se estiver usando um bash , você pode querer fazer set --posix primeiro) .

E assim, por uma questão de demonstração ...

str="{0,1

}}, {,}alph}'a, {(x,y,z)}, {{1,2,3}, {a,b,c}}" 
OPTIND=1 l=0 r=0; set ""
while   getopts : na -"$str"
do      [ "$l" -gt "$r" ]
        case    $?$OPTARG  in
        (1,)  ! l=0 r=0    ;;
        (0})    r=$((r+1)) ;;
        (?{)    l=$((l+1)) ;;
        esac    &&
        set -- "$@$OPTARG" ||
        set -- "$@" ""
done;   for W do alias "Word=${W# }" Word
done
Word='{0,1

}}'
Word='{,}alph}'\''a'
Word='{(x,y,z)}'
Word='{{1,2,3}, {a,b,c}}'

... onde até mesmo os espaços principais são tratados de forma simples ...

    
por 03.02.2016 / 11:51
0

Uma solução bash adicional:

  • Ele manipulará pares de chaves incomparáveis { .
  • Não aceita uma chave de fechamento até que uma ou mais chaves de abertura apareçam.
  • Redefinirá a contagem de chaves para 0 no final da linha.
  • Aceitará uma vírgula como válida depois de mais chaves de fechamento do que de chaves abertas.
  • Removerá um espaço na frente da solução.
  • Irá citar a palavra resultante.

Código:

str="}}{0,1}}, {,}alph}'a"

            fin='false' d='0'
until  $fin
do     IFS=   read -r -d '' -n 1 a || fin='true'
       if     [[ $a == '{' ]] ; then (( d++ )) ; fi ### count openning braces.
       if     [[ $a == ',' ]] && (( d<1 )) || $fin  ### ',' out of braces or end.
       then   $fin && s="${s%$'\n'}"                ### removing a last newline.
              set -- "$@" "$s"                      ### store in an array.
              unset a s d                           ### unset working variables.
       fi
       if [[ $a == '}' ]] && ((d>0)); then ((d--)); fi  ### close braces.
       s="$s$a"
done <<<"$str"
printf 'Word=%q\n' "${@# }"       ### print a quoted value removing front space.

Saída:

Word=\}\}\{0\,1\}\}
Word=\{\,\}alph\}\'a

Ou um pouco mais enigmático:

str="{0,1

}}, {,}alph}'a, {(x,y,z)}, {{1,2,3}, {a,b,c}}"

        fin='false' d='0'
until  $fin
do     IFS=   read -r -d '' -n 1 a || fin='true'
       [[ $a == '{' ]] && (( d++ ))                 ### count openning braces.
       [[ $a == ',' ]] && (( d<1 )) || $fin && {    ### ',' no braces (or end).
              $fin && s="${s%$'\n'}"                ### removing a last newline.
              set -- "$@" "$s"                      ### store in an array.
              unset a s d                           ### unset working variables.
       }
       [[ $a == '}' ]] && (( d>0 )) && ((d--))      ### substract closing braces.
       s="$s$a"
done <<<"$str"
printf 'Word=%q\n' "${@# }"    ### print a quoted value with front space removed.

Resultado:

Word=$'{0,1\n\n}}'
Word=\{\,\}alph\}\'a
Word=\{\(x\,y\,z\)\}
Word=\{\{1\,2\,3\}\,\ \{a\,b\,c\}\}
    
por 12.02.2016 / 09:47