Report errors to error output stream

Error messages should be written to stderr, not stdout. We should also return a non-zero value when we fail - ideally EXIT_FAILURE, defined in <stdlib.h>:

if (argc < 2) {
    fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
    return EXIT_FAILURE;
}

Note also that I've interpolated argv[0], so as to print the actual name we were invoked as, rather than having to match the code and the build system.

Accept many arguments

Users would like to be able to operate on multiple files at once:

NoSpace *

We can support this (and I've hinted at this above, if you spotted the test argc < 2) by looping over the arguments:

for (int i = 1;  i < argc;  ++i) {
    const char *const src = argv[i];
    unspace(src);
}

No-action-required is not an error

Possibly an opinion point, but I would suggest that a file that doesn't need any replacement shouldn't be considered a failure.

Avoid overwriting existing files

We don't want to accidentally destroy data if the target filename already exists. For Linux, we can avoid this without risk of a race between checking and acting by using the renameat2() system call:

    renameat2(AT_FDCWD, file_name_old, AT_FDCWD, file_name_new, RENAME_NOREPLACE);

Buffer size (BUG)

We allocate strlen(src) characters as buffer, but we need one more, to account for the terminating NUL character. Note that strlen returns a size_t, not an int (although it's unlikely to make a practical difference here).

Replace during copy

We can perform the substitution as we copy:

    char *const src = argv[i];
    size_t filename_size = strlen(argv[i]);
    char dest[filename_size+1];
    for (char *p = src, *q = dest;  *p;  ++p, ++q)
        if (*p == ' ')
            *q = '_';
        else
            *q = *p;
    dest[filename_size] = '\0';

More compactly:

    for (char *p = src, *q = dest;  *p;  ++p, ++q)
        *q = *p == ' ' ? '_' : *p;

Note that we use character constants ' ' and '_', so our code works in non-ASCII environments and so that its intention is clear without needing comments.

Check the result of the rename() call

The library call may fail - perhaps the destination name exists (and we're using the RENAME_NOREPLACE flag), or the directory is not writable.

    if (renameat2(AT_FDCWD, src, AT_FDCWD, dest, RENAME_NOREPLACE)) {
        perror(src);
    }

We can make this more portable, by testing whether it's supported:

#if _POSIX_C_SOURCE >= 200809L
#define rename(src, dest)  renameat2(AT_FDCWD, src, AT_FDCWD, dest, RENAME_NOREPLACE);
#endif

Complete program

#include <fcntl.h>

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

#if _POSIX_C_SOURCE >= 200809L
#define rename(src, dest)  renameat2(AT_FDCWD, src, AT_FDCWD, dest, RENAME_NOREPLACE);
#endif

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        return EXIT_FAILURE;
    }

    int ret_val = EXIT_SUCCESS;
    for (int i = 1;  i < argc;  ++i) {
        char *const src = argv[i];
        size_t filename_size = strlen(argv[i]);
        char dest[filename_size+1];
        for (char *p = src, *q = dest;  *p;  ++p, ++q)
            *q = *p == ' ' ? '_' : *p;
        dest[filename_size] = '\0';

        if (rename(src, dest)) {
            perror(src);
            ret_val = EXIT_FAILURE;
        }
    }

    return ret_val;
}

Enhancement suggestions

You might want to look into accepting some option flags to adjust behaviour. I suggest the following (modelled on the mv command):

  • -v for verbose output: print a line for every file successfully moved
  • -f to overwrite existing files without checking
  • -i to ask before overwriting an existing file
Answer from Toby Speight on Stack Exchange
Top answer
1 of 4
2

Report errors to error output stream

Error messages should be written to stderr, not stdout. We should also return a non-zero value when we fail - ideally EXIT_FAILURE, defined in <stdlib.h>:

if (argc < 2) {
    fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
    return EXIT_FAILURE;
}

Note also that I've interpolated argv[0], so as to print the actual name we were invoked as, rather than having to match the code and the build system.

Accept many arguments

Users would like to be able to operate on multiple files at once:

NoSpace *

We can support this (and I've hinted at this above, if you spotted the test argc < 2) by looping over the arguments:

for (int i = 1;  i < argc;  ++i) {
    const char *const src = argv[i];
    unspace(src);
}

No-action-required is not an error

Possibly an opinion point, but I would suggest that a file that doesn't need any replacement shouldn't be considered a failure.

Avoid overwriting existing files

We don't want to accidentally destroy data if the target filename already exists. For Linux, we can avoid this without risk of a race between checking and acting by using the renameat2() system call:

    renameat2(AT_FDCWD, file_name_old, AT_FDCWD, file_name_new, RENAME_NOREPLACE);

Buffer size (BUG)

We allocate strlen(src) characters as buffer, but we need one more, to account for the terminating NUL character. Note that strlen returns a size_t, not an int (although it's unlikely to make a practical difference here).

Replace during copy

We can perform the substitution as we copy:

    char *const src = argv[i];
    size_t filename_size = strlen(argv[i]);
    char dest[filename_size+1];
    for (char *p = src, *q = dest;  *p;  ++p, ++q)
        if (*p == ' ')
            *q = '_';
        else
            *q = *p;
    dest[filename_size] = '\0';

More compactly:

    for (char *p = src, *q = dest;  *p;  ++p, ++q)
        *q = *p == ' ' ? '_' : *p;

Note that we use character constants ' ' and '_', so our code works in non-ASCII environments and so that its intention is clear without needing comments.

Check the result of the rename() call

The library call may fail - perhaps the destination name exists (and we're using the RENAME_NOREPLACE flag), or the directory is not writable.

    if (renameat2(AT_FDCWD, src, AT_FDCWD, dest, RENAME_NOREPLACE)) {
        perror(src);
    }

We can make this more portable, by testing whether it's supported:

#if _POSIX_C_SOURCE >= 200809L
#define rename(src, dest)  renameat2(AT_FDCWD, src, AT_FDCWD, dest, RENAME_NOREPLACE);
#endif

Complete program

#include <fcntl.h>

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

#if _POSIX_C_SOURCE >= 200809L
#define rename(src, dest)  renameat2(AT_FDCWD, src, AT_FDCWD, dest, RENAME_NOREPLACE);
#endif

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        return EXIT_FAILURE;
    }

    int ret_val = EXIT_SUCCESS;
    for (int i = 1;  i < argc;  ++i) {
        char *const src = argv[i];
        size_t filename_size = strlen(argv[i]);
        char dest[filename_size+1];
        for (char *p = src, *q = dest;  *p;  ++p, ++q)
            *q = *p == ' ' ? '_' : *p;
        dest[filename_size] = '\0';

        if (rename(src, dest)) {
            perror(src);
            ret_val = EXIT_FAILURE;
        }
    }

    return ret_val;
}

Enhancement suggestions

You might want to look into accepting some option flags to adjust behaviour. I suggest the following (modelled on the mv command):

  • -v for verbose output: print a line for every file successfully moved
  • -f to overwrite existing files without checking
  • -i to ask before overwriting an existing file
2 of 4
4

Although this indeed does something similar to your goal, here are some criticisms / improvements:

  1. There's absolutely no need to take a copy of the original filename.
  2. If you meet an error condition, return 1 (or better EXIT_FAILURE from stdlib.h), so scripts calling your tool get a chance to detect the error.
  3. Use functions; this helps your code to keep structure
  4. Your tool will not work correctly for filenames with paths, so better search from the end and stop replacing when you hit a path separator character.
  5. When iterating through a string, it's often easier to just use pointers instead of array indexing.

I'd suggest something like this:

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

#ifdef _WIN32
#define isPathSep(x) ((x) == '\\' || (x) == '/')
#else
#define isPathSep(x) ((x) == '/')
#endif

char *sanitizeFileName(const char *filename)
{
    size_t namelen = strlen(filename);

    char *sanitized = malloc(namelen + 1);
    if (!sanitized)
    {
        perror("malloc");
        exit(EXIT_FAILURE);
    }

    strcpy(sanitized, filename);

    char *p = sanitized + namelen - 1;
    while (p >= sanitized)
    {
        if (isPathSep(*p)) break;
        if (*p == ' ') *p = '_';
        --p;
    }

    return sanitized;
}

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s [filename]\n", argv[0]);
        return EXIT_FAILURE;
    }

    char *safename = sanitizeFileName(argv[1]);

    // instead of rename(), for demonstration:
    printf("%s -> %s\n", argv[1], safename);

    free(safename);

    return EXIT_SUCCESS;
}
Discussions

