Por que [AZ] corresponde a letras minúsculas no bash?

42

Em todos os shells que eu conheço, rm [A-Z]*remove todos os arquivos que começam com uma letra maiúscula, mas com o bash isso remove todos os arquivos que começam com uma letra.

Como esse problema existe no Linux e Solaris com o bash-3 e o bash-4, não pode ser um bug causado por um correspondente de padrão de bugs na libc ou por uma definição de localidade com configuração incorreta.

Esse comportamento estranho e arriscado é intencional ou é apenas um bug que existe sem correção há muitos anos?

esperto
fonte
3
O que produz locale? Não consigo reproduzir isso ( touch foo; echo [A-Z]*gera o padrão literal, não "foo", em um diretório vazio).
chepner
4
Considerando quantas pessoas disseram que funciona para elas ou mostrou exemplos de como LC_COLLATE afeta isso, talvez você possa editar sua pergunta para adicionar uma sessão de amostra do bash que ilustra exatamente o cenário que você está perguntando. Inclua a versão do bash que você está usando.
Kenster
Se você leu todo o texto aqui, saberia qual versão do bash eu uso e o que eu fiz desde que eu já postei a solução na minha pergunta. Deixe-me repetir a solução: o bash não gerencia seu próprio código de idioma para que a configuração LC_COLLATE não mude nada até que você inicie outro processo do bash com o novo ambiente.
schily
1
Consulte também LC_COLLATE (deve) afetar os intervalos de caracteres? (mas essa pergunta não era especificamente sobre bash)
Gilles 'para de ser mau' em
"definir LC_COLLATE não muda nada até você iniciar outro processo bash com o novo ambiente." Isso não corresponde ao comportamento que vejo com o bash-4 no Solaris. Está mudando o comportamento no shell em execução. # echo [A-Z]* ; export LC_COLLATE=C ; echo [A-Z]*A b B z ZABZ
BowlOfRed

Respostas:

67

Observe que, ao usar expressões de intervalo como [az], podem ser incluídas letras do outro caso, dependendo da configuração de LC_COLLATE.

LC_COLLATE é uma variável que determina a ordem de intercalação usada ao classificar os resultados da expansão do nome do caminho e determina o comportamento das expressões de intervalo, classes de equivalência e sequências de intercalação na expansão do nome do caminho e na correspondência de padrões.


Considere o seguinte:

$ touch a A b B c C x X y Y z Z
$ ls
a  A  b  B  c  C  x  X  y  Y  z  Z
$ echo [a-z] # Note the missing uppercase "Z"
a A b B c C x X y Y z
$ echo [A-Z] # Note the missing lowercase "a"
A b B c C x X y Y z Z

Observe que quando o comando echo [a-z]é chamado, a saída esperada seria todos os arquivos com caracteres minúsculos. Além disso, com echo [A-Z], arquivos com caracteres maiúsculos seriam esperados.


Agrupamentos padrão com localidades, como en_USa seguinte ordem:

aAbBcC...xXyYzZ
  • Entre ae z(in [a-z]) são TODAS as letras maiúsculas, exceto Z.
  • Entre Ae Z(in [A-Z]) são TODAS as letras minúsculas, exceto a.

Vejo:

     aAbBcC[...]xXyYzZ
     |              |
from a      to      z

     aAbBcC[...]xXyYzZ
      |              |
from  A     to       Z

Se você alterar a LC_COLLATEvariável para Ca aparência esperada:

$ export LC_COLLATE=C
$ echo [a-z]
a b c x y z
$ echo [A-Z]
A B C X Y Z

Portanto, não é um bug , é um problema de agrupamento .


Em vez de expressões de intervalo, você pode usar classes de caracteres definidas no POSIX , como upperou lower. Eles também funcionam com LC_COLLATEconfigurações diferentes e até com caracteres acentuados :

