【问题标题】:Bug in getopt (C) for non-global use?非全局使用的 getopt (C) 中的错误?
【发布时间】:2020-12-14 15:09:53
【问题描述】:

我正在编写一个从文本文件中解析多个命令的代码,我发现我认为这是 getopt 中的一个错误。只是想知道是否有人以前看过这个?

基本上我有一个循环,在每次迭代中我读取一个字符串,我用'strtok'将它转换为可以馈送到getopt并检查提供的选项的东西。它在某些情况下有效:

$ ./test_short
cmd -ia
  -- option i
  -- option a


cmd -a
  -- option a


cmd -i
  -- option i

但它会失败,例如:

$ ./test_short
cmd -ia
  -- option i
  -- option a


cmd -i -a
cmd: invalid option -- '-'
  -- option unknown
  -- option a
  -- option a

代码可以从https://github.com/angel-devicente/test-getopt 克隆,但为了完整起见,我也在下面放置了简短选项。

知道这是一个真正的错误还是我忘记重置的其他全局变量?

谢谢,

#include <stdio.h>     /* for printf */
#include <stdlib.h>    /* for exit */
#include <string.h>
#include <unistd.h>

#define MAX_ARGS 10
#define MAX_C 100


int main(int argc, char **argv) {
  int c;
  char       cmd[MAX_C+1];

  int argc_i;
  char *argv_i[MAX_ARGS];
  
  
  while (1) {
    // Read string, for example: cmd -i -a
    fgets(cmd,MAX_C,stdin);
    cmd[strlen(cmd) - 1] = 0;
    if (strlen(cmd) <= 1) break;

    // Transform into argc_i, argv_i, to feed to getopt
    argc_i = 0;
    char *p2 = strtok(cmd, " ");
    while (p2 && argc_i < MAX_ARGS)
      {
    argv_i[(argc_i)++] = p2;
    p2 = strtok(NULL, " ");
      }
    argv_i[argc_i] = 0;


    // Reset getopt
    optind  = 1;
    int option_index = 0;

    
    while (1) {
      c = getopt(argc_i, argv_i, "ia");

      if (c == -1)
    break;

      switch (c) {
      case 'a':
    printf("  -- option a\n");
    break;

      case 'i':
    printf("  -- option i\n");
    break;

      case '?':
    printf("  -- option unknown\n");
    break;

      default:
    printf("?? getopt returned character code 0%o ??\n", c);
      }
    }

    printf("\n\n");

    if (optind < argc) {
      printf("non-option ARGV-elements: ");
      while (optind < argc)
    printf("%s ", argv[optind++]);
      printf("\n");
    }
  }

  exit(EXIT_SUCCESS);
}

【问题讨论】:

  • 不清楚您是否将getopt() 重置为迭代之间的起始状态(新的模拟命令行参数集)。确实,没有清楚地记录您是如何做到这一点的;不同的实现可能有不同的规则集。
  • 他做到了:optind = 1;。顺便说一句,optind = 0; 工作正常。 This condition in glibc getopt.c is strange
  • @KamilCuk — 呃;在参加电话会议时很难阅读代码。使用optind = 0; 是更有可能的方法之一。也就是说,在 Mac 上,当程序启动时,optind == 1。但是,在 Mac 上,问题不会重现。
  • 我认为这是 getopt 的 glibc 实现中的一个错误,因为代码没有使用 GNU 扩展或更改 POSIXLY_CORRECT 环境变量,设置 optind = 1;(而不是 GNU-扫描之间的特定optind = 0;) 应该足够了。
  • 检查 RHEL 7.4,问题确实重现了 — 正如我所指出的,getopt() 的不同实现有不同的重置方式,并且并不总是记录你是如何做到的。

标签: c linux getopt


【解决方案1】:

这看起来像是有意偏离了 getopt 的 POSIX 规范。

首先,我在您的代码中更改了这一行:

argv_i[(argc_i)++] = p2;

到这里:

argv_i[(argc_i)++] = strdup(p2);

并将其添加到 while (1) 循环的末尾:

for (i=0;i<argc_i;i++) {
    free(argv_i[i]);
}

并将其添加到顶部:

memset(argv_i,0,sizeof(argv_i));

然后我跑过 valgrind:

==54748== Memcheck, a memory error detector
==54748== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==54748== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==54748== Command: ./x1
==54748== 
optind=1
cmd -ia
arg 0: cmd
arg 1: -ia
  -- option i
  -- option a


cmd -i -a
==54748== Invalid read of size 1
==54748==    at 0x4F192A1: _getopt_internal_r (in /usr/lib64/libc-2.17.so)
==54748==    by 0x4F1A56A: _getopt_internal (in /usr/lib64/libc-2.17.so)
==54748==    by 0x4F1A5B7: getopt (in /usr/lib64/libc-2.17.so)
==54748==    by 0x400A2E: main (x1.c:48)
==54748==  Address 0x5205093 is 3 bytes inside a block of size 4 free'd
==54748==    at 0x4C2AF9D: free (vg_replace_malloc.c:540)
==54748==    by 0x400B25: main (x1.c:89)
==54748==  Block was alloc'd at
==54748==    at 0x4C29EA3: malloc (vg_replace_malloc.c:309)
==54748==    by 0x4EC3AA9: strdup (in /usr/lib64/libc-2.17.so)
==54748==    by 0x400991: main (x1.c:31)
==54748== 
  -- option i
  -- option a

