Skip to content

Commit

Permalink
feat: use handlebars for command template (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
quantumsheep authored Feb 21, 2024
1 parent d3ecc21 commit 22d9479
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 83 deletions.
163 changes: 163 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ codegen-units = 1
clap = { version = "4.5.0", features = ["derive"] }
crossterm = "0.27.0"
fuzzy-matcher = "0.3.7"
handlebars = "5.1.0"
itertools = "0.12.1"
ratatui = "0.26.1"
regex = { version = "1.10.3", default-features = false, features = ["std"] }
serde = { version = "1.0.197", features = ["derive"] }
shellexpand = "3.1.0"
shlex = "1.3.0"
strum = "0.26.1"
Expand Down
7 changes: 4 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ struct Args {
#[arg(long, default_value_t = true)]
sort: bool,

#[arg(short, long)]
pattern: Option<String>,
/// Handlebars template of the command to execute
#[arg(short, long, default_value = "ssh {{#if user}}{{user}}@{{/if}}{{destination}}{{#if port}} -p {{port}}{{/if}}")]
template: String,

/// Exit after ending the SSH session
#[arg(short, long, default_value_t = false)]
Expand All @@ -41,7 +42,7 @@ fn main() -> Result<(), Box<dyn Error>> {
search_filter: args.search,
sort_by_name: args.sort,
show_proxy_command: args.show_proxy_command,
ssh_pattern: args.pattern,
command_template: args.template,
exit_after_ssh: args.exit,
})?;
app.start()?;
Expand Down
91 changes: 28 additions & 63 deletions src/ssh.rs
Original file line number Diff line number Diff line change
@@ -1,80 +1,45 @@
use handlebars::Handlebars;
use itertools::Itertools;
use regex::Regex;
use serde::Serialize;
use std::collections::VecDeque;
use std::error::Error;
use std::process::Command;

use crate::ssh_config::{self, HostVecExt};

#[derive(Debug, Clone)]
#[derive(Debug, Serialize, Clone)]
pub struct Host {
pub hostname: String,
pub name: String,
pub aliases: String,
pub user: Option<String>,
pub target: String,
pub destination: String,
pub port: Option<String>,
}

/// # Errors
///
/// Will return `Err` if the SSH command cannot be executed.
pub fn connect(host: &Host) -> Result<(), Box<dyn Error>> {
let mut command = Command::new("ssh");
impl Host {
/// Uses the provided Handlebars template to run a command.
///
/// # Errors
///
/// Will return `Err` if the command cannot be executed.
///
/// # Panics
///
/// Will panic if the regex cannot be compiled.
pub fn run_command_template(&self, pattern: &str) -> Result<(), Box<dyn Error>> {
let handlebars = Handlebars::new();
let command = handlebars.render_template(pattern, &self)?;

if let Some(user) = &host.user {
command.arg(format!("{}@{}", user, host.target));
} else {
command.arg(host.target.clone());
}

if let Some(port) = &host.port {
command.arg("-p").arg(port);
}
let mut args = shlex::split(&command)
.ok_or(format!("Failed to parse command: {command}"))?
.into_iter()
.collect::<VecDeque<String>>();
let command = args.pop_front().ok_or("Failed to get command")?;

command.spawn()?.wait()?;
Command::new(command).args(args).spawn()?.wait()?;

Ok(())
}

/// # Format
/// - %h - Hostname
/// - %u - User
/// - %p - Port
///
/// Use %% to escape the % character.
///
/// # Errors
///
/// Will return `Err` if the command cannot be executed.
///
/// # Panics
///
/// Will panic if the regex cannot be compiled.
pub fn run_with_pattern(pattern: &str, host: &Host) -> Result<(), Box<dyn Error>> {
let re = Regex::new(r"(?P<skip>%%)|(?P<h>%h)|(?P<u>%u)|(?P<p>%p)").unwrap();
let command = re.replace_all(pattern, |caps: &regex::Captures| {
if let Some(p) = caps.name("skip") {
p.as_str().to_string()
} else if caps.name("h").is_some() {
host.hostname.clone()
} else if caps.name("u").is_some() {
host.user.clone().unwrap_or_default()
} else if caps.name("p").is_some() {
host.port.clone().unwrap_or_default()
} else {
String::new()
}
});

let mut args = shlex::split(&command)
.ok_or(format!("Failed to parse command: {command}"))?
.into_iter()
.collect::<VecDeque<String>>();
let command = args.pop_front().ok_or("Failed to get command")?;

Command::new(command).args(args).spawn()?.wait()?;

Ok(())
Ok(())
}
}

/// # Errors
Expand All @@ -94,14 +59,14 @@ pub fn parse_config(raw_path: &String) -> Result<Vec<Host>, Box<dyn Error>> {
.iter()
.filter(|host| host.get(&ssh_config::EntryType::Hostname).is_some())
.map(|host| Host {
hostname: host
name: host
.get_patterns()
.first()
.unwrap_or(&String::new())
.clone(),
aliases: host.get_patterns().iter().skip(1).join(", "),
user: host.get(&ssh_config::EntryType::User),
target: host
destination: host
.get(&ssh_config::EntryType::Hostname)
.unwrap_or_default(),
port: host.get(&ssh_config::EntryType::Port),
Expand Down
Loading

0 comments on commit 22d9479

Please sign in to comment.