aboutsummaryrefslogtreecommitdiffstats
path: root/atuin-client/src/import/fish.rs
diff options
context:
space:
mode:
authorConrad Ludgate <conradludgate@gmail.com>2022-05-09 07:46:52 +0100
committerGitHub <noreply@github.com>2022-05-09 07:46:52 +0100
commit1d030b9d32f539fd38f5ff3335234c5111c3303f (patch)
tree08619ad238362f66270919902c887c6357404bcd /atuin-client/src/import/fish.rs
parentBump clap from 3.1.15 to 3.1.16 (#392) (diff)
downloadatuin-1d030b9d32f539fd38f5ff3335234c5111c3303f.zip
Importer V3 (#395)
* start of importer refactor * fish * resh * zsh
Diffstat (limited to 'atuin-client/src/import/fish.rs')
-rw-r--r--atuin-client/src/import/fish.rs190
1 files changed, 90 insertions, 100 deletions
diff --git a/atuin-client/src/import/fish.rs b/atuin-client/src/import/fish.rs
index 7c05d180..af932d74 100644
--- a/atuin-client/src/import/fish.rs
+++ b/atuin-client/src/import/fish.rs
@@ -1,99 +1,90 @@
// import old shell history!
// automatically hoover up all that we can find
-use std::{
- fs::File,
- io::{self, BufRead, BufReader, Read, Seek},
- path::{Path, PathBuf},
-};
+use std::{fs::File, io::Read, path::PathBuf};
+use async_trait::async_trait;
use chrono::{prelude::*, Utc};
use directories::BaseDirs;
use eyre::{eyre, Result};
-use super::{count_lines, Importer};
+use super::{get_histpath, unix_byte_lines, Importer, Loader};
use crate::history::History;
#[derive(Debug)]
-pub struct Fish<R> {
- file: BufReader<R>,
- strbuf: String,
- loc: usize,
+pub struct Fish {
+ bytes: Vec<u8>,
}
-impl<R: Read + Seek> Fish<R> {
- fn new(r: R) -> Result<Self> {
- let mut buf = BufReader::new(r);
- let loc = count_lines(&mut buf)?;
+/// see https://fishshell.com/docs/current/interactive.html#searchable-command-history
+fn default_histpath() -> Result<PathBuf> {
+ let base = BaseDirs::new().ok_or_else(|| eyre!("could not determine data directory"))?;
+ let data = base.data_local_dir();
- Ok(Self {
- file: buf,
- strbuf: String::new(),
- loc,
- })
- }
-}
+ // fish supports multiple history sessions
+ // If `fish_history` var is missing, or set to `default`, use `fish` as the session
+ let session = std::env::var("fish_history").unwrap_or_else(|_| String::from("fish"));
+ let session = if session == "default" {
+ String::from("fish")
+ } else {
+ session
+ };
+
+ let mut histpath = data.join("fish");
+ histpath.push(format!("{}_history", session));
-impl<R: Read> Fish<R> {
- fn new_entry(&mut self) -> io::Result<bool> {
- let inner = self.file.fill_buf()?;
- Ok(inner.starts_with(b"- "))
+ if histpath.exists() {
+ Ok(histpath)
+ } else {
+ Err(eyre!("Could not find history file. Try setting $HISTFILE"))
}
}
-impl Importer for Fish<File> {
+#[async_trait]
+impl Importer for Fish {
const NAME: &'static str = "fish";
- /// see https://fishshell.com/docs/current/interactive.html#searchable-command-history
- fn histpath() -> Result<PathBuf> {
- let base = BaseDirs::new().ok_or_else(|| eyre!("could not determine data directory"))?;
- let data = base.data_local_dir();
-
- // fish supports multiple history sessions
- // If `fish_history` var is missing, or set to `default`, use `fish` as the session
- let session = std::env::var("fish_history").unwrap_or_else(|_| String::from("fish"));
- let session = if session == "default" {
- String::from("fish")
- } else {
- session
- };
-
- let mut histpath = data.join("fish");
- histpath.push(format!("{}_history", session));
-
- if histpath.exists() {
- Ok(histpath)
- } else {
- Err(eyre!("Could not find history file. Try setting $HISTFILE"))
- }
+ async fn new() -> Result<Self> {
+ let mut bytes = Vec::new();
+ let path = get_histpath(default_histpath)?;
+ let mut f = File::open(path)?;
+ f.read_to_end(&mut bytes)?;
+ Ok(Self { bytes })
}
- fn parse(path: impl AsRef<Path>) -> Result<Self> {
- Self::new(File::open(path)?)
+ async fn entries(&mut self) -> Result<usize> {
+ Ok(super::count_lines(&self.bytes))
}
-}
-impl<R: Read> Iterator for Fish<R> {
- type Item = Result<History>;
-
- fn next(&mut self) -> Option<Self::Item> {
+ async fn load(self, loader: &mut impl Loader) -> Result<()> {
+ let now = Utc::now();
let mut time: Option<DateTime<Utc>> = None;
let mut cmd: Option<String> = None;
- loop {
- self.strbuf.clear();
- match self.file.read_line(&mut self.strbuf) {
- // no more content to read
- Ok(0) => break,
- // bail on IO error
- Err(e) => return Some(Err(e.into())),
- _ => (),
- }
+ for b in unix_byte_lines(&self.bytes) {
+ let s = match std::str::from_utf8(b) {
+ Ok(s) => s,
+ Err(_) => continue, // we can skip past things like invalid utf8
+ };
+
+ if let Some(c) = s.strip_prefix("- cmd: ") {
+ // first, we must deal with the prev cmd
+ if let Some(cmd) = cmd.take() {
+ let time = time.unwrap_or(now);
- // `read_line` adds the line delimeter to the string. No thanks
- self.strbuf.pop();
+ loader
+ .push(History::new(
+ time,
+ cmd,
+ "unknown".into(),
+ -1,
+ -1,
+ None,
+ None,
+ ))
+ .await?;
+ }
- if let Some(c) = self.strbuf.strip_prefix("- cmd: ") {
// using raw strings to avoid needing escaping.
// replaces double backslashes with single backslashes
let c = c.replace(r"\\", r"\");
@@ -102,7 +93,7 @@ impl<R: Read> Iterator for Fish<R> {
// TODO: any other escape characters?
cmd = Some(c);
- } else if let Some(t) = self.strbuf.strip_prefix(" when: ") {
+ } else if let Some(t) = s.strip_prefix(" when: ") {
// if t is not an int, just ignore this line
if let Ok(t) = t.parse::<i64>() {
time = Some(Utc.timestamp(t, 0));
@@ -110,47 +101,40 @@ impl<R: Read> Iterator for Fish<R> {
} else {
// ... ignore paths lines
}
-
- match self.new_entry() {
- // next line is a new entry, so let's stop here
- // only if we have found a command though
- Ok(true) if cmd.is_some() => break,
- // bail on IO error
- Err(e) => return Some(Err(e.into())),
- _ => (),
- }
}
- let cmd = cmd?;
- let time = time.unwrap_or_else(Utc::now);
+ // we might have a trailing cmd
+ if let Some(cmd) = cmd.take() {
+ let time = time.unwrap_or(now);
- Some(Ok(History::new(
- time,
- cmd,
- "unknown".into(),
- -1,
- -1,
- None,
- None,
- )))
- }
+ loader
+ .push(History::new(
+ time,
+ cmd,
+ "unknown".into(),
+ -1,
+ -1,
+ None,
+ None,
+ ))
+ .await?;
+ }
- fn size_hint(&self) -> (usize, Option<usize>) {
- // worst case, entry per line
- (0, Some(self.loc))
+ Ok(())
}
}
#[cfg(test)]
mod test {
- use std::io::Cursor;
+
+ use crate::import::{tests::TestLoader, Importer};
use super::Fish;
- #[test]
- fn parse_complex() {
+ #[tokio::test]
+ async fn parse_complex() {
// complicated input with varying contents and escaped strings.
- let input = r#"- cmd: history --help
+ let bytes = r#"- cmd: history --help
when: 1639162832
- cmd: cat ~/.bash_history
when: 1639162851
@@ -181,14 +165,20 @@ ERROR
when: 1639163066
paths:
- ~/.local/share/fish/fish_history
-"#;
- let cursor = Cursor::new(input);
- let mut fish = Fish::new(cursor).unwrap();
+"#
+ .as_bytes()
+ .to_owned();
+
+ let fish = Fish { bytes };
+
+ let mut loader = TestLoader::default();
+ fish.load(&mut loader).await.unwrap();
+ let mut history = loader.buf.into_iter();
// simple wrapper for fish history entry
macro_rules! fishtory {
($timestamp:expr, $command:expr) => {
- let h = fish.next().expect("missing entry in history").unwrap();
+ let h = history.next().expect("missing entry in history");
assert_eq!(h.command.as_str(), $command);
assert_eq!(h.timestamp.timestamp(), $timestamp);
};