Skip to content

Commit

Permalink
Merge pull request #444 from str4d/clap-completions-and-manpages
Browse files Browse the repository at this point in the history
`clap` completions and manpages
  • Loading branch information
str4d authored Jan 11, 2024
2 parents a1173d4 + 91a5818 commit 405304d
Show file tree
Hide file tree
Showing 19 changed files with 607 additions and 674 deletions.
6 changes: 0 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,6 @@ jobs:
run: cargo build --release --locked --target ${{ matrix.target }} ${{ matrix.build_flags }}
working-directory: ./rage

- name: Generate completions
run: cargo run --example generate-completions

- name: Generate manpages
run: cargo run --example generate-docs

- name: Update Debian package config for cross-compile
run: sed -i '/\/rage-mount/d' rage/Cargo.toml
if: matrix.name != 'linux'
Expand Down
25 changes: 13 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 21 additions & 14 deletions rage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,18 @@ assets = [
["target/release/rage", "usr/bin/", "755"],
["target/release/rage-keygen", "usr/bin/", "755"],
["target/release/rage-mount", "usr/bin/", "755"],
["../target/completions/rage.bash", "usr/share/bash-completion/completions/rage", "644"],
["../target/completions/rage-keygen.bash", "usr/share/bash-completion/completions/rage-keygen", "644"],
["../target/completions/rage-mount.bash", "usr/share/bash-completion/completions/rage-mount", "644"],
["../target/completions/rage.fish", "usr/share/fish/completions/", "644"],
["../target/completions/rage-keygen.fish", "usr/share/fish/completions/", "644"],
["../target/completions/rage-mount.fish", "usr/share/fish/completions/", "644"],
["../target/completions/rage.zsh", "usr/share/zsh/functions/Completion/Debian/", "644"],
["../target/completions/rage-keygen.zsh", "usr/share/zsh/functions/Completion/Debian/", "644"],
["../target/completions/rage-mount.zsh", "usr/share/zsh/functions/Completion/Debian/", "644"],
["../target/manpages/rage.1.gz", "usr/share/man/man1/", "644"],
["../target/manpages/rage-keygen.1.gz", "usr/share/man/man1/", "644"],
["../target/manpages/rage-mount.1.gz", "usr/share/man/man1/", "644"],
["target/release/completions/rage.bash", "usr/share/bash-completion/completions/rage", "644"],
["target/release/completions/rage-keygen.bash", "usr/share/bash-completion/completions/rage-keygen", "644"],
["target/release/completions/rage-mount.bash", "usr/share/bash-completion/completions/rage-mount", "644"],
["target/release/completions/rage.fish", "usr/share/fish/completions/", "644"],
["target/release/completions/rage-keygen.fish", "usr/share/fish/completions/", "644"],
["target/release/completions/rage-mount.fish", "usr/share/fish/completions/", "644"],
["target/release/completions/_rage", "usr/share/zsh/functions/Completion/Debian/", "644"],
["target/release/completions/_rage-keygen", "usr/share/zsh/functions/Completion/Debian/", "644"],
["target/release/completions/_rage-mount", "usr/share/zsh/functions/Completion/Debian/", "644"],
["target/release/manpages/rage.1.gz", "usr/share/man/man1/", "644"],
["target/release/manpages/rage-keygen.1.gz", "usr/share/man/man1/", "644"],
["target/release/manpages/rage-mount.1.gz", "usr/share/man/man1/", "644"],
["../README.md", "usr/share/doc/rage/README.md", "644"],
]
features = ["mount"]
Expand Down Expand Up @@ -75,10 +75,17 @@ tar = { version = "0.4", optional = true }
time = { version = ">=0.3.7, <0.3.24", optional = true } # time 0.3.24 has MSRV 1.67
zip = { version = "0.6.2", optional = true }

[dev-dependencies]
[build-dependencies]
clap = { workspace = true, features = ["string", "unstable-styles"] }
clap_complete = "4"
clap_mangen = "0.2"
flate2 = "1"
man = "0.3"
i18n-embed = { workspace = true, features = ["desktop-requester"] }
i18n-embed-fl.workspace = true
lazy_static.workspace = true
rust-embed.workspace = true

[dev-dependencies]
trycmd = "0.14"

[features]
Expand Down
243 changes: 243 additions & 0 deletions rage/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
use std::env;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;

use clap::{Command, CommandFactory, ValueEnum};
use clap_complete::{generate_to, Shell};
use clap_mangen::{
roff::{Inline, Roff},
Man,
};
use flate2::{write::GzEncoder, Compression};

mod i18n {
include!("src/bin/rage/i18n.rs");
}
mod rage {
include!("src/bin/rage/cli.rs");
}
mod rage_keygen {
include!("src/bin/rage-keygen/cli.rs");
}
mod rage_mount {
include!("src/bin/rage-mount/cli.rs");
}

#[macro_export]
macro_rules! fl {
($message_id:literal) => {{
i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id)
}};

($message_id:literal, $($args:expr),* $(,)?) => {{
i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *)
}};
}

struct Example {
text: String,
cmd: &'static str,
output: Option<String>,
}

impl Example {
const fn new(text: String, cmd: &'static str, output: Option<String>) -> Self {
Self { text, cmd, output }
}
}

struct Examples<const N: usize>([Example; N]);

