quicklatex 0.1.0

A program to help me write LaTeX quickly
Documentation
//! Module for argument parsing
//!
//! This module provides the argument parsing infrastructure for
//! `main.rs`.  This module is different from all the rest of the
//! library part of this crate as it is the only thing that is *not*
//! downstream from [`lib::run`](crate::run), but is seperately
//! called.  Therefore it's only really useful if you run `quicklatex`
//! as a standalone program, and not in it's library use.

use std::{ffi::OsString, os::unix::ffi::OsStrExt, path::PathBuf};

use anyhow::{anyhow, bail, Context, Error};

/// All the arguments currently supported by the CLI program
#[derive(Debug, PartialEq, Eq)]
pub struct Arguments
{
    /// The path of the file to be build
    pub path: PathBuf,
    /// The number of times pdflatex should be run on it.  Defaults to
    /// `1`.  This feature is necessary since LaTeX needs to be run
    /// multiple times to achieve a steady-state state as every run
    /// uses the information produced by the previous ones.
    pub count: u16,
    /// Whether timing information should be printed.  Defaults to
    /// `false`.
    pub should_profile: bool,
}

enum State
{
    Options,
    Count,
    Finished,
}

/// Parses the provided arguments
///
/// This parses the in the argument `args` provided arguments and
/// returns an [`Arguments`] or an error.  This function is designed
/// such that you can just pass it an
/// [`args_os()`](std::env::args_os).  Importantly this means that the
/// first argument must be the program name (or something else you
/// don't care about having skipped), because this will just get
/// skipped.
///
/// # Errors
/// Returns an error if the arguments were in some way invalid.
// These two pedantic lints recommend a structuring of else blocks
// that is in my opinion less readable than the current one.
#[allow(clippy::redundant_else, clippy::collapsible_else_if)]
pub fn parse_args<I>(args: I) -> Result<Arguments, Error>
where
    I: Iterator<Item = OsString>,
{
    let mut path = None;
    let mut count = None;
    let mut should_profile = false;

    let mut state = State::Options;

    for arg in args.skip(1)
    {
        if arg.is_empty()
        {
            continue;
        }

        match state
        {
            State::Options =>
            {
                if arg.as_bytes()[0] == b'-'
                {
                    if arg.as_bytes() == b"-p" || arg.as_bytes() == b"--profiling"
                    {
                        should_profile = true;
                    }
                    else
                    {
                        if let Some(arg) = arg.to_str()
                        {
                            bail!("Unknown option {arg:?}");
                        }
                        else
                        {
                            bail!("Unknown option {arg:?}");
                        }
                    }
                }
                else
                {
                    path = Some(PathBuf::from(arg));
                    state = State::Count;
                }
            }
            State::Count =>
            {
                let count_unparsed = arg
                    .to_str()
                    .ok_or_else(|| anyhow!("The repetition count must be a number, not {arg:?}"))?;

                count =
                    Some(count_unparsed.parse().with_context(|| {
                        format!("The number that was attempted to be parsed is {count_unparsed:?}")
                    })?);

                state = State::Finished;
            }
            State::Finished => bail!("No arguments are expected after the count, but were provided"),
        }
    }

    Ok(Arguments {
        path: path.ok_or_else(|| anyhow!("No path specified"))?,
        count: count.unwrap_or(1),
        should_profile,
    })
}

#[cfg(test)]
mod tests
{
    use std::path::PathBuf;

    use crate::arguments::{parse_args, Arguments};

    #[test]
    fn argument_parsing()
    {
        let tests = vec![
            (
                vec!["vröätver", "abc/def"],
                Arguments {
                    path: PathBuf::from("abc/def"),
                    count: 1,
                    should_profile: false,
                },
            ),
            (
                vec!["", "abc/def"],
                Arguments {
                    path: PathBuf::from("abc/def"),
                    count: 1,
                    should_profile: false,
                },
            ),
            (
                vec!["", "abc/def", "5"],
                Arguments {
                    path: PathBuf::from("abc/def"),
                    count: 5,
                    should_profile: false,
                },
            ),
            (
                vec!["", "-p", "abc/def"],
                Arguments {
                    path: PathBuf::from("abc/def"),
                    count: 1,
                    should_profile: true,
                },
            ),
            (
                vec!["zbjtzbjut", "--profiling", "abc/def"],
                Arguments {
                    path: PathBuf::from("abc/def"),
                    count: 1,
                    should_profile: true,
                },
            ),
            (
                vec!["quicklatex", "--profiling", "abc/def", "0"],
                Arguments {
                    path: PathBuf::from("abc/def"),
                    count: 0,
                    should_profile: true,
                },
            ),
            (
                vec![
                    "quicklatex",
                    "",
                    "",
                    "--profiling",
                    "",
                    "abc/def",
                    "",
                    "",
                    "",
                    "",
                    "",
                    "",
                    "0",
                    "",
                    "",
                ],
                Arguments {
                    path: PathBuf::from("abc/def"),
                    count: 0,
                    should_profile: true,
                },
            ),
            (
                vec!["", "1", "2"],
                Arguments {
                    path: PathBuf::from("1"),
                    count: 2,
                    should_profile: false,
                },
            ),
            (
                vec!["", "-p", "1", "2"],
                Arguments {
                    path: PathBuf::from("1"),
                    count: 2,
                    should_profile: true,
                },
            ),
        ];

        for (input, output) in tests
        {
            assert_eq!(parse_args(input.into_iter().map(Into::into)).unwrap(), output);
        }
    }

    #[test]
    fn failing_argument_parsing()
    {
        let tests = vec![
            vec![""],
            vec!["", ""],
            vec!["", "-p"],
            vec!["", "5", "-p"],
            vec!["", "-a"],
            vec!["", "-p", "-a", "test"],
            vec!["", "1", "2", "3"],
            vec!["", "-p", "1", "2", "3"],
            vec!["", "-pa", "abc/def"],
            vec!["", "--p", "abc/def"],
            vec!["", "--profilinga", "abc/def"],
        ];

        for input in tests
        {
            assert!(parse_args(input.into_iter().map(Into::into)).is_err());
        }
    }
}