Skip to main content

lib/systemd/unit_file/value.ex

defmodule Systemd.UnitFile.Value do
  @moduledoc """
  Helpers for systemd directive values.

  The unit-file parser preserves directive values as raw strings. These helpers
  provide focused value-level parsing for callers that need shell-like words or
  simple quoted strings without baking value semantics into every directive.
  """

  @type word :: String.t()

  @doc """
  Splits a directive value into whitespace-separated words.

  Double and single quotes group whitespace. A backslash escapes the following
  character outside single quotes.
  """
  @spec words(String.t()) :: {:ok, [word()]} | {:error, term()}
  def words(value) when is_binary(value) do
    value
    |> String.to_charlist()
    |> do_words([], [], :bare)
  end

  defp do_words([], current, words, :bare) do
    {:ok, finish_words(current, words)}
  end

  defp do_words([], _current, _words, quote) when quote in [:single, :double] do
    {:error, {:unterminated_quote, quote}}
  end

  defp do_words([char | rest], current, words, :bare) when char in [?\s, ?\t] do
    do_words(rest, [], maybe_push_word(current, words), :bare)
  end

  defp do_words([?' | rest], current, words, :bare), do: do_words(rest, current, words, :single)
  defp do_words([?" | rest], current, words, :bare), do: do_words(rest, current, words, :double)

  defp do_words([?\\, char | rest], current, words, :bare),
    do: do_words(rest, [char | current], words, :bare)

  defp do_words([char | rest], current, words, :bare),
    do: do_words(rest, [char | current], words, :bare)

  defp do_words([?' | rest], current, words, :single), do: do_words(rest, current, words, :bare)

  defp do_words([char | rest], current, words, :single),
    do: do_words(rest, [char | current], words, :single)

  defp do_words([?" | rest], current, words, :double), do: do_words(rest, current, words, :bare)

  defp do_words([?\\, char | rest], current, words, :double),
    do: do_words(rest, [char | current], words, :double)

  defp do_words([char | rest], current, words, :double),
    do: do_words(rest, [char | current], words, :double)

  defp finish_words(current, words) do
    current
    |> maybe_push_word(words)
    |> Enum.reverse()
  end

  defp maybe_push_word([], words), do: words
  defp maybe_push_word(current, words), do: [current |> Enum.reverse() |> to_string() | words]
end