【问题标题】:Smart variadic expansion based on format string基于格式字符串的智能可变参数扩展
【发布时间】:2010-10-17 12:49:21
【问题描述】:

我有一个守护进程,它读取配置文件以便知道在哪里写东西。在配置文件中,存在这样一行:

output = /tmp/foo/%d/%s/output

或者,它可能看起来像这样:

output = /tmp/foo/%s/output/%d

...或者简单地说:

output = /tmp/foo/%s/output

...或者最后:

output = /tmp/output

我在我的程序中将该行作为 cfg->pathfmt。我现在想做的是想出一些巧妙的使用方法。

稍微解释一下,路径最多可以包含两个要格式化的组件。 %d 将扩展为作业 ID (int),%s 将扩展为作业名称(字符串)。用户可能希望在配置文件中使用一个、两个或一个都不使用。在我最终将其传递给 snprintf() 之前,我需要知道他们想要什么以及按什么顺序。我可以缩小范围,但我一直想与 strtok() 交谈,这看起来很难看。

我想为用户提供这种灵活性,但是我在寻找一种明智、便携的方式来实现它时迷失了方向。我也完全不知道如何开始搜索这个。

如果满足以下条件,我会很高兴:

  • 有人可以帮我缩小搜索范围以找到好的示例
  • 有人可以发布一个链接,指向实现此功能的某个 OSS 项目
  • 有人可以发布一些伪代码

我不想为我编写代码,我只是真的坚持(我认为)应该是非常简单的东西,并且需要一些帮助才能第一口。我真的觉得我想多了,忽略了显而易见的事情。

最终结果应该是这样的布尔函数:

bool output_sugar(const char *fmt, int jobid, const char *jobname, struct job *j);

然后它会在 j->outpath 上调用 snprintf()(明智地),如果某种垃圾(即 % 后跟不是 s、d 或 % 的东西)在配置行中(或其 null),则返回 false。健全性检查很容易,我只是花一点时间来获取参数的数量(和顺序)以正确格式化。

提前致谢。另外,如果您有声誉,请随时编辑此标题,正如我所说,我不太确定如何在一行中提出问题。我认为我需要的是parser,但是使用成熟的词法分析器/解析器来处理一个简单的字符串感觉很尴尬。

【问题讨论】:

  • 我只是在等待有人在这个网站上发布柠檬派对的链接......你让我暂停...... lmao
  • @ojblass,Lemon 非常适合这种事情,并且不会引入新的构建依赖项.. 除非您在 SCM 中跟踪解析器的输出?
  • 不,链接中的柠檬这个名字是我不会点击的。我无法描述原因,但我没有点击带有柠檬这个词的链接。
  • 如果你得到这个工作,记得添加对包含类似 ../../../etc/passwd 的配置文件的保护。
  • @MattMcNabb 是的,当然 - 取决于你如何使用它。请注意,../config/file.dat 有效,../../etc if 在某人的主目录/构建目录中也是如此。我最终做的是检查遍历,并在第 (x) 行需要绝对路径时出错 - 例如/home/jdoe/output' - 在健全性检查中测试读/写访问也很重要。

标签: c string tokenize variadic


【解决方案1】:

是的,您需要某种解析器。不过,它不必很复杂:

void format_filename(const char *fmt, int jobid, const char *jobname,
                     char *buffer, size_t buflen)
{
    char *end = buffer + buflen - 1;
    const char *src = fmt;
    char *dst = buffer;
    char c;
    assert(buffer != 0 && fmt != 0 && buflen != 0 && jobname != 0);
    while ((c = *src++) != '\0')
    {
        if (dst >= end)
            err_exit("buffer overflow in %s(): format = %s\n",
                     __func__, fmt);
        else if (c != '%')
            *dst++ = c;
        else if ((c = *src++) == '\0' || c == '%')
        {
            *dst++ = '%';
            if (c == '\0')
                break;
        }
        else if (c == 's')
        {
            size_t len = strlen(jobname);
            if (len > end - dst)
                err_exit("buffer overflow on jobname in %s(): format = %s\n",
                         __func__, fmt);
            else
            {
                strcpy(dst, jobname);
                dst += len;
            }
        }
        else if (c == 'd')
        {
             int nchars = snprintf(dst, end - dst, "%d", jobid);
             if (nchars < 0 || nchars >= end - dst)
                 err_exit("format error on jobid in %s(); format = %s\n",
                          __func__, fmt);
             dst += nchars;
        }
        else
            err_exit("invalid format character %d in %s(): format = %s\n",
                     c, __func__, fmt);
    }
    *dst = '\0';
}

