use crate::abbrs::Abbreviation; use crate::abbrs::{self}; use crate::abbrs::{with_abbrs, with_abbrs_mut}; use crate::complete::{CompletionList, CompletionReceiver}; use crate::env::{EnvMode, EnvStackSetResult}; use crate::expand::{expand_to_receiver, ExpandResultCode}; use crate::operation_context::{no_cancel, EXPANSION_LIMIT_DEFAULT}; use crate::parse_constants::ParseErrorList; use crate::parser::Parser; use crate::tests::prelude::*; use crate::wildcard::ANY_STRING; use crate::{ expand::{expand_string, ExpandFlags}, operation_context::OperationContext, wchar::prelude::*, }; use std::collections::hash_map::RandomState; use std::collections::HashSet; fn expand_test_impl( input: &wstr, flags: ExpandFlags, expected: Vec, error_message: Option<&str>, ) { let mut output = CompletionList::new(); let mut errors = ParseErrorList::new(); let pwd = PwdEnvironment::default(); let ctx = OperationContext::test_only_foreground( Parser::principal_parser().shared(), &pwd, Box::new(no_cancel), ); if expand_string( input.to_owned(), &mut output, flags, &ctx, Some(&mut errors), ) == ExpandResultCode::error { assert_ne!( errors, vec![], "Bug: Parse error reported but no error text found." ); panic!( "{}", errors[0].describe(input, ctx.parser().is_interactive()) ); } let expected_set: HashSet = HashSet::from_iter(expected); let output_set = HashSet::from_iter(output.into_iter().map(|c| c.completion)); assert_eq!( expected_set, output_set, "{}", error_message.unwrap_or("expand mismatch") ); } // Test globbing and other parameter expansion. #[test] #[serial] fn test_expand() { let _cleanup = test_init(); /// Perform parameter expansion and test if the output equals the zero-terminated parameter list /// supplied. /// /// \param in the string to expand /// \param flags the flags to send to expand_string /// \param ... A zero-terminated parameter list of values to test. /// After the zero terminator comes one more arg, a string, which is the error /// message to print if the test fails. macro_rules! expand_test { ($input:expr, $flags:expr, ( $($expected:expr),* $(,)? )) => { expand_test_impl(L!($input), $flags, vec![$( $expected.into(), )*], None) }; ($input:expr, $flags:expr, ( $($expected:expr),* $(,)? ), $error:literal) => { expand_test_impl(L!($input), $flags, vec![$( $expected.into(), )*], Some($error)) }; ($input:expr, $flags:expr, $expected:expr) => { expand_test_impl(L!($input), $flags, vec![ $expected.into() ], None) }; ($input:expr, $flags:expr, $expected:expr, $error:literal) => { expand_test_impl(L!($input), $flags, vec![ $expected.into() ], Some($error)) }; } // Testing parameter expansion let noflags = ExpandFlags::default(); expand_test!("foo", noflags, "foo", "Strings do not expand to themselves"); expand_test!( "a{b,c,d}e", noflags, ("abe", "ace", "ade"), "Bracket expansion is broken" ); expand_test!( "a*", ExpandFlags::SKIP_WILDCARDS, "a*", "Cannot skip wildcard expansion" ); expand_test!( "/bin/l\\0", ExpandFlags::FOR_COMPLETIONS, (), "Failed to handle null escape in expansion" ); expand_test!( "foo\\$bar", ExpandFlags::SKIP_VARIABLES, "foo$bar", "Failed to handle dollar sign in variable-skipping expansion" ); // bb // x // bar // baz // xxx // yyy // bax // xxx // lol // nub // q // .foo // aaa // aaa2 // x std::fs::create_dir_all("test/fish_expand_test/").unwrap(); std::fs::create_dir_all("test/fish_expand_test/bb/").unwrap(); std::fs::create_dir_all("test/fish_expand_test/baz/").unwrap(); std::fs::create_dir_all("test/fish_expand_test/bax/").unwrap(); std::fs::create_dir_all("test/fish_expand_test/lol/nub/").unwrap(); std::fs::create_dir_all("test/fish_expand_test/aaa/").unwrap(); std::fs::create_dir_all("test/fish_expand_test/aaa2/").unwrap(); std::fs::write("test/fish_expand_test/.foo", []).unwrap(); std::fs::write("test/fish_expand_test/bb/x", []).unwrap(); std::fs::write("test/fish_expand_test/bar", []).unwrap(); std::fs::write("test/fish_expand_test/bax/xxx", []).unwrap(); std::fs::write("test/fish_expand_test/baz/xxx", []).unwrap(); std::fs::write("test/fish_expand_test/baz/yyy", []).unwrap(); std::fs::write("test/fish_expand_test/lol/nub/q", []).unwrap(); std::fs::write("test/fish_expand_test/aaa2/x", []).unwrap(); // This is checking that .* does NOT match . and .. // (https://github.com/fish-shell/fish-shell/issues/270). But it does have to match literal // components (e.g. "./*" has to match the same as "*". expand_test!( "test/fish_expand_test/.*", noflags, "test/fish_expand_test/.foo", "Expansion not correctly handling dotfiles" ); expand_test!( "test/fish_expand_test/./.*", noflags, "test/fish_expand_test/./.foo", "Expansion not correctly handling literal path components in dotfiles" ); expand_test!( "test/fish_expand_test/*/xxx", noflags, ( "test/fish_expand_test/bax/xxx", "test/fish_expand_test/baz/xxx" ), "Glob did the wrong thing 1" ); expand_test!( "test/fish_expand_test/*z/xxx", noflags, "test/fish_expand_test/baz/xxx", "Glob did the wrong thing 2" ); expand_test!( "test/fish_expand_test/**z/xxx", noflags, "test/fish_expand_test/baz/xxx", "Glob did the wrong thing 3" ); expand_test!( "test/fish_expand_test////baz/xxx", noflags, "test/fish_expand_test////baz/xxx", "Glob did the wrong thing 3" ); expand_test!( "test/fish_expand_test/b**", noflags, ( "test/fish_expand_test/bb", "test/fish_expand_test/bb/x", "test/fish_expand_test/bar", "test/fish_expand_test/bax", "test/fish_expand_test/bax/xxx", "test/fish_expand_test/baz", "test/fish_expand_test/baz/xxx", "test/fish_expand_test/baz/yyy" ), "Glob did the wrong thing 4" ); // A trailing slash should only produce directories. expand_test!( "test/fish_expand_test/b*/", noflags, ( "test/fish_expand_test/bb/", "test/fish_expand_test/baz/", "test/fish_expand_test/bax/" ), "Glob did the wrong thing 5" ); expand_test!( "test/fish_expand_test/b**/", noflags, ( "test/fish_expand_test/bb/", "test/fish_expand_test/baz/", "test/fish_expand_test/bax/" ), "Glob did the wrong thing 6" ); expand_test!( "test/fish_expand_test/**/q", noflags, "test/fish_expand_test/lol/nub/q", "Glob did the wrong thing 7" ); expand_test!( "test/fish_expand_test/BA", ExpandFlags::FOR_COMPLETIONS, ( "test/fish_expand_test/bar", "test/fish_expand_test/bax/", "test/fish_expand_test/baz/" ), "Case insensitive test did the wrong thing" ); expand_test!( "test/fish_expand_test/BA", ExpandFlags::FOR_COMPLETIONS, ( "test/fish_expand_test/bar", "test/fish_expand_test/bax/", "test/fish_expand_test/baz/" ), "Case insensitive test did the wrong thing" ); expand_test!( "test/fish_expand_test/bb/yyy", ExpandFlags::FOR_COMPLETIONS, (), /* nothing! */ "Wrong fuzzy matching 1" ); expand_test!( "test/fish_expand_test/bb/x", ExpandFlags::FOR_COMPLETIONS | ExpandFlags::FUZZY_MATCH, "", // we just expect the empty string since this is an exact match "Wrong fuzzy matching 2" ); // Some vswprintfs refuse to append ANY_STRING in a format specifiers, so don't use // format_string here. let fuzzy_comp = ExpandFlags::FOR_COMPLETIONS | ExpandFlags::FUZZY_MATCH; let any_str_str = ANY_STRING.to_string(); expand_test!( "test/fish_expand_test/b/xx*", fuzzy_comp, ( (String::from("test/fish_expand_test/bax/xx") + &any_str_str), (String::from("test/fish_expand_test/baz/xx") + &any_str_str) ), "Wrong fuzzy matching 3" ); expand_test!( "test/fish_expand_test/b/yyy", fuzzy_comp, "test/fish_expand_test/baz/yyy", "Wrong fuzzy matching 4" ); expand_test!( "test/fish_expand_test/aa/x", fuzzy_comp, "test/fish_expand_test/aaa2/x", "Wrong fuzzy matching 5" ); expand_test!( "test/fish_expand_test/aaa/x", fuzzy_comp, (), "Wrong fuzzy matching 6 - shouldn't remove valid directory names (#3211)" ); // Dotfiles expand_test!( "test/fish_expand_test/.*", noflags, "test/fish_expand_test/.foo", "" ); // Literal path components in dotfiles. expand_test!( "test/fish_expand_test/./.*", noflags, "test/fish_expand_test/./.foo", "" ); pushd("test/fish_expand_test"); expand_test!( "b/xx", fuzzy_comp, ("bax/xxx", "baz/xxx"), "Wrong fuzzy matching 5" ); // multiple slashes with fuzzy matching - #3185 expand_test!("l///n", fuzzy_comp, "lol///nub/", "Wrong fuzzy matching 6"); popd(); } #[test] #[serial] fn test_expand_overflow() { let _cleanup = test_init(); // Testing overflowing expansions // Ensure that we have sane limits on number of expansions - see #7497. // Make a list of 64 elements, then expand it cartesian-style 64 times. // This is far too large to expand. let vals: Vec = (1..=64).map(|i| i.to_wstring()).collect(); let expansion = WString::from_str(&str::repeat("$bigvar", 64)); let parser = Parser::principal_parser().shared(); parser.vars().push(true); let set = parser.vars().set(L!("bigvar"), EnvMode::LOCAL, vals); assert_eq!(set, EnvStackSetResult::ENV_OK); let mut errors = ParseErrorList::new(); let ctx = OperationContext::foreground(parser.clone(), Box::new(no_cancel), EXPANSION_LIMIT_DEFAULT); // We accept only 1024 completions. let mut output = CompletionReceiver::new(1024); let res = expand_to_receiver( expansion, &mut output, ExpandFlags::default(), &ctx, Some(&mut errors), ); assert_ne!(errors, vec![]); assert_eq!(res, ExpandResultCode::error); parser.vars().pop(); } #[test] #[serial] fn test_abbreviations() { let _cleanup = test_init(); // Testing abbreviations with_abbrs_mut(|abbrset| { abbrset.add(Abbreviation::new( L!("gc").to_owned(), L!("gc").to_owned(), L!("git checkout").to_owned(), abbrs::Position::Command, false, )); abbrset.add(Abbreviation::new( L!("foo").to_owned(), L!("foo").to_owned(), L!("bar").to_owned(), abbrs::Position::Command, false, )); abbrset.add(Abbreviation::new( L!("gx").to_owned(), L!("gx").to_owned(), L!("git checkout").to_owned(), abbrs::Position::Command, false, )); abbrset.add(Abbreviation::new( L!("yin").to_owned(), L!("yin").to_owned(), L!("yang").to_owned(), abbrs::Position::Anywhere, false, )); }); // Helper to expand an abbreviation, enforcing we have no more than one result. let abbr_expand_1 = |token, pos| -> Option { let result = with_abbrs(|abbrset| abbrset.r#match(token, pos)); if result.is_empty() { return None; } assert_eq!( &result[1..], &[], "abbreviation expansion for {token} returned more than 1 result" ); Some(result.into_iter().next().unwrap().replacement) }; let cmd = abbrs::Position::Command; assert!( abbr_expand_1(L!(""), cmd).is_none(), "Unexpected success with empty abbreviation" ); assert!( abbr_expand_1(L!("nothing"), cmd).is_none(), "Unexpected success with missing abbreviation" ); assert_eq!( abbr_expand_1(L!("gc"), cmd), Some(L!("git checkout").into()) ); assert_eq!(abbr_expand_1(L!("foo"), cmd), Some(L!("bar").into())); }