3
\$\begingroup\$

Usage:

./usort <options> <filename>

Name:

./usort - sort lines of text.

Description:

Write sorted concatenation of FILE to standard output. In the absence of options, lexicographical order is taken as default.

Ordering Options:

-n, --numeric sorts by numeric order.
-r, --reverse sorts in reverse order.
-h, --help displays this message and exits.
-o, --output=FILE writes result to FILE instead of standard output.

Review Goals:

I would like some feedback regarding:

  • Non-idiomatic code
  • Style
  • Error-Checking
  • Bad practices
  • Further improvements

Code:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
#include <string.h>
#include <getopt.h>
#include <stddef.h>

#define MAX 1000
#define MAXLINES 50000
#define OP_LIST "nrho:"

FILE *fp;

struct flags {
    bool nflag ;     /* Numeric flag */
    bool rflag ;     /* Reverse flag */
    bool oflag ;     /* Output to FILE flag */
    };

static void parse_options (struct option long_options[], struct flags *opt_ptr, int argc, char *argv[], char *out_file);
static void usage(size_t status);
static int read_file(char *lines[], char *filename);
static void write_to_stdout(char *lines[], int num_lines, struct flags *opt_ptr);
static void write_to_FILE (char *lines[], int num_lines, char *optarg, struct flags *opt_ptr);
static int scmp(const void *s1, const void *s2);
static int numcmp(const void *s1, const void *s2);
static void *xcalloc(size_t size);
static void free_allocs(char *lines[], char *out_file, char *in_file);

int main(int argc, char *argv[]) {

    static struct option long_options[] = {
        {"numeric", no_argument,       NULL, 'n'},
        {"reverse", no_argument,       NULL , 'r'},
        {"output",  required_argument, NULL, 'o'},
        {"help",    no_argument,       NULL , 'h'},
        {NULL, 0, NULL, 0}
    };

    char *in_file = xcalloc(256);
    char *out_file = xcalloc(256);

    struct flags options = {false, false, false};
    struct flags *opt_ptr = &options;

    parse_options(long_options, opt_ptr, argc, argv, out_file);

    /* If there is no file argument, call usage */
    if (optind >= argc) {
        usage(EXIT_FAILURE);
    }

    /* If there is more or less than 1 argument, call usage */
    else if ((optind + 1) != argc) {
        usage(EXIT_FAILURE);
    }

    strcpy(in_file, argv[optind]);

    char *lines[MAXLINES];
    size_t i;

    for (i = 0; i < MAXLINES; i++) {
        lines[i] = xcalloc(MAX);
    }

    size_t num_lines;
    num_lines = read_file(lines, in_file);

    qsort(lines, num_lines, sizeof *lines, opt_ptr->nflag? numcmp: scmp);

    opt_ptr->oflag ? write_to_FILE(lines, num_lines, out_file, opt_ptr) : write_to_stdout(lines, num_lines, opt_ptr);

    free_allocs(lines, out_file, in_file);

    return EXIT_SUCCESS;
}

/* ====================================================================================================================================================================================================== */
static void usage(size_t status) {

    if (status != EXIT_FAILURE) {
        fprintf(stderr, "\nUSAGE\n\tusort <options> <filename>\n\n");
        fputs("NAME\n\tusort - sort lines of text.\n\n\
DESCRIPTION\n\tWrite sorted concatenation of FILE to standard output.\n\
        In the absence of options, lexicographical order is taken as default.\n\n", stdout);

            fputs("ORDERING OPTIONS:\n\n\
               -n, --numeric                sorts by numeric order.\n\n\
               -r, --reverse                sorts in reverse order.\n\n\
               -h, --help                       displays this message and exits.\n\n\
               -o, --output=FILE                writes result to FILE instead of standard output.\n\n", stdout);
            exit(status);
    }

    else {
        fprintf(stderr, "The syntax of the command is incorrect.\n\
Try %s: -h for more information.\n", "usort");

        exit(status);
    }
}
/* ======================================================================================================================================================================================================*/
static void parse_options (struct option long_options[], struct flags *opt_ptr, int argc, char *argv[], char *out_file) {

    int c;

    while ((c = getopt_long(argc, argv, OP_LIST, long_options, NULL)) != -1) {
        switch(c) {
        case 'n':
                opt_ptr->nflag = true;
                break;

        case 'r':
                opt_ptr->rflag = true;
                break;

        case 'h':
                usage(EXIT_SUCCESS);
                break;

        case 'o':
                opt_ptr->oflag = true;
                strcpy(out_file, optarg);
                break;

        /* case '?' */
        default:
                usage(EXIT_FAILURE);
                break;
        }
    }
}

