diff options
| author | Conrad Ludgate <conradludgate@gmail.com> | 2021-12-11 09:48:53 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-12-11 09:48:53 +0000 |
| commit | 87df7d80eca0ede9e149d1ef533e71650e4b919a (patch) | |
| tree | 389e2c5b32208a8a05259d308af857f7a61cb7f9 /atuin-client/src/import | |
| parent | Bump serde_json from 1.0.64 to 1.0.72 (#219) (diff) | |
| download | atuin-87df7d80eca0ede9e149d1ef533e71650e4b919a.zip | |
Fish importing (#234)
* make a start on fish
* fix
* test
* enable fish
* fmt
* update histpath
set up fish init script
* update readme
* cover edge case
* fmt
* fix session variables
Co-authored-by: PJ <me@panekj.dev>
* respect NOBIND
Co-authored-by: PJ <me@panekj.dev>
* fix env var setting
Co-authored-by: PJ <me@panekj.dev>
* fix whitespace
Co-authored-by: PJ <me@panekj.dev>
* add fish to supported shells
Co-authored-by: PJ <me@panekj.dev>
Diffstat (limited to 'atuin-client/src/import')
| -rw-r--r-- | atuin-client/src/import/fish.rs | 221 | ||||
| -rw-r--r-- | atuin-client/src/import/mod.rs | 1 |
2 files changed, 222 insertions, 0 deletions
diff --git a/atuin-client/src/import/fish.rs b/atuin-client/src/import/fish.rs new file mode 100644 index 00000000..4079e122 --- /dev/null +++ b/atuin-client/src/import/fish.rs @@ -0,0 +1,221 @@ +// 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 chrono::prelude::*; +use chrono::Utc; +use directories::BaseDirs; +use eyre::{eyre, Result}; + +use super::{count_lines, Importer}; +use crate::history::History; + +#[derive(Debug)] +pub struct Fish<R> { + file: BufReader<R>, + strbuf: String, + loc: usize, +} + +impl<R: Read + Seek> Fish<R> { + fn new(r: R) -> Result<Self> { + let mut buf = BufReader::new(r); + let loc = count_lines(&mut buf)?; + + Ok(Self { + file: buf, + strbuf: String::new(), + loc, + }) + } +} + +impl<R: Read> Fish<R> { + fn new_entry(&mut self) -> io::Result<bool> { + let inner = self.file.fill_buf()?; + Ok(inner.starts_with(b"- ")) + } +} + +impl Importer for Fish<File> { + 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")) + } + } + + fn parse(path: impl AsRef<Path>) -> Result<Self> { + Self::new(File::open(path)?) + } +} + +impl<R: Read> Iterator for Fish<R> { + type Item = Result<History>; + + fn next(&mut self) -> Option<Self::Item> { + 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())), + _ => (), + } + + // `read_line` adds the line delimeter to the string. No thanks + self.strbuf.pop(); + + 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"\"); + // replaces escaped newlines + let c = c.replace(r"\n", "\n"); + // TODO: any other escape characters? + + cmd = Some(c); + } else if let Some(t) = self.strbuf.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)); + } + } 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); + + Some(Ok(History::new( + time, + cmd, + "unknown".into(), + -1, + -1, + None, + None, + ))) + } + + fn size_hint(&self) -> (usize, Option<usize>) { + // worst case, entry per line + (0, Some(self.loc)) + } +} + +#[cfg(test)] +mod test { + use chrono::{TimeZone, Utc}; + use std::io::Cursor; + + use super::Fish; + use crate::history::History; + + // simple wrapper for fish history entry + macro_rules! fishtory { + ($timestamp:literal, $command:literal) => { + History::new( + Utc.timestamp($timestamp, 0), + $command.into(), + "unknown".into(), + -1, + -1, + None, + None, + ) + }; + } + + #[test] + fn parse_complex() { + // complicated input with varying contents and escaped strings. + let input = r#"- cmd: history --help + when: 1639162832 +- cmd: cat ~/.bash_history + when: 1639162851 + paths: + - ~/.bash_history +- cmd: ls ~/.local/share/fish/fish_history + when: 1639162890 + paths: + - ~/.local/share/fish/fish_history +- cmd: cat ~/.local/share/fish/fish_history + when: 1639162893 + paths: + - ~/.local/share/fish/fish_history +ERROR +- CORRUPTED: ENTRY + CONTINUE: + - AS + - NORMAL +- cmd: echo "foo" \\\n'bar' baz + when: 1639162933 +- cmd: cat ~/.local/share/fish/fish_history + when: 1639162939 + paths: + - ~/.local/share/fish/fish_history +- cmd: echo "\\"" \\\\ "\\\\" + when: 1639163063 +- cmd: cat ~/.local/share/fish/fish_history + when: 1639163066 + paths: + - ~/.local/share/fish/fish_history +"#; + let cursor = Cursor::new(input); + let fish = Fish::new(cursor).unwrap(); + + let history = fish.collect::<Result<Vec<_>, _>>().unwrap(); + assert_eq!( + history, + vec![ + fishtory!(1639162832, "history --help"), + fishtory!(1639162851, "cat ~/.bash_history"), + fishtory!(1639162890, "ls ~/.local/share/fish/fish_history"), + fishtory!(1639162893, "cat ~/.local/share/fish/fish_history"), + fishtory!(1639162933, "echo \"foo\" \\\n'bar' baz"), + fishtory!(1639162939, "cat ~/.local/share/fish/fish_history"), + fishtory!(1639163063, r#"echo "\"" \\ "\\""#), + fishtory!(1639163066, "cat ~/.local/share/fish/fish_history"), + ] + ); + } +} diff --git a/atuin-client/src/import/mod.rs b/atuin-client/src/import/mod.rs index d73d3857..471b7f98 100644 --- a/atuin-client/src/import/mod.rs +++ b/atuin-client/src/import/mod.rs @@ -6,6 +6,7 @@ use eyre::Result; use crate::history::History; pub mod bash; +pub mod fish; pub mod resh; pub mod zsh; |
