Before you begin
Why this matters
When a program runs with the wrong setting, the first question is: where did this value come from? If you can follow the order in which your code looks, you can answer that by reading it, and you can spot the lines that turn what someone typed into a different number without reporting an error.
What this helps you reason about
- Predict which place a setting comes from when several disagree.
- Spot atoi turning text such as 80x into a number without any error.
- Report an error that says which place held the bad value.
A program often needs one setting, such as a port number, that can come from several places. This lesson builds the part of a C program that picks one value, checks it, and says where it came from.
Already know this? The short version
- Take the setting from the highest source that is present, here flag, then environment variable, then file, then the default.
- A const char * that is NULL is absent; a pointer to "" is present and empty, which is what getenv returns for a variable set to the empty string.
- Check only the chosen text, with your own rule (here 1 to 5 ASCII digits, then 1 to 65535): atoi("80x") is 80, atoi("x80") is 0, and atoi cannot report either.
- An invalid winner is an error that names its source and text; never fall back to a lower source or to the default.
- Read argv, getenv, and files in one place, at the start of the program, and pass their text to a function that reads nothing itself.
Words in this lesson (8)
- setting
- One value a program needs before it can do its work, such as the port number it listens on.
- source
- A place a setting can come from: a command-line flag, an environment variable, a configuration file, or a default written in the program.
- precedence
- The fixed order that decides which source wins when several supply the same setting. In this lesson: flag, then environment variable, then file, then default.
- present and absent
- A source is present when it supplies a value for the setting, even an empty one. It is absent when it supplies nothing at all.
- null pointer
- A pointer that points to nothing, written NULL in code and shown as 0x0 by the debugger. getenv returns NULL for a variable that is not set.
- undefined behavior
- Code for which the C standard sets no requirements. The program may crash, print wrong results, or seem to work, and the result can change between builds.
- validation
- Checking that a value follows the rules the program needs, such as “1 to 5 digits, from 1 to 65535”, before using it.
- provenance
- A record of where a value came from: here, the name of the source that won, kept with the value and named in every error.
Build brief · one port setting
- Input
- The text of a flag, an environment variable, and a file line, each of which may be absent.
- Output
- A port number and the source it came from, or an error that names the source and its text.
- Policy
- flag, then env, then file, then the default 3000. In this program’s policy, a source that is set counts even when its value is empty. Only the winner is checked: 1 to 5 digits, 1 to 65535. An invalid winner is an error.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Precedence
Which source wins?
A setting is one value a program needs before it can do its work, such as the port number it listens on. A setting can come from several sources: a command-line flag such as --port=7000, typed after the program’s name; an environment variable such as APP_PORT, set before the program starts; a line in a configuration file; or a default written in the program.
When several sources supply the port, the program needs a rule. Precedence is that rule: a fixed order of sources in which the first one that supplies a value wins. This program uses a common order: flag, then environment variable, then file, then the default 3000.
For now, the sources are parameters of type const char *, a pointer to text the function will not change. NULL, the null pointer, means the source is absent.
void choose(const char *flag, const char *env, const char *file) {
if (flag != NULL) {
printf("%s from flag\n", flag);
} else if (env != NULL) {
printf("%s from env\n", env);
} else if (file != NULL) {
printf("%s from file\n", file);
} else {
printf("3000 from default\n");
}
} Look at the sources in precedence order and stop at the first one that supplies a value.
Precisely: the order is a choice
No language or operating system sets this order. Flag, then environment, then file, then default is a common convention, because each source is more specific to one run than the next. Some tools add more sources, such as a file per user and a file per project. Whatever the order, a program should write it down where its users can read it.
Experiment
Follow the sources from the highest
NULL stands for an absent source. choose tests the sources in precedence order with else if, so it prints for the first pointer that is not NULL and skips the rest.
Why it matters When a setting surprises you, follow the same order by hand. The first source that is present is the one that won.
Predict which source wins before running. Then change "5000" in the call to NULL, so the environment variable is absent too, and predict again.
#include <stdio.h>
void choose(const char *flag, const char *env, const char *file) {
if (flag != NULL) {
printf("%s from flag\n", flag);
} else if (env != NULL) {
printf("%s from env\n", env);
} else if (file != NULL) {
printf("%s from file\n", file);
} else {
printf("3000 from default\n");
}
}
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
choose(NULL, "5000", "4100");
return 0;
}
What to look for
Step into choose. flag shows 0x0, the null pointer, so the first test fails and the next stop is the env test. env shows an address followed by "5000", and the next stop is the printf for env. After the edit, env shows 0x0 too, the next stops are the file test and its printf, and the run prints 4100 from file.
Expected output
5000 from env02 / Presence
Is an empty value the same as a missing one?
A source is present when it supplies a value, and absent when it supplies nothing. Starting a program with APP_PORT set to "" sets the variable, to an empty value. In this program’s policy, a source that is set counts even when its value is empty: someone set it, so the program checks that empty value and reports an error, instead of moving on to the next source. Some tools choose the other policy and treat an empty variable as not set; either is fine if the documentation says which.
getenv returns NULL when the variable is not set, and a pointer to its text when it is set, even to "". Comparing with NULL tells the two apart. Testing port[0] == '\0' tells only whether the text is empty, and doing that on a NULL pointer is undefined behavior.
const char *port = getenv("APP_PORT");
if (port == NULL) {
printf("absent\n");
} else {
printf("[%s]\n", port); /* [] when set to "" */
} Compare with NULL to decide whether a source is present, before reading its text.
Coming from Java: getenv’s NULL plays the part of Java’s null for a missing variable, but reading through it is undefined behavior, not a NullPointerException.
Precisely: setenv is POSIX
getenv is standard C; setenv and unsetenv, which the experiment uses to give APP_PORT a value, come from POSIX. Compiled with -std=c11, <stdlib.h> declares them only when _POSIX_C_SOURCE is defined first, which is why the experiment starts with that line. The pointer getenv returns may be changed by a later setenv, so copy the text if you keep it.
Experiment
Tell an empty value from a missing one
getenv returns NULL when the variable is not set, and a pointer to its text when it is set, even to "". Comparing with NULL tells the two apart. Testing port[0] == '\0' tells only whether the text is empty, and reading through NULL is undefined behavior.
Why it matters Compare with NULL to decide whether a source is present. Check for NULL before reading the text.
Predict the output before running. Then change setenv("APP_PORT", "", 1); to unsetenv("APP_PORT"); and predict again.
#define _POSIX_C_SOURCE 200809L // makes <stdlib.h> declare setenv
#include <stdio.h>
#include <stdlib.h>
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
setenv("APP_PORT", "", 1); // as if the shell set APP_PORT to ""
const char *port = getenv("APP_PORT");
if (port == NULL) {
printf("absent\n");
} else {
printf("[%s]\n", port);
}
return 0;
}
What to look for
Step past getenv and look at port. It shows an address followed by "", the empty string: the variable is set, so getenv returns a pointer to its text, and the run prints []. After the edit, port shows 0x0, the null pointer, the if takes the first branch, and the run prints absent.
Expected output
[]03 / Validation
What if the winning value is not a port?
The chosen value is still text. Before the program uses it as a port, validation checks the text against the program’s own rule: one to five ASCII digits (the characters 0 to 9), and a number from 1 to 65535. Leading zeros are allowed, so 00080 means 80.
atoi is not that rule. It converts the digits at the start of the text and ignores the rest, so atoi("80x") is 80. It returns 0 when there are no digits, the same as for "0", and it cannot report an error. So the program checks every character first, then converts with strtol, the standard function for converting text to a long.
When the winning value fails the check, the program could fall through to the next source or stop with an error. This program stops. If the flag says 80x, the person asked for a port on this run. Using the environment’s 5000 instead, with no error, would start the program on a port the person did not ask for.
size_t length = strlen(s.raw);
int digits = length >= 1 && length <= 5;
for (size_t i = 0; i < length; i++) {
if (s.raw[i] < '0' || s.raw[i] > '9') {
digits = 0;
}
}
if (!digits) {
s.problem = "is not 1 to 5 digits";
return s;
}
long port = strtol(s.raw, NULL, 10); Check the value that won. If it fails, report an error; do not try the next source.
Coming from Python: atoi never raises an error the way int("80x") does: it returns 80, or 0, and carries on.
Precisely: strtol on its own
strtol(raw, &end, 10) sets end to the first character it did not use, so *end != '\0' means there was text after the number. It still skips leading spaces and accepts a + or - sign, and it sets errno to ERANGE when the number does not fit in a long. atoi has undefined behavior in that case. After the digit check above, none of this can happen. Base 10 also matters: it reads 00080 as 80, while base 0 would read a leading 0 as the start of an octal number.
Experiment
Give the parser a value that is not a port
atoi converts the digits at the start of the text and ignores the rest. It returns 0 when there are none, and it has no way to report an error. Its behavior is undefined when the number does not fit in an int.
Why it matters Check every character before converting, or use strtol and check where it stopped.
Predict the output before running. Then change "80x" to "x80" and predict again.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
const char *raw = "80x";
int port = atoi(raw);
if (port < 1 || port > 65535) {
printf("flag: \"%s\" is not a port\n", raw);
} else {
printf("%d from flag\n", port);
}
return 0;
}
What to look for
Step past atoi and look at port: it shows 80. atoi read the 8 and the 0 and stopped at x without reporting anything, and the run prints 80 from flag. After the edit, port shows 0, the range test fails, and the run prints flag: "x80" is not a port. atoi also returns 0 for "0", so port alone cannot tell you what went wrong.
Expected output
80 from flag04 / Provenance
How does the program say where a value came from?
Provenance is a record of where a value came from. The program keeps it by returning the source with the port, and by putting the source and the raw text in every error. Then error: flag "80x" is not 1 to 5 digits tells the person which of the four places to fix.
load returns a struct by value. It holds the source’s name, the raw text, the port, and problem, which is NULL when the text is a valid port and a short description when it is not. The code that called load prints the error from those fields.
typedef struct {
/* where the value came from */
const char *source;
/* the text exactly as it was given */
const char *raw;
/* 0 when the text is not a valid port */
long port;
/* NULL when the text is a valid port */
const char *problem;
} Setting; Return the source with the value, and name the source and the raw text in every error.
Precisely: whose text it is
source and raw point at text the struct does not own: string literals, getenv’s result, or argv. They stay valid as long as that text does. If a later setenv could change the environment, copy the text first.
05 / Real sources
Where do the real flags, variables, and files come in?
A real program reads its sources from outside. main receives the words of the command line as argc and argv, getenv reads an environment variable, and fopen with fgets reads a file. Each gives text, or NULL when something is missing.
Keep those reads in one place, at the start of the program. That code collects the text and passes it to load; load itself reads nothing from outside. Then you can call load with any combination of sources, as the experiments and the program on this page do.
The stepper on this page always starts a program without flags, and with an environment you cannot set from outside. So this lesson passes the sources as arguments. The presence experiment calls setenv inside the program to give APP_PORT a value; a real program would not set its own setting.
/* The start of a real program; not run on this page. */
int main(int argc, char *argv[]) {
const char *flag = NULL;
for (int i = 1; i < argc; i++) {
if (strncmp(argv[i], "--port=", 7) == 0) {
flag = argv[i] + 7;
}
}
Setting s = load(flag, getenv("APP_PORT"), NULL); Read the command line, the environment, and files in one place, and pass plain values to the code that decides.
Precisely: the command line
argv[0] is the program’s own name, so the loop starts at 1. argv[i] + 7 points just past the seven characters of --port=. A real tool usually uses getopt_long, a GNU and BSD extension (POSIX specifies only getopt), which also handles --port 7000 with a space. The file is passed as NULL here; reading one is a separate topic in this module, Files, parsing, and serialization.
06 / Try it
Can you predict the winner and the outcome?
The workbench below is a model of this lesson’s policy that runs in your browser. It does not run C code or read your environment. It draws the sources in the reverse of this lesson’s order: flag, the highest source, is at the right end. So the winner is the source furthest to the right that is present.
Predict the result before each change: turn off flag; turn off env as well; turn flag back on and clear its text; then type broken into file while flag holds 7000.
The flag is absent, APP_PORT is set to 0080, and the file says 70000. What does the program report, and why?
Reveal the reasoning
80 from env. The environment variable is the highest source that is present. 0080 is four digits, and leading zeros are allowed, so it gives 80. The file’s 70000 would fail the range check, but the file did not win, so it is never checked.
07 / Whole program
Does the whole program follow the policy?
The complete program below joins the pieces and runs the thirteen acceptance cases, one output line each. Each case gives the flag, the environment variable, and the file, with NULL for an absent source. Copy it into a file, compile it as C11, and run it to see the same thirteen lines.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
/* where the value came from */
const char *source;
/* the text exactly as it was given */
const char *raw;
/* 0 when the text is not a valid port */
long port;
/* NULL when the text is a valid port */
const char *problem;
} Setting;
Setting load(const char *flag, const char *env, const char *file) {
Setting s = {"default", "3000", 0, NULL};
if (flag != NULL) {
s.source = "flag";
s.raw = flag;
} else if (env != NULL) {
s.source = "env";
s.raw = env;
} else if (file != NULL) {
s.source = "file";
s.raw = file;
}
size_t length = strlen(s.raw);
int digits = length >= 1 && length <= 5;
for (size_t i = 0; i < length; i++) {
if (s.raw[i] < '0' || s.raw[i] > '9') {
digits = 0;
}
}
if (!digits) {
s.problem = "is not 1 to 5 digits";
return s;
}
long port = strtol(s.raw, NULL, 10);
if (port < 1 || port > 65535) {
s.problem = "is not between 1 and 65535";
return s;
}
s.port = port;
return s;
}
int main(void) {
/* Each case is {flag, env, file}; NULL means absent. */
const char *cases[][3] = {
{"7000", "5000", "4100"},
{NULL, "5000", "4100"},
{NULL, NULL, "4100"},
{NULL, NULL, NULL},
{"", "5000", NULL},
{"80x", "5000", NULL},
{" 80", NULL, NULL},
{"+80", NULL, NULL},
{"0", NULL, NULL},
{"65536", NULL, NULL},
{"7000", NULL, "broken"},
{"00080", NULL, NULL},
{NULL, "", "4100"},
};
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) {
Setting s = load(cases[i][0], cases[i][1], cases[i][2]);
if (s.problem != NULL) {
printf("error: %s \"%s\" %s\n", s.source, s.raw, s.problem);
} else {
printf("%ld from %s\n", s.port, s.source);
}
}
return 0;
}
Acceptance cases
- flag 7000, env 5000, file 41007000 from flag
- no flag; env 5000, file 41005000 from env
- only file 41004100 from file
- no flag, env, or file3000 from default
- flag "" (set, but empty), env 5000error: flag "" is not 1 to 5 digits
- flag 80x, env 5000error: flag "80x" is not 1 to 5 digits
- flag " 80" (a space first)error: flag " 80" is not 1 to 5 digits
- flag +80error: flag "+80" is not 1 to 5 digits
- flag 0error: flag "0" is not between 1 and 65535
- flag 65536error: flag "65536" is not between 1 and 65535
- flag 7000, file broken7000 from flag; the file is never checked
- flag 0008080 from flag
- env "" (set, but empty), file 4100error: env "" is not 1 to 5 digits
What does this program settle, and what does it leave out?
Reveal the reasoning
It settles one setting: which source wins, whether its text is a port, and how to say where it came from. It does not read a real command line, environment, or file, check settings that did not win, or start anything on the port. Each of those is a separate piece with its own rules.
Practice tickets
- CFG-101 · Stop hiding a bad retention setting → Free
- CFG-102 · Let the last width win → Sign in · Pro
- CFG-103 · Choose the watering minutes → Sign in · Pro
Keep exploring
I/O, buffering, and external state →