grumble turns a Go CLI into a program with a shell built into it
A powerful modern CLI and SHELL
At a glance
- What is it?
- grumble is a Go library that gives a command line application an interactive shell when it is started without arguments, so the same binary serves as both a tool and a REPL. It handles flags, positional arguments, completion and multiline input, and it can be reached over TCP by handing it a readline instance.
- Who is it for?
- Adopt grumble if you are writing a Go program that should work both as a one-shot command and as an interactive session, because the shell starts itself when no command arguments are given and the same command set is available in both modes. Do not adopt it if your program needs to be a pure one-shot tool with no interactive surface, because the interactive shell is the feature and it comes with the readline dependency and its own quirks.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The idea: one binary, two modes
grumble is described as a powerful modern CLI and shell, and the README explains its position in one paragraph.
There are a handful of powerful Go CLI libraries available, and it names spf13/cobra and urfave/cli. However, sometimes an integrated shell interface is a great and useful extension for the actual application. This library offers a simple API to create powerful CLI applications and automatically starts an integrated interactive shell if the application is started without any command arguments.
That last clause is the whole design. A grumble program is a CLI that also happens to be a REPL. Run it with a command and it does that command and exits, run it with no arguments and it drops you into an interactive shell where the same commands are available.
For a server tool, a database client, a deployment utility, that is a much better shape than having two separate binaries or writing your own prompt loop. The user gets the scriptable path for automation and the interactive path for exploration, and you maintain one command tree.
The library builds on ideas from the ishell library, which the credits name, and the implementation is by desertbit, who also maintains several of the supporting packages in the go.mod: closer, columnize, go-shlex and readline. The last one is the interactive line editor underneath, and grumble delegates the terminal handling to it, which is why the readline package also provides the remote access features the README demonstrates.
The primary language is Go, the module path is github.com/desertbit/grumble, it requires Go 1.25.0, and the licence is MIT. The last push was on 2026-04-07, the repository is not archived, and there are no GitHub releases, so the version you depend on is a commit or a module version rather than a tag.
Building the app, and registering a command
The API has three pieces: an app created from a Config, commands added to it, and a run call.
The app is created like this:
var app = grumble.New(&grumble.Config{
Name: "app",
Description: "short app description",
Flags: func(f *grumble.Flags) {
f.String("d", "directory", "DEFAULT", "set an alternative directory path")
f.Bool("v", "verbose", false, "enable verbose mode")
},
})The Config carries the application name and description plus a Flags function that declares global flags. Note the shape of each flag declaration: a short name, a long name, a default value and a usage string. A string flag takes a default string and a bool flag takes a bool default, and the pairing of short and long forms is per flag rather than global, so you can use a single letter where it makes sense.
Commands are then registered on the app, and sub commands are supported as well:
app.AddCommand(&grumble.Command{
Name: "daemon",
Help: "run the daemon",
Aliases: []string{"run"},
Flags: func(f *grumble.Flags) {
f.Duration("t", "timeout", time.Second, "timeout duration")
},
Args: func(a *grumble.Args) {
a.String("service", "which service to start", grumble.Default("server"))
},
Run: func(c *grumble.Context) error {
// Parent Flags.
c.App.Println("directory:", c.Flags.String("directory"))
c.App.Println("verbose:", c.Flags.Bool("verbose"))
// Flags.
c.App.Println("timeout:", c.Flags.Duration("timeout"))
// Args.
c.App.Println("service:", c.Args.String("service"))
return nil
},
})The Command struct has a name, a help string and an alias list, so the same command can be reached by a short name. It has its own Flags function, which is how a command gets flags that its parent does not have, and its own Args function, which declares positional arguments with a name, a description and a default value.
The Run callback receives a Context and that context separates the two scopes. c.Flags is the command's own flags, c.Args is the positional arguments, and c.App is the application, which is what you use to print output through the app rather than straight to stdout. That last detail is what lets the app's output be redirected or decorated when the program runs as a shell.
Finally the program runs:
err := app.Run()or the builtin grumble.Main function handles errors automatically:
func main() {
grumble.Main(app)
}The difference matters in an interactive context. app.Run() gives you the error to handle yourself, while grumble.Main presumably decides what to do with it, and in a shell where the user can run another command after a failure, that decision is not trivial.
Flag and argument parsing details that bite
The Flags section of the README is short and covers four behaviours that are worth knowing before you ship a CLI with them.
Flags can be passed in two ways, cmd --flag value or cmd --flag=value. There are then some exceptions and additions. A bool flag offers a third option that does not require a value, so cmd --boolflag works on its own. A string flag with cmd --stringflag="some test string" leads to the value some test string, because the double quotes are stripped from the value.
That last point is a real behaviour rather than a documentation nicety. If your flag value legitimately contains a double quote, the stripping will surprise you, and if your value is meant to include surrounding quotes as part of the data, they will not survive.
The separate flags and args section solves a specific problem: passing a flag-like value as a positional argument. If you need that, you use a double dash:
The double dash separates the library's own flag parsing from everything after it, so --myPositionalArg is taken as an argument rather than rejected as an unknown flag.
The double dash separates the library's own flag parsing from everything after it, so --myPositionalArg is taken as an argument rather than rejected as an unknown flag. This is the same convention that getopt and most Unix tools use, and grumble is following it rather than inventing one.
The shell also supports multiline input out of the box. The syntax uses the prompt and continuation markers:
>>> This is \
... a multi line \
... commandThe trailing backslash continues the line and the continuation marker changes, which means a multi-line command can be pasted or typed in a shell and treated as one command. For a tool whose commands take arguments like SQL statements or shell fragments, that is the difference between a usable REPL and an unusable one.
The repository has a completer.go and a functions.go at the top level, alongside args.go, argmap.go, argopt.go, flagmap.go and flags.go, so completion and argument handling are separate concerns in the code rather than folded into the command type.
Remote access, and the interrupt handler you have to override
The most unusual feature is remote shell access, and it comes from the readline package rather than from grumble's own code.
By calling RunWithReadline() rather than Run(), you can pass an instance of readline.Instance. The README points at one interesting usage, remote access to your shell:
handleFunc := func(rl *readline.Instance) {
var app = grumble.New(&grumble.Config{
// override default interrupt handler to avoid remote shutdown
InterruptHandler: func(a *grumble.App, count int) {
// do nothing
},
// your usual grumble configuration
})
// add commands
app.RunWithReadline(rl)
}
cfg := &readline.Config{}
readline.ListenRemote("tcp", ":5555", cfg, handleFunc)The structure is: listen on a TCP port, and for each connection construct a readline instance and a grumble app bound to it, then run the app with that instance.
The interrupt handler override is the part that shows the author has thought about this. By default, pressing Ctrl-C in a grumble app presumably exits the app, and in a remote session that would kill the server process rather than just cancelling the current input. The override makes the interrupt do nothing, which is what you want when the person at the other end of the socket is not supposed to be able to take the service down with a keystroke.
On the client side, the client uses readline's built in DialRemote function:
if err := readline.DialRemote("tcp", ":5555"); err != nil {
fmt.Errorf("An error occurred: %s \n", err.Error())
}Worth flagging for anyone considering this: the listen address is hardcoded to port 5555 on all interfaces in the example, with no authentication shown. The README does not describe any access control, so a deployment would need to add it, and the sample as written listens on a port that a scanner will find. That is a property of the example rather than a flaw in the library, but it is the thing to fix first if you copy it.
The remote feature is also the reason grumble depends on desertbit/readline at all, and the reason the library's own Windows limitation matters more than it otherwise would.
Where grumble is the wrong choice
Three limits, and the first is about the stability commitment.
The first is that the API can change under a minor version. The README states it plainly: we do not guarantee 100% backwards compatibility between minor versions. However, the API is mostly stable and should not change much. The hedge is in the original, and it is a real constraint for a library that other people depend on. If you build a CLI on grumble, you are accepting that a dependency bump from 1.x to 1.y may need a source change, and since the repository has no GitHub releases you have no changelog to read before bumping.
The second is Windows. The known issues section lists one entry: Windows unicode not fully supported, with an issue link. That is a single line in the README and it is the kind of line that decides whether a library is usable for you. A CLI on Windows that cannot render unicode correctly will mangle any output containing non-ASCII characters, and for a tool that prints logs, file names or database values that is a lot of mangling. There is also no statement about which Windows versions or terminals are affected, so the practical approach is to try it.
The third is the scope of what you get. grumble gives you flags, arguments, command registration, completion, an interactive shell and multiline input. It does not give you configuration files, structured logging, a plugin system, shell completion script generation, or output formatting. The README points at two additional useful packages for the gaps, AlecAivazis/survey for interactive prompts and tj/go-spin for a spinner, and the go.mod already includes survey.v1 as a direct dependency, so the interactive prompt library is in the tree even though it is not part of the grumble API.
The two projects listed as using grumble are both by the same author, grml, a build automation tool written in Go, and orbit, an RPC-like networking backend written in Go. That tells you the library is exercised on real tools, and it also tells you the reference implementations are personal projects rather than a spread of independent adopters.
The alternative approach is cobra or urfave/cli for the CLI part, which is what the README itself names first, and adding an interactive shell yourself with readline directly if you need one. The difference in approach is that grumble bundles the two and gives you one coherent app object with commands available in both modes, whereas cobra gives you a much larger and better-known ecosystem and no shell, and readline on its own gives you the line editor with none of the command dispatch. If your tool is genuinely dual-mode, grumble saves you the wiring. If it is a one-shot tool with a rare interactive need, cobra plus readline is the more conventional choice.
Editorial conclusion
Adopt grumble if you are writing a Go program that should work both as a one-shot command and as an interactive session, because the shell starts itself when no command arguments are given and the same command set is available in both modes. Do not adopt it if your program needs to be a pure one-shot tool with no interactive surface, because the interactive shell is the feature and it comes with the readline dependency and its own quirks. Verify first that the API you need exists in the version you pin, since the README states plainly that backwards compatibility is not guaranteed between minor versions, that you are on a platform where readline behaves, since Windows unicode is not fully supported and is listed as a known issue, and that your terminal handles the multiline continuation syntax, where a trailing backslash continues the line.
Frequently asked questions
What is grumble and when does the interactive shell start?
It is a Go library for building CLI applications, and it automatically starts an integrated interactive shell if the application is started without any command arguments. The same commands are therefore available both as a scriptable one-shot invocation and inside a REPL.
How do I add a command with its own flags and arguments in grumble?
Call app.AddCommand with a Command that has a Name, a Help string, an Aliases list, a Flags function declaring command-level flags, an Args function declaring positional arguments with defaults, and a Run callback that receives a Context. In the callback, c.Flags holds the command flags, c.Args the arguments, and c.App is the application you print through.
How do I pass a flag-like value as a positional argument in grumble?
Use a double dash to separate the library's flag parsing from what follows, for example >>> command --flag1=something -- --myPositionalArg. Flags themselves accept both cmd --flag value and cmd --flag=value, and a bool flag can be given without a value.
Can grumble provide remote shell access?
Yes. Call RunWithReadline instead of Run to bind the app to a readline instance, then use readline.ListenRemote on a TCP port to accept connections and readline.DialRemote on the client side. The README's example overrides the interrupt handler so a remote Ctrl-C does not shut the server down.
Is grumble's API stable between minor versions?
Not guaranteed. The README states that 100% backwards compatibility between minor versions is not guaranteed, while adding that the API is mostly stable and should not change much. There are no GitHub releases, so there is no changelog to consult before upgrading.
What are the known limitations of grumble?
The README lists one known issue, that Windows unicode is not fully supported, with a link to the issue. The module requires Go 1.25.0, and the licence is MIT.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/desertbit-grumble)Was this useful?