Globbing e as variáveis ‘LANG’ e ‘LC_’

Globbing e as variáveis 'LANG' e 'LC_'

Uma das coisas que mais podem afetar o resultado de expansões de nomes de arquivos e casamentos de padrões no Bash são as definições regionais de idioma e localização, as chamadas categorias de localidade. Além das diferenças dos alfabetos de cada região, cada categoria de localidade pode tratar de formas diferentes a forma como os caracteres são agrupados, a sequência em que eles aparecem, suas classes de equivalência, etc...

Nos sistemas GNU/Linux, é possível ver quais são as configurações regionais que estamos usando com o comando locale. Aqui no meu Debian Buster, por exemplo, estas seriam as minhas configurações:

:~$ locale
LANG=pt_BR.UTF-8
LANGUAGE=pt_BR:pt:en
LC_CTYPE="pt_BR.UTF-8"
LC_NUMERIC="pt_BR.UTF-8"
LC_TIME="pt_BR.UTF-8"
LC_COLLATE="pt_BR.UTF-8"
LC_MONETARY="pt_BR.UTF-8"
LC_MESSAGES="pt_BR.UTF-8"
LC_PAPER="pt_BR.UTF-8"
LC_NAME="pt_BR.UTF-8"
LC_ADDRESS="pt_BR.UTF-8"
LC_TELEPHONE="pt_BR.UTF-8"
LC_MEASUREMENT="pt_BR.UTF-8"
LC_IDENTIFICATION="pt_BR.UTF-8"
LC_ALL=

NOTA: O utilitário locale não define as variáveis de ambiente relativas à localização! O que ele faz é pegar os valores definidos em LANG e LANGUAGE para deduzir quais configurações regionais seriam aplicadas às demais variáveis de ambiente listadas caso elas não tenham sido definidas.

Como podemos ver na saída do comando, alguns valores são apresentados sem aspas e outros entre aspas. Os valores sem aspas são aqueles que já estão definidos no sistema, ao contrário dos que estão entre aspas, que foram simplesmente deduzidos pelo comando locale para indicar quais são as regras de localização regional atualmente em uso para cada uma dessas variáveis.

Nós podemos verificar a afirmação acima de uma forma muito interessante utilizando a expansão de parâmetros ${NOME:-valor_se_indefinida}:

:~$ echo ${LANG:-indefinida}
pt_BR.UTF-8

:~$ echo ${LANGUAGE:-indefinida}
pt_BR:pt:en

:~$ echo ${LC_CTYPE:-indefinida}
indefinida

:~$ echo ${LC_COLLATE:-indefinida}
indefinida

:~$ echo ${LC_ALL:-indefinida}
indefinida

Como podemos ver nas saídas, apenas LANG e LANGUAGE estão definidas, e isso é bastante comum em sistemas GNU/Linux localizados para regiões que falam o português brasileiro. Outras regiões, com outras codificações de caracteres, ou até sistemas que permitem um grau de customização maior nas suas instalações, podem apresentar definições bem diferentes.

Variáveis de localização que mais afetam o casamento de padrões

Obviamente, todas as variáveis LANG e LC afetam textos e, portanto, afetam casamentos de padrões. Mas, o que queremos aqui neste tópico, são aquelas que podem interferir principalmente com o agrupamento e a ordenação de caracteres. Vejamos então, quais seriam essas variáveis de ambiente:

Variável Descrição
LANG Usada para determinar a categoria da localidade para qualquer categoria não especificada com uma das variáveis iniciadas com LC_.
LC_ALL Sobrescreve o valor de LANG e qualquer outra variável LC_ que especifique uma categoria de localidade.
LC_COLLATE Determina a ordem de agrupamento utilizada quando da classificação dos resultados de uma expansão de nomes de arquivos e determina o comportamento de expressões de faixas, classes de equivalência e sequências de agrupamento numa expansão de nomes de arquivos e casamentos de padrões.
LC_CTYPE Determina a interpretação de caracteres e o comportamento de classes de caracteres em expansões de nomes de arquivos e casamentos de padrões.

Como já dissemos, os sistemas GNU/Linux da nossa região costumam definir apenas o valor da variável LANG, deixando indefinidas (unset) as variáveis LC_ALL, LC_COLLATE e LC_TYPE. Contudo, para a correta execução de scripts e comandos, é possível defini-las ou aletrá-las localmente (no sentido de apenas para a sessão corrente do shell) ou globalmente. Para isso, basta fazer algo como:

# Alterar localmente...
VARIÁVEL=VALOR

# Alterar globalmente...
export VARIÁVEL=VALOR
# Ou...
declare -x VARIÁVEL=VALOR

Utilizando o declare, você pode remover o atributo de exportação da variável utilizando um + no lugar do -:

