Usando substituição de parâmetro em uma matriz Bash

8

Eu tenho file.txt que eu preciso ler em uma matriz Bash. Preciso remover espaços, aspas duplas e quase a primeira vírgula em cada entrada . Aqui está o quão longe eu cheguei:

$ cat file.txt
10,this
2 0 , i s
30,"all"
40,I
50,n,e,e,d,2
60",s e,e"

$ cat script.sh
#!/bin/bash
readarray -t ARRAY<$1
ARRAY=( "${ARRAY[@]// /}" )
ARRAY=( "${ARRAY[@]//\"/}" )
for ELEMENT in "${ARRAY[@]}";do
    echo "|ELEMENT|$ELEMENT|"
done

$ ./script.sh file.txt
|ELEMENT|10,this|
|ELEMENT|20,is|
|ELEMENT|30,all|
|ELEMENT|40,I|
|ELEMENT|50,n,e,e,d,2|
|ELEMENT|60,se,e|

O que funciona muito bem, exceto pela situação de vírgula. Estou ciente de que existem várias maneiras de criar a pele desse gato, mas devido ao script maior do qual faz parte, eu realmente gostaria de usar a substituição de parâmetro para chegar até aqui:

|ELEMENT|10,this|
|ELEMENT|20,is|
|ELEMENT|30,all|
|ELEMENT|40,I|
|ELEMENT|50,need2|
|ELEMENT|60,see|

Isso é possível através da substituição de parâmetros?

Jon Red
fonte
3
Existe algum motivo para você manter o texto em uma matriz e por que você não pode permitir, por exemplo, awkou sedprocessar o dado?
Kusalananda
@ Jeff - Fazer um loop sobre o array será um pesadelo para implementar no script maior em que estou trabalhando.
Jon Red
3
@JonRed Eu não sei o que você está fazendo, por isso é perfeitamente possível que você não tenha escolha, mas geralmente, quando você se encontra fazendo acrobacias de cadeia complexa no shell, isso é uma indicação muito boa de que você deve estar usando uma linguagem de programação real. O shell não foi projetado como uma linguagem de programação e, embora possa ser usado como um, realmente não é uma boa idéia para coisas mais complexas. Peço fortemente que você considere mudar para perl, python ou qualquer outra linguagem de script.
terdon
@terdon É engraçado, acabei de dizer quase exatamente a mesma coisa ao meu colega antes de ler este post. Eu basicamente disse que esta é a versão final deste script e que quaisquer outros requisitos precisarão ser reescritos em Perl. Então sim, eu definitivamente concordo
Jon Red

Respostas:

9

Eu removeria o que você precisa remover sed antes de carregar na matriz (observe também os nomes das variáveis ​​em minúsculas, em geral é melhor evitar variáveis ​​em maiúsculas nos scripts de shell):

#!/bin/bash
readarray -t array< <(sed 's/"//g; s/  *//g; s/,/"/; s/,//g; s/"/,/' "$1")
for element in "${array[@]}";do
    echo "|ELEMENT|$element|"
done

Isso produz a seguinte saída no seu arquivo de exemplo:

$ foo.sh file 
|ELEMENT|10,this|
|ELEMENT|20,is|
|ELEMENT|30,all|
|ELEMENT|40,I|
|ELEMENT|50,need2|
|ELEMENT|60,see|

Se você realmente deve usar a substituição de parâmetro, tente algo como isto:

#!/bin/bash
readarray -t array< "$1"
array=( "${array[@]// /}" )
array=( "${array[@]//\"/}" )
array=( "${array[@]/,/\"}" )
array=( "${array[@]//,/}" )
array=( "${array[@]/\"/,}" )

for element in "${array[@]}"; do
    echo "|ELEMENT|$element|"
done
terdon
fonte
1
@ JonRed Adicionei uma versão com substituição de parâmetro, mas é complexa, complicada e feia. Fazer esse tipo de coisa no shell raramente é uma boa ideia.
terdon
1
Observe que, se você removeu os espaços e as aspas duplas, esses caracteres estarão disponíveis para uso em vez do seu RANDOMTEXTTHATWILLNEVERBEINTHEFILE.
Kusalananda
1
@ Kusalananda sim, acabei de ler sua resposta. Deveria ter pensado nisso! Obrigado :)
terdon
Responde diretamente à pergunta, ilustra por que minha solução preferida não é ideal e fornece a alternativa mais viável. Você ganha, melhor resposta.
Jon Red
10

