1

Given a format string, a counter variable for the number of specifiers and an array of the strings to be inputted, how could this be printed?

Here's an example:

char *format_str = "str(%s)ing(%s)";
int count = 2;
char **specs = { [0] = "rts", [1] = "gni" };

So, the list of strings aligns respectively with the ordering of specifiers. When printed, the end result would be:

"str(rts)ing(gni)"

Could a function be written to print such a string with any format string & any number of specifiers & respective arguments? I have tried to do this using strtok(), vsprintf, snprintf etc, but I still cannot get it quite right.

EDIT: To clarify, format_str contains count number of specifiers and the array specs contains count number of strings. The proposed function would therefore should print count number of strings into format_str.

13
  • with any format string & any number of specifiers & respective arguments? I do not understand. What does it mean "any"? No, %s expects a char *, it can't take another type. Could you provide more examples? Could you provide what have you tried? Even if it failed it will help as a boilerplate code for others. Could you provide a function declaration of how would you see the interface? What should the input be? how could this be printed? is printf(format_str, specs[0], specs[1]) not enough? Commented Aug 3, 2020 at 14:58
  • Is the string reversing part of the question or just an example? Commented Aug 3, 2020 at 15:03
  • 1
    Is the number of %s in the format string alway equal to count Commented Aug 3, 2020 at 15:04
  • 1
    @4386427 Yes. That's the point. format_str contains <count> number of specifiers and **specs contains <count> number of strings. The function would therefore print <count> number of strings to format_str. Commented Aug 3, 2020 at 15:07
  • 1
    So the count information is redundant - the count of elements is already available via parsing the formatting string? Honestly, this looks like XY problem. Commented Aug 3, 2020 at 15:51

4 Answers 4

2

As others said there is no direct way of doing that. You can build your own function which dumps the values of strings at the correct format specifiers. Below function makes a temporary format string for each %s and appends it to the earlier build string using snprintf().

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXBUF      4096

char *strmaker(char* format, int num_args, char** strings)
{
    char* prnt = calloc(sizeof(char), MAXBUF);
    int prnt_ct = 0;
    char* tmp_fmt = malloc(strlen(format) + 1); // Prepare for the worst case (format == tmp_fmt).
    int fmt_ct = 0;

    /* Append the strings to the prnt buffer */

    for (int i = 0; i < num_args; i++) {
        char* s_loc = strstr(format + fmt_ct, "%s");    // Search the format-string for string specifier (%s)
        if (s_loc == NULL)
            return prnt;

        int tmp_fmt_len = (int) (s_loc + 2 - format - fmt_ct);  // +2 for %s
        strncpy(tmp_fmt, format + fmt_ct, tmp_fmt_len); // Make tmp_fmt
        tmp_fmt[tmp_fmt_len] = '\0';
        fmt_ct = fmt_ct + tmp_fmt_len;

        int p_return = snprintf(prnt + prnt_ct, MAXBUF - prnt_ct, tmp_fmt, strings[i]);   // If no error, return the number characters printed excluding nul (man page)

        if (p_return >= MAXBUF - prnt_ct)   // If buffer overflows (man page)
            return prnt;

        prnt_ct = prnt_ct + p_return;   // Update the index location.
    }

    return prnt;
}

int main(int argc, char *argv[]) // Pass format and arguments
{
    if (argc <= 1)
       return -1;

    char *s = strmaker(argv[1], argc - 2, argv + 2);
    printf("%s\n", s);
    free(s);

    return 0;
}

Terminal Session:

$ ./a.out '%s %s %s' 1 2 3 
1 2 3
$ ./a.out 'one %s two %s three %s' 1 2 3 
one 1 two 2 three 3
$ ./a.out 'one %s two %s three' 1 2 3 
one 1 two 2
$ ./a.out 'one %s two %s three %s' 1 2 
one 1 two 2
Sign up to request clarification or add additional context in comments.

Comments

2

To my knowledge there is no way to give a different number of arguments to printf at runtime.

Therefore you'll have to build the output string yourself.

I'll not dump all the code but only give you some ideas at high level.

#define OUT_STR_SIZE 8192

char* outStr = calloc(OUT_STR_SIZE, 1);   // Allocate an output buffer
assert(outStr  != NULL);
char* tmp = format_str;  // tmp pointer to track how much of the format string
                         // that has been handled
