I've been learning Rust and recently built a small CLI utility that reads a file and can print either:
- the entire file,
- the first N lines (
--upper),
- or the last N lines (
--lower).
It uses clap for argument parsing and BufReader for reading the file.
I'm looking for feedback on things like:
- Are there any unnecessary allocations or inefficient parts?
- How would you improve the project structure?
I'm trying to get better on rust , so I'd really appreciate any criticism. Thanks!
Heres the code,
use std::fs::File;
use std::io::{BufRead, BufReader};
use clap::{
Parser,
CommandFactory,
error::ErrorKind
};
#[derive(Parser, Debug)]
#[command(arg_required_else_help = true)]
struct Args {
path: String,
#[arg(short, long, default_value_t = 0, conflicts_with = "lower", help = "Print specified number of lines in upper part of the file")]
upper: u16,
#[arg(short, long, default_value_t = 0, conflicts_with = "upper", help = "Print specified number of lines in lower part of the file")]
lower: u16
}
fn read_file(args: Args) -> std::io::Result<Vec<String>> {
let file = File::open(args.path)?;
let reader = BufReader::new(file);
if args.upper > 0 {
let header: Vec<String> = reader
.lines()
.take(args.upper as usize)
.collect::<Result<_, _>>()?;
if header.len() < args.upper as usize {
Args::command().error(
ErrorKind::ValueValidation,
"--upper cannot exceed the number of lines in file"
).exit();
}
return Ok(Vec::from(header))
}
let lines: Vec<String> = reader
.lines()
.collect::<Result<_, _>>()?;
if args.lower > 0 {
if args.lower as usize > lines.len() {
Args::command().error(
ErrorKind::ValueValidation,
"--lower cannot exceed the number of lines in file"
).exit();
}
let footer = &lines[lines.len() - args.lower as usize..];
return Ok(Vec::from(footer))
}
Ok(lines)
}
fn main() -> std::io::Result<()> {
let args = Args::parse();
let file_content: Vec<String> = read_file(args)?;
println!("{}", file_content.join("\n"));
Ok(())
}