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};
use i18n_embed::unic_langid::LanguageIdentifier;
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");
}
#[cfg(feature = "mount")]
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), *)
}};
}
fn format_localized(content: String) -> Vec<Inline> {
if let Some((l, r)) = content.split_once(" _") {
Some(Inline::Roman(l.into()))
.into_iter()
.chain(r.split(" _").flat_map(|s| {
let (l, r) = s
.split_once("_ ")
.expect("italics should always be terminated");
[
Inline::Roman(" ".into()),
Inline::Italic(l.into()),
Inline::Roman(format!(" {r}")),
]
}))
.collect()
} else {
vec![Inline::Roman(content)]
}
}
struct ManpageSection(Roff);
impl ManpageSection {
fn new(heading: String) -> Self {
let mut roff = Roff::default();
roff.control("SH", [heading.as_str()]);
Self(roff)
}
fn subheading(mut self, subheading: String) -> Self {
self.0.control("SS", [subheading.as_str()]);
self
}
fn paragraph(mut self, content: String) -> Self {
self.0.text(format_localized(content));
self
}
fn render(self, w: &mut impl io::Write) -> io::Result<()> {
self.0.to_writer(w)
}
}
struct Example {
text: String,
commands: Vec<ExampleCommand>,
}
struct ExampleCommand {
cmd: &'static str,
output: Vec<String>,
}
impl Example {
const fn new(text: String) -> Self {
Self {
text,
commands: vec![],
}
}
fn cmd(self, cmd: &'static str) -> Self {
self.cmd_out(cmd, vec![])
}
fn cmd_out(mut self, cmd: &'static str, output: Vec<String>) -> Self {
self.commands.push(ExampleCommand { cmd, output });
self
}
}
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", [fl!("man-examples-heading").as_str()]);
for example in self.0 {
roff.control("TP", []);
roff.text(
[
Inline::Roman(format!("{}:", example.text)),
Inline::LineBreak,
]
.into_iter()
.chain(example.commands.into_iter().flat_map(|example| {
example
.cmd
.lines()
.enumerate()
.flat_map(|(i, line)| {
(i != 0)
.then(|| [Inline::Bold(" \\".into()), Inline::LineBreak])
.into_iter()
.flatten()
.chain(Some(Inline::Bold(format!(
"{} {}",
if i == 0 { '$' } else { ' ' },
line
))))
})
.chain(Some(Inline::LineBreak))
.chain(
example
.output
.into_iter()
.flat_map(|output| [Inline::Roman(output), Inline::LineBreak]),
)
.chain(Some(Inline::LineBreak))
}))
.collect::<Vec<_>>(),
);
}
roff.to_writer(w)
}
}
#[derive(Clone)]
struct Cli {
rage: Command,
rage_keygen: Command,
#[cfg(feature = "mount")]
rage_mount: Command,
}
impl Cli {
fn build() -> Self {
Self {
rage: rage::AgeOptions::command(),
rage_keygen: rage_keygen::AgeOptions::command(),
#[cfg(feature = "mount")]
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)?;
#[cfg(feature = "mount")]
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,
see_also: &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_description_section(&mut w)?;
man.render_options_section(&mut w)?;
custom(&man, &mut w)?;
ManpageSection::new(fl!("man-see-also-heading"))
.paragraph(see_also.into())
.render(&mut w)?;
man.render_version_section(&mut w)?;
man.render_authors_section(&mut w)
}
#[cfg(feature = "mount")]
let (rage_see_also, rage_keygen_see_also) =
("rage-keygen(1), rage-mount(1)", "rage(1), rage-mount(1)");
#[cfg(not(feature = "mount"))]
let (rage_see_also, rage_keygen_see_also) = ("rage-keygen(1)", "rage(1)");
generate_manpage(
out_dir,
"rage",
rage_see_also,
self.rage
.about(fl!("man-rage-about"))
.long_about(fl!("man-rage-description"))
.mut_arg("output", |a| a.long_help(fl!("man-rage-flag-output")))
.mut_arg("encrypt", |a| a.long_help(fl!("man-rage-flag-encrypt")))
.mut_arg("recipient", |a| a.long_help(fl!("man-rage-flag-recipient")))
.mut_arg("recipients_file", |a| {
a.long_help(fl!("man-rage-flag-recipients-file"))
})
.mut_arg("passphrase", |a| {
a.long_help(fl!("man-rage-flag-passphrase"))
})
.mut_arg("armor", |a| a.long_help(fl!("man-rage-flag-armor")))
.mut_arg("decrypt", |a| a.long_help(fl!("man-rage-flag-decrypt")))
.mut_arg("identity", |a| {
a.long_help(fl!("man-rage-flag-identity-decrypt"))
})
.mut_arg("plugin_name", |a| {
a.long_help(fl!("man-rage-flag-plugin-decrypt"))
})
.after_help(rage::after_help_content("rage-keygen")),
|_, w| {
ManpageSection::new(fl!("man-rage-recipients-and-identities-heading"))
.paragraph(fl!("man-rage-recipients-and-identities"))
.subheading(fl!("man-rage-native-x25519-keys-heading"))
.paragraph(fl!(
"man-rage-native-x25519-keys",
example_age_recipient = "age1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh",
example_age_identity =
"AGE-SECRET-KEY-1KTYK6RVLN5TAPE7VF6FQQSKZ9HWWCDSKUGXXNUQDWZ7XXT5YK5LSF3UTKQ",
))
.subheading(fl!("man-rage-ssh-keys-heading"))
.paragraph(fl!(
"man-rage-ssh-keys",
example_ssh_rsa = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbi[...]GU4BtElAbzh8=",
example_ssh_ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEas[...]l1uZc31FGYMXa",
))
.subheading(fl!("man-rage-plugins-heading"))
.paragraph(fl!("man-rage-plugins"))
.render(w)?;
Examples([
Example::new(fl!("man-rage-example-single"))
.cmd_out(
"rage-keygen -o key.txt",
vec![format!(
"{}: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p",
fl!("tty-pubkey")
)],
)
.cmd("tar cvz ~/data | rage -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data.tar.gz.age")
.cmd("rage -d -o data.tar.gz -i key.txt data.tar.gz.age"),
Example::new(fl!(
"man-rage-example-enc-multiple",
input = "example.jpg",
output = "example.jpg.age"
))
.cmd(
"rage -o example.jpg.age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p\n \
-r age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg example.jpg",
),
Example::new(fl!("man-rage-example-enc-list"))
.cmd_out(
"cat > recipients.txt",
vec![
"# Alice".into(),
"age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p".into(),
"# Bob".into(),
"age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg".into(),
],
)
.cmd("rage -R recipients.txt example.jpg > example.jpg.age"),
Example::new(fl!("man-rage-example-password"))
.cmd_out(
"rage -p secrets.txt > secrets.txt.age",
vec![
fl!("autogenerated-passphrase"),
" release-response-step-brand-wrap-ankle-pair-unusual-sword-train".into(),
],
)
.cmd_out(
"rage -d secrets.txt.age > secrets.txt",
vec![format!("{}:", fl!("type-passphrase"))],
),
Example::new(fl!("man-rage-example-identity-passphrase"))
.cmd_out(
"rage -p <(rage-keygen) > key.age",
vec![
format!(
"{}: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5",
fl!("tty-pubkey")
),
fl!("autogenerated-passphrase"),
" hip-roast-boring-snake-mention-east-wasp-honey-input-actress".into(),
],
)
.cmd("rage -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age")
.cmd_out(
"rage -d -i key.age secrets.txt.age > secrets.txt",
vec![format!("{}:", fl!("type-passphrase"))],
),
Example::new(fl!("man-rage-example-ssh"))
.cmd("rage -R ~/.ssh/id_ed25519.pub example.jpg > example.jpg.age")
.cmd("rage -d -i ~/.ssh/id_ed25519 example.jpg.age > example.jpg"),
Example::new(fl!("man-rage-example-yubikey"))
.cmd_out("age-plugin-yubikey", vec![format!("# {}", fl!("man-rage-example-yubikey-setup"))])
.cmd(
"rage -r age1yubikey1qwt50d05nh5vutpdzmlg5wn80xq5negm4uj9ghv0snvdd3yysf5yw3rhl3t secrets.txt > secrets.txt.age",
)
.cmd("rage -d -i age-yubikey-identity-388178f3.txt secrets.txt.age"),
Example::new(fl!("man-rage-example-enc-github"))
.cmd("curl https://github.com/benjojo.keys | rage -R - example.jpg > example.jpg.age"),
])
.render(w)
},
)?;
generate_manpage(
out_dir,
"rage-keygen",
rage_keygen_see_also,
self.rage_keygen
.about(fl!("man-keygen-about"))
.long_about(fl!("man-keygen-description"))
.mut_arg("output", |a| a.long_help(fl!("man-keygen-flag-output")))
.mut_arg("convert", |a| a.long_help(fl!("man-keygen-flag-convert"))),
|_, w| {
Examples([
Example::new(fl!("man-keygen-example-stdout")).cmd_out(
"rage-keygen",
vec![
format!("# {}: 2021-01-02T15:30:45+01:00", fl!("identity-file-created")),
format!(
"# {}: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z",
fl!("identity-file-pubkey"),
),
"AGE-SECRET-KEY-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9".to_owned(),
],
),
Example::new(fl!("man-keygen-example-file", filename = "key.txt")).cmd_out(
"rage-keygen -o key.txt",
vec![format!(
"{}: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p",
fl!("tty-pubkey")
)],
),
Example::new(fl!("man-keygen-example-convert")).cmd_out(
"rage-keygen -y key.txt",
vec!["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p".to_owned()],
),
])
.render(w)
},
)?;
#[cfg(feature = "mount")]
generate_manpage(
out_dir,
"rage-mount",
"rage-keygen(1), rage(1)",
self.rage_mount
.about(fl!("man-mount-about"))
.long_about(fl!("man-mount-description"))
.mut_arg("types", |a| {
a.long_help(fl!(
"man-mount-flag-types",
types = crate::rage_mount::TYPES,
))
})
.mut_arg("identity", |a| {
a.long_help(fl!("man-rage-flag-identity-decrypt"))
}),
|_, w| {
Examples([
Example::new(fl!("man-mount-example-identity"))
.cmd("rage-mount -t tar -i key.txt encrypted.tar.age ./tmp"),
Example::new(fl!("man-mount-example-passphrase")).cmd_out(
"rage-mount -t zip encrypted.zip.age ./tmp",
vec![format!("{}:", fl!("type-passphrase"))],
),
])
.render(w)
},
)?;
Ok(())
}
}
fn main() -> io::Result<()> {
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(),
};
i18n::load_languages(&[]);
Cli::build().generate_completions(&out_dir.join("completions"))?;
let manpage_dir = out_dir.join("manpages");
for lang_dir in fs::read_dir("./i18n")? {
let lang_dir = lang_dir?.file_name();
let lang: LanguageIdentifier = lang_dir
.to_str()
.expect("should be valid Unicode")
.parse()
.expect("should be valid language identifier");
let mut out_dir = if lang.language.as_str() == "en" {
manpage_dir.clone()
} else {
let mut lang_str = lang.language.as_str().to_owned();
if let Some(region) = lang.region {
lang_str += "_";
lang_str += region.as_str();
}
manpage_dir.join(lang_str)
};
out_dir.push("man1");
i18n::load_languages(&[lang]);
Cli::build().generate_manpages(&out_dir)?;
}
Ok(())
}