C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
189: Working with Environment Variables in Programs
You've probably noticed that hardcoding configuration—like file paths, API keys, or port numbers—directly into your source code is a recipe for disaster. Not only is it a security risk, but it means you have to recompile your entire app just to change a single timeout value. In the real world, we use environment variables for this. They let us change the behavior of a binary without touching the code.
In C, the easiest way to get these is through getenv() from <stdlib.h>. It's a simple function: you give it a string (the name of the variable), and it returns a pointer to the value. Let's build a small utility that adjusts its verbosity based on a LOG_LEVEL variable.
Pulling a value from the shell
I want my program to print detailed debug info only if LOG_LEVEL is set to "DEBUG". Otherwise, it should stay quiet. Here is how I'll start the implementation.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *level = getenv("LOG_LEVEL");
if (strcmp(level, "DEBUG") == 0) {
printf("Debug mode active: Scanning system resources...\n");
} else {
printf("Running in standard mode.\n");
}
return 0;
}
On the surface, this looks fine. If I run LOG_LEVEL=DEBUG ./logger in my terminal, it works perfectly. But here is where I almost tripped up—and where you likely will too.
The dreaded NULL pointer crash
I ran the program normally, without setting the environment variable first. The result? A segmentation fault. I forgot a fundamental rule of getenv(): if the variable doesn't exist in the environment, it doesn't return an empty string; it returns NULL.
Passing NULL into strcmp() is an immediate crash because strcmp expects a valid memory address to read from. It's a classic C mistake. I need to verify the pointer exists before I even think about comparing strings.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *level = getenv("LOG_LEVEL");
// Fix: Always check for NULL before using the return value of getenv
if (level == NULL) {
printf("LOG_LEVEL not set. Defaulting to standard mode.\n");
level = "INFO"; // Assign a default to keep the rest of the logic clean
}
if (strcmp(level, "DEBUG") == 0) {
printf("Debug mode active: Scanning system resources...\n");
} else {
printf("Running in standard mode.\n");
}
return 0;
}
Handling multiple configuration keys
Usually, you aren't just checking one variable. You're checking a handful. If we add a APP_PORT variable, we have to remember that getenv() always returns a string. If you need an integer, you'll have to convert it using something like atoi().
Let's wrap this up into a slightly more robust example that handles both a string and a number.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *level = getenv("LOG_LEVEL");
char *port_str = getenv("APP_PORT");
// Default values
int port = 8080;
int is_debug = 0;
if (level != NULL && strcmp(level, "DEBUG") == 0) {
is_debug = 1;
}
if (port_str != NULL) {
port = atoi(port_str);
}
if (is_debug) {
printf("[DEBUG] Initializing server on port %d...\n", port);
} else {
printf("Server started on port %d.\n", port);
}
return 0;
}
Now the program is resilient. It has sensible defaults, it doesn't crash when the environment is empty, and it's fully configurable from the shell. Just remember: getenv is a read-only operation. If you want to set an environment variable from within your C code to be inherited by child processes, you'll need setenv(), but that's a conversation for another time.
📋 Practical Task
Build a Database Connection String Parser
Write a program that simulates connecting to a database using environment variables. Your program must:
- Attempt to read
DB_USER,DB_PASS, andDB_HOST. - If any of these three variables are missing (NULL), print an error message stating exactly which variable is missing and exit the program with a non-zero status.
- If all three are present, print a mock connection string in the format:
Connecting to host [DB_HOST] as user [DB_USER]...(do NOT print the password for security reasons).
There are no comments for now.