Shell Rice: Last Command Exit Code

Rice?

For those wondering, the term rice in the title generally refers to customizing software for fun. Although commonly used to describe visual improvements without any functionality changes, the term is not strictly defined.

Last Command Exit Code

I like to show the exit code of the last command in the shell. Since I use the fish shell, I can access the exit code using the $status environment variable:

$ bash -c "exit 42"
$ echo "$status"
42

Having it in my prompt is as simple as checking if the value is not 0 (I don’t want any output if the last command was successful), then printing it in the appropriate location:

# Somewhere in fish_prompt.fish or fish_right_prompt.fish
if test $status -ne 0
  echo $status
end

However, having just the number isn’t very helpful. Sometimes, I wish to know why the command exited unsuccessfully, not just what the exit code is.

Let’s take a detour and break it down, starting with why we got an output of 70 when you exited with 12358.

Exit Codes

Disclaimer: I might get something incorrect or inaccurate here, let me know if there is a mistake! I assume a modern-ish Linux system in the examples below.

We can observe the restriction on exit codes in various places; all of them show that only the least significant 8 bits of the status code are kept. Therefore, we can see that 12358 is 0x3046, which is 0x46 or 70 truncated.

POSIX Standard

The value of status may be 0, EXITSUCCESS, EXITFAILURE, or any other value, though only the least significant 8 bits (that is, status & 0377) shall be available from wait() and waitpid(); the full value shall be available from waitid() and in the siginfo_t passed to a signal handler for SIGCHLD.

IEEE Std 1003.1-2017

The POSIX standard for the exit function provides some information about the truncation of status bits. We shall see soon enough that although the requirement is only for wait and waitpid, and that waitid needs to return the full value, in practice the exit code is still truncated to 8 bits. Note that _Exit, the “don’t call atexit” version of exit, is similarly restricted.

I learned that the hard way completing a university project:

$ gcc -x c -o oops - << EOF

#include <stdio.h>
int main(void) {
    printf("%d is not %d\n", 010, 10);
}
EOF

$ ./oops
8 is not 10

Libc

Programs that use a C standard library generally exit using exit or _Exit. This includes returning from main in C:

…a return from the initial call to the main function is equivalent to calling the exit function with the value returned by the main function as its argument…<br><br>

Source: 5.1.2.3 of ISO C Standard (PDF)

Thus, we can look at the implementations of exit in various C libraries to see what they do (certain parts omitted):

  1. glibc in ../unix/sysv/linux/_exit.c

    void _exit (int status) {
      while (1) {
        INLINE_SYSCALL (exit_group, 1, status);
      }
    }
  2. musl in src/exit/_Exit.c

    _Noreturn void _Exit(int ec) {
        __syscall(SYS_exit_group, ec);
        for (;;) __syscall(SYS_exit, ec);
    }
  3. uClibc in ../sysdeps/linux/common/_exit.c

    # define __NR_exit __NR_exit_group
    
    void _exit(int status) {
      /* The loop is added only to keep gcc happy. */
      while(1) {
        INLINE_SYSCALL(exit, 1, status);
      }
    }

We can see that all of them end up calling the exit_group syscall, which brings us to…

Linux

In kernel/exit.c, we see what the kernel does with the error code:

SYSCALL_DEFINE1(exit_group, int, error_code)
{
	do_group_exit((error_code & 0xff) << 8);
	/* NOTREACHED */
	return 0;
}

It only keeps the last 8 bits! The shifting to the left is to store other information, and we can see how glibc fulfills the POSIX requirement for waitpid and friends when extracting the exit code in bits/waitstatus.h:

/* If WIFEXITED(STATUS), the low-order 8 bits of the status.  */
#define	__WEXITSTATUS(status)	(((status) & 0xff00) >> 8)

Mission accomplished!

Exit Code Convention

The Linux Documentation Project specifies certain meanings for certain exit code numbers. Some interesting exit code conventions include 126 meaning command invoked cannot execute, 127 meaning command not found, and 128+n meaning fatal error signal “n”. Applications and scripts are discouraged from using these exit codes, as they may cause confusion with general shell-error exit codes.

Spelunking in the fish shell source code, we can see that the 128+n convention is observed:

int status_value() const {
    if (signal_exited()) {
        return 128 + signal_code();
    } else if (normal_exited()) {
        return exit_code();
    } else {
        DIE("Process is not exited");
    }
}

…and also that the conventions for error code 126 and 127 are observed:

enum {
    /// The status code used for normal exit in a command.
    STATUS_CMD_OK = 0,
    /// The status code used for failure exit in a command (but not if the args were invalid).
    STATUS_CMD_ERROR = 1,
    /// The status code used for invalid arguments given to a command. This is distinct from valid
    /// arguments that might result in a command failure. An invalid args condition is something
    /// like an unrecognized flag, missing or too many arguments, an invalid integer, etc. But
    STATUS_INVALID_ARGS = 2,

