Update: link to corresponding release
The code presented here is tagged as release version v0.1.0 on Github for easy reference.
Introduction
bump
is a command-line utility that I use to "bump" up the semantic version numbers present in any text file. It is written entirely in C and doesn't use malloc anywhere. Here's a table describing how the various bump levels work:
| Bump | Release | Result on 3.2.6 | |-------|--------------------|-----------------| | Patch | x.y.z -> x.y.(z+1) | 3.2.7 | | Mnior | x.y.z -> x.(y+1).0 | 3.3.0 | | Major | x.y.z -> (x+1).0.0 | 4.0.0 |
The CLI can be used in two ways:
- Interactive mode - when the user simply types
bump
and does not provide any command-line arguments. In this case the program will prompt the user for input. - Silent mode - when the user passes in the command line arguments like
bump -i version.h
and the program verifies the argument, deduces the default value for the other arguments and proceeds silently.
The full source code is available on Github under the MIT License.
Overview of the code
Here's how the algorithm works:
- Read the input from the file line-by-line.
- Process every line character-by-character.
- If we read a digit, start a greedy search for the
x.y.z
pattern. - Proceed until no characters left to read.
The main logic is contained in bump.h
and bump.c
.
bump.h
#ifndef BUMP_H #define BUMP_H #include <stdio.h> #include <stdlib.h> typedef struct version_struct { size_t major; size_t minor; size_t patch; } Version; typedef struct line_state_struct { const char *input; char *output; size_t input_index; size_t output_index; size_t limit; } LineState; typedef struct file_state_struct { FILE *input; FILE *output; size_t limit; const char *bump_level; } FileState; char *initialize_version(Version *version, size_t major, size_t minor, size_t patch); char *initialize_line_state(LineState *state, const char *input, char *output, size_t limit); char *initialize_file_state(FileState *state, const char *input_path, const char *output_path, const char *bump_level, size_t limit); char *bump_major(Version *version); char *bump_minor(Version *version); char *bump_patch(Version *version); char *convert_to_string(Version *version, char *output_buffer, size_t *length); char *process_line(LineState *state, const char *bump_level); char *process_file(FileState *state); #endif//BUMP_H
bump.c
#include <bump/bump.h> #include <bump/fileutil.h> #include <ctype.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <string.h> char *initialize_version(Version *version, const size_t major, const size_t minor, const size_t patch) { if (!version) { return "Empty pointer received."; } version->major = major; version->minor = minor; version->patch = patch; return NULL; } static char *sanity_check(Version *version) { if (!version) { return "Empty pointer received."; } if (version->major == SIZE_MAX) { return "Major version number is too big."; } if (version->minor == SIZE_MAX) { return "Minor version number is too big."; } if (version->patch == SIZE_MAX) { return "Patch version number is too big."; } return NULL; } char *bump_major(Version *version) { char *message = sanity_check(version); if (message) { return message; } version->major++; version->minor = 0; version->patch = 0; return NULL; } char *bump_minor(Version *version) { char *message = sanity_check(version); if (message) { return message; } version->minor++; version->patch = 0; return NULL; } char *bump_patch(Version *version) { char *message = sanity_check(version); if (message) { return message; } version->patch++; return NULL; } char *convert_to_string(Version *version, char *output_buffer, size_t *length) { if (!version) { return "Empty version pointer value."; } if (!output_buffer) { return "Empty output buffer pointer."; } *length = 0; int count = sprintf(output_buffer, "%zu.%zu.%zu", version->major, version->minor, version->patch); if (count < 0) { return "Error occurred while trying to write to string buffer."; } *length = (size_t) count; return NULL; } char *initialize_line_state(LineState *state, const char *input, char *output, const size_t limit) { if (!state) { return "Empty pointer received for state"; } if (!input) { return "Input buffer is null"; } if (!output) { return "Output buffer is null"; } state->input = input; state->output = output; state->input_index = 0; state->output_index = 0; // The minimum length needed for a version string is 5. // If we have a string length limit smaller than that, we clamp the limit to 0. state->limit = limit < 5 ? 0 : limit; return NULL; } static void keep_going(LineState *state) { if (state->input_index == state->limit) { return; } state->output[state->output_index] = state->input[state->input_index]; state->input_index++; state->output_index++; } static size_t extract_decimal_number(LineState *state) { char c; size_t value = 0; while (state->input_index < state->limit && isdigit(c = state->input[state->input_index])) { value *= 10; value += c - '0'; state->input_index++; } return value; } char *process_line(LineState *state, const char *bump_level) { if (!state) { return "Null value received for state"; } if (state->limit == 0 || state->input_index == state->limit) { return NULL; } while (state->input_index < state->limit) { char c = state->input[state->input_index]; if (isdigit(c)) { // Start a greedy search for the pattern of "x.y.z" where x, y, and z are decimal numbers size_t major = extract_decimal_number(state); c = state->input[state->input_index]; if (c != '.') { // We have a normal number. Dump it to the output and proceed normally. int chars_printed = sprintf(state->output + state->output_index, "%zu", major); if (chars_printed < 1) { return "Error occurred while trying to write to output buffer"; } state->output_index += chars_printed; keep_going(state); continue; } state->input_index++; if (state->input_index == state->limit) { return NULL; } c = state->input[state->input_index]; if (!isdigit(c)) { // We have a x. followed by a non-digit int chars_printed = sprintf(state->output + state->output_index, "%zu.", major); if (chars_printed < 1) { return "Error occurred while trying to write to output buffer"; } state->output_index += chars_printed; keep_going(state); continue; } size_t minor = extract_decimal_number(state); if (state->input_index == state->limit) { return NULL; } c = state->input[state->input_index]; if (c != '.') { // We have an input of the form x.y only. No period follows the y int chars_printed = sprintf(state->output + state->output_index, "%zu.%zu", major, minor); if (chars_printed < 1) { return "Error occurred while trying to write to output buffer"; } state->output_index += chars_printed; keep_going(state); continue; } state->input_index++; c = state->input[state->input_index]; if (!isdigit(c)) { // We have a x.y. followed by a non-digit int chars_printed = sprintf(state->output + state->output_index, "%zu.%zu.", major, minor); if (chars_printed < 1) { return "Error occurred while trying to write to output buffer"; } state->output_index += chars_printed; keep_going(state); continue; } size_t patch = extract_decimal_number(state); c = state->input[state->input_index]; if (c == '.') { // We have x.y.z. which is invalid. int chars_printed = sprintf(state->output + state->output_index, "%zu.%zu.%zu", major, minor, patch); if (chars_printed < 1) { return "Error occurred while trying to write to output buffer"; } state->output_index += chars_printed; keep_going(state); continue; } // We now have all three numbers. Version version = {0}; initialize_version(&version, major, minor, patch); if (strcmp(bump_level, "major") == 0) { bump_major(&version); } else if (strcmp(bump_level, "minor") == 0) { bump_minor(&version); } else if (strcmp(bump_level, "patch") == 0) { bump_patch(&version); } else { return "Invalid bump level"; } size_t version_len; char *error = convert_to_string(&version, state->output + state->output_index, &version_len); state->output_index += version_len; if (error) { return error; } if (state->input_index < state->limit) { strcpy(state->output + state->output_index, state->input + state->input_index); } // We are done so we exit early return NULL; } else { keep_going(state); } } return NULL; } char *initialize_file_state(FileState *state, const char *input_path, const char *output_path, const char *bump_level, const size_t limit) { if (!state) { return "Null pointer received for FileState"; } if (!input_path) { return "Empty file path provided for input"; } if (!output_path) { return "Empty file path provided for output"; } if (!bump_level) { return "Invalid value received for bump level"; } if (!file_is_valid(input_path, "r")) { return "Cannot open input file for reading"; } if (!file_is_valid(output_path, "w")) { return "Cannot open output file for writing"; } state->input = fopen(input_path, "r"); state->output = fopen(output_path, "w"); state->bump_level = bump_level; state->limit = limit; return NULL; } char *process_file(FileState *state) { if (!state) { return "File state is null"; } char input_buffer[state->limit + 1]; char output_buffer[state->limit + 1]; size_t len; bool keep_going = true; while (1) { char *error; error = read_line(state->input, input_buffer, &len, state->limit); if (error) { keep_going = false; } memset(output_buffer, 0, state->limit); LineState line_state = {0}; error = initialize_line_state(&line_state, input_buffer, output_buffer, state->limit); if (error) { return error; } while (line_state.input_index < len) { error = process_line(&line_state, state->bump_level); if (error) { return error; } } if (keep_going) { fprintf(state->output, "%s\n", output_buffer); return NULL; } else { fprintf(state->output, "%s", output_buffer); fclose(state->input); fclose(state->output); return "End of file reached"; } } }
fileutil.h
#ifndef BUMP_FILEUTIL_H #define BUMP_FILEUTIL_H #include <stdio.h> #include <stdbool.h> bool file_is_valid(const char *input_path, const char *mode); char *read_line(FILE *input, char *buffer, size_t *length, size_t limit); #endif//BUMP_FILEUTIL_H
fileutil.c
#include <bump/fileutil.h> #include <memory.h> #include <stdbool.h> #include <stdio.h> bool file_is_valid(const char *input_path, const char *mode) { FILE *input_file = fopen(input_path, mode); bool result = input_file != NULL; if (result) { fclose(input_file); } return result; } static char *validate(FILE *input, const char *buffer) { if (!input) { return "Empty pointer for input file."; } if (!buffer) { return "Storage buffer is empty."; } return NULL; } char *read_line(FILE *input, char *buffer, size_t *length, size_t limit) { char *error = validate(input, buffer); if (error) { return error; } int ch; *length = 0; memset(buffer, 0, limit); while (*length < limit && (ch = fgetc(input)) != '\n' && ch != EOF) { buffer[*length] = (char) ch; (*length)++; } buffer[*length] = '\0'; return ch == EOF ? "End of file reached" : NULL; }
main.c
This file handles all the cases where we need to process the arguments provided or we need to obtain inputs from the user. These are essential for the executable, but not the library, so that's why I've separated the components this way.
#include <bump/bump.h> #include <bump/fileutil.h> #include <bump/version.h> #include <ctype.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_LINE_LENGTH 511 #define INCORRECT_USAGE "Incorrect usage. Type bump --help for more information." #define INTERMEDIATE_FILE "intermediate.temp" /** * Convert the characters stored in the source string to lowercase and store * them in the destination buffer. Additionally, store the number of characters * processed in the len pointer. * * @param destination The buffer to store the lowercase string in. * @param source The source string to convert to lowercase. * @param len The size_t pointer to store the number of characters processed. */ static void convert_to_lowercase_and_store_length(char *destination, const char *source, size_t *len) { *len = 0; for (char *p = (char *) source; *p; p++, (*len)++) { destination[*len] = (char) tolower(*p); } } static void store_file_name_in(const char *prompt, char *file_name_buffer, bool for_input) { bool we_have_file = false; while (true) { printf("%s", prompt); char buffer[MAX_LINE_LENGTH + 1]; size_t len = 0; char *error = read_line(stdin, buffer, &len, MAX_LINE_LENGTH); if (error) { printf("Could not read line. The following error occurred: %s\nTry again.\n", error); continue; } we_have_file = file_is_valid(buffer, for_input ? "r" : "w"); if (we_have_file) { memcpy(file_name_buffer, buffer, len + 1); break; } printf("Could not location file with path: \"%s\"\nTry again.\n", buffer); } } static void store_bump_level_in(char *bump_level_buffer) { bool we_have_bump = false; while (true) { printf("Enter bump level (major/minor/[patch] or M/m/[p]): "); char buffer[MAX_LINE_LENGTH + 1]; size_t len = 0; char *error = read_line(stdin, buffer, &len, MAX_LINE_LENGTH); if (error) { printf("Could not read line. The following error occurred: %s\nTry again.\n", error); continue; } if (len == 0) { strcpy(bump_level_buffer, "patch"); len = strlen("patch"); we_have_bump = true; } else if (len == 1) { bool major = buffer[0] == 'M'; bool minor = buffer[0] == 'm'; bool patch = buffer[0] == 'p'; if (major || minor || patch) { we_have_bump = true; } if (major) strcpy(bump_level_buffer, "major"); if (minor) strcpy(bump_level_buffer, "minor"); if (patch) strcpy(bump_level_buffer, "patch"); } else { if (strcmp(buffer, "major") == 0 || strcmp(buffer, "minor") == 0 || strcmp(buffer, "patch") == 0) { we_have_bump = true; } memcpy(bump_level_buffer, buffer, len + 1); } if (we_have_bump) { break; } printf("Could not identify bump level; use major/minor/patch or M/m/p.\nTry again.\n"); } } static bool read_confirmation() { while (true) { printf("Modify the input file? ([yes]/no or [y]/n): "); char buffer[MAX_LINE_LENGTH + 1]; size_t len = 0; char *error = read_line(stdin, buffer, &len, MAX_LINE_LENGTH); if (error) { printf("Could not read line. The following error occurred: %s\nTry again.\n", error); continue; } convert_to_lowercase_and_store_length(buffer, buffer, &len); if (len == 0) { return true; } else if (len == 1) { if (buffer[0] == 'y') { return true; } if (buffer[0] == 'n') { return false; } } else { if (strcmp(buffer, "yes") == 0) { return true; } if (strcmp(buffer, "no") == 0) { return false; } } printf("Could not understand input.\nTry again.\n"); } } static void print_help_message() { const char *help_message = "Bump semantic version value\n" "===========================\n" "Version : " BUMP_VERSION "\n" "Author : Subhomoy Haldar (@hungrybluedev)\n" "Details : A command-line utility that bumps up the semantic version\n" " number of all instances it detects in a file. It can work\n" " in-place and bump the patch, minor, or major version number.\n\n" "Usage :\n" "1. bump\n" " Starts the program in interactive mode.\n" "2. bump [--help|-h]\n" " Displays this help message.\n" "3. bump [--version|-v]\n" " Shows the current version of this program.\n" "4. bump [--input|-i] path/to/file.txt [[--level|-l] [major|minor|patch]]? \\\n" " [[--output|-o] path/to/output_file.txt]?\n" " Performs the processing on the file path provided if it exists.\n\n" " The level switch and value is optional. The values allowed are:\n" " a. patch or p - a.b.c -> a.b.(c + 1)\n" " b. minor or m - a.b.c -> a.(b + 1).0\n" " c. major or M - a.b.c -> (a + 1).0.0\n\n" " The default level is \"patch\".\n\n" " The output switch and value pair is also optional. By default\n" " the result is stored in-place, in the input file."; printf("%s\n", help_message); } static char *process_input_path_value(char *input_file_name, bool *we_have_input_path, const char *file_path) { if (*we_have_input_path) { return "Repeated input file switch."; } if (!file_is_valid(file_path, "r")) { return "The input file path provided is not valid."; } strcpy(input_file_name, file_path); *we_have_input_path = true; return NULL; } static char *process_output_path_value(char *output_file_name, bool *we_have_output_path, const char *file_path) { if (*we_have_output_path) { return "Repeated output file switch."; } if (!file_is_valid(file_path, "w")) { return "The output file path provided is not valid."; } strcpy(output_file_name, file_path); *we_have_output_path = true; return NULL; } static char *process_single_switch(const char *switch_value) { char command[MAX_LINE_LENGTH] = {0}; size_t len; convert_to_lowercase_and_store_length(command, switch_value, &len); if (len == 2) { // We have an abbreviated switch. It must begin with a '-' if (command[0] != '-') { return INCORRECT_USAGE; } switch (command[1]) { case 'h': print_help_message(); return NULL; case 'v': printf("Bump version : %s\n", BUMP_VERSION); return NULL; default: return INCORRECT_USAGE; } } if (len == 6 && strcmp(command, "--help") == 0) { print_help_message(); return NULL; } if (len == 9 && strcmp(command, "--version") == 0) { printf("Bump version : %s\n", BUMP_VERSION); return NULL; } // If we reached here, we must have had no matching switch return INCORRECT_USAGE; } static char *process_bump_value(char *bump_level, bool *we_have_bump_value, const char *bump_level_argument) { char buffer[MAX_LINE_LENGTH + 1]; size_t value_len; convert_to_lowercase_and_store_length(buffer, bump_level_argument, &value_len); if (*we_have_bump_value) { return "Repeated patch level switch."; } if (value_len == 1) { switch (bump_level_argument[0]) { case 'p': // No need to copy, it is already the default. break; case 'm': strcpy(bump_level, "minor"); break; case 'M': strcpy(bump_level, "major"); break; default: return "Unrecognised patch value. Valid levels are major(M), minor(m), or patch(p)."; } } else if (value_len == 5) { if (strcmp(bump_level_argument, "major") == 0) { strcpy(bump_level, "major"); } else if (strcmp(bump_level_argument, "minor") == 0) { strcpy(bump_level, "minor"); } else if (strcmp(bump_level_argument, "patch") == 0) { // No need to copy } else { return "Unrecognised patch value. Valid levels are major(M), minor(m), or patch(p)."; } } *we_have_bump_value = true; return NULL; } int main(int argc, char const *argv[]) { char input_file_name[MAX_LINE_LENGTH + 1] = {0}; char output_file_name[MAX_LINE_LENGTH + 1] = {0}; char bump_level[MAX_LINE_LENGTH + 1] = {0}; char *error; // Process command-line arguments if (argc == 1) { // There are no arguments. We need to obtain the values required through user input. store_file_name_in("Enter file name to process : ", input_file_name, true); store_bump_level_in(bump_level); if (read_confirmation()) { strcpy(output_file_name, input_file_name); } else { store_file_name_in("Enter output file name : ", output_file_name, false); } } else if (argc == 2) { // Help and version commands. // In all cases, the program will never execute code afterwards outside this block. error = process_single_switch(argv[1]); if (error) { puts(error); return EXIT_FAILURE; } else { return EXIT_SUCCESS; } } else if (argc % 2 == 0) { // The pairs are mismatched puts(INCORRECT_USAGE); return EXIT_FAILURE; } else { // The parameters should be present in pairs. // The count must be odd because the name of the executable is the first argument. bool we_have_input_path = false; bool we_have_bump_value = false; bool we_have_output_path = false; strcpy(bump_level, "patch"); // Iterate pair wise. We do not care about the order in which // the pairs appear. We are only interested in the values, and // if they are valid for the switch given. for (size_t index = 1; index < argc; index += 2) { // The first of the pair should be a switch if (argv[index][0] != '-') { puts(INCORRECT_USAGE); return EXIT_FAILURE; } size_t arg_len = strlen(argv[index]); if (arg_len == 2) { // Abbreviated switch switch (argv[index][1]) { case 'i': error = process_input_path_value(input_file_name, &we_have_input_path, argv[index + 1]); if (error) { puts(error); return EXIT_FAILURE; } break; case 'l': error = process_bump_value(bump_level, &we_have_bump_value, argv[index + 1]); if (error) { puts(error); return EXIT_FAILURE; } break; case 'o': error = process_output_path_value(output_file_name, &we_have_output_path, argv[index + 1]); if (error) { puts(error); return EXIT_FAILURE; } break; default: puts(INCORRECT_USAGE); return EXIT_FAILURE; } } else { // Switch is not abbreviated if (strcmp(argv[index], "--input") == 0) { error = process_input_path_value(input_file_name, &we_have_input_path, argv[index + 1]); if (error) { puts(error); return EXIT_FAILURE; } } else if (strcmp(argv[index], "--level") == 0) { error = process_bump_value(bump_level, &we_have_bump_value, argv[index + 1]); if (error) { puts(error); return EXIT_FAILURE; } } else if (strcmp(argv[index], "--output") == 0) { error = process_output_path_value(output_file_name, &we_have_output_path, argv[index + 1]); if (error) { puts(error); return EXIT_FAILURE; } } } } if (!we_have_input_path) { puts("Input file not specified."); return EXIT_FAILURE; } if (!we_have_output_path) { strcpy(output_file_name, input_file_name); } } bool inplace = strcmp(input_file_name, output_file_name) == 0; if (inplace) { // Check if we can create the file for writing if (!file_is_valid(INTERMEDIATE_FILE, "w")) { puts("Could not create temporary file for writing."); return EXIT_FAILURE; } // Mark the output as the temporary. strcpy(output_file_name, INTERMEDIATE_FILE); } FileState state = {0}; initialize_file_state(&state, input_file_name, output_file_name, bump_level, MAX_LINE_LENGTH); while (!process_file(&state)) ; if (inplace) { int c; FILE *temp = fopen(INTERMEDIATE_FILE, "r"); FILE *dump = fopen(input_file_name, "w"); while ((c = fgetc(temp)) != EOF) { fputc(c, dump); } fclose(temp); fclose(dump); remove(INTERMEDIATE_FILE); } return EXIT_SUCCESS; }
Unit tests
I use µnit for unit testing. These are the tests that I have in place for CI/CD checks on Github
#include <stdio.h> #include <bump/bump.h> #include <bump/version.h> #include <munit.h> /* * * UNIT TEST FUNCTIONS * =================== * * All unit tests are of the form: * * MunitResult <test_name>(const MunitParameter params[], * void *user_data_or_fixture) { * // perform tests. * // use the munit_assert_... macros. * return MUNIT_OK; * } * * It is necessary for the unit test functions to be added to * the `test` array. */ MunitResult bump_patch_0_0_1() { Version version = {0}; munit_assert_null(initialize_version(&version, 0, 0, 1)); bump_patch(&version); munit_assert(version.major == 0); munit_assert(version.minor == 0); munit_assert(version.patch == 2); return MUNIT_OK; } MunitResult bump_patch_0_0_15() { Version version = {0}; munit_assert_null(initialize_version(&version, 0, 0, 15)); bump_patch(&version); munit_assert(version.major == 0); munit_assert(version.minor == 0); munit_assert(version.patch == 16); return MUNIT_OK; } MunitResult bump_minor_0_0_23() { Version version = {0}; munit_assert_null(initialize_version(&version, 0, 0, 23)); bump_minor(&version); munit_assert(version.major == 0); munit_assert(version.minor == 1); munit_assert(version.patch == 0); return MUNIT_OK; } MunitResult bump_major_0_2_8() { Version version = {0}; munit_assert_null(initialize_version(&version, 0, 2, 8)); bump_major(&version); munit_assert(version.major == 1); munit_assert(version.minor == 0); munit_assert(version.patch == 0); return MUNIT_OK; } MunitResult convert_0_0_1() { char line[16]; Version version = {0}; munit_assert_null(initialize_version(&version, 0, 0, 1)); size_t len; munit_assert_null(convert_to_string(&version, line, &len)); munit_assert_string_equal(line, "0.0.1"); munit_assert(len == 5); return MUNIT_OK; } MunitResult convert_4_23_56() { char line[16]; Version version = {0}; munit_assert_null(initialize_version(&version, 4, 23, 56)); size_t len; munit_assert_null(convert_to_string(&version, line, &len)); munit_assert_string_equal(line, "4.23.56"); munit_assert(len == 7); return MUNIT_OK; } MunitResult process_line_0_1_1() { char line[10] = {0}; const char *input_line = "v0.1.1"; LineState state = {0}; initialize_line_state(&state, input_line, line, strlen(input_line)); munit_assert_null(process_line(&state, "patch")); munit_assert_string_equal(line, "v0.1.2"); munit_assert_size(state.input_index, ==, 6); munit_assert_size(state.output_index, ==, 6); return MUNIT_OK; } MunitResult process_line_1_1_51() { char line[40] = {0}; const char *input_line = "2.5 is a number 1.1.51 is the version"; LineState state = {0}; initialize_line_state(&state, input_line, line, strlen(input_line)); munit_assert_null(process_line(&state, "minor")); munit_assert_string_equal(line, "2.5 is a number 1.2.0 is the version"); munit_assert_size(state.input_index, ==, 22); munit_assert_size(state.output_index, ==, 21); return MUNIT_OK; } MunitResult process_two() { char line[50] = {0}; char buffer[50] = {0}; const char *input_line = "First we (12) have 1.6.84, then we have 8.16.3!"; LineState state = {0}; initialize_line_state(&state, input_line, line, strlen(input_line)); munit_assert_null(process_line(&state, "patch")); munit_assert_string_equal(line, "First we (12) have 1.6.85, then we have 8.16.3!"); munit_assert_size(state.input_index, ==, 25); munit_assert_size(state.output_index, ==, 25); strcpy(buffer, line); munit_assert_null(process_line(&state, "major")); munit_assert_string_equal(line, "First we (12) have 1.6.85, then we have 9.0.0!"); munit_assert_size(state.input_index, ==, 46); munit_assert_size(state.output_index, ==, 45); return MUNIT_OK; } MunitResult process_test_cases() { // Things to update: // 1. the `count` variable - stores the number of test cases // 2. the `input_lines` array - stores the input lines // 3. the `expected_lines` array - stores 3 types of outputs for each input line. The order is patch, minor, major const size_t count = 5;// <- update this const size_t max_line_width = 256; char line[max_line_width]; char copy[max_line_width]; const char *bump_levels[] = {"patch", "minor", "major"}; const char *input_lines[] = { "<modelVersion>4.0.0</modelVersion>", "#define VERSION \"0.3.56\"", "2.5 is a number but 1.0.7 is a version", "8.0. is not a version", "Let's put one at the end 9.", }; // ^ // +-- Add new input lines at the end const char *expected_lines[] = { "<modelVersion>4.0.1</modelVersion>", "<modelVersion>4.1.0</modelVersion>", "<modelVersion>5.0.0</modelVersion>", "#define VERSION \"0.3.57\"", "#define VERSION \"0.4.0\"", "#define VERSION \"1.0.0\"", "2.5 is a number but 1.0.8 is a version", "2.5 is a number but 1.1.0 is a version", "2.5 is a number but 2.0.0 is a version", "8.0. is not a version", "8.0. is not a version", "8.0. is not a version", "Let's put one at the end 9.", "Let's put one at the end 9.", "Let's put one at the end 9.", }; // ^ // +-- Add the three variations of the outputs at the end. Remember, the order is // patch, minor, major for (size_t index = 0; index < count; ++index) { strcpy(line, input_lines[index]); for (size_t bump_index = 0; bump_index < 3; ++bump_index) { memset(copy, 0, max_line_width); LineState state = {0}; munit_assert_null(initialize_line_state(&state, line, copy, max_line_width)); while (state.input_index < state.limit) { process_line(&state, bump_levels[bump_index]); } munit_assert_string_equal(expected_lines[index * 3 + bump_index], copy); } } return MUNIT_OK; } /* * MUNIT TEST CONFIGURATION * ======================== * * Boilerplate code for the munit testing library. * The last NULL test item acts as a sentinel. */ MunitTest tests[] = { {"/bump_patch_0_0_1", bump_patch_0_0_1, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {"/bump_patch_0_0_15", bump_patch_0_0_15, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {"/bump_minor_0_0_23", bump_minor_0_0_23, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {"/bump_major_0_2_8", bump_major_0_2_8, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {"/convert_0_0_1", convert_0_0_1, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {"/convert_4_23_56", convert_4_23_56, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {"/process_line_0_1_1", process_line_0_1_1, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {"/process_line_1_1_51", process_line_1_1_51, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {"/process_two", process_two, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {"/process_test_cases", process_test_cases, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}}; static const MunitSuite suite = {"/bump-test-suite", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}; /* * MAIN FUNCTION * ============= */ int main(int argc, char *argv[]) { printf("Bump project version: %s\n\n", BUMP_VERSION); return munit_suite_main(&suite, NULL, argc, argv); }
Specific concerns
- The
process_line
function seems a bit repetitive but that's the best I could think of. Any suggestions to improve this? - Are there any potential security issues? Does anything seem like blasphemy?
- What could I do to improve the project in general?
Edit: Agreement with G. Sliepen
My main intention was to make a working prototype and get some eyeballs on this project as soon as possible. This is one of the reasons why I didn't jump the gun and release it as version 1.0.0. It's still at 0.0.8.
I will create a follow-up post addressing (some of) the following issues:
- Writing error messages to
stderr
. This was an oversight on my part. - Checking the error messages returned by my functions. Overconfidence and tiredness. Will fix.
- Avoiding TOCTOU. I wasn't aware of this. I will consider it from now on.
- Writing to buffers blindly. I will use the size safe variants of the functions.
- Check for overflows when reading in values. I will prefer using
strtoull
and other related functions properly. - About hard-coding file lengths: this I mean to change eventually. I want the interactive input file length to be limited, while the actual files being operated on can have any arbitrary file length.
- Processing files in chunks instead of byte-by-byte. This is just a prototype and I will eventually work on improving the performance. I will take care of this as well.
- More rigorous error-checking in general. Won't hurt; will help a lot.
There are a few reasons why I do not want to use third-party libraries:
- While I agree that using a regular expression library will make my code easier to read, I doubt it will be faster. Also, I'm learning about Automata Theory - this is practice for my future projects.
- Finding compatible licenses is difficult. Best to avoid controversy and write everything myself. Less hoops to jump through if something needs fixing.
- I know I'm re-inventing the wheel. I've been told this several times. But it's the only way I've ever learned a lot really quickly.
- I try to avoid using malloc.
- I'm writing this for all the three major OS's: Windows, MacOS and Linux.
I'm open to more suggestions and recommendations. I take all relevant feedback into account.
malloc()
calls is great, but don't make this a religion. And it's great that you try to write cross-platform code!\$\endgroup\$