Something went wrong. Try again.
A small configuration language for Zig: ZON with let, if and templates over a context, evaluated to a struct, always finishing.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! `struthio`, the command: `struthio fmt`, which does to struthio files what//! `zig fmt` does to Zig, and `struthio lsp`, a language server for editors.//!//! ```console//! $ struthio fmt config.struthio conf/ # rewrite in place, name what changed//! $ struthio fmt --check . # name what would change, and fail//! $ struthio fmt --stdin < a.struthio # formatted to stdout, for editors//! $ struthio lsp # the Language Server Protocol, on stdio//! ```//!//! A directory is searched for `*.struthio` files, leaving out anything//! whose name starts with `.`, which keeps it out of `.git` and//! `.zig-cache`.
const std = @import("std");const Io = std.Io;const Allocator = std.mem.Allocator;
const struthio = @import("struthio");const lsp = @import("lsp.zig");
const usage = \\usage: struthio fmt [--check] [--stdin] [FILE | DIR]... \\ struthio lsp \\ \\fmt: format struthio programs, as `zig fmt` formats Zig. \\ \\ FILE, DIR rewrite each file, and each *.struthio file under each \\ directory, in place, and print the names of those changed \\ --check change nothing: print the names of the files that are not \\ formatted, and exit with 1 if there are any \\ --stdin format standard input to standard output; with --check, \\ print nothing and exit with 1 if it is not formatted \\ \\lsp: be a language server, speaking the Language Server Protocol on \\standard input and output, for an editor to start. \\;
/// Larger than any configuration file has a reason to be.const max_file_size = 16 << 20;
const Fmt = struct { io: Io, gpa: Allocator, stdout: *Io.Writer, stderr: *Io.Writer, check: bool, /// Some file was not formatted, with `--check`, or could not be. failed: bool = false,
/// Format the file at `path`, in `dir`; `display` is how to name it. fn file(self: *Fmt, dir: Io.Dir, path: []const u8, display: []const u8) !void { const source = dir.readFileAlloc(self.io, path, self.gpa, .limited(max_file_size)) catch |err| { try self.stderr.print("{s}: {t}\n", .{ display, err }); self.failed = true; return; }; defer self.gpa.free(source); const formatted = self.format(source, display) orelse return; defer self.gpa.free(formatted); if (std.mem.eql(u8, source, formatted)) return;
try self.stdout.print("{s}\n", .{display}); if (self.check) { self.failed = true; return; } self.replace(dir, path, formatted) catch |err| { try self.stderr.print("{s}: {t}\n", .{ display, err }); self.failed = true; }; }
/// Write `contents` over `path` all at once, keeping its permissions, so /// that nothing ever sees it half written. fn replace(self: *Fmt, dir: Io.Dir, path: []const u8, contents: []const u8) !void { const stat = try dir.statFile(self.io, path, .{}); var atomic = try dir.createFileAtomic(self.io, path, .{ .permissions = stat.permissions, .replace = true }); defer atomic.deinit(self.io); try atomic.file.writeStreamingAll(self.io, contents); try atomic.replace(self.io); }
/// `source` formatted, or null after saying why not. fn format(self: *Fmt, source: []const u8, display: []const u8) ?[]u8 { var diag: struthio.Diagnostic = .{}; return struthio.format(self.gpa, source, .{}, &diag) catch |err| { switch (err) { error.Syntax => self.stderr.print("{s}:{d}:{d}: error: {s}\n", .{ display, diag.pos.line, diag.pos.column, diag.message(), }) catch {}, error.OutOfMemory => self.stderr.print("{s}: out of memory\n", .{display}) catch {}, } self.failed = true; return null; }; }
fn directory(self: *Fmt, path: []const u8) !void { var dir = try Io.Dir.cwd().openDir(self.io, path, .{ .iterate = true }); defer dir.close(self.io); var walker = try dir.walkSelectively(self.gpa); defer walker.deinit(); while (try walker.next(self.io)) |entry| { if (std.mem.startsWith(u8, entry.basename, ".")) continue; switch (entry.kind) { .directory => try walker.enter(self.io, entry), .file => if (std.mem.endsWith(u8, entry.basename, ".struthio")) { const display = try std.fs.path.join(self.gpa, &.{ path, entry.path }); defer self.gpa.free(display); try self.file(entry.dir, entry.basename, display); }, else => {}, } } }
fn stdin(self: *Fmt) !void { var buf: [4096]u8 = undefined; var reader = Io.File.stdin().reader(self.io, &buf); const source = try reader.interface.allocRemaining(self.gpa, .limited(max_file_size)); defer self.gpa.free(source); const formatted = self.format(source, "<stdin>") orelse return; defer self.gpa.free(formatted); if (self.check) { if (!std.mem.eql(u8, source, formatted)) self.failed = true; return; } try self.stdout.writeAll(formatted); }};
pub fn main(init: std.process.Init) !void { const io = init.io; const gpa = init.gpa;
var stdout_buf: [4096]u8 = undefined; var stdout_writer = Io.File.stdout().writer(io, &stdout_buf); var stderr_buf: [1024]u8 = undefined; var stderr_writer = Io.File.stderr().writer(io, &stderr_buf); const stderr = &stderr_writer.interface;
var args: std.process.Args.Iterator = .init(init.minimal.args); _ = args.skip(); const command = args.next() orelse { try stderr.writeAll(usage); try stderr.flush(); std.process.exit(2); }; if (std.mem.eql(u8, command, "-h") or std.mem.eql(u8, command, "--help") or std.mem.eql(u8, command, "help")) { try stdout_writer.interface.writeAll(usage); try stdout_writer.interface.flush(); return; } if (std.mem.eql(u8, command, "lsp")) { if (args.next()) |arg| { try stderr.print("struthio lsp: unexpected argument '{s}'\n\n{s}", .{ arg, usage }); try stderr.flush(); std.process.exit(2); } var stdin_buf: [4096]u8 = undefined; var stdin_reader = Io.File.stdin().reader(io, &stdin_buf); var server: lsp.Server = .init(gpa, io, &stdout_writer.interface); defer server.deinit(); const status = try server.run(&stdin_reader.interface); if (status != 0) std.process.exit(status); return; } if (!std.mem.eql(u8, command, "fmt")) { try stderr.print("struthio: unknown command '{s}'\n\n{s}", .{ command, usage }); try stderr.flush(); std.process.exit(2); }
var check = false; var from_stdin = false; var paths: std.ArrayList([]const u8) = .empty; defer paths.deinit(gpa); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--check")) { check = true; } else if (std.mem.eql(u8, arg, "--stdin")) { from_stdin = true; } else if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) { try stdout_writer.interface.writeAll(usage); try stdout_writer.interface.flush(); return; } else if (std.mem.startsWith(u8, arg, "-") and arg.len > 1) { try stderr.print("struthio fmt: unknown option '{s}'\n\n{s}", .{ arg, usage }); try stderr.flush(); std.process.exit(2); } else { try paths.append(gpa, arg); } } if (from_stdin == (paths.items.len != 0)) { try stderr.writeAll(if (from_stdin) "struthio fmt: --stdin takes no files\n" else "struthio fmt: no files to format\n"); try stderr.writeAll(usage); try stderr.flush(); std.process.exit(2); }
var f: Fmt = .{ .io = io, .gpa = gpa, .stdout = &stdout_writer.interface, .stderr = stderr, .check = check, }; if (from_stdin) { try f.stdin(); } else for (paths.items) |path| { const stat = Io.Dir.cwd().statFile(io, path, .{}) catch |err| { try stderr.print("{s}: {t}\n", .{ path, err }); f.failed = true; continue; }; if (stat.kind == .directory) { f.directory(path) catch |err| { try stderr.print("{s}: {t}\n", .{ path, err }); f.failed = true; }; } else { try f.file(Io.Dir.cwd(), path, path); } } try f.stdout.flush(); try stderr.flush(); if (f.failed) std.process.exit(1);}
test { _ = lsp;}