在Mac OS X上出现RE错误:非法字节序列。

264

我正在尝试在Mac OS X上替换Makefile中的字符串,以便进行iOS交叉编译。该字符串包含嵌入式双引号。命令如下:

sed -i "" 's|"iphoneos-cross","llvm-gcc:-O3|"iphoneos-cross","clang:-Os|g' Configure

错误提示:

sed: RE error: illegal byte sequence

我尝试过转义双引号、逗号、破折号和冒号,但都没有成功。例如:

sed -i "" 's|\"iphoneos-cross\"\,\"llvm-gcc\:\-O3|\"iphoneos-cross\"\,\"clang\:\-Os|g' Configure

我正在艰难地调试问题,有人知道如何让sed打印非法字节序列的位置吗?或者有人知道非法字节序列是什么吗?


4
非法的字节序列听起来像是当你给期望UTF-8的东西输入8位ASCII时出现的问题。 - Klas Lindbäck
75
你能试试这个命令吗:LC_CTYPE=C && LANG=C && sed 命令 - anubhava
11
谢谢大家。问题出在"LANG"这个东西上。叹气... - jww
7
BSD的sed(在OS X上也使用)需要-i''(单独的空字符串选项参数)才能进行原地更新而不创建备份文件;而对于GNU的sed,仅使用-i本身就可以实现 - 参见https://dev59.com/3pzha4cB1Zd3GeqPKMZr#40777793。 - mklement0
6
支持 LANG 这个东西。天哪,这太晦涩难懂了,而且令人惊讶的难以研究。 - Spudley
显示剩余3条评论
8个回答

393

一个展示这种症状的样例命令:sed 's/./@/' <<<$'\xfc' 失败了,因为字节 0xfc 不是有效的 UTF-8 字符。
注意,相比之下,GNUsed(Linux,但也可安装在 macOS 上)只是简单地将无效字节通过,而不报告错误。

如果你不介意失去对真实区域设置的支持,使用 曾经被接受的答案 的方法是一种选择(如果你使用的是美国系统,而且你永远不需要处理外国字符,那么这可能没问题)。

然而,同样的效果可以通过一个 临时 的方法来实现,只针对一个单独的命令

LC_ALL=C sed -i "" 's|"iphoneos-cross","llvm-gcc:-O3|"iphoneos-cross","clang:-Os|g' Configure

注意:重要的是设置一个有效的LC_CTYPE,所以LC_CTYPE=C sed...通常也会起作用,但如果LC_ALL被设置(而不是C),它将覆盖个别的LC_*类别变量,例如LC_CTYPE。因此,最健壮的方法是设置LC_ALL。
然而,将LC_CTYPE有效地设置为C会把字符串视为每个字节都是自己的字符(不执行基于编码规则的解释),而不考虑OS X默认使用的多字节按需UTF-8编码,其中外来字符具有多字节编码。
简而言之:将LC_CTYPE设置为C会导致shell和实用程序仅将基本英文字母识别为字母(在7位ASCII范围内的字母),因此外文字符将不被视为字母,例如大小写转换将失败。

如果您不需要匹配多字节编码字符,例如é,并且只想通过这样的字符,则可能可以接受这种情况。

如果这不足够,或者您想要了解原始错误的原因(包括确定哪些输入字节引起了问题)并且按需执行编码转换,请继续阅读下文。


问题在于输入文件的编码与shell不匹配。
更具体地说,输入文件包含以UTF-8格式无效的字符编码(如@Klas Lindbäck在评论中所述) - 这就是sed错误消息通过“无效字节序列”试图表达的内容。

很可能,您的输入文件使用单字节8位编码,例如ISO-8859-1,常用于编码“西欧”语言。

示例:

重音字母à的Unicode代码点为0xE0(224) - 与ISO-8859-1相同。但是,由于UTF-8编码的特性,这个单一代码点被表示为2个字节 - 0xC3 0xA0,而尝试传递单个字节0xE0在UTF-8下是无效的。

这是一个问题的演示,使用字符串voilà编码为ISO-8859-1,其中à表示为一个字节(通过使用\x{e0}创建字节的ANSI-C引用bash字符串($'...')):

请注意,sed命令实际上是一个无操作命令,只是将输入传递,但我们需要它来引发错误:

  # -> 'illegal byte sequence': byte 0xE0 is not a valid char.