Tanto quanto posso ver, não há necessidade de lê-lo em uma bashmatriz para criar essa saída:

$ sed 's/[ "]//g; s/,/ /; s/,//g; s/ /,/; s/.*/|ELEMENT|&|/' <file
|ELEMENT|10,this|
|ELEMENT|20,is|
|ELEMENT|30,all|
|ELEMENT|40,I|
|ELEMENT|50,need2|
|ELEMENT|60,see|

A sedexpressão exclui espaços e aspas duplas, substitui a primeira vírgula por um espaço (não há outros espaços na cadeia neste momento), exclui todas as outras vírgulas, restaura a primeira vírgula e precede e acrescenta os dados extras.

Como alternativa, com o GNU sed:

sed 's/[ "]//g; s/,//2g; s/.*/|ELEMENT|&|/' <file

(o padrão sednão suporta a combinação de 2e gcomo sinalizadores no scomando).

Kusalananda
fonte
1
com GNU sed, você pode usar 's/,//2gpara remover vírgulas, começando com o 2º
Glenn Jackman
2
E, os últimos 2 s /// comandos podem ser, s/.*/|ELEMENT|&|/mas isso pode ser mais esforço para sed.
Glenn Jackman
1
@glennjackman Possivelmente, mas parece bastante arrumado.
Kusalananda
Sim, isso faz parte de um script maior. A matriz é necessária, não apenas para a saída. Daí o meu interesse na substituição de parâmetros. Eu poderia fazer um loop na matriz com isso, mas isso será um pesadelo para implementar. A Terndon forneceu uma solução sem loop usando o sed, que eu provavelmente recorrerei se a substituição de parâmetros não for possível.
26619 Jon Red
Se não estivesse vinculado ao uso de uma matriz, essa seria a melhor solução.
Jon Red
9
ELEMENT='50,n,e,e,d,2'
IFS=, read -r first rest <<<"$ELEMENT"
printf "%s,%s\n" "$first" "${rest//,/}"
50,need2

Saia do hábito de usar nomes de variáveis ​​ALLCAPS. Você acabará colidindo com uma variável "sistema" crucial como PATH e quebrará seu código.

Glenn Jackman
fonte
Não é substituição de parâmetro. MAS, eu não sabia que os nomes de variáveis ​​do ALLCAPS eram um mau hábito no Bash. Você faz um bom argumento, que um pesquisador superficial confirma definitivamente. Obrigado por melhorar meu estilo! :)
Jon Red
1
Respondi a perguntas em que a pessoa escreveu PATH=something; ls $PATHe depois me perguntei sobre o ls: command not founderro.
Glenn Jackman
1
Existem quase cem variáveis ​​internas nomeadas em todas as maiúsculas (clique neste link da página de manual ) para ver ...
Jeff Schaller
8

[Esta é essencialmente uma versão mais desenvolvida da resposta de glenn jackmann ]

Construindo uma matriz associativa a partir da chave e do valor retirados, usando a primeira vírgula como separador:

declare -A arr
while IFS=, read -r k v; do arr["${k//[ \"]}"]="${v//[ ,\"]}"; done < file.txt
for k in "${!arr[@]}"; do 
  printf '|ELEMENT|%s,%s|\n' "$k" "${arr[$k]}"
done
|ELEMENT|20,is|
|ELEMENT|10,this|
|ELEMENT|50,need2|
|ELEMENT|40,I|
|ELEMENT|60,see|
|ELEMENT|30,all|
chave de aço
fonte
6

Você pode fazer um loop sobre a matriz e usar uma variável intermediária:

for((i=0; i < "${#ARRAY[@]}"; i++))
do
  rest="${ARRAY[i]#*,}"
  ARRAY[i]="${ARRAY[i]%%,*}","${rest//,/}"
done

Isso atribui à restparte após a primeira vírgula; concatenamos três partes novamente na variável original:

  • a parte antes da primeira vírgula
  • uma vírgula
  • a substituição restde cada vírgula com nada
Jeff Schaller
fonte
Este foi o meu primeiro pensamento e é bastante simples para o exemplo, mas isso faz parte de um script maior, em que a matriz é massiva e já existem loops e isso seria uma coisa toda. Definitivamente isso funcionaria, mas seria muito complicado de implementar no projeto maior em que estou trabalhando.
Jon Red
1
Justo; Eu apenas tentei responder dentro das limitações (somente expansão de parâmetros).
Jeff Schaller