clawsh 0.4.0

A shell built in rust for codecrafters
Documentation
use rustyline::history::{FileHistory, History, SearchDirection};
use std::{
    cmp::min,
    collections::HashSet,
    fs::{File, read, write},
    io::Write,
    path::Path,
};

pub(crate) fn invoke_history(args_str: &[String], history: &mut FileHistory) -> Option<String> {
    use std::fmt::Write;
    let mut args_iter = args_str.iter();
    let length = if let Some(arg) = args_iter.next() {
        match arg.parse::<usize>() {
            Ok(n) => min(n, history.len()),
            Err(_) => match arg.as_ref() {
                "-r" => {
                    invoke_history_read_file(history, &mut args_iter);
                    0
                }
                "-w" => {
                    invoke_history_write(history, &mut args_iter);
                    0
                }
                "-a" => {
                    invoke_history_append(history, args_iter);
                    0
                }
                _ => history.len(),
            },
        }
    } else {
        history.len()
    };
    let mut buf = String::new();
    for i in 0..length {
        let entry_idx = history.len() - length + i;
        #[allow(clippy::expect_used)]
        let search_result = history
            .get(entry_idx, SearchDirection::Reverse)
            .expect("Rustyline implementation of get on HistoryFile can not error");
        if let Some(entry) = search_result {
            let entry = entry.entry;
            let _ = writeln!(buf, "  {} {}", entry_idx + 1, entry);
        }
    }
    if buf.is_empty() { None } else { Some(buf) }
}

fn invoke_history_write(history: &mut FileHistory, args_iter: &mut std::slice::Iter<'_, String>) {
    if let Some(file_name) = args_iter.next() {
        let mut new_contents = vec![];
        for entry in history.iter() {
            let mut new_entry = entry.clone();
            new_entry.push('\n');
            new_contents.append(&mut new_entry.as_bytes().to_owned());
        }
        let _ = write(file_name, new_contents);
    }
}

fn invoke_history_append(history: &mut FileHistory, mut args_iter: std::slice::Iter<'_, String>) {
    if let Some(file_name) = args_iter.next() {
        let mut file_options = File::options();
        file_options.create(true).write(true).append(true);
        if let Ok(mut file) = file_options.open(file_name)
            && let Ok(old_contents) = read(file_name)
        {
            let mut set = HashSet::new();

            let old_string = String::from_utf8_lossy(&old_contents);
            for s in old_string.lines() {
                set.insert(s);
            }

            let mut last_append_index = None;

            for (i, entry) in history.iter().rev().skip(1).enumerate() {
                if entry.starts_with("history -a") {
                    last_append_index = Some(history.len() - 2 - i);
                    break;
                }
            }

            let start = last_append_index.map_or(0, |i| i + 1);

            let mut new_entries = Vec::new();

            for entry in history.iter().skip(start) {
                if !entry.starts_with("history -a") {
                    new_entries.push(entry.clone());
                }
            }

            let mut written = false;
            for entry in new_entries {
                if set.contains(entry.as_str()) {
                    continue;
                }
                _ = file.write_all(entry.as_bytes());
                _ = file.write_all(b"\n");
                written = true;
            }

            if written {
                _ = file.write_all(format!("history -a {file_name}\n").as_bytes());
            }
        }
    }
}

fn invoke_history_read_file(
    history: &mut FileHistory,
    args_iter: &mut std::slice::Iter<'_, String>,
) {
    let env_file = std::env::var_os("HISTFILE");

    let history_file = if let Some(file_name) = env_file {
        _ = File::create(&file_name);
        file_name
    } else {
        _ = File::create("/tmp/history.txt");
        "/tmp/history.txt".into()
    };

    if let Some(file_name) = args_iter.next() {
        if history.load(Path::new(file_name)).is_err() {
            eprintln!("Could not read history from file {file_name}");
        } else {
            let mut new_contents = format!("history -r {file_name}\n").as_bytes().to_owned();
            if let Ok(mut contents) = read(file_name) {
                new_contents.append(&mut contents);
                let _ = write(&history_file, new_contents);
                let _ = history.load(Path::new(&history_file));
            }
        }
    }
}