Skip to content

Instantly share code, notes, and snippets.

@joshtrujillo
Created February 6, 2025 17:42
Show Gist options
  • Save joshtrujillo/c981f824bc48591fbd177a1514c35ef5 to your computer and use it in GitHub Desktop.
Save joshtrujillo/c981f824bc48591fbd177a1514c35ef5 to your computer and use it in GitHub Desktop.
Super simple shell written in zig.
// zig-shell.zig
// Simple shell interface written in zig.
// @author Josh Trujillo
const std = @import("std");
const MAX_LINE = 80;
pub fn main() !u8 {
const writer = std.io.getStdOut().writer();
const reader = std.io.getStdIn().reader();
try shellLoop(reader, writer);
return 0;
}
/// Creates the null terminated many item pointer needed by execvpeZ()
/// for environmental variables.
fn buildEnvArray(allocator: std.mem.Allocator) ![*:null]const ?[*:0]const u8 {
var env_list = std.ArrayList(?[*:0]const u8).init(allocator);
defer env_list.deinit();
var env_map = try std.process.getEnvMap(allocator);
defer env_map.deinit();
var iter = env_map.iterator();
while (iter.next()) |entry| {
// Create null terminated key=value pairs
const full_entry = try std.fmt.allocPrintZ(allocator, "{s}={s}", .{ entry.key_ptr.*, entry.value_ptr.* });
try env_list.append(full_entry.ptr);
}
try env_list.append(null); // Null-terminate the array
return try env_list.toOwnedSliceSentinel(null);
}
/// Main shell loop which exits when the user enters "exit"
fn shellLoop(reader: std.fs.File.Reader, writer: std.fs.File.Writer) !void {
var args: [MAX_LINE / 2][MAX_LINE:0]u8 = undefined;
var args_ptrs: [MAX_LINE / 2:null]?[*:0]u8 = undefined;
var buffer: [MAX_LINE]u8 = undefined;
var last_command: [MAX_LINE]u8 = undefined;
var has_history: bool = false;
// Allocator for environment variables
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
// ArenaAllocator frees everything at once
defer arena.deinit();
const allocator = arena.allocator();
// Prompt information
const user = std.process.getEnvVarOwned(allocator, "USER") catch "";
const pwd = std.process.getEnvVarOwned(allocator, "PWD") catch "";
shell: while (true) {
try writer.print("{s}@zig-shell:{s} $ ", .{ user, pwd });
// Read command
const command = try reader.readUntilDelimiterOrEof(buffer[0..], '\n') orelse {
try writer.print("\n", .{});
return;
};
if (command.len == 0) continue;
// Handle !! for previous command
var command_to_execute = command;
if (std.mem.eql(u8, command, "!!")) {
if (!has_history) {
try writer.print("No command history\n", .{});
continue :shell;
}
command_to_execute = &last_command;
try writer.print("Executing previous command: {s}\n", .{last_command});
} else { // Clear and store last_command
@memset(&last_command, 0);
std.mem.copyForwards(u8, &last_command, command_to_execute);
has_history = true;
}
// Split command into tokens
var tokens = std.mem.splitSequence(u8, command_to_execute, " ");
var i: usize = 0;
while (tokens.next()) |token| : (i += 1) {
// Handle exit case
if (std.mem.eql(u8, token, "exit")) {
return;
} else if (std.mem.eql(u8, token, "!!")) {}
std.mem.copyForwards(u8, &args[i], token);
args[i][token.len] = 0;
args_ptrs[i] = &args[i];
}
args_ptrs[i] = null; // Null termination
// fork call
const pid = try std.posix.fork();
if (pid == 0) { // Child process
const envptr = try buildEnvArray(allocator);
const result = std.posix.execvpeZ(args_ptrs[0].?, &args_ptrs, envptr);
switch (result) {
error.FileNotFound => try writer.print("Command not found: {s}\n", .{args_ptrs[0].?}),
error.AccessDenied => try writer.print("Permission Denied: {s}\n", .{args_ptrs[0].?}),
else => try writer.print("Execution error: {}\n", .{result}),
}
std.posix.exit(127);
} else {
// Parent process
_ = std.posix.waitpid(pid, 0);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment