在 ruby 中,当文件名包含某些字符时,optparse 会引发错误

In ruby, optparse raises error when filename contains certain characters

提问人:HippoMan 提问时间:2/23/2023 最后编辑:HippoMan 更新时间:5/6/2023 访问量:204

问:

我在 Linux 下的 ruby 程序 () 中使用。如果任何命令行参数是包含“特殊”字符的文件名,则该方法将失败,并出现以下错误:optparseruby 2.7.1p83parse!

invalid byte sequence in UTF-8

这是失败的代码......

parser = OptionParser.new {
  |opts|
  ... etc. ...
}
parser.parse! # error occurs here

我知道在 ruby 中进行编码的方法和其他方法。但是,发生错误的地方是在库例程 () 中,我无法控制此库例程如何处理字符串。scrubOptionParser#parse!

我可以预处理命令行参数,并将这些参数中的特殊字符替换为可接受的编码,但是,如果参数是文件名,我将无法稍后在程序中打开该文件,因为我接受到程序中的文件名将从文件的原始名称更改。

我可以做一些复杂的事情,比如预先遍历参数,构建一个哈希图,其中键是编码的参数,值是原始参数,将 ARGV 值更改为编码值,使用 解析编码的参数,然后在完成后遍历生成的参数,并使用哈希映射在将编码参数替换为其原始值的过程中......然后继续执行该程序。OptionParserOptionParser

但我希望在红宝石中能有一种更简单的方法来解决这个问题。

提前感谢您的任何想法或建议。

更新:这是更详细的信息...

为了测试这一点,我编写了以下最小程序:rtest.rb

#!/usr/bin/env ruby                                                                                                                               
# -*- ruby -*-                                                                                                                                        

require 'optparse'

parser = OptionParser.new {
}
parser.parse!

Process.exit(0)

我按如下方式运行它,当前目录中唯一存在的文件是它本身,另一个文件具有此名称:...rtest.rbÄfoo

export LC_TYPE='en_us.UTF-8'
export LC_COLLATE='en_us.UTF-8'
./rtest.rb *

它生成了以下错误和堆栈跟踪...

Traceback (most recent call last):
    7: from /home/hippo/bin/rtest.rb:8:in `<main>'
    6: from /opt/rubies/ruby-2.7.1/lib/ruby/2.7.0/optparse.rb:1691:in `parse!'
    5: from /opt/rubies/ruby-2.7.1/lib/ruby/2.7.0/optparse.rb:1666:in `permute!'
    4: from /opt/rubies/ruby-2.7.1/lib/ruby/2.7.0/optparse.rb:1569:in `order!'
    3: from /opt/rubies/ruby-2.7.1/lib/ruby/2.7.0/optparse.rb:1575:in `parse_in_order'
    2: from /opt/rubies/ruby-2.7.1/lib/ruby/2.7.0/optparse.rb:1575:in `catch'
    1: from /opt/rubies/ruby-2.7.1/lib/ruby/2.7.0/optparse.rb:1579:in `block in parse_in_order'
/opt/rubies/ruby-2.7.1/lib/ruby/2.7.0/optparse.rb:1579:in `===': invalid byte sequence in UTF-8 (ArgumentError)