$ echo [[:lower:]]
a b c x y z à è é
$ echo [[:upper:]]
A B C X Y Z
caos
fonte
Se esse comportamento fosse controlável por variáveis ​​de ambiente LC_ *, não perguntei. Eu trabalho no comitê padrão POSIX e sei como agrupar problemas, por exemplo, trentão foi isso que verifiquei primeiro.
schily
@ Schily Não consigo reproduzir o seu problema com um velho bash-3 ou um bash-4; ambos são controláveis ​​através do LC_COLLATEqual também está documentado no manual.
caos
Desculpe, não posso reproduzir o que você acredita, mas veja minha própria resposta ... A partir das idéias nesta discussão, descobri o motivo do problema.
schily
25

[A-Z]in bashcorresponde a todos os elementos de intercalação (caracteres, mas também podem ser sequências de caracteres, como Dsznas localidades húngaras) que são classificadas após Ae classificadas antes Z. No seu local, cprovavelmente classifica entre B e C.

$ printf '%s\n' A a á b B c C Ç z Z  | sort
a
A
á
b
B
c
C
Ç
z
Z

Então, cou zseria correspondido por [A-Z], mas não ou a.

$ printf '%s\n' A a á b B c C Ç z Z  |
pipe>  bash -c 'while IFS= read -r x; do case $x in [A-Z]) echo "$x"; esac; done'
A
á
b
B
c
C
Ç
z
Z

No código C, o pedido seria:

$ printf '%s\n' A a á b B c C Ç z Z  | LC_COLLATE=C sort
A
B
C
Z
a
b
c
z
Ç
á

Então [A-Z]iria corresponder A, B, C, Z, mas não Çe ainda não .

Se você deseja combinar com letras maiúsculas (em qualquer script), você pode usá-lo [[:upper:]]. Não existe uma maneira incorporada bashde combinar apenas letras maiúsculas no script latino (exceto listando-as individualmente).

Se você quiser combinar com o Ade Z Inglês letras sem diacríticos, você pode usar [A-Z]ou [[:upper:]]mas no Clocal (assumindo que os dados não são codificados em conjuntos de caracteres como BIG5 ou GB18030, que tem vários personagens cujas codificação contém a codificação dessas cartas) ou lista eles individualmente ( [ABCDEFGHIJKLMNOPQRSTUVWXYZ]).

Observe que há alguma variação entre as conchas.

For zsh, bash -O globasciiranges(opção de nome estranho introduzida no bash-4.3), schily-she yash, [A-Z]corresponde aos caracteres cujo ponto de código está entre o de Ae o de Z, portanto seria equivalente ao comportamento do código de bashidioma C.

Para cinzas, mksh e cascas antigas, o mesmo que zshacima, mas limitado a conjuntos de caracteres de byte único. Ou seja, em um código de idioma UTF-8, por exemplo, [É-Ź]não corresponderia Ó, mas, como isso [<c3><89>-<c5><b9>], corresponderia aos valores de bytes 0x89 a 0xc5!

ksh93comporta-se como bashexceto que trata como intervalos de casos especiais cujas extremidades começam com letras minúsculas ou maiúsculas. Nesse caso, ele corresponde apenas aos elementos de intercalação que se classificam entre essas extremidades, mas que são (ou o primeiro caractere para elementos de intercalação de vários caracteres) também em minúsculas (ou maiúsculas, respectivamente). Portanto [A-Z], haveria correspondência em É, mas não em, ecomo ea classificação entre Ae Zmas não é maiúscula como Ae Z.

Para fnmatch()padrões (como em find -name '[A-Z]') ou expressões regulares do sistema (como em grep '[A-Z]'), isso depende do sistema e da localidade. Por exemplo, em um sistema GNU aqui, [A-Z]não corresponde no código xdo en_GB.UTF-8idioma, mas no th_TH.UTF-8. Não está claro para mim quais informações são usadas para determinar isso, mas aparentemente são baseadas em uma tabela de pesquisa derivada dos dados do código de idioma LC_COLLATE ).

Todos os comportamentos são permitidos pelo POSIX, pois o POSIX deixa o comportamento dos intervalos não especificados em códigos de idioma que não sejam o código C. Agora podemos discutir sobre os benefícios de cada abordagem.