    /// The status code used when a command was not found.
    STATUS_CMD_UNKNOWN = 127,

    /// The status code used when an external command can not be run.
    STATUS_NOT_EXECUTABLE = 126,

    /// The status code used when a wildcard had no matches.
    STATUS_UNMATCHED_WILDCARD = 124,
    /// The status code used when illegal command name is encountered.
    STATUS_ILLEGAL_CMD = 123,
    /// The status code used when `read` is asked to consume too much data.
    STATUS_READ_TOO_MUCH = 122,
    /// The status code when an expansion fails, for example, "$foo["
    STATUS_EXPAND_ERROR = 121,
};

Spelunking has yielded benefits! We see other defined exit codes, which may or may not have the same meaning in other shells as I am too lazy to spelunk further. Still a plus for source code searching and good documentation!

Anyway, I use fish and write tools for my own use only so I can hardcode the meanings of the different exit codes. Let’s do it in C!

Doing It in C

We will write a C program that takes in a number representing the exit code and prints a prettier representation to standard output. Let’s start with a simple program that expects a single argument:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[argc + 1]) {
  if (argc != 2) {
    if (argc <= 1) {
      fprintf(stderr, "Not enough arguments,");
    }
    else {
      fprintf(stderr, "Too many arguments,");
    }
    fprintf(stderr, " expected a single integer representing an exit code.\n");
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}

Now, let’s define a helper function to convert the string to an integer, courtesy of freebsd’s strtonum:

#include <errno.h>

long strtonum(char const *str) {
  errno = 0;
  char *end;
  long num = strtol(str, &end, 10);

  if (errno != 0) {
    perror("Error: strtol: ");
    exit(EXIT_FAILURE);
  }
  else if ((num == 0 && end == str) || *end != '\0') {
    fprintf(stderr, "Error: entire string is not a valid integer: %s\n", str);
    exit(EXIT_FAILURE);
  }

  return num;
}

Note that we abort on failure because it’s our program and we can do whatever we want. Now that we have a number, we can add the conversion code to form our little program:

#define _GNU_SOURCE
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

long strtonum(char const *str) {
  errno = 0;
  char *end;
  long num = strtol(str, &end, 10);

  if (errno != 0) {
    perror("Error: strtol: ");
    exit(EXIT_FAILURE);
  } else if ((num == 0 && end == str) || *end != '\0') {
    fprintf(stderr, "Error: entire string is not a valid integer: %s\n", str);
    exit(EXIT_FAILURE);
  }

  return num;
}

int main(int argc, char *argv[argc + 1]) {
  if (argc != 2) {
    if (argc <= 1) {
      fprintf(stderr, "Not enough arguments,");
    } else {
      fprintf(stderr, "Too many arguments,");
    }
    fprintf(stderr, " expected a single integer representing an exit code.\n");
    return EXIT_FAILURE;
  }

  char *exit_code_str = argv[1];
  long exit_code = strtonum(exit_code_str);

  // Signal-related exit
  if (exit_code > 128 && exit_code <= 255) {
    char const *signal = sigabbrev_np((int)exit_code - 128);
    if (signal != NULL) {
      fprintf(stdout, "SIG%s\n", signal);
    } else {
      fprintf(stdout, "%ld\n", exit_code);
    }
  }
  // General error
  else if (exit_code == 1) {
    fprintf(stdout, "❌\n");
  }
  // Command not executable
  else if (exit_code == 126) {
    fprintf(stdout, "❗\n");
  }
  // Command not found
  else if (exit_code == 127) {
    fprintf(stdout, "❓\n");
  }
  // Catch-all, just return the exit code
  else {
    fprintf(stdout, "%ld\n", exit_code);
  }

  return EXIT_SUCCESS;
}

And with that, our little program is complete!

Good catch! The regular C library function to get a string describing a signal is strsignal. However, like most C standard library functions, it has some warts:

The string returned by strsignal() is localized according to the LC_MESSAGES category in the current locale>

This isn’t a big deal in this specific, particular case, but know that locales are a very bad idea.

The strsignal() function returns the appropriate description string, or an unknown signal message if the signal number is in‐valid. On some systems (but not on Linux), NULL may instead be returned for an invalid signal number.

That’s just weird; Instead of just checking for NULL, I’d also have to do a string comparison for a system-specific string.

Putting aside the warts, strsignal returns a message like Interrupted for SIGINT. Such messages get long and unwieldly in the prompt, and I’d much rather have the abbreviation INT that sigabbrev_np gives me. The catch is that this is a non-portable extension; I have to place #define _GNU_SOURCE at the top of my source file and compile with glibc.

Wrapping Up

Placing the compiled program somewhere in my $PATH (I called it intstr), I’m good to go after a slight tweak to my fish config:

# Somewhere in fish_prompt.fish or fish_right_prompt.fish
if test $status -ne 0
  echo (intstr $status)
end

I still have a lot of other things to share about my shell prompt, so stay tuned!