mirror of
https://github.com/nushell/nushell
synced 2025-01-09 03:39:03 +00:00
9e1e2a4320
fixes #8551 # Description Use `open::commands` function to get list of command available for starting given path. run commands directly, providing environment, until one of them is successful. example of output if start was not successful: ``` ~\code\nushell> start ..\nustart\a.myext 09/12/2023 01:37:55 PM Error: nu:🐚:external_command × External command failed ╭─[entry #1:1:1] 1 │ start ..\nustart\a.myext · ─────────┬──────── · ╰── No command found to start with this path ╰──── help: Try different path or install appropriate command Command `cmd /c start "" "..\nustart\a.myext"` failed with exit code: 1 ``` # User-Facing Changes `start` command now provides environment to the external command. This is how it worked in `nu 0.72`, see linked issue. # Tests + Formatting `start` command didn't have any tests and this PR does not add any. Integration-level tests will require setup specific to OS and potentially change global environment on testing machine. For unit-level test it is possible to test `try_commands` function. But is still runs external commands, and robust test will require apriori knowledge which commands are necessary successful to run and which are not.
198 lines
7.2 KiB
Rust
198 lines
7.2 KiB
Rust
use itertools::Itertools;
|
|
use nu_engine::env_to_strings;
|
|
use nu_engine::CallExt;
|
|
use nu_path::canonicalize_with;
|
|
use nu_protocol::ast::Call;
|
|
use nu_protocol::engine::{Command, EngineState, Stack};
|
|
use nu_protocol::{
|
|
Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type,
|
|
};
|
|
use std::ffi::{OsStr, OsString};
|
|
use std::path::Path;
|
|
use std::process::Stdio;
|
|
|
|
#[derive(Clone)]
|
|
pub struct Start;
|
|
|
|
impl Command for Start {
|
|
fn name(&self) -> &str {
|
|
"start"
|
|
}
|
|
|
|
fn usage(&self) -> &str {
|
|
"Open a folder, file or website in the default application or viewer."
|
|
}
|
|
|
|
fn search_terms(&self) -> Vec<&str> {
|
|
vec!["load", "folder", "directory", "run", "open"]
|
|
}
|
|
|
|
fn signature(&self) -> nu_protocol::Signature {
|
|
Signature::build("start")
|
|
.input_output_types(vec![(Type::Nothing, Type::Any), (Type::String, Type::Any)])
|
|
.required("path", SyntaxShape::String, "path to open")
|
|
.category(Category::FileSystem)
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
_input: PipelineData,
|
|
) -> Result<PipelineData, ShellError> {
|
|
let path = call.req::<Spanned<String>>(engine_state, stack, 0)?;
|
|
let path = Spanned {
|
|
item: nu_utils::strip_ansi_string_unlikely(path.item),
|
|
span: path.span,
|
|
};
|
|
let path_no_whitespace = &path.item.trim_end_matches(|x| matches!(x, '\x09'..='\x0d'));
|
|
// only check if file exists in current current directory
|
|
let file_path = Path::new(path_no_whitespace);
|
|
if file_path.exists() {
|
|
open_path(path_no_whitespace, engine_state, stack, path.span)?;
|
|
} else if file_path.starts_with("https://") || file_path.starts_with("http://") {
|
|
let url = url::Url::parse(&path.item).map_err(|_| {
|
|
ShellError::GenericError(
|
|
format!("Cannot parse url: {}", &path.item),
|
|
"".to_string(),
|
|
Some(path.span),
|
|
Some("cannot parse".to_string()),
|
|
Vec::new(),
|
|
)
|
|
})?;
|
|
open_path(url.as_str(), engine_state, stack, path.span)?;
|
|
} else {
|
|
// try to distinguish between file not found and opening url without prefix
|
|
if let Ok(canon_path) =
|
|
canonicalize_with(path_no_whitespace, std::env::current_dir()?.as_path())
|
|
{
|
|
open_path(canon_path, engine_state, stack, path.span)?;
|
|
} else {
|
|
// open crate does not allow opening URL without prefix
|
|
let path_with_prefix = Path::new("https://").join(&path.item);
|
|
let common_domains = ["com", "net", "org", "edu", "sh"];
|
|
if let Some(url) = path_with_prefix.to_str() {
|
|
let url = url::Url::parse(url).map_err(|_| {
|
|
ShellError::GenericError(
|
|
format!("Cannot parse url: {}", &path.item),
|
|
"".to_string(),
|
|
Some(path.span),
|
|
Some("cannot parse".to_string()),
|
|
Vec::new(),
|
|
)
|
|
})?;
|
|
if let Some(domain) = url.host() {
|
|
let domain = domain.to_string();
|
|
let ext = Path::new(&domain).extension().and_then(|s| s.to_str());
|
|
if let Some(url_ext) = ext {
|
|
if common_domains.contains(&url_ext) {
|
|
open_path(url.as_str(), engine_state, stack, path.span)?;
|
|
}
|
|
}
|
|
}
|
|
return Err(ShellError::GenericError(
|
|
format!("Cannot find file or url: {}", &path.item),
|
|
"".to_string(),
|
|
Some(path.span),
|
|
Some("Use prefix https:// to disambiguate URLs from files".to_string()),
|
|
Vec::new(),
|
|
));
|
|
}
|
|
};
|
|
}
|
|
Ok(PipelineData::Empty)
|
|
}
|
|
|
|
fn examples(&self) -> Vec<nu_protocol::Example> {
|
|
vec![
|
|
Example {
|
|
description: "Open a text file with the default text editor",
|
|
example: "start file.txt",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Open an image with the default image viewer",
|
|
example: "start file.jpg",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Open the current directory with the default file manager",
|
|
example: "start .",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Open a pdf with the default pdf viewer",
|
|
example: "start file.pdf",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Open a website with default browser",
|
|
example: "start https://www.nushell.sh",
|
|
result: None,
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
fn open_path(
|
|
path: impl AsRef<OsStr>,
|
|
engine_state: &EngineState,
|
|
stack: &Stack,
|
|
span: Span,
|
|
) -> Result<(), ShellError> {
|
|
try_commands(open::commands(path), engine_state, stack, span)
|
|
}
|
|
|
|
fn try_commands(
|
|
commands: Vec<std::process::Command>,
|
|
engine_state: &EngineState,
|
|
stack: &Stack,
|
|
span: Span,
|
|
) -> Result<(), ShellError> {
|
|
let env_vars_str = env_to_strings(engine_state, stack)?;
|
|
commands
|
|
.into_iter()
|
|
.map(|mut cmd| {
|
|
let status = cmd
|
|
.envs(&env_vars_str)
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status();
|
|
match status {
|
|
Ok(status) if status.success() => Ok(()),
|
|
Ok(status) => Err(format!(
|
|
"\nCommand `{}` failed with {}",
|
|
format_command(&cmd),
|
|
status
|
|
)),
|
|
Err(err) => Err(format!(
|
|
"\nCommand `{}` failed with {}",
|
|
format_command(&cmd),
|
|
err
|
|
)),
|
|
}
|
|
})
|
|
.take_while_inclusive(|result| result.is_err())
|
|
.fold(Err("".to_string()), |combined_result, next_result| {
|
|
combined_result.or_else(|combined_message| {
|
|
next_result.map_err(|next_message| combined_message + &next_message)
|
|
})
|
|
})
|
|
.map_err(|message| ShellError::ExternalCommand {
|
|
label: "No command found to start with this path".to_string(),
|
|
help: "Try different path or install appropriate command\n".to_string() + &message,
|
|
span,
|
|
})
|
|
}
|
|
|
|
fn format_command(command: &std::process::Command) -> String {
|
|
let parts_iter = std::iter::repeat(command.get_program())
|
|
.take(1)
|
|
.chain(command.get_args());
|
|
Itertools::intersperse(parts_iter, " ".as_ref())
|
|
.collect::<OsString>()
|
|
.to_string_lossy()
|
|
.into_owned()
|
|
}
|