/* ====================================================================================================================================================================================================== */
static int read_file(char *lines[], char *filename) {

    fp = fopen(filename, "r");
    if (!fp)
    {
        perror("fopen");
        usage(EXIT_FAILURE);
    }

    char buffer[MAX];
    size_t i = 0;

    while (fgets(buffer, sizeof(buffer), fp))
    {
        buffer[strcspn(buffer, "\n")] = 0;
        strcpy(lines[i], buffer);
        i++;
    }

    fclose(fp);

    return i;
}

/* ====================================================================================================================================================================================================== */
static void write_to_stdout(char *lines[], int num_lines, struct flags *opt_ptr) {

    int i;

    if (opt_ptr->rflag) {
        for (i = num_lines - 1; i >= 0; i--) {
            fprintf(stdout, "%s\n", lines[i]);
        }
    }

    else {
        for (i = 0; i < num_lines; i++) {
            fprintf(stdout, "%s\n", lines[i]);
        }
    }
}

/* ====================================================================================================================================================================================================== */
static void write_to_FILE (char *lines[], int num_lines, char *out_file, struct flags *opt_ptr) {

    int i;

    fp = fopen(out_file, "w");
    if (!fp) {
        perror("fopen");
        exit(EXIT_FAILURE);
    }

    if (opt_ptr->rflag) {
        for (i = num_lines - 1; i >= 0; i--) {
            fprintf(fp, "%s\n", lines[i]);
        }
    }

    else {
        for (i = 0; i < num_lines; i++) {
            fprintf(fp, "%s\n", lines[i]);
        }

    fclose(fp);
    }
}

/* ============================================================================================================================================================== */
/* Compares s1 and s2 lexicographically */

static int scmp(const void *s1, const void *s2) {

    const char *v1, *v2;

    v1 = *(char **)s1;  
    v2 = *(char **)s2;  //the comparison method gets passed 'pointers to pointers' (char**) by qsort...

    return strcmp(v1, v2);
}

/* =============================================================================================================================================================== */
/* Compares s1 and s2 numerically */

static int numcmp(const void *s1, const void *s2) {

    long *v1 = *(long * const *)s1;
    long *v2 = *(long * const *)s2;
    
    return (*v1 > *v2) - (*v1 < *v2);
}

/* =============================================================================================================================================================== */
/* A wrapper function for calloc, returns a void ptr on success, exits on failure */

static void *xcalloc(size_t size) {

    if (size == 0) {
        exit(EXIT_FAILURE);
    }

    void *ptr = calloc(1, size);
    if (!ptr) {
        perror("calloc ");
        exit(EXIT_FAILURE);
    }

    return ptr;
}

/* ================================================================================================================================================================ */
static void free_allocs(char *lines[], char *out_file, char *in_file) {

    size_t i;

    for (i = 0; i < MAXLINES; i++) {
       free(lines[i]);
    }

    free(in_file);
    free(out_file);
}

How do I go about dealing with the magic numbers? What options are there to deduce the file size and the number of lines beforehand? Is allocating storage for 50000 lines beforehand a waste?

Edit: The deletion of the post was due to a bug @Martin found, after which I deemed the post off-topic for the site and self-deleted it. The question may remain closed until it's fixed.