Whitespace replace in C language - Stack Overflow
What is the right way to replace a white space with _ in string passCode with 2 characters? In the end it should input/output: (a ) → (a_). Is there a way to do this using the isspace? isspace(pa... More on stackoverflow.com
🌐 stackoverflow.com
c++ - Replace space with an underscore - Stack Overflow
I am trying to write something that will replace all the spaces in a string with an underscore. What I have so far. string space2underscore(string text) { for(int i = 0; i More on stackoverflow.com
🌐 stackoverflow.com
How to replace space(s) in a string by another charater like and underscore (_)? - Statalist
So, my question is: what code would replace space(s) in a string by another character like an underscore (_)? ... Four arguments are needed for that function, not three. ... subinstr(s1,s2,s3,n) Description: s1, where the first n occurrences in s1 of s2 have been replaced with s3 subinstr() ... More on statalist.org
🌐 statalist.org
April 9, 2020
How can I replace a space in a string with an underscore in C#? - Stack Overflow
I have strings such as: var abc = "Menu Link"; Is there a simple way I can change the space to an underscore? More on stackoverflow.com
🌐 stackoverflow.com
May 1, 2016
Top answer
1 of 1
2

Can I write a single-parameter macro which takes a sequence of words/tokens separated by whitespace, and produces the same sequence but with underscores between each word/token?

Of course, it's not possible. There are no string manipulation utilities in preprocessor.

Och, who am I kidding. First, you have to build a dictionary with all possible words combinations. For the purpose of this, we will have a small dictionary with few words:

#define WORD_world  world,
#define WORD_new    new,
// etc.

You might get the pattern. Then let's implement the macro that will do the following:

brave new  world                 // our starting argument
WORD_##brave new world           // add WORD_ to all arguments and join arguments with spaces
WORD_brave new world
brave, new world                 // expand WORD_brave macro
WORD_brave WORD_new world        // add WORD_ to all arguments and join arguments with spaces
brave, new, world                // expand WORD_* macros
WORD_brave WORD_new WORD_world   // add WORD_ to all arguments and join arguments with spaces
brave, new, world,               // expand WORD_* macros
     /* --- repeat above steps up to maximum words you need to handle --- */
brave_new_world                  // join arguments with `_` ignoring last empty one

The following code:

// our dictionary
#define WORD_world  world,
#define WORD_new    new,
#define WORD_brave  brave,
#define WORD_hello  hello,
#define WORD_Hello  Hello,

// the classics
#define COMMA(...)  ,
#define FIRST(a, ...)  a

// apply function f for each argument recursively with tail
#define FOREACHTAIL_1(f,a)      f(a,)
#define FOREACHTAIL_2(f,a,...)  f(a,FOREACHTAIL_1(f,__VA_ARGS__)) 
#define FOREACHTAIL_3(f,a,...)  f(a,FOREACHTAIL_2(f,__VA_ARGS__)) 
#define FOREACHTAIL_4(f,a,...)  f(a,FOREACHTAIL_3(f,__VA_ARGS__)) 
#define FOREACHTAIL_N(_4,_3,_2,_1,N,...)  \
        FOREACHTAIL_##N
#define FOREACHTAIL(f,...) \
        FOREACHTAIL_N(__VA_ARGS__,4,3,2,1)(f,__VA_ARGS__)

// if there are two arguments, expand to true. Otherwise false.
#define IFTWO_N(_0,_1,N,...)     N
#define IFTWO(true, false, ...)  IFTWO_N(__VA_ARGS__, true, false)

// If empty, expand to true, otherwise false.
// https://gustedt.wordpress.com/2010/06/08/detect-empty-macro-arguments/
#define IFEMPTY(true, false, ...)  IFTWO(true, false, COMMA __VA_ARGS__ ())

// Join arguments with `_`.
#define JOIN_U(a, b)      a##_##b
#define JOIN_TWO_IN(a,b)  IFEMPTY(FIRST, JOIN_U, b)(a, b)
#define JOIN_TWO(a,b)     JOIN_TWO_IN(a,b)
#define JOIN(...)         FOREACHTAIL(JOIN_TWO, __VA_ARGS__)

// Append WORD_ to each argument and join arguments with spaces.
#define WORD_             /* the last one expands to empty */
#define WORDS_TWO(a, b)   WORD_##a b
#define WORDS(...)        FOREACHTAIL(WORDS_TWO, __VA_ARGS__)

#define MAGIC_MACRO(a)  JOIN(WORDS(WORDS(WORDS(WORDS(WORDS(a))))))

MAGIC_MACRO(brave new  world)
MAGIC_MACRO(Hello world)

Produces:

brave_new_world
Hello_world
🌐
Cprogramming
cboard.cprogramming.com › c-programming › 70053-converting-underscores-into-spaces.html
Converting underscores into spaces?
September 24, 2005 - How do I take all the underscores inside of a char, and then convert them into spaces. I don't want to print them out after, but rather replace that same variable with the new non-underscored phrase. for example, if I have char name = "I_love_you"; I want to replace name with "I love you" sorry, ...
Top answer
1 of 5
28

You've got your getline issue fixed but I just wanted to say the Standard Library contains a lot of useful functions. Instead of a hand-rolled loop you could do:

std::string space2underscore(std::string text)
{
    std::replace(text.begin(), text.end(), ' ', '_');
    return text;
}

This works, it's fast, and it actually expresses what you are doing.

2 of 5
16

The problem is that cin >> word is only going to read in the first word. If you want to operate on a whole like at a time, you should use std::getline.

For example:

std::string s;
std::getline(std::cin, s);
s = space2underscore(s);
std::cout << s << std::endl;

Also, you may want to check that you actually were able to read a line. You can do that like this:

std::string s;
if(std::getline(std::cin, s)) {
    s = space2underscore(s);
    std::cout << s << std::endl;
}

Finally, as a side note, you could probably write your function in a cleaner way. Personally I would write it like this:

std::string space2underscore(std::string text) {
    for(std::string::iterator it = text.begin(); it != text.end(); ++it) {
        if(*it == ' ') {
            *it = '_';
        }
    }
    return text;
}

Or for bonus points, use std::transform!

EDIT: If you happen to be lucky enough to be able to use c++0x features (and I know that's a big if) you could use lambdas and std::transform, which results in some very simple code:

std::string s = "hello stackoverflow";
std::transform(s.begin(), s.end(), s.begin(),  {
    return ch == ' ' ? '_' : ch;
});
std::cout << s << std::endl;
🌐
Mefancy
mefancy.com › textchange › replace-space-underscore
Replace Spaces with Underscores (and more) - Online Text Tool | MeFancy
Free online tool to replace spaces with underscores, hyphens, or custom characters. Also converts underscores/hyphens back to spaces. Fast, secure, and client-side.
Find elsewhere
🌐
Bytes
bytes.com › home › forum › topic
How to replace underscore with space in std::string - Post.Byes
July 23, 2005 - Re: How to replace underscore with space in std::string std::replace, perhaps? #include <algorithm> #include <iostream> #include <string> int main() { std::string s("this is a test"); std::replace(s. begin(), s.end(), ' ', '_'); std::cout << s << '\n'; }
🌐
Statalist
statalist.org › forums › forum › general-stata-discussion › general › 1545744-how-to-replace-space-s-in-a-string-by-another-charater-like-and-underscore-_
How to replace space(s) in a string by another charater like and underscore (_)? - Statalist
April 9, 2020 - So, my question is: what code would replace space(s) in a string by another character like an underscore (_)? ... Four arguments are needed for that function, not three. ... subinstr(s1,s2,s3,n) Description: s1, where the first n occurrences in s1 of s2 have been replaced with s3 subinstr() is intended for use with only plain ASCII characters and for use by programmers who want to perform byte-based substitution.
🌐
Codecademy
codecademy.com › forum_questions › 54208e6c80ff336390000948
How to substitute spaces for an underscore? | Codecademy
I'm trying to substitute any spaces that the 'title' input may have, with an underscore. That way it is semantically correct when it gets converted to...
🌐
Google Groups
groups.google.com › g › vim_use › c › ixgPuKgUSuo
Replace spaces with underscore after some pattern in line
Here, the expression says, any whitespace (\s) that is preceded by "XXX.*" be replaced with "_". ... Either email addresses are anonymous for this group or you need the view member email addresses permission to view the original message ... >I am a newbie to vim, but would like to learn it . Please tell me what >exactly does 1_ do in your command ? I knew that you wanted to substitute by underscore, but why >1 was there?
🌐
Cplusplus
cplusplus.com › forum › general › 269651
Replace any space ' ' by '_' in 2-charac - C++ Forum
In this topic using std::replace_if would be the way to go. ... I think you may be battling against the stream and modern programming style, @fewdiefie. If you are iterating through an entire array then a range-based loop is simpler, safer and possibly open to multi-processor or multi-thread optimisation. If you compare with python, there the standard loop is a range-based loop ("for x in A:").
Top answer
1 of 2
1

There is no reason to dynamically allocate storage for your name and surname. Looking at your input, neither will exceed 9-characters, so simply using an array for each of 64-chars provides 6X the storage required (if you are unsure, double that to 128-chars and have 1200% additional space). That avoids the comparatively expensive calls to malloc.

To check whether keyword exists in nameList[i], you don't need to separate the values first and then compare. Simply use strstr (nameList[i], keyword) to determine if keyword is contained in nameList[i]. If you then want to match only the name or surname you can compare again after they are separated. (up to you)

To parse the names from the nameList[i] string, all you need is a single pointer to locate the '_' character. A simple call to strchr() will do and it does not modify nameList[i] so there is no need to duplicate.

After using strchr() to locate the '_' character, simply memcpy() from the start of nameList[i] to your pointer to your name array, increment the pointer and then strcpy() from p to surname. Now you have separated name and surname, simply call toupper() on the first character of each and then output the names separate by a space, e.g.

...
#include <ctype.h>

#define NLEN 64

void searchKeyword (const char *nameList[], int n, const char keyword[])
{
    for (int i = 0; i < n; i++) {               /* loop over each name in list */
        if (strstr (nameList[i], keyword)) {    /* does name contain keyword? */
            char name[NLEN], surname[NLEN];     /* storage for name, surname */
            const char *p = nameList[i];        /* pointer to parse nameList[i] */
            if ((p = strchr(p, '_'))) {         /* find '_' in nameList[i] */
                /* copy first-name to name */
                memcpy (name, nameList[i], p - nameList[i]);
                name[p++ - nameList[i]] = 0;    /* nul-terminate first name */
                *name = toupper (*name);        /* convert 1st char to uppwer */
                /* copy last name to surname */
                strcpy (surname, p);
                *surname = toupper (*surname);  /* convert 1st char to upper */

                printf ("%s %s\n", name, surname);  /* output "Name Surname" */
            } 
        }
    }
}

Example Use/Output

Used with the remainder of your code, searching for "james" locates those names containing "james" and provides what looks like the output you requested, e.g.

$ ./bin/keyword_surname
Enter a keyword: james

James Bale
James Willis
Michael James

zoe_bale
sam_rodriguez
jack_alonso
david_studi
denzel_feldman
james_bale
james_willis
michael_james
dustin_bale

(note: to match only the name or surname add an additional strcmp before the call to printf to determine which you want to output)

Notes On Your Existing Code

Additional notes continuing from the comments on your existing code,

char *str = (char *) malloc((strlen(nameList[0])+1)*sizeof(char));

should simply be

str = malloc (strlen (nameList[i]) + 1);

You have previously declared char *str; so the declaration before your call to malloc() shadows your previous declaration. If you are using gcc/clang, you can add -Wshadow to your compile string to ensure you are warned of shadowed variables. (they can have dire consequences in other circumstances)

Next, sizeof (char) is always 1 and should be omitted from your size calculation. There is no need to cast the return of malloc() in C. See: Do I cast the result of malloc?

Your comparison if (nameList[i] == '_') is a comparison between a pointer and integer and will not work. Your compiler should be issuing a diagnostic telling you that is incorrect (do not ignore compiler warnings -- do not accept code until it compiles without warning)

Look things over and let me know if you have further questions.

2 of 2
0

that worked for me and has no memory leaks.

void searchKeyword(const char * nameList[], int n, const char keyword[])
{
    int found = 0;
    const char delim = '_';

    for (int i = 0; i < n; i++) {
        const char *fst = nameList[i];

        for (const char *tmp = fst; *tmp != '\0'; tmp++) {
            if (*tmp == delim) {
                const char *snd = tmp + 1;

                int fst_length = (snd - fst) / sizeof(char) - 1;
                int snd_length = strlen(fst) - fst_length - 1;

                if (strncmp(fst, keyword, fst_length) == 0 ||
                        strncmp(snd, keyword, snd_length) == 0) {
                    found = 1;
                    printf("%c%.*s %c%s\n",
                           fst[0]-32, fst_length-1, fst+1,
                           snd[0]-32, snd+1);
                }

                break;
            }
        }
    }

    if (!found)
        puts("No such keyword found");
}

hopefully it's fine for you too, although I use string.h-functions very rarely.

🌐
Reddit
reddit.com › r/rlanguage › how to convert a space to an underscore, but not quite that easy
r/Rlanguage on Reddit: How to convert a space to an underscore, but not quite that easy
March 1, 2022 -

I am working on a class using R and some bike share data from 3 different cities. I used the code below to select the top 5 start stations for each city.

You can see the line that says:

names(popStart) = c(paste('Most_common_', City, '_start_station, sep=' '), 'Count')

This works just fine for me to then be able to plot like this:

ggplot(aes(x=Most_common_Chicago_start_station, y=Count), data=chiStart) +

geom_bar(stat="identity") +

theme(axis.text.x = element_text(angle = 90)) +

coord_flip() +

ggtitle('Chicago most common start staion')

That works fine for Chicago and Washington, but since the City name for new york is New York City with the spaces it doesn't work when I do:

ggplot(aes(x=Most_common_New York City_start_station, y=Count), data=chiStart) +

It doesn't work because of the spaces in the City name. I could obviously update the City name in the data frame to have underscores, but that seems overboard just for this one piece. Is there any way I can make this work inline by modifying the code I have provided? I am very new to R, so I may be missing something simple

🌐
PhraseFix
phrasefix.com › tools › replace-spaces
Replace All Spaces Tools - PhraseFix
Use this tool to replace any horizontal whitespace with a comma, underscore, period, dash, or any text you desire. Replace Spaces Example
🌐
Avantix Learning
avantixlearning.ca › home › how to replace spaces in excel with underscores (_), dashes (-) or other values
How to Replace Spaces in Excel with Underscores (_), Dashes (-) or Other Values
April 13, 2022 - You can also click the Home tab in the Ribbon and select Replace in the Find & Select group. In the Find what box, type a space. In the Replace with box, type an underscore, dash, or other value.
🌐
Bubble
forum.bubble.io › need help
How to convert all the spaces in an input to underscores - Need help - Bubble Forum
February 21, 2023 - Here for this input i want to covert all the spaces to underscores Kindly guide
🌐
Online Text Tools
onlinetexttools.com › replace-text-spaces
Replace Spaces in Text – Online Text Tools
It works with the three standard whitespaces – regular spaces, tabs, and newlines. For example, if you enable the "Replace Spaces" option and set the new space character to the underscore symbol "_", the phrase "Never give up" will become ...