【问题标题】:Perl + Curses: Expecting a UTF-8 encoded multibyte character from getchar(), but not getting anyPerl + Curses:期望来自 getchar() 的 UTF-8 编码的多字节字符,但没有得到任何
【发布时间】:2020-07-09 04:37:28
【问题描述】:

我正在试用 Bryan Henderson 的 ncurses 库的 Perl 接口:Curses

作为一个简单的练习,我尝试获取在屏幕上键入的单个字符。这直接基于NCURSES Programming HOWTO,并进行了改编。

当我调用 Perl 库的 getchar() 时,我希望收到一个字符,可能是多字节的(这有点复杂,正如 this part of the library manpage 中解释的那样,因为必须处理功能键和无输入的特殊情况,但是那只是通常的花饰)。

就是下面代码中的子程序read1ch()

这适用于 ASCII 字符,但不适用于 0x7F 以上的字符。例如,当点击è (Unicode 0x00E8, UTF-8: 0xC3, 0xA8) 时,我实际上获得了代码 0xE8 而不是 UTF-8 编码的东西。将其打印到LANG=en_GB.UTF-8 不起作用的终端上,无论如何我期待0xC3A8。

我需要更改什么才能使其正常工作,即将è 作为正确的字符或 Perl 字符串?

getchar() 截取的C 代码是here 顺便说一句。也许它只是没有用C_GET_WCH set 编译?如何发现?

附录

附录 1

尝试使用设置binmode

binmode STDERR, ':encoding(UTF-8)';
binmode STDOUT, ':encoding(UTF-8)';

这应该可以解决任何编码问题,因为终端期望并发送 UTF-8,但这没有帮助。

还尝试使用use open 设置流编码(不太确定此方法与上述方法之间的区别),但这也无济于事

use open qw(:std :encoding(UTF-8));

附录 2

Perl Curses shim 的手册页说:

如果 wget_wch() 不可用(即 Curses 库不可用 理解宽字符),这调用wgetch() [得到一个1字节的字符 从一个诅咒窗口],但返回 尽管如此,上述值。这可能是一个问题,因为 像 UTF-8 这样的多字节字符编码,您将收到两个 两字节字符的单字符字符串(例如,“Ô和“¤” “一种”)。

这里可能就是这种情况,但wget_wch() 确实存在于这个系统上。

附录 3

试图查看C代码做了什么,并在curses/Curses-1.36/CursesFunWide.c的多字节处理代码中直接添加了fprintf,重新编译,没有设法用我自己的通过LD_LIBRARY_PATH覆盖系统Curses.so(为什么不是吗?为什么一切都只工作了一半?),所以直接替换了系统库(拿那个!)。

#ifdef C_GET_WCH
    wint_t wch;
    int ret = wget_wch(win, &wch);
    if (ret == OK) {
        ST(0) = sv_newmortal();
        fprintf(stderr,"Obtained win_t 0x%04lx\n", wch);
        c_wchar2sv(ST(0), wch);
        XSRETURN(1);
    } else if (ret == KEY_CODE_YES) {
        XST_mUNDEF(0);
        ST(1) = sv_newmortal();
        sv_setiv(ST(1), (IV)wch);
        XSRETURN(2);
    } else {
        XSRETURN_UNDEF;
    }
#else

这只是一个胖子 NOPE,当按下 ü 时会看到:

Obtained win_t 0x00fc

所以运行了正确的代码,但数据是ISO-8859-1,而不是UTF-8。所以它是wget_wch,它的行为很糟糕。所以这是一个诅咒配置问题。呵呵。

附录 4

让我感到震惊的是,ncurses 可能假设了默认语言环境,即C。要使其ncurses 使用宽字符,必须“初始化语言环境”,这可能意味着将状态从“未设置”(从而使ncurses 回退到C)到“设置为系统指示”(应该是 LANG 环境变量中的内容)。 ncurses 的手册页说:

库使用调用程序已初始化的语言环境。 这通常通过 setlocale 完成:

setlocale(LC_ALL, "");

如果语言环境未初始化,则库假定字符 可按 ISO-8859-1 打印,以与某些遗留程序一起使用。 您应该初始化语言环境,而不是依赖于 尚未设置语言环境时的库。

这也不起作用,但我觉得解决方案就在这条路上。

附录 5

来自CursesWide.cwin_t(显然与wchar_t 相同)转换代码,将从wget_wch() 接收到的wint_t(此处视为wchar_t)转换为Perl 字符串。 SV 是“标量值”类型。

另见:https://perldoc.perl.org/perlguts.html

这里插入两个fprintf,看看发生了什么:

static void
c_wchar2sv(SV *    const sv,
           wchar_t const wc) {
/*----------------------------------------------------------------------------
  Set SV to a one-character (not -byte!) Perl string holding a given wide
  character
-----------------------------------------------------------------------------*/
    if (wc <= 0xff) {
        char s[] = { wc, 0 };
        fprintf(stderr,"Not UTF-8 string: %02x %02x\n", ((int)s[0])&0xFF, ((int)s[1])&0xFF);
        sv_setpv(sv, s);
        SvPOK_on(sv);
        SvUTF8_off(sv);
    } else {
        char s[UTF8_MAXBYTES + 1] = { 0 };
        char *s_end = (char *)UVCHR_TO_UTF8((U8 *)s, wc);
        *s_end = 0;
        fprintf(stderr,"UTF-8 string: %02x %02x %02x\n", ((int)s[0])&0xFF, ((int)s[1])&0xFF, ((int)s[2])&0xFF);
        sv_setpv(sv, s);
        SvPOK_on(sv);
        SvUTF8_on(sv);
    }
}

使用 perl-Curses 测试代码

  • 已尝试使用 perl-Curses-1.36-9.fc30.x86_64
  • 已尝试使用 perl-Curses-1.36-11.fc31.x86_64

如果您尝试,请按 BACKSPACE 退出循环,因为 CTRL-C 不再被解释。

下面代码很多,但关键区域标有----- Testing

#!/usr/bin/perl

# pmap -p PID
# shows the per process using 
# /usr/lib64/libncursesw.so.6.1
# /usr/lib64/perl5/vendor_perl/auto/Curses/Curses.so

# Trying https://metacpan.org/release/Curses

use warnings;
use strict;
use utf8;          # Meaning "This lexical scope (i.e. file) contains utf8"

use Curses;        # On Fedora: dnf install perl-Curses

# This didn't fix it 
# https://perldoc.perl.org/open.html

use open qw(:std :encoding(UTF-8));

# https://perldoc.perl.org/perllocale.html#The-setlocale-function

use POSIX ();
my $loc = POSIX::setlocale(&POSIX::LC_ALL, "");

# ---
# Surrounds the actual program
# ---

sub setup() {
   initscr();
   raw();
   keypad(1);
   noecho();
}

sub teardown {
   endwin();
}

# ---
# Mainly for prettyprinting
# ---

my $special_keys = setup_special_keys();

# ---
# Error printing
# ---

sub mt {
   return sprintf("%i: ",time());
}

sub ae {
   my ($x,$fname) = @_;
   if ($x == ERR) { 
      printw mt();
      printw "Got error code from '$fname': $x\n"
   }
}

# ---
# Where the action is
# ---

sub announce {
   my $res = printw "Type any character to see it in bold! (or backspace to exit)\n";
   ae($res, "printw");
   return { refresh => 1 }
}