这是出现在文件的相关部分中的内容。见行.../opt/rubies/ruby-2.7.1/lib/ruby/2.7.0/optparse.rb1579

 1572   def parse_in_order(argv = default_argv, setter = nil, &nonopt)  # :nodoc:                                                                     
 1573     opt, arg, val, rest = nil
 1574     nonopt ||= proc {|a| throw :terminate, a}
 1575     argv.unshift(arg) if arg = catch(:terminate) {
 1576       while arg = argv.shift
 1577         case arg
 1578           # long option                                                                                                                           
 1579           when /\A--([^=]*)(?:=(.*))?/m
 1580             opt, rest = $1, $2

换句话说,由于此编码问题,参数上的正则表达式匹配失败。

当我有时间时(不幸的是,不是马上),我会在该模块中放入一些代码来对变量进行编码,看看这是否可以解决问题。arg

进一步更新:我在 下运行,提供的 ruby 版本是 2.7.0。我还设法在我的旧盒子上运行 2.7.1。此错误在两种环境中都会发生。在尝试 2.7.7 版或 3.x 版之前,我必须安装较新版本的 ruby 或从源代码编译它。Ubuntu 20.0.4debian 8

另一个更新:我有一些意想不到的空闲时间,所以我从源代码构建了 ruby-3.3.0 并重新运行测试。我遇到了同样的错误!

% /opt/local/rubies/ruby-3.3.0/bin/ruby ./rtest.rb *
/opt/local/rubies/ruby-3.3.0/lib/ruby/3.3.0+0/optparse.rb:1640:in `===': invalid byte sequence in UTF-8 (ArgumentError)
    from /opt/local/rubies/ruby-3.3.0/lib/ruby/3.3.0+0/optparse.rb:1640:in `block in parse_in_order'
    from /opt/local/rubies/ruby-3.3.0/lib/ruby/3.3.0+0/optparse.rb:1636:in `catch'
    from /opt/local/rubies/ruby-3.3.0/lib/ruby/3.3.0+0/optparse.rb:1636:in `parse_in_order'
    from /opt/local/rubies/ruby-3.3.0/lib/ruby/3.3.0+0/optparse.rb:1630:in `order!'
    from /opt/local/rubies/ruby-3.3.0/lib/ruby/3.3.0+0/optparse.rb:1739:in `permute!'
    from /opt/local/rubies/ruby-3.3.0/lib/ruby/3.3.0+0/optparse.rb:1764:in `parse!'
    from ./rtest.rb:8:in `<main>'

但是,我现在认为发生错误是因为文件名以不寻常的方式编码。如果我在该目录中这样做,我会看到这个,这就是我所期望的:echo *

% echo *
Äfoo rtest.rb

但是,如果我在同一目录中执行,我会看到以下内容:/bin/ls

% /bin/ls *
''$'\304''foo'   rtest.rb

甚至操作系统也无法识别名称如下的文件......

% /bin/cat 'Äfoo'
/bin/cat: Äfoo: No such file or directory

但是,如果我使用较长的编码文件名,操作系统访问该文件没有问题......

% /bin/cat ''$'\304''foo
File contents
File contents

该命令似乎知道如何将文件名编码为 ,但 ruby 似乎不知道如何做到这一点。lsÄfoo''$'\304''foo

Linux 命令行 编码 Ruby 解析

评论

0赞 Jan Vítek 2/23/2023
您确定您有正确的系统区域设置吗?我尝试解析,系统语言环境设置为 , ruby 3.1.2, optparse 0.2.0 没问题./test.rb -r testěščřžýáíéúůen_US.UTF-8
0赞 engineersmnky 2/23/2023
你能更具体地介绍一下“'特殊'角色”吗?此外,您是否确定这发生在它执行的操作中,而不是它执行的操作之一,例如 解析?OptionParserCSV
0赞 HippoMan 2/23/2023
完全空的块也会发生此错误;即,使用“......等等......”我原来的问题中的东西是空白的。此外,当我显式导出 and both as 时,它仍然会发生。此操作失败的文件名之一是 。我使用的是 ruby 2.7.1,而不是 3.1.2。也许我需要尝试升级 ruby ... ???OptionParserLC_TYPELC_COLLATEen_us.UTF-8Äfoo
0赞 Jan Vítek 2/23/2023
我没有 2.7.1,但尝试了 2.7.7,没有问题。Ä
0赞 HippoMan 2/23/2023
嗯。。。好吧,我会在今天晚些时候或明天尝试 ruby 升级。也许这会解决问题。我将在这里报告我的发现。

答:

0赞 HippoMan 2/23/2023 #1

注意:我更喜欢我的另一个答案。但是,我也保留了这个答案,以防有人仍然感兴趣。

根据我最初问题下面的讨论,尤其是根据@Schwern的评论,似乎此错误是由于我一直遇到的文件名中一组不可解析和不可编码的字节。因此,在 ruby 中可能无法正确处理这样命名的文件。

需要明确的是,任何包含此类不可编码字节的字符串都会出现此问题,而不仅仅是文件名。

因此,我只是在命令行上检查这种不可解析的字符串,如果遇到任何错误,我会退出 ruby 脚本并出现错误。

以下是我改进的测试程序,它显示了我现在如何处理这种情况:

#!/usr/bin/env ruby                                                                                                               
# -*- ruby -*-                                                                                                                                        

require 'optparse'

badargs = []
ARGV.each {
  |arg|
  begin
    # The following test is being used because this                                                                                                   
    # error shows up in a regex match of the argument                                                                                               
    # when done within the OptionParser code. There are
    # no doubt other and possibly better ways to trigger
    # this error, but this is good enough for me for the
    # time being, especially because this is just an
    # illustrative test program.                                                                                                      
    arg =~ /./m
  rescue
    badargs << arg
  end
}

nbad = badargs.length
if nbad > 0 then
  if nbad == 1 then
    object = 'this command-line argument'
  else
    object = 'these command-line arguments'
  end
  puts "Unable to parse #{object}: #{badargs}"
  Process.exit(1)
end

parser = OptionParser.new {
}
parser.parse!

Process.exit(0)

我暂时将其视为我的“答案”,除非出现更好的情况。
现在确实出现了一个更好的答案。在这里查看我的另一个答案。

1赞 HippoMan 2/25/2023 #2

我想出了这个略显诡异的解决方法,在我看来,它比我在这里发布的另一个答案更好。

因此,这是我的首选答案。

我编写了一个名为预处理器的预处理器,它试图根据程序的命令行参数确定哪种编码可能适用于给定的 ruby 程序(请参阅下面的代码以检查代码)。然后,所有符合的 ruby 程序只需要按如下方式编写......ruby-preprocruby-preproc

#!/usr/bin/env ruby-preproc
# -*- ruby -*-

[ ... normal ruby code goes here ... ]

如果调用使用此约定的 ruby 程序,那么它将被正常调用:the-script.rb

./the-script.rb args ...

此外,此预处理器还支持使用特殊的、可选的初始参数 .在这种情况下,将强制执行指定的编码,而不是检查参数列表。例如,对于任何设置为使用此预处理器的 ruby 程序,可以执行以下操作......-E<encoding>

./the-script.rb -EISO-8859-1 args

如果给出初始参数,则处理器会检查所有已指定的命令行参数,并查找适用于每个参数的编码。如果找到此类编码,则使用指定的编码运行脚本。-E<encoding>ruby-preproc

这是代码(这是我昨天在这个“答案”中发布的原版的改进版本)......ruby-preproc

#!/opt/local/rubies/ruby-3.3.0/bin/ruby  
# -*- ruby -*-

# Note that any recent standard ruby executable can be
# used in the initial shebang line.
#
# Also note that the following construct is a way to
# obtain the value of whatever appears in the shebang
# line, so that this file name doesn't need to be
# entered twice in this program:

require 'rbconfig'
ruby_executable = File.join(RbConfig::CONFIG["bindir"],
                            RbConfig::CONFIG["RUBY_INSTALL_NAME"] +
                            RbConfig::CONFIG["EXEEXT"])

$default_enc = 'default'
$tried       = []
$success     = true
$prog        = ARGV[0]

nargs = ARGV.length
if nargs > 1 && ARGV[1] =~ /^-E(.+)$/ then
  # If we're here, then the -E<encoding> parameter
  # appears on the command line. Just use that
  # specified encoding.

  $curr_enc = $1
  $success  = true
  $args     = [ $prog ] + ARGV[2..-1]
else
  # If we're here, no -E<encoding> was specified,
  # so examine the command-line arguments in order
  # to find out whether any of the following listed
  # encodings might work properly for all of these
  # arguments.
  #
  # Put as many encodings into this list as is desired.
  # And I believe it's best to also include $default_enc.

  encodings_to_try = [
    $default_enc,
    'UTF-8',
    'ISO-8859-1',
  ]

  $curr_enc = $default_enc
  $args     = ARGV

  encodings_to_try.each {
    |enc|
    $success  = true
    $curr_enc = enc
    $args.each {
      |arg|
      newarg = arg.dup
      begin
        if enc != $default_enc then
          newarg.encode!(enc)
        end
        newarg =~ /.?/m
      rescue Exception => e
        if e.to_s.include?('invalid byte sequence')
          $success = false
          break
        end
      end
    }
    if $success then
      break
    else
      $tried << $curr_enc
    end
  }
end

if $success then
  if $curr_enc == $default_enc then
    Process.exec(ruby_executable, *$args)
  else
    Process.exec(ruby_executable, "-E#{$curr_enc}", *$args)
  end
else
  tlen = $tried.length
  if tlen < 1 then
    via = ''
  elsif tlen == 1 then
    via = " via this encoding: #{$tried[0]}"
  else
    via = " via any of these encodings: #{$tried}"
  end
  puts("Unable to run `#{$prog}` because one or more arguments cannot be parsed#{via}")
  Process.exit(1)
end