Elmord's Magic Valley

Computers, languages, and computer languages. Às vezes em Português, sometimes in English.

Reading and splitting

2012-12-31 13:39 -0200. Tags: comp, prog, bash, em-portugues

Durante os últimos 354 anos eu estive splitando linhas em bash assim:

line="root:x:0:0:root:/root:/bin/bash"

IFS=":"
set -- $line
echo "$1"               # root
echo "$7"               # /bin/bash

# ou:
IFS=":"
fields=($line)
echo "${fields[0]}"     # root
echo "${fields[6]}"     # /bin/bash

Turns out que faz 354 anos que eu estou fazendo isso errado.

Acontece que expansão de parâmetros (i.e., $var) fora de aspas duplas não só causa word splitting (que é o que queremos), mas também causa pathname expansion (substituição de *, ?, ~ e afins pelos nomes de arquivo que casam com o padrão):

cd /
line='a*:b*:c*'
IFS=":"
fields=($line)

echo "${fields[0]}"     # a*
echo "${fields[1]}"     # bin
echo "${fields[2]}"     # boot
echo "${fields[3]}"     # c*

Esse problema nunca me ocorreu na prática, mas me dei conta disso depois de escrever o post sobre manipulação de strings em bash. O pior é que eu já sabia há muito tempo que variáveis fora das aspas sofrem pathname expansion, mas acho que adquiri o hábito de splitar strings assim antes de saber disso. Uma solução correta é usar o comando IFS=delimitadores read -a arrayname (que lê uma linha com campos separados por delimitadores e coloca os pedaços na variável arrayname) em conjunto com o operador <<<string (que alimenta a stdin do comando com a string):

line='a*:b*:c*'
IFS=":" read -a fields <<<"$line"

echo "${fields[0]}"     # a*
echo "${fields[1]}"     # b*
echo "${fields[2]}"     # c*

De brinde agora é possível incluir : em um campo usando a seqüência \:, já que o read entende o \ como um indicador de que o próximo caractere deve ser interpretado literalmente (a menos que a opção -r (de raw) seja usada).

O último exemplo de splitting do post anterior (que procura o nome do shell de um usuário no /etc/passwd) ficaria assim:

#!/bin/bash
user="$1"

IFS=":"
while read -a campos line; do
    if [ "${campos[0]}" = "$user" ]; then
        echo "${campos[6]}"
    fi
done </etc/passwd

Outra feature interessante do read é a opção -d, que permite especificar um caractere diferente de \n como terminador de linha. Isso é útil, por exemplo, em conjunto com o find -print0, que imprime os nomes dos arquivos separados pelo caractere \0 (ASCII NUL) ao invés de \n:

find . -name '*.bkp' -print0 | while IFS= read -d $'\0' file; do
    echo "Arquivo $file será renomeado."
    mv "$file" "${file%.bkp}~"
done

Isso garante que o script vai funcionar mesmo que o nome de algum arquivo contenha quebras de linha, o que faz com que o script não seja vulnerável ao usuário malandro que criar um arquivo chamado foo\n/bin/bash\nbar.bkp em seu home.

Comentários / Comments (4)

Cara, 2013-01-01 22:25:51 -0200 #

Pelo menos são só os últimos 354 anos =D Imagina se tivessem sido todos os teus 2k anos?


Eric Ambiel, 2017-09-12 19:06:03 -0300 #

Olá Vitor.

Estou criando um scrip em ash para Busybox, que esta em um dispositivo embarcado, mas ele não fornece suporte para array, existe algum outro jeito de eu separar as palavras de uma string sem usar array. Minha ideia é ler um arquivo com algum texto, separar as palavras e criar pastas com essas palavras.

Obrigado.


Vítor De Araújo, 2017-09-12 22:23:42 -0300 #

@Eric: Se você tem o texto em questão separado por espaços numa variável, se você não colocar espaços em volta da $variável ao usá-la, o shell vai splitar o valor da variável. Por exemplo:

texto="foo bar baz"
mkdir $texto

vai chamar "mkdir foo bar baz", e criar os três diretórios.

Resta o problema de como pegar o texto do arquivo, mas isso você pode fazer, por exemplo, pegando a saída do cat:

mkdir $(cat arquivo-com-palavras.txt)

Ou então, se quiser fazer algo mais complexo com cada arquivo, pode fazer um 'for':

for palavra in $(cat arquivo-com-palavras.txt); do
mkdir $palavra
...
done

O único problema com essa abordagem é que, se alguma palavra contiver '*', '?' ou [], o shell pode acabar expandindo o nome, como explicado neste post. Há maneiras de contornar isso, mas não sei se para o seu caso vale a pena o trabalho.

Qualquer coisa estamos aí. :)


Eric Ambiel, 2017-09-13 08:01:53 -0300 #

Ola Vitor, bom dia.

Muito obrigado pela resposta.

Vou tentar e ver se funciona, retorno assim que implementar.

Abraços.


Deixe um comentário / Leave a comment

Main menu

Recent posts

Recent comments

Tags

em-portugues (213) comp (148) prog (71) in-english (62) life (49) unix (38) pldesign (37) lang (32) random (28) about (28) mind (26) lisp (25) fenius (22) mundane (22) web (20) ramble (18) img (13) rant (12) hel (12) scheme (10) privacy (10) freedom (8) esperanto (7) music (7) lash (7) bash (7) academia (7) copyright (7) home (6) mestrado (6) shell (6) android (5) conlang (5) misc (5) emacs (5) latex (4) editor (4) etymology (4) php (4) worldly (4) book (4) politics (4) network (3) c (3) tour-de-scheme (3) security (3) kbd (3) film (3) wrong (3) cook (2) treta (2) poem (2) physics (2) x11 (2) audio (2) comic (2) lows (2) llvm (2) wm (2) philosophy (2) perl (1) wayland (1) ai (1) german (1) en-esperanto (1) golang (1) translation (1) kindle (1) pointless (1) old-chinese (1)

Elsewhere

Quod vide


Copyright © 2010-2024 Vítor De Araújo
O conteúdo deste blog, a menos que de outra forma especificado, pode ser utilizado segundo os termos da licença Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International.

Powered by Blognir.