sub read1ch {
   # Read a next character, waiting until it is there.
   # Use the wide-character aware functions unless you want to deal with
   # collating individual bytes yourself!
   # Readings:
   # https://metacpan.org/pod/Curses#Wide-Character-Aware-Functions
   # https://perldoc.perl.org/perlunicode.html#Unicode-Character-Properties
   # https://www.ahinea.com/en/tech/perl-unicode-struggle.html
   # https://hexdump.wordpress.com/2009/06/19/character-encoding-issues-part-ii-perl/
   my ($ch, $key) = getchar();
   if (defined $key) {
      # it's a function key
      printw "Function key pressed: $key"; 
      printw " with known alias '" . $$special_keys{$key} . "'" if (exists $$special_keys{$key});
      printw "\n";
      # done if backspace was hit
      return { done => ($key == KEY_BACKSPACE()) }
   }
   elsif (defined $ch) {
      # "$ch" should be a String of 1 character

      # ----- Testing

      printw "Locale: $loc\n";
      printw "Multibyte output test: öüäéèà периоду\n";
      printw sprintf("Received string '%s' of length %i with ordinal 0x%x\n", $ch, length($ch), ord($ch));

      {
         # https://perldoc.perl.org/bytes.html
         use bytes;
         printw sprintf("... length is %i\n"     , length($ch));
         printw sprintf("... contents are %vd\n" , $ch);
      }

      # ----- Testing

      return { ch => $ch }
   }
   else {
      # it's an error
      printw "getchar() failed\n";
      return {}
   }
}

sub feedback {
   my ($ch) = @_;
   printw "The pressed key is: ";
   attron(A_BOLD);
   printw("%s\n","$ch"); # do not print $txt directly to make sure escape sequences are not interpreted!
   attroff(A_BOLD);
   return { refresh => 1 }  # should refresh
}

sub do_curses_run {

   setup;

   my $done = 0;
   while (!$done) {
      my $bubl;
      $bubl = announce(); 
      refresh() if $$bubl{refresh};
      $bubl = read1ch();
      $done = $$bubl{done};
      if (defined $$bubl{ch}) {
         $bubl = feedback($$bubl{ch}); 
         refresh() if $$bubl{refresh};
      }
   }

   teardown;
}

# ---
# main
# ---

do_curses_run();