\$\endgroup\$
6
  • \$\begingroup\$ Did you test the “numeric” option? Converting a string to a number cannot work by just casting pointers (in numcmp). \$\endgroup\$
    – Martin R
    Commented Dec 11, 2022 at 12:45
  • \$\begingroup\$ I'd change the function and edit the question. \$\endgroup\$
    – Harith
    Commented Dec 11, 2022 at 13:08
  • \$\begingroup\$ There was an answer incoming. If you have improved code after reading the answer, feel free to post this as a new question. \$\endgroup\$
    – Mast
    Commented Dec 12, 2022 at 18:51
  • \$\begingroup\$ @Mast: It is unclear to me why the question was mod-undeleted. OP realized (after my comment) that the code is not working as intended (which makes the question off-topic), and self-deleted the question before any answer was posted. \$\endgroup\$
    – Martin R
    Commented Dec 12, 2022 at 19:02
  • \$\begingroup\$ @MartinR I deleted the question due to that bug, intending to un-delete once it was fixed. But I'd edit the question to add this information just in case someone reviews it. \$\endgroup\$
    – Harith
    Commented Dec 12, 2022 at 19:10

2 Answers 2

4
\$\begingroup\$

Compiler warnings

GCC reports some dodgy type conversions:

gcc-12 -std=c17 -fPIC -gdwarf-4 -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Wconversion  -Wstrict-prototypes -fanalyzer       281850.c    -o 281850
281850.c: In function ‘main’:
281850.c:69:17: warning: conversion to ‘size_t’ {aka ‘long unsigned int’} from ‘int’ may change the sign of the result [-Wsign-conversion]
   69 |     num_lines = read_file(lines, in_file);
      |                 ^~~~~~~~~
281850.c:73:43: warning: conversion from ‘size_t’ {aka ‘long unsigned int’} to ‘int’ may change value [-Wconversion]
   73 |     opt_ptr->oflag ? write_to_FILE(lines, num_lines, out_file, opt_ptr) : write_to_stdout(lines, num_lines, opt_ptr);
      |                                           ^~~~~~~~~
281850.c:73:98: warning: conversion from ‘size_t’ {aka ‘long unsigned int’} to ‘int’ may change value [-Wconversion]
   73 |     opt_ptr->oflag ? write_to_FILE(lines, num_lines, out_file, opt_ptr) : write_to_stdout(lines, num_lines, opt_ptr);
      |                                                                                                  ^~~~~~~~~
281850.c: In function ‘usage’:
281850.c:94:18: warning: conversion from ‘size_t’ {aka ‘long unsigned int’} to ‘int’ may change value [-Wconversion]
   94 |             exit(status);
      |                  ^~~~~~
281850.c:101:14: warning: conversion from ‘size_t’ {aka ‘long unsigned int’} to ‘int’ may change value [-Wconversion]
  101 |         exit(status);
      |              ^~~~~~
281850.c: In function ‘read_file’:
281850.c:158:12: warning: conversion from ‘size_t’ {aka ‘long unsigned int’} to ‘int’ may change value [-Wconversion]
  158 |     return i;
      |            ^

These are all avoidable.


Forward declarations

It shouldn't be necessary to declare all our functions twice. If we write them in bottom-up order, we can make all the declarations be definitions.


usage() should be two functions

Since we only ever call usage() with literal argument to switch between two behaviours, it looks like we ought to have two functions. And it doesn't make sense that the help message is split across two different streams - I think it should all go to stdout.

static void help(void)
{
    puts("\nUSAGE\n\tusort <options> <filename>\n\n"
         "NAME\n\tusort - sort lines of text.\n\n"
         "DESCRIPTION\n\tWrite sorted concatenation of FILE to standard output.\n"
         "        In the absence of options, lexicographical order is taken as default.\n\n"
         "ORDERING OPTIONS:\n\n"
         "               -n, --numeric                sorts by numeric order.\n\n"
         "               -r, --reverse                sorts in reverse order.\n\n"
         "               -h, --help                       displays this message and exits.\n\n"
         "               -o, --output=FILE                writes result to FILE instead of standard output.\n\n");
        exit(EXIT_SUCCESS);
}