由于测试程序中没有对 malloc 内存的引用,这意味着 getopt 实现保留了指向它的指针。

现在让我们看一下https://github.com/lattera/glibc/blob/master/posix/getopt.c 中的几行代码,包括_getopt_internal_r

/* Index in ARGV of the next element to be scanned.
   This is used for communication to and from the caller
   and for communication between successive calls to 'getopt'.
   On entry to 'getopt', zero means this is the first call; initialize.
   When 'getopt' returns -1, this is the index of the first of the
   non-option elements that the caller should itself scan.
   Otherwise, 'optind' communicates from one call to the next
   how much of ARGV has been scanned so far.  */

/* 1003.2 says this must be 1 before any call.  */
int optind = 1;
...
static struct _getopt_data getopt_data;
...
int
_getopt_internal_r (int argc, char **argv, const char *optstring,
            const struct option *longopts, int *longind,
            int long_only, struct _getopt_data *d, int posixly_correct)
{
  int print_errors = d->opterr;

  if (argc < 1)
    return -1;

  d->optarg = NULL;

  if (d->optind == 0 || !d->__initialized)
    optstring = _getopt_initialize (argc, argv, optstring, d, posixly_correct);

...

int
_getopt_internal (int argc, char **argv, const char *optstring,
          const struct option *longopts, int *longind, int long_only,
          int posixly_correct)
{
  int result;

  getopt_data.optind = optind;
  getopt_data.opterr = opterr;

  result = _getopt_internal_r (argc, argv, optstring, longopts,
                   longind, long_only, &getopt_data,
                   posixly_correct);

  optind = getopt_data.optind;
  optarg = getopt_data.optarg;
  optopt = getopt_data.optopt;

  return result;
}

#define GETOPT_ENTRY(NAME, POSIXLY_CORRECT)         \
  int                               \
  NAME (int argc, char *const *argv, const char *optstring) \
  {                             \
    return _getopt_internal (argc, (char **)argv, optstring,    \
                 0, 0, 0, POSIXLY_CORRECT);     \
  }

#ifdef _LIBC
GETOPT_ENTRY(getopt, 0)
GETOPT_ENTRY(__posix_getopt, 1)
#else
GETOPT_ENTRY(getopt, 1)
#endif

这里我们可以看到 optind 被初始化为 1 和一个未初始化的内部静态结构。还有一条评论说“在进入'getopt'时,零表示这是第一次调用;初始化。”表示不同的初始化条件。

getopt 调用getopt_internal,随后将optind 分配给静态结构的相应成员,然后将其传递给_getopt_internal_r_getopt_internal_r 函数然后检查结构的 optind 成员是否为 0 或 __initialized 成员是否为 0,如果是,则调用初始化函数,定义如下:

static const char *
_getopt_initialize (int argc _GL_UNUSED,
            char **argv _GL_UNUSED, const char *optstring,
            struct _getopt_data *d, int posixly_correct)
{
  /* Start processing options with ARGV-element 1 (since ARGV-element 0
     is the program name); the sequence of previously skipped
     non-option ARGV-elements is empty.  */
  if (d->optind == 0)
    d->optind = 1;

  d->__first_nonopt = d->__last_nonopt = d->optind;
  d->__nextchar = NULL;

  /* Determine how to handle the ordering of options and nonoptions.  */
  if (optstring[0] == '-')
    {
      d->__ordering = RETURN_IN_ORDER;
      ++optstring;
    }
  else if (optstring[0] == '+')
    {
      d->__ordering = REQUIRE_ORDER;
      ++optstring;
    }
  else if (posixly_correct || !!getenv ("POSIXLY_CORRECT"))
    d->__ordering = REQUIRE_ORDER;
  else
    d->__ordering = PERMUTE;

  d->__initialized = 1;
  return optstring;
}

我们可以在这里看到 optind 成员如果当前为 0 则将被设置为 1。另外,请特别注意这一行:

d->__nextchar = NULL;

如果我们再查看定义此字段的https://github.com/lattera/glibc/blob/master/posix/getopt_int.h,我们会看到以下内容:

  /* The next char to be scanned in the option-element
     in which the last option character we returned was found.
     This allows us to pick up the scan where we left off.
     If this is zero, or a null string, it means resume the scan
     by advancing to the next ARGV-element.  */
  char *__nextchar;

因此,此变量会跟踪处理中断的位置。但是回到_getopt_internal_r中的这些行

  if (d->optind == 0 || !d->__initialized)
    optstring = _getopt_initialize (argc, argv, optstring, d, posixly_correct);

仅当optind 为 0 时才会发生重置。

所以在这个实现中,将optind 设置为 0 将重置处理。然而,这不是可移植的,因为 POSIX 指定这样做是未指定的行为。

这意味着要么此代码应更改为将 optind == 1 作为初始化条件,要么应更新文档以反映此与 POSIX 标准的偏差并声明这是一个偏差。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-06-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-14
    相关资源
    最近更新 更多