sub setup_special_keys {
   # the key codes on the left must be called once to resolve to a numeric constant!
   my $res = {
      KEY_BREAK()       => "Break key",
      KEY_DOWN()        => "Arrow down",
      KEY_UP()          => "Arrow up",
      KEY_LEFT()        => "Arrow left",
      KEY_RIGHT()       => "Arrow right",
      KEY_HOME()        => "Home key",
      KEY_BACKSPACE()   => "Backspace",
      KEY_DL()          => "Delete line",
      KEY_IL()          => "Insert line",
      KEY_DC()          => "Delete character",
      KEY_IC()          => "Insert char or enter insert mode",
      KEY_EIC()         => "Exit insert char mode",
      KEY_CLEAR()       => "Clear screen",
      KEY_EOS()         => "Clear to end of screen",
      KEY_EOL()         => "Clear to end of line",
      KEY_SF()          => "Scroll 1 line forward",
      KEY_SR()          => "Scroll 1 line backward (reverse)",
      KEY_NPAGE()       => "Next page",
      KEY_PPAGE()       => "Previous page",
      KEY_STAB()        => "Set tab",
      KEY_CTAB()        => "Clear tab",
      KEY_CATAB()       => "Clear all tabs",
      KEY_ENTER()       => "Enter or send",
      KEY_SRESET()      => "Soft (partial) reset",
      KEY_RESET()       => "Reset or hard reset",
      KEY_PRINT()       => "Print or copy",
      KEY_LL()          => "Home down or bottom (lower left)",
      KEY_A1()          => "Upper left of keypad",
      KEY_A3()          => "Upper right of keypad",
      KEY_B2()          => "Center of keypad",
      KEY_C1()          => "Lower left of keypad",
      KEY_C3 ()         => "Lower right of keypad",
      KEY_BTAB()        => "Back tab key",
      KEY_BEG()         => "Beg(inning) key",
      KEY_CANCEL()      => "Cancel key",
      KEY_CLOSE()       => "Close key",
      KEY_COMMAND()     => "Cmd (command) key",
      KEY_COPY()        => "Copy key",
      KEY_CREATE()      => "Create key",
      KEY_END()         => "End key",
      KEY_EXIT()        => "Exit key",
      KEY_FIND()        => "Find key",
      KEY_HELP()        => "Help key",
      KEY_MARK()        => "Mark key",
      KEY_MESSAGE()     => "Message key",
      KEY_MOUSE()       => "Mouse event read",
      KEY_MOVE()        => "Move key",
      KEY_NEXT()        => "Next object key",
      KEY_OPEN()        => "Open key",
      KEY_OPTIONS()     => "Options key",
      KEY_PREVIOUS()    => "Previous object key",
      KEY_REDO()        => "Redo key",
      KEY_REFERENCE()   => "Ref(erence) key",
      KEY_REFRESH()     => "Refresh key",
      KEY_REPLACE()     => "Replace key",
      KEY_RESIZE()      => "Screen resized",
      KEY_RESTART()     => "Restart key",
      KEY_RESUME()      => "Resume key",
      KEY_SAVE()        => "Save key",
      KEY_SBEG()        => "Shifted beginning key",
      KEY_SCANCEL()     => "Shifted cancel key",
      KEY_SCOMMAND()    => "Shifted command key",
      KEY_SCOPY()       => "Shifted copy key",
      KEY_SCREATE()     => "Shifted create key",
      KEY_SDC()         => "Shifted delete char key",
      KEY_SDL()         => "Shifted delete line key",
      KEY_SELECT()      => "Select key",
      KEY_SEND()        => "Shifted end key",
      KEY_SEOL()        => "Shifted clear line key",
      KEY_SEXIT()       => "Shifted exit key",
      KEY_SFIND()       => "Shifted find key",
      KEY_SHELP()       => "Shifted help key",
      KEY_SHOME()       => "Shifted home key",
      KEY_SIC()         => "Shifted input key",
      KEY_SLEFT()       => "Shifted left arrow key",
      KEY_SMESSAGE()    => "Shifted message key",
      KEY_SMOVE()       => "Shifted move key",
      KEY_SNEXT()       => "Shifted next key",
      KEY_SOPTIONS()    => "Shifted options key",
      KEY_SPREVIOUS()   => "Shifted prev key",
      KEY_SPRINT()      => "Shifted print key",
      KEY_SREDO()       => "Shifted redo key",
      KEY_SREPLACE()    => "Shifted replace key",
      KEY_SRIGHT()      => "Shifted right arrow",
      KEY_SRSUME()      => "Shifted resume key",
      KEY_SSAVE()       => "Shifted save key",
      KEY_SSUSPEND()    => "Shifted suspend key",
      KEY_SUNDO()       => "Shifted undo key",
      KEY_SUSPEND()     => "Suspend key",
      KEY_UNDO()        => "Undo key"
   };

   for (my $f = 1; $f <= 64; $f++) {
      $$res{KEY_F($f)} = "KEY_F($f)"   
   }

   return $res

}