sed 's/.*/&/' <<<$'voil\x{e0}'

为了简单地忽略这个问题,可以使用上述的 LCTYPE=C 方法:
  # No error, bytes are passed through ('á' will render as '?', though).
LC_CTYPE=C sed 's/.*/&/' <<<$'voil\x{e0}'

如果您想确定输入的哪些部分引起了问题,请尝试以下方法:
  # Convert bytes in the 8-bit range (high bit set) to hex. representation.
  # -> 'voil\x{e0}'
iconv -f ASCII --byte-subst='\x{%02x}' <<<$'voil\x{e0}'

该输出将以十六进制形式显示所有具有高位设置的字节(即超出7位ASCII范围的字节)。 (但请注意,这也包括正确编码的UTF-8多字节序列 - 针对无效的UTF-8字节,需要更复杂的方法才能进行识别。)

按需执行编码转换:

可以使用标准实用程序iconv将编码转换为(-t)和/或从(-f)编码; iconv -l列出了所有支持的编码。

示例:

ISO-8859-1转换为当前shell中生效的编码(基于默认情况下LC_CTYPEUTF-8),以下是在上面示例的基础上构建的:

  # Converts to UTF-8; output renders correctly as 'voilà'
sed 's/.*/&/' <<<"$(iconv -f ISO-8859-1 <<<$'voil\x{e0}')"

请注意,这种转换使您能够正确匹配外文字符:
  # Correctly matches 'à' and replaces it with 'ü': -> 'voilü'
sed 's/à/ü/' <<<"$(iconv -f ISO-8859-1 <<<$'voil\x{e0}')"

在处理后将输入转换回ISO-8859-1,只需将结果传输到另一个iconv命令即可:

sed 's/à/ü/' <<<"$(iconv -f ISO-8859-1 <<<$'voil\x{e0}')" | iconv -t ISO-8859-1

6
我认为这是一个更好的选择。首先,我不想失去终端中的多语言支持。其次,被接受的答案感觉像是一个全局解决方案来解决一个局部问题——这是需要避免的。 - Alex
我对此进行了一些小的调整。我希望得到反馈。https://dev59.com/C2Ik5IYBdhLWcg3we-H5#35046218 - Heath Borders
在 Sierra 上,LC_CTYPE=C sed 's/.*/&/' <<<$'voil\x{e0}' 会输出 sed: RE error: illegal byte sequenceecho $LC_ALL 输出 en_US.UTF-8 - ahcox
4
是的,因为设置LC_ALL会_覆盖_所有其他的LC_*变量,包括LC_CTYPE,就像答案中所解释的那样。 - mklement0
3
@mklement0 很棒,这个命令有效:"LC_ALL=C sed 's/.*/&/' <<<$'voil\x{e0}'"。这里解释了优先级,方便我这些粗心的无知者理解:http://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html - ahcox
显示剩余10条评论

181
将以下代码添加到您的~/.bash_profile~/.zshrc文件中。
export LC_CTYPE=C 
export LANG=C

34
确实有效,但您能否解释原因? - Hoang Pham
17
LC_CTYPE设置为C会使字符串中的每个字节都成为自己的字符,而不应用任何编码规则。由于违反(UTF-8)编码规则导致了原始问题,这样做可以解决问题。但是,你需要付出的代价是,shell和实用程序只会将基本的英文字母(7位ASCII范围内的字母)识别为字母。详情请参见我的回答。 - mklement0
10
将此设置永久化到您的 shell 启动文件中会禁用许多有用的行为。您应该仅在确实需要时将其应用于单个命令。 - tripleee
8
太危险了,可能会引发意想不到的后果。可以使用“LC_CTYPE=C sed ...”,即仅在sed命令上进行操作。 - Yongwei Wu
5
这将完全禁用您的Shell对Unicode字符的支持。再见表情符号、花式线条符号和带重音字母等等。最好只针对sed命令进行设置,如其他回答所述。 - asmeurer
显示剩余2条评论

24

我的解决方案是使用Perl:

find . -type f -print0 | xargs -0 perl -pi -e 's/was/now/g'

