在 Bash 中通配重音文件

Globbing accented files in Bash

我正在尝试验证 Bash 中是否存在文件。我知道文件名(在变量中)但不知道扩展名(可以是 .pmdl.umdl)。

在 OSX 上有效:

$> ls
ecole.pmdl
$> filename="ecole"
$> ls "$filename."[pu]mdl
ecole.pmdl

但当文件名包含重音符号时则不会:

$> ls
école.pmdl
$> filename="école"
$> ls "$filename."[pu]mdl
ls: école.[pu]mdl: No such file or directory

但是,如果我不使用 globbing,它会起作用:

$> ls "$filename."pmdl
école.pmdl

我正在寻找一种适用于 Linux 和 OSX 的简单解决方案。 This is the closest question 我找到了那个主题。

编辑:

$> bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin16)
Copyright (C) 2007 Free Software Foundation, Inc.

编辑 2:

证明场景失败的简短版本(系统地)在 OSX Bash v3.2.57 上使用相同的 é 字符。 Linux Bash 4.3.30 上的相同场景系统地工作(找到)。

$> touch é.txt
$> ls é*
ls: é*: No such file or directory

é != é

$ echo "école." | xxd 
00000000: c3a9 636f 6c65 0a                        ..cole.

$ echo "école." | xxd
00000000: 65cc 8163 6f6c 650a                      e..cole.

因此我们可以看出它们是不同的字符:

$ echo -e "\x65\xCC\x81"
é
$ echo -e "\xC3\xA9"
é

You are not using the same character in your filename as set in your variable.

for i in {1..3}; do f="école"; ls "$f."[pu]mdl; echo "$i: $f."[pu]mdl; done
for i in {1..3}; do f="école"; ls "$f."[pu]mdl; echo "$i: $f."[pu]mdl; done
ls: école.[pu]mdl: No such file or directory
1: école.[pu]mdl
ls: école.[pu]mdl: No such file or directory
2: école.[pu]mdl
ls: école.[pu]mdl: No such file or directory
3: école.[pu]mdl
école.pmdl
1: école.[pu]mdl
école.pmdl
2: école.[pu]mdl
école.pmdl
3: école.[pu]mdl

这个错误可能很难重现,因为将字符从一个地方复制粘贴到另一个地方可能会被编辑器翻译,shell,等等,完全改变它。看似相同的角色,却在看似无法区分的细节上有着天壤之别。

这是HFS的要求+here and here (Apple filesystem) to store Unicode strings in decomposed form (as opposed to a pre-composed character).

然后Unicode码位U+0E9的é这样的字符被分解成两个个字符e´ 的 Unicode 代码位置分别为 U+065 和 U+0301。

您可以通过创建一个干净的空目录并执行以下操作来查看此差异:

$ a='é'
$ echo "$a" >.text
$ touch "$a"
$ ls > .list

然后比较这两个命令的输出:

$ od -vAn -tx1c .text
  c3  a9  0a
 303 251  \n

$ od -vAn -tx1c .list
  65  cc  81  0a
   e 314 201  \n

不相等。

您可以尝试在您的系统中使用此模式:

ls "e$(echo -e '\xcc\x81')cole".[pu]mdl

这只是é在文件系统中由两个字符表示的表达式。

了解到此问题已在较新的 bash 版本中得到解决。

参考:

How to enter special characters so that bash terminal understands them

tl;dr

  • 任一:使用以下解决方法之一

    • ls "$(iconv -t UTF-8-MAC <<<'école')."[pu]mdl - 最通用,但很麻烦。
    • ls $'e\x{cc}\x{81}cole'.[pu]mdl - 很难记住,并且特定于手头的变音符号(尖音,´)。
    • ls e?cole.[pu]mdl - 易于输入和记忆,但仅限于 1 个组合变音符号,可能会产生误报。
  • 或者:通过 Homebrew 安装 Bash 4.3.30 或更高版本 并使用它代替 Bash 3.x macOS 仍然自带:brew install bash.

血淋淋的细节如下。