declare +x VARIÁVEL=VALOR

Mas, se a variável foi definida com o comando export, você pode remover o atributo de exportação com a opção -n:

export -n VARIÁVEL=VALOR

Finalmente, se a variável foi definida localmente, ela será restaurada ao final da sessão, mas você pode utilizar o comando unset para garantir que ela será destruída:

unset VARIÁVEL

A categoria de localidade 'C'

Enquanto categorias de localidades como pt_BR.UTF-8 e fr_FR.UTF-8, por exemplo, trazem definições específicas para a codificação de caracteres de cada idioma e região, existe uma localidade especial bem mais simples e universal -- do ponto de vista da computação -- que é a localidade C.

Na localidade C, cada caractere é formado com apenas um byte, geralmente seguindo o padrão da tabela ASCII, e a sua ordem de classificação é baseada no valor em bytes de cada caractere.

Para entender melhor como as variáveis LC e a localidade C afetam a ordem de classificação, o agrupamento e o casamento de padrões, observe a listagem de diretório abaixo:

:~$ ls
0.txt  5.txt  a.txt  C.txt  f.txt  H.txt
1.txt  6.txt  A.txt  d.txt  F.txt  i.txt
2.txt  7.txt  b.txt  D.txt  g.txt  I.txt
3.txt  8.txt  B.txt  e.txt  G.txt  j.txt
4.txt  9.txt  c.txt  E.txt  h.txt  J.txt

Lendo de cima para baixo e da esquerda para a direita, observe que o shell ordena primeiro os números e depois lista alternadamente os caracteres minúsculos e maiúsculos (aAbBcC...). Como já vimos, estamos utilizando a distribuição Debian com as nossas configurações de localidade do português brasileiro, e a única variável relativa à minha localidade é LANG...

:~$ printenv LANG
pt_BR.UTF-8

Contudo, lembre-se de que a opção do shell globasciiranges está ligada por padrão tanto para o shell interativo (terminal) quanto para as sessões dos scripts. Sabendo que esta opção trabalha com faixas de caracteres e se sobrepõe às definições de localização forçando o agrupamento e o casamento de padrões segundo as definições da localidade C, como ficaria a listagem dos arquivos se desligássemos esta opção?

:~$ shopt -u globasciiranges
:~$ ls
0.txt  5.txt  a.txt  C.txt  f.txt  H.txt
1.txt  6.txt  A.txt  d.txt  F.txt  i.txt
2.txt  7.txt  b.txt  D.txt  g.txt  I.txt
3.txt  8.txt  B.txt  e.txt  G.txt  j.txt
4.txt  9.txt  c.txt  E.txt  h.txt  J.txt

Aparentemente, nada mudou. Mas é só na aparência! Veja o que acontece se tentarmos casar alguns padrões agora...

:~$ ls [a-z]*
a.txt  b.txt  c.txt  d.txt  e.txt  f.txt  g.txt  h.txt  i.txt  j.txt
A.txt  B.txt  C.txt  D.txt  E.txt  F.txt  G.txt  H.txt  I.txt  J.txt

:~$ ls [A-Z]*
A.txt  B.txt  C.txt  D.txt  E.txt  F.txt  G.txt  H.txt  I.txt  J.txt
b.txt  c.txt  d.txt  e.txt  f.txt  g.txt  h.txt  i.txt  j.txt

Com globasciiranges desligado, na hora de expandir as faixas de caracteres [a-z] e [A-Z], o shell não encontrou uma definição de agrupamento além daquela definida pela categoria de localidade definida em LANG que, por sua vez, agrupa as faixas de caracteres alternando maiúsculas e maiúsculas (aABbCc...xXyYzZ). E você pode comprovar este fato observando que, no segundo comando, o arquivo a.txt (com 'a' minúsculo) não foi listado. Agora, imagine a dor de cabeça que isso poderia causar em um bloco case dentro do seu script!

Felizmente, não é comum precisarmos nos preocupar com isso. Mas, se fosse o caso, seria possível brincar com as variáveis LC e a localização C. Por exemplo, ainda com globasciiranges desligado:

:~$ LC_COLLATE=C

:~$ ls [a-z]*
a.txt  b.txt  c.txt  d.txt  e.txt  f.txt  g.txt  h.txt  i.txt  j.txt

:~$ ls [A-Z]*
A.txt  B.txt  C.txt  D.txt  E.txt  F.txt  G.txt  H.txt  I.txt  J.txt