size_t idx = 0;          // next position in output buffer to write
size_t str_idx = 0;      // index of next string to copy when %s is found

while(*tmp)  // Loop the whole format string
{
    if (*tmp = '%' && *(tmp+1) == 's')
    {
        // Copy a string to output buffer
        strcpy(&outStr[idx], specs[str_idx]);  // Append a string from specs
        idx = idx + strlen(str_idx);
        ++str_idx;
        tmp += 2;
    }
    else
    {
        // Copy a single char to output buffer
        outStr[idx] = *tmp;
        ++idx;
        ++tmp;
    }
}
assert(count == str_idx);  // Just checking that all %s was handled

printf("%s", outStr);

free(outStr);

Bad things with the code that need to be fixed

The output string size is fixed at 8192 chars. If that's not always enough, you need to check the available space as you add new chars and use realloc when you run out of space.

The code will fail for format strings like "hello\%s%s" due to the '\'

I'll leave it as an exercise for OP to fix these things.

1 Comment

strcat(outStr, ...) is a terrible idea as it would make the cost of such a simple function quadratic in the length of the final string.
2

If you are lazy, you can do:

int func(const char *fmt, int count, const char **specs) {
    switch(count) {
    case 1: return printf(fmt, specs[0]);
    case 2: return printf(fmt, specs[0], specs[1]);
    case 3: return printf(fmt, specs[0], specs[1], specs[2]);
    // etc. for as many args you want to support
    }
}

If you are not lazy, you should parse %s formatting string on your own (for example as in the other answer).

Comments

0

The C standard library does not provide functions similar to printf that work on a variable number of arguments provided as an array. In order to do what you want, you would have to roll your own.

If you want to dynamically construct such a string, a good old for(...) realloc() loop is the way to go. Here's a simple implementation (it could probably be optimized more).

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

char *my_sprintf(const char *fmt, size_t n, char *const *strings) {
    const char *fmt_start, *fmt_end;
    size_t i, len, prev_len, fmt_len, spec_len;
    char *res, *tmp;

    fmt_start = fmt;
    len = 0;
    res = NULL;

    for (i = 0; i < n; i++) {
        // Find position of next %s format specifier.
        fmt_end = strstr(fmt_start, "%s");
        if (fmt_end == NULL) {
            // Error out if not found.
            free(res);
            return NULL;
        }
        
        // Do some math...
        fmt_len = fmt_end - fmt_start; // Length of current format specifier segment.
        spec_len = strlen(strings[i]); // Length of current string.
        prev_len = len;            // Previous total length.
        len += fmt_len + spec_len; // New total length.

        // Increase the size of the final string.
        tmp = realloc(res, len + 1);
        if (tmp == NULL) {
            // Error out if realloc() fails.
            free(res);
            return NULL;
        }

        res = tmp;
        
        // Copy specifier segment and i-th string at the end of the final string.
        memcpy(res + prev_len, fmt_start, fmt_len);
        memcpy(res + prev_len + fmt_len, strings[i], spec_len);
        
        // Skip current specifier.
        fmt_start = fmt_end + 2;
    }

    // Copy last specifier segment (if needed).
    
    fmt_len = strlen(fmt_start);
    prev_len = len;
    len += fmt_len;

    tmp = realloc(res, len + 1);
    if (tmp == NULL) {
        free(res);
        return NULL;
    }

    res = tmp;
    memcpy(res + prev_len, fmt_start, fmt_len);
    res[len] = '\0';

    return res;
}

int main(int argc, char **argv) {
    char *res = my_sprintf(argv[1], argc - 2, argv + 2);

    if (res != NULL) {
        puts(res);
        free(res);
    } else {
        puts("ERR");
    }

    return 0;
}

I particularly like this approach because it has a few advantages:

  1. No need to know the length of the resulting string upfront, final string is dynamically allocated.
  2. No need to modify any of the strings provided as argument.
  3. Uses memcpy(), iterating over the whole final string twice (one time to check length, one to copy), unlike solutions involving strlen() + strcpy(), which could potentially iterate over the result three times (a common implementation for strcpy(dst, src) is memcpy(dst, src, strlen(src) + 1)).
  4. Simple error checking in case the number of specifiers is not enough (you can decide what to do inside those if statements if you don't want to just return NULL.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.