Rewrite of CLI using clap (#468)

This commit is contained in:
Sébastien d'Herbais de Thun 2023-03-30 21:59:28 +02:00 committed by GitHub
parent ed36ef3312
commit 9414d56f97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 319 additions and 133 deletions

193
Cargo.lock generated
View File

@ -26,6 +26,46 @@ dependencies = [
"libc", "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]] [[package]]
name = "arrayref" name = "arrayref"
version = "0.3.7" version = "0.3.7"
@ -142,6 +182,48 @@ dependencies = [
"winapi", "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]] [[package]]
name = "codespan-reporting" name = "codespan-reporting"
version = "0.11.1" version = "0.11.1"
@ -179,6 +261,21 @@ dependencies = [
"syn 1.0.109", "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]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.3" version = "0.8.3"
@ -258,7 +355,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"scratch", "scratch",
"syn 2.0.4", "syn 2.0.11",
] ]
[[package]] [[package]]
@ -275,7 +372,7 @@ checksum = "631569015d0d8d54e6c241733f944042623ab6df7bc3be7466874b05fcdb1c5f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.4", "syn 2.0.11",
] ]
[[package]] [[package]]
@ -333,6 +430,27 @@ dependencies = [
"stable_deref_trait", "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]] [[package]]
name = "fancy-regex" name = "fancy-regex"
version = "0.7.1" version = "0.7.1"
@ -458,6 +576,12 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]] [[package]]
name = "hypher" name = "hypher"
version = "0.1.1" version = "0.1.1"
@ -577,6 +701,29 @@ dependencies = [
"libc", "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]] [[package]]
name = "isolang" name = "isolang"
version = "2.2.0" version = "2.2.0"
@ -669,6 +816,12 @@ version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd550e73688e6d578f0ac2119e32b797a327631a42f9433e59d02e139c8df60d"
[[package]] [[package]]
name = "lipsum" name = "lipsum"
version = "0.9.0" version = "0.9.0"
@ -1006,6 +1159,20 @@ dependencies = [
"xmlparser", "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]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.12" version = "1.0.12"
@ -1075,7 +1242,7 @@ checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.4", "syn 2.0.11",
] ]
[[package]] [[package]]
@ -1128,6 +1295,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "strum" name = "strum"
version = "0.24.1" version = "0.24.1"
@ -1190,9 +1363,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.4" version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c622ae390c9302e214c31013517c2061ecb2699935882c60a9b37f82f8625ae" checksum = "21e3787bb71465627110e7d87ed4faaa36c1f61042ee67badb9e2ef173accc40"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1252,7 +1425,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.4", "syn 2.0.11",
] ]
[[package]] [[package]]
@ -1358,6 +1531,7 @@ name = "typst-cli"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap",
"codespan-reporting", "codespan-reporting",
"comemo", "comemo",
"dirs", "dirs",
@ -1365,7 +1539,6 @@ dependencies = [
"memmap2", "memmap2",
"notify", "notify",
"once_cell", "once_cell",
"pico-args",
"same-file", "same-file",
"siphasher", "siphasher",
"typst", "typst",
@ -1585,6 +1758,12 @@ dependencies = [
"svgtypes", "svgtypes",
] ]
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"

View File

@ -23,10 +23,10 @@ elsa = "1.7"
memmap2 = "0.5" memmap2 = "0.5"
notify = "5" notify = "5"
once_cell = "1" once_cell = "1"
pico-args = "0.4"
same-file = "1" same-file = "1"
siphasher = "0.3" siphasher = "0.3"
walkdir = "2" walkdir = "2"
clap = { version = "4.2.1", features = ["derive"] }
[features] [features]
default = ["embed-fonts"] default = ["embed-fonts"]

View File

