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
' 'and'_'? Also, your program will not play nice with full qualified path input like"/user/home/My Awesome Folder/My File with Space to be replaced"\$\endgroup\$strchrto search for a particular character in a string to make the code slightly simpler, but you've got it about as efficient as possible. \$\endgroup\$strlen() + 1to account for the terminating'\0'character. These are too short:char file_name_old[filename_size], file_name_new[filename_size];And you need to check the return value fromrename()and not ignore it. \$\endgroup\$strstr( argv[1], " ") == NULLwith simply!strstr( argv[1], " "). Entirely up to you. \$\endgroup\$