bashA abordagem de faz muito sentido [C-G], pois queremos que os personagens entre Ce G. E usar a ordem de classificação do usuário para o que determina o que é intermediário é a abordagem mais lógica.

Agora, o problema é que isso quebra as expectativas de muitas pessoas, especialmente aquelas que estão acostumadas com o comportamento tradicional do pré-Unicode, mesmo nos dias anteriores à internacionalização. Embora, para um usuário normal, faça sentido que [C-I]inclua hcomo a hletra está entre Ce Ie que [A-g]não inclua Z, é uma questão diferente para as pessoas que lidam com o ASCII apenas por décadas.

Esse bashcomportamento também é diferente do [A-Z]intervalo correspondente em outras ferramentas GNU, como nas expressões regulares do GNU (como em grep/ sed...) ou fnmatch()como em find -name.

Isso também significa que o que [A-Z]corresponde varia de acordo com o ambiente, com o sistema operacional e com a versão do sistema operacional. O fato de [A-Z]corresponder Á, mas não Ź também é subótimo.

Para zsh/ yash, usamos uma ordem de classificação diferente. Em vez de confiar na noção de ordem de caracteres do usuário, usamos os valores do código do ponto de caractere. Isso tem o benefício de ser fácil de entender, mas de um ponto prático de poucos, fora do ASCII, não é muito útil. [A-Z]corresponde às 26 letras maiúsculas em inglês dos EUA, [0-9]corresponde aos dígitos decimais. Existem pontos de código no Unicode que seguem a ordem de alguns alfabetos, mas isso não é generalizado e não pode ser generalizado, pois pessoas diferentes que usam o mesmo script não necessariamente concordam com a ordem das letras.

Para shells e mksh tradicionais, traço, está quebrado (agora que a maioria das pessoas usa caracteres de vários bytes), mas principalmente porque ainda não têm suporte para vários bytes. A adição de suporte de vários bytes a shells como bashe zshtem sido um grande esforço e ainda está em andamento. yash(shell japonês) foi projetado inicialmente com suporte a vários bytes desde o início.

A abordagem do ksh93 tem o benefício de ser consistente com as expressões regulares do sistema ou fnmatch () (ou pelo menos parece ser pelo menos nos sistemas GNU). Lá, isso não quebra a expectativa de algumas pessoas, pois [A-Z]não inclui letras minúsculas, [A-Z]inclui É(e Á, mas não Ź). Não é consistente com sortou geralmente strcoll()ordem.

Stéphane Chazelas
fonte
1
Se você estivesse certo, isso poderia ser controlado através de variáveis ​​LC_ *. Parece haver uma razão diferente.
schily 2/09/2015
1
@cuonglm, mais parecido mksh(ambos derivados do pdksh). posh -c $'case Ó in [É-Ź]) echo yes; esac'não retorna nada.
Stéphane Chazelas
2
@ Schily, eu mencionei sortporque os bashglobs são baseados na ordem de classificação dos caracteres. No momento, não tenho acesso a uma versão tão antiga do bash, mas posso verificar mais tarde. Foi diferente então?
Stéphane Chazelas
1
Deixe-me mencionar novamente: zsh, POSIX-ksh88, ksh93t + Bourne Shell, todos se comportam da mesma maneira que eu esperava. O Bash é o único shell que se comporta de maneira diferente e o bash não é controlável via local neste caso.
schily
2
@ Schily, observe que \xFFexiste o byte 0xFF, não o caractere U + 00FF ( ÿele próprio codificado como 0xC3 0xBF). \xFFsozinho não forma um caractere válido, portanto não vejo por que ele deve corresponder [É-Ź].
Stéphane Chazelas
9

Ele foi planejado e documentado na bashdocumentação, seção de correspondência de padrões . A expressão de intervalo [X-Y]incluirá todos os caracteres entre Xe Yusando a sequência de intercalação e o conjunto de caracteres do código de idioma atual:

LC_ALL=en_US.utf8 bash -c 'case b in [A-Z]) echo yes; esac' 
yes