static void usage_err(void)
{
    fputs("The syntax of the command is incorrect.\n"
          "Try usort -h for more information.\n",
          stderr);
    exit(EXIT_FAILURE);
}

I'm not a big fan of the extra blank lines in the help output, but I left them as is.


Writing should be one function

Conversely, the two functions write_to_stdout() and write_to_FILE() should be a single function that takes a FILE* argument to determine where to write to:

static void write_to(FILE *f, char *lines[], size_t num_lines,
                     const struct flags *opt_ptr)
{
    if (opt_ptr->rflag) {
        for (size_t i = num_lines; i > 0; i--) {
            fprintf(f, "%s\n", lines[i-1]);
        }
    } else {
        for (size_t i = 0; i < num_lines; i++) {
            fprintf(f, "%s\n", lines[i]);
        }
    }
}

In any case, when writing to a file, we should call fclose() regardless of whether -r flag was given.


Open the output file early

Don't wait until we've done all the processing before discovering that the output file isn't writeable - do this first, before reading input. It also allows us to discard the name of the output file straight away, and just keep its FILE pointer.

Similarly, if we represent input by its file-pointer, we can read from standard input as easily as from a file.


Check for errors when performing I/O

When fgets() returns a null pointer, we should check whether it's because we have reached the end of input or we have an error.

When we write to and close our output stream, we need to check whether these operations were successful.


Numeric sorting is broken

You can't sort numerically just by reinterpreting character sequences as if they were integers:

./281850 -n <(seq 99999995 100000005)
100000000
100000001
100000002
100000003
100000004
100000005
99999995
99999996
99999997
99999998
99999999

We need to convert to integer, perhaps like this:

static int numcmp(const void *s1, const void *s2) 
{
    const char *v1 = s1;
    const char *v2 = s2;
    long long a1 = atoll(v1);
    long long a2 = atoll(v2);
    return (a1 > a2) - (a2 < a1);
}

This can be improved further by using strtoll() (which "saturates" out of range values). Instead of using long long, we could use the platform's widest integer type and the corresponding strtoimax() (from <inttypes.h>). We could also consider falling back to the string comparison if lines are numerically equal.


Reduce copying

                strcpy(out_file, optarg);

If we made out_file a plain pointer, we could just assign it from optarg, which points to the argv array we never modify.