Como vimos na tabela das variáveis, LC_COLLATE "determina o comportamento de expressões de faixas, classes de equivalência e sequências de agrupamento numa expansão de nomes de arquivos e casamentos de padrões". Foi exatamente o que vimos neste último exemplo! Agora, a faixa [a-z] contém apenas minúsculas e a faixa [A-Z] contém apenas maiúsculas, porque LC_COLLATE=C mudou a ordem e a forma de "agrupamento" (collation, daí o nome collate) dos caracteres.

Para verificar este fato, vamos a outro exemplo onde globasciiranges está desligado e nós ainda não definimos LC_COLLATE:

# Nosso diretório...
:~$ ls
a.txt  b.txt  c.txt  d.txt  e.txt
A.txt  B.txt  C.txt  D.txt  E.txt

:~$ for f in *; do echo $f; done
a.txt
A.txt
b.txt
B.txt
c.txt
C.txt
d.txt
D.txt
e.txt
E.txt

# Definindo 'LC_COLLATE'...
:~$ LC_COLLATE=C

:~$ for f in *; do echo $f; done
A.txt
B.txt
C.txt
D.txt
E.txt
a.txt
b.txt
c.txt
d.txt
e.txt

Repare que agora o shell expandiu o coringa * em outra sequência (e outra ordem), agrupando as maiúsculas antes das minúsculas. Por isso, nós podemos utilizar as listas com faixas [a-z] e [A-Z] tranquilamente, pois elas não estão mais entrelaçadas.

NOTA: Este agrupamento também se reflete nos caracteres acentuados, como no exemplo abaixo:

:~$ unset LC_COLLATE
:~$ for f in *; do echo $f; done
a.txt
A.txt
á.txt
b.txt
B.txt
c.txt
C.txt
d.txt
D.txt
e.txt
E.txt
é.txt
í.txt
ó.txt
ú.txt

:$ LC_COLLATE=C
:~$ for f in *; do echo $f; done
A.txt
B.txt
C.txt
D.txt
E.txt
a.txt
b.txt
c.txt
d.txt
e.txt
á.txt
é.txt
í.txt
ó.txt
ú.txt

Casando padrões com classes POSIX

Mas existe outra forma de resolver esse problema sem declarar um valor em LC_COLLATE, ainda com globasciiranges desligado, seria utilizando as classes POSIX [:upper:] e [:lower:]. Elas serão as estrelas de outro tópico, quando falarmos de expressões regulares. Mas, até lá, é bom saber que também podemos utilizá-las para casar padrões dentro das listas ([ ]).

Apenas para ilustrar, vamos dar um exemplo:

:~$ # Primeiro vamos destruir LC_COLLATE...
:~$ unset LC_COLLATE

:~$ ls [[:upper:]]*
A.txt  B.txt  C.txt  D.txt  E.txt  F.txt  G.txt  H.txt  I.txt  J.txt

:~$ ls [[:lower:]]*
a.txt  b.txt  c.txt  d.txt  e.txt  f.txt  g.txt  h.txt  i.txt  j.txt

Ou seja, parafraseando o mítico Dr. Ian Malcolm...

"O shell sempre encontra um caminho."

Mas no tire conclusões apressadas! As classes POSIX só funciona porque elas não representam faixas de caracteres, elas são listas pré-definidas de caracteres. Ou seja, a classe [:lower:], por exemplo, contém apenas caracteres minúsculos, inclusive cedilhados e acentuados. Elas não contam com as definições regionais do ambiente e, justamente por isso, talvez sejam a melhor opção quando queremos casar caracteres que podem estar acentuados.

Uma última observação

Para encerrar este tópico, é importante destacar que globasciiranges e as variáveis LC (especialmente LC_COLLATE) afetam apenas expansões de nomes de arquivos e casamento de padrões. Expansões de intervalos em chaves ({ }), por exemplo, não são afetadas, como podemos ver nos exemplos abaixo, onde globasciiranges está desligado:

:~$ ls
a.txt  A.txt  b.txt  B.txt  c.txt  C.txt  d.txt  D.txt  e.txt  E.txt

:~$ for f in {A..E}; do echo $f.txt; done
A.txt
B.txt
C.txt
D.txt
E.txt

:~$ for f in {a..e}; do echo $f.txt; done
a.txt
b.txt
c.txt
d.txt
e.txt

:~$ ls [a-e].txt
a.txt  A.txt  b.txt  B.txt  c.txt  C.txt  d.txt  D.txt  e.txt

:~$ ls [A-E].txt
A.txt  b.txt  B.txt  c.txt  C.txt  d.txt  D.txt  e.txt  E.txt

O fato é que é muito provável que a opção globasciiranges já esteja habilitada no seu sistema GNU/Linux e que você não tenha que se preocupar com isso na grande maioria das vezes. Mas, no caso de emergências, você já sabe o que fazer.

Músico, programador, designer e apaixonado pelos ideais do Software Livre.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Post comment