CLI tool
mattn/go-shellwords avatar
mattn/go-shellwords

go-shellwords splits a command line the way a shell would, without running it

Parse line as shell words

578 stars89 forksGoMIT

At a glance

What is it?
mattn's go-shellwords is a small Go package that takes a line of text and returns the words a shell would have seen, with optional handling for environment assignments, environment expansion, backtick command substitution and comments. It parses, it does not execute, and the flags that turn on substitution are the ones worth reading before you point it at untrusted input.
Who is it for?
Adopt go-shellwords when your program receives a command line as one string, from a configuration file, an environment variable or a container entrypoint, and you need the same word splitting a shell would give you. Do not adopt it to run commands, since it does not execute anything except the backtick substitution you explicitly enable.
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?
Yes. The repository last received commits 22 days 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

One function for a problem every tool has

The package does one thing, and the README says it in three words: parse line as shell words.

The problem it solves comes up whenever a program receives a command line that is a single string. A container image whose entrypoint is one string, a configuration value that holds a command, an environment variable that a supervisor runs, a service definition that stores a command line as text. In all of those cases the program needs the list of arguments that a shell would have produced, and the shell is not there to produce them.

The library is `github.com/mattn/go-shellwords`, it is MIT licensed, and the module file declares `go 1.13`, so it builds on any reasonably current Go toolchain. The single tagged release listed is v1.0.15 from 2026-09-12, and the last push was on 2026-10-01. The README credits the work as based on the cpan module Parse::CommandLine, and the author is Yasuhiro Matsumoto, known as mattn.

The repository layout is small in the way a small library should be. The parser is in `shellwords.go`, with `util_posix.go` and `util_windows.go` for the platform differences, and the test suite is split across `shellwords_test.go`, `shellwords_security_test.go` and `shellwords_benchmark_test.go`, with a `go.test.sh` script and an `_example/` directory.

That a security test file exists at all is a hint about the direction of travel, and worth a look before you rely on the package with hostile input.

Parse returns the words and an error

The simplest call splits a line and returns the arguments:

go
args, err := shellwords.Parse("./foo --bar=baz")
// args should be ["./foo", "--bar=baz"]

The signature is two return values, the words and an error, so the only thing a caller has to decide is what to do when the line cannot be parsed, which happens with unbalanced quotes.

The two values in the example are worth noticing for a second reason. The first is the program name and the second is a flag with an equals sign in it, and the equals sign stays inside the single word rather than splitting it. That is the behaviour you want from a shell and it is where a naive `strings.Split` on spaces falls down, along with quoting, which the parser handles because a shell would.

Everything else in the README is a variation on this call.

ParseWithEnvs separates the KEY=value prefix

The second entry point handles the case where the line begins with environment assignments, which is a shell feature that looks like one word but is not:

go
envs, args, err := shellwords.ParseWithEnvs("FOO=foo BAR=baz ./foo --bar=baz")
// envs should be ["FOO=foo", "BAR=baz"]
// args should be ["./foo", "--bar=baz"]

The result is three values, the environment assignments, the arguments and the error, and the two slices are cleanly separated so the caller can set the variables and then decide what to do with the command.

This is the shape you want when the input is a container command line or a supervisor definition, where the assignments are meant for the child process and the rest is the command itself. The alternative, parsing with `Parse` and then looking for words that contain an equals sign, gets the split wrong as soon as an argument legitimately contains one.

A parser object turns the optional behaviours on

The remaining features are reached through a parser object, and each one is a field you set before parsing.

Environment expansion is the first. The README sets the variable in the process environment, enables the option, and parses a line that refers to it:

go
os.Setenv("FOO", "bar")
p := shellwords.NewParser()
p.ParseEnv = true
args, err := p.Parse("./foo $FOO")
// args should be ["./foo", "bar"]

So with `ParseEnv` the parser expands `$FOO` from the environment of the process doing the parsing, and the value ends up in the argument list as a literal word.

The design point is that the defaults are conservative. A plain `Parse` call does not expand variables, does not run backticks and does not strip comments, and you opt in to each behaviour. The README also shows that some of these are settable as package level variables, since it demonstrates both `p.ParseBacktick = true` on a parser and `shellwords.ParseBacktick = true` on the package.

