-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add
cmd/1
& cmd!/1
to run GPG commands
- Loading branch information
Showing
5 changed files
with
135 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import Config | ||
|
||
if config_env() == :test do | ||
config :gpg_ex, | ||
gpg_home: "/tmp/gpg_ex_test_gpg_home" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,108 @@ | ||
defmodule GPGex do | ||
@moduledoc """ | ||
A simple wrapper to run GPG commands. | ||
Tested on Linux with `gpg (GnuPG) 2.2.27`. | ||
> ### Warning {: .warning} | ||
> | ||
> This is a pre-release version. As such, anything _may_ change | ||
> at any time, the public API _should not_ be considered stable, | ||
> and using a pinned version is _recommended_. | ||
## Configuration | ||
You can specify an optional directory to be passed to `--homedir`. | ||
Without this option your default GPG keyring will be used. | ||
config :gpg_ex, | ||
gpg_home: "/tmp/gpg_ex_home" | ||
""" | ||
|
||
@doc """ | ||
Hello world. | ||
@base_args ["--batch", "--status-fd=1"] | ||
|
||
@doc ~S""" | ||
Runs GPG with the given args. | ||
Returns parsed status messages and stdout rows separately in a tuple. | ||
## Examples | ||
iex> GPGex.hello() | ||
:world | ||
iex> {:ok, messages, stdout} = GPGex.cmd(["--recv-keys", "18D5DCA13E5D61587F552A1BDEB5A837B34DD01D"]) | ||
iex> messages | ||
[ | ||
"KEY_CONSIDERED 18D5DCA13E5D61587F552A1BDEB5A837B34DD01D 0", | ||
"IMPORTED DEB5A837B34DD01D GPGEx Test <spam@sherlox.io>", | ||
"IMPORT_OK 1 18D5DCA13E5D61587F552A1BDEB5A837B34DD01D", | ||
"IMPORT_RES 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0" | ||
] | ||
iex> stdout | ||
[ | ||
"key DEB5A837B34DD01D: public key \"GPGEx Test <spam@sherlox.io>\" imported", | ||
"Total number processed: 1", | ||
"imported: 1" | ||
] | ||
iex> GPGex.cmd(["--delete-keys", "18D5DCA13E5D61587F552A1BDEB5A837B34DD01D"]) | ||
{:ok, [], []} | ||
iex> GPGex.cmd(["--recv-keys", "91C8AFC4674BF0963E7A90CEB7FFBE9D2DF23D67"]) | ||
{:error, ["FAILURE recv-keys 167772218"], ["keyserver receive failed: No data"]} | ||
""" | ||
@spec cmd([String.t()]) :: {:ok | :error, [String.t()], [String.t()]} | ||
def cmd(args) when is_list(args) do | ||
{res, code} = | ||
System.cmd( | ||
"gpg", | ||
base_args() ++ args, | ||
into: [], | ||
stderr_to_stdout: true | ||
) | ||
|
||
[stdout, messages] = process_output(res) | ||
|
||
case code do | ||
0 -> {:ok, stdout, messages} | ||
_ -> {:error, stdout, messages} | ||
end | ||
end | ||
|
||
@doc """ | ||
Same as `cmd/1` but raises a | ||
`RuntimeError` if the command fails. | ||
""" | ||
def hello do | ||
:world | ||
@spec cmd!([String.t()]) :: {[String.t()], [String.t()]} | ||
def cmd!(args) when is_list(args) do | ||
case cmd(args) do | ||
{:ok, stdout, messages} -> | ||
{stdout, messages} | ||
|
||
{:error, stdout, _} -> | ||
raise RuntimeError, | ||
"GPG command failed with: #{stdout}" | ||
end | ||
end | ||
|
||
defp process_output(stdout) do | ||
stdout | ||
|> Enum.join() | ||
|> String.split("\n", trim: true) | ||
|> Enum.split_with(&String.starts_with?(&1, "[GNUPG:]")) | ||
|> Tuple.to_list() | ||
|> Enum.map(&cleanup_lines(&1)) | ||
end | ||
|
||
defp cleanup_lines(lines) do | ||
lines | ||
|> Enum.map(fn line -> String.replace(line, ~r/(\[GNUPG:\]|gpg:)\s/, "") |> String.trim() end) | ||
end | ||
|
||
defp base_args() do | ||
case Application.fetch_env(:gpg_ex, :gpg_home) do | ||
{:ok, gpg_home} -> ["--homedir", gpg_home] ++ @base_args | ||
_ -> @base_args | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,23 @@ | ||
defmodule GPGexTest do | ||
use ExUnit.Case | ||
doctest GPGex | ||
|
||
test "greets the world" do | ||
assert GPGex.hello() == :world | ||
setup_all do | ||
with {:ok, gpg_home} <- Application.fetch_env(:gpg_ex, :gpg_home) do | ||
if !String.starts_with?(gpg_home, "/tmp"), | ||
do: raise("Trying to delete '#{gpg_home}' which is outside of '/tmp'. Aborting.") | ||
|
||
File.rm_rf!(gpg_home) | ||
File.mkdir!(gpg_home) | ||
File.chmod!("#{gpg_home}", 0o700) | ||
|
||
System.shell("gpg --homedir #{gpg_home} --fingerprint", | ||
into: [], | ||
stderr_to_stdout: true | ||
) | ||
end | ||
|
||
:ok | ||
end | ||
|
||
doctest GPGex | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
ExUnit.configure(seed: 0) | ||
|
||
ExUnit.start() |