mitex_cli/
lib.rs

1//! The CLI for MiTeX.
2
3pub mod utils;
4mod version;
5
6pub use version::{intercept_version, VersionFormat};
7
8use std::path::PathBuf;
9
10use build_info::VERSION;
11use clap::{Args, Command, FromArgMatches, Parser, Subcommand, ValueEnum};
12
13/// CLI options
14#[derive(Debug, Parser)]
15#[clap(name = "mitex-cli", version = VERSION)]
16pub struct Opts {
17    /// Prints version
18    #[arg(short = 'V', long, group = "version-dump")]
19    pub version: bool,
20
21    /// Prints version in format
22    #[arg(long = "VV", alias = "version-fmt", group = "version-dump", default_value_t = VersionFormat::None)]
23    pub vv: VersionFormat,
24
25    /// Subcommands
26    #[clap(subcommand)]
27    pub sub: Option<Subcommands>,
28}
29
30/// Available compile stages for `$program compile`
31#[derive(ValueEnum, Debug, Clone, PartialEq, Eq)]
32#[value(rename_all = "kebab-case")]
33pub enum CompileStage {
34    /// Generates an AST.
35    Syntax,
36    /// Generates a Typst document.
37    Document,
38}
39
40/// Compile arguments.
41#[derive(Default, Debug, Clone, Parser)]
42#[clap(next_help_heading = "Compile options")]
43pub struct CompileArgs {
44    /// Path to workspace.
45    ///
46    /// This is used to resolve imports in `\iftypst` blocks.
47    ///
48    /// ## Example
49    ///
50    /// ```bash
51    /// mitex compile -w '/my-workspace' main.tex
52    /// ```
53    ///
54    /// Resolves `/some-file.typ` with `/my-workspace/some-file.typ`
55    ///
56    /// ```latex
57    /// \iftypst
58    /// #import "/some-file.typ"
59    /// \fi
60    /// ```
61    #[clap(long, short, default_value = ".")]
62    pub workspace: String,
63
64    /// Entry file.
65    ///
66    /// ## Example
67    ///
68    /// ```bash
69    /// mitex compile main.tex
70    /// ```
71    ///
72    /// ## Example
73    ///
74    /// ```bash
75    /// mitex compile -i main.tex
76    /// ```
77    #[clap(long, short, default_value = "")]
78    pub input: String,
79
80    /// Compile stage.
81    ///
82    /// ## Example
83    ///
84    /// ```bash
85    /// mitex compile --stage syntax main.tex
86    /// ```
87    ///
88    /// ## Example
89    ///
90    /// ```bash
91    /// mitex compile --stage document main.tex
92    /// ```
93    #[clap(long, value_enum)]
94    pub stage: Option<CompileStage>,
95
96    /// Output to file, default to entry file name with `.typ` extension.
97    ///
98    /// ## Example
99    ///
100    /// ```bash
101    /// mitex compile main.tex main.typ
102    /// ```
103    ///
104    /// ## Example
105    ///
106    /// ```bash
107    /// mitex compile -i main.tex -o main.typ
108    /// ```
109    ///
110    /// ## Example
111    ///
112    /// ```bash
113    /// mitex compile --stage syntax main.tex main.ast.txt
114    /// ```
115    #[clap(long, short, default_value = "")]
116    pub output: String,
117
118    /// Default positional arguments for input and output file.
119    ///
120    /// ## Example
121    ///
122    /// ```bash
123    /// mitex compile main.tex
124    /// ```
125    ///
126    /// ## Example
127    ///
128    /// ```bash
129    /// mitex compile main.tex main.typ
130    /// ```
131    #[arg(trailing_var_arg = true, hide = true)]
132    _i_or_o_args: Vec<String>,
133}
134
135/// Subcommands
136#[derive(Debug, Subcommand)]
137#[clap(about = "The CLI for MiTeX.", next_display_order = None)]
138#[allow(clippy::large_enum_variant)]
139pub enum Subcommands {
140    /// Runs MiTeX transpiler.
141    #[clap(visible_alias = "c")]
142    Compile(CompileArgs),
143
144    /// Generates a shell completion script.
145    Completion(CompletionArgs),
146
147    /// Generates a manual.
148    Manual(ManualArgs),
149
150    /// Subcommands about command specification for MiTeX.
151    #[clap(subcommand)]
152    Spec(SpecSubCommands),
153}
154
155/// Generate shell completion script.
156#[derive(Debug, Clone, Parser)]
157pub struct CompletionArgs {
158    /// Completion script kind.
159    #[clap(value_enum)]
160    pub shell: clap_complete::Shell,
161}
162
163/// Generate shell completion script.
164#[derive(Debug, Clone, Parser)]
165pub struct ManualArgs {
166    /// Path to output directory.
167    pub dest: PathBuf,
168}
169
170/// Commands about command specification for MiTeX.
171#[derive(Debug, Subcommand)]
172#[clap(next_display_order = None)]
173#[allow(clippy::large_enum_variant)]
174pub enum SpecSubCommands {
175    /// Generates a command specification file for MiTeX.
176    Generate(GenerateSpecArgs),
177}
178
179/// Generates a command specification file for MiTeX.
180#[derive(Debug, Clone, Parser)]
181pub struct GenerateSpecArgs {}
182
183/// Struct for build info.
184pub mod build_info {
185    /// The version of the mitex-core crate.
186    pub static VERSION: &str = env!("CARGO_PKG_VERSION");
187}
188
189/// Get a CLI command instance with/without subcommand.
190pub fn get_cli(sub_command_required: bool) -> Command {
191    let cli = Command::new("$").disable_version_flag(true);
192    Opts::augment_args(cli).subcommand_required(sub_command_required)
193}
194
195/// Process CLI options uniformly.
196fn process_opts(mut opts: Opts) -> Result<Opts, clap::Error> {
197    if let Some(Subcommands::Compile(args)) = &mut opts.sub {
198        let io_args = std::mem::take(&mut args._i_or_o_args);
199        if args.input.is_empty() && args.output.is_empty() {
200            match io_args.len() {
201                0 => {}
202                1 => {
203                    args.input.clone_from(&io_args[0]);
204                }
205                2 => {
206                    args.input.clone_from(&io_args[0]);
207                    args.output.clone_from(&io_args[1]);
208                }
209                _ => Err(clap::Error::raw(
210                    clap::error::ErrorKind::ValueValidation,
211                    "Too many positional arguments.",
212                ))?,
213            }
214        } else if !io_args.is_empty() {
215            Err(clap::Error::raw(
216                clap::error::ErrorKind::ValueValidation,
217                "Input and output file cannot be positional arguments\
218                if any of them is specified by named argument.",
219            ))?;
220        }
221
222        if args.input.is_empty() {
223            Err(clap::Error::raw(
224                clap::error::ErrorKind::ValueValidation,
225                "Input file is required.",
226            ))?;
227        }
228        if args.output.is_empty() {
229            std::path::Path::new(&args.input)
230                .with_extension("tex")
231                .to_str()
232                .ok_or_else(|| {
233                    clap::Error::raw(
234                        clap::error::ErrorKind::ValueValidation,
235                        "Input file name is invalid.",
236                    )
237                })?
238                .clone_into(&mut args.output);
239        }
240    }
241
242    Ok(opts)
243}
244
245/// Get CLI options from command line arguments.
246///
247/// # Errors
248/// Errors if the command line arguments are invalid.
249pub fn get_os_opts(sub_command_required: bool) -> Result<Opts, clap::Error> {
250    let res = Opts::from_arg_matches(&get_cli(sub_command_required).get_matches())?;
251
252    process_opts(res)
253}