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",
]
[[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"

View File

@ -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"]

View File

@ -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<T> = Result<T, CodespanError>;
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.
#[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<PathBuf>,
watch: bool,
font_paths: Vec<PathBuf>,
/// Path to output PDF file
output: Option<PathBuf>,
}
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] <input.typ> [output.pdf]
typst [SUBCOMMAND] ...
/// Path to output PDF file
output: Option<PathBuf>,
}
ARGS:
<input.typ> 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 <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>,
/// 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 <dir> 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<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.
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<Command> {
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<bool> {
fn compile_once(world: &mut SystemWorld, command: &CompileSettings) -> StrResult<bool> {
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 {