I've been pretty excited by the record builder changes that got added last year, and I've been seeing some discussion around us having a good example of using them. I thought of all of my experience using Clap in Rust, since their #[derive(Parser)] user experience is great because you can "Parse, not Validate". I spent the last week cracking out a library to do type-safe CLI parser generation using the record builder pattern and I'm really happy with the results!
Some of the nice features we get:
: <- can do for themBelow is a small example of using the lib, and here's the repo if you're interested: https://github.com/smores56/weaver
expect
subcommandParser =
cliBuilder {
d: <- numOption { name: Short "d", help: "A required number." },
f: <- maybeNumOption { name: Short "f", help: "An optional number." },
}
|> finishSubcommand { name: "sub", description: "A specific action to take.", mapper: Sub }
{ parser, config: _ } =
cliBuilder {
alpha: <- numOption { name: Short "a", help: "Set the alpha level." },
beta: <- flagOption { name: Both "b" "beta" },
xyz: <- strOption { name: Long "xyz" },
verbosity: <- occurrenceOption { name: Both "v" "verbose" },
sc: <- subcommandField [subcommandParser],
}
|> finishCli { name: "app", version: "v0.0.1", authors: ["Some One <some.one@mail.com>"] }
|> assertCliIsValid # crash immediately on unrecoverable issues, e.g. empty flag names
out = parser ["app", "-a", "123", "-b", "--xyz", "some_text", "-vvvv", "sub", "-d", "456"]
out
== SuccessfullyParsed {
alpha: 123,
beta: Bool.true,
xyz: "some_text",
verbosity: 4,
sc: Ok (Sub { d: 456, f: Err NoValue }),
}
As mentioned in the README, I've got the basics mostly sorted, but I've got to go through and do a good bit more testing than is currently being done. Then, the next step is documentation, and once this feels stable, I'll start doing GitHub releases so you can use this library without having to download it first :sweat_smile:
I have to go to bed now, but I'd love to discuss any ideas you have for "must-have" features or ergonomic fixes you'd make to the API or internals. Feel free to ping me!
Awesome!
Clever use of the record builder syntax!
wow, fantastic!!! :heart_eyes:
I love that you got the automatic help text generation in there...being able to support that was one of the original motivating reasons behind having the record builder syntax, and it's super exciting to see the first-ever example of it in action! :grinning_face_with_smiling_eyes:
what do you think of putting the different argument styles into a separate module? e.g. d: <- Arg.num { ... } instead of d: <- numOption { ... }
Yeah, that seems like a good way to compactify the method calls. I was also trying to do something terse for the argument names with default records, but I realized that default values in type definitions universalize, meaning two function calls to the same function can't omit different fields from the same record definition. Is that intended behavior?
I'm still a beginner, so my apologies. I could be misunderstanding, but if I'm not, this isn't intended behaviour. It was recently discussed here.
I figured, thanks for the link!
Nice! Looking forward to using this :smiley:
Weaver has now been released on GitHub, so you can use it in your applications now! The example in the README shows a working example of using Weaver from the net. I've documented all of the public features for now, and I've released said docs on GitHub pages here using the GitHub workflows I stole borrowed from @Luke Boswell 's roc-json library.
Since you last saw it, I made the change that @Richard Feldman suggested to name functions assuming they'll be called as module members, so numOption is now Opt.num, and strListParam is now Param.strList, and so on. (I didn't call it Arg.num because that overlaps with basic-cli's Arg module and we can't currently rename imports, though I know that's in progress).
I probably won't be going nearly as fast in developing it now that's it's in an MVP state, but I'll still be working on it after my vacation comes and goes this weekend. Happy Weaving!
Repo: https://github.com/smores56/weaver
Docs: https://smores56.github.io/weaver/Cli/
Download URL: https://github.com/smores56/weaver/releases/download/0.1.0/MnJi0GTNzOI77qDnH99iuBNsM5ZKnc-gZTLFj7sIdqo.tar.br
Weaver has now reached v0.2.0! The main changes are:
U64At the suggestion of Richard Feldman, I wrote an article that explains why I wrote Weaver, which should be ready to go. However, before it's properly disseminated, I'd love to get a pair of eyes that aren't mine on it just to double check it. Then we can get more Roc propaganda on HackerNews!
Nice.
Noticed some issues with the article on my phone using chrome.
Screenshot_20240505-112812_Chrome.jpg
I cant read or scroll to see on the right.
Yeah, seems that making it narrower on my computer wasn't narrow enough
Can you put the Arg.List! inline in the when statement? If that doesnt work I might need to look into the unrwapping for that part.
when Cli.parseOrDisplayMessage cli (Arg.list!) is
Only if you want to do it like that
Can you put the Arg.list! inline?
Done! And your mobile should work better, please let me know if you've still got issues
I tested that on the basic.roc example from the Weaver repo and it worked
Looks good on mobile now :+1:
Btw, I've been using Weaver on something I've been working on and it's super awesome. :ok: It just works nicely and haven't had any issues. Looking forward to trying out v2.
Nice article, I'm looking forward to use Weaver for my next Roc project!
I'm confused about the Python example though, that syntax is very unfamiliar to me, is it real code or just hypothetical?
Using the Python stdlib, I would write something like:
import argparse
parser = argparse.ArgumentParser(description='my-app')
parser.add_argument('-f', '--force', action='store_true')
parser.add_argument('file', type=str)
parser.add_argument('amount', type=int)
args = parser.parse_args()
print("File:", args.file)
print("Amount:", args.amount)
print("Force:", args.force)
The type arguments are functions that are run on the corresponding argument to parse the string to a specified type, which makes it seem like there could be some type inference, but the args variable has the Namespace type, and all the properties have the Any type.
great article! I don't really understand the builder syntax (and applicative in general lol), so I'm even more impressed by weaver
i believe the type for allColors here would be a List [Red, Green, Custom U64]?
image.png
ziutech said:
I don't really understand the builder syntax (and applicative in general lol)
@Agus Zubiaga and I have talked about a way to make it simpler from both a types and syntax perspective, so this is encouraging feedback to hear! :smiley:
I'll write that design up, maybe in the next week or two
Hannes said:
I'm confused about the Python example though, that syntax is very unfamiliar to me, is it real code or just hypothetical?
It's hypothetical. I tried to clarify that a bit in the article by saying it's a "simplified example" and "if I used the stdlib, it'd be nicer, but only because dynamic type system"
Okay @Richard Feldman it's ready to send out (link), at your leisure. Unless you'd rather I send it out?
go for it! :smiley:
thanks for writing it up!
Not a social media person, so uh... where should I send it? I don't have a twitter or an HN account.
I'll put it on Reddit for starters
https://www.reddit.com/r/programming/comments/1ckxqyi/announcing_weaver_an_ergonomic_cli_parsing/
https://www.reddit.com/r/functionalprogramming/comments/1ckxnub/announcing_weaver_an_ergonomic_cli_parsing/
I posted it to these two subreddits to get a large audience, and a small but more-appropriate audience. Feel free to upvote if you want to!
very nice, will do!
thanks for writing it up, and on a personal note, thank you for the kind words in it :hug:
I've submitted it to Lobsters, I'm not sure if Lobsters penalises upvotes from a direct link like HN does, so if you want to upvote there, go to https://lobste.rs/recent and search for "Weaver" :)
Weaver has been updated to work with current Roc: https://github.com/smores56/weaver/releases/tag/0.4.0
You should be able to use it for all your purity inference needs once that rolls out.
download here: https://github.com/smores56/weaver/releases/download/0.5.1/nqyqbOkpECWgDUMbY-rG9ug883TVbOimHZFHek-bQeI.tar.br
This release brings OS-aware arg parsing to Weaver!
Most operating systems don't guarantee UTF-8 encoding of arguments, and Windows doesn't even use bytes, but U16 codepoints. To handle this for you, Weaver and basic-cli have provided OS-aware arguments that are U8 lists for Unix systems and U16 lists for Windows systems. You'll get these from main! in basic-cli, and you can call Weaver almost the same way you used to, but you'll need to pass one extra argument to Cli.parse_or_display_message:
main! = \args ->
data =
Cli.parse_or_display_message cli_parser args Arg.to_os_raw
|> try Result.onErr! \message ->
try Stdout.line! message
Err (Exit 1 "")
try Stdout.line! "Successfully parsed! Here's what I got:"
try Stdout.line! ""
try Stdout.line! (Inspect.toStr data)
Ok {}
You'll see that there's an extra argument, Arg.to_os_raw. Cli.parse_or_display_message now takes a generic List arg for the args, and that third function is something that converts such a generic arg to a [Unix (List U8), Windows (List U16)]. Arg.to_os_raw is provided by basic-cli in the Arg module, but you could use any such function if you have a different arg type.
This new argument should be temporary, as with the accepted static dispatch proposal we should be able to infer a method to do this conversion.
NOTE:
basic-clionly just merged the PR for OS-awareArgs, so you'll need to wait a few days for you to upgrade to using this Weaver version untilbasic-cliat least makes a pre-release.
CAUTION:
StrandNum *arguments can now fail decoding if the provided command line args are not valid UTF-8.
Opt and Param types: Arg and List U8 (bytes)Now that Args are represented in a variable encoding, there are two new groups of methods available for both Opt and Param:
Opt.arg requires an Arg argumentOpt.maybe_arg takes zero or one ArgsOpt.arg_list takes zero or more ArgsOpt.bytes requires a List U8 argument (which it gets by infallibly converting an Arg to the underlying bytes)Opt.maybe_bytes takes zero or one List U8sOpt.bytes_list takes zero or more List U8sThe same APIs are available for Param. These arg types are a great way to support reading file names might not be UTF-8 encoded.
Great job Luke!
Yes, thanks to @Luke Boswell for implementing the basic-cli side of things, and to the team for helping design this!
Thanks for your work on this Sam!
Last updated: Nov 28 2025 at 12:16 UTC