【问题讨论】:

  • 在对模块了解不多的情况下,我没有看到启用流中的 uf8 编码?喜欢代码顶部的use open qw(:std :encoding(UTF-8));?还是模块应该处理这个问题?
  • 好吧,看来我不能轻易拥有那个版本(cpanm 安装被炸得很惨——也许我的 CentOS7 上的系统诅咒太旧了)。必须在流上设置编码,是的;问题是图书馆是否这样做。不过很容易尝试:只需在程序顶部添加我的第一条评论中的行。见open pragma
  • binmodeopen pragma 之间的区别我想说的是,open 是一种比 binmode 更干净的方法,它可以处理所有标准流( pragma 也是词法的);代码中显示的binmode 行没有处理STDIN。然后,如果还有其他输入通道(比如@ARGV、套接字...),您将需要Encode::decode 或等效项。
  • 但是从表面上看getchar 应该处理它;那么你也不想自己做。
  • decode("iso-8859-1", $x") 没有意义。这是一个无操作。 (它可能会创建一个具有不同内部格式的字符串,但没有人应该关心这一点。这表明 elsewhere 存在错误。)

标签: perl encoding utf-8 locale ncurses


【解决方案1】:

其实看起来是对的。

使用 strace 运行脚本会有所帮助...我这样做是为了查看系统调用:

strace -fo strace.out -s 1024 ./foo

并且可以看到读取、消息等。可以使用调试库来为 ncurses 获取类似的跟踪,尽管打包者在提供启用跟踪的方面并不一致。

UTF-8 中的

ü\303\274(八进制),其 Unicode 值为 252(十进制) ,或 0xfc(十六进制)。这部分问题似乎忽略了这一点:

这只是一个胖子 NOPE,当按下 ü 时会看到:

Obtained win_t 0x00fc

因此运行了正确的代码,但数据是 ISO-8859-1,而不是 UTF-8。所以是 wget_wch 表现不好。所以这是一个诅咒配置问题。呵呵。

wget_wch 返回(出于实际目的)一个 Unicode 值(不是 UTF-8 字节序列)。 ISO-8859-1 代码 160-255 碰巧(并非巧合)与 Unicode 代码点匹配,尽管后者在 UTF-8 中肯定会编码不同。

wgetch 将返回 UTF-8 字节,但 Perl 脚本只会将其用作后备(因为这会导致 Perl 脚本将 UTF-8 字符串转换为 Unicode 值)。

【讨论】:

  • 谢谢托马斯。我必须考虑一下。
  • 好的,我添加了一个“答案”。显然 Perl 对 sprintf 有问题,Curses 库对 printw 有问题。但数据确实被正确接收。只是没有正确打印。
【解决方案2】:

Thomas Dickey 正确地指出收到了正确的数据

这花了我一些时间才真正确定。

混淆归结为 Perl 的 sprintf 无法处理 UTF-8 和 Perl 诅咒 printw 无法处理区域 0x800x7F

这需要更长的时间才能确定。

事实上,我已经对此提出了一个新问题:

Are there one (or two) solid bugs in the `curses` shim for Perl?

【讨论】:

  • waddwstr 如果 Perl 传入 wchar_t 的数组(这不太可能),将是合适的。 waddstr 处理 UTF-8 字符串。 Perl“知道”字符串何时是 UTF-8,如果它忘记了(如果字符串被分配给 原始字符串),它的猜测可能会导致您提到的问题。
  • Re "$ch 的字节被解释为 ISO-8859-1。",sprintf%s 不执行任何解释。它只是将参数字符串中的字符一对一地复制到输出字符串中。例如,sprintf("[%s]", $x)"[$x]""[" . $x . "]" 都是等价的
  • Re "我不确定 sprintf hack。",这不是 hack;它只是意味着printw 采用像printf 这样的格式模式。与printf 一样,您必须转义任何要逐字打印的%,或使用printw("%s", "...")
  • @Ok 对于“sprintf hack”。这确实是 C 接口所期望的:` int printw(const char *fmt, ...);`知道了!
  • @ikegami 我已将其移至stackoverflow.com/questions/60971499/…
【解决方案3】:

[ 此答案假定 libncursesw 可用且正在使用。尝试在没有宽字符支持的情况下输出“宽字符”是没有意义的:)]


简答

getchar 工作正常。它返回一串 Unicode 代码点(也称为解码文本),这是理想的。

printw 已损坏,但可以通过将以下内容添加到程序中来使其接受一串 Unicode 代码点(也称为解码文本):

{
   # Add wide character support to printw.
   # This only modifies the current package (main),
   # so it won't affect any code by ours.
   no warnings qw( redefine );
   sub printw { addstring(sprintf shift, @_) }
}