Você pode ver, bclassificado entre Ae Zno en_US.utf8código do idioma.

Você tem algumas opções para evitar esse comportamento:

# Setting LC_ALL or LC_COLLATE to C
LC_ALL=C bash -c 'echo [A-Z]*'

# Or using POSIX character class
LC_ALL=C bash -c 'echo [[:upper:]]*'

ou ativar globasciiranges(com bash 4.3 e acima):

bash -O globasciiranges -c 'echo [A-Z]*'
cuonglm
fonte
6

Eu observei esse comportamento em uma nova instância do Amazon EC2. Como o OP não ofereceu um MCVE , postarei um:

$ cd $(mktemp -d)
$ touch foo
$ echo [A-Z]*     # prepare for a surprise!
foo

$ echo $BASH_VERSION
4.1.2(1)-release
$ uname -a
Linux spinup-tmp12 3.14.27-25.47.amzn1.x86_64 #1 SMP Wed Dec 17 18:36:15 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

$ env | grep LC_  # no locale, let's set one
$ LC_ALL=C
$ echo [A-Z]*
[A-Z]*

$ unset LC_ALL    # ok, good. what if we go back to no locale?
$ echo [A-Z]*
foo

Portanto, não ter meu LC_*conjunto leva o lançamento do bash 4.1.2 (1) no Linux para produzir um comportamento aparentemente estranho. Posso alternar com segurança o comportamento ímpar definindo e desabilitando as respectivas variáveis ​​de localidade. Sem surpresa, esse comportamento parece consistente através da exportação:

$ export LC_ALL=C
$ bash
$ echo [A-Z]*
[A-Z]*
$ exit
$ echo $SHLVL
1
$ unset LC_ALL
$ bash
$ echo [A-Z]*
foo

Enquanto estou vendo o bash se comportar quando Stéphane "Shellshock" Chazelas respondeu , acho que a documentação do bash sobre correspondência de padrões é buggy:

Por exemplo, no código de idioma C padrão , '[a-dx-z]' é equivalente a '[abcdxyz]'

Eu li essa frase (ênfase minha) como "se as variáveis ​​de localidade relevantes não estiverem definidas, o bash será padronizado como a localidade C". Bash não parece estar fazendo isso. Em vez disso, parece estar padronizado para uma localidade em que os caracteres são classificados em ordem de dicionário com dobras diacríticas:

$ echo [A-E]*
[A-E]*
$ echo [A-F]*
foo
$ touch "évocateur"
$ echo [A-F]*
foo évocateur

Eu acho que seria bom para o bash documentar como ele se comportará quando LC_*(especificamente LC_CTYPEe LC_COLLATE) estiver indefinido. Mas enquanto isso, vou compartilhar um pouco de sabedoria :

... você precisa ter muito cuidado com [intervalos de caracteres] porque eles não produzirão os resultados esperados, a menos que sejam configurados corretamente. Por enquanto, você deve evitar usá-los e usar classes de caracteres.

e

Se você é realmente adequado e / ou está criando scripts para um ambiente com várias localidades, provavelmente é melhor garantir que você saiba quais são as suas variáveis ​​de local quando estiver fazendo a correspondência de arquivos ou se está codificando em um maneira completamente genérica.


Atualização Com base no comentário do @ G-Man, vamos analisar mais profundamente o que está acontecendo:

$ env | grep LANG
LANG=en_US.UTF-8

Ah ha! Isso explica o agrupamento visto anteriormente. Vamos remover todas as variáveis ​​de localidade:

$ unset LANG LANGUAGE LC_ALL
$ env | grep 'LC_|LANG'
$ echo [A-Z]*
[A-Z]*

Aqui vamos nós. Agora, o bash opera de forma consistente com relação à documentação neste sistema Linux. Se qualquer uma das variáveis de região são definidos ( LANGUAGE, LANG, LC_COLLATE, LC_CTYPE, LC_ALL, etc.), em seguida, Bash usa aqueles de acordo com o manual. Caso contrário, o bash volta para C.

