5
\$\begingroup\$

First. If this question is not allowed, I am sorry. Please, tell me so, and I will try to delete this. If it is, I will delete this paragraph.

Personal-ish note/Motivation

I was studying assembly because of Tanenbaum and C, and decided to make a "graduation" project from the book: a C compiler for AVR and x86 ISAs, using the C standard with no external source on compilers! For fun, or suffering... and to make my first C "project", not only scripts. So, I wrote a skeleton program that takes files and options ("switches"). So, the file can really grow to any place.

It also means it haunts me at night, with so many design decisions, like enum for errors and so many static functions; besides features ideas... I always think something is wrong and there will be pain in growing the project, because it will be unreadable, consequentially buggy.

Explanation

Like GCC, any input argument that is not an option is a file name. Unlike GCC, it calls options switches, because Windows does it, and I am using Windows. This is all. It is mostly a skeleton for a program that takes files from the user.

/*
 * This file contains the entry function (main) for the SSGCompiler.
 * The SSGCompiler is a compiler done as a challenge from the author
 * to the author. More information may be found on the GitHub
 * repository [REDACTED].
 *
 * This was originally written at 05/06/2023.
 */

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

#define EXECUTABLE_NAME "ssgcompiler"
#define SWITCH_PREFIX '/'

enum error {
    FATAL_ERROR,
    NOT_FATAL_ERROR
};

/* Prints the preamble of an error appended with a message. */
static void print_error_message(enum error err, const char *msg)
{
        fputs(EXECUTABLE_NAME, stdout);

        if (err == FATAL_ERROR)
                fputs(". Fatal error: ", stdout);
        else
                fputs(". Error: ", stdout);

        fputs(msg, stdout);
}

/* Returns whether the switch is a help switch */
static bool switch_is_help(const char *opt)
{
        if (    !strcmp(opt, "/?")      ||  !strcmp(opt, "--help")
            ||  !strcmp(opt, "-help")   ||  !strcmp(opt, "--h")
            ||  !strcmp(opt, "-h")      ||  !strcmp(opt, "help")) {
                return true;
        }

        return false;
}

/* Returns whether the argument array has the help switch. */
static bool args_contain_help_switch(const char **argv, const int argLen)
{
        for (int i = 0; i < argLen; i++) {
                if (switch_is_help(argv[i]))
                        return true;
        }

        return false;
}

int main(const int argc, const char **argv)
{
        if (argc < 2) {
                print_error_message(FATAL_ERROR, "no input files.\n");
                printf("See '" EXECUTABLE_NAME " /?' for help.\n");
                return 1;
        }

        if (args_contain_help_switch(argv, argc)) {
                fputs(  "Usage: ssgcompiler [switches] file...\n"
                        "Switches:\n"
                        "       /? Displays this help message (also --help, "
                        "-help, --h, -h, help)\n", stdout);

                return 0;
        }

        for (size_t i = 1; i < argc; i++) {
                if (!strlen(argv[i]) || argv[i][0] != SWITCH_PREFIX)
                        continue;
            
                print_error_message(NOT_FATAL_ERROR, "Unrecognized switch: ");
                printf("'%s'.\n", argv[i] + 1);
        }

        return 1;
}
\$\endgroup\$
1
  • \$\begingroup\$ I see you like to make some parameters const as argLen in args_contain_help_switch(const char **argv, const int argLen). Why just argLen and not also argv as neither are changed? \$\endgroup\$ Jun 8 at 14:07

2 Answers 2

6
\$\begingroup\$

Use getopt_long() to parse the command line

Parsing a command line quickly becomes more complex if you want to allow a combination of regular arguments (like filenames), and switches with and without extra arguments. You want your program to parse options the same way as other programs do, as this will less likely confuse your users.

On any UNIX-like operating system, the de-facto standard is to use GNU's getopt_long(). On Windows, I would try to find a drop-in replacement library for it. While it does not conform with how Microsoft's programs work, it's not uncommon on Windows to have programs use the UNIX style of command line flags, and it is better if your program behaves the same way on all operating systems.

Print warnings and error messages to stderr

Standard output is for the regular, expected output of your program. Use stderr when printing warnings and error messages. The benefit is that they stay visible even if the user redirected the standard output of your program to a file or to another program.

Note that if the user passed the --help flag, print that help message to stdout; it was after all explicitly requested by the user so it is expected, and then they can easily pipe long help messages through a pager like less.

Don't hardcode the name of the program

You don't have to hardcode the name of the program; it's actually passed to the program in argv[0]. So you'd typically write something like:

int main(const int argc, const char **argv)
{
    …
    fprintf(stderr, "See '%s --help' for help.\n", argv[0]);
    …
}