impl<const N: usize> Examples<N> {
fn render(self, w: &mut impl io::Write) -> io::Result<()> {
let mut roff = Roff::default();
roff.control("SH", ["EXAMPLES"]);
for example in self.0 {
roff.control("TP", []);
roff.text(
[
Inline::Roman(format!("{}:", example.text)),
Inline::LineBreak,
Inline::Bold(format!("$ {}", example.cmd)),
Inline::LineBreak,
]
.into_iter()
.chain(
example
.output
.into_iter()
.flat_map(|output| [Inline::Roman(output), Inline::LineBreak]),
)
.collect::<Vec<_>>(),
);
}
roff.to_writer(w)
}
}

#[derive(Clone)]
struct Cli {
rage: Command,
rage_keygen: Command,
rage_mount: Command,
}

impl Cli {
fn build() -> Self {
Self {
rage: rage::AgeOptions::command(),
rage_keygen: rage_keygen::AgeOptions::command(),
rage_mount: rage_mount::AgeMountOptions::command(),
}
}

fn generate_completions(&mut self, out_dir: &Path) -> io::Result<()> {
fs::create_dir_all(out_dir)?;

for &shell in Shell::value_variants() {
generate_to(shell, &mut self.rage, "rage", out_dir)?;
generate_to(shell, &mut self.rage_keygen, "rage-keygen", out_dir)?;
generate_to(shell, &mut self.rage_mount, "rage-mount", out_dir)?;
}

Ok(())
}

fn generate_manpages(self, out_dir: &Path) -> io::Result<()> {
fs::create_dir_all(out_dir)?;

fn generate_manpage(
out_dir: &Path,
name: &str,
cmd: Command,
custom: impl FnOnce(&Man, &mut GzEncoder<fs::File>) -> io::Result<()>,
) -> io::Result<()> {
let file = fs::File::create(out_dir.join(format!("{}.1.gz", name)))?;
let mut w = GzEncoder::new(file, Compression::best());

let man = Man::new(cmd);
man.render_title(&mut w)?;
man.render_name_section(&mut w)?;
man.render_synopsis_section(&mut w)?;
man.render_options_section(&mut w)?;
custom(&man, &mut w)?;
man.render_version_section(&mut w)?;
man.render_authors_section(&mut w)
}

generate_manpage(
out_dir,
"rage",
self.rage
.about(fl!("man-rage-about"))
.after_help(rage::after_help_content("rage-keygen")),
|man, w| {
man.render_extra_section(w)?;
Examples([
Example::new(
fl!("man-rage-example-enc-single"),
"echo \"_o/\" | rage -o hello.age -r age1uvscypafkkxt6u2gkguxet62cenfmnpc0smzzlyun0lzszfatawq4kvf2u",
None,
),
Example::new(
fl!("man-rage-example-enc-multiple"),
"echo \"_o/\" | rage -r age1uvscypafkkxt6u2gkguxet62cenfmnpc0smzzlyun0lzszfatawq4kvf2u \
-r age1ex4ty8ppg02555at009uwu5vlk5686k3f23e7mac9z093uvzfp8sxr5jum > hello.age",
None,
),
Example::new(
fl!("man-rage-example-enc-password"),
"rage -p -o hello.txt.age hello.txt",
Some(format!("{}:", fl!("type-passphrase"))),
),
Example::new(
fl!("man-rage-example-enc-list"),
"tar cv ~/xxx | rage -R recipients.txt > xxx.tar.age",
None,
),
Example::new(
fl!("man-rage-example-enc-identities"),
"tar cv ~/xxx | rage -e -i keyA.txt -i keyB.txt > xxx.tar.age",
None,
),
Example::new(
fl!("man-rage-example-enc-url"),
"echo \"_o/\" | rage -o hello.age -R <(curl https://github.com/str4d.keys)",
None,
),
Example::new(
fl!("man-rage-example-dec-identities"),
"rage -d -o hello -i keyA.txt -i keyB.txt hello.age",
None,
),
])
.render(w)
},
)?;
generate_manpage(
out_dir,
"rage-keygen",
self.rage_keygen.about(fl!("man-keygen-about")),
|_, w| {
Examples([
Example::new(fl!("man-keygen-example-stdout"), "rage-keygen", None),
Example::new(
fl!("man-keygen-example-file"),
"rage-keygen -o key.txt",
Some(format!(
"{}: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p",
fl!("tty-pubkey")
)),
),
])
.render(w)
},
)?;
generate_manpage(
out_dir,
"rage-mount",
self.rage_mount.about(fl!("man-mount-about")),
|_, w| {
Examples([
Example::new(
fl!("man-mount-example-identity"),
"rage-mount -t tar -i key.txt encrypted.tar.age ./tmp",
None,
),
Example::new(
fl!("man-mount-example-passphrase"),
"rage-mount -t zip encrypted.zip.age ./tmp",
Some(format!("{}:", fl!("type-passphrase"))),
),
])
.render(w)
},
)?;

Ok(())
}
}

fn main() -> io::Result<()> {
i18n::load_languages();

// `OUT_DIR` is "intentionally opaque as it is only intended for `rustc` interaction"
// (https://github.com/rust-lang/cargo/issues/9858). Peek into the black box and use
// it to figure out where the target directory is.
let out_dir = match env::var_os("OUT_DIR") {
None => return Ok(()),
Some(out_dir) => PathBuf::from(out_dir)
.ancestors()
.nth(3)
.expect("should be absolute path")
.to_path_buf(),
};

let mut cli = Cli::build();
cli.generate_completions(&out_dir.join("completions"))?;
cli.generate_manpages(&out_dir.join("manpages"))?;

Ok(())
}
Loading

0 comments on commit 405304d

Please sign in to comment.