我不清楚在C的argv
中使用了哪些编码。 特别是,我对以下情况感兴趣:
N
包含非ASCII字符的文件 P在命令行上看到什么字节序列?
我观察到,在Linux上,在UTF-8语言环境中创build文件名,然后在(例如) zw_TW.big5
语言环境中对其进行zw_TW.big5
似乎会导致我的程序P被送入UTF-8而不是Big5
。 但是,在OS X上,相同的一系列操作会导致我的程序P获取Big5
编码的文件名。
以下是我认为目前为止(长时间,我可能是错的,需要纠正):
文件名以一些Unicode格式存储在磁盘上。 所以Windows取名为N
,从L1(当前代码页)转换为N
的Unicode版本,我们将调用N1
,并将N1
存储在磁盘上。
我后来假设的是,当后面的选项卡完成时,名称N1
被转换为区域L2(新的当前代码页)以供显示。 幸运的是,这将产生原来的名字N
– 但是如果N
包含在L2中不可表示的字符,这将是不正确的。 我们称之为新名字N2
。
当用户实际按下回车键以运行该参数时,名称N2
被转换回Unicode,再次产生N1
。 这个N1
现在可以通过GetCommandLineW
/ wmain
/ tmain
以UCS2格式提供给程序,但GetCommandLine
/ main
用户将在当前语言环境(代码页)中看到名字N2
。
就我所知,磁盘存储的故事是一样的。 OS X将文件名称存储为Unicode。
对于Unicodeterminal,我认为terminal会在Unicode缓冲区中build立命令行。 因此,当您完成选项卡时,它会将该文件名作为Unicode文件名复制到该缓冲区。
运行该命令时,该Unicode缓冲区将转换为当前语言环境L2,并通过argv
传送给程序,程序可以将当前语言环境的argv解码为Unicode以显示。
在Linux上,一切都不一样,我对发生的事情感到非常困惑。 Linux将文件名存储为字节string ,而不是Unicode。 所以,如果在locale L1中创build一个名称为N
的文件, N
作为字节string是存储在磁盘上的文件。
当我以后运行terminal并尝试并完成名称时,我不确定会发生什么情况。 它在我看来像命令行被构造为一个字节缓冲区,文件的名称作为一个字节string被串联到该缓冲区。 我假定当你input一个标准字符时,它会被dynamic地编码成附加到该缓冲区的字节。
当你运行一个程序时,我认为这个缓冲区是直接发送给argv
。 现在, argv
有什么编码? 它看起来像在命令行中键入的任何字符,而在语言环境中L2将采用L2编码,但文件名将采用L1编码 。 所以argv
包含两个编码的混合!
如果有人能让我知道这里发生了什么,我真的很喜欢它。 我现在所拥有的仅仅是猜测和猜测,并不是真的合在一起。 我真正想要的是在当前代码页(Windows)或当前的语言环境(Linux / OS X)中编码argv
,但似乎并不是这种情况。
这是一个简单的候选程序P,可以让你观察自己的编码:
#include <stdio.h> int main(int argc, char **argv) { if (argc < 2) { printf("Not enough arguments\n"); return 1; } int len = 0; for (char *c = argv[1]; *c; c++, len++) { printf("%d ", (int)(*c)); } printf("\nLength: %d\n", len); return 0; }
您可以使用locale -a
查看可用的语言环境,并使用export LC_ALL=my_encoding
更改您的语言环境。
感谢大家的回应。 我已经学到了很多关于这个问题的知识,并且发现了以下解决了我的问题的事情:
如上所述,在Windows上,argv使用当前代码页进行编码。 但是,您可以使用GetCommandLineW以UTF-16格式检索命令行。 对于支持unicode的现代Windows应用程序,建议不要使用argv,因为不推荐使用代码页。
在Unix上,argv没有固定的编码:
a)通过tab-completion / globbing插入的文件名将在argv 逐字中出现,就像它们在磁盘上被命名的字节序列一样。 即使这些字节序列在当前语言环境中没有意义,情况也是如此。
b)用户使用IME直接输入的输入将出现在区域编码的argv中。 (Ubuntu似乎使用LOCALE来决定如何编码IME输入,而OS X使用Terminal.app编码首选项。)
这对于像Python,Haskell或Java这样的语言来说是烦人的,它们想把命令行参数当作字符串处理。 他们需要决定如何将argv
解码为内部使用的String
(这些语言是UTF-16)。 但是,如果他们只是使用语言环境编码进行解码,则输入中的有效文件名可能无法解码,从而导致异常。
Python 3采用的解决方案是代理字节编码方案( http://www.python.org/dev/peps/pep-0383/ ),它将argv中任何不可解码的字节表示为特殊的Unicode代码点。 当该代码点被解码回字节流时,它就再次成为原始字节。 这允许通过本地Python字符串类型从当前编码中无效的argv(即,以不同于当前语言环境命名的文件名)往返传输数据,并返回字节而不丢失信息。
正如你所看到的,情况非常混乱:-)
我现在只能说Windows。 在Windows上,代码页仅适用于传统应用程序,不能由系统或现代应用程序使用。 Windows使用UTF-16(并已经这么做了很长时间了):文本显示,文件名,终端,系统API。 UTF-16和遗留代码页之间的转换只能在最高级别上执行,直接在系统和应用程序之间的接口上执行(从技术上说,旧的API函数是两次执行的 – 功能函数FunctionW
是真正的工作和期望UTF-16字符串和一个兼容性函数FunctionA
,它将当前(线程)代码页的输入字符串转换为UTF-16,调用FunctionW
,并将结果转换回来。 制表符完成应该总是产生UTF-16字符串(当使用TrueType字体时肯定会这样做),因为控制台也只使用UTF-16。 制表符完成的UTF-16文件名被移交给应用程序。 如果现在应用程序是一个遗留应用程序(即,它使用main
而不是wmain
/ GetCommandLineW
等),那么Microsoft C运行时(可能)使用GetCommandLineA
让系统转换命令行。 所以基本上我认为你对Windows的看法是正确的(只有在制表符完成时可能没有涉及到转换): argv
数组总是包含当前应用程序代码页中的参数,因为信息代码页(L1)在中间UTF-16阶段, 原来的程序使用已经不可逆转地丢失了。
结论与Windows一样:避免遗留代码页; 尽可能使用UTF-16 API。 如果您必须使用main
而不是wmain
(例如,独立于平台),请使用GetCommandLineW
而不是argv
数组。
测试应用程序的输出需要进行一些修改才行,您需要十六进制代码,并且需要清除负值。 或者你不能打印像UTF-8特殊字符的东西,所以你可以阅读它们。
首先修改SW:
#include <stdio.h> int main(int argc, char **argv) { if (argc < 2) { printf("Not enough arguments\n"); return 1; } int len = 0; for (unsigned char *c = argv[1]; *c; c++, len++) { printf("%x ", (*c)); } printf("\nLength: %d\n", len); return 0; }
然后在我使用UTF-8的Ubuntu盒子上得到这个输出。
$> gcc -std=c99 argc.c -o argc $> ./argc 1ü 31 c3 bc Length: 3
在这里你可以看到,在我的情况下,ü编码2个字符,而1是一个字符。 或多或少,你所期望的UTF-8编码。
这实际上和env LANG的变化是一致的。
$> env | grep LANG LANG=en_US.utf8
希望这个稍微澄清一下linux的情况。
/祝你好运
是的,用户在Unix上混合语言环境时一定要小心。 显示和更改文件名的GUI文件管理器也有这个问题。 在Mac OS X上,标准的Unix编码是UTF-8。 事实上,当通过Unix接口调用HFS +文件系统时,它强制执行UTF-8文件名,因为它需要将其转换为UTF-16以存储在文件系统中。