现在测试代码。请注意,它支持 '%%' 表示法以允许用户在输出中嵌入单个 '%'。此外,它将字符串末尾的单个 '%' 视为有效并等效于 '%%'。出错时调用 err_exit();您可以选择适合您系统的替代错误策略。我只是假设您已包含&lt;assert.h&gt;&lt;stdio.h&gt;&lt;string.h&gt; 以及err_exit()(可变参数)函数的标头。


测试代码...

#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <assert.h>

static void err_exit(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
    exit(1);
}

...然后format_filename() 如上所述,然后...

#define DIM(x) (sizeof(x)/sizeof(*(x)))

static const char *format[] =
{
    "/tmp/%d/name/%s",
    "/tmp/%s/number/%d",
    "/tmp/%s.%d%%",
    "/tmp/%",
};

int main(void)
{
    char buffer[64];
    size_t i;

    for (i = 0; i < DIM(format); i++)
    {
        format_filename(format[i], 1234, "job-name", buffer, sizeof(buffer));
        printf("fmt = %-20s; name = %s\n", format[i], buffer);
    }

    return(0);
}

【讨论】:

  • 乔纳森,谢谢,你把我宠坏了 :) 我的指针算法不是最好的,这是构建我自己的函数的一个很好的例子。将其更改为接受的答案,因为它提供的信息最多。
  • 好吧,现在假设你想为用户名添加 %U...恭喜你让我访问了像 *dst++ = '%'; 这样的代码我会诅咒你直到末日。
  • 灵活性和使用自制符号的代价是您必须对代码进行更改以支持符号的更改。由于没有任何东西可以支持请求者“开箱即用”的要求(据我所知),因此不可避免地需要一种定制的、因此有点特别的解决方案。
  • 我不同意,但承认我并不知道一切。
  • 对于这种特殊用途,字符串(和类型)中的标记数可能永远不会改变。它不必按比例缩放。引入一个解析器来处理一个字符串的想法就像在一个桶里用一个有激光瞄准器的大象枪射击一条鱼......在空白范围内。
【解决方案2】:

如果选项的数量很少,并且您不希望/需要解析器的额外灵活性和复杂性,您可以简单地使用 strstr() 搜索每个潜在的替换子字符串。

如果你只有这两个选项,你可以容忍地创建一个四分支的 if/else 结构(只有 A,只有 B,在 B 之前都有 A,在 A 之前都有 B),在其中调用 sprintf()正确排序的参数。否则,进行多次 sprintf() 调用,每个调用仅替换格式字符串中的第一个替换标记。 (这意味着建立一个需要替换的列表并按出现顺序对它们进行排序......)

【讨论】:

  • 为什么每个人都喜欢创建和解析自己的字符串?
【解决方案3】:

使用 strtok 容易出错。您可以使用 (fl)lex 和 yacc 将变量视为迷你语言。 There is simple tutorial here

%{
#include <stdio.h>
%}

%%
%d                      printf("%04d",jobid);
%s                      printf("%s",stripspaces(dirname));
%%

我制作了一个 ODBC 包装器,可以让您执行类似 dbprintf("insert into blah values %s %D %T %Y", stuff here...); 之类的操作。但那是很多年前的事了,我咬了一下,用 strtok 解析了格式字符串。

【讨论】:

  • 我真的非常希望不要引入词法解析器来处理一个字符串:)
  • 好吧,我知道在我离开那份工作后,他们厌倦了我的 strtok 并用上面的替换它。 John Levine 讨厌手写解析器是有原因的。
  • 是的,如果字符串必须扩展,哎哟。好的..我去解析器。谢谢你的链接 :) 至少,我可以证明使用一个来使配置文件更智能(有点像 BIND)。
猜你喜欢
  • 2020-07-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多