相对于 非 ASCII 字符,

  • macOS 文件系统HFS+,仅[=235] =] NFD分解Unicode规范化形式),其中重音字母2 个或更多 Unicode 代码点 表示: 基本字母 ,后跟 组合变音符号(重音符号):

    • é的情况下:
      • ASCII基字母-eU+0065,UTF-8编码0x65
      • 后跟[=​​235=]组合尖音符(前一个基本字母上方的´U+0301, UTF-8 编码 0xcc 0x81).
    • 一些重音字符分解为一个基本字母,后跟 多个 组合变音符号,例如 .
    • 请注意,当 创建 文件并匹配文件名 字面意思 时,文件系统接受 NFC 字符串(见下一点),并自动将它们转换为他们的 NFD 等价物(分解它们)。
    • 顺便说一句:Linus Torvalds 是 HFS+ 的一般批评家,尤其是它对 NFD 的使用,如 this article.
    • 中所述
  • 通常,但是 - 例如当您在终端或大多数编辑器中键入字符时 - NFC ( composed Unicode规范化形式)被使用,其中(习惯)重音字母表示为1 Unicode 代码点:

    • é单个Unicode字符U+00E9,UTF-8编码0xc3 0xa9.
    • NFD 和 NFC 应该 被视为 等效 ,但从 Bash 3.x - 在 macOS 上发现 -​​ 不是NFC(以及 NFD)输入 原样globbing(在终端中输入或大多数编辑器在 UTF-8 编码脚本中保存)并匹配它 根据文件系统的 NFD 表示逐个代码点,不识别等效的 NFC 和 NFD 表示。
      实际上,这意味着 在终端中键入或由大多数编辑器生成的带重音的 NFC 字符与 HFS+ 文件系统中的 NFD 等效字符不匹配
    • 相比之下,指定 literal 文件名 - without globbing - 不受影响:ls école,表示为 NFC,确实找到存储在 NFD 中的名为 école 的文件 - 大概是因为 Bash 只是将 NFC 表示传递给 system 函数,该函数确实识别等价。

了解这些 Unicode 正常(规范化)形式 here

简而言之:Bash 应该 将 NFD 和 NFC 表示视为等效,但从 macOS 10.12 的过时版本开始.1 附带 - Bash 3.2.57.

虽然问题至少从 Bash 4.3.30 when 运行 on macOS 开始得到修复,Apple 还没有t 更新到 Bash 4.x 版本 licensing 原因(参见下面的解决方案)。

查看此 post 的底部,了解 Linux 世界。


解决方法 用于在 macOS 上使用带重音字符的 globbing 文件名:

  • [如果可行] 使用Homebrew, 安装最新的4.x Bash 版本 并使用它代替 macOS 附带的那个:brew install bash.

    • 请注意,如果您使用这样的 Bash 版本 (>= 4.3.30),不仅下面描述的其他解决方法不再 必要 ,它们实际上停止工作,因为 Bash 然后仅支持 NFC 输入作为 globbing 模式的一部分(但将其正确映射到文件系统中的 NFD 等效项).
  • [稳健,但更精细] 使用iconv -t UTF-8-MAC转换你的Bash 从 NFC 到 NFD 的字符串文字,以便它与文件系统表示相匹配:
    ls "$(iconv -t UTF-8-MAC <<<'école')."[pu]mdl

    • 或者,也可以使用 ANSI C-quoted string 来表示确切的 NFD UTF-8 字节序列,但晦涩难懂:
      ls $'e\x{cc}\x{81}cole'.[pu]mdl
  • [更简单,但次优]将每个重音字符表示为<base-char>?,因为从Bash的角度来看,文件系统报告的重音字符等于基本字符 e 后跟 另一个 字符(组合变音符号;相应地调整 multiple 结合变音符号)。 (这种方法显然不是最优的,因为它不会匹配 just é,而是 any 以 [= 开头的双字符序列16=]):
    ls e?cole.[pu]mdl


ext 文件系统 被许多 Linux 发行版 存储文件名 完全符合规定:

换句话说:使用 NFC 名称创建的文件将按原样存储,就像使用 NFD 名称的文件一样。

因此,ext 考虑 NFC 和 NFD 不同的形式,因为它们的字节级表示不同,所以它甚至允许(概念上)相同名称的文件仅在 Unicode 标准形式上不同 - 例如,名为 $'e\xcc\x81cole'$'\xc3\xa9cole' 的文件在由 ls (école) 打印时无法区分,但它们是不同的文件 (!).

因此 - 并且适当地 - Linux 上的 Bash 版本 识别 NFC / NFD 等效,即使版本 >= 4.3.30(与 macOS 不同)。

警告dash,它在 Ubuntu 上充当 /bin/sh,例如,从 Ubuntu 16.04 开始不识别区域设置(多字节字符编码识别),至少在 globbing 时:globbing 符号 ? 匹配单个 byte而不是单个 字符(由活动语言环境的字符编码定义,反映在语言环境类别 LC_CTYPE 中,通常为 UTF-8)。因此,为了匹配单个非ASCII字符,您需要知道该字符的UTF-8编码由多少字节组成,并为每个字节使用?;例如,NFC é(2 个字节)必须与 ??.[1]

匹配

当您在 shebang 行为 #!/bin/sh 的脚本中使用 globbing 时,这可能很重要。

在实践中,很少遇到 NFD 字符串,因此 NFC 字符串既用于创建文件又用于稍后通过 glob 匹配它们,macOS 遇到的不同 Unicode 规范形式的问题很少出现在 Linux。


[1] dash 旨在成为一个快速的、POSIX 兼容的 shell 实现(主要是 受限 到 POSIX 特征),但在这种情况下它似乎不足:part of the POSIX spec. describing the pattern-matching notation 清楚地谈论 个字符 ,而不是 个字节 : A <question-mark> is a pattern that shall match any character.
Character Sets.

部分描述了对多字节字符编码的支持