while (fgets(buffer, sizeof(buffer), fp)) {
    buffer[strcspn(buffer, "\n")] = 0;
    strcpy(lines[i], buffer);

No reason to bounce through buffer - just input directly into the lines array.


Get rid of the arbitrary limits

We create storage of MAX * MAXLINES characters, which in many cases is far more than we need (e.g. Valgrind reports total heap usage: 50,002 allocs, 50,002 frees, 50,004,568 bytes allocated when I sort an empty input!), and for some cases is not nearly enough (and when it's not enough, we overrun the storage - that's very bad).

Instead, we could read all the input into storage, then count the newlines and allocate the right amount of storage to hold them (or reallocate as we encounter newlines).


Modified code

With the above fixes applied (but still won't handle files containing \0, though one could argue that those aren't text files)

#include <errno.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <getopt.h>
#include <unistd.h>

struct flags {
    bool nflag;     /* Numeric flag */
    bool rflag;     /* Reverse flag */
    FILE *output;   /* Output to FILE */
};

static void help(void)
{
    puts("\nUSAGE\n\tusort <options> <filename>\n\n"
         "NAME\n\tusort - sort lines of text.\n\n"
         "DESCRIPTION\n\tWrite sorted concatenation of FILE to standard output.\n"
         "        In the absence of options, lexicographical order is taken as default.\n\n"
         "ORDERING OPTIONS:\n\n"
         "               -n, --numeric                sort in numeric order.\n\n"
         "               -r, --reverse                sort in reverse order.\n\n"
         "               -h, --help                       display this message and exits.\n\n"
         "               -o, --output=FILE                write result to FILE instead of standard output.\n\n");
        exit(EXIT_SUCCESS);
}

static void usage_err(void)
{
    fputs("The syntax of the command is incorrect.\n"
          "Try usort -h for more information.\n",
          stderr);
    exit(EXIT_FAILURE);
}

static void parse_options(const struct option long_options[],
                          struct flags *opt_ptr,
                          int argc, char *argv[])
{
    int c;
    while ((c = getopt_long(argc, argv, "nrho:", long_options, NULL)) != -1) {
        switch (c) {
        case 'n':
            opt_ptr->nflag = true;
            break;

        case 'r':
            opt_ptr->rflag = true;
            break;

        case 'h':
            help();
            break;

        case 'o':
            /* We'll seek to beginning once we've read input,
               in case it's the same file. */
            opt_ptr->output = fopen(optarg, "a");
            if (!opt_ptr->output) {
                perror(optarg);
                exit(EXIT_FAILURE);
            }
            break;

            /* case '?' */
        default:
            usage_err();
            break;
        }
    }
}

/* Return an array of lines in the file, or a null pointer on failure.
 * Caller must free the zeroth element and the lines array.
 */
static char *read_file(FILE *fp)
{
    static const size_t page_size = 4096;
    char *content = 0;
    size_t len = 0;
    for (size_t rcount = 1; rcount;  len += rcount) {
        char *new = realloc(content, len + page_size);
        if (!new) {
            free(content);
            errno = ENOMEM;
            return 0;
        }
        content = new;
        rcount = fread(content + len, 1, page_size - 1, fp);
        if (ferror(fp)) {
            free(content);
            errno = ENOMEM;
            return 0;
        }
    }

    content[len] = '\0';
    return content;
}

size_t split_lines(char *s, char ***lines)
{
    static const size_t chunk_size = 1000;
    size_t capacity = 0;
    size_t line_count = 0;

    while (s && *s) {
        if (line_count >= capacity) {
            char **new = realloc(*lines, sizeof **lines * (capacity += chunk_size));
            if (!new) {
                free(*lines);
                *lines = NULL;
                errno = ENOMEM;
                return 0;
            }
            *lines = new;
        }
        (*lines)[line_count++] = s;
        s = strchr(s, '\n');
        if (s) { *s++ = '\0'; }
    }

    return line_count;
}

static bool write_to(char *lines[], size_t num_lines,
                     const struct flags *opt_ptr)
{
    FILE *const f = opt_ptr->output;
    bool reverse = opt_ptr->rflag;

    if (f != stdout && ftruncate(fileno(f), 0)) {
        perror("seek");
        return false;
    }

    for (size_t i = 0; i < num_lines; i++) {
        size_t n = reverse ? num_lines - 1 - i : i;
        if (fprintf(f, "%s\n", lines[n]) < 0) {
            perror("write");
            return false;
        }
    }

    return f == stdout || !fclose(f);
}


/* Compare s1 and s2 lexicographically */
static int scmp(const void *s1, const void *s2) 
{
    const char *const *v1 = s1;
    const char *const *v2 = s2;
    return strcmp(*v1, *v2);
}

/* Compare s1 and s2 numerically */
static int numcmp(const void *s1, const void *s2)
{
    const char *const *v1 = s1;
    const char *const *v2 = s2;
    char *p1;
    char *p2;
    intmax_t a1 = strtoimax(*v1, &p1, 10);
    intmax_t a2 = strtoimax(*v2, &p2, 10);
    return a1 > a2 ? 1
        :  a1 < a2 ? -1
        :  scmp(&p1, &p2);    /* sort by rest of line as a string */
}

int main(int argc, char *argv[]) 
{
    static const struct option long_options[] =
        {
            {"numeric", no_argument,       NULL, 'n'},
            {"reverse", no_argument,       NULL, 'r'},
            {"output",  required_argument, NULL, 'o'},
            {"help",    no_argument,       NULL, 'h'},
            {NULL, 0, NULL, 0}
        };

    FILE *in_file = stdin;

    struct flags options = {false, false, stdout};

    parse_options(long_options, &options, argc, argv);

    if ((optind + 1) == argc) {
        in_file = fopen(argv[optind], "r");
        if (!in_file) {
            perror(argv[optind]);
            return EXIT_FAILURE;
        }
    } else if (optind > argc) {
        usage_err();
    }

    char *content = read_file(in_file);
    char **lines = NULL;
    size_t num_lines = split_lines(content, &lines);
    if (!lines) {
        free(content);
        perror("read");
        return EXIT_FAILURE;
    }

    qsort(lines, num_lines, sizeof *lines, options.nflag ? numcmp : scmp);

    if (!write_to(lines, num_lines, &options)) {
        free(content);
        free(lines);
        return EXIT_FAILURE;
    }

    free(content);
    free(lines);

    return EXIT_SUCCESS;
}
\$\endgroup\$
7
  • \$\begingroup\$ Perhaps go for the max: intmax_t a1 = strtoimaxl(v1, NULL, 10);? \$\endgroup\$
    – chux
    Commented Dec 13, 2022 at 4:40
  • \$\begingroup\$ Aside: char **new sufficient as void *new. Its only a temporary. \$\endgroup\$
    – chux
    Commented Dec 13, 2022 at 4:47
  • \$\begingroup\$ Thanks @chux; I was unaware of strtoimax() (which lives in <inttypes.h> rather than in <stdlib.h> with the other conversions). As to converting from void* to char** - yes, that can be done at either stage; purely personal preference to minimise void*! \$\endgroup\$ Commented Dec 13, 2022 at 8:37
  • \$\begingroup\$ @TobySpeight Could you elaborate on this statement? return f == stdout || !fclose(f The equality operator has higher precedence than logical OR , so it will be evaluated first. Is the statement equivalent to (f == stdout) || !fclose(f)? \$\endgroup\$
    – Harith
    Commented Dec 14, 2022 at 4:55
  • \$\begingroup\$ Exactly - though it's actually an expression (part of the return statement). The function returns true if we're writing to standard out; if not then it only returns true if we successfully closed the stream. Or viewed another way, it returns false only if we needed to close the stream but failed when doing so. The most common causes of fclose() failure are the same as for writing, because closing a stream causes any remaining buffered output to be flushed. \$\endgroup\$ Commented Dec 14, 2022 at 8:15
3
\$\begingroup\$

In addition to other's review points.

Not much left.

Exit with no message

Expect fprintf() message before failure.

if (size == 0) {
    // Why exit message here
    exit(EXIT_FAILURE);
}

Naked define

#define MAX 1000 deserves more description. Perhaps something to describe it as the line size.

Declare where needed

//size_t i;
//for (i = 0; i < MAXLINES; i++) {
//    lines[i] = xcalloc(MAX);
// }

for (size_t i = 0; i < MAXLINES; i++) {
    lines[i] = xcalloc(MAX);
}

Use const

For refenced data that is not changed, use const for wider usage and potential performance improvements. It also better documents functionality.

// static int read_file(char *lines[], char *filename) {
static int read_file(char *lines[], const char *filename) {

As a habit, tolerate freeing NULL

This mimics free(NULL) and allows easier clean-up during error handling.

static void free_allocs(char *lines[], char *out_file, char *in_file) {
  if (lines) {  // Add test
    size_t i;

    for (i = 0; i < MAXLINES; i++) {
      free(lines[i]);
    }
\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.