Skip to content

HOWTO Add a shell command

kazah-png edited this page Jul 27, 2026 · 2 revisions

HOWTO Add a shell command

How to add a builtin to the NyxOS kernel shell. The worked example adds uptime, which prints the time since boot.

Tip

Consider whether the feature belongs in ring 3 instead. A kernel builtin runs with full privilege, cannot be piped, and grows the kernel image; a userspace program can be piped, killed, and cannot crash the machine. Add a builtin only when the command needs kernel state that no syscall exposes. See HOWTO-Write-a-userspace-program.

See also: Shell, Kernel-Data-Structures, Building, HOWTO-Write-a-userspace-program

How the shell dispatches

kernel/core/kernel.c holds one table of builtins. Resolution order for a typed word:

  1. Match against the commands[] table → run the builtin
  2. Otherwise, if /<name>.elf exists → auto-exec it in the foreground with argv forwarded
  3. Otherwise → command not found

FILE — kernel/core/kernel.c (the table entry type)

typedef struct {
    const char* name;
    void (*func)(int argc, char** argv);
    const char* help;
    bool hidden;
} command_t;
Field Meaning
name The typed word
func Handler, receiving argc/argv with argv[0] = the command name
help One line shown by help, and the place to document the syntax
hidden true omits it from help and from tab completion

Step 1 — Declare the handler

Handlers are forward-declared above the table.

FILE — kernel/core/kernel.c

static void cmd_df(int argc, char** argv);
static void cmd_setres(int argc, char** argv);
static void cmd_uptime(int argc, char** argv);   /* new */

Step 2 — Add the table entry

Place it near related commands — the table is grouped by topic.

FILE — kernel/core/kernel.c

static const command_t commands[] = {
    /* … */
    {"date",    cmd_date,   "Show current date and time", false},
    {"uptime",  cmd_uptime, "Show time since boot", false},      /* new */
    /* … */
    {NULL, NULL, NULL, false}
};

Important

The {NULL, NULL, NULL, false} sentinel terminates the table. Adding an entry after it makes the command unreachable and breaks help.

Step 3 — Implement the handler

FILE — kernel/core/kernel.c

static void cmd_uptime(int argc, char** argv) {
    (void)argc; (void)argv;
    uint64_t secs = tick_count / 1000;          /* PIT runs at 1000 Hz */
    printf("up %llu:%02llu:%02llu\n",
           secs / 3600, (secs / 60) % 60, secs % 60);
}

Conventions

Rule Reason
Cast unused parameters to (void) The build is -Wall -Wextra and must stay warning-free
Validate argc before touching argv[n] The shell does not check arity for you
Print errors with printf, do not panic A bad argument must not take the machine down
Keep the handler short It runs on the compositor's stack, which is shared with the GUI

Argument handling — the standard shape

static void cmd_setres(int argc, char** argv) {
    if (argc < 3) {
        printf("usage: setres <width> <height>\n");
        return;
    }
    int w = atoi(argv[1]);
    int h = atoi(argv[2]);
    /* … */
}

Warning

A kernel builtin runs in ring 0 on a stack shared with the compositor. Deep recursion or a large stack array can overflow it and corrupt unrelated memory. Allocate with kmalloc instead of declaring a large local.

Blocking and long-running work

A builtin that blocks stalls the shell — and, in the GUI, the terminal window with it. For anything long-running:

  • Use sleep(ms), never a busy loop, so other tasks get the CPU
  • Or spawn a kernel thread with create_process(name, entry, arg) and return immediately
  • Or make it a userspace program and let the scheduler handle it

Touching shared state

The shell runs on the compositor thread with interrupts enabled, so it can be preempted mid-operation. Anything a user process can also reach needs a lock:

preempt_disable();          /* single-core re-entrancy only */
/* … */
preempt_enable();

Caution

preempt_disable() stops a context switch on the local core and means nothing to another core. On SMP, shared state needs a real spinlock. See SMP.

Step 4 — Build and verify

CODE — Rebuild and boot with a serial console

host $ make -C kernel
host $ ./run.ps1 -Mode serial
nyx> uptime
up 0:01:47
nyx> help
[…]
  uptime       Show time since boot
[…]

Check three things: the command runs, it appears in help, and upt + Tab completes it.

Adding an alias

Point a second entry at the same handler. fastfetch does exactly this:

{"nyxfetch",  cmd_nyxfetch, "Show system info with ASCII logo", false},
{"fastfetch", cmd_nyxfetch, "Alias for nyxfetch",               false},

Adding a self-test command

Self-tests are the project's standard way to prove a subsystem. By convention the name ends in test, the implementation returns 0 on success, and the handler prints a per-case result.

{"giftest", cmd_giftest, "GIF image decoder self-test (LZW + interlace + transparency)", false},

The existing suite is listed in Shell.

Checklist

  • Handler forward-declared above commands[]
  • Entry added before the {NULL, …} sentinel
  • help string states the syntax, e.g. "…: cmd <arg> [opt]"
  • argc validated before indexing argv
  • Unused parameters cast to (void)
  • No large stack locals; kmalloc instead
  • Shared state locked appropriately
  • Builds with zero warnings
  • Appears in help and in Tab completion
  • Shell page updated in this wiki

Troubleshooting

Symptom Cause Fix
command not found Entry added after the sentinel, or hidden set Move it before {NULL, …}
Missing from help and Tab completion hidden is true Set it to false
Build fails with unused parameter -Wextra Cast to (void)
Machine freezes when run Busy loop or blocking call on the compositor thread Use sleep(ms), or spawn a thread
Random crashes elsewhere afterwards Kernel stack overflow from a large local kmalloc the buffer
A userspace program shadows the name A builtin always wins over /name.elf Rename one of them

See also

External resources

Clone this wiki locally