Chapter 13: Intermediate Topics: Command-Line Arguments and Environment Variables

Chapter 13: Intermediate Topics: Command-Line Arguments and Environment Variables

C programs are often run in terminal or shell environments, making direct interaction with the execution context crucial. This interaction primarily happens through command-line arguments and environment variables. Understanding these mechanisms allows you to write flexible programs that can be configured at runtime and integrate seamlessly into larger system scripts or workflows.

In this chapter, we will deepen our understanding of:

  • Command-line arguments (argc, argv): How to parse and utilize inputs provided directly when executing your program.
  • Environment variables (getenv, putenv, setenv): How programs can read and sometimes modify system-wide or user-specific configuration settings.

13.1 Command-Line Arguments (argc and argv)

We briefly introduced argc and argv in Chapter 12 with function pointers. Let’s explore them in more detail.

The main function serves as the entry point of a C program and can accept two parameters to receive command-line arguments:

int main(int argc, char *argv[]) {
    // ...
    return 0;
}
  • argc (Argument Count): An integer that stores the number of arguments passed to the program. This always includes the program’s name itself, so argc is always at least 1.
  • argv (Argument Vector): An array of character pointers (char *[]). Each element argv[i] is a C-style string (a char * terminated by \0) representing one of the command-line arguments.
    • argv[0]: Points to the name of the executable program.
    • argv[1]: Points to the first argument provided by the user.
    • argv[argc - 1]: Points to the last argument.
    • argv[argc]: Is guaranteed to be a NULL pointer, which can be useful for iterating.

Example: Basic Command-Line Argument Processing

#include <stdio.h>
#include <stdlib.h> // For atoi()
#include <string.h> // For strcmp()

int main(int argc, char *argv[]) {
    printf("Program name: %s\n", argv[0]);
    printf("Number of arguments (argc): %d\n", argc);

    if (argc < 2) {
        printf("Usage: %s <message> [number]\n", argv[0]);
        return 1; // Indicate error
    }

    // Print all arguments
    printf("\nAll arguments:\n");
    for (int i = 0; i < argc; i++) {
        printf("  Argument %d: \"%s\"\n", i, argv[i]);
    }

    // Accessing specific arguments
    printf("\n--- Specific Argument Processing ---\n");
    printf("Your message: \"%s\"\n", argv[1]);

    // Optional argument processing
    if (argc > 2) {
        int num = atoi(argv[2]); // Convert second argument to integer
        printf("The number you provided is: %d\n", num);

        // Conditional logic based on arguments
        if (strcmp(argv[1], "hello") == 0) {
            printf("You said hello!\n");
        }
    } else {
        printf("No optional number argument provided.\n");
    }

    return 0;
}

To Compile and Run:

gcc cli_args.c -o cli_args
./cli_args
./cli_args "Hello World"
./cli_args mymessage 123
./cli_args hello 456

Parsing Numerical Arguments: Arguments are always received as strings. To use them as numbers, you need to convert them:

  • atoi(const char *str): Converts a string to an int. Returns 0 on error or if the string isn’t a valid number.
  • atof(const char *str): Converts a string to a double.
  • atol(const char *str): Converts a string to a long int.
  • strtol(const char *nptr, char **endptr, int base): More robust conversion for long int. Allows error checking and specifying the number base.
  • strtod(const char *nptr, char **endptr): More robust conversion for double.

Robust Argument Parsing: For more complex command-line interfaces (e.g., with flags like -v for verbose or --help), you’d typically implement more sophisticated parsing logic or use libraries like getopt (POSIX standard) or custom solutions.

13.2 Environment Variables

Environment variables are dynamic named values that can influence the way running processes behave in a computer’s operating system. They are part of the environment in which a process runs. Common examples include PATH, HOME, USER, LANG, etc.

C programs can read environment variables using getenv() and, on some systems, modify them using putenv() or setenv().

13.2.1 getenv(): Reading Environment Variables

getenv() searches the environment list for a string that matches the provided name.

Syntax:

