
Unix Cli
- 261 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
unix-cli: A skill for development. This provides functionality for development workflows.
Key points
- unix-cli
Unix Cli by the numbers
- 261 all-time installs (skills.sh)
- +6 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,463 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill unix-cliAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 261 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I use unix-cli for development tasks?
Use unix-cli for development tasks
Who is it for?
Best when you're working on backend & apis and need structured help with unix-cli.
Skip if: Teams with no backend & apis needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to use unix-cli for development tasks, or when unix-cli: a skill for development. this provides functionality for development workflows.
What you get
Structured output aligned to unix-cli: unix-cli.
Files
UNIX/POSIX Standards CLI Best Practices
Comprehensive guidelines for building command-line tools that follow UNIX conventions, designed for AI agents and LLMs. Contains 44 rules across 8 categories, prioritized by impact from critical (argument handling, exit codes, output streams) to incremental (configuration and environment).
When to Apply
Reference these guidelines when:
- Writing new CLI tools in any language
- Parsing command-line arguments and flags
- Deciding what goes to stdout vs stderr
- Choosing appropriate exit codes
- Handling signals like SIGINT and SIGTERM
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Argument & Flag Design | CRITICAL | args- |
| 2 | Exit Codes | CRITICAL | exit- |
| 3 | Output Streams | CRITICAL | output- |
| 4 | Error Handling | HIGH | error- |
| 5 | I/O & Composition | HIGH | io- |
| 6 | Help & Documentation | MEDIUM-HIGH | help- |
| 7 | Signals & Robustness | MEDIUM | signal- |
| 8 | Configuration & Environment | MEDIUM | config- |
Quick Reference
1. Argument & Flag Design (CRITICAL)
- `args-use-getopt` - Use standard argument parsing libraries
- `args-provide-long-options` - Provide long options for all short options
- `args-support-double-dash` - Support double-dash to terminate options
- `args-require-help-version` - Implement --help and --version options
- `args-prefer-flags-over-positional` - Prefer flags over positional arguments
- `args-use-standard-flag-names` - Use standard flag names
- `args-never-read-secrets-from-flags` - Never read secrets from command-line flags
- `args-support-option-bundling` - Support option bundling
2. Exit Codes (CRITICAL)
- `exit-zero-for-success` - Return zero for success only
- `exit-use-standard-codes` - Use standard exit codes
- `exit-signal-codes` - Use 128+N for signal termination
- `exit-partial-success` - Handle partial success consistently
- `exit-distinguish-error-types` - Distinguish error types with different exit codes
3. Output Streams (CRITICAL)
- `output-stdout-for-data` - Write data to stdout only
- `output-stderr-for-errors` - Write errors and diagnostics to stderr
- `output-detect-tty` - Detect TTY for human-oriented output
- `output-provide-machine-format` - Provide machine-readable output format
- `output-line-based-text` - Use line-based output for text streams
- `output-respect-no-color` - Respect NO_COLOR environment variable
4. Error Handling (HIGH)
- `error-include-program-name` - Include program name in error messages
- `error-actionable-messages` - Make error messages actionable
- `error-use-strerror` - Use strerror for system errors
- `error-avoid-stack-traces` - Avoid stack traces in user-facing errors
- `error-validate-early` - Validate input early and fail fast
5. I/O & Composition (HIGH)
- `io-support-stdin` - Support reading from stdin
- `io-write-to-stdout` - Write output to stdout by default
- `io-be-stateless` - Design stateless operations
- `io-handle-binary-safely` - Handle binary data safely
- `io-atomic-writes` - Use atomic file writes
- `io-handle-multiple-files` - Handle multiple input files consistently
6. Help & Documentation (MEDIUM-HIGH)
- `help-show-usage-on-error` - Show brief usage on argument errors
- `help-structure-help-output` - Structure help output consistently
- `help-show-defaults` - Show default values in help
- `help-include-examples` - Include practical examples in help
- `help-version-format` - Format version output correctly
7. Signals & Robustness (MEDIUM)
- `signal-handle-sigint` - Handle SIGINT gracefully
- `signal-handle-sigterm` - Handle SIGTERM for clean shutdown
- `signal-handle-sigpipe` - Handle SIGPIPE for broken pipes
- `signal-cleanup-on-second-interrupt` - Skip cleanup on second interrupt
8. Configuration & Environment (MEDIUM)
- `config-follow-xdg` - Follow XDG Base Directory Specification
- `config-precedence-order` - Apply configuration in correct precedence order
- `config-env-naming` - Use consistent environment variable naming
- `config-never-store-secrets` - Never store secrets in config files or environment
- `config-respect-standard-vars` - Respect standard environment variables
How to Use
Read individual reference files for detailed explanations and code examples:
- Section definitions - Category structure and impact levels
- Rule template - Template for adding new rules
Reference Files
| File | Description |
|---|---|
| references/_sections.md | Category definitions and ordering |
| assets/templates/_template.md | Template for new rules |
| metadata.json | Version and reference information |
Rule Title Here
Brief explanation (1-3 sentences) of WHY this matters. Focus on the performance or correctness implications for CLI tools.
Incorrect (description of what's wrong):
// Bad code example - production-realistic
// Comment explaining the cost or problemCorrect (description of what's right):
// Good code example - minimal diff from incorrect
// Comment explaining the benefitAlternative (when to use this approach):
// Alternative approach for specific contextsWhen NOT to use this pattern:
- Exception 1
- Exception 2
Benefits:
- Benefit 1
- Benefit 2
Reference: Reference Title
{
"version": "1.0.6",
"organization": "UNIX/POSIX Standards",
"technology": "UNIX CLI",
"date": "January 2026",
"abstract": "Comprehensive guidelines for building command-line tools that follow UNIX conventions, designed for AI agents and LLMs. Contains 44 rules across 8 categories, prioritized by impact from critical (argument handling, exit codes, output streams) to incremental (configuration and environment). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.",
"references": [
"https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html",
"https://www.gnu.org/prep/standards/standards.html",
"https://clig.dev/",
"https://man.freebsd.org/cgi/man.cgi?query=sysexits",
"https://www.gnu.org/prep/standards/html_node/Errors.html",
"https://12factor.net/",
"https://no-color.org/",
"https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html"
]
}
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules.
---
1. Argument & Flag Design (args)
Impact: CRITICAL Description: Proper argument parsing is the entry point to every CLI invocation; violations break composition, confuse users, and prevent script automation.
2. Exit Codes (exit)
Impact: CRITICAL Description: Exit codes are the primary interface for scripts and CI/CD pipelines; incorrect codes cause cascading automation failures and silent data corruption.
3. Output Streams (output)
Impact: CRITICAL Description: Correctly separating stdout and stderr enables UNIX pipes and redirection; mixing them breaks the fundamental composability of the UNIX tool chain.
4. Error Handling (error)
Impact: HIGH Description: Clear, actionable error messages reduce user frustration and support burden; poor errors multiply debugging time across every failure.
5. I/O & Composition (io)
Impact: HIGH Description: Proper stdin/stdout handling and stateless design enable tools to work together; poor I/O patterns break the UNIX philosophy of small, composable programs.
6. Help & Documentation (help)
Impact: MEDIUM-HIGH Description: Discoverable, well-structured help reduces learning curve and support requests; poor help forces users to external documentation or trial-and-error.
7. Signals & Robustness (signal)
Impact: MEDIUM Description: Proper signal handling prevents orphaned processes, enables graceful shutdown, and respects user intent when interrupting operations.
8. Configuration & Environment (config)
Impact: MEDIUM Description: Following XDG and environment variable conventions prevents config file sprawl and ensures consistent behavior across environments.
Never Read Secrets from Command-Line Flags
Command-line arguments appear in process listings (ps aux), shell history, and logs. Never accept passwords, API keys, or tokens as flag values.
Incorrect (secret visible in process list):
# Anyone on the system can see this password
$ ps aux | grep mytool
user 1234 mytool --password=SuperSecret123 --user=admin
# Password saved in shell history
$ history
500 mytool --password=SuperSecret123 --user=adminstatic struct option opts[] = {
{"password", required_argument, NULL, 'p'}, // Visible in ps
{"api-key", required_argument, NULL, 'k'}, // Visible in ps
{NULL, 0, NULL, 0}
};Correct (read secrets from file or stdin):
# Read from file (with restricted permissions)
$ mytool --password-file=/etc/mytool/credentials
# Read from environment (slightly better, still visible in /proc)
$ MYTOOL_PASSWORD=secret mytool
# Read from stdin (safest, not logged anywhere)
$ echo "secret" | mytool --password-stdin
$ mytool --password-stdin < /dev/ttystatic struct option opts[] = {
{"password-file", required_argument, NULL, 'P'},
{"password-stdin", no_argument, NULL, 's'},
{NULL, 0, NULL, 0}
};
char *read_password(int from_stdin, const char *file) {
if (from_stdin) {
return read_line(stdin); // Read from stdin
}
if (file) {
FILE *f = fopen(file, "r");
// Check file permissions (should be 0600)
return read_line(f);
}
return NULL;
}Additional security measures:
- Check that password files have mode 0600
- Use a secrets manager or keyring when available
- Clear password from memory after use
Reference: Command Line Interface Guidelines - Security
Prefer Flags Over Positional Arguments
Use named flags instead of positional arguments when the meaning isn't obvious. Flags are self-documenting, order-independent, and allow adding new options without breaking existing scripts.
Incorrect (positional args with unclear meaning):
# What does "5" mean? What does "json" mean?
mytool input.txt output.txt 5 jsonint main(int argc, char *argv[]) {
if (argc != 5) {
fprintf(stderr, "Usage: %s input output retries format\n", argv[0]);
return 1;
}
char *input = argv[1];
char *output = argv[2];
int retries = atoi(argv[3]); // User must remember position
char *format = argv[4];
}Correct (flags are self-documenting):
# Intent is clear, order doesn't matter
mytool --input input.txt --output output.txt --retries 5 --format json
mytool -i input.txt -o output.txt -r 5 -f jsonint main(int argc, char *argv[]) {
char *input = NULL, *output = NULL, *format = "text";
int retries = 3;
static struct option opts[] = {
{"input", required_argument, NULL, 'i'},
{"output", required_argument, NULL, 'o'},
{"retries", required_argument, NULL, 'r'},
{"format", required_argument, NULL, 'f'},
{NULL, 0, NULL, 0}
};
int opt;
while ((opt = getopt_long(argc, argv, "i:o:r:f:", opts, NULL)) != -1) {
switch (opt) {
case 'i': input = optarg; break;
case 'o': output = optarg; break;
case 'r': retries = atoi(optarg); break;
case 'f': format = optarg; break;
}
}
}When positional arguments are acceptable:
- File operands after all options:
cat file1.txt file2.txt - Well-known patterns:
cp source dest,mv old new
Reference: Command Line Interface Guidelines
Provide Long Options for All Short Options
Every single-letter option should have an equivalent long-form option. Long options are self-documenting in scripts and easier to remember.
Incorrect (short options only, cryptic in scripts):
#!/bin/bash
# What does -q -f -r mean? Reader must check docs
mytool -q -f -r /datawhile ((opt = getopt(argc, argv, "qfr")) != -1) {
switch (opt) {
case 'q': quiet = 1; break;
case 'f': force = 1; break;
case 'r': recursive = 1; break;
}
}Correct (long options are self-documenting):
#!/bin/bash
# Intent is immediately clear
mytool --quiet --force --recursive /datastatic struct option long_options[] = {
{"quiet", no_argument, NULL, 'q'},
{"force", no_argument, NULL, 'f'},
{"recursive", no_argument, NULL, 'r'},
{NULL, 0, NULL, 0}
};
while ((opt = getopt_long(argc, argv, "qfr", long_options, NULL)) != -1) {
switch (opt) {
case 'q': quiet = 1; break;
case 'f': force = 1; break;
case 'r': recursive = 1; break;
}
}Benefits:
- Scripts become self-documenting
- Users can guess options (
--verbose,--help,--version) - Reduces trips to man pages
Reference: GNU Coding Standards
Implement --help and --version Options
Every CLI must support --help and --version. These are universally expected and required by GNU Coding Standards. Exit with status 0 after displaying either.
Incorrect (no help or version support):
int main(int argc, char *argv[]) {
// No --help or --version handling
if (argc < 2) {
fprintf(stderr, "Error: missing argument\n");
return 1; // User has no way to learn usage
}
process(argv[1]);
}Correct (both options implemented properly):
static void print_help(const char *progname) {
printf("Usage: %s [OPTIONS] FILE...\n", progname);
printf("Process files according to specified options.\n\n");
printf("Options:\n");
printf(" -v, --verbose increase verbosity\n");
printf(" -o, --output=F write output to F\n");
printf(" -h, --help display this help and exit\n");
printf(" --version output version information and exit\n");
printf("\nReport bugs to: bugs@example.com\n");
}
static void print_version(void) {
printf("mytool 1.0.0\n");
printf("Copyright (C) 2024 Example Inc.\n");
printf("License GPLv3+: GNU GPL version 3 or later\n");
}
int main(int argc, char *argv[]) {
static struct option long_options[] = {
{"help", no_argument, NULL, 'h'},
{"version", no_argument, NULL, 'V'},
{NULL, 0, NULL, 0}
};
int opt;
while ((opt = getopt_long(argc, argv, "hV", long_options, NULL)) != -1) {
switch (opt) {
case 'h': print_help(argv[0]); return EXIT_SUCCESS;
case 'V': print_version(); return EXIT_SUCCESS;
}
}
}Note: The --version output should include program name, version, copyright, and license. Bug reports go in --help.
Reference: GNU Coding Standards - --version
Support Double-Dash to Terminate Options
The -- argument terminates option processing, allowing operands that start with - to be treated as filenames rather than options. This is required by POSIX and essential for safe file handling.
Incorrect (cannot process files starting with dash):
int main(int argc, char *argv[]) {
for (int i = 1; i < argc; i++) {
if (argv[i][0] == '-') {
process_option(argv[i]); // -myfile.txt treated as option
} else {
process_file(argv[i]);
}
}
}# User cannot delete a file named -rf
$ rm -rf # Interpreted as options, not filename
rm: missing operandCorrect (getopt handles -- automatically):
int main(int argc, char *argv[]) {
int opt;
while ((opt = getopt(argc, argv, "v")) != -1) {
switch (opt) {
case 'v': verbose = 1; break;
}
}
// After getopt, optind points past -- if present
for (int i = optind; i < argc; i++) {
process_file(argv[i]); // -myfile.txt processed as filename
}
}# Double-dash allows processing files starting with dash
$ rm -- -rf # Deletes file named "-rf"
$ grep pattern -- -myfile.txtWhen NOT to use this pattern:
- Interactive shells where
--has other meanings
Reference: POSIX Utility Conventions
Support Option Bundling
Allow multiple single-letter options to be combined behind one hyphen. This is standard POSIX behavior and users expect it to work.
Incorrect (requires separate hyphens):
// Manual parsing that doesn't support bundling
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-l") == 0) long_format = 1;
else if (strcmp(argv[i], "-a") == 0) show_all = 1;
else if (strcmp(argv[i], "-h") == 0) human_readable = 1;
}# User expects this to work but it fails
$ mytool -lah
mytool: invalid option -- 'lah'
# User forced to type each separately
$ mytool -l -a -hCorrect (getopt handles bundling automatically):
int opt;
while ((opt = getopt(argc, argv, "lahvo:")) != -1) {
switch (opt) {
case 'l': long_format = 1; break;
case 'a': show_all = 1; break;
case 'h': human_readable = 1; break;
case 'v': verbose = 1; break;
case 'o': output_file = optarg; break;
}
}# All of these work identically
$ mytool -lah
$ mytool -l -a -h
$ mytool -la -h
$ mytool -h -al
# With argument at end
$ mytool -lavo output.txtNote: Options with required arguments can appear at the end of a bundle: -lvo file means -l -v -o file.
Reference: POSIX Utility Conventions - Guideline 5
Use Standard Argument Parsing Libraries
Use getopt, getopt_long, or equivalent libraries instead of manual argument parsing. These libraries handle edge cases, provide consistent behavior, and generate proper error messages.
Incorrect (manual parsing, breaks on edge cases):
int main(int argc, char *argv[]) {
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-v") == 0) {
verbose = 1;
} else if (strcmp(argv[i], "-o") == 0) {
output_file = argv[++i]; // Crashes if -o is last arg
}
}
// Doesn't handle: -vo file, --verbose, -v -o, unknown options
}Correct (getopt handles all edge cases):
int main(int argc, char *argv[]) {
int opt;
while ((opt = getopt(argc, argv, "vo:")) != -1) {
switch (opt) {
case 'v': verbose = 1; break;
case 'o': output_file = optarg; break;
default:
fprintf(stderr, "Usage: %s [-v] [-o file]\n", argv[0]);
return EXIT_FAILURE;
}
}
// Handles: -vo file, -v -o file, missing arg errors
}Alternative (getopt_long for GNU-style long options):
static struct option long_options[] = {
{"verbose", no_argument, NULL, 'v'},
{"output", required_argument, NULL, 'o'},
{NULL, 0, NULL, 0}
};
while ((opt = getopt_long(argc, argv, "vo:", long_options, NULL)) != -1) {
// Same switch as above, now handles --verbose and --output=file
}Use Standard Flag Names
Use conventional flag names that users already know. Consistency across tools reduces cognitive load and prevents errors.
Incorrect (non-standard flag names):
static struct option opts[] = {
{"silent", no_argument, NULL, 's'}, // Should be --quiet
{"outfile", required_argument, NULL, 'O'}, // Should be --output
{"test-mode", no_argument, NULL, 't'}, // Should be --dry-run
{"info", no_argument, NULL, 'I'}, // Conflicts with --interactive
{NULL, 0, NULL, 0}
};Correct (uses established conventions):
static struct option opts[] = {
// Standard names users expect
{"verbose", no_argument, NULL, 'v'},
{"quiet", no_argument, NULL, 'q'},
{"debug", no_argument, NULL, 'd'},
{"force", no_argument, NULL, 'f'},
{"recursive", no_argument, NULL, 'r'},
{"output", required_argument, NULL, 'o'},
{"dry-run", no_argument, NULL, 'n'},
{"help", no_argument, NULL, 'h'},
{"version", no_argument, NULL, 'V'},
{NULL, 0, NULL, 0}
};Standard flag conventions:
| Flag | Long Form | Meaning |
|---|---|---|
-v | --verbose | Increase output verbosity |
-q | --quiet | Suppress non-error output |
-d | --debug | Enable debug mode |
-f | --force | Force operation without confirmation |
-r | --recursive | Operate recursively |
-o | --output | Specify output file |
-n | --dry-run | Show what would happen without doing it |
-i | --interactive | Prompt before actions |
-h | --help | Show help text |
-V | --version | Show version |
Reference: Command Line Interface Guidelines
Use Consistent Environment Variable Naming
Prefix environment variables with your program name and use SCREAMING_SNAKE_CASE. This prevents conflicts and makes variables discoverable.
Incorrect (inconsistent or generic names):
// Generic names conflict with other programs
const char *debug = getenv("DEBUG");
const char *port = getenv("PORT");
const char *timeout = getenv("TIMEOUT");
// Inconsistent casing
const char *level = getenv("MyTool_LogLevel");
const char *file = getenv("mytool-config");Correct (prefixed, consistent naming):
// All variables prefixed with MYTOOL_
const char *debug = getenv("MYTOOL_DEBUG");
const char *port = getenv("MYTOOL_PORT");
const char *timeout = getenv("MYTOOL_TIMEOUT");
const char *log_level = getenv("MYTOOL_LOG_LEVEL");
const char *config = getenv("MYTOOL_CONFIG");
// Document supported variables
void print_env_help(void) {
printf("Environment variables:\n");
printf(" MYTOOL_DEBUG Enable debug output (1 or 0)\n");
printf(" MYTOOL_PORT Server port (default: 8080)\n");
printf(" MYTOOL_TIMEOUT Timeout in seconds (default: 30)\n");
printf(" MYTOOL_LOG_LEVEL Log level: debug, info, warn, error\n");
printf(" MYTOOL_CONFIG Config file path\n");
}# Clear which program these configure
$ export MYTOOL_PORT=9000
$ export MYTOOL_DEBUG=1
$ export MYTOOL_LOG_LEVEL=debug
$ mytool serveNaming conventions:
- Prefix:
APPNAME_ - Format:
SCREAMING_SNAKE_CASE - Only uppercase letters, digits, underscores
- No hyphens (not valid in all shells)
Reference: POSIX Environment Variables
Follow XDG Base Directory Specification
Store configuration in XDG directories (~/.config/appname/) instead of dotfiles in home. This keeps home directories clean and enables per-user and system-wide configurations.
Incorrect (dotfile in home directory):
const char *get_config_path(void) {
static char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/.mytoolrc", getenv("HOME"));
return path;
}$ ls -la ~
.bashrc
.gitconfig
.mytoolrc # Yet another dotfile
.anothertool
.toolconfig
# Home directory cluttered with dotfilesCorrect (XDG-compliant paths):
#include <stdlib.h>
const char *get_config_dir(void) {
const char *xdg = getenv("XDG_CONFIG_HOME");
if (xdg && xdg[0]) {
return xdg;
}
static char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/.config", getenv("HOME"));
return path;
}
const char *get_config_path(void) {
static char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/mytool/config.yaml",
get_config_dir());
return path;
}
const char *get_data_dir(void) {
const char *xdg = getenv("XDG_DATA_HOME");
if (xdg && xdg[0]) {
return xdg;
}
static char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/.local/share", getenv("HOME"));
return path;
}$ ls ~/.config/mytool/
config.yaml
$ ls ~/.local/share/mytool/
cache.db
historyXDG directories:
| Purpose | Environment Variable | Default |
|---|---|---|
| Config | XDG_CONFIG_HOME | ~/.config |
| Data | XDG_DATA_HOME | ~/.local/share |
| Cache | XDG_CACHE_HOME | ~/.cache |
| State | XDG_STATE_HOME | ~/.local/state |
Reference: XDG Base Directory Specification
Never Store Secrets in Config Files or Environment
Config files get committed to version control. Environment variables appear in process listings and logs. Use dedicated secret storage mechanisms.
Incorrect (secrets in config or environment):
# config.yaml - might get committed to git
database:
host: db.example.com
password: SuperSecret123 # Exposed in version control!
api:
key: sk-abc123xyz # API key in plain text# Secrets visible in process listing
$ ps aux | grep mytool
user 1234 mytool --api-key=sk-abc123Correct (use secure secret sources):
#include <stdlib.h>
// Read from file with restricted permissions
char *read_secret_file(const char *path) {
struct stat st;
if (stat(path, &st) < 0) return NULL;
// Warn if file permissions are too open
if (st.st_mode & (S_IRGRP | S_IROTH)) {
fprintf(stderr, "Warning: %s is readable by others\n", path);
}
FILE *f = fopen(path, "r");
// ... read secret
}
// Get secret from multiple sources
char *get_api_key(void) {
// 1. File reference in config
const char *file = config_get("api.key_file");
if (file) return read_secret_file(file);
// 2. Named pipe or socket
const char *pipe = getenv("MYTOOL_API_KEY_PIPE");
if (pipe) return read_from_pipe(pipe);
// 3. System keyring (macOS Keychain, GNOME Keyring, etc.)
char *secret = keyring_get("mytool", "api_key");
if (secret) return secret;
fprintf(stderr, "No API key configured\n");
return NULL;
}# config.yaml - references file, not value
database:
host: db.example.com
password_file: /run/secrets/db_password # Docker secret or similar
api:
key_file: ~/.config/mytool/api_key # Mode 0600Secure secret sources:
- Dedicated files with
chmod 600 - OS keychain/credential manager
- Docker/Kubernetes secrets
- Environment files not in version control (
.envin.gitignore)
Reference: The Twelve-Factor App - Config
Apply Configuration in Correct Precedence Order
Apply configuration from multiple sources in a consistent, predictable order. Command-line flags should override everything else.
Incorrect (unpredictable precedence):
// Loads config randomly, flags might not override
void load_config(void) {
apply_flags(); // First?
load_user_config(); // Could override flags?
apply_env_vars(); // Unclear precedence
load_system_config(); // Final say?
}Correct (well-defined precedence, highest to lowest):
typedef struct {
int verbose;
int port;
char *output;
} Config;
Config load_config(int argc, char *argv[]) {
Config config = {0};
// 1. Built-in defaults (lowest priority)
config.verbose = 0;
config.port = 8080;
config.output = "stdout";
// 2. System config (/etc/mytool/config)
load_config_file(&config, "/etc/mytool/config");
// 3. User config (~/.config/mytool/config)
char user_config[PATH_MAX];
snprintf(user_config, sizeof(user_config),
"%s/.config/mytool/config", getenv("HOME"));
load_config_file(&config, user_config);
// 4. Project config (./.mytool.yaml)
load_config_file(&config, ".mytool.yaml");
// 5. Environment variables
const char *env_port = getenv("MYTOOL_PORT");
if (env_port) config.port = atoi(env_port);
// 6. Command-line flags (highest priority)
parse_args(&config, argc, argv);
return config;
}Precedence order (highest to lowest):
| Priority | Source | Example |
|---|---|---|
| 1 | Command-line flags | --port=9000 |
| 2 | Environment variables | MYTOOL_PORT=9000 |
| 3 | Project config | ./.mytool.yaml |
| 4 | User config | ~/.config/mytool/config |
| 5 | System config | /etc/mytool/config |
| 6 | Built-in defaults | Hardcoded values |
Reference: The Twelve-Factor App - Config
Respect Standard Environment Variables
Honor standard environment variables like NO_COLOR, EDITOR, PAGER, and proxy settings. This ensures your tool integrates well with the user's environment.
Incorrect (ignores standard variables):
void open_editor(const char *file) {
// Hardcoded editor
execlp("vim", "vim", file, NULL);
}
void show_paged_output(const char *text) {
// Hardcoded pager
FILE *p = popen("less", "w");
fputs(text, p);
pclose(p);
}
void make_request(const char *url) {
// Ignores proxy settings
curl_easy_setopt(curl, CURLOPT_URL, url);
}Correct (respects standard variables):
void open_editor(const char *file) {
const char *editor = getenv("VISUAL");
if (!editor) editor = getenv("EDITOR");
if (!editor) editor = "vi"; // POSIX default
execlp(editor, editor, file, NULL);
}
void show_paged_output(const char *text) {
const char *pager = getenv("PAGER");
if (!pager) pager = "less";
// Don't page if output is not a terminal
if (!isatty(STDOUT_FILENO)) {
puts(text);
return;
}
FILE *p = popen(pager, "w");
fputs(text, p);
pclose(p);
}
void configure_proxy(CURL *curl) {
const char *http_proxy = getenv("HTTP_PROXY");
if (!http_proxy) http_proxy = getenv("http_proxy");
if (http_proxy) {
curl_easy_setopt(curl, CURLOPT_PROXY, http_proxy);
}
}Standard environment variables:
| Variable | Purpose |
|---|---|
EDITOR, VISUAL | Text editor preference |
PAGER | Output pager (less, more) |
SHELL | User's preferred shell |
TERM | Terminal type |
HOME | User's home directory |
TMPDIR | Temporary file directory |
TZ | Timezone |
LANG, LC_* | Locale settings |
NO_COLOR | Disable colored output |
HTTP_PROXY, HTTPS_PROXY | Proxy settings |
NO_PROXY | Proxy exclusions |
Reference: POSIX Environment Variables
Make Error Messages Actionable
Error messages should tell users what went wrong AND how to fix it. A good error message saves users from searching documentation or Stack Overflow.
Incorrect (describes problem without solution):
if (port < 1 || port > 65535) {
fprintf(stderr, "Invalid port number\n");
return 1;
}
if (!config_file_exists) {
fprintf(stderr, "Configuration error\n");
return 1;
}$ mytool --port 70000
Invalid port number
# User must guess valid range
$ mytool
Configuration error
# User doesn't know what config or whereCorrect (explains problem and suggests fix):
if (port < 1 || port > 65535) {
fprintf(stderr, "%s: port %d is out of range (must be 1-65535)\n",
program_name, port);
return 1;
}
if (!config_file_exists) {
fprintf(stderr, "%s: config file not found at %s\n",
program_name, config_path);
fprintf(stderr, " Create one with: %s --init\n", program_name);
fprintf(stderr, " Or specify location: %s --config PATH\n", program_name);
return 1;
}$ mytool --port 70000
mytool: port 70000 is out of range (must be 1-65535)
$ mytool
mytool: config file not found at ~/.config/mytool/config.yaml
Create one with: mytool --init
Or specify location: mytool --config PATHActionable error components: 1. What went wrong (specific value/condition) 2. Why it's wrong (constraint violated) 3. How to fix it (concrete next step)
Reference: Command Line Interface Guidelines
Avoid Stack Traces in User-Facing Errors
Stack traces are useful for developers but overwhelming for users. Show clean error messages by default; reserve stack traces for debug mode.
Incorrect (dumps internal details to users):
# Python script that vomits internals
if __name__ == "__main__":
process_file(sys.argv[1])$ mytool missing.txt
Traceback (most recent call last):
File "/usr/local/bin/mytool", line 45, in <module>
process_file(sys.argv[1])
File "/usr/local/lib/mytool/processor.py", line 123, in process_file
with open(filename) as f:
FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt'
# User sees internals they don't understandCorrect (clean errors, debug mode for details):
import sys
import traceback
def main():
try:
process_file(sys.argv[1])
except FileNotFoundError as e:
print(f"mytool: {e.filename}: {e.strerror}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"mytool: internal error: {e}", file=sys.stderr)
if os.getenv("DEBUG"):
traceback.print_exc()
else:
print(" Run with DEBUG=1 for details", file=sys.stderr)
sys.exit(70) # EX_SOFTWARE# Clean error for users
$ mytool missing.txt
mytool: missing.txt: No such file or directory
# Developers can get details when needed
$ DEBUG=1 mytool corrupt.dat
mytool: internal error: invalid header checksum
Traceback (most recent call last):
...When to show technical details:
--debugor-dflag is setDEBUGenvironment variable is set- Error is clearly a bug (internal assertion, unexpected exception)
Reference: Command Line Interface Guidelines
Include Program Name in Error Messages
Prefix error messages with the program name. When multiple tools run in a pipeline or script, this identifies which tool produced the error.
Incorrect (no program name, ambiguous source):
int main(int argc, char *argv[]) {
FILE *f = fopen(argv[1], "r");
if (!f) {
fprintf(stderr, "Cannot open file\n"); // Which program?
return 1;
}
}# In a pipeline, unclear which tool failed
$ producer | transformer | consumer
Cannot open file
# Which of the three tools failed?Correct (program name identifies error source):
const char *program_name;
int main(int argc, char *argv[]) {
program_name = argv[0];
// Or use basename for cleaner output:
// program_name = basename(argv[0]);
FILE *f = fopen(argv[1], "r");
if (!f) {
fprintf(stderr, "%s: %s: %s\n",
program_name, argv[1], strerror(errno));
return 1;
}
}# Error source is immediately clear
$ producer | transformer | consumer
transformer: input.dat: No such file or directory
# Standard format: progname: context: message
$ mytool config.yaml
mytool: config.yaml: Permission deniedGNU standard error format:
progname: filename:line: message
progname: filename: message
progname: messageReference: GNU Coding Standards - Errors
Use strerror for System Errors
When system calls fail, use strerror(errno) or perror() to provide the standard system error message. These messages are consistent, localized, and familiar to users.
Incorrect (custom messages for system errors):
int fd = open(filename, O_RDONLY);
if (fd < 0) {
fprintf(stderr, "File could not be opened\n"); // Vague
return 1;
}
if (mkdir(path, 0755) < 0) {
fprintf(stderr, "mkdir failed\n"); // Missing why
return 1;
}$ mytool /etc/shadow
File could not be opened
# Was it permissions? Not found? Disk error?Correct (uses system error messages):
#include <errno.h>
#include <string.h>
int fd = open(filename, O_RDONLY);
if (fd < 0) {
fprintf(stderr, "%s: %s: %s\n",
program_name, filename, strerror(errno));
return 1;
}
// Or use perror for simpler cases:
if (mkdir(path, 0755) < 0) {
perror(path); // Prints: path: Permission denied
return 1;
}$ mytool /etc/shadow
mytool: /etc/shadow: Permission denied
$ mytool /nonexistent
mytool: /nonexistent: No such file or directory
$ mytool /dev/full
mytool: /dev/full: No space left on deviceNote: Save errno immediately after the failing call if you need to do other work before printing the error.
int fd = open(filename, O_RDONLY);
int saved_errno = errno; // Save before other calls
log_attempt(filename); // This might change errno
if (fd < 0) {
fprintf(stderr, "%s: %s\n", filename, strerror(saved_errno));
}Reference: strerror(3) - Linux manual page
Validate Input Early and Fail Fast
Validate all inputs before starting work. Failing halfway through an operation can leave data in an inconsistent state and waste user time.
Incorrect (validates during operation):
int main(int argc, char *argv[]) {
// Starts processing immediately
for (int i = 1; i < argc; i++) {
FILE *f = fopen(argv[i], "r");
if (!f) {
fprintf(stderr, "%s: cannot open\n", argv[i]);
continue; // Already processed some files!
}
process(f);
fclose(f);
}
write_output(); // Partial output written
}# Partial output exists, hard to recover
$ mytool file1.txt file2.txt missing.txt file4.txt
Processing file1.txt... done
Processing file2.txt... done
missing.txt: cannot open
Processing file4.txt... done
# Output contains 3 of 4 files - inconsistent stateCorrect (validates all inputs first):
int main(int argc, char *argv[]) {
// Phase 1: Validate all inputs
FILE **files = malloc((argc - 1) * sizeof(FILE *));
for (int i = 1; i < argc; i++) {
files[i - 1] = fopen(argv[i], "r");
if (!files[i - 1]) {
fprintf(stderr, "%s: %s: %s\n",
program_name, argv[i], strerror(errno));
// Clean up already-opened files
for (int j = 0; j < i - 1; j++) fclose(files[j]);
free(files);
return EXIT_FAILURE; // Fail before any processing
}
}
// Phase 2: Process (all inputs validated)
for (int i = 0; i < argc - 1; i++) {
process(files[i]);
fclose(files[i]);
}
free(files);
write_output();
return EXIT_SUCCESS;
}# All-or-nothing: either all succeed or none processed
$ mytool file1.txt file2.txt missing.txt file4.txt
mytool: missing.txt: No such file or directory
# No partial output, no inconsistent stateValidation checklist:
- Required arguments present
- Files exist and are readable/writable
- Network services reachable
- Resource limits sufficient
Reference: Crash-Only Software
Distinguish Error Types with Different Exit Codes
Use different exit codes for different error categories so scripts can respond appropriately. A config error should be distinguishable from a network error.
Incorrect (same exit code for all errors):
int main(int argc, char *argv[]) {
if (!load_config()) {
fprintf(stderr, "Config error\n");
return 1; // Same code for all failures
}
if (!connect()) {
fprintf(stderr, "Network error\n");
return 1; // Can't distinguish from config error
}
if (!process()) {
fprintf(stderr, "Processing error\n");
return 1; // All errors look the same
}
}Correct (distinct codes for different error categories):
#include <sysexits.h>
// Document exit codes in --help and man page
enum {
EXIT_OK = 0,
EXIT_USAGE = EX_USAGE, // 64: bad arguments
EXIT_CONFIG = EX_CONFIG, // 78: config error
EXIT_NETWORK = EX_UNAVAILABLE, // 69: network unavailable
EXIT_DATA = EX_DATAERR, // 65: input data error
EXIT_INTERNAL = EX_SOFTWARE // 70: internal error
};
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s config\n", argv[0]);
return EXIT_USAGE;
}
if (!load_config(argv[1])) {
fprintf(stderr, "Invalid config: %s\n", argv[1]);
return EXIT_CONFIG;
}
if (!connect()) {
fprintf(stderr, "Cannot connect to server\n");
return EXIT_NETWORK;
}
return EXIT_OK;
}#!/bin/bash
mytool config.yaml
case $? in
0) echo "Success" ;;
64) echo "Bad arguments, check usage" ;;
69) echo "Network down, will retry" && sleep 60 && mytool config.yaml ;;
78) echo "Fix config file" ;;
*) echo "Unknown error" ;;
esacNote: Document your exit codes in --help output and man pages.
Reference: BSD sysexits
Handle Partial Success Consistently
When processing multiple inputs, return non-zero if any operation failed. Use a consistent strategy: either fail fast or process all and report aggregate status.
Incorrect (inconsistent partial success handling):
int main(int argc, char *argv[]) {
for (int i = 1; i < argc; i++) {
if (!process_file(argv[i])) {
fprintf(stderr, "Failed: %s\n", argv[i]);
// Continues but forgets the failure
}
}
return 0; // Bug: returns success even if files failed
}$ mytool good.txt bad.txt other.txt
Failed: bad.txt
$ echo $?
0 # Caller thinks everything succeededCorrect (tracks and reports failures):
int main(int argc, char *argv[]) {
int failures = 0;
for (int i = 1; i < argc; i++) {
if (!process_file(argv[i])) {
fprintf(stderr, "%s: processing failed\n", argv[i]);
failures++;
}
}
if (failures > 0) {
fprintf(stderr, "%d file(s) failed\n", failures);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}$ mytool good.txt bad.txt other.txt
bad.txt: processing failed
1 file(s) failed
$ echo $?
1 # Caller knows something failedAlternative (fail-fast mode with --keep-going option):
if (!process_file(argv[i])) {
if (keep_going) {
failures++;
continue;
}
return EXIT_FAILURE; // Stop immediately
}Reference: GNU Make - Errors in Recipes
Use 128+N for Signal Termination
When a program terminates due to a signal, it should exit with code 128 plus the signal number. This convention allows parent processes to determine the exact cause of termination.
Incorrect (exits with arbitrary code on signal):
void handle_sigterm(int sig) {
cleanup();
exit(1); // Bug: parent can't distinguish from normal error
}
int main(void) {
signal(SIGTERM, handle_sigterm);
// ...
}$ mytool &
$ kill $!
$ echo $?
1 # Parent thinks it was a normal errorCorrect (exits with 128+signal number):
#include <signal.h>
volatile sig_atomic_t got_signal = 0;
void handle_sigterm(int sig) {
got_signal = sig;
}
int main(void) {
signal(SIGTERM, handle_sigterm);
signal(SIGINT, handle_sigterm);
while (!got_signal) {
do_work();
}
cleanup();
// Re-raise signal for correct exit code, or:
exit(128 + got_signal);
}$ mytool &
$ kill $! # Sends SIGTERM (15)
$ echo $?
143 # 128 + 15 = 143
$ mytool &
$ kill -INT $! # Sends SIGINT (2)
$ echo $?
130 # 128 + 2 = 130Common signal exit codes:
| Signal | Number | Exit Code |
|---|---|---|
| SIGHUP | 1 | 129 |
| SIGINT | 2 | 130 |
| SIGQUIT | 3 | 131 |
| SIGTERM | 15 | 143 |
| SIGKILL | 9 | 137 |
Reference: Bash Reference - Exit Status
Use Standard Exit Codes
Use the BSD sysexits.h conventions for exit codes. These provide semantic meaning that scripts can act upon. Avoid inventing custom codes that conflict with reserved values.
Incorrect (arbitrary non-standard codes):
#define ERR_FILE 100 // Conflicts with nothing but isn't standard
#define ERR_NETWORK 200 // Over 125, conflicts with shell conventions
#define ERR_PARSE 256 // Invalid: codes are 0-255
int main(int argc, char *argv[]) {
if (!open_file()) return ERR_FILE;
if (!connect()) return ERR_NETWORK; // Wraps to 200-256=?
}Correct (uses sysexits.h standard codes):
#include <sysexits.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s file\n", argv[0]);
return EX_USAGE; // 64: command line usage error
}
FILE *f = fopen(argv[1], "r");
if (!f) {
perror(argv[1]);
return EX_NOINPUT; // 66: cannot open input
}
if (!parse(f)) {
fprintf(stderr, "Parse error\n");
return EX_DATAERR; // 65: data format error
}
return EX_OK; // 0: successful termination
}Standard exit codes (sysexits.h):
| Code | Name | Meaning |
|---|---|---|
| 0 | EX_OK | Successful termination |
| 64 | EX_USAGE | Command line usage error |
| 65 | EX_DATAERR | Data format error |
| 66 | EX_NOINPUT | Cannot open input |
| 69 | EX_UNAVAILABLE | Service unavailable |
| 70 | EX_SOFTWARE | Internal software error |
| 73 | EX_CANTCREAT | Cannot create output file |
| 74 | EX_IOERR | Input/output error |
| 75 | EX_TEMPFAIL | Temporary failure; retry |
| 77 | EX_NOPERM | Permission denied |
Codes to avoid: 126 (command not executable), 127 (command not found), 128+ (signal termination)
Reference: BSD sysexits
Return Zero for Success Only
Exit code 0 means success and nothing else. Any non-zero exit code indicates failure. Scripts, CI/CD pipelines, and shell operators (&&, ||) depend on this convention.
Incorrect (returns 0 on failure):
int main(int argc, char *argv[]) {
FILE *f = fopen(argv[1], "r");
if (!f) {
fprintf(stderr, "Could not open file\n");
return 0; // Bug: returns success on failure!
}
process(f);
return 0;
}# Script continues despite failure
$ mytool missing.txt && process_output
Could not open file
# process_output runs because exit code was 0Correct (returns non-zero on any failure):
int main(int argc, char *argv[]) {
FILE *f = fopen(argv[1], "r");
if (!f) {
fprintf(stderr, "Could not open file\n");
return EXIT_FAILURE; // Non-zero signals error
}
process(f);
return EXIT_SUCCESS; // 0 only on success
}# Script stops on failure as expected
$ mytool missing.txt && process_output
Could not open file
# process_output does NOT run because exit code was 1Benefits:
set -ein scripts works correctly- CI/CD pipelines detect failures
&&and||operators work as expectedmakestops on tool failures
Reference: POSIX Exit Status
Include Practical Examples in Help
Include 2-4 real-world examples in your --help output. Examples teach faster than abstract descriptions.
Incorrect (no examples):
void print_help(void) {
printf("Usage: imgconv [OPTIONS] INPUT OUTPUT\n");
printf("Convert images between formats.\n\n");
printf("Options:\n");
printf(" -q, --quality=N output quality (1-100)\n");
printf(" -s, --scale=SCALE resize factor or dimensions\n");
printf(" -f, --format=FMT output format\n");
}Correct (practical examples included):
void print_help(void) {
printf("Usage: imgconv [OPTIONS] INPUT OUTPUT\n");
printf("Convert images between formats.\n\n");
printf("Options:\n");
printf(" -q, --quality=N output quality 1-100 (default: 85)\n");
printf(" -s, --scale=SCALE resize: 50%%, 0.5, or 800x600\n");
printf(" -f, --format=FMT output format: jpg, png, webp (default: auto)\n\n");
printf("Examples:\n");
printf(" imgconv photo.png photo.jpg\n");
printf(" Convert PNG to JPEG with default quality\n\n");
printf(" imgconv -q 95 raw.tiff final.jpg\n");
printf(" Convert TIFF to high-quality JPEG\n\n");
printf(" imgconv -s 50%% large.png thumbnail.png\n");
printf(" Create half-size thumbnail\n\n");
printf(" imgconv -s 1920x1080 photo.jpg wallpaper.jpg\n");
printf(" Resize to specific dimensions\n");
}$ imgconv --help
Usage: imgconv [OPTIONS] INPUT OUTPUT
Convert images between formats.
Options:
-q, --quality=N output quality 1-100 (default: 85)
-s, --scale=SCALE resize: 50%, 0.5, or 800x600
-f, --format=FMT output format: jpg, png, webp (default: auto)
Examples:
imgconv photo.png photo.jpg
Convert PNG to JPEG with default quality
imgconv -q 95 raw.tiff final.jpg
Convert TIFF to high-quality JPEG
imgconv -s 50% large.png thumbnail.png
Create half-size thumbnailExample selection criteria:
- Most common use case first
- Show different option combinations
- Progress from simple to complex
Reference: Command Line Interface Guidelines
Show Default Values in Help
Display default values for all options that have them. Users should know what happens without specifying each option.
Incorrect (defaults not shown):
printf("Options:\n");
printf(" -t, --timeout=SEC connection timeout\n");
printf(" -r, --retries=N number of retries\n");
printf(" -p, --port=PORT server port\n");$ mytool --help
Options:
-t, --timeout=SEC connection timeout
-r, --retries=N number of retries
-p, --port=PORT server port
# What are the defaults? User must guess or read sourceCorrect (defaults clearly shown):
printf("Options:\n");
printf(" -t, --timeout=SEC connection timeout in seconds (default: 30)\n");
printf(" -r, --retries=N number of retries on failure (default: 3)\n");
printf(" -p, --port=PORT server port (default: 8080)\n");
printf(" -l, --log=LEVEL log level: debug, info, warn, error (default: info)\n");
printf(" -c, --config=FILE config file (default: ~/.config/mytool/config.yaml)\n");$ mytool --help
Options:
-t, --timeout=SEC connection timeout in seconds (default: 30)
-r, --retries=N number of retries on failure (default: 3)
-p, --port=PORT server port (default: 8080)
-l, --log=LEVEL log level: debug, info, warn, error (default: info)
-c, --config=FILE config file (default: ~/.config/mytool/config.yaml)Formatting conventions:
(default: VALUE)at end of description- List valid options for enum-like values
- Show both the value and its meaning when not obvious
Reference: Command Line Interface Guidelines
Show Brief Usage on Argument Errors
When arguments are missing or invalid, show a brief usage hint along with the error. Don't force users to run --help separately.
Incorrect (error without guidance):
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Error: missing file argument\n");
return 1;
}
}$ mytool
Error: missing file argument
# User must now run mytool --help to learn syntaxCorrect (error includes usage hint):
void print_usage_hint(FILE *stream, const char *progname) {
fprintf(stream, "Usage: %s [OPTIONS] FILE...\n", progname);
fprintf(stream, "Try '%s --help' for more information.\n", progname);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "%s: missing file operand\n", argv[0]);
print_usage_hint(stderr, argv[0]);
return EX_USAGE;
}
// Unknown option
if (unknown_opt) {
fprintf(stderr, "%s: unrecognized option '%s'\n", argv[0], opt);
print_usage_hint(stderr, argv[0]);
return EX_USAGE;
}
}$ mytool
mytool: missing file operand
Usage: mytool [OPTIONS] FILE...
Try 'mytool --help' for more information.
$ mytool --badopt
mytool: unrecognized option '--badopt'
Usage: mytool [OPTIONS] FILE...
Try 'mytool --help' for more information.Note: Brief usage goes to stderr (since it's an error condition). Full --help output goes to stdout.
Reference: GNU Coding Standards - User Interfaces
Structure Help Output Consistently
Follow the standard help output structure: synopsis, description, options (with defaults), examples, and where to report bugs.
Incorrect (unstructured help):
void print_help(void) {
printf("mytool - does stuff with files\n");
printf("-v makes it verbose\n");
printf("-o is for output\n");
printf("You can also use -f\n");
}Correct (well-structured help):
void print_help(const char *progname) {
printf("Usage: %s [OPTIONS] FILE...\n", progname);
printf("Process files and output results.\n\n");
printf("Options:\n");
printf(" -f, --format=FMT output format: text, json, csv (default: text)\n");
printf(" -o, --output=FILE write output to FILE (default: stdout)\n");
printf(" -r, --recursive process directories recursively\n");
printf(" -v, --verbose increase verbosity (can be repeated)\n");
printf(" -q, --quiet suppress non-error output\n");
printf(" -n, --dry-run show what would be done without doing it\n");
printf(" -h, --help display this help and exit\n");
printf(" --version output version information and exit\n\n");
printf("Examples:\n");
printf(" %s file.txt Process single file\n", progname);
printf(" %s -r -o out.json dir/ Process directory recursively\n", progname);
printf(" %s -v --format=csv *.log Process logs with verbose CSV output\n\n", progname);
printf("Report bugs to: https://github.com/example/mytool/issues\n");
}$ mytool --help
Usage: mytool [OPTIONS] FILE...
Process files and output results.
Options:
-f, --format=FMT output format: text, json, csv (default: text)
-o, --output=FILE write output to FILE (default: stdout)
-r, --recursive process directories recursively
-v, --verbose increase verbosity (can be repeated)
-q, --quiet suppress non-error output
-n, --dry-run show what would be done without doing it
-h, --help display this help and exit
--version output version information and exit
Examples:
mytool file.txt Process single file
mytool -r -o out.json dir/ Process directory recursively
mytool -v --format=csv *.log Process logs with verbose CSV output
Report bugs to: https://github.com/example/mytool/issuesReference: GNU Coding Standards - --help
Format Version Output Correctly
The --version output should include the program name, version number, copyright, and license. Follow the GNU format for consistency.
Incorrect (incomplete version info):
void print_version(void) {
printf("v1.2.3\n"); // Missing program name, license
}$ mytool --version
v1.2.3
# Can't tell which program this is from output aloneCorrect (full version information):
void print_version(void) {
printf("mytool 1.2.3\n");
printf("Copyright (C) 2024 Example Corporation\n");
printf("License MIT: <https://opensource.org/licenses/MIT>\n");
printf("This is free software: you are free to change and redistribute it.\n");
printf("There is NO WARRANTY, to the extent permitted by law.\n\n");
printf("Written by Jane Developer.\n");
}$ mytool --version
mytool 1.2.3
Copyright (C) 2024 Example Corporation
License MIT: <https://opensource.org/licenses/MIT>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Written by Jane Developer.Version string format:
- First line:
PROGRAM VERSION - No "v" prefix on version (just
1.2.3, notv1.2.3) - Semantic versioning recommended
- Exit with status 0 after printing
For automated parsing:
$ mytool --version | head -1 | awk '{print $2}'
1.2.3Reference: GNU Coding Standards - --version
Use Atomic File Writes
Write to a temporary file and rename it to the final destination. This prevents partial writes from corrupting data if the program crashes or is interrupted.
Incorrect (direct write can corrupt):
int main(int argc, char *argv[]) {
FILE *f = fopen(argv[1], "w");
// If crash/interrupt happens here, file is empty/partial
write_data(f);
fclose(f);
}# Ctrl-C during write corrupts the file
$ mytool important.conf
^C
$ cat important.conf
# Partial content, file corruptedCorrect (write to temp, then rename):
#include <stdio.h>
#include <unistd.h>
int write_file_atomic(const char *path, const char *content) {
char temp_path[PATH_MAX];
snprintf(temp_path, sizeof(temp_path), "%s.XXXXXX", path);
// Create temp file in same directory (for same filesystem)
int fd = mkstemp(temp_path);
if (fd < 0) return -1;
FILE *f = fdopen(fd, "w");
if (!f) {
close(fd);
unlink(temp_path);
return -1;
}
// Write to temp file
if (fputs(content, f) == EOF) {
fclose(f);
unlink(temp_path);
return -1;
}
// Ensure data is flushed to disk
fflush(f);
fsync(fileno(f));
fclose(f);
// Atomic rename
if (rename(temp_path, path) < 0) {
unlink(temp_path);
return -1;
}
return 0;
}# Interrupt can't corrupt the file
$ mytool important.conf
^C
$ cat important.conf
# Original content preserved (or new content complete)Requirements for atomicity:
- Temp file must be on same filesystem as target
- Use
fsync()before rename for crash safety - Clean up temp file on failure
Reference: Ensuring data reaches disk
Design Stateless Operations
Prefer stateless operations that don't depend on hidden state. Given the same inputs, the tool should produce the same outputs. This enables reliable automation and easy recovery from failures.
Incorrect (depends on hidden state):
static int run_count = 0; // Persisted somewhere
int main(int argc, char *argv[]) {
load_state(&run_count);
run_count++;
if (run_count > 3) {
fprintf(stderr, "Trial expired\n");
return 1;
}
process(argv[1]);
save_state(run_count);
}# Behavior depends on invisible state
$ mytool data.txt # Works
$ mytool data.txt # Works
$ mytool data.txt # Works
$ mytool data.txt # Fails! But why?
Trial expiredCorrect (stateless, deterministic):
int main(int argc, char *argv[]) {
// All state comes from explicit inputs
Config config = parse_config(argc, argv);
// Same inputs always produce same outputs
Result result = process(config.input_file, config.options);
output_result(result, config.output_file);
return result.success ? 0 : 1;
}# Deterministic behavior
$ mytool data.txt # Same result every time
$ mytool data.txt # Same result every time
$ mytool --format=csv data.txt # Different options, predictable result
# Easy to automate and retry
$ mytool data.txt || mytool data.txt # Retry makes senseWhen state is necessary:
- Make it explicit with config files or database
- Document the state location
- Provide
--resetor--cleanoptions - Consider
--dry-runto preview without modifying state
Reference: The Twelve-Factor App - Processes
Handle Binary Data Safely
When processing binary data, use binary mode for file operations and avoid functions that assume text encoding. Text-mode I/O can corrupt binary data.
Incorrect (text mode corrupts binary):
int main(int argc, char *argv[]) {
// Text mode on Windows translates \r\n
FILE *in = fopen(argv[1], "r"); // Text mode!
FILE *out = fopen(argv[2], "w"); // Text mode!
int c;
while ((c = fgetc(in)) != EOF) {
fputc(c, out); // Binary corrupted on Windows
}
}# On Windows, binary file gets corrupted
$ mytool image.png copy.png
$ md5sum image.png copy.png
a1b2c3d4 image.png
e5f6g7h8 copy.png # Different! Corrupted.Correct (explicit binary mode):
int main(int argc, char *argv[]) {
// Binary mode preserves data exactly
FILE *in = fopen(argv[1], "rb"); // Binary read
FILE *out = fopen(argv[2], "wb"); // Binary write
if (!in || !out) {
perror(argv[0]);
return 1;
}
char buffer[8192];
size_t n;
while ((n = fread(buffer, 1, sizeof(buffer), in)) > 0) {
if (fwrite(buffer, 1, n, out) != n) {
perror("write");
return 1;
}
}
}# Binary files preserved correctly
$ mytool image.png copy.png
$ md5sum image.png copy.png
a1b2c3d4 image.png
a1b2c3d4 copy.png # IdenticalBinary I/O checklist:
- Use
"rb"and"wb"modes for file operations - Use
fread/fwriteinstead offgets/fputs - Set
_setmode(_fileno(stdin), _O_BINARY)on Windows for stdin/stdout - Avoid string functions on binary data
Reference: fopen(3) - Linux manual page
Handle Multiple Input Files Consistently
Accept multiple input files and process them as a stream, like cat, grep, and other standard tools. Support shell globbing patterns naturally.
Incorrect (processes only one file):
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s FILE\n", argv[0]);
return 1;
}
FILE *f = fopen(argv[1], "r");
process(f);
}# Can't process multiple files
$ mytool *.txt
Usage: mytool FILE
# User must loop manually
$ for f in *.txt; do mytool "$f"; doneCorrect (handles multiple files as stream):
int main(int argc, char *argv[]) {
int exit_status = 0;
if (argc < 2 || (argc == 2 && strcmp(argv[1], "-") == 0)) {
// No args or "-": read from stdin
process_stream(stdin, "(stdin)");
} else {
// Process each file in order
for (int i = 1; i < argc; i++) {
FILE *f;
const char *filename = argv[i];
if (strcmp(filename, "-") == 0) {
f = stdin;
filename = "(stdin)";
} else {
f = fopen(filename, "r");
if (!f) {
fprintf(stderr, "%s: %s: %s\n",
argv[0], filename, strerror(errno));
exit_status = 1;
continue; // Continue with other files
}
}
process_stream(f, filename);
if (f != stdin) fclose(f);
}
}
return exit_status;
}# Works with multiple files
$ mytool file1.txt file2.txt file3.txt
# Works with glob patterns
$ mytool *.txt
# Mix files and stdin
$ echo "extra data" | mytool file1.txt - file2.txt
# Returns error if any file failed
$ mytool good.txt missing.txt other.txt
mytool: missing.txt: No such file or directory
$ echo $?
1Reference: cat(1) - concatenate files
Support Reading from stdin
When no input file is specified, read from stdin. This enables your tool to work as a filter in pipelines, a core UNIX pattern.
Incorrect (requires file argument):
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s FILE\n", argv[0]);
return 1; // Can't use in pipeline
}
FILE *f = fopen(argv[1], "r");
process(f);
}# Cannot use in pipeline
$ cat data.txt | mytool
Usage: mytool FILE
# Must create temp file
$ cat data.txt > /tmp/data.txt && mytool /tmp/data.txtCorrect (reads stdin when no file given):
int main(int argc, char *argv[]) {
FILE *input;
if (argc < 2 || strcmp(argv[1], "-") == 0) {
input = stdin; // Read from stdin
} else {
input = fopen(argv[1], "r");
if (!input) {
fprintf(stderr, "%s: %s: %s\n",
argv[0], argv[1], strerror(errno));
return 1;
}
}
process(input);
if (input != stdin) {
fclose(input);
}
return 0;
}# Works as a filter
$ cat data.txt | mytool
$ curl -s https://example.com/data | mytool
# Explicit stdin with -
$ mytool - < data.txt
# Still works with file argument
$ mytool data.txtPattern: Treat - as explicit stdin, useful when other arguments are also files.
Reference: POSIX Utility Conventions - Guideline 13
Write Output to stdout by Default
Write output to stdout unless an output file is explicitly specified with -o/--output. Let users redirect output with standard shell operators.
Incorrect (writes directly to file):
int main(int argc, char *argv[]) {
// Always writes to output.txt
FILE *out = fopen("output.txt", "w");
process(stdin, out);
fclose(out);
}# Can't pipe or redirect output
$ mytool < input.txt | wc -l
# Output went to output.txt, not the pipe
# Must specify output location
$ mytool < input.txt
$ cat output.txt | wc -lCorrect (writes to stdout, file optional):
int main(int argc, char *argv[]) {
FILE *output = stdout; // Default to stdout
char *output_file = NULL;
int opt;
while ((opt = getopt(argc, argv, "o:")) != -1) {
switch (opt) {
case 'o': output_file = optarg; break;
}
}
if (output_file) {
output = fopen(output_file, "w");
if (!output) {
fprintf(stderr, "%s: %s: %s\n",
argv[0], output_file, strerror(errno));
return 1;
}
}
process(stdin, output);
if (output != stdout) {
fclose(output);
}
return 0;
}# Pipe output
$ mytool < input.txt | sort | head
# Redirect output
$ mytool < input.txt > output.txt
# Explicit output file
$ mytool -o output.txt < input.txt
# View and save simultaneously
$ mytool < input.txt | tee output.txt | lessNote: Use -o for output file to follow GNU conventions. Positional output arguments are confusing.
Reference: GNU Coding Standards - Output
Detect TTY for Human-Oriented Output
Check if stdout is a terminal before adding colors, progress bars, or interactive formatting. When output is piped or redirected, use plain machine-readable output.
Incorrect (always uses fancy formatting):
void print_status(const char *msg) {
printf("\033[1;32m✓\033[0m %s\n", msg); // Always uses colors/emoji
}
int main(void) {
print_status("File processed");
}# Colors and escape codes corrupt piped output
$ mytool | head
^[[1;32m✓^[[0m File processed
# Escape codes visible as garbageCorrect (adapts output to terminal type):
#include <unistd.h>
int use_color = 0;
void init_output(void) {
use_color = isatty(STDOUT_FILENO);
}
void print_status(const char *msg) {
if (use_color) {
printf("\033[1;32m✓\033[0m %s\n", msg);
} else {
printf("OK: %s\n", msg);
}
}
int main(void) {
init_output();
print_status("File processed");
}# Terminal gets nice formatting
$ mytool
✓ File processed
# Pipe gets clean output
$ mytool | cat
OK: File processedAdditional checks:
NO_COLORenvironment variable should disable colorsTERM=dumbshould disable colors- Provide
--no-colorand--color=alwaysflags for overrides
Reference: NO_COLOR Standard
Use Line-Based Output for Text Streams
Default output should be one record per line, terminated by newlines. This enables processing with standard tools like grep, awk, sort, and head.
Incorrect (multi-line records, unclear boundaries):
void print_record(Record *r) {
printf("Name: %s\n", r->name);
printf("Email: %s\n", r->email);
printf("Phone: %s\n", r->phone);
printf("\n"); // Blank line separator
}# Hard to process with standard tools
$ mytool list | grep "John"
Name: John Doe # Only gets partial record
$ mytool list | head -1
Name: Alice Smith # Not a complete recordCorrect (one record per line):
void print_record(Record *r) {
printf("%s\t%s\t%s\n", r->name, r->email, r->phone);
}
void print_record_verbose(Record *r) {
// Verbose mode can use multi-line
printf("Name: %s\n", r->name);
printf("Email: %s\n", r->email);
printf("Phone: %s\n", r->phone);
printf("\n");
}# Easy to process with standard tools
$ mytool list | grep "John"
John Doe john@example.com 555-1234
$ mytool list | head -3
Alice Smith alice@example.com 555-0001
Bob Jones bob@example.com 555-0002
Carol White carol@example.com 555-0003
$ mytool list | cut -f2 | sort -u # Extract unique emails
$ mytool list | wc -l # Count recordsConventions:
- Use tab (
\t) or colon (:) as field delimiter - Handle fields containing delimiter by quoting or escaping
- For complex data, provide
--jsonoption
Reference: The Art of Unix Programming - Textuality
Provide Machine-Readable Output Format
Offer a --json, --porcelain, or similar flag for structured, machine-parseable output. Human-readable output changes; machine output is a stable API.
Incorrect (only human-readable output):
void list_files(void) {
printf("Found 3 files:\n");
printf(" - document.pdf (2.5 MB, modified yesterday)\n");
printf(" - image.png (156 KB, modified 2 hours ago)\n");
printf(" - notes.txt (1.2 KB, modified just now)\n");
}# Scripts must parse fragile human-readable format
$ mytool list | grep -oP '\d+\.\d+ [KMG]B'
2.5 MB
156 KB
1.2 KB
# Breaks when format changesCorrect (provides structured output option):
void list_files(int json_output) {
if (json_output) {
printf("[\n");
printf(" {\"name\": \"document.pdf\", \"size\": 2621440, \"mtime\": 1706140800},\n");
printf(" {\"name\": \"image.png\", \"size\": 159744, \"mtime\": 1706220000},\n");
printf(" {\"name\": \"notes.txt\", \"size\": 1229, \"mtime\": 1706227200}\n");
printf("]\n");
} else {
printf("Found 3 files:\n");
printf(" - document.pdf (2.5 MB, modified yesterday)\n");
// ... human-readable format
}
}# Scripts use reliable JSON
$ mytool list --json | jq '.[].name'
"document.pdf"
"image.png"
"notes.txt"
# Format is stable, won't break scripts
$ mytool list --json | jq 'map(select(.size > 1000000))'Machine format conventions:
--jsonfor JSON output--porcelainfor stable line-based format (git convention)--format=for user-specified templates
Reference: Command Line Interface Guidelines
Respect NO_COLOR Environment Variable
When NO_COLOR environment variable is set (to any value), disable all color output. This is a cross-tool standard for accessibility and compatibility.
Incorrect (ignores NO_COLOR):
void print_error(const char *msg) {
printf("\033[1;31mError:\033[0m %s\n", msg); // Always red
}
void print_success(const char *msg) {
printf("\033[1;32mSuccess:\033[0m %s\n", msg); // Always green
}# User with color blindness or screen reader
$ export NO_COLOR=1
$ mytool process data.txt
Error: invalid data # Still shows escape codesCorrect (checks NO_COLOR and other indicators):
#include <stdlib.h>
#include <unistd.h>
int should_use_color(void) {
// NO_COLOR takes precedence
if (getenv("NO_COLOR") != NULL) return 0;
// Check for dumb terminal
const char *term = getenv("TERM");
if (term && strcmp(term, "dumb") == 0) return 0;
// Check if stdout is a terminal
if (!isatty(STDOUT_FILENO)) return 0;
// Check for forced color
if (getenv("FORCE_COLOR") != NULL) return 1;
return 1;
}
void print_error(const char *msg) {
if (should_use_color()) {
fprintf(stderr, "\033[1;31mError:\033[0m %s\n", msg);
} else {
fprintf(stderr, "Error: %s\n", msg);
}
}# Color disabled correctly
$ export NO_COLOR=1
$ mytool process data.txt
Error: invalid data
# Force color in pipes if needed
$ FORCE_COLOR=1 mytool status | less -RColor control precedence (highest to lowest): 1. --color=always or --color=never flags 2. NO_COLOR or FORCE_COLOR environment variables 3. TERM=dumb check 4. isatty() check
Reference: NO_COLOR.org
Write Errors and Diagnostics to stderr
All error messages, warnings, progress indicators, and diagnostic information must go to stderr. This allows stdout to be cleanly piped or redirected.
Incorrect (errors go to stdout):
int main(int argc, char *argv[]) {
FILE *f = fopen(argv[1], "r");
if (!f) {
printf("Error: cannot open %s\n", argv[1]); // stdout!
return 1;
}
printf("Warning: file is large\n"); // stdout!
// ...
}# Error message ends up in the pipe
$ mytool missing.txt | grep pattern
Error: cannot open missing.txt
# grep receives the error message as input!Correct (all non-data goes to stderr):
int main(int argc, char *argv[]) {
FILE *f = fopen(argv[1], "r");
if (!f) {
fprintf(stderr, "Error: cannot open %s\n", argv[1]);
return 1;
}
fprintf(stderr, "Warning: file is large\n");
// ...
}# Error is visible, pipe is clean
$ mytool missing.txt | grep pattern
Error: cannot open missing.txt
# grep receives nothing (no stdout output)
$ echo $?
1 # Pipeline fails correctlyWhat goes to stderr:
- Error messages
- Warnings
- Progress bars and spinners
- Debug/verbose output
- Usage messages (when invoked incorrectly)
Reference: Standard Streams - Wikipedia
Write Data to stdout Only
Standard output is for program data that can be piped to other programs. Progress messages, warnings, and any human-oriented text must go to stderr. Mixing them breaks pipes.
Incorrect (messages mixed with data on stdout):
int main(int argc, char *argv[]) {
printf("Processing %s...\n", argv[1]); // Message on stdout
process_and_output(argv[1]); // Data on stdout
printf("Done!\n"); // Message on stdout
}# User tries to capture output, gets garbage
$ mytool data.csv > output.csv
$ head output.csv
Processing data.csv... # Garbage in output file!
id,name,value
1,foo,100
Done! # More garbage!Correct (only data goes to stdout):
int main(int argc, char *argv[]) {
fprintf(stderr, "Processing %s...\n", argv[1]); // Message to stderr
process_and_output(argv[1]); // Data to stdout
fprintf(stderr, "Done!\n"); // Message to stderr
}# Output is clean, messages visible in terminal
$ mytool data.csv > output.csv
Processing data.csv...
Done!
$ head output.csv
id,name,value
1,foo,100Rule of thumb: If the output is meant for another program, use stdout. If it's meant for a human watching the terminal, use stderr.
Reference: Command Line Interface Guidelines
Skip Cleanup on Second Interrupt
If cleanup is taking too long, a second Ctrl-C should exit immediately. Don't trap users in a slow cleanup process.
Incorrect (ignores second interrupt):
volatile sig_atomic_t interrupted = 0;
void handle_sigint(int sig) {
interrupted = 1;
}
void cleanup(void) {
fprintf(stderr, "Cleaning up...\n");
// Long cleanup operations
delete_temp_files(); // 5 seconds
flush_buffers(); // 3 seconds
close_connections(); // 2 seconds
// User presses Ctrl-C again but nothing happens
}Correct (second interrupt exits immediately):
volatile sig_atomic_t interrupt_count = 0;
void handle_sigint(int sig) {
interrupt_count++;
if (interrupt_count >= 2) {
fprintf(stderr, "\nForced exit\n");
_exit(130); // Exit immediately, skip all cleanup
}
fprintf(stderr, "\nInterrupted, cleaning up (Ctrl-C again to force quit)...\n");
}
void cleanup(void) {
// Check between each slow operation
if (interrupt_count < 2) {
delete_temp_files();
}
if (interrupt_count < 2) {
flush_buffers();
}
if (interrupt_count < 2) {
close_connections();
}
}
int main(void) {
signal(SIGINT, handle_sigint);
do_work();
if (interrupt_count > 0) {
cleanup();
}
return interrupt_count > 0 ? 130 : 0;
}$ mytool very_large_operation
^C
Interrupted, cleaning up (Ctrl-C again to force quit)...
Cleaning up... ^C
Forced exit
$ echo $?
130Pattern: Tell users about the escape hatch when cleanup starts.
Reference: Command Line Interface Guidelines - Signals
Handle SIGINT Gracefully
When the user presses Ctrl-C (SIGINT), exit promptly after minimal cleanup. Don't ignore the signal or take too long to exit.
Incorrect (ignores SIGINT or hangs):
void do_work(void) {
signal(SIGINT, SIG_IGN); // Ignores Ctrl-C!
while (1) {
slow_operation(); // User can't exit
}
}
// Or: handler that does too much
void handle_sigint(int sig) {
save_state(); // Slow
close_connections(); // Slow
write_logs(); // Slow
cleanup_temp_files(); // User waiting...
exit(1);
}Correct (exits promptly with signal acknowledgment):
#include <signal.h>
volatile sig_atomic_t interrupted = 0;
void handle_sigint(int sig) {
interrupted = 1; // Set flag only, exit from main loop
}
int main(void) {
struct sigaction sa = {
.sa_handler = handle_sigint,
.sa_flags = 0
};
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, NULL);
while (!interrupted) {
if (do_work_chunk() < 0) break;
// Check after each chunk
if (interrupted) {
fprintf(stderr, "\nInterrupted\n");
break;
}
}
// Quick cleanup only
quick_cleanup();
return interrupted ? 130 : 0; // 128 + SIGINT(2)
}$ mytool large_file.dat
Processing... ^C
Interrupted
$ echo $?
130 # Indicates SIGINT terminationGuidelines:
- Don't ignore SIGINT
- Set a flag in handler, check in main loop
- Exit within 1-2 seconds of Ctrl-C
- Print brief message acknowledging interrupt
Reference: Command Line Interface Guidelines - Signals
Handle SIGPIPE for Broken Pipes
Ignore SIGPIPE and handle write errors explicitly. Default SIGPIPE behavior terminates the program when the pipe reader closes early.
Incorrect (crashes on broken pipe):
int main(void) {
// Default SIGPIPE behavior
for (int i = 0; i < 1000000; i++) {
printf("Line %d\n", i); // SIGPIPE kills process
}
}# Process dies silently when head closes pipe
$ mytool | head -5
Line 0
Line 1
Line 2
Line 3
Line 4
# mytool crashed with SIGPIPE, no error shownCorrect (handles broken pipe gracefully):
#include <signal.h>
#include <errno.h>
int main(void) {
// Ignore SIGPIPE, handle EPIPE error instead
signal(SIGPIPE, SIG_IGN);
for (int i = 0; i < 1000000; i++) {
if (printf("Line %d\n", i) < 0) {
if (errno == EPIPE) {
// Reader closed pipe, exit gracefully
break;
}
// Other write error
perror("write");
return 1;
}
}
return 0;
}# Exits cleanly when pipe closes
$ mytool | head -5
Line 0
Line 1
Line 2
Line 3
Line 4
$ echo $?
0 # Clean exit, no crashAlternative (check output stream):
for (int i = 0; i < 1000000; i++) {
printf("Line %d\n", i);
if (fflush(stdout) == EOF || ferror(stdout)) {
break; // Stop on any output error
}
}Reference: signal(7) - SIGPIPE
Handle SIGTERM for Clean Shutdown
Handle SIGTERM to allow graceful shutdown when killed by process managers, init systems, or container orchestrators.
Incorrect (no SIGTERM handling):
int main(void) {
// No signal handling - abrupt termination
while (1) {
process_request(); // Killed mid-request
}
}# Container orchestrator sends SIGTERM, then SIGKILL after timeout
$ docker stop mycontainer
# Transactions may be left incompleteCorrect (graceful SIGTERM handling):
#include <signal.h>
volatile sig_atomic_t shutdown_requested = 0;
void handle_sigterm(int sig) {
shutdown_requested = 1;
}
int main(void) {
struct sigaction sa = {
.sa_handler = handle_sigterm,
.sa_flags = 0
};
sigemptyset(&sa.sa_mask);
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL); // Handle both
fprintf(stderr, "Server started, PID %d\n", getpid());
while (!shutdown_requested) {
// Finish current request before checking
if (has_pending_request()) {
process_request();
}
if (shutdown_requested) {
fprintf(stderr, "Shutdown requested, finishing...\n");
break;
}
}
// Graceful cleanup
finish_pending_transactions();
close_connections();
fprintf(stderr, "Shutdown complete\n");
return 0;
}$ mytool &
Server started, PID 12345
$ kill 12345 # Sends SIGTERM
Shutdown requested, finishing...
Shutdown completeFor long-running services:
- Finish current operation before exiting
- Drain connection pools
- Flush buffers and caches
- Log shutdown completion
Reference: systemd - Daemon Shutdown
Related skills
FAQ
What does unix-cli do?
unix-cli: A skill for development. This provides functionality for development workflows.
When should I use unix-cli?
When you need to use unix-cli for development tasks, or when unix-cli: a skill for development. this provides functionality for development workflows.
What are the main capabilities?
unix-cli.