@ -6,6 +6,7 @@ use std::io::{self, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process; use std::process;
use clap::{ArgAction, Parser, Subcommand};
use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::term::{self, termcolor}; use codespan_reporting::term::{self, termcolor};
use comemo::Prehashed; use comemo::Prehashed;
@ -13,7 +14,6 @@ use elsa::FrozenVec;
use memmap2::Mmap; use memmap2::Mmap;
use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use once_cell::unsync::OnceCell; use once_cell::unsync::OnceCell;
use pico_args::Arguments;
use same_file::{is_same_file, Handle}; use same_file::{is_same_file, Handle};
use siphasher::sip128::{Hasher128, SipHasher}; use siphasher::sip128::{Hasher128, SipHasher};
use termcolor::{ColorChoice, StandardStream, WriteColor}; use termcolor::{ColorChoice, StandardStream, WriteColor};
@ -28,141 +28,156 @@ use walkdir::WalkDir;
type CodespanResult<T> = Result<T, CodespanError>; type CodespanResult<T> = Result<T, CodespanError>;
type CodespanError = codespan_reporting::files::Error; 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<PathBuf>,
/// Configure the root for absolute paths
#[clap(long = "root", value_name = "DIR")]
root: Option<PathBuf>,
/// The typst command to run
#[command(subcommand)]
command: Command,
}
/// What to do. /// What to do.
#[derive(Debug, Clone, Subcommand)]
#[command()]
enum Command { enum Command {
/// Compiles the input file into a PDF file
Compile(CompileCommand), Compile(CompileCommand),
/// Watches the input file and recompiles on changes
Watch(WatchCommand),
/// List all discovered fonts in system and custom font paths
Fonts(FontsCommand), Fonts(FontsCommand),
} }
/// Compile a .typ file into a PDF file. /// Compiles the input file into a PDF file
struct CompileCommand { #[derive(Debug, Clone, Parser)]
pub struct CompileCommand {
/// Path to input Typst file
input: PathBuf, input: PathBuf,
output: PathBuf,
root: Option<PathBuf>, /// Path to output PDF file
watch: bool, output: Option<PathBuf>,
font_paths: Vec<PathBuf>,
} }
const HELP: &'static str = "\ /// Watches the input file and recompiles on changes
typst creates PDF files from .typ files #[derive(Debug, Clone, Parser)]
pub struct WatchCommand {
/// Path to input Typst file
input: PathBuf,
USAGE: /// Path to output PDF file
typst [OPTIONS] <input.typ> [output.pdf] output: Option<PathBuf>,
typst [SUBCOMMAND] ... }
ARGS: /// List all discovered fonts in system and custom font paths
<input.typ> Path to input Typst file #[derive(Debug, Clone, Parser)]
[output.pdf] Path to output PDF file pub struct FontsCommand {
/// Add additional directories to search for fonts
OPTIONS: #[arg(long)]
-h, --help Print this help
-V, --version Print the CLI's version
-w, --watch Watch the inputs and recompile on changes
--font-path <dir> Add additional directories to search for fonts
--root <dir> 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<PathBuf>,
variants: bool, variants: bool,
} }
const HELP_FONTS: &'static str = "\ /// A summary of the input arguments relevant to compilation.
typst --fonts lists all discovered fonts in system and custom font paths struct CompileSettings {
/// The path to the input file.
input: PathBuf,
USAGE: /// The path to the output file.
typst --fonts [OPTIONS] output: PathBuf,
OPTIONS: /// Whether to watch the input files for changes.
-h, --help Print this help watch: bool,
--font-path <dir> Add additional directories to search for fonts
--variants Also list style variants of each font family /// The root directory for absolute paths.
"; root: Option<PathBuf>,
/// The paths to search for fonts.
font_paths: Vec<PathBuf>,
}
impl CompileSettings {
/// Create a new compile settings from the field values.
pub fn new(
input: PathBuf,
output: Option<PathBuf>,
watch: bool,
root: Option<PathBuf>,
font_paths: Vec<PathBuf>,
) -> 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<PathBuf>,
/// Wether to include font variants
variants: bool,
}
impl FontsSettings {
/// Create font settings from the field values.
pub fn new(font_paths: Vec<PathBuf>, 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. /// Entry point.
fn main() { fn main() {
let command = parse_args(); let arguments = CliArguments::parse();
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");
}
process::exit(1);
}
}
/// Parse command line arguments. let res = match &arguments.command {
fn parse_args() -> StrResult<Command> { Command::Compile(_) | Command::Watch(_) => {
let mut args = Arguments::from_env(); compile(CompileSettings::with_arguments(arguments))
if args.contains(["-V", "--version"]) {
print_version();
} }
Command::Fonts(_) => fonts(FontsSettings::with_arguments(arguments)),
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 })
}; };
// Don't allow excess arguments. if let Err(msg) = res {
let rest = args.finish(); print_error(&msg).expect("failed to print error");
if !rest.is_empty() {
Err(format!("unexpected argument{}", if rest.len() > 1 { "s" } else { "" }))?;
} }
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). /// Print an application-level error (independent from a source file).
@ -177,16 +192,8 @@ fn print_error(msg: &str) -> io::Result<()> {
writeln!(w, ": {msg}.") 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. /// Execute a compilation command.
fn compile(command: CompileCommand) -> StrResult<()> { fn compile(command: CompileSettings) -> StrResult<()> {
let root = if let Some(root) = &command.root { let root = if let Some(root) = &command.root {
root.clone() root.clone()
} else if let Some(dir) = command } else if let Some(dir) = command
@ -254,7 +261,7 @@ fn compile(command: CompileCommand) -> StrResult<()> {
} }
/// Compile a single time. /// Compile a single time.
fn compile_once(world: &mut SystemWorld, command: &CompileCommand) -> StrResult<bool> { fn compile_once(world: &mut SystemWorld, command: &CompileSettings) -> StrResult<bool> {
status(command, Status::Compiling).unwrap(); status(command, Status::Compiling).unwrap();
world.reset(); world.reset();
@ -280,7 +287,7 @@ fn compile_once(world: &mut SystemWorld, command: &CompileCommand) -> StrResult<
} }
/// Clear the terminal and render the status message. /// 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 { if !command.watch {
return Ok(()); return Ok(());
} }
@ -373,7 +380,7 @@ fn print_diagnostics(
} }
/// Execute a font listing command. /// Execute a font listing command.
fn fonts(command: FontsCommand) -> StrResult<()> { fn fonts(command: FontsSettings) -> StrResult<()> {
let mut searcher = FontSearcher::new(); let mut searcher = FontSearcher::new();
searcher.search_system(); searcher.search_system();
for path in &command.font_paths { for path in &command.font_paths {