DEV Community

hinika
hinika

Posted on

wisp v3 update: fixed the pty-only job-control bugs, plus LuaRocks packages

Update on wisp, the Linux shell that swaps bash syntax for Lua. Since that post, I found and fixed a batch of bugs that only showed up under a real pty, not in the unit test suite, and added a few things people asked for.

wisp demo

The core idea is simple: any global function you define in ~/.config/wisp/init.lua becomes a shell command. No alias syntax, no config DSL -- just Lua.

function ll(input)
    return sh("ls -la")
end

function g(input, pattern)
    return sh("grep " .. pattern)
end
Enter fullscreen mode Exit fullscreen mode

Then use them like any command: wisp> ll | g "wisp"

Why Lua?

Every shell invents its own scripting language. Bash has its own grammar, fish has its own, zsh has its own. They all have different syntax for aliases, functions, conditionals, and loops.

Lua is already a real programming language with closures, tables, loops, and a standard library. It's the most embedded scripting language on the planet. Why invent another one?

Structured Pipeline Data

The other thing that's different: native pipeline stages (user-defined functions and builtins) pass Lua tables between each other in-process. No fork, no serialization.

function nums(input)
  return { {n = 3}, {n = 1}, {n = 2} }
end

function sorted(input)
  table.sort(input, function(a, b) return a.n < b.n end)
  return input
end
Enter fullscreen mode Exit fullscreen mode

wisp> nums | sorted passes the table directly from nums to sorted without forking or text parsing. Only when a pipeline hits an external command (grep, wc, anything on $PATH) does wisp fork and serialize to bytes.

What's in v3

  • Full job control: fg, bg, kill, disown, wait, jobs -l
  • Globbing, brace expansion
  • ${VAR:-default} expansion
  • 2>&1 redirect
  • Tab completion for commands, filenames, Lua functions
  • Levenshtein command-not-found suggestions
  • Script mode: ./wisp -f script.lua or #!/usr/bin/env wisp
  • pkg -- a thin front-end to LuaRocks (pkg install luasocket), with installed rocks immediately available via require()
  • Builtins: cd, exit, export, command, jobs, fg, bg, kill, disown, wait, echo, source, pwd, type, pkg
  • A round of bug fixes that only showed up under a real pty: command substitution, builtins in pipelines/redirects, Ctrl-C at the prompt, cd/$PWD staleness, and more (see commit history)

Benchmarks

Measured against bash 5.3, zsh 5.9, dash 0.5, and Lua 5.4 itself:

wisp bash zsh dash
Binary, stripped 423K 1.1M ~900K ~120K
Source lines 2,533 ~500K ~250K ~20K
Startup, best of 40 1.6ms 2.1ms 1.2ms 1.2ms

wisp starts faster than bash and runs at about a third of the binary size, while staying honest about the tradeoff: it pays for luaL_openlibs up front, which is what buys a real language at the prompt instead of sub-millisecond startup. Full methodology and more benchmarks (throughput, peak RSS, pipeline overhead) are in the README.

What it can't do (yet)

No array syntax, no [[ ]] tests, no ${!prefix*} expansions, no heredocs. Not a bash replacement for scripts -- focused on interactive use.

I used AI for help with the C++ I had trouble with, but the design decisions -- the Lua embedding model, the parser structure, the job control -- are mine, and the code has been through real testing and pty-level debugging.

GitHub: https://github.com/Hinikaa/wisp

Top comments (0)