char *getenv(const char *name);
  • name: The name of the environment variable (e.g., “PATH”, “HOME”).
  • Returns a pointer to the value of the environment variable if found, or NULL if not found. The returned string should not be modified, as it might point to a static buffer.

Example:

#include <stdio.h>
#include <stdlib.h> // For getenv

int main() {
    char *path_env = getenv("PATH");
    char *home_env = getenv("HOME");
    char *user_env = getenv("USER"); // On Linux/macOS
    char *username_env = getenv("USERNAME"); // On Windows

    printf("--- Reading Environment Variables ---\n");

    if (path_env != NULL) {
        printf("PATH: %s\n", path_env);
    } else {
        printf("PATH environment variable not found.\n");
    }

    if (home_env != NULL) {
        printf("HOME: %s\n", home_env);
    } else {
        printf("HOME environment variable not found.\n");
    }

    if (user_env != NULL) {
        printf("USER: %s\n", user_env);
    } else if (username_env != NULL) { // Try Windows equivalent
        printf("USERNAME: %s\n", username_env);
    } else {
        printf("USER/USERNAME environment variable not found.\n");
    }

    char *custom_env = getenv("MY_CUSTOM_VAR");
    if (custom_env != NULL) {
        printf("MY_CUSTOM_VAR: %s\n", custom_env);
    } else {
        printf("MY_CUSTOM_VAR environment variable not found.\n");
    }

    return 0;
}

To Compile and Run (and set custom variable):

Linux/macOS:

gcc env_vars.c -o env_vars
./env_vars
export MY_CUSTOM_VAR="Hello From Shell"
./env_vars
unset MY_CUSTOM_VAR

Windows (Command Prompt):

cl env_vars.c
env_vars.exe
set MY_CUSTOM_VAR=Hello From Cmd
env_vars.exe
set MY_CUSTOM_VAR=

13.2.2 putenv() (POSIX.1), setenv() (POSIX.1), unsetenv() (POSIX.1): Modifying Environment Variables

These functions are part of the POSIX standard, so they might not be available on all systems (e.g., older Windows compilers might not have them by default). putenv and setenv modify the environment. unsetenv removes a variable.

  • int putenv(char *string);: Adds or changes an environment variable. string must be in the format NAME=VALUE. The string passed to putenv should ideally be dynamically allocated or a non-stack-allocated array, as putenv might store a pointer to it.
  • int setenv(const char *name, const char *value, int overwrite);: Adds or changes an environment variable. If overwrite is non-zero, an existing variable is overwritten. If overwrite is zero, an existing variable is not changed. More robust than putenv.
  • int unsetenv(const char *name);: Removes an environment variable from the environment.

Example (POSIX-compliant systems):

#include <stdio.h>
#include <stdlib.h> // For getenv, setenv, unsetenv
#include <string.h> // For strlen, strcat (for putenv)

int main() {
    printf("--- Before Modification ---\n");
    char *test_var = getenv("MY_TEST_VAR");
    if (test_var) printf("MY_TEST_VAR: %s\n", test_var); else printf("MY_TEST_VAR not set.\n");

    // Using setenv to create/overwrite
    printf("\n--- Setting MY_TEST_VAR to 'InitialValue' ---\n");
    if (setenv("MY_TEST_VAR", "InitialValue", 1) != 0) { // 1 to overwrite if exists
        perror("setenv failed");
        return 1;
    }
    test_var = getenv("MY_TEST_VAR");
    if (test_var) printf("MY_TEST_VAR: %s\n", test_var);

    // Using setenv to NOT overwrite
    printf("\n--- Trying to set MY_TEST_VAR to 'NewValue' (no overwrite) ---\n");
    if (setenv("MY_TEST_VAR", "NewValue", 0) != 0) { // 0 to not overwrite
        perror("setenv failed");
    }
    test_var = getenv("MY_TEST_VAR");
    if (test_var) printf("MY_TEST_VAR (should be InitialValue): %s\n", test_var);

    // Using setenv to overwrite
    printf("\n--- Overwriting MY_TEST_VAR to 'FinalValue' ---\n");
    if (setenv("MY_TEST_VAR", "FinalValue", 1) != 0) {
        perror("setenv failed");
        return 1;
    }
    test_var = getenv("MY_TEST_VAR");
    if (test_var) printf("MY_TEST_VAR: %s\n", test_var);

    // Unsetting the variable
    printf("\n--- Unsetting MY_TEST_VAR ---\n");
    if (unsetenv("MY_TEST_VAR") != 0) {
        perror("unsetenv failed");
    }
    test_var = getenv("MY_TEST_VAR");
    if (test_var == NULL) printf("MY_TEST_VAR successfully unset.\n");

    /*
    // Example of putenv (less safe due to string ownership, generally prefer setenv)
    printf("\n--- Using putenv (less preferred) ---\n");
    char new_env_string[100];
    strcpy(new_env_string, "MY_PUTENV_VAR=HelloPutenv");
    if (putenv(new_env_string) != 0) { // Pass a modifiable string
        perror("putenv failed");
    }
    char *putenv_var = getenv("MY_PUTENV_VAR");
    if (putenv_var) printf("MY_PUTENV_VAR: %s\n", putenv_var);
    */

    return 0;
}

