nurlbox/find.nu — walking a tree: stat, du, find.
All three walk with fs_lstat, never fs_stat: a walk that follows symlinks visits whatever they point at, which turns du into a double-count and find -delete into a way to lose files outside the tree you named.
@ ap_stat ( Vec String ) argv → i: i DU_ALL 1: i DU_SUMMARY 2: i DU_HUMAN 4: i DU_BYTES 8: i DU_MEGA 16: i DU_TOTAL 32@ ap_du ( Vec String ) argv → i: FindCtx: FindCtx {
s path
s name
i mode
i size
i mtime
i depth
b live // F while inside a short-circuited branch: no actions run
}
: ~ i g_find_pos 0The parser cursor. find parses one expression at a time in one thread, so a cursor global is simpler than threading inout i pos through four mutually-recursive functions.
: ~ i g_find_maxdepth 1000000: ~ i g_find_mindepth 0: ~ b g_find_has_action F: ~ b g_find_depth_first F: ~ b g_find_prune F: ~ i g_find_rc 0@ ap_find ( Vec String ) argv → inurlbox/proc.nu — what the machine is doing.
ps / kill / killall / pidof / free / uptime / df / mount.
Everything except kill and df reads /proc, because on Linux that IS the process table — there is no syscall that enumerates processes, and a tool that pretended otherwise would be reading the same files through a wrapper. On a machine with no /proc (the unikernel, a container without it mounted) each of these says so rather than reporting an empty system: "no processes" and "I cannot see the processes" are different answers.
: s PROC_ROOT /proc``@ ap_ps ( Vec String ) argv → i: s BX_SIGNAMES HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS``@ ap_kill ( Vec String ) argv → i@ ap_killall ( Vec String ) argv → i@ ap_pidof ( Vec String ) argv → i@ ap_free ( Vec String ) argv → i@ ap_uptime ( Vec String ) argv → i: MountEnt: MountEnt {
String dev
String dir
String kind
String opts
}
@ ap_mount ( Vec String ) argv → i@ ap_df ( Vec String ) argv → inurlbox/textmisc.nu — the smaller text utilities.
comm / paste / fold / expand / unexpand / shuf / dos2unix / unix2dos / factor / sum.
@ ap_comm ( Vec String ) argv → i@ ap_paste ( Vec String ) argv → i@ ap_fold ( Vec String ) argv → i@ ap_expand ( Vec String ) argv → i@ ap_unexpand ( Vec String ) argv → i@ ap_shuf ( Vec String ) argv → i@ ap_dos2unix ( Vec String ) argv → i@ ap_factor ( Vec String ) argv → i@ ap_sum ( Vec String ) argv → inurlbox/binio.nu — the byte-level tools.
od / hexdump / xxd / cmp / dd / split / strings.
These are the applets people reach for when something is not text after all, so every one of them is byte-exact: no line-ending rewriting, no NUL truncation, and no assumption that the input fits in memory unless the tool's own definition requires it.
: s BX_HEX_LOWER 0123456789abcdef``: s BX_HEX_UPPER 0123456789ABCDEF``@ bx_push_hex String out i value i digits b upper → v@ bx_push_octal String out i value i digits → v@ bx_push_right String out s text i width → v@ ap_od ( Vec String ) argv → i@ ap_hexdump ( Vec String ) argv → i@ ap_xxd ( Vec String ) argv → i@ ap_cmp ( Vec String ) argv → i@ ap_strings ( Vec String ) argv → i@ ap_split ( Vec String ) argv → i@ ap_dd ( Vec String ) argv → inurlbox/sys.nu — the applets that ask the system about itself.
pwd / basename / dirname / env / printenv / printf / sleep / usleep / uname / arch / hostname / whoami / id / groups / logname / nproc / which / date / sync / clear / tty.
& c @ getuid → i32& c @ geteuid → i32& c @ getgid → i32& c @ getegid → i32& c @ getgroups i32 size *u list → i32@ ap_pwd ( Vec String ) argv → i@ ap_basename ( Vec String ) argv → i@ ap_dirname ( Vec String ) argv → i@ ap_env ( Vec String ) argv → i@ ap_printenv ( Vec String ) argv → i@ ap_printf ( Vec String ) argv → i@ ap_sleep ( Vec String ) argv → i@ ap_usleep ( Vec String ) argv → i@ ap_uname ( Vec String ) argv → i@ ap_arch ( Vec String ) argv → i@ ap_hostname ( Vec String ) argv → i@ ap_whoami ( Vec String ) argv → i@ ap_logname ( Vec String ) argv → i@ ap_groups ( Vec String ) argv → i@ ap_id ( Vec String ) argv → i@ ap_nproc ( Vec String ) argv → i@ ap_which ( Vec String ) argv → i@ ap_date ( Vec String ) argv → i@ ap_sync ( Vec String ) argv → i@ ap_clear ( Vec String ) argv → i@ ap_tty ( Vec String ) argv → inurlbox/sh.nu — the shell.
A POSIX-shaped sh: quoting, parameter and command substitution, arithmetic, globbing, pipelines, redirections, && / ||, if, while, until, for, case, functions, and the builtins a script cannot be written without.
Two decisions shape the implementation.
Applets run in-process. When a command names one of nurlbox's own applets, the shell calls it directly rather than exec'ing itself. That is busybox's standalone-shell trick, and here it is what makes the shell work on a machine with no fork at all — the unikernel runs one program in one address space, and a shell that could only work by spawning would be a shell that could not run there.
Quoting is tracked per byte. Every word carries a mask parallel to its text saying how each byte was quoted: 0 unquoted, 1 single-quoted, 2 double-quoted. Expansion consults it, so $x expands inside " and not inside ', and the RESULT of an expansion is split into fields and globbed only where the $ itself was unquoted. Anything less than a per-byte answer gets "$@" or echo "a b" wrong.
: ShWord: ShWord {
String text
String mask // one byte per text byte: 0 bare, 1 single, 2 double
}
: i SHT_WORD 0: i SHT_OP 1: i SHT_NEWLINE 2: i SHT_EOF 3: ShTok: ShTok {
i kind
ShWord word // for SHT_WORD
String op // for SHT_OP
}
: i SH_SIMPLE 0: i SH_PIPE 1: i SH_AND 2: i SH_OR 3: i SH_SEQ 4: i SH_IF 5: i SH_WHILE 6: i SH_UNTIL 7: i SH_FOR 8: i SH_CASE 9: i SH_GROUP 10: i SH_SUBSHELL 11: i SH_FUNC 12: i SH_BACKGROUND 13: i SH_NOT 14: i SHR_IN 0 // <: i SHR_OUT 1 // >: i SHR_APPEND 2 // >>: i SHR_HEREDOC 3 // <<: i SHR_DUP 4 // >&N / <&N: ShRedir: ShRedir {
i kind
i fd // the descriptor being redirected
ShWord word // filename, heredoc body, or target descriptor
}
: ShNode: ShNode {
i kind
i a
i b
i c
( Vec i ) kids
( Vec ShWord ) words
( Vec ShRedir ) redirs
String text
}
: ~ i g_sh_pos 0: ~ i g_sh_here 0: ~ b g_sh_perr F: ShState: ShState {
( Vec String ) names
( Vec String ) values
( Vec String ) fnames
( Vec i ) fnodes
( Vec String ) params
String argv0
i status
i exiting // 1 once `exit` has been run
i exit_code
i brk // pending `break` levels
i cont // pending `continue` levels
i returning
i depth // command-substitution nesting, for a sanity bound
}
: ~ i g_sh_state 0: ~ i g_ari_pos 0& c @ dup i32 fd → i32: i SH_O_RDONLY 0: i SH_O_WRONLY 1: i SH_O_CREAT 64: i SH_O_TRUNC 512: i SH_O_APPEND 1024: ~ i g_sh_have_fork -1Does this machine have processes at all? Asked once, because the answer changes what a pipeline and a $(…) can mean, and a unikernel says no.
@ ap_sh ( Vec String ) argv → inurlbox/filter.nu — the line-shaped filters.
tac / rev / nl / cut / tr / sort / uniq / tee / comm / paste / fold / expand / unexpand / split / cmp.
Each reads its inputs through bx_reader, so - and a missing operand both mean stdin, and each writes through one String buffer so a long run costs one write per few thousand lines rather than one per line.
@ bx_read_lines s path ( Vec String ) out → bCollect every line of one input, terminators stripped.
@ bx_free_lines ( Vec String ) v → v@ bx_inputs BxOpts o ( Vec String ) out → vEvery operand, or - when there are none.
@ ap_tac ( Vec String ) argv → i@ ap_rev ( Vec String ) argv → i@ ap_nl ( Vec String ) argv → i: CutList: CutList {
( Vec i ) lo
( Vec i ) hi
b ok
}
A LIST like 1,3-5,7- as a membership test. Ranges are 1-based and N- means "to the end", which is how every cut spells it.
@ ap_cut ( Vec String ) argv → i@ ap_tr ( Vec String ) argv → i: ~ i g_sort_flags 0: ~ i g_sort_key 0: ~ i g_sort_key_end 0: ~ i g_sort_delim -1: i SORT_REVERSE 1: i SORT_NUMERIC 2: i SORT_FOLD 4: i SORT_BLANKS 8@ bx_is_blank i c → b@ ap_sort ( Vec String ) argv → i@ ap_uniq ( Vec String ) argv → i@ ap_tee ( Vec String ) argv → inurlbox/bx.nu — the plumbing every applet shares.
busybox's own glue is three things: a diagnostic prefix that names the applet rather than the binary, one option scanner (getopt32) that every applet drives from a short spec string, and a handful of byte-level output helpers. This file is that, in NURL.
Option scanning follows busybox's model deliberately:
: BxOpts o ( bx_getopt argv 1 ln: number=n,lines=n ) ? ( bx_has o l ) { ... } {} : s v ( bx_val o n ) // `` when the option was absent : ( Vec String ) rest ( bx_operands o )
The spec is a run of option letters; a letter followed by : takes a value. longs maps long names onto those letters, name=letter comma-separated, so --lines 5 and -n5 land in the same slot. Clustered shorts (-la), an attached value (-n5), a detached value (-n 5), --name=value, -- and a bare - (an operand, by universal convention stdin) all behave as POSIX describes.
: ~ s g_bx_name nurlbox``The name diagnostics carry. Set once by the dispatcher, so every message reads rm: cannot remove 'x' and not nurlbox: ....
@ bx_name → s@ bx_set_name s n → v@ bx_err s msg → v@ bx_err_at s subject s msg → vapplet: subject: msg — the shape every coreutils error takes.
@ bx_write String buf → vWrite a String's bytes verbatim — NULs and all — through stdout's ordinary buffer, so it never reorders against nurl_print.
@ bx_write_bytes ( Vec u ) buf → v@ bx_streq s a s b → b@ bx_at ( Vec String ) v i idx → sBorrowed view of argv[idx]; the empty string past the end. Callers treat it as read-only — the Vec still owns the bytes.
: BxOpts: BxOpts {
s spec
i flags // bit k set = the k-th option letter of `spec` was given
( Vec String ) vals // one slot per option letter, `` when unset
( Vec String ) allvals // every value-bearing occurrence, in order
( Vec i ) allords // the option ordinal each `allvals` entry belongs to
( Vec String ) args // the operands, in order
b ok
}
@ bx_getopt ( Vec String ) argv i start s spec s longs → BxOptsScan argv from start. See the file header for the spec grammar.
@ bx_has BxOpts o s letter → b@ bx_val BxOpts o s letter → sThe value given for an option, or `` when it was never supplied.
@ bx_vals BxOpts o s letter ( Vec String ) out → vEvery value given for an option, in command-line order. Appends to out; the caller owns the Strings it receives.
@ bx_operand_count BxOpts o → i@ bx_operand BxOpts o i idx → s@ bx_ok BxOpts o → b@ bx_opts_free BxOpts o → v@ bx_is_stdin s path → b@ bx_ioerr IoErr e → sThe POSIX spelling of an I/O failure — No such file or directory, not the stdlib enum's terser not found. Utilities are read by people and by scripts that grep their stderr, so the wording matters.
@ bx_reader s path → ?BufReaderOpen path (or stdin for -) as a buffered line reader. On failure the error is reported here — the caller only needs the option.
@ bx_slurp s path inout b ok → ( Vec u )Whole input as bytes, diagnostics included. ok says whether the returned Vec means anything.
@ bx_count s text → i── Numbers ───────────────────────────────────────────────────────
A count with the size suffixes every utility accepts: b (512), k/K (1024), M, G, and the decimal kB/MB/GB. Returns -1 when the text is not a count at all, so a caller can diagnose it.
@ bx_is_digit i c → b@ bx_is_hex i c → b@ bx_hex_val i c → inurlbox/hash.nu — digests and encodings.
md5sum / sha1sum / sha256sum / sha512sum / base64 / cksum / crc32.
Every digest here is the shipped pure-NURL implementation from the stdlib — no OpenSSL, no libcrypto, nothing to link. The -c check mode reads the same format it writes, so sha256sum * > SUMS and sha256sum -c SUMS round-trip.
: i HASH_MD5 0: i HASH_SHA1 1: i HASH_SHA256 2: i HASH_SHA512 3@ bx_sum i kind ( Vec String ) argv → i@ ap_md5sum ( Vec String ) argv → i@ ap_sha1sum ( Vec String ) argv → i@ ap_sha256sum ( Vec String ) argv → i@ ap_sha512sum ( Vec String ) argv → i@ ap_base64 ( Vec String ) argv → i@ ap_cksum ( Vec String ) argv → i@ ap_crc32 ( Vec String ) argv → inurlbox/text.nu — the stream utilities.
cat / echo / head / tail / wc / seq / yes. Every one of them is a byte-exact filter: a line is copied with the terminator the input carried, so a CRLF file survives a head and a file with no final newline does not grow one.
@ ap_echo ( Vec String ) argv → i: i CAT_NUMBER 1 // -n: i CAT_NONBLANK 2 // -b: i CAT_ENDS 4 // -E: i CAT_TABS 8 // -T: i CAT_NONPRINT 16 // -v: i CAT_SQUEEZE 32 // -s@ ap_cat ( Vec String ) argv → i@ ap_head ( Vec String ) argv → i@ ap_tail ( Vec String ) argv → i: WcCount: WcCount {
i lines
i words
i bytes
i chars
i longest
}
@ ap_wc ( Vec String ) argv → i@ ap_seq ( Vec String ) argv → i@ ap_yes ( Vec String ) argv → inurlbox/grep.nu — grep / egrep / fgrep.
Three matchers behind one applet, because that is what the three names mean:
grep POSIX BASIC regular expressions (\(, \|, \+) grep -E POSIX EXTENDED regular expressions ((, |, +) = egrep grep -F fixed strings, no metacharacters at all = fgrep
The engine is the stdlib's ext/regex, which speaks ERE. Basic regular expressions are therefore TRANSLATED to extended ones rather than approximated: in a BRE, ( ) | + ? { } are literal characters and their backslashed forms are the operators, which is exactly the swap _bre_to_ere performs. A grep that quietly treated a+ as "one or more a" would silently mis-answer every script that meant a literal plus.
: i GREP_INVERT 1: i GREP_IGNORE 2: i GREP_COUNT 4: i GREP_LINENO 8: i GREP_FILES 16: i GREP_NOFILES 32: i GREP_QUIET 64: i GREP_WORD 128: i GREP_LINE 256: i GREP_ONLY 512: i GREP_FIXED 1024: i GREP_WITHNAME 2048: i GREP_NOMATCH_FILES 4096: i GREP_RECURSE 8192: i GREP_SILENT 16384@ _bre_to_ere s pat → StringBRE → ERE. \( becomes ( and a bare ( becomes \(; same for ), |, +, ?, {, }. Inside a bracket expression nothing is special, so the scan tracks that.
: GrepPat: GrepPat {
Regex rx
String lit
b fixed
}
One pattern, compiled or literal.
@ _grep_lower s text → StringCase folding is done by lowering both the pattern and the line — the engine has no case-insensitive mode, and lowering the input is what every grep without one does.
@ ap_grep ( Vec String ) argv → inurlbox/fileops.nu — the applets that touch the filesystem.
ls / stat / mkdir / rmdir / rm / cp / mv / ln / touch / readlink / realpath / truncate / chmod / mktemp.
Everything here goes through fs_lstat rather than fs_stat when it walks: a recursive remove or copy that follows a symlink leaves the tree it was given, which is the difference between deleting a directory and deleting whatever it happened to point at.
: BxEnt: BxEnt {
String name
i mode
i size
i mtime
i nlink
i uid
i gid
i blocks
i ino
b ok // F when the entry could not be stat'ed at all
}
: i LS_ALL 1 // -a: i LS_LONG 2 // -l: i LS_ONE 4 // -1: i LS_DIRS 8 // -d: i LS_REVERSE 16 // -r: i LS_TIME 32 // -t: i LS_SIZE_SORT 64 // -S: i LS_RECURSE 128 // -R: i LS_ALMOST 256 // -A: i LS_CLASSIFY 512 // -F: i LS_INODE 1024 // -i: i LS_HUMAN 2048 // -h: i LS_NUMERIC 4096 // -n: i LS_BLOCKS 8192 // -s: i LS_SLASH 16384 // -p: i LS_COLUMNS 32768 // -C / a tty@ bx_human i n → String1024-based sizes the way -h prints them: 4.0K, 1.5M, 12G.
: ~ i g_ls_sort_flags 0The comparator's flags ride a global rather than a capture: a capturing closure handed to a GENERIC callee cannot be proven invoke-only, so its heap environment becomes the caller's to free — one 16-byte block per ls, which LeakSanitizer duly reported. A capture-free closure allocates nothing at all.
@ ap_ls ( Vec String ) argv → i@ ap_mkdir ( Vec String ) argv → i@ ap_rmdir ( Vec String ) argv → i@ bx_parse_mode s text i base → iAn octal mode (755, 0644) or a symbolic one (u+x, a-w, go=rX), applied to base. Returns -1 when the text is neither.
@ ap_chmod ( Vec String ) argv → i@ ap_rm ( Vec String ) argv → i& c @ link s target s linkpath → i32: i CP_RECURSE 1: i CP_PRESERVE 2: i CP_FORCE 4: i CP_VERBOSE 8: i CP_NODEREF 16: i CP_NOCLOBBER 32@ ap_cp ( Vec String ) argv → i@ ap_mv ( Vec String ) argv → i@ ap_ln ( Vec String ) argv → i@ ap_touch ( Vec String ) argv → i@ ap_readlink ( Vec String ) argv → i@ ap_realpath ( Vec String ) argv → i@ ap_truncate ( Vec String ) argv → i@ ap_mktemp ( Vec String ) argv → inurlbox/sed.nu — the stream editor.
Addresses (N, $, /re/, ranges, !), the substitute command with its g / p / i / Nth-occurrence flags and & / \1 back references, the hold space, transliteration, branches and labels, and { } blocks.
Back references are why stdlib/ext/regex.nu grew capture groups: a sed whose s/\(a\)\(b\)/\2\1/ silently produced nothing would be a sed nobody could use, and the honest place to fix that was the engine, not this file.
Regular expressions follow POSIX: sed is BASIC (so \( groups and a bare + is a literal plus), sed -E is EXTENDED. The translation is grep.nu's _bre_to_ere, shared rather than written twice.
: i SED_ADDR_NONE 0: i SED_ADDR_LINE 1: i SED_ADDR_LAST 2: i SED_ADDR_RE 3: SedCmd: SedCmd {
i a1kind
i a1line
Regex a1re
i a2kind
i a2line
Regex a2re
b negate
i cmd // the command letter's byte
Regex re // s/// pattern
b has_re
String arg1 // s replacement / y source / a,i,c text / label / filename
String arg2 // y destination
i sflags // 1 = g, 2 = p, 4 = i
i soccur // Nth occurrence (0 = first)
i jump // `{` → index just past the matching `}`
}
: i SED_S_GLOBAL 1: i SED_S_PRINT 2: i SED_S_ICASE 4: ~ i g_sed_pos 0: ~ b g_sed_bad F: ~ b g_sed_ere F: ~ b g_sed_nonl FA file whose last line has no newline must not grow one — sed is a filter, and a filter that silently appends a byte breaks every checksum downstream of it. The missing terminator is therefore remembered rather than invented: if anything else is printed after such a line, the newline that separates them is emitted then.
: ~ b g_sed_quit F: ~ i g_sed_rc 0@ ap_sed ( Vec String ) argv → inurlbox — one binary, many utilities.
The busybox idea, in NURL: a single executable that decides which utility it is from the name it was invoked under. Symlink cat at it and it is cat; run nurlbox cat and it is cat too. Everything is pure NURL over the shipped stdlib — no shelling out, nothing that needs a system coreutils underneath.
nurlbox list the applets nurlbox cat file run one, by name nurlbox --install DIR populate DIR with a symlink per applet ln -s nurlbox /bin/cat the busybox way, thereafter cat file
: s NURLBOX_VERSION 0.2.0``@ bx_run_applet s name ( Vec String ) argv → iThe dispatcher. argv is the applet's own argument vector: argv[0] is the applet name, exactly as a directly-executed utility sees it.
@ bx_is_applet s name → bIs name one of ours? A binary invoked under a name it does not implement is the MULTIPLEXER — that is how nurlbox behaves when the unikernel's loader calls it main, and how a copy named anything else still works.
@ main → inurlbox/shell.nu — the applets a shell script is made of.
test / [ / expr / xargs / timeout / nohup / basename-style plumbing.
test and expr are where a clone earns its keep: they are called once per loop iteration by every script in existence, and their grammar is small but exact. Both are implemented as real parsers with POSIX precedence, not as flag lists.
: ~ i g_test_pos 0@ ap_test ( Vec String ) argv → i: ~ i g_expr_pos 0: ~ b g_expr_err F@ ap_expr ( Vec String ) argv → i@ ap_xargs ( Vec String ) argv → inurlbox/archive.nu — tar, gzip, gunzip, zcat.
Both formats are the shipped pure-NURL implementations — stdlib/ext/tar.nu and stdlib/ext/compress.nu over stdlib/std/deflate.nu — so an image carrying these applets links nothing beyond libc: no libz, no libarchive.
Whole-archive: an archive is read into memory, transformed and written back. tar's own format is a stream and could be processed as one; this is the honest limit of the current implementation and the reason a multi-gigabyte tarball is not this tool's job yet.
@ ap_gzip ( Vec String ) argv → i@ ap_tar ( Vec String ) argv → i