C: validating numeric input with strtol

Accept only text that is entirely a number in the allowed range, and reject everything else.

C
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

static int parse_port(const char *text, int *out) {
  if (text[0] == '\0' || isspace((unsigned char)text[0])) {
    return 0;
  }
  char *end;
  errno = 0;
  long value = strtol(text, &end, 10);
  if (*end != '\0' || errno == ERANGE || value < 1 || value > 65535) {
    return 0;
  }
  *out = (int)value;
  return 1;
}

int main(void) {
  const char *inputs[] = {"8080", "0", "70000", "80a", "", " 22", "99999999999999999999"};
  for (int i = 0; i < 7; i++) {
    int port;
    if (parse_port(inputs[i], &port)) {
      printf("'%s' -> %d\n", inputs[i], port);
    } else {
      printf("'%s' -> rejected\n", inputs[i]);
    }
  }
  return 0;
}
Output
'8080' -> 8080
'0' -> rejected
'70000' -> rejected
'80a' -> rejected
'' -> rejected
' 22' -> rejected
'99999999999999999999' -> rejected

Explanation

Input from users, files, and the network cannot be trusted. Validate it: check its form, that the whole text was used, and that the value is in range. atoi is unsafe for this because it cannot report errors at all. It returns 0 for text such as abc.

strtol converts text to a long and reports where it stopped through an end pointer. The text is a complete number only if the end pointer reaches the terminating zero. errno becomes ERANGE if the value does not fit in a long. strtol also skips leading white space, so a strict validator rejects that explicitly.

Explore the dry run
Guided walkthroughReady
1#include <ctype.h>
2#include <errno.h>
3#include <stdio.h>
4#include <stdlib.h>
5
6static int parse_port(const char *text, int *out) {
7 if (text[0] == '\0' || isspace((unsigned char)text[0])) {
8 return 0;
9 }
10 char *end;
11 errno = 0;
12 long value = strtol(text, &end, 10);
13 if (*end != '\0' || errno == ERANGE || value < 1 || value > 65535) {
14 return 0;
15 }
16 *out = (int)value;
17 return 1;
18}
19
20int main(void) {
21 const char *inputs[] = {"8080", "0", "70000", "80a", "", " 22", "99999999999999999999"};
22 for (int i = 0; i < 7; i++) {
23 int port;
24 if (parse_port(inputs[i], &port)) {
25 printf("'%s' -> %d\n", inputs[i], port);
26 } else {
27 printf("'%s' -> rejected\n", inputs[i]);
28 }
29 }
30 return 0;
31}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Reject empty text and leading white space up front, since strtol would skip the spaces.—
—
Convert in base 10; end points at the first character that was not used.—
—
Reject trailing characters, overflow, and out-of-range values.—
—
Only 8080 passes all checks.—
'8080' -> 8080
'0' -> rejected
'70000' -> rejected
'80a' -> rejected
'' -> rejected
' 22' -> rejected
'99999999999999999999' -> rejected

Cost and common mistakes

O(n) for n characters.

  • errno must be set to 0 before the call, because strtol only sets it on failure.
  • Cast the char to unsigned char before passing it to isspace, or negative values cause undefined behaviour.
  • A valid number is still untrusted. Use it only for its intended purpose and range.

Try it yourself

Why does '80a' fail?

Reveal answer and reasoning

strtol stops at the letter a, so end does not point to the terminating zero, which means part of the text was not a number.

Further reading