Important Note on setenv and putenv: Changes made to environment variables using setenv or putenv only affect the current process and its child processes. They do not modify the environment of the parent shell or other sibling processes. The changes are temporary for the lifetime of your program and any programs it launches.

13.2.3 The environ Pointer

On POSIX systems, there is an external global variable extern char **environ; (declared in <unistd.h> or <stdlib.h>) that points to the array of environment strings. This allows you to iterate through all environment variables.

Example:

#include <stdio.h>
#include <stdlib.h> // For environ on some systems, though <unistd.h> is more common for extern char **environ

// Declare the external environment variable array
extern char **environ;

int main() {
    printf("--- All Environment Variables ---\n");
    for (char **env = environ; *env != NULL; env++) {
        printf("%s\n", *env);
    }
    return 0;
}

Exercise 13.1: Config File or Environment?

Write a C program that tries to get a “DEBUG_LEVEL” setting.

  1. First, attempt to read the DEBUG_LEVEL environment variable using getenv(). If found, convert it to an integer using atoi().
  2. If DEBUG_LEVEL is not found in the environment, check for a command-line argument -d <level> or --debug <level>. Parse this argument.
  3. If neither is provided, default debug_level to 0.
  4. Based on the final debug_level:
    • 0: Print “No debug output.”
    • 1: Print “Basic debug output enabled.”
    • 2: Print “Detailed debug output enabled.”
    • Any other: Print “Invalid debug level.”

Example Execution:

# Case 1: Environment variable
export DEBUG_LEVEL=2
./config_check
# Output: Detailed debug output enabled.
unset DEBUG_LEVEL

# Case 2: Command-line argument
./config_check --debug 1
# Output: Basic debug output enabled.

# Case 3: Neither
./config_check
# Output: No debug output.

# Case 4: Invalid argument
./config_check --debug xyz
# Output: Invalid debug level.

Exercise 13.2: Custom my_echo Command (Mini-Challenge)

Implement a simplified version of the echo command (or print) that prints its command-line arguments to stdout, separated by spaces. Add a simple option to print environment variables.

Instructions:

  1. Your program should handle all command-line arguments and print them, separated by a single space, followed by a newline.
  2. If the argument "--env" is present, instead of printing other arguments, print all environment variables (similar to the environ example) to the console, each on a new line. The "--env" flag should take precedence.

Example Usage:

$ ./my_echo Hello World C Programming
Hello World C Programming

$ ./my_echo --env
# (Prints all your system's environment variables)

$ ./my_echo First --env Second
# (Should only print environment variables because --env takes precedence)

You now have a solid understanding of how C programs interact with their execution environment through command-line arguments and environment variables. These are indispensable tools for creating configurable, adaptable, and system-friendly applications. In the next chapter, we’ll dive into advanced topics related to memory, specifically memory alignment and various optimization techniques.