getchar 有问题吗?

所以你认为getchar 有问题。让我们尝试通过检查 getchar 返回的内容来确认这一点。我们将通过添加以下内容来做到这一点:

printw("String received from getchar: %vX\n", $ch);

%vX 将以十六进制打印字符串的每个字符的值,并用句点连接。)

  • 当按下e (U+0065),一个 7 位字符时,会看到:

    String received from getchar: 65
    
  • 当按下é (U+00E9),一个 8 位字符时,会看到:

    String received from getchar: E9
    
  • 当按下ē (U+0113),一个 9 位字符时,会看到:

    String received from getchar: 113
    

在所有三种情况下,我们都会得到一个只有一个字符长的字符串,并且该字符由输入的 Unicode 代码点组成。[1] 这正是我们想要的。应用和移除字符编码应该在外围完成,这样程序的主要逻辑就不必担心编码,并且正在这样做。

结论:getchar没有问题。


printw 有问题吗?

所以问题一定出在输出上。为了确认这一点,我在您的程序中添加了以下内容:

sub _d { utf8::downgrade( my $s = shift ); $s }
sub _u { utf8::upgrade(   my $s = shift ); $s }

for (
   [ "7-bit, UTF8=0" => _d(chr(0x65)) ],   # Expect e
   [ "7-bit, UTF8=1" => _u(chr(0x65)) ],   # Expect e
   [ "8-bit, UTF8=0" => _d(chr(0xE9)) ],   # Expect é
   [ "8-bit, UTF8=1" => _u(chr(0xE9)) ],   # Expect é
   [ "9-bit, UTF8=1" => chr(0x113)    ],   # Expect ē
) {
   my ($name, $chr) = @$_;
   printw("%s: %s\n", $name, $chr);
}

输出:

7-bit, UTF8=0: e
7-bit, UTF8=1: e
8-bit, UTF8=0:
8-bit, UTF8=1: é
9-bit, UTF8=1:  S

从上面我们观察到:

  • 我们看到_d(chr(0xE9))_u(chr(0xE9)) 的结果之间存在差异,即使两个标量包含相同的字符串(_d(chr(0xE9)) eq _u(chr(0xE9)) 为真)。因此,此函数存在 Unicode 错误。
  • 根据 8 位测试,它似乎接受 Unicode 代码点(解码文本)而不是 UTF-8。这是理想的选择。
  • 根据 9 位测试,它似乎不接受 Unicode 代码点。随后的测试表明它也不接受chr(0x113)的UTF-8编码。

结论:printw存在重大问题。


printw解决问题

解决 Unicode 错误很容易,但缺乏对 0​​xFF 以上字符的支持是个问题。让我们深入研究代码。

好的,我们不必费力寻找问题。我们看到printw 是根据addstr 定义的,而addstr 早于宽字符支持。 addstring 对应的是宽字符支持,所以让printw 使用addstring 而不是addstr

{
   # Add wide character support to printw.
   # This only modifies the current package (main),
   # so it won't affect any code by ours.
   no warnings qw( redefine );
   sub printw { addstring(sprintf shift, @_) }
}

输出:

7-bit, UTF8=0: e
7-bit, UTF8=1: e
8-bit, UTF8=0: é
8-bit, UTF8=1: é
9-bit, UTF8=1: ē

宾果游戏!

从上面我们观察到:

  • 我们发现UTF8=0 测试的结果与其对应的UTF8=1 测试之间没有差异。因此,此函数不会受到 Unicode 错误的影响。
  • 它始终接受 Unicode 代码点(解码文本)字符串。值得注意的是,它不需要 UTF-8 或语言环境的编码。

这正是我们所期望/渴望的。


  1. 具体来说,getchar 没有像您认为的那样返回输入的 iso-8859-1 编码。这种混淆是可以理解的,因为 Unicode 是 iso-8859-1 的扩展。

【讨论】:

猜你喜欢
  • 2013-06-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多