Support bash, resolves #3

This commit is contained in:
Ellie Huxtable 2021-04-26 11:50:31 +01:00
parent 4f16e8411e
commit 7b5c3d543d
11 changed files with 280 additions and 63 deletions

View file

@ -1,7 +1,6 @@
<h1 align="center">
Atuin
</h1>
<em align="center">Magical shell history</em>
<p align="center">
<a href="https://github.com/ellie/atuin/actions?query=workflow%3ARust"><img src="https://img.shields.io/github/workflow/status/ellie/atuin/Rust?style=flat-square" /></a>
@ -30,6 +29,7 @@
## Supported Shells
- zsh
- bash
# Quickstart
@ -43,12 +43,15 @@ atuin sync
## Install
### AUR
### Script (recommended)
Atuin is available on the [AUR](https://aur.archlinux.org/packages/atuin/)
The install script will help you through the setup, ensuring your shell is
properly configured. It will also use one of the below methods, preferring the
system package manager where possible (AUR, homebrew, etc etc).
```
yay -S atuin # or your AUR helper of choice
# do not run this as root, root will be asked for if required
curl https://github.com/ellie/atuin/blob/main/install.sh | sh
```
### With cargo
@ -60,6 +63,14 @@ toolchain, then you can run:
cargo install atuin
```
### AUR
Atuin is available on the [AUR](https://aur.archlinux.org/packages/atuin/)
```
yay -S atuin # or your AUR helper of choice
```
### From source
```
@ -68,15 +79,31 @@ cd atuin
cargo install --path .
```
### Shell plugin
## Shell plugin
Once the binary is installed, the shell plugin requires installing. Add
Once the binary is installed, the shell plugin requires installing. If you use
the install script, this should all be done for you!
### zsh
```
eval "$(atuin init)"
echo 'eval "$(atuin init zsh)"' >> ~/.zshrc
```
to your `.zshrc`
### bash
We need to setup some hooks, so first install bash-preexec:
```
curl https://raw.githubusercontent.com/rcaloras/bash-preexec/master/bash-preexec.sh -o ~/.bash-preexec.sh
echo '[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' >> ~/.bashrc
```
Then setup Atuin
```
echo 'eval "$(atuin init bash)"' >> ~/.bashrc
```
## ...what's with the name?

View file

@ -0,0 +1,79 @@
use std::io::{BufRead, BufReader};
use std::{fs::File, path::Path};
use eyre::{eyre, Result};
use super::count_lines;
use crate::history::History;
#[derive(Debug)]
pub struct Bash {
file: BufReader<File>,
pub loc: u64,
pub counter: i64,
}
impl Bash {
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
let file = File::open(path)?;
let mut buf = BufReader::new(file);
let loc = count_lines(&mut buf)?;
Ok(Self {
file: buf,
loc: loc as u64,
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 Iterator for Bash {
type Item = Result<History>;
fn next(&mut self) -> Option<Self::Item> {
let line = self.read_line()?;
if let Err(e) = line {
return Some(Err(e)); // :(
}
let mut line = line.unwrap();
while line.ends_with("\\\n") {
let next_line = self.read_line()?;
if next_line.is_err() {
break;
}
line.push_str(next_line.unwrap().as_str());
}
let time = chrono::Utc::now();
let offset = chrono::Duration::seconds(self.counter);
let time = time - offset;
self.counter += 1;
Some(Ok(History::new(
time,
line.trim_end().to_string(),
String::from("unknown"),
-1,
-1,
None,
None,
)))
}
}

View file

@ -0,0 +1,15 @@
use std::fs::File;
use std::io::{BufRead, BufReader, Seek, SeekFrom};
use eyre::Result;
pub mod bash;
pub mod zsh;
// this could probably be sped up
fn count_lines(buf: &mut BufReader<File>) -> Result<usize> {
let lines = buf.lines().count();
buf.seek(SeekFrom::Start(0))?;
Ok(lines)
}

View file

@ -1,7 +1,7 @@
// import old shell history!
// automatically hoover up all that we can find
use std::io::{BufRead, BufReader, Seek, SeekFrom};
use std::io::{BufRead, BufReader};
use std::{fs::File, path::Path};
use chrono::prelude::*;
@ -9,7 +9,8 @@ use chrono::Utc;
use eyre::{eyre, Result};
use itertools::Itertools;
use super::history::History;
use super::count_lines;
use crate::history::History;
#[derive(Debug)]
pub struct Zsh {
@ -19,14 +20,6 @@ pub struct Zsh {
pub counter: i64,
}
// this could probably be sped up
fn count_lines(buf: &mut BufReader<File>) -> Result<usize> {
let lines = buf.lines().count();
buf.seek(SeekFrom::Start(0))?;
Ok(lines)
}
impl Zsh {
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
let file = File::open(path)?;
@ -39,36 +32,7 @@ impl Zsh {
counter: 0,
})
}
}
fn parse_extended(line: &str, counter: i64) -> History {
let line = line.replacen(": ", "", 2);
let (time, duration) = line.splitn(2, ':').collect_tuple().unwrap();
let (duration, command) = duration.splitn(2, ';').collect_tuple().unwrap();
let time = time
.parse::<i64>()
.unwrap_or_else(|_| chrono::Utc::now().timestamp());
let offset = chrono::Duration::milliseconds(counter);
let time = Utc.timestamp(time, 0);
let time = time + offset;
let duration = duration.parse::<i64>().map_or(-1, |t| t * 1_000_000_000);
// use nanos, because why the hell not? we won't display them.
History::new(
time,
command.trim_end().to_string(),
String::from("unknown"),
0, // assume 0, we have no way of knowing :(
duration,
None,
None,
)
}
impl Zsh {
fn read_line(&mut self) -> Option<Result<String>> {
let mut line = String::new();
@ -140,6 +104,33 @@ impl Iterator for Zsh {
}
}
fn parse_extended(line: &str, counter: i64) -> History {
let line = line.replacen(": ", "", 2);
let (time, duration) = line.splitn(2, ':').collect_tuple().unwrap();
let (duration, command) = duration.splitn(2, ';').collect_tuple().unwrap();
let time = time
.parse::<i64>()
.unwrap_or_else(|_| chrono::Utc::now().timestamp());
let offset = chrono::Duration::milliseconds(counter);
let time = Utc.timestamp(time, 0);
let time = time + offset;
let duration = duration.parse::<i64>().map_or(-1, |t| t * 1_000_000_000);
// use nanos, because why the hell not? we won't display them.
History::new(
time,
command.trim_end().to_string(),
String::from("unknown"),
0, // assume 0, we have no way of knowing :(
duration,
None,
None,
)
}
#[cfg(test)]
mod test {
use chrono::prelude::*;

View file

@ -123,6 +123,8 @@ async fn sync_upload(
client.post_history(&buffer).await?;
cursor = buffer.last().unwrap().timestamp;
remote_count = client.count().await?;
debug!("upload cursor: {:?}", cursor);
}
Ok(())

View file

@ -1,5 +0,0 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"

View file

@ -7,7 +7,7 @@ use structopt::StructOpt;
use atuin_client::database::Database;
use atuin_client::history::History;
use atuin_client::import::Zsh;
use atuin_client::import::{bash::Bash, zsh::Zsh};
use indicatif::ProgressBar;
#[derive(StructOpt)]
@ -23,11 +23,17 @@ pub enum Cmd {
aliases=&["z", "zs"],
)]
Zsh,
#[structopt(
about="import history from the bash history file",
aliases=&["b", "ba", "bas"],
)]
Bash,
}
impl Cmd {
pub async fn run(&self, db: &mut (impl Database + Send + Sync)) -> Result<()> {
println!(" A'Tuin ");
println!(" Atuin ");
println!("======================");
println!(" \u{1f30d} ");
println!(" \u{1f418}\u{1f418}\u{1f418}\u{1f418} ");
@ -49,6 +55,7 @@ impl Cmd {
}
Self::Zsh => import_zsh(db).await,
Self::Bash => import_bash(db).await,
}
}
}
@ -120,3 +127,61 @@ async fn import_zsh(db: &mut (impl Database + Send + Sync)) -> Result<()> {
Ok(())
}
// TODO: don't just copy paste this lol
async fn import_bash(db: &mut (impl Database + Send + Sync)) -> Result<()> {
// 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 histpath = env::var("HISTFILE");
let histpath = if let Ok(p) = histpath {
let histpath = PathBuf::from(p);
if !histpath.exists() {
return Err(eyre!(
"Could not find history file {:?}. try updating $HISTFILE",
histpath
));
}
histpath
} else {
let user_dirs = UserDirs::new().unwrap();
let home_dir = user_dirs.home_dir();
home_dir.join(".bash_history")
};
let bash = Bash::new(histpath)?;
let progress = ProgressBar::new(bash.loc);
let buf_size = 100;
let mut buf = Vec::<History>::with_capacity(buf_size);
for i in bash
.filter_map(Result::ok)
.filter(|x| !x.command.trim().is_empty())
{
buf.push(i);
if buf.len() == buf_size {
db.save_bulk(&buf).await?;
progress.inc(buf.len() as u64);
buf.clear();
}
}
if !buf.is_empty() {
db.save_bulk(&buf).await?;
progress.inc(buf.len() as u64);
}
progress.finish();
println!("Import complete!");
Ok(())
}

View file

@ -1,19 +1,32 @@
use std::env;
use eyre::{eyre, Result};
use structopt::StructOpt;
#[derive(StructOpt)]
pub enum Cmd {
#[structopt(about = "zsh setup")]
Zsh,
#[structopt(about = "bash setup")]
Bash,
}
fn init_zsh() {
let full = include_str!("../shell/atuin.zsh");
println!("{}", full);
}
pub fn init() -> Result<()> {
let shell = env::var("SHELL")?;
fn init_bash() {
let full = include_str!("../shell/atuin.bash");
println!("{}", full);
}
if shell.ends_with("zsh") {
init_zsh();
impl Cmd {
pub fn run(&self) -> Result<()> {
match self {
Self::Zsh => init_zsh(),
Self::Bash => init_bash(),
}
Ok(())
} else {
Err(eyre!("Could not detect shell, or shell unsupported"))
}
}

View file

@ -37,7 +37,7 @@ pub enum AtuinCmd {
Stats(stats::Cmd),
#[structopt(about = "output shell setup")]
Init,
Init(init::Cmd),
#[structopt(about = "generates a UUID")]
Uuid,
@ -101,7 +101,7 @@ impl AtuinCmd {
Self::Import(import) => import.run(&mut db).await,
Self::Server(server) => server.run(&server_settings).await,
Self::Stats(stats) => stats.run(&mut db, &client_settings).await,
Self::Init => init::init(),
Self::Init(init) => init.run(),
Self::Search {
cwd,
exit,

30
src/shell/atuin.bash Normal file
View file

@ -0,0 +1,30 @@
_atuin_preexec() {
id=$(atuin history start "$1")
export ATUIN_HISTORY_ID="$id"
}
_atuin_precmd() {
local EXIT="$?"
[[ -z "${ATUIN_HISTORY_ID}" ]] && return
(RUST_LOG=error atuin history end $ATUIN_HISTORY_ID --exit $EXIT &) > /dev/null 2>&1
}
__atuin_history ()
{
tput rmkx
HISTORY="$(RUST_LOG=error atuin search -i $BUFFER 3>&1 1>&2 2>&3)"
tput smkx
READLINE_LINE=${HISTORY}
READLINE_POINT=${#READLINE_LINE}
}
preexec_functions+=(_atuin_preexec)
precmd_functions+=(_atuin_precmd)
bind -x '"\C-r": __atuin_history'

View file

@ -6,7 +6,7 @@ export ATUIN_HISTORY="atuin history list"
export ATUIN_BINDKEYS="true"
_atuin_preexec(){
id=$(atuin history start $1)
id=$(atuin history start "$1")
export ATUIN_HISTORY_ID="$id"
}