summaryrefslogtreecommitdiffstats
path: root/atuin-client/src/import/zsh.rs
diff options
context:
space:
mode:
Diffstat (limited to 'atuin-client/src/import/zsh.rs')
-rw-r--r--atuin-client/src/import/zsh.rs127
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)));
+ }
}