That package level form is worth a moment's thought in a larger program, because a global setting changes the behaviour of every other `Parse` call in the process, while the parser object keeps the change local.

Backtick substitution is the feature to be careful with

With `ParseBacktick` enabled, the parser evaluates a backtick command and substitutes its output as a word:

go
p := shellwords.NewParser()
p.ParseBacktick = true
args, err := p.Parse("./foo `echo $SHELL`")
// args should be ["./foo", "/bin/bash"]

This is the one option that executes something. Everything else in the package is text processing, but this turns a string from your input into a subprocess, with whatever the current user can run.

The security test file in the repository is the place to look for the maintainer's own view of that risk, and the fact that the option is off by default is the important part. A tool that parses a configuration value should not silently run commands in it, and the package's defaults reflect that. If you turn the option on for a trusted input, keep the trust assumption explicit in the code that calls it.

For untrusted input, the safer path is to leave the substitution off and treat the parsed words as data.

Comments and separators, and the limits of the model

The last two options control what counts as a separator.

Comments are handled with `ParseComment`, and a `#` starts a comment that runs to the end of the line:

go
p := shellwords.NewParser()
p.ParseComment = true
args, err := p.Parse("./foo # comment")
// args should be ["./foo"]

And the separator set itself can be narrowed with `SetExcludeSeparators`:

go
p := shellwords.NewParser()
p.SetExcludeSeparators('\t', ';')

The excluded characters stop splitting words, which is how a parser models a line where semicolons and tabs are part of an argument rather than separators. It is a small API and it is the kind of thing you reach for when the input format is a shell script fragment rather than a command line.

The model has boundaries worth stating. The package parses a line, it does not run a program, it does not implement redirections, pipelines, globbing or job control, and the environment expansion it performs is the simple `$NAME` form rather than a full shell. It also has platform specific files, `util_posix.go` and `util_windows.go`, so the behaviour is not identical everywhere.

The alternative approach for most Go programs is not to parse at all: take the arguments as a slice, and use `os/exec` with an explicit argument list. That is safer, because nothing is ever interpreted. The case for go-shellwords is the case where the input genuinely is one string and you cannot change where it comes from.

Editorial conclusion

Adopt go-shellwords when your program receives a command line as one string, from a configuration file, an environment variable or a container entrypoint, and you need the same word splitting a shell would give you. Do not adopt it to run commands, since it does not execute anything except the backtick substitution you explicitly enable. Verify first that the parser options you turn on match what your input can contain, in particular that environment expansion and backtick substitution stay off for input you do not control, and that the word splitting rules the package applies are the ones your caller's quoting actually needs.

Frequently asked questions

What does shellwords.Parse do in Go?

It parses a line into shell words and returns the arguments together with an error. The README's example is `args, err := shellwords.Parse("./foo --bar=baz")`, where the result is the two words `"./foo"` and `"--bar=baz"`, with the equals sign kept inside the second word.

What is the difference between Parse and ParseWithEnvs?

ParseWithEnvs separates leading environment assignments from the command. The README's example, `shellwords.ParseWithEnvs("FOO=foo BAR=baz ./foo --bar=baz")`, returns the assignments `["FOO=foo", "BAR=baz"]` separately from the arguments `"./foo", "--bar=baz"`.

Does go-shellwords run the commands it parses?

Not by default. Expansion of environment variables, handling of `#` comments and backtick substitution are all opt-in through a parser created with `shellwords.NewParser()`. With `p.ParseBacktick = true` the parser does evaluate a backtick command, which is why that option is the one to think about before pointing the package at input you do not control.

How do I handle comments and custom separators with go-shellwords?

Set `p.ParseComment = true` to treat a `#` as the start of a comment, which makes `"./foo # comment"` parse to just `"./foo"`, and call `p.SetExcludeSeparators('\t', ';')` to stop those characters from splitting words. The README's example uses a tab and a semicolon.

Official sources

  1. Issues
  2. License: MIT
  3. mattn/go-shellwords on GitHub
  4. README
  5. Releases
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mattn-go-shellwords.svg)](https://hysenlabs.com/projects/mattn-go-shellwords)

Was this useful?