如何使getLine接受Unicode字符?

4
当运行以下代码时:
do line <- getLine
   putStrLn line

或者,
getLine >>= putStrLn

并且,在之后,
 getLine >>= putStrLn

进入

µ

当遇到这个输出时:

现在,我已经尝试了chcp 65001,但它没有起作用,而stdin的编码是utf8
没有putStrLn的检查结果如下:
 getLine
µ
'\NIL'

我的环境:
Windows 10 版本 10.0.17134 Build 17134
联想 ideapad 510-15IKB
BIOS 版本 LENOVO 3JCN30WW
GHCi v 8.2.2
如何解决这个问题?
注:以下操作序列会导致此问题: 1. 打开 cmd 2. 输入 chcp 65001 3. 输入 ghci 4. 输入 getLine >>= putStrLn 5. 输入 µ
但是以下操作则不会出现问题: 1. 搜索 ghci 2. 在 %PROGRAMS%\Haskell Platform\8.2.2\bin 打开 ghci.exe 3. 重复步骤 4-5
注意:%PROGRAMS% 不是真正的环境变量。
按要求,下面是 GHC.IO.Encoding.getLocaleEncoding 的输出结果:
UTF-8

此外,System.IO.hGetEncoding stdin 的输出为:
Just UTF-8

(当使用chcp 65001时)

编辑:字符为U+00B5。我使用的是德国键盘,系统区域设置为德国,语言设置为英语,键盘语言为带有德国布局的英语。


5
这是在Windows上吗? - that other guy
6
这对我来说符合预期。您能提供更多关于您的环境的细节吗? - DarthFennec
1
你确定你输入的 stdin 是 utf8 而不是 utf16 吗?那个字母是 mu (U+03BC) 还是 micro sign (U+00B5)? - Dan Robertson
1
在出现故障的 ghci 会话中,GHC.IO.Encoding.getLocaleEncoding 返回什么? - Daniel Wagner
1
@Mark Neu,如果你真的很想看到一个解决方法,我在我的答案中进行了编辑,并提供了一个最小化的可行解决方案。 - lehins
显示剩余2条评论
1个回答

4
控制台输入/输出在Windows上已经完全崩溃了一段时间。以下是跟IO相关的所有问题的顶级票证: https://ghc.haskell.org/trac/ghc/ticket/11394 我认为,这两个票证最好描述了您正在体验的行为: 现在唯一的解决方法是手动使用Windows API来处理控制台输出/输入,这本身就是一种痛苦。 编辑 所以,仅仅是出于好奇,我决定忍受一些疼痛。 :)
以下是下面代码的输出:
====
Input: µ
Output: µ
====

这绝不是一个完全正确或安全的解决方案,但它确实可以工作:
module Main where

import Control.Monad
import System.IO
import Foreign.Ptr
import Foreign.ForeignPtr
import Foreign.C.String
import Foreign.C.Types
import Foreign.Storable

import System.Win32
import System.Win32.Types
import Graphics.Win32.Misc

foreign import ccall unsafe "windows.h WriteConsoleW"
  c_WriteConsoleW :: HANDLE -> LPWSTR -> DWORD -> LPDWORD -> LPVOID -> IO BOOL

foreign import ccall unsafe "windows.h ReadConsoleW"
  c_ReadConsoleW :: HANDLE -> LPWSTR -> DWORD -> LPDWORD -> LPVOID -> IO BOOL

-- | Read n characters from a handle, which should be a console stdin
hwGetStrN :: Int -> Handle -> IO String
hwGetStrN maxLen hdl = do
  withCWStringLen (Prelude.replicate maxLen '\NUL') $ \(cstr, len) -> do
    lpNumberOfCharsWrittenForeignPtr <- mallocForeignPtr
    withHandleToHANDLE hdl $ \winHANDLE ->
      withForeignPtr lpNumberOfCharsWrittenForeignPtr $ \lpNumberOfCharsRead -> do
        c_ReadConsoleW winHANDLE cstr (fromIntegral len) lpNumberOfCharsRead nullPtr
        numWritten <- peek lpNumberOfCharsRead
        peekCWStringLen (cstr, fromIntegral numWritten)

-- | Write a string to a handle, which should be a console stdout or stderr.
hwPutStr :: Handle -> String -> IO ()
hwPutStr hdl str = do
  void $ withCWStringLen str $ \(cstr, len) -> do
    lpNumberOfCharsWrittenForeignPtr <- mallocForeignPtr
    withHandleToHANDLE hdl $ \winHANDLE ->
      withForeignPtr lpNumberOfCharsWrittenForeignPtr $ \ lpNumberOfCharsWritten ->
      c_WriteConsoleW winHANDLE cstr (fromIntegral len) lpNumberOfCharsWritten nullPtr

main :: IO ()
main = do
  hwPutStr stdout "====\nInput: "
  str <- hwGetStrN 10 stdin
  hwPutStr stdout "Output: "
  hwPutStr stdout str
  hwPutStr stdout "====\n"

编辑 2

@dfeuer要求我列出那篇回答中不安全、不正确或不完整的事情。我只在Linux上编写代码,所以我不是Windows程序员,但以下是让该代码在实际程序中使用之前需要更改的一些事项:

  • 最重要的部分是该代码仅适用于控制台句柄,可以通过GetConsoleMode API调用来确定。
  • 对于其他类型的句柄,如使用管道或文件句柄,则上述代码将无效,这也涉及到编码的问题,但这是完全不同的问题。
  • 未考虑API调用失败的情况。因此,我们必须通过查看返回的BOOL来检查调用是否成功,并在每次不成功时使用GetLastError将错误报告给用户。
  • 上面实现的功能非常有限,没有检查实际读/写到/从缓冲区的数据量。因此,hwGetStrN只能处理n个字符,因此需要递归调用才能获得类似于hGetLine的行为。
  • 进行所有的合法性检查,例如:DWORDWord32,因此fromIntegral len调用容易受到整数溢出的影响,这既不正确也不安全。
  • 在32位操作系统上,FFI调用必须为stdcall,而对于x86_64则为ccall,因此需要一些CPP。

你能在回答中指出具体哪些部分可能是不完整、不正确或不安全的吗?如果你有一点想法的话。 - dfeuer
1
@dfeuer,这种功能肯定应该在“base”中,我不建议使用我提供的示例代码。无论如何,如果要在任何地方使用该代码,我已经更新了答案,并列出了必须修复的问题。 - lehins
啊,看这里,我稍微搜索了一下,发现6年前有人提出了一个类似但更全面的解决方案 :) https://dev59.com/QWgv5IYBdhLWcg3wMN-0 - lehins

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