I'm new to C as in I've only studied it for a year and it's my first language. I still don't know how to handle real world code and so would like to see how a real world programmer would write up a program that can find and replace text in a text file.
It took me 3 days to figure this out and I've tried my best to write it to standards. If anyone can suggest a way to improve this code without resorting to advanced techniques such as vectors or data structures, that would be great.
Here is my attempt:
#include <string.h>
#include <stdlib.h>
#define CUTOFF 100
void *malloc_safe(size_t size); // Wrapper for malloc() that includes exiting the program upon failure of dynamic memory allocation
void *realloc_safe(void *ptr, size_t new_size); // Wrapper for realloc() that includes exiting the program upon failure of dynamic memory allocation
char *dynamic_input_fgets(void); // Initiates an input sequence with fgets() and returns the user input as a string in dynamically allocated memory
void strcpy_0n(char *dest, const char *source, size_t num); // Copies `num` number of characters from `source` into `dest`, but without copying the null-terminator into the destination.
void strshift(char *str, int num, int direction); // Shifts all characters in the null terminated string `str` by `num` indexes left or right depending on the integer `direction`. direction can only be 1(shift right) or -1(shift left).
int main(void)
{
char file_loc[1024];
char *find_str, *replace_str, *file_content_stream;
FILE *fp;
int i, cur_sizeof_malloc_str;
printf("Find and Replace a string in a text file. \nEnter file location: ");
fgets(file_loc, 1024, stdin);
if (strrchr(file_loc, '\n') != NULL)
*(strrchr(file_loc, '\n')) = '\0';
fp = fopen(file_loc, "r+");
if (fp == NULL)
{
printf("\nError opening file: Are you sure the file exists? ");
return 5;
}
printf("\nEnter string to find: ");
find_str = dynamic_input_fgets();
printf("\nEnter string to replace: ");
replace_str = dynamic_input_fgets();
// import file contents into program memory
file_content_stream = malloc_safe(CUTOFF + 5);
cur_sizeof_malloc_str = CUTOFF + 5;
*file_content_stream = 0;
while (1)
{
char tempstore[CUTOFF] = {0};
if (fgets(tempstore, CUTOFF, fp) == NULL)
{
if (feof(fp))
break;
printf("\n\nAbnormal program termination. File access function \"fgets()\" failed");
return 126;
}
strcat(file_content_stream, tempstore);
cur_sizeof_malloc_str = cur_sizeof_malloc_str + CUTOFF;
file_content_stream = realloc_safe(file_content_stream, cur_sizeof_malloc_str);
}
fclose(fp);
// find and replace action
for (i = 0; ; i++)
{
char *occurrence = strstr(file_content_stream, find_str);
if (occurrence == NULL)
break;
else
{
// turns out find and replace is easier said than done
// case: replace string is smaller or equal to find string...
{
int length_find_str = strlen(find_str);
int length_replace_str = strlen(replace_str);
// reallocate to increase space for larger string if string is longer...
if (length_find_str < length_replace_str)
{
int offset = length_replace_str - length_find_str;
file_content_stream = realloc_safe(file_content_stream, strlen(file_content_stream) + offset + 5);
// this causes occurrence to become invalid, refind occurrence again...
occurrence = strstr(file_content_stream, find_str);
if (occurrence == NULL)
break;
strshift(occurrence + length_find_str, offset, 1);
strcpy_0n(occurrence, replace_str, length_replace_str);
}
else // replace_str is smaller or equal, so no need to reallocate...
{
int offset = length_find_str - length_replace_str;
if (offset == 0)
{
strcpy_0n(occurrence, replace_str, length_replace_str);
}
else
{
strcpy_0n(occurrence, replace_str, length_replace_str);
strshift(occurrence + length_find_str, offset, -1);
}
}
}
}
}
if (i == 0)
{
printf("No matches found. ");
return 0;
}
else
{
fp = fopen(file_loc, "w");
if (fp == NULL)
{
printf("\nError opening/creating file: Are you sure the file exists? ");
return 5;
}
fputs(file_content_stream, fp);
printf("\n\nFound and replaced %d match(es)", i);
fclose(fp);
return 0;
}
}
void *malloc_safe(size_t size)
{
void *addr = malloc(size);
if (addr == NULL)
{
printf("\n\nAbnormal program termination. Call to dynamic memory allocation failed");
exit(255);
}
else
return addr;
}
void *realloc_safe(void *ptr, size_t new_size)
{
void *addr = realloc(ptr, new_size);
if (addr == NULL)
{
printf("\n\nAbnormal program termination. Call to dynamic memory allocation failed");
exit(255);
}
else
return addr;
}
char *dynamic_input_fgets(void)
{
char *str = malloc_safe(CUTOFF + 5);
int cur_sizeof_malloc_str = CUTOFF + 5;
*str = 0;
while (1)
{
char tempstore[CUTOFF] = {0};
fgets(tempstore, CUTOFF, stdin);
strcat(str, tempstore);
if (strrchr(str, '\n') != NULL) // so it has a newline at the end which means user ended input
{
str[strlen(str) - 1] = '\0';
break;
}
cur_sizeof_malloc_str = cur_sizeof_malloc_str + CUTOFF;
str = realloc_safe(str, cur_sizeof_malloc_str);
}
return str;
}
void strcpy_0n(char *dest, const char *source, size_t num)
{
size_t i;
for (i = 0; i < num; i++)
{
*dest = *source;
dest++;
source++;
}
}
void strshift(char *str, int num, int direction)
{
int i = strlen(str);
// strshift assumes there is enough allocated memory to support shift left/right movements and hence makes no checks on illegal memory access. Ensure you have allocated enough space in memory to support shift right and left.
switch (direction)
{
case 1: // shift right by num indexes/chars
for(; i >= 0 ; i--)
str[i + num] = str[i];
num--;
for(; num >= 0; num--)
str[num] = 178; // masking agent for debug, usually this shouldn't even be visible once you have overwritten it.
break;
case -1: // shift left by num indexes/chars
for (i = -num; str[i + num] != 0; i++)
str[i] = str[i + num];
str[i] = str[i + num]; // copies the null terminator too, couldn't figure out how to put this into the for loop without messing up the program.
break;
default:
printf("Secondary compile error: Illegal parameter int direction for function strshift(). Program aborting.");
exit(255);
}
}
```