4
这个很好用。与其他工具不同,我没有遇到任何转义特殊字符的错误。之前的工具给了我问题,比如“sed:RE 错误:非法字节序列”或“sed:1:“path_to_file”:无效的命令代码”。 - JMags1632
2
简单而且不需要任何配置等。简直太棒了。 - Thanos

5

您只需要在sed命令之前使用iconv命令即可。

例如,使用file.txt输入:

iconv -f ISO-8859-1 -t UTF8-MAC file.txt | sed 's/something/àéèêçùû/g' | ......

-f选项是“from”字符集,而-t选项是“to”字符集转换。

请注意大小写,网页通常显示为小写,如< charset=iso-8859-1"/>,而iconv使用大写。 您可以使用iconv -l命令列出您的系统支持的iconv编码集列表。

UTF8-MAC是用于转换的现代Mac操作系统字符集。


1
请参阅iconv邮件列表上的iconv和字符集名称 - jww

4

mklement0的答案非常好,但我有一些小改动。

当使用iconv时,显式指定bash编码似乎是一个好主意。此外,我们应该在前面添加字节顺序标记(尽管unicode标准不推荐使用),因为UTF-8和ASCII之间可能存在合法的混淆而没有字节顺序标记。不幸的是,iconv在你显式指定字节序(UTF-16BEUTF-16LE)时不会添加字节顺序标记,所以我们需要使用平台特定的字节序来使用UTF-16,然后使用file --mime-encoding来发现真实的字节序iconv使用。

(我将所有编码都大写,因为当您使用iconv -l列出所有支持的编码时,它们都是大写的。)

# Find out MY_FILE's encoding
# We'll convert back to this at the end
FILE_ENCODING="$( file --brief --mime-encoding MY_FILE )"
# Find out bash's encoding, with which we should encode
# MY_FILE so sed doesn't fail with 
# sed: RE error: illegal byte sequence
BASH_ENCODING="$( locale charmap | tr [:lower:] [:upper:] )"
# Convert to UTF-16 (unknown endianness) so iconv ensures
# we have a byte-order mark
iconv -f "$FILE_ENCODING" -t UTF-16 MY_FILE > MY_FILE.utf16_encoding
# Whether we're using UTF-16BE or UTF-16LE
UTF16_ENCODING="$( file --brief --mime-encoding MY_FILE.utf16_encoding )"
# Now we can use MY_FILE.bash_encoding with sed
iconv -f "$UTF16_ENCODING" -t "$BASH_ENCODING" MY_FILE.utf16_encoding > MY_FILE.bash_encoding
# sed!
sed 's/.*/&/' MY_FILE.bash_encoding > MY_FILE_SEDDED.bash_encoding
# now convert MY_FILE_SEDDED.bash_encoding back to its original encoding
iconv -f "$BASH_ENCODING" -t "$FILE_ENCODING" MY_FILE_SEDDED.bash_encoding > MY_FILE_SEDDED
# Now MY_FILE_SEDDED has been processed by sed, and is in the same encoding as MY_FILE

1
++ 对于有用的技巧,特别是使用 file -b --mime-encoding 来发现和报告文件的编码方式。然而,还有一些值得注意的方面,我将在单独的评论中进行说明。 - mklement0
3
可以说,Unix世界已经广泛采用UTF-8编码了:默认的“LC_CTYPE”值通常是“<lang_region>.UTF-8”,因此任何没有BOM(字节序标记)的文件都会被解释为UTF-8文件。只有在Windows世界中才使用伪BOM“0xef 0xbb 0xff”;根据定义,UTF-8不需要BOM并且不建议使用(正如您所述);在Windows之外的世界,这个伪BOM会导致一些问题。 - mklement0
3
很抱歉,iconv在明确指定字节序(UTF-16BE或UTF-16LE)时不会添加字节顺序标记(BOM),这是有意为之的设计。如果您明确指定了字节序,就不需要通过BOM再次反映它,因此没有添加。 - mklement0
2
关于 LC_* / LANG 变量:bashkshzsh(可能还有其他的,但不包括 dash)确实会考虑字符编码;在具有基于 UTF-8 的语言环境的 POSIX 类似 shell 中进行验证,使用 v='ä'; echo "${#v}" 命令:一个支持 UTF-8 的 shell 应该报告 1;也就是说,它应该将多字节序列 ä0xc3 0xa4)识别为一个单一字符。然而,更重要的是:标准工具(如 sedawkcut 等)也需要了解语言环境/编码,并且尽管现代类 Unix 平台上的大多数工具都支持,但也存在例外,例如 OSX 上的 awk 和 Linux 上的 cut - mklement0
2
file 识别 UTF-8 伪 BOM 是值得赞扬的,但问题是,大多数处理文件的 Unix 工具都不识别并且通常在遇到时会破坏或至少表现不正常。没有 BOM,file 可以正确地将全 7 位字节文件识别为 ASCII,并将具有有效 UTF-8 多字节字符的文件识别为 UTF-8。UTF-8 的美妙之处在于它是 ASCII 的 _超集_:任何有效的 ASCII 文件都是有效的 UTF-8 文件(反之则不然);把 ASCII 文件视为 UTF-8 是完全安全的(从技术上讲,它实际上就是 UTF-8,只是恰好不包含多字节字符)。 - mklement0
显示剩余3条评论