If the program was called using an explicit path, like ./ssgcompiler ..., then this will also end up in the help message:

See './ssgcompiler --help' for help.

This way it's even a valid command the user can copy&paste.

Make help discoverable

In case the user did not provide any arguments, you print an error message and a helpful line that says how to get more help. That's great! However, you should do this consistently. For example, also if an unrecognized switch was passed. Also consider the case where switches were passed but no filenames: argc will be >= 2 in that case, so your check at the beginning of the program is not correct.

\$\endgroup\$
5
  • \$\begingroup\$ As far as I know there is no drop in replacement for getopt() or getopt_long() on Windows. Cygwin might provide a way to use it, but Windows native systems like Visual Studio don't support it. \$\endgroup\$
    – pacmaninbw
    Jun 8 at 12:58
  • 1
    \$\begingroup\$ There are lots of getopt libraries out there that you can use. Yes, it's an external dependency, which is not ideal. \$\endgroup\$
    – G. Sliepen
    Jun 8 at 13:54
  • 1
    \$\begingroup\$ " it's actually passed to the program in argv[0]" --> not certainly. IAC, providing help should rely on minimal assumptions. I see good reasons for using "ssgcompiler" and for argv[0]. Hmmm. \$\endgroup\$ Jun 8 at 14:44
  • 1
    \$\begingroup\$ @chux-ReinstateMonica argv[0] is wat all the GNU tools use when printing usage information. According to cppreference, either argv[0] contains the name of the program or it is a NULL pointer. So to be really correct, use the equivalent of argv[0] ? argv[0] : "ssgcompiler". \$\endgroup\$
    – G. Sliepen
    Jun 8 at 15:06
  • \$\begingroup\$ I deleted my first comment because I said something you said. Thank you for the answer and the great points! \$\endgroup\$ Jun 8 at 22:12
6
\$\begingroup\$

Not much to add after @G. Sliepen.

Avoid manual formatting

Save time. Avoid manual formatting and use the auto formatting available via various tool chains.

Why check program name?

In args_contain_help_switch(const char **argv, const int argLen), code unnecessarily checks argv[0].

    // for (int i = 0; i < argLen; i++) {
    for (int i = 1; i < argLen; i++) {

Non-standard main() signature

main() is a special function - it has specifications that differ from other functions. For maximum portability, avoid any non-standard aspects with main() unless you have a compelling reason. I recommend using the form from the C spec. At least it is easier to review your code.

// int main(const int argc, const char **argv)
int main(int argc, char *argv[])  // C17dr § 5.1.2.2.1 1

Main allows changing the strings pointed to by argv[i], so using const in const char ** is not needed.

Minor: Avoid mixing sign-ness

argc is an int. Iterate likewise:

    // for (size_t i = 1; i < argc; i++) {
    for (int i = 1; i < argc; i++) {
\$\endgroup\$
6
  • \$\begingroup\$ Thank you, Chux, for answering! I did not use auto formatting because it was always worse than what I wanted, but it is probably a configuration. Also, I did not even think about const in main()! Thank you again. \$\endgroup\$ Jun 8 at 19:12
  • \$\begingroup\$ @GabrielSilvaSchilive Re always worse: I would advocate any check-in of code to a repository go through the group's auto formatting standard. It is not productive to have individual coding standards and better to agree on a group one. Instead, learn how to code in a way, with your group's coding standard, that is close to what you want. Judicious use of // helps. \$\endgroup\$ Jun 8 at 22:23
  • \$\begingroup\$ I agree that everyone using their own standard is bad. I actually tried to use the Linux kernel style guideline. But I don't think it needs an automatic tools, Though, I have never worked or coded with someone. I will take a better look on automatic tools. Thank you for answering my comment. \$\endgroup\$ Jun 8 at 22:46
  • \$\begingroup\$ @GabrielSilvaSchilive "But I don't think it needs an automatic tools" --> True such tools are not needed, yet your employer may appreciate a more productive employee - or give you more time for other more worthwhile endeavors - your call. \$\endgroup\$ Jun 9 at 2:02
  • \$\begingroup\$ About const in function parameters: the const that applies to the variable itself is not part of the function signature. So you can declare f(const int x); and then define f(int x) {…}, and that is perfectly fine, and vice versa. In the case of const char **argv, the const does not apply to the variable: the array of pointers is still modifyable, it's just that the pointers in that array point to const chars. So int main(const int argc, char **const argv) would still be in-spec, if I'm not mistaken. (But that's just being pedantic, I agree with chux.) \$\endgroup\$
    – G. Sliepen
    Jun 9 at 8:05

Your Answer

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

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