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.
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 _
, 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 _
.
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>
Thus, we can look at the implementations of exit
in various C libraries to see what they do (certain parts omitted):
-
glibc in ../unix/sysv/linux/_exit.c
void _exit (int status) { while (1) { INLINE_SYSCALL (exit_group, 1, status); } }
-
musl in src/exit/_Exit.c
_Noreturn void _Exit(int ec) { __syscall(SYS_exit_group, ec); for (;;) __syscall(SYS_exit, ec); }
-
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_
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 theLC_
category in the current locale>MESSAGES
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_
gives me.
The catch is that this is a non-portable extension; I have to place #define _
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!