2
有人知道如何让sed打印出非法字节序列的位置吗?或者有人知道非法字节序列是什么吗?
$ uname -a
Darwin Adams-iMac 18.7.0 Darwin Kernel Version 18.7.0: Tue Aug 20 16:57:14 PDT 2019; root:xnu-4903.271.2~2/RELEASE_X86_64 x86_64

通过使用 tr,我已经解决了上面的一部分问题。

我有一个 .csv 文件,是信用卡账单,我正在尝试将其导入 Gnucash。由于我在瑞士,所以我必须处理像苏黎世这样的词汇。怀疑 Gnucash 不喜欢数值字段中的 " ",我决定简单地替换所有的 " "。

; ;

使用

;;

Here goes:

$ head -3 Auswertungen.csv | tail -1 | sed -e 's/; ;/;;/g'
sed: RE error: illegal byte sequence

我使用od来揭示一些信息:注意这个od -c输出中间的374。
$ head -3 Auswertungen.csv | tail -1 | od -c
0000000    1   6   8   7       9   6   1   9       7   1   2   2   ;   5
0000020    4   6   8       8   7   X   X       X   X   X   X       2   6
0000040    6   0   ;   M   Y       N   A   M   E       I   S   X   ;   1
0000060    4   .   0   2   .   2   0   1   9   ;   9   5   5   2       -
0000100        M   i   t   a   r   b   e   i   t   e   r   r   e   s   t
0000120                Z 374   r   i   c   h                            
0000140    C   H   E   ;   R   e   s   t   a   u   r   a   n   t   s   ,
0000160        B   a   r   s   ;   6   .   2   0   ;   C   H   F   ;    
0000200    ;   C   H   F   ;   6   .   2   0   ;       ;   1   5   .   0
0000220    2   .   2   0   1   9  \n                                    
0000227

然后我想尝试说服 tr 将374替换为正确的字节码。所以我首先尝试了一些简单的东西,但没有成功,但有一个副作用,就是告诉我哪个字节有问题:

$ head -3 Auswertungen.csv | tail -1 | tr . .  ; echo
tr: Illegal byte sequence
1687 9619 7122;5468 87XX XXXX 2660;MY NAME ISX;14.02.2019;9552 - Mitarbeiterrest   Z

您可以在第374个字符处看到 tr bails。

使用perl似乎可以避免这个问题。

$ head -3 Auswertungen.csv | tail -1 | perl -pne 's/; ;/;;/g'
1687 9619 7122;5468 87XX XXXX 2660;ADAM NEALIS;14.02.2019;9552 - Mitarbeiterrest   Z?rich       CHE;Restaurants, Bars;6.20;CHF;;CHF;6.20;;15.02.2019

1

我的解决方法是使用gnu的sed。对于我的目的来说,效果很好。


1
确实,如果您想在输入流中忽略无效字节(无需使用“LC_ALL=C sed…”解决方法),那么_GNU_ sed是一个选项,因为GNU sed只是将无效字节_直接传递_而不报告错误。但请注意,如果您想正确识别和处理输入字符串中的所有字符,则必须先更改输入的编码(通常使用iconv)。 - mklement0

-1
对我来说,这个问题是由于命令试图打开/编辑 .DS_Store 文件所导致的。删除这些文件解决了我的问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接