From 1751ac12f44fc49f19a82791a1ae5ae96068e46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan?= Date: Fri, 13 Oct 2023 20:45:36 +0200 Subject: [PATCH] allow multiple extensions (#10593) # Description This PR allows `open` to handle files with multiple extensions; i.e it will try to call `from tar.gz`, `from gz` when calling ```nu open file.tar.gz ``` # User-Facing Changes No breaking changes. --- crates/nu-command/src/filesystem/open.rs | 76 ++++++++++++++---------- crates/nu-command/tests/commands/open.rs | 63 ++++++++++++++++++++ 2 files changed, 109 insertions(+), 30 deletions(-) diff --git a/crates/nu-command/src/filesystem/open.rs b/crates/nu-command/src/filesystem/open.rs index bc8a02e793..76964c5cde 100644 --- a/crates/nu-command/src/filesystem/open.rs +++ b/crates/nu-command/src/filesystem/open.rs @@ -171,36 +171,35 @@ impl Command for Open { metadata: None, trim_end_newline: false, }; - let ext = if raw { + let exts_opt: Option> = if raw { None } else { - path.extension() - .map(|name| name.to_string_lossy().to_string().to_lowercase()) + let path_str = path + .file_name() + .unwrap_or(std::ffi::OsStr::new(path)) + .to_string_lossy() + .to_lowercase(); + Some(extract_extensions(path_str.as_str())) }; - if let Some(ext) = ext { - match engine_state.find_decl(format!("from {ext}").as_bytes(), &[]) { - Some(converter_id) => { - let decl = engine_state.get_decl(converter_id); - let command_output = if let Some(block_id) = decl.get_block_id() { - let block = engine_state.get_block(block_id); - eval_block( - engine_state, - stack, - block, - file_contents, - false, - false, - ) - } else { - decl.run( - engine_state, - stack, - &Call::new(call_span), - file_contents, - ) - }; - output.push(command_output.map_err(|inner| { + let converter = exts_opt.and_then(|exts| { + exts.iter().find_map(|ext| { + engine_state + .find_decl(format!("from {}", ext).as_bytes(), &[]) + .map(|id| (id, ext.to_string())) + }) + }); + + match converter { + Some((converter_id, ext)) => { + let decl = engine_state.get_decl(converter_id); + let command_output = if let Some(block_id) = decl.get_block_id() { + let block = engine_state.get_block(block_id); + eval_block(engine_state, stack, block, file_contents, false, false) + } else { + decl.run(engine_state, stack, &Call::new(call_span), file_contents) + }; + output.push(command_output.map_err(|inner| { ShellError::GenericError( format!("Error while parsing as {ext}"), format!("Could not parse '{}' with `from {}`", path.display(), ext), @@ -209,11 +208,8 @@ impl Command for Open { vec![inner], ) })?); - } - None => output.push(file_contents), } - } else { - output.push(file_contents) + None => output.push(file_contents), } } } @@ -265,3 +261,23 @@ fn permission_denied(dir: impl AsRef) -> bool { Ok(_) => false, } } + +fn extract_extensions(filename: &str) -> Vec { + let parts: Vec<&str> = filename.split('.').collect(); + let mut extensions: Vec = Vec::new(); + let mut current_extension = String::new(); + + for part in parts.iter().rev() { + if current_extension.is_empty() { + current_extension.push_str(part); + } else { + current_extension = format!("{}.{}", part, current_extension); + } + extensions.push(current_extension.clone()); + } + + extensions.pop(); + extensions.reverse(); + + extensions +} diff --git a/crates/nu-command/tests/commands/open.rs b/crates/nu-command/tests/commands/open.rs index b218f1f67e..68f7f5777f 100644 --- a/crates/nu-command/tests/commands/open.rs +++ b/crates/nu-command/tests/commands/open.rs @@ -34,6 +34,69 @@ fn parses_file_with_uppercase_extension() { }) } +#[test] +fn parses_file_with_multiple_extensions() { + Playground::setup("open_test_multiple_extensions", |dirs, sandbox| { + sandbox.with_files(vec![ + FileWithContent("file.tar.gz", "this is a tar.gz file"), + FileWithContent("file.tar.xz", "this is a tar.xz file"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + hide "from tar.gz" ; + hide "from gz" ; + + def "from tar.gz" [] { 'opened tar.gz' } ; + def "from gz" [] { 'opened gz' } ; + open file.tar.gz + "# + )); + + assert_eq!(actual.out, "opened tar.gz"); + + let actual2 = nu!( + cwd: dirs.test(), pipeline( + r#" + hide "from tar.xz" ; + hide "from xz" ; + hide "from tar" ; + + def "from tar" [] { 'opened tar' } ; + def "from xz" [] { 'opened xz' } ; + open file.tar.xz + "# + )); + + assert_eq!(actual2.out, "opened xz"); + }) +} + +#[test] +fn parses_dotfile() { + Playground::setup("open_test_dotfile", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + ".gitignore", + r#" + /target/ + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + hide "from gitignore" ; + + def "from gitignore" [] { 'opened gitignore' } ; + open .gitignore + "# + )); + + assert_eq!(actual.out, "opened gitignore"); + }) +} + #[test] fn parses_csv() { Playground::setup("open_test_1", |dirs, sandbox| {