ShellWords (ShellWords v0.1.0)

Copy Markdown View Source

POSIX-like shell word splitting, escaping, and joining.

Inspired by Python's shlex (split/quote/join) and Ruby's Shellwords, with an Elixir-native API:

  • split/2 / split!/2 — parse a shell-like command string into words
  • escape/1 — make one argument safe as a single shell word
  • join/1 — escape and join argv into one shell-safe string

Examples

iex> ShellWords.split(~S(cp "my file.txt" /tmp))
{:ok, ["cp", "my file.txt", "/tmp"]}

iex> ShellWords.join(["git", "commit", "-m", "initial commit"])
"git commit -m 'initial commit'"

Scope

This is not a shell interpreter. There is no command execution, pipe or redirect handling, variable expansion, command substitution, globbing, tilde expansion, or comment parsing. # is an ordinary character.

Only POSIX-like shells are targeted; there is no Windows cmd.exe or PowerShell escaping.

Security

Prefer argv-based command execution when possible:

System.cmd("echo", ["hello world"])

Use escape/1 or join/1 only when you must build a shell command string. The core guarantee, verified by property-based tests, is the round trip:

ShellWords.split(ShellWords.join(argv)) == {:ok, argv}

for every list of valid UTF-8 strings.

Details

Word separators are exactly ASCII space, tab, newline, and carriage return; Unicode whitespace is an ordinary character. Backslash-newline is not line continuation: the newline is kept as a literal character. Error positions are 0-based byte offsets. Behavior on invalid UTF-8 input is unspecified and may change.

Summary

Functions

Escapes a string so a POSIX shell parses it back as a single word with the original content.

Escapes each argument with escape/1 and joins them with single spaces, producing one shell-safe command string.

Splits a shell-like command string into a list of words.

Like split/2, but returns the word list directly and raises ShellWords.ParseError on parse errors.

Functions

escape(arg)

@spec escape(String.t()) :: String.t()

Escapes a string so a POSIX shell parses it back as a single word with the original content.

Returns the string unchanged only when it is non-empty and consists solely of unambiguously safe ASCII characters; otherwise wraps it in single quotes, escaping embedded single quotes. The empty string becomes "''".

Output style matches Python's shlex.quote (single-quote wrapping), not Ruby's backslash-style Shellwords.escape.

Examples

iex> ShellWords.escape("hello")
"hello"

iex> ShellWords.escape("hello world")
"'hello world'"

iex> ShellWords.escape("")
"''"

join(argv)

@spec join([String.t()]) :: String.t()

Escapes each argument with escape/1 and joins them with single spaces, producing one shell-safe command string.

Examples

iex> ShellWords.join(["echo", "hello world"])
"echo 'hello world'"

iex> ShellWords.join([])
""

split(input, opts \\ [])

@spec split(
  String.t(),
  keyword()
) :: {:ok, [String.t()]} | {:error, ShellWords.ParseError.t()}

Splits a shell-like command string into a list of words.

Returns {:ok, words} or {:error, %ShellWords.ParseError{}}.

No options are supported yet; the opts argument exists to keep the arity stable for future releases. Unknown options raise ArgumentError.

Examples

iex> ShellWords.split("ls -la /tmp")
{:ok, ["ls", "-la", "/tmp"]}

iex> ShellWords.split("")
{:ok, []}

split!(input, opts \\ [])

@spec split!(
  String.t(),
  keyword()
) :: [String.t()]

Like split/2, but returns the word list directly and raises ShellWords.ParseError on parse errors.

Examples

iex> ShellWords.split!(~S(echo "hello world"))
["echo", "hello world"]