linsel 0.1.0-alpha.1

A small program to print out selected lines of a file
Documentation
// SPDX-FileCopyrightText: 2026 David Zaslavsky <diazona@ellipsix.net>
//
// SPDX-License-Identifier: GPL-3.0-or-later

mod range;

use std::{io, iter};

pub use range::{CreateRangeError, ParseRangeError, Range, RangeElement};

#[derive(Debug, PartialEq)]
pub enum ParseArgumentsError {
    Help,
    CreateRangeError(range::CreateRangeError),
    ParseRangeError(range::ParseRangeError),
}

impl From<range::ParseRangeError> for ParseArgumentsError {
    fn from(error: range::ParseRangeError) -> Self {
        ParseArgumentsError::ParseRangeError(error)
    }
}

impl From<range::CreateRangeError> for ParseArgumentsError {
    fn from(error: range::CreateRangeError) -> Self {
        ParseArgumentsError::CreateRangeError(error)
    }
}

pub fn parse_arguments<T: AsRef<str>>(args: &[T]) -> Result<Vec<Range>, ParseArgumentsError> {
    if args.iter().any(|s| s.as_ref() == "-h" || s.as_ref() == "--help") {
        Err(ParseArgumentsError::Help)
    }
    else {
        args.iter()
            .flat_map(|arg| arg.as_ref().split(","))
            .map(|r| r.parse::<Range>().map_err(ParseArgumentsError::ParseRangeError))
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::{ParseArgumentsError, Range, parse_arguments};

    #[test]
    fn no_arguments() {
        let result = parse_arguments(&Vec::<&str>::new()).unwrap();
        assert_eq!(result, Vec::<Range>::new());
    }

    #[test]
    fn single_number() {
        let result = parse_arguments(&["1"]).unwrap();
        let expected = vec![Range::new(1, 1, 1).unwrap()];
        assert_eq!(result, expected);
    }

    #[test]
    fn single_range() {
        let result = parse_arguments(&["1-10"]).unwrap();
        assert_eq!(result, vec![Range::new(1, 10, 1).unwrap()]);
    }

    #[test]
    fn single_range_with_step() {
        let result = parse_arguments(&["1-10/2"]).unwrap();
        assert_eq!(result, vec![Range::new(1, 10, 2).unwrap()]);
    }

    #[test]
    fn single_arg_multiple_ranges_with_steps() {
        let result = parse_arguments(&["1-10/2,12-25/3"]).unwrap();
        assert_eq!(result, vec![Range::new(1, 10, 2).unwrap(), Range::new(12, 25, 3).unwrap()]);
    }

    #[test]
    fn multiple_args() {
        let result = parse_arguments(&["1-10/2", "12-25/3"]).unwrap();
        assert_eq!(result, vec![Range::new(1, 10, 2).unwrap(), Range::new(12, 25, 3).unwrap()]);
    }

    #[test]
    fn multiple_args_multiple_ranges() {
        let result = parse_arguments(&["1-10/2,12-25/3", "30-40/5"]).unwrap();
        assert_eq!(
            result,
            vec![
                Range::new(1, 10, 2).unwrap(),
                Range::new(12, 25, 3).unwrap(),
                Range::new(30, 40, 5).unwrap()
            ]
        );
    }

    #[test]
    fn short_help() {
        let result = parse_arguments(&["-h"]);
        assert_eq!(result.unwrap_err(), ParseArgumentsError::Help);
    }

    #[test]
    fn long_help() {
        let result = parse_arguments(&["--help"]);
        assert_eq!(result.unwrap_err(), ParseArgumentsError::Help);
    }

    #[test]
    fn long_help_with_other_args() {
        let result = parse_arguments(&["1-10/2", "--help"]);
        assert_eq!(result.unwrap_err(), ParseArgumentsError::Help);
    }
}

pub fn print_usage(program: &str) {
    eprintln!("Usage: {program} <range>[,<range>]...");
    eprintln!("Read standard input and print the lines selected by the given ranges.");
    eprintln!();
    eprintln!("      --help     display this help and exit");
}

pub fn select_elements<T>(
    source: impl iter::Iterator<Item = T>,
    ranges: &[Range],
) -> impl iter::Iterator<Item = T> {
    source.enumerate().filter_map(|(i, elem)| {
        if ranges.iter().any(|r| r.contains(i + 1)) {
            Some(elem)
        }
        else {
            None
        }
    })
}

pub fn print_selected_lines(ranges: &[Range]) {
    select_elements(io::stdin().lines(), ranges).for_each(|line| println!("{}", line.unwrap()))
}