From 9414d56f97c5be71d9c37abb0fd1632a76bde995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20d=27Herbais=20de=20Thun?= Date: Thu, 30 Mar 2023 21:59:28 +0200 Subject: [PATCH] Rewrite of CLI using clap (#468) --- Cargo.lock | 193 ++++++++++++++++++++++++++++++++++-- cli/Cargo.toml | 2 +- cli/src/main.rs | 257 +++++++++++++++++++++++++----------------------- 3 files changed, 319 insertions(+), 133 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20d959677..7394da540 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,46 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "342258dd14006105c2b75ab1bd7543a03bdf0cfc94383303ac212a04939dff6f" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-wincon", + "concolor-override", + "concolor-query", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2" + +[[package]] +name = "anstyle-parse" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7d1bb534e9efed14f3e5f44e7dd1a4f709384023a4165199a4241e18dff0116" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-wincon" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3127af6145b149f3287bb9a0d10ad9c5692dba8c53ad48285e5bec4063834fa" +dependencies = [ + "anstyle", + "windows-sys 0.45.0", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -142,6 +182,48 @@ dependencies = [ "winapi", ] +[[package]] +name = "clap" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046ae530c528f252094e4a77886ee1374437744b2bff1497aa898bbddbbb29b3" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.11", +] + +[[package]] +name = "clap_lex" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -179,6 +261,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "concolor-override" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a855d4a1978dc52fb0536a04d384c2c0c1aa273597f08b77c8c4d3b2eec6037f" + +[[package]] +name = "concolor-query" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf" +dependencies = [ + "windows-sys 0.45.0", +] + [[package]] name = "core-foundation-sys" version = "0.8.3" @@ -258,7 +355,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.4", + "syn 2.0.11", ] [[package]] @@ -275,7 +372,7 @@ checksum = "631569015d0d8d54e6c241733f944042623ab6df7bc3be7466874b05fcdb1c5f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.4", + "syn 2.0.11", ] [[package]] @@ -333,6 +430,27 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "errno" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "fancy-regex" version = "0.7.1" @@ -458,6 +576,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hypher" version = "0.1.1" @@ -577,6 +701,29 @@ dependencies = [ "libc", ] +[[package]] +name = "io-lifetimes" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "256017f749ab3117e93acb91063009e1f1bb56d03965b14c2c8df4eb02c524d8" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys 0.45.0", +] + [[package]] name = "isolang" version = "2.2.0" @@ -669,6 +816,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd550e73688e6d578f0ac2119e32b797a327631a42f9433e59d02e139c8df60d" + [[package]] name = "lipsum" version = "0.9.0" @@ -1006,6 +1159,20 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "rustix" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e78cc525325c06b4a7ff02db283472f3c042b7ff0c391f96c6d5ac6f4f91b75" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + [[package]] name = "rustversion" version = "1.0.12" @@ -1075,7 +1242,7 @@ checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" dependencies = [ "proc-macro2", "quote", - "syn 2.0.4", + "syn 2.0.11", ] [[package]] @@ -1128,6 +1295,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strum" version = "0.24.1" @@ -1190,9 +1363,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.4" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c622ae390c9302e214c31013517c2061ecb2699935882c60a9b37f82f8625ae" +checksum = "21e3787bb71465627110e7d87ed4faaa36c1f61042ee67badb9e2ef173accc40" dependencies = [ "proc-macro2", "quote", @@ -1252,7 +1425,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.4", + "syn 2.0.11", ] [[package]] @@ -1358,6 +1531,7 @@ name = "typst-cli" version = "0.0.0" dependencies = [ "chrono", + "clap", "codespan-reporting", "comemo", "dirs", @@ -1365,7 +1539,6 @@ dependencies = [ "memmap2", "notify", "once_cell", - "pico-args", "same-file", "siphasher", "typst", @@ -1585,6 +1758,12 @@ dependencies = [ "svgtypes", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "version_check" version = "0.9.4" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index e4fbb74c9..f484bf322 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -23,10 +23,10 @@ elsa = "1.7" memmap2 = "0.5" notify = "5" once_cell = "1" -pico-args = "0.4" same-file = "1" siphasher = "0.3" walkdir = "2" +clap = { version = "4.2.1", features = ["derive"] } [features] default = ["embed-fonts"] diff --git a/cli/src/main.rs b/cli/src/main.rs index 514beaa25..021012c79 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -6,6 +6,7 @@ use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::process; +use clap::{ArgAction, Parser, Subcommand}; use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::term::{self, termcolor}; use comemo::Prehashed; @@ -13,7 +14,6 @@ use elsa::FrozenVec; use memmap2::Mmap; use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use once_cell::unsync::OnceCell; -use pico_args::Arguments; use same_file::{is_same_file, Handle}; use siphasher::sip128::{Hasher128, SipHasher}; use termcolor::{ColorChoice, StandardStream, WriteColor}; @@ -28,141 +28,156 @@ use walkdir::WalkDir; type CodespanResult = Result; type CodespanError = codespan_reporting::files::Error; +const TYPST_VERSION: &str = env!("TYPST_VERSION"); + +/// typst creates PDF files from .typ files +#[derive(Debug, Clone, Parser)] +#[clap(name = "typst", version = TYPST_VERSION, author)] +pub struct CliArguments { + /// Add additional directories to search for fonts + #[clap(long = "font-path", value_name = "DIR", action = ArgAction::Append)] + font_paths: Vec, + + /// Configure the root for absolute paths + #[clap(long = "root", value_name = "DIR")] + root: Option, + + /// The typst command to run + #[command(subcommand)] + command: Command, +} + /// What to do. +#[derive(Debug, Clone, Subcommand)] +#[command()] enum Command { + /// Compiles the input file into a PDF file Compile(CompileCommand), + + /// Watches the input file and recompiles on changes + Watch(WatchCommand), + + /// List all discovered fonts in system and custom font paths Fonts(FontsCommand), } -/// Compile a .typ file into a PDF file. -struct CompileCommand { +/// Compiles the input file into a PDF file +#[derive(Debug, Clone, Parser)] +pub struct CompileCommand { + /// Path to input Typst file input: PathBuf, - output: PathBuf, - root: Option, - watch: bool, - font_paths: Vec, + + /// Path to output PDF file + output: Option, } -const HELP: &'static str = "\ -typst creates PDF files from .typ files +/// Watches the input file and recompiles on changes +#[derive(Debug, Clone, Parser)] +pub struct WatchCommand { + /// Path to input Typst file + input: PathBuf, -USAGE: - typst [OPTIONS] [output.pdf] - typst [SUBCOMMAND] ... + /// Path to output PDF file + output: Option, +} -ARGS: - Path to input Typst file - [output.pdf] Path to output PDF file - -OPTIONS: - -h, --help Print this help - -V, --version Print the CLI's version - -w, --watch Watch the inputs and recompile on changes - --font-path Add additional directories to search for fonts - --root Configure the root for absolute paths - -SUBCOMMANDS: - --fonts List all discovered fonts in system and custom font paths -"; - -/// List discovered system fonts. -struct FontsCommand { - font_paths: Vec, +/// List all discovered fonts in system and custom font paths +#[derive(Debug, Clone, Parser)] +pub struct FontsCommand { + /// Add additional directories to search for fonts + #[arg(long)] variants: bool, } -const HELP_FONTS: &'static str = "\ -typst --fonts lists all discovered fonts in system and custom font paths +/// A summary of the input arguments relevant to compilation. +struct CompileSettings { + /// The path to the input file. + input: PathBuf, -USAGE: - typst --fonts [OPTIONS] + /// The path to the output file. + output: PathBuf, -OPTIONS: - -h, --help Print this help - --font-path Add additional directories to search for fonts - --variants Also list style variants of each font family -"; + /// Whether to watch the input files for changes. + watch: bool, + + /// The root directory for absolute paths. + root: Option, + + /// The paths to search for fonts. + font_paths: Vec, +} + +impl CompileSettings { + /// Create a new compile settings from the field values. + pub fn new( + input: PathBuf, + output: Option, + watch: bool, + root: Option, + font_paths: Vec, + ) -> Self { + let output = match output { + Some(path) => path, + None => input.with_extension("pdf"), + }; + + Self { input, output, watch, root, font_paths } + } + + /// Create a new compile settings from the CLI arguments and a compile command. + /// + /// # Panics + /// Panics if the command is not a compile or watch command. + pub fn with_arguments(args: CliArguments) -> Self { + let (input, output, watch) = match args.command { + Command::Compile(command) => (command.input, command.output, false), + Command::Watch(command) => (command.input, command.output, true), + _ => unreachable!(), + }; + Self::new(input, output, watch, args.root, args.font_paths) + } +} + +struct FontsSettings { + /// The font paths + font_paths: Vec, + + /// Wether to include font variants + variants: bool, +} + +impl FontsSettings { + /// Create font settings from the field values. + pub fn new(font_paths: Vec, variants: bool) -> Self { + Self { font_paths, variants } + } + + /// Create a new font settings from the CLI arguments. + /// + /// # Panics + /// Panics if the command is not a fonts command. + pub fn with_arguments(args: CliArguments) -> Self { + match args.command { + Command::Fonts(command) => Self::new(args.font_paths, command.variants), + _ => unreachable!(), + } + } +} /// Entry point. fn main() { - let command = parse_args(); - let ok = command.is_ok(); - if let Err(msg) = command.and_then(dispatch) { - print_error(&msg).unwrap(); - if !ok { - println!("\nfor more information, try --help"); + let arguments = CliArguments::parse(); + + let res = match &arguments.command { + Command::Compile(_) | Command::Watch(_) => { + compile(CompileSettings::with_arguments(arguments)) } - process::exit(1); - } -} - -/// Parse command line arguments. -fn parse_args() -> StrResult { - let mut args = Arguments::from_env(); - if args.contains(["-V", "--version"]) { - print_version(); - } - - let help = args.contains(["-h", "--help"]); - let font_paths = args.values_from_str("--font-path").unwrap(); - - let command = if args.contains("--fonts") { - if help { - print_help(HELP_FONTS); - } - - Command::Fonts(FontsCommand { font_paths, variants: args.contains("--variants") }) - } else { - if help { - print_help(HELP); - } - - let root = args.opt_value_from_str("--root").map_err(|_| "missing root path")?; - let watch = args.contains(["-w", "--watch"]); - let (input, output) = parse_input_output(&mut args, "pdf")?; - Command::Compile(CompileCommand { input, output, watch, root, font_paths }) + Command::Fonts(_) => fonts(FontsSettings::with_arguments(arguments)), }; - // Don't allow excess arguments. - let rest = args.finish(); - if !rest.is_empty() { - Err(format!("unexpected argument{}", if rest.len() > 1 { "s" } else { "" }))?; + if let Err(msg) = res { + print_error(&msg).expect("failed to print error"); } - - Ok(command) -} - -/// Parse two freestanding path arguments, with the output path being optional. -/// If it is omitted, it is determined from the input path's file stem plus the -/// given extension. -fn parse_input_output(args: &mut Arguments, ext: &str) -> StrResult<(PathBuf, PathBuf)> { - let input: PathBuf = args.free_from_str().map_err(|_| "missing input file")?; - let output = match args.opt_free_from_str().ok().flatten() { - Some(output) => output, - None => { - let name = input.file_name().ok_or("source path does not point to a file")?; - Path::new(name).with_extension(ext) - } - }; - - // Ensure that the source file is not overwritten. - if is_same_file(&input, &output).unwrap_or(false) { - Err("source and destination files are the same")?; - } - - Ok((input, output)) -} - -/// Print a help string and quit. -fn print_help(help: &'static str) -> ! { - print!("{help}"); - std::process::exit(0); -} - -/// Print the version hash and quit. -fn print_version() -> ! { - println!("typst {}", env!("TYPST_VERSION")); - std::process::exit(0); } /// Print an application-level error (independent from a source file). @@ -177,16 +192,8 @@ fn print_error(msg: &str) -> io::Result<()> { writeln!(w, ": {msg}.") } -/// Dispatch a command. -fn dispatch(command: Command) -> StrResult<()> { - match command { - Command::Compile(command) => compile(command), - Command::Fonts(command) => fonts(command), - } -} - /// Execute a compilation command. -fn compile(command: CompileCommand) -> StrResult<()> { +fn compile(command: CompileSettings) -> StrResult<()> { let root = if let Some(root) = &command.root { root.clone() } else if let Some(dir) = command @@ -254,7 +261,7 @@ fn compile(command: CompileCommand) -> StrResult<()> { } /// Compile a single time. -fn compile_once(world: &mut SystemWorld, command: &CompileCommand) -> StrResult { +fn compile_once(world: &mut SystemWorld, command: &CompileSettings) -> StrResult { status(command, Status::Compiling).unwrap(); world.reset(); @@ -280,7 +287,7 @@ fn compile_once(world: &mut SystemWorld, command: &CompileCommand) -> StrResult< } /// Clear the terminal and render the status message. -fn status(command: &CompileCommand, status: Status) -> io::Result<()> { +fn status(command: &CompileSettings, status: Status) -> io::Result<()> { if !command.watch { return Ok(()); } @@ -373,7 +380,7 @@ fn print_diagnostics( } /// Execute a font listing command. -fn fonts(command: FontsCommand) -> StrResult<()> { +fn fonts(command: FontsSettings) -> StrResult<()> { let mut searcher = FontSearcher::new(); searcher.search_system(); for path in &command.font_paths {