diff options
Diffstat (limited to 'atuin-client/src/import/zsh.rs')
-rw-r--r-- | atuin-client/src/import/zsh.rs | 127 |
1 files changed, 91 insertions, 36 deletions
diff --git a/atuin-client/src/import/zsh.rs b/atuin-client/src/import/zsh.rs index 46e9af63..b3db36b6 100644 --- a/atuin-client/src/import/zsh.rs +++ b/atuin-client/src/import/zsh.rs @@ -1,50 +1,73 @@ // import old shell history! // automatically hoover up all that we can find -use std::io::{BufRead, BufReader}; -use std::{fs::File, path::Path}; +use std::{ + fs::File, + io::{BufRead, BufReader, Read, Seek}, + path::{Path, PathBuf}, +}; use chrono::prelude::*; use chrono::Utc; +use directories::UserDirs; use eyre::{eyre, Result}; use itertools::Itertools; -use super::count_lines; +use super::{count_lines, Importer}; use crate::history::History; #[derive(Debug)] -pub struct Zsh { - file: BufReader<File>, - - pub loc: u64, - pub counter: i64, +pub struct Zsh<R> { + file: BufReader<R>, + strbuf: String, + loc: usize, + counter: i64, } -impl Zsh { - pub fn new(path: impl AsRef<Path>) -> Result<Self> { - let file = File::open(path)?; - let mut buf = BufReader::new(file); +impl<R: Read + Seek> Zsh<R> { + fn new(r: R) -> Result<Self> { + let mut buf = BufReader::new(r); let loc = count_lines(&mut buf)?; Ok(Self { file: buf, - loc: loc as u64, + strbuf: String::new(), + loc, counter: 0, }) } +} - fn read_line(&mut self) -> Option<Result<String>> { - let mut line = String::new(); - - match self.file.read_line(&mut line) { - Ok(0) => None, - Ok(_) => Some(Ok(line)), - Err(e) => Some(Err(eyre!("failed to read line: {}", e))), // we can skip past things like invalid utf8 +impl Importer for Zsh<File> { + const NAME: &'static str = "zsh"; + + fn histpath() -> Result<PathBuf> { + // oh-my-zsh sets HISTFILE=~/.zhistory + // zsh has no default value for this var, but uses ~/.zhistory. + // we could maybe be smarter about this in the future :) + let user_dirs = UserDirs::new().unwrap(); + let home_dir = user_dirs.home_dir(); + + let mut candidates = [".zhistory", ".zsh_history"].iter(); + loop { + match candidates.next() { + Some(candidate) => { + let histpath = home_dir.join(candidate); + if histpath.exists() { + break Ok(histpath); + } + } + None => break Err(eyre!("Could not find history file. Try setting $HISTFILE")), + } } } + + fn parse(path: impl AsRef<Path>) -> Result<Self> { + Self::new(File::open(path)?) + } } -impl Iterator for Zsh { +impl<R: Read> Iterator for Zsh<R> { type Item = Result<History>; fn next(&mut self) -> Option<Self::Item> { @@ -52,18 +75,17 @@ impl Iterator for Zsh { // These lines begin with : // So, if the line begins with :, parse it. Otherwise it's just // the command - let line = self.read_line()?; - - if let Err(e) = line { - return Some(Err(e)); // :( + self.strbuf.clear(); + match self.file.read_line(&mut self.strbuf) { + Ok(0) => return None, + Ok(_) => (), + Err(e) => return Some(Err(eyre!("failed to read line: {}", e))), // we can skip past things like invalid utf8 } - let mut line = line.unwrap(); - - while line.ends_with("\\\n") { - let next_line = self.read_line()?; + self.loc -= 1; - if next_line.is_err() { + while self.strbuf.ends_with("\\\n") { + if self.file.read_line(&mut self.strbuf).is_err() { // There's a chance that the last line of a command has invalid // characters, the only safe thing to do is break :/ // usually just invalid utf8 or smth @@ -71,19 +93,19 @@ impl Iterator for Zsh { // better to have some items that should have been part of // something else, than to miss things. So break. break; - } + }; - line.push_str(next_line.unwrap().as_str()); + self.loc -= 1; } // We have to handle the case where a line has escaped newlines. // Keep reading until we have a non-escaped newline - let extended = line.starts_with(':'); + let extended = self.strbuf.starts_with(':'); if extended { self.counter += 1; - Some(Ok(parse_extended(line.as_str(), self.counter))) + Some(Ok(parse_extended(&self.strbuf, self.counter))) } else { let time = chrono::Utc::now(); let offset = chrono::Duration::seconds(self.counter); @@ -93,7 +115,7 @@ impl Iterator for Zsh { Some(Ok(History::new( time, - line.trim_end().to_string(), + self.strbuf.trim_end().to_string(), String::from("unknown"), -1, -1, @@ -102,6 +124,10 @@ impl Iterator for Zsh { ))) } } + + fn size_hint(&self) -> (usize, Option<usize>) { + (0, Some(self.loc)) + } } fn parse_extended(line: &str, counter: i64) -> History { @@ -133,10 +159,12 @@ fn parse_extended(line: &str, counter: i64) -> History { #[cfg(test)] mod test { + use std::io::Cursor; + use chrono::prelude::*; use chrono::Utc; - use super::parse_extended; + use super::*; #[test] fn test_parse_extended_simple() { @@ -164,4 +192,31 @@ mod test { assert_eq!(parsed.duration, 10_000_000_000); assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); } + + #[test] + fn test_parse_file() { + let input = r": 1613322469:0;cargo install atuin +: 1613322469:10;cargo install atuin; \ +cargo update +: 1613322469:10;cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷ +"; + + let cursor = Cursor::new(input); + let mut zsh = Zsh::new(cursor).unwrap(); + assert_eq!(zsh.loc, 4); + assert_eq!(zsh.size_hint(), (0, Some(4))); + + assert_eq!(&zsh.next().unwrap().unwrap().command, "cargo install atuin"); + assert_eq!( + &zsh.next().unwrap().unwrap().command, + "cargo install atuin; \\\ncargo update" + ); + assert_eq!( + &zsh.next().unwrap().unwrap().command, + "cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷" + ); + assert!(zsh.next().is_none()); + + assert_eq!(zsh.size_hint(), (0, Some(0))); + } } |