O FAQ do bash do Wooledge tem o seguinte a dizer:

Em sistemas GNU recentes, as variáveis ​​são usadas nesta ordem. Se LANGUAGE estiver definido, use-o, a menos que LANG esteja definido como C, nesse caso, LANGUAGE será ignorado. Além disso, alguns programas simplesmente não usam LANGUAGE. Caso contrário, se LC_ALL estiver definido, use isso. Caso contrário, se a variável LC_ * específica que cobre esse uso estiver configurada, use isso. (Por exemplo, LC_MESSAGES cobre mensagens de erro.) Caso contrário, use LANG.

Portanto, o aparente problema, tanto na operação quanto na documentação, pode ser explicado analisando a soma total de todas as variáveis ​​de localização do código do idioma.

bispo
fonte
Se nenhuma variável LC_ estiver presente e o bash não se comportar conforme documentado para a Clocalidade, isso é um bug.
schily
1
@ bispo: (1) Digitação: MVCE deve ser MCVE. (2) Se você quiser que seu exemplo seja completo, adicione env | grep LANGou echo "$LANG".
G-Man diz 'Restabelecer Monica
@schily Uma investigação mais aprofundada me convenceu de que não há nenhum erro na documentação ou operação deste sistema Linux.
bispo
@ G-Man Obrigado! Eu esqueci LANG. Com essa dica, tudo é explicado.
bispo
O LANG foi introduzido por volta de 1988 pela Sun para as primeiras tentativas de localização, antes de descobrirem que uma única variável não é suficiente. Hoje ele é usado como fallback e LC_ALL é usado como substituição forçada.
schily
3

A localidade pode alterar quais caracteres são correspondidos [A-Z]. Usar

(LC_ALL=C; rm [A-Z]*)

para eliminar a influência. (Eu usei um subshell para localizar a alteração).

choroba
fonte
Isso não funciona, ele ainda corresponde a todas as cartas
Schily
7
Isso não funcionará porque o glob foi feito antes da execução do rm. Tente export LC_ALL=Cprimeiro.
cuonglm
Desculpe, você não entendeu a pergunta que está relacionada ao bash e não ao rm.
schily
@ Schily: Sim, eu estava errado, você tem que separar as declarações. Verifique a atualização.
choroba 02/09/2015
2

Como já foi dito, esse é um problema de "ordem de classificação".

O intervalo az pode conter letras maiúsculas em alguns locais:

     aAbBcC[...]xXyYzZ
     |              |
from a      to      z

A solução correta desde o bash 4.3 é definir a opção globasciiranges:

shopt -s globasciiranges

para fazer festa de agir como se LC_COLLATE=Cfoi definido no glob faixas ing.


fonte
-6

Parece que encontrei a resposta certa para minha própria pergunta:

O Bash é um buggy, pois não gerencia seu próprio local. Portanto, definir LC_ * em um processo bash não tem efeito nesse processo de shell.

Se você definir LC_COLLATE = C e depois iniciar outro bash, a globbing funcionará conforme o esperado no novo processo do bash.

esperto
fonte
2
Não em nenhuma das minhas festas.
caos
2
Não reproduzo isso em nenhuma versão do bash na minha máquina, parece que você não fez exportisso corretamente.
Chris
Então, você acredita que algo que é exportado corretamente, para afetar um novo processo do bash, não é exportado corretamente?
schily
4
O manuseio do ambiente pelo Solaris é notoriamente deficiente, portanto, não ficaria surpreso se o "bug" no bash fosse a falta de uma solução específica para o Solaris.
hobbs 03/09
1
@schily: Você tem uma citação de onde é necessário alterar as variáveis ​​LC_ * dentro de um shell para fazer com que ele atualize seu próprio estado de localidade? Eu pensaria exatamente o oposto. Em particular, para um shell executando um script, a alteração da localidade no meio da análise / execução do script nem teria um comportamento bem definido, pois o script é um arquivo de texto e "arquivo de texto" é significativo apenas no contexto de um codificação de caracteres únicos.
R ..