mirror of
https://github.com/denisidoro/navi
synced 2025-02-16 20:48:28 +00:00
Merge pull request #259 from mrVanDalo/feature/allow-extra
add `--allow-extra` option
This commit is contained in:
commit
b809739113
5 changed files with 239 additions and 73 deletions
|
@ -176,7 +176,13 @@ $ y: echo -e "$((x+10))\n$((x+20))"
|
|||
|
||||
### Variable options
|
||||
|
||||
For lines starting with `$` you can add extra options using `---`.
|
||||
For lines starting with `$` you can add use`---` to parse parameters to `fzf`.
|
||||
* `--allow-extra` *(experimental)*: handles `fzf` option `--print-query`. `enter` will prefer a selection,
|
||||
`tab` will prefer the query typed.
|
||||
* `--multi` : forwarded option to `fzf`.
|
||||
* `--header-lines` : forwarded option to `fzf`
|
||||
* `--column` : forwarded option to `fzf`.
|
||||
* `--delimiter` : forwarded option to `fzf`.
|
||||
|
||||
#### Table formatting
|
||||
|
||||
|
|
91
src/cheat.rs
91
src/cheat.rs
|
@ -7,14 +7,29 @@ use std::collections::HashMap;
|
|||
use std::fs;
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct SuggestionOpts {
|
||||
pub header_lines: u8,
|
||||
pub column: Option<u8>,
|
||||
pub multi: bool,
|
||||
pub delimiter: Option<String>,
|
||||
pub suggestion_type: SuggestionType,
|
||||
}
|
||||
|
||||
pub type Value = (String, Option<SuggestionOpts>);
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum SuggestionType {
|
||||
/// fzf will not print any suggestions.
|
||||
Disabled,
|
||||
/// fzf will only select one of the suggestions
|
||||
SingleSelection,
|
||||
/// fzf will select multiple ones of the suggestions
|
||||
MultipleSelections,
|
||||
/// fzf will select one of the suggestions or use the Query
|
||||
SingleRecommendation,
|
||||
/// initial snippet selection
|
||||
SnippetSelection,
|
||||
}
|
||||
|
||||
pub type Suggestion = (String, Option<SuggestionOpts>);
|
||||
|
||||
fn gen_snippet(snippet: &str, line: &str) -> String {
|
||||
if snippet.is_empty() {
|
||||
|
@ -32,6 +47,7 @@ fn parse_opts(text: &str) -> SuggestionOpts {
|
|||
let mut header_lines: u8 = 0;
|
||||
let mut column: Option<u8> = None;
|
||||
let mut multi = false;
|
||||
let mut allow_extra = false;
|
||||
let mut delimiter: Option<String> = None;
|
||||
|
||||
let mut parts = text.split(' ');
|
||||
|
@ -39,6 +55,7 @@ fn parse_opts(text: &str) -> SuggestionOpts {
|
|||
while let Some(p) = parts.next() {
|
||||
match p {
|
||||
"--multi" => multi = true,
|
||||
"--allow-extra" => allow_extra = true,
|
||||
"--header" | "--headers" | "--header-lines" => {
|
||||
header_lines = remove_quote(parts.next().unwrap()).parse::<u8>().unwrap()
|
||||
}
|
||||
|
@ -51,8 +68,12 @@ fn parse_opts(text: &str) -> SuggestionOpts {
|
|||
SuggestionOpts {
|
||||
header_lines,
|
||||
column,
|
||||
multi,
|
||||
delimiter,
|
||||
suggestion_type: match (multi, allow_extra) {
|
||||
(true, _) => SuggestionType::MultipleSelections, // multi wins over allow-extra
|
||||
(false, true) => SuggestionType::SingleRecommendation,
|
||||
(false, false) => SuggestionType::SingleSelection,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,16 +83,13 @@ fn parse_variable_line(line: &str) -> (&str, &str, Option<SuggestionOpts>) {
|
|||
let variable = caps.get(1).unwrap().as_str().trim();
|
||||
let mut command_plus_opts = caps.get(2).unwrap().as_str().split("---");
|
||||
let command = command_plus_opts.next().unwrap();
|
||||
let opts = match command_plus_opts.next() {
|
||||
Some(o) => Some(parse_opts(o)),
|
||||
None => None,
|
||||
};
|
||||
(variable, command, opts)
|
||||
let command_options = command_plus_opts.next().map(parse_opts);
|
||||
(variable, command, command_options)
|
||||
}
|
||||
|
||||
fn read_file(
|
||||
path: &str,
|
||||
variables: &mut HashMap<String, Value>,
|
||||
variables: &mut HashMap<String, Suggestion>,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
) {
|
||||
let mut tags = String::from("");
|
||||
|
@ -97,7 +115,7 @@ fn read_file(
|
|||
}
|
||||
// variable
|
||||
else if line.starts_with('$') {
|
||||
let (variable, command, opts) = parse_variable_line(&line[..]);
|
||||
let (variable, command, opts) = parse_variable_line(&line);
|
||||
variables.insert(
|
||||
format!("{};{}", tags, variable),
|
||||
(String::from(command), opts),
|
||||
|
@ -135,8 +153,11 @@ fn read_file(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn read_all(config: &Config, stdin: &mut std::process::ChildStdin) -> HashMap<String, Value> {
|
||||
let mut variables: HashMap<String, Value> = HashMap::new();
|
||||
pub fn read_all(
|
||||
config: &Config,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
) -> HashMap<String, Suggestion> {
|
||||
let mut variables: HashMap<String, Suggestion> = HashMap::new();
|
||||
|
||||
let mut fallback: String = String::from("");
|
||||
let folders_str = config.path.as_ref().unwrap_or_else(|| {
|
||||
|
@ -161,3 +182,49 @@ pub fn read_all(config: &Config, stdin: &mut std::process::ChildStdin) -> HashMa
|
|||
|
||||
variables
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_variable_line() {
|
||||
let (variable, command, command_options) =
|
||||
parse_variable_line("$ user : echo -e \"$(whoami)\\nroot\" --- --allow-extra");
|
||||
assert_eq!(command, " echo -e \"$(whoami)\\nroot\" ");
|
||||
assert_eq!(variable, "user");
|
||||
assert_eq!(
|
||||
command_options,
|
||||
Some(SuggestionOpts {
|
||||
header_lines: 0,
|
||||
column: None,
|
||||
delimiter: None,
|
||||
suggestion_type: SuggestionType::SingleRecommendation
|
||||
})
|
||||
);
|
||||
}
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
#[test]
|
||||
fn test_read_file() {
|
||||
let path = "tests/cheats/ssh.cheat";
|
||||
let mut variables: HashMap<String, Suggestion> = HashMap::new();
|
||||
let mut child = Command::new("cat").stdin(Stdio::piped()).spawn().unwrap();
|
||||
let child_stdin = child.stdin.as_mut().unwrap();
|
||||
read_file(path, &mut variables, child_stdin);
|
||||
let mut result: HashMap<String, (String, std::option::Option<_>)> = HashMap::new();
|
||||
result.insert(
|
||||
"ssh;user".to_string(),
|
||||
(
|
||||
r#" echo -e "$(whoami)\nroot" "#.to_string(),
|
||||
Some(SuggestionOpts {
|
||||
header_lines: 0,
|
||||
column: None,
|
||||
delimiter: None,
|
||||
suggestion_type: SuggestionType::SingleRecommendation,
|
||||
}),
|
||||
),
|
||||
);
|
||||
assert_eq!(variables, result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use crate::display;
|
|||
use crate::fzf;
|
||||
use crate::option::Config;
|
||||
|
||||
use crate::cheat::SuggestionType;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
|
@ -22,7 +23,7 @@ fn gen_core_fzf_opts(variant: Variant, config: &Config) -> fzf::Opts {
|
|||
preview: !config.no_preview,
|
||||
autoselect: !config.no_autoselect,
|
||||
overrides: config.fzf_overrides.as_ref(),
|
||||
copyable: true,
|
||||
suggestion_type: SuggestionType::SnippetSelection,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
@ -35,8 +36,8 @@ fn gen_core_fzf_opts(variant: Variant, config: &Config) -> fzf::Opts {
|
|||
opts
|
||||
}
|
||||
|
||||
fn extract_from_selections(raw_output: &str, contains_key: bool) -> (&str, &str, &str) {
|
||||
let mut lines = raw_output.split('\n');
|
||||
fn extract_from_selections(raw_snippet: &str, contains_key: bool) -> (&str, &str, &str) {
|
||||
let mut lines = raw_snippet.split('\n');
|
||||
let key = if contains_key {
|
||||
lines.next().unwrap()
|
||||
} else {
|
||||
|
@ -58,20 +59,20 @@ fn extract_from_selections(raw_output: &str, contains_key: bool) -> (&str, &str,
|
|||
fn prompt_with_suggestions(
|
||||
varname: &str,
|
||||
config: &Config,
|
||||
suggestion: &cheat::Value,
|
||||
suggestion: &cheat::Suggestion,
|
||||
values: &HashMap<String, String>,
|
||||
) -> String {
|
||||
let mut vars_cmd = String::from("");
|
||||
for (k, v) in values.iter() {
|
||||
vars_cmd.push_str(format!("{}=\"{}\"; ", k, v).as_str());
|
||||
for (key, value) in values.iter() {
|
||||
vars_cmd.push_str(format!("{}=\"{}\"; ", key, value).as_str());
|
||||
}
|
||||
|
||||
let cmd = format!("{vars} {cmd}", vars = vars_cmd, cmd = &suggestion.0);
|
||||
let (suggestion_command, suggestion_options) = &suggestion;
|
||||
let command = format!("{} {}", vars_cmd, suggestion_command);
|
||||
|
||||
let child = Command::new("bash")
|
||||
.stdout(Stdio::piped())
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.arg(command)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
|
@ -88,8 +89,8 @@ fn prompt_with_suggestions(
|
|||
let mut column: Option<u8> = None;
|
||||
let mut delimiter = r"\s\s+";
|
||||
|
||||
if let Some(o) = &suggestion.1 {
|
||||
opts.multi = o.multi;
|
||||
if let Some(o) = &suggestion_options {
|
||||
opts.suggestion_type = o.suggestion_type;
|
||||
opts.header_lines = o.header_lines;
|
||||
column = o.column;
|
||||
if let Some(d) = o.delimiter.as_ref() {
|
||||
|
@ -114,12 +115,12 @@ fn prompt_with_suggestions(
|
|||
}
|
||||
}
|
||||
|
||||
fn prompt_without_suggestions(varname: &str) -> String {
|
||||
fn prompt_without_suggestions(variable_name: &str) -> String {
|
||||
let opts = fzf::Opts {
|
||||
preview: false,
|
||||
autoselect: false,
|
||||
suggestions: false,
|
||||
prompt: Some(display::variable_prompt(varname)),
|
||||
prompt: Some(display::variable_prompt(variable_name)),
|
||||
suggestion_type: SuggestionType::Disabled,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
@ -139,29 +140,33 @@ fn gen_replacement(value: &str) -> String {
|
|||
fn replace_variables_from_snippet(
|
||||
snippet: &str,
|
||||
tags: &str,
|
||||
variables: HashMap<String, cheat::Value>,
|
||||
variables: HashMap<String, cheat::Suggestion>,
|
||||
config: &Config,
|
||||
) -> String {
|
||||
let mut interpolated_snippet = String::from(snippet);
|
||||
let mut values: HashMap<String, String> = HashMap::new();
|
||||
|
||||
let re = Regex::new(r"<(\w[\w\d\-_]*)>").unwrap();
|
||||
for cap in re.captures_iter(snippet) {
|
||||
let bracketed_varname = &cap[0];
|
||||
let varname = &bracketed_varname[1..bracketed_varname.len() - 1];
|
||||
for captures in re.captures_iter(snippet) {
|
||||
let bracketed_variable_name = &captures[0];
|
||||
let variable_name = &bracketed_variable_name[1..bracketed_variable_name.len() - 1];
|
||||
|
||||
if values.get(varname).is_none() {
|
||||
let k = format!("{};{}", tags, varname);
|
||||
if values.get(variable_name).is_none() {
|
||||
let key = format!("{};{}", tags, variable_name);
|
||||
|
||||
let value = match variables.get(&k[..]) {
|
||||
Some(suggestion) => prompt_with_suggestions(varname, &config, suggestion, &values),
|
||||
None => prompt_without_suggestions(varname),
|
||||
let value = match variables.get(&key[..]) {
|
||||
Some(suggestion) => {
|
||||
prompt_with_suggestions(variable_name, &config, suggestion, &values)
|
||||
}
|
||||
None => prompt_without_suggestions(variable_name),
|
||||
};
|
||||
|
||||
values.insert(varname.to_string(), value.clone());
|
||||
values.insert(variable_name.to_string(), value.clone());
|
||||
|
||||
interpolated_snippet = interpolated_snippet
|
||||
.replace(bracketed_varname, gen_replacement(&value[..]).as_str());
|
||||
interpolated_snippet = interpolated_snippet.replace(
|
||||
bracketed_variable_name,
|
||||
gen_replacement(&value[..]).as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,11 +176,11 @@ fn replace_variables_from_snippet(
|
|||
pub fn main(variant: Variant, config: Config, contains_key: bool) -> Result<(), Box<dyn Error>> {
|
||||
let _ = display::WIDTHS;
|
||||
|
||||
let (raw_output, variables) = fzf::call(gen_core_fzf_opts(variant, &config), |stdin| {
|
||||
let (raw_selection, variables) = fzf::call(gen_core_fzf_opts(variant, &config), |stdin| {
|
||||
Some(cheat::read_all(&config, stdin))
|
||||
});
|
||||
|
||||
let (key, tags, snippet) = extract_from_selections(&raw_output[..], contains_key);
|
||||
let (key, tags, snippet) = extract_from_selections(&raw_selection[..], contains_key);
|
||||
let interpolated_snippet =
|
||||
replace_variables_from_snippet(snippet, tags, variables.unwrap(), &config);
|
||||
|
||||
|
|
146
src/fzf.rs
146
src/fzf.rs
|
@ -2,6 +2,8 @@ use crate::cheat;
|
|||
use crate::display;
|
||||
use crate::filesystem;
|
||||
|
||||
use crate::cheat::SuggestionType;
|
||||
use crate::cheat::SuggestionType::SingleSelection;
|
||||
use std::collections::HashMap;
|
||||
use std::process;
|
||||
use std::process::{Command, Stdio};
|
||||
|
@ -14,9 +16,7 @@ pub struct Opts<'a> {
|
|||
pub autoselect: bool,
|
||||
pub overrides: Option<&'a String>, // TODO: remove &'a
|
||||
pub header_lines: u8,
|
||||
pub multi: bool,
|
||||
pub copyable: bool,
|
||||
pub suggestions: bool,
|
||||
pub suggestion_type: SuggestionType,
|
||||
}
|
||||
|
||||
impl Default for Opts<'_> {
|
||||
|
@ -29,20 +29,18 @@ impl Default for Opts<'_> {
|
|||
overrides: None,
|
||||
header_lines: 0,
|
||||
prompt: None,
|
||||
multi: false,
|
||||
copyable: false,
|
||||
suggestions: true,
|
||||
suggestion_type: SingleSelection,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call<F>(opts: Opts, stdin_fn: F) -> (String, Option<HashMap<String, cheat::Value>>)
|
||||
pub fn call<F>(opts: Opts, stdin_fn: F) -> (String, Option<HashMap<String, cheat::Suggestion>>)
|
||||
where
|
||||
F: Fn(&mut process::ChildStdin) -> Option<HashMap<String, cheat::Value>>,
|
||||
F: Fn(&mut process::ChildStdin) -> Option<HashMap<String, cheat::Suggestion>>,
|
||||
{
|
||||
let mut c = Command::new("fzf");
|
||||
let mut fzf_command = Command::new("fzf");
|
||||
|
||||
c.args(&[
|
||||
fzf_command.args(&[
|
||||
"--preview-window",
|
||||
"up:2",
|
||||
"--with-nth",
|
||||
|
@ -56,38 +54,46 @@ where
|
|||
]);
|
||||
|
||||
if opts.autoselect {
|
||||
c.arg("--select-1");
|
||||
fzf_command.arg("--select-1");
|
||||
}
|
||||
|
||||
if opts.multi {
|
||||
c.arg("--multi");
|
||||
}
|
||||
|
||||
if opts.copyable {
|
||||
c.args(&["--expect", "ctrl-y,enter"]);
|
||||
match opts.suggestion_type {
|
||||
SuggestionType::MultipleSelections => {
|
||||
fzf_command.arg("--multi");
|
||||
}
|
||||
SuggestionType::Disabled => {
|
||||
fzf_command.args(&["--print-query", "--no-select-1", "--height", "1"]);
|
||||
}
|
||||
SuggestionType::SnippetSelection => {
|
||||
fzf_command.args(&["--expect", "ctrl-y,enter"]);
|
||||
}
|
||||
SuggestionType::SingleRecommendation => {
|
||||
fzf_command.args(&["--print-query", "--expect", "tab,enter"]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if opts.preview {
|
||||
c.args(&[
|
||||
fzf_command.args(&[
|
||||
"--preview",
|
||||
format!("{} preview {{}}", filesystem::exe_string()).as_str(),
|
||||
]);
|
||||
}
|
||||
|
||||
if let Some(q) = opts.query {
|
||||
c.args(&["--query", &q]);
|
||||
fzf_command.args(&["--query", &q]);
|
||||
}
|
||||
|
||||
if let Some(f) = opts.filter {
|
||||
c.args(&["--filter", &f]);
|
||||
fzf_command.args(&["--filter", &f]);
|
||||
}
|
||||
|
||||
if let Some(p) = opts.prompt {
|
||||
c.args(&["--prompt", &p]);
|
||||
fzf_command.args(&["--prompt", &p]);
|
||||
}
|
||||
|
||||
if opts.header_lines > 0 {
|
||||
c.args(&["--header-lines", format!("{}", opts.header_lines).as_str()]);
|
||||
fzf_command.args(&["--header-lines", format!("{}", opts.header_lines).as_str()]);
|
||||
}
|
||||
|
||||
if let Some(o) = opts.overrides {
|
||||
|
@ -96,15 +102,11 @@ where
|
|||
.map(|s| s.to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.for_each(|s| {
|
||||
c.arg(s);
|
||||
fzf_command.arg(s);
|
||||
});
|
||||
}
|
||||
|
||||
if !opts.suggestions {
|
||||
c.args(&["--print-query", "--no-select-1", "--height", "1"]);
|
||||
}
|
||||
|
||||
let child = c
|
||||
let child = fzf_command
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
|
@ -123,7 +125,7 @@ where
|
|||
|
||||
let out = child.wait_with_output().unwrap();
|
||||
|
||||
let mut text = match out.status.code() {
|
||||
let text = match out.status.code() {
|
||||
Some(0) | Some(1) => String::from_utf8(out.stdout).unwrap(),
|
||||
Some(130) => process::exit(130),
|
||||
_ => {
|
||||
|
@ -132,7 +134,87 @@ where
|
|||
panic!("External command failed:\n {}", err)
|
||||
}
|
||||
};
|
||||
text.truncate(text.len() - 1);
|
||||
|
||||
(text, result)
|
||||
(parse_output_single(text, opts.suggestion_type), result)
|
||||
}
|
||||
|
||||
fn parse_output_single(mut text: String, suggestion_type: SuggestionType) -> String {
|
||||
match suggestion_type {
|
||||
SuggestionType::SingleSelection => text.lines().next().unwrap().to_string(),
|
||||
SuggestionType::MultipleSelections
|
||||
| SuggestionType::Disabled
|
||||
| SuggestionType::SnippetSelection => {
|
||||
text.truncate(text.len() - 1);
|
||||
text
|
||||
}
|
||||
SuggestionType::SingleRecommendation => {
|
||||
let lines: Vec<&str> = text.lines().collect();
|
||||
|
||||
match (lines.get(0), lines.get(1), lines.get(2)) {
|
||||
(Some(one), Some(termination), Some(two)) if *termination == "enter" => {
|
||||
if two.is_empty() {
|
||||
one.to_string()
|
||||
} else {
|
||||
two.to_string()
|
||||
}
|
||||
}
|
||||
(Some(one), Some(termination), None) if *termination == "enter" => one.to_string(),
|
||||
(Some(one), Some(termination), _) if *termination == "tab" => one.to_string(),
|
||||
_ => "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_output1() {
|
||||
let text = "palo\n".to_string();
|
||||
let output = parse_output_single(text, SuggestionType::SingleSelection);
|
||||
assert_eq!(output, "palo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_output2() {
|
||||
let text = "\nenter\npalo".to_string();
|
||||
let output = parse_output_single(text, SuggestionType::SingleRecommendation);
|
||||
assert_eq!(output, "palo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_recommendation_output_1() {
|
||||
let text = "\nenter\npalo".to_string();
|
||||
let output = parse_output_single(text, SuggestionType::SingleRecommendation);
|
||||
assert_eq!(output, "palo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_recommendation_output_2() {
|
||||
let text = "p\nenter\npalo".to_string();
|
||||
let output = parse_output_single(text, SuggestionType::SingleRecommendation);
|
||||
assert_eq!(output, "palo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_recommendation_output_3() {
|
||||
let text = "peter\nenter\n".to_string();
|
||||
let output = parse_output_single(text, SuggestionType::SingleRecommendation);
|
||||
assert_eq!(output, "peter");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_output3() {
|
||||
let text = "p\ntab\npalo".to_string();
|
||||
let output = parse_output_single(text, SuggestionType::SingleRecommendation);
|
||||
assert_eq!(output, "p");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_snippet_request() {
|
||||
let text = "enter\nssh ⠀login to a server and forward to ssh key (d… ⠀ssh -A <user>@<server> ⠀ssh ⠀login to a server and forward to ssh key (dangerous but usefull for bastion hosts) ⠀ssh -A <user>@<server> ⠀\n".to_string();
|
||||
let output = parse_output_single(text, SuggestionType::SnippetSelection);
|
||||
assert_eq!(output, "enter\nssh ⠀login to a server and forward to ssh key (d… ⠀ssh -A <user>@<server> ⠀ssh ⠀login to a server and forward to ssh key (dangerous but usefull for bastion hosts) ⠀ssh -A <user>@<server> ⠀");
|
||||
}
|
||||
}
|
||||
|
|
6
tests/cheats/ssh.cheat
Normal file
6
tests/cheats/ssh.cheat
Normal file
|
@ -0,0 +1,6 @@
|
|||
% ssh
|
||||
|
||||
# login to a server with a key and port
|
||||
ssh -i <sshkey> -p <port> <user>@<server>
|
||||
|
||||
$ user : echo -e "$(whoami)\nroot" --- --allow-extra
|
Loading…
Add table
Reference in a new issue