├── example-posix ├── build.zig.zon ├── build.zig └── src │ └── main.zig ├── .github └── workflows │ └── build.yml ├── MIT-LICENSE ├── README.md └── src └── embshell.zig /example-posix/build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .example_posix, 3 | .fingerprint = 0xfedd49cf991075e4, 4 | .version = "0.0.0", 5 | .dependencies = .{ 6 | .embshell = .{ .path = ".." }, 7 | }, 8 | .paths = .{ 9 | "build.zig", 10 | "build.zig.zon", 11 | "src", 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, macos-latest] 10 | runs-on: ${{matrix.os}} 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | path: zig-embshell 16 | - name: Setup Zig 17 | uses: mlugg/setup-zig@v2 18 | with: 19 | version: 0.15.2 20 | - name: Build lib 21 | run: zig build 22 | working-directory: zig-embshell 23 | - name: Build test 24 | run: zig build 25 | working-directory: zig-embshell/example-posix 26 | 27 | -------------------------------------------------------------------------------- /example-posix/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | const exe = b.addExecutable(.{ 8 | .name = "example-posix", 9 | .root_module = b.createModule(.{ 10 | .root_source_file = b.path("src/main.zig"), 11 | .target = target, 12 | .optimize = optimize, 13 | .link_libc = true, 14 | }), 15 | }); 16 | exe.addIncludePath(b.path("src/")); 17 | 18 | const embshell_dep = b.dependency("embshell", .{ 19 | .target = target, 20 | .optimize = optimize, 21 | }); 22 | const embshell_mod = embshell_dep.module("embshell"); 23 | exe.root_module.addImport("embshell", embshell_mod); 24 | 25 | b.installArtifact(exe); 26 | 27 | const run = b.step("run", "Run the demo"); 28 | const run_cmd = b.addRunArtifact(exe); 29 | run.dependOn(&run_cmd.step); 30 | } 31 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Toby Jaffey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EmbShell 2 | 3 | A very small interactive command shell for (embedded) Zig programs. 4 | 5 | EmbShell makes an ideal system monitor for debugging and interacting with a small embedded system. It interactively takes lines of text, parses commands and makes callbacks into handler functions. 6 | 7 | Compared with Readline, Linenoise and Editline - EmbShell is tiny. It lacks most of their features, but it does have: 8 | 9 | - Tab completion for command names 10 | - Backspace for line editing 11 | - No reliance on libc and very little use of Zig's `std` (ie. no fancy print formatting) 12 | - Very little RAM use (just a configurable buffer for the incoming command line) 13 | 14 | In EmbShell: 15 | 16 | - All commands and configuration are set at `comptime` to optimise footprint 17 | - All arguments are separated by whitespace, there is no support for quoted strings, multiline commands or escaped data 18 | - All handler arguments are strings, leaving it to the app to decide how to parse them 19 | - No runtime memory allocations 20 | 21 | ## Using 22 | 23 | Developed with `zig 0.14.0` 24 | 25 | ### Run the sample 26 | 27 | cd example-posix 28 | zig build run 29 | 30 | ``` 31 | myshell> help 32 | echo 33 | led 34 | myshell> echo hello world 35 | You said: { echo, hello, world } 36 | OK 37 | myshell> led 1 38 | If we had an LED it would be set to true 39 | OK 40 | ``` 41 | 42 | ## Using in your own project 43 | 44 | First add the library as a dependency in your `build.zig.zon` file. 45 | 46 | `zig fetch --save git+https://github.com/ringtailsoftware/zig-embshell.git` 47 | 48 | And add it to `build.zig` file. 49 | ```zig 50 | const embshell_dep = b.dependency("embshell", .{ 51 | .target = target, 52 | .optimize = optimize, 53 | }); 54 | exe.root_module.addImport("embshell", embshell_dep.module("embshell")); 55 | ``` 56 | 57 | `@import` the module and provide a configuration. 58 | 59 | - `.prompt` is the string shown to the user before each command is entered 60 | - `.maxargs` is the maximum number of arguments EmbShell will process (e.g. "mycmd foo bar" is 3 arguments) 61 | - `.maxlinelen` is the maximum length of a line to be handled, a buffer of this size will be created 62 | - `.cmdtable` an array of names and handler function for commands 63 | 64 | ```zig 65 | const UserdataT = u32; 66 | const EmbShellT = @import("embshell").EmbShellFixedParams(UserdataT); 67 | const EmbShell = @import("embshell").EmbShellFixed(.{ 68 | .prompt = "myshell> ", 69 | .maxargs = 16, 70 | .maxlinelen = 128, 71 | .cmdtable = &.{ 72 | .{ .name = "echo", .handler = echoHandler }, 73 | .{ .name = "led", .handler = ledHandler }, 74 | }, 75 | .userdataT = UserdataT, 76 | }); 77 | ``` 78 | 79 | 80 | Each handler function is in the following form. EmbShell prints "OK" after successfully executing each function and "Failed" if an error is returned. 81 | 82 | ```zig 83 | fn myHandler(userdata: UserdataT, args:[][]const u8) anyerror!void { 84 | // process args 85 | // optionally return error 86 | } 87 | ``` 88 | 89 | Next, call `.init()` and provide a write callback to allow EmbShell to emit data 90 | 91 | ```zig 92 | fn write(data:[]const u8) void { 93 | // emit data to terminal 94 | } 95 | 96 | var shell = try EmbShell.init(write, userdata); 97 | ``` 98 | 99 | Finally, feed EmbShell with incoming data from the terminal to be processed 100 | 101 | ```zig 102 | const buf = readFromMyTerminal(); 103 | shell.feed(buf) 104 | ``` 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /example-posix/src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const EmbShellT = @import("embshell").EmbShellFixedParams(u32); 4 | const EmbShell = @import("embshell").EmbShellFixed(EmbShellT{ 5 | .prompt = "myshell> ", 6 | .maxargs = 16, 7 | .maxlinelen = 128, 8 | .cmdtable = &.{ 9 | .{ .name = "echo", .handler = echoHandler }, 10 | .{ .name = "led", .handler = ledHandler }, 11 | }, 12 | .userdataT = u32, 13 | }); 14 | 15 | var stdin_reader:*std.Io.Reader = undefined; 16 | const stdin_reader_handle = std.fs.File.stdin().handle; 17 | 18 | var stdout_writer:*std.Io.Writer = undefined; 19 | 20 | // handler for the "echo" command 21 | fn echoHandler(userdata: u32, args: [][]const u8) anyerror!void { 22 | try stdout_writer.print("userdata={any} You said: {any}\r\n", .{ userdata, args }); 23 | try stdout_writer.flush(); 24 | } 25 | 26 | // handler for the "led" command 27 | fn ledHandler(userdata: u32, args: [][]const u8) anyerror!void { 28 | if (args.len < 2) { 29 | // check that there are the right number of arguments 30 | try stdout_writer.print("userdata={any} {s} <0|1>\r\n", .{ userdata, args[0] }); 31 | try stdout_writer.flush(); 32 | return error.BadArgs; 33 | } 34 | 35 | const val = std.fmt.parseInt(u32, args[1], 10) catch 0; // if it parses and > 0, default to 0 36 | try stdout_writer.print("If we had an LED it would be set to {}\r\n", .{val > 0}); 37 | try stdout_writer.flush(); 38 | } 39 | 40 | var original_termios: ?std.posix.termios = null; 41 | 42 | // setup terminal in raw mode, for instant feedback on typed characters 43 | pub fn raw_mode_start() !void { 44 | const handle = stdin_reader_handle; 45 | var termios = try std.posix.tcgetattr(handle); 46 | original_termios = termios; 47 | 48 | termios.iflag.BRKINT = false; 49 | termios.iflag.ICRNL = false; 50 | termios.iflag.INPCK = false; 51 | termios.iflag.ISTRIP = false; 52 | termios.iflag.IXON = false; 53 | termios.oflag.OPOST = false; 54 | termios.lflag.ECHO = false; 55 | termios.lflag.ICANON = false; 56 | termios.lflag.IEXTEN = false; 57 | termios.lflag.ISIG = false; 58 | termios.cflag.CSIZE = .CS8; 59 | termios.cc[@intFromEnum(std.posix.V.TIME)] = 0; 60 | termios.cc[@intFromEnum(std.posix.V.MIN)] = 1; 61 | 62 | try std.posix.tcsetattr(handle, .FLUSH, termios); 63 | } 64 | 65 | // return to original terminal mode 66 | pub fn raw_mode_stop() void { 67 | if (original_termios) |termios| { 68 | std.posix.tcsetattr(stdin_reader_handle, .FLUSH, termios) catch {}; 69 | } 70 | _ = stdout_writer.print("\r\n", .{}) catch 0; 71 | _ = stdout_writer.flush() catch 0; 72 | } 73 | 74 | // callback for EmbShell to write bytes 75 | fn write(buf: []const u8) void { 76 | _ = stdout_writer.write(buf) catch 0; 77 | _ = stdout_writer.flush() catch 0; 78 | } 79 | 80 | pub fn main() !void { 81 | var done: bool = false; 82 | var stdoutwrbuf: [512]u8 = undefined; 83 | var w = std.fs.File.stdout().writer(&stdoutwrbuf); 84 | stdout_writer = &w.interface; 85 | 86 | var stdinrdbuf:[512]u8 = undefined; 87 | var r = std.fs.File.stdin().reader(&stdinrdbuf); 88 | stdin_reader = &r.interface; 89 | 90 | // setup raw mode on terminal so we can handle individual keypresses 91 | try raw_mode_start(); 92 | defer raw_mode_stop(); 93 | 94 | // setup embshell with write callback 95 | var shell = try EmbShell.init(write, 42); 96 | 97 | // read from keyboard 98 | outer: while (!done) { 99 | var fds = [_]std.posix.pollfd{.{ 100 | .fd = stdin_reader_handle, 101 | .events = std.posix.POLL.IN, 102 | .revents = undefined, 103 | }}; 104 | const ready = std.posix.poll(&fds, 1000) catch 0; 105 | if (ready > 0) { 106 | if (fds[0].revents == std.posix.POLL.IN) { 107 | var buf:[128]u8 = undefined; 108 | const count = try std.posix.read(stdin_reader_handle, &buf); 109 | 110 | if (count > 0) { 111 | // send bytes to EmbShell for processing 112 | shell.feed(buf[0..count]) catch |err| switch (err) { 113 | else => { 114 | done = true; 115 | continue :outer; 116 | }, 117 | }; 118 | } else { 119 | done = true; 120 | continue :outer; 121 | } 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/embshell.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const ascii = std.ascii.control_code; 3 | 4 | pub fn EmbShellFixedParams(dataT: type) type { 5 | return struct { 6 | const EmbShellCmd = struct { name: []const u8, handler: *const fn (userdata: dataT, args: [][]const u8) anyerror!void }; 7 | 8 | prompt: []const u8, 9 | maxlinelen: usize, 10 | maxargs: usize, 11 | cmdtable: []const EmbShellCmd, 12 | userdataT: type, 13 | }; 14 | } 15 | 16 | // returns an EmbShell setup according to EmbShellFixedParams 17 | pub fn EmbShellFixed(comptime params: anytype) type { 18 | return struct { 19 | const Self = @This(); 20 | 21 | got_line: bool, 22 | cmdbuf: [params.maxlinelen + 1]u8 = undefined, 23 | cmdbuf_len: usize, 24 | writeFn: *const fn (data: []const u8) void, 25 | userdata: params.userdataT, 26 | 27 | pub const Cmd = struct { 28 | name: []const u8, 29 | handler: *const fn (args: [][]const u8) anyerror!void, 30 | }; 31 | 32 | pub fn prompt(self: *const Self) !void { 33 | self.writeFn(params.prompt); 34 | } 35 | 36 | pub fn init(wfn: *const fn (data: []const u8) void, userdata: params.userdataT) !Self { 37 | const self = Self{ 38 | .writeFn = wfn, 39 | .got_line = false, 40 | .cmdbuf_len = 0, 41 | .cmdbuf = .{0} ** (params.maxlinelen + 1), 42 | .userdata = userdata, 43 | }; 44 | try self.prompt(); 45 | return self; 46 | } 47 | 48 | fn runcmd(self: *Self, args: [][]const u8) !void { 49 | if (args.len > 0) { 50 | for (params.cmdtable) |cmd| { 51 | if (std.mem.eql(u8, cmd.name, args[0])) { 52 | // exec cmd handler 53 | cmd.handler(self.userdata, args) catch { 54 | self.writeFn("Failed\r\n"); 55 | return; 56 | }; 57 | self.writeFn("OK\r\n"); 58 | return; 59 | } 60 | } 61 | // special case for help, generate it automatically from cmdtable 62 | if (std.mem.eql(u8, args[0], "help")) { 63 | for (params.cmdtable) |cmd| { 64 | self.writeFn(cmd.name); 65 | self.writeFn("\r\n"); 66 | } 67 | } 68 | } 69 | } 70 | 71 | // execute a command line 72 | fn execline(self: *Self, line: []const u8) !void { 73 | // tokenize, returns iterator to slices 74 | var tokens = std.mem.tokenizeAny(u8, line, " "); 75 | // setup argv array to hold tokens 76 | var argv: [params.maxargs][]const u8 = .{undefined} ** params.maxargs; 77 | var argc: u8 = 0; 78 | 79 | while (tokens.next()) |chunk| : (argc += 1) { 80 | if (argc >= params.maxargs) { 81 | break; 82 | } 83 | argv[argc] = chunk; 84 | } 85 | try self.runcmd(argv[0..argc]); 86 | } 87 | 88 | pub fn feed(self: *Self, data: []const u8) !void { 89 | for (data) |key| { 90 | if (self.got_line) { 91 | // buffer is already full 92 | return; 93 | } 94 | switch (key) { 95 | ascii.etx => { // ctrl-c 96 | return error.embshellCtrlC; 97 | }, 98 | ascii.cr, ascii.lf => { 99 | self.got_line = true; 100 | self.writeFn("\r\n"); 101 | }, 102 | ascii.del, ascii.bs => { 103 | if (self.cmdbuf_len > 0) { 104 | self.cmdbuf_len -= 1; 105 | self.cmdbuf[self.cmdbuf_len] = 0; 106 | 107 | const bs: [3]u8 = .{ ascii.bs, ' ', ascii.bs }; 108 | self.writeFn(&bs); 109 | } 110 | }, 111 | ascii.ht => { // Tab 112 | var matches: [params.cmdtable.len]usize = .{undefined} ** (params.cmdtable.len); // indices of matching commands 113 | var numMatches: usize = 0; 114 | // look for matches 115 | for (params.cmdtable, 0..) |cmd, index| { 116 | if (std.mem.startsWith(u8, cmd.name, self.cmdbuf[0..self.cmdbuf_len])) { 117 | matches[numMatches] = index; 118 | numMatches += 1; 119 | } 120 | } 121 | if (numMatches > 0) { 122 | switch (numMatches) { 123 | 1 => { // exactly one match 124 | const cmd = params.cmdtable[matches[0]]; 125 | self.writeFn(cmd.name[self.cmdbuf_len..]); 126 | std.mem.copyForwards(u8, &self.cmdbuf, cmd.name); 127 | self.cmdbuf_len = cmd.name.len; 128 | self.cmdbuf[self.cmdbuf_len] = 0; 129 | }, 130 | else => { // multiple matches 131 | self.writeFn("\r\n"); 132 | for (matches) |match| { 133 | const cmd = params.cmdtable[match]; 134 | self.writeFn(cmd.name); 135 | self.writeFn("\r\n"); 136 | } 137 | try self.prompt(); 138 | self.writeFn(self.cmdbuf[0..self.cmdbuf_len]); 139 | }, 140 | } 141 | } 142 | }, 143 | else => { 144 | // echo 145 | if (self.cmdbuf_len < params.maxlinelen) { 146 | self.writeFn(@as(*const [1]u8, @ptrCast(&key))); // u8 to single-item slice 147 | 148 | self.cmdbuf[self.cmdbuf_len] = key; 149 | self.cmdbuf_len += 1; 150 | self.cmdbuf[self.cmdbuf_len] = 0; 151 | } else { 152 | const bel: u8 = ascii.bel; 153 | self.writeFn(@as(*const [1]u8, @ptrCast(&bel))); // u8 to single-item slice 154 | } 155 | }, 156 | } 157 | if (self.got_line) { 158 | if (self.cmdbuf_len > 0) { 159 | try self.execline(self.cmdbuf[0..self.cmdbuf_len]); 160 | } 161 | self.cmdbuf_len = 0; 162 | self.cmdbuf[self.cmdbuf_len] = 0; 163 | try self.prompt(); 164 | self.got_line = false; 165 | } 166 | } 167 | } 168 | }; 169 | } 170 | --------------------------------------------------------------------------------