1use std::env;
11use std::ffi::OsStr;
12use std::io::{self, Stdout};
13use std::path::{Path, PathBuf};
14
15pub fn get_text_stdout() -> Stdout {
34 io::stdout()
35}
36
37pub fn get_text_stderr() -> io::Stderr {
51 io::stderr()
52}
53
54pub fn get_binary_stdout() -> Stdout {
59 io::stdout()
60}
61
62pub fn get_binary_stdin() -> io::Stdin {
66 io::stdin()
67}
68
69pub fn format_filename(path: &Path) -> String {
89 let path_str = path.to_string_lossy();
90
91 if let Some(home) = home_dir() {
93 let home_str = home.to_string_lossy();
94 if path_str.starts_with(home_str.as_ref()) {
95 let relative = &path_str[home_str.len()..];
96 let relative = relative.trim_start_matches(['/', '\\']);
97 return format!("~/{}", relative.replace('\\', "/"));
98 }
99 }
100
101 path_str.replace('\\', "/")
103}
104
105pub fn get_app_dir(app_name: &str, _roaming: bool) -> PathBuf {
128 #[cfg(target_os = "windows")]
129 {
130 let base = if _roaming {
131 env::var("APPDATA").ok()
132 } else {
133 env::var("LOCALAPPDATA").ok()
134 };
135
136 match base {
137 Some(base) => PathBuf::from(base).join(app_name),
138 None => PathBuf::from(".").join(app_name),
139 }
140 }
141
142 #[cfg(target_os = "macos")]
143 {
144 if let Some(home) = home_dir() {
145 home.join("Library")
146 .join("Application Support")
147 .join(app_name)
148 } else {
149 PathBuf::from(".").join(app_name)
150 }
151 }
152
153 #[cfg(all(unix, not(target_os = "macos")))]
154 {
155 if _roaming {
156 if let Ok(xdg_data) = env::var("XDG_DATA_HOME") {
158 return PathBuf::from(xdg_data).join(app_name);
159 }
160 if let Some(home) = home_dir() {
161 return home.join(".local").join("share").join(app_name);
162 }
163 }
164
165 if let Some(home) = home_dir() {
167 home.join(format!(".{}", app_name))
168 } else {
169 PathBuf::from(".").join(format!(".{}", app_name))
170 }
171 }
172}
173
174pub fn expand_path(path: &str) -> PathBuf {
194 let mut result = path.to_string();
195
196 if result.starts_with('~') {
198 if let Some(home) = home_dir() {
199 let home_str = home.to_string_lossy();
200 if result == "~" {
201 return home;
202 } else if result.starts_with("~/") || result.starts_with("~\\") {
203 result = format!("{}{}", home_str, &result[1..]);
204 }
205 }
206 }
207
208 result = expand_env_vars(&result);
210
211 PathBuf::from(result)
212}
213
214fn expand_env_vars(s: &str) -> String {
218 let mut result = String::with_capacity(s.len());
219 let mut chars = s.chars().peekable();
220
221 while let Some(c) = chars.next() {
222 if c == '$' {
223 if chars.peek() == Some(&'{') {
224 chars.next(); let var_name: String = chars.by_ref().take_while(|&c| c != '}').collect();
227 if let Ok(value) = env::var(&var_name) {
228 result.push_str(&value);
229 }
230 } else {
231 let mut var_name = String::new();
234 while let Some(&next_c) = chars.peek() {
235 if next_c.is_alphanumeric() || next_c == '_' {
236 var_name.push(next_c);
237 chars.next();
238 } else {
239 break;
240 }
241 }
242 if !var_name.is_empty() {
243 if let Ok(value) = env::var(&var_name) {
244 result.push_str(&value);
245 }
246 }
247 }
248 } else {
249 result.push(c);
250 }
251 }
252
253 result
254}
255
256pub fn home_dir() -> Option<PathBuf> {
260 if let Ok(home) = env::var("HOME") {
262 return Some(PathBuf::from(home));
263 }
264
265 #[cfg(target_os = "windows")]
267 {
268 if let Ok(profile) = env::var("USERPROFILE") {
269 return Some(PathBuf::from(profile));
270 }
271
272 if let (Ok(drive), Ok(path)) = (env::var("HOMEDRIVE"), env::var("HOMEPATH")) {
274 return Some(PathBuf::from(format!("{}{}", drive, path)));
275 }
276 }
277
278 None
279}
280
281pub fn get_os_args() -> Vec<String> {
298 env::args().collect()
299}
300
301pub fn get_os_args_skip_program() -> Vec<String> {
305 env::args().skip(1).collect()
306}
307
308pub fn should_strip_ansi() -> bool {
329 if env::var("NO_COLOR").is_ok() {
331 return true;
332 }
333
334 if let Ok(term) = env::var("TERM") {
336 if term == "dumb" {
337 return true;
338 }
339 }
340
341 if env::var("COLORTERM").is_ok() {
343 return false;
344 }
345
346 if let Ok(term) = env::var("TERM") {
348 if term.contains("color") || term.contains("256") || term.contains("xterm") {
349 return false;
350 }
351 }
352
353 false
355}
356
357pub fn is_tty() -> bool {
359 !should_strip_ansi()
360}
361
362pub fn get_terminal_width() -> Option<usize> {
370 if let Ok(cols) = env::var("COLUMNS") {
372 if let Ok(width) = cols.parse::<usize>() {
373 if width > 0 {
374 return Some(width);
375 }
376 }
377 }
378
379 None
382}
383
384pub fn safecall<T: AsRef<str>>(s: T) -> String {
392 let s = s.as_ref();
393 let mut result = String::with_capacity(s.len());
394
395 for c in s.chars() {
396 if c.is_control() && c != '\n' && c != '\t' && c != '\r' {
397 result.push_str(&format!("\\x{:02x}", c as u32));
399 } else {
400 result.push(c);
401 }
402 }
403
404 result
405}
406
407pub fn pluralize<'a>(count: usize, singular: &'a str, plural: &'a str) -> &'a str {
419 if count == 1 {
420 singular
421 } else {
422 plural
423 }
424}
425
426pub fn join_with_conjunction(items: &[&str], separator: &str, conjunction: &str) -> String {
443 match items.len() {
444 0 => String::new(),
445 1 => items[0].to_string(),
446 2 => format!("{}{}{}", items[0], conjunction, items[1]),
447 _ => {
448 let (last, rest) = items.split_last().unwrap();
449 format!("{}{}{}", rest.join(separator), conjunction, last)
450 }
451 }
452}
453
454pub fn make_safe_filename(filename: &str) -> String {
471 const UNSAFE_CHARS: &[char] = &['<', '>', ':', '"', '/', '\\', '|', '?', '*', '\0'];
473
474 let mut result = String::with_capacity(filename.len());
475 let mut last_was_space = false;
476
477 for c in filename.chars() {
478 if UNSAFE_CHARS.contains(&c) {
479 continue;
481 } else if c.is_whitespace() {
482 if !last_was_space {
484 result.push('_');
485 last_was_space = true;
486 }
487 } else if c.is_control() {
488 continue;
490 } else {
491 result.push(c);
492 last_was_space = false;
493 }
494 }
495
496 result.trim_end_matches('_').to_string()
498}
499
500pub fn get_extension(path: &Path) -> Option<&str> {
502 path.extension().and_then(OsStr::to_str)
503}
504
505pub fn strip_extension(path: &Path) -> PathBuf {
507 let mut result = path.to_path_buf();
508 result.set_extension("");
509 result
510}
511
512pub fn split_arg_string(s: &str) -> Vec<String> {
548 let mut result = Vec::new();
549 let mut current = String::new();
550 let mut chars = s.chars().peekable();
551 let mut in_single_quote = false;
552 let mut in_double_quote = false;
553
554 while let Some(c) = chars.next() {
555 if in_single_quote {
556 if c == '\'' {
558 in_single_quote = false;
559 } else {
560 current.push(c);
561 }
562 } else if in_double_quote {
563 if c == '"' {
565 in_double_quote = false;
566 } else if c == '\\' {
567 if let Some(&next) = chars.peek() {
569 if next == '"' || next == '\\' {
570 current.push(chars.next().unwrap());
571 } else {
572 current.push(c);
574 }
575 } else {
576 current.push(c);
577 }
578 } else {
579 current.push(c);
580 }
581 } else {
582 if c == '\'' {
584 in_single_quote = true;
585 } else if c == '"' {
586 in_double_quote = true;
587 } else if c == '\\' {
588 if let Some(next) = chars.next() {
590 current.push(next);
591 }
592 } else if c.is_whitespace() {
593 if !current.is_empty() {
595 result.push(current);
596 current = String::new();
597 }
598 } else {
599 current.push(c);
600 }
601 }
602 }
603
604 if !current.is_empty() {
606 result.push(current);
607 }
608
609 result
610}
611
612pub fn expand_args(args: &[String]) -> Vec<String> {
645 let mut result = Vec::new();
646
647 for arg in args {
648 if has_glob_pattern(arg) {
649 match expand_glob(arg) {
651 Some(matches) if !matches.is_empty() => {
652 result.extend(matches);
653 }
654 _ => {
655 result.push(arg.clone());
657 }
658 }
659 } else {
660 result.push(arg.clone());
661 }
662 }
663
664 result
665}
666
667fn has_glob_pattern(s: &str) -> bool {
669 s.chars().any(|c| c == '*' || c == '?' || c == '[')
670}
671
672fn expand_glob(pattern: &str) -> Option<Vec<String>> {
677 let mut matches = Vec::new();
681
682 let (dir, file_pattern) = split_pattern_path(pattern);
687
688 let read_dir = if dir.is_empty() {
690 std::fs::read_dir(".")
691 } else {
692 std::fs::read_dir(&dir)
693 };
694
695 let entries = match read_dir {
696 Ok(entries) => entries,
697 Err(_) => return None,
698 };
699
700 let matcher = compile_glob_pattern(&file_pattern);
702
703 for entry in entries.flatten() {
704 let file_name = entry.file_name();
705 let name = file_name.to_string_lossy();
706
707 if matches_pattern(&name, &matcher) {
708 let path = if dir.is_empty() {
709 name.to_string()
710 } else {
711 format!("{}/{}", dir, name)
712 };
713 matches.push(path);
714 }
715 }
716
717 matches.sort();
719
720 Some(matches)
721}
722
723fn split_pattern_path(pattern: &str) -> (String, String) {
725 let glob_start = pattern
727 .chars()
728 .position(|c| c == '*' || c == '?' || c == '[')
729 .unwrap_or(pattern.len());
730
731 let prefix = &pattern[..glob_start];
732 let last_sep = prefix.rfind(|c| c == '/' || c == '\\');
733
734 match last_sep {
735 Some(idx) => (pattern[..idx].to_string(), pattern[idx + 1..].to_string()),
736 None => (String::new(), pattern.to_string()),
737 }
738}
739
740#[derive(Debug)]
742enum GlobPart {
743 Literal(String),
744 Any, AnySequence, CharClass(Vec<char>, bool), }
748
749fn compile_glob_pattern(pattern: &str) -> Vec<GlobPart> {
751 let mut parts = Vec::new();
752 let mut chars = pattern.chars().peekable();
753 let mut literal = String::new();
754
755 while let Some(c) = chars.next() {
756 match c {
757 '*' => {
758 if !literal.is_empty() {
759 parts.push(GlobPart::Literal(literal));
760 literal = String::new();
761 }
762 while chars.peek() == Some(&'*') {
764 chars.next();
765 }
766 parts.push(GlobPart::AnySequence);
767 }
768 '?' => {
769 if !literal.is_empty() {
770 parts.push(GlobPart::Literal(literal));
771 literal = String::new();
772 }
773 parts.push(GlobPart::Any);
774 }
775 '[' => {
776 if !literal.is_empty() {
777 parts.push(GlobPart::Literal(literal));
778 literal = String::new();
779 }
780 let negated = chars.peek() == Some(&'!');
782 if negated {
783 chars.next();
784 }
785 let mut class_chars = Vec::new();
786 while let Some(&ch) = chars.peek() {
787 if ch == ']' {
788 chars.next();
789 break;
790 }
791 class_chars.push(chars.next().unwrap());
792 }
793 parts.push(GlobPart::CharClass(class_chars, negated));
794 }
795 '\\' => {
796 if let Some(next) = chars.next() {
798 literal.push(next);
799 }
800 }
801 _ => {
802 literal.push(c);
803 }
804 }
805 }
806
807 if !literal.is_empty() {
808 parts.push(GlobPart::Literal(literal));
809 }
810
811 parts
812}
813
814fn matches_pattern(s: &str, parts: &[GlobPart]) -> bool {
816 matches_pattern_recursive(s, parts, 0)
817}
818
819fn matches_pattern_recursive(s: &str, parts: &[GlobPart], part_idx: usize) -> bool {
821 if part_idx >= parts.len() {
822 return s.is_empty();
823 }
824
825 let part = &parts[part_idx];
826
827 match part {
828 GlobPart::Literal(lit) => {
829 if s.starts_with(lit.as_str()) {
830 matches_pattern_recursive(&s[lit.len()..], parts, part_idx + 1)
831 } else {
832 false
833 }
834 }
835 GlobPart::Any => {
836 if s.is_empty() {
837 false
838 } else {
839 let mut chars = s.chars();
841 chars.next();
842 matches_pattern_recursive(chars.as_str(), parts, part_idx + 1)
843 }
844 }
845 GlobPart::AnySequence => {
846 if matches_pattern_recursive(s, parts, part_idx + 1) {
849 return true;
850 }
851 for (i, _) in s.char_indices() {
853 if matches_pattern_recursive(&s[i + 1..], parts, part_idx + 1) {
854 return true;
855 }
856 }
857 false
858 }
859 GlobPart::CharClass(chars, negated) => {
860 if s.is_empty() {
861 return false;
862 }
863 let first = s.chars().next().unwrap();
864 let in_class = chars.contains(&first);
865 let matches = if *negated { !in_class } else { in_class };
866 if matches {
867 let mut remaining = s.chars();
868 remaining.next();
869 matches_pattern_recursive(remaining.as_str(), parts, part_idx + 1)
870 } else {
871 false
872 }
873 }
874 }
875}
876
877#[cfg(test)]
882mod tests {
883 use super::*;
884
885 #[test]
886 fn test_format_filename() {
887 let path = Path::new("/usr/local/bin/test");
889 let formatted = format_filename(path);
890 assert!(!formatted.contains('\\'));
891
892 let path = Path::new("./relative/path");
894 let formatted = format_filename(path);
895 assert_eq!(formatted, "./relative/path");
896 }
897
898 #[test]
899 fn test_expand_path_tilde() {
900 if let Some(home) = home_dir() {
901 let path = expand_path("~/test");
902 assert_eq!(path, home.join("test"));
903
904 let path = expand_path("~");
905 assert_eq!(path, home);
906 }
907 }
908
909 #[test]
910 fn test_expand_path_env_vars() {
911 env::set_var("CLICK_TEST_VAR", "/test/path");
912
913 let path = expand_path("$CLICK_TEST_VAR/file.txt");
914 assert_eq!(path, PathBuf::from("/test/path/file.txt"));
915
916 let path = expand_path("${CLICK_TEST_VAR}/file.txt");
917 assert_eq!(path, PathBuf::from("/test/path/file.txt"));
918
919 env::remove_var("CLICK_TEST_VAR");
920 }
921
922 #[test]
923 fn test_expand_path_no_expansion() {
924 let path = expand_path("/absolute/path");
925 assert_eq!(path, PathBuf::from("/absolute/path"));
926
927 let path = expand_path("relative/path");
928 assert_eq!(path, PathBuf::from("relative/path"));
929 }
930
931 #[test]
932 fn test_get_os_args() {
933 let args = get_os_args();
934 assert!(!args.is_empty());
936 }
937
938 #[test]
939 fn test_get_app_dir() {
940 let app_dir = get_app_dir("testapp", false);
941 assert!(!app_dir.as_os_str().is_empty());
943 }
944
945 #[test]
946 fn test_pluralize() {
947 assert_eq!(pluralize(0, "file", "files"), "files");
948 assert_eq!(pluralize(1, "file", "files"), "file");
949 assert_eq!(pluralize(2, "file", "files"), "files");
950 assert_eq!(pluralize(100, "item", "items"), "items");
951 }
952
953 #[test]
954 fn test_join_with_conjunction() {
955 assert_eq!(join_with_conjunction(&[], ", ", " and "), "");
956 assert_eq!(join_with_conjunction(&["a"], ", ", " and "), "a");
957 assert_eq!(join_with_conjunction(&["a", "b"], ", ", " and "), "a and b");
958 assert_eq!(
959 join_with_conjunction(&["a", "b", "c"], ", ", " and "),
960 "a, b and c"
961 );
962 assert_eq!(
963 join_with_conjunction(&["a", "b", "c", "d"], ", ", " or "),
964 "a, b, c or d"
965 );
966 }
967
968 #[test]
969 fn test_make_safe_filename() {
970 assert_eq!(make_safe_filename("normal.txt"), "normal.txt");
971 assert_eq!(make_safe_filename("file name.txt"), "file_name.txt");
972 assert_eq!(make_safe_filename("a<b>c.txt"), "abc.txt");
973 assert_eq!(make_safe_filename("a:b/c\\d.txt"), "abcd.txt");
974 assert_eq!(
975 make_safe_filename(" multiple spaces "),
976 "_multiple_spaces"
977 );
978 }
979
980 #[test]
981 fn test_safecall() {
982 assert_eq!(safecall("normal text"), "normal text");
983 assert_eq!(safecall("with\nnewline"), "with\nnewline");
984 assert_eq!(safecall("with\ttab"), "with\ttab");
985 assert_eq!(safecall("with\x07bell"), "with\\x07bell");
987 }
988
989 #[test]
990 fn test_get_extension() {
991 assert_eq!(get_extension(Path::new("file.txt")), Some("txt"));
992 assert_eq!(get_extension(Path::new("file.tar.gz")), Some("gz"));
993 assert_eq!(get_extension(Path::new("file")), None);
994 assert_eq!(get_extension(Path::new(".hidden")), None);
995 }
996
997 #[test]
998 fn test_strip_extension() {
999 assert_eq!(
1000 strip_extension(Path::new("file.txt")),
1001 PathBuf::from("file")
1002 );
1003 assert_eq!(
1004 strip_extension(Path::new("path/to/file.txt")),
1005 PathBuf::from("path/to/file")
1006 );
1007 assert_eq!(
1008 strip_extension(Path::new("file.tar.gz")),
1009 PathBuf::from("file.tar")
1010 );
1011 }
1012
1013 #[test]
1014 fn test_home_dir() {
1015 let home = home_dir();
1017 if home.is_some() {
1018 assert!(home.unwrap().exists() || env::var("HOME").is_ok());
1019 }
1020 }
1021
1022 #[test]
1023 #[cfg(unix)]
1024 fn test_should_strip_ansi_no_color() {
1025 let saved = env::var("NO_COLOR").ok();
1027
1028 env::set_var("NO_COLOR", "1");
1029 assert!(should_strip_ansi());
1030
1031 match saved {
1033 Some(v) => env::set_var("NO_COLOR", v),
1034 None => env::remove_var("NO_COLOR"),
1035 }
1036 }
1037
1038 #[test]
1039 #[cfg(unix)]
1040 fn test_should_strip_ansi_dumb_term() {
1041 let saved = env::var("TERM").ok();
1043 env::remove_var("NO_COLOR");
1044
1045 env::set_var("TERM", "dumb");
1046 assert!(should_strip_ansi());
1047
1048 match saved {
1050 Some(v) => env::set_var("TERM", v),
1051 None => env::remove_var("TERM"),
1052 }
1053 }
1054
1055 #[test]
1060 fn test_split_arg_string_simple() {
1061 let args = split_arg_string("foo bar baz");
1062 assert_eq!(args, vec!["foo", "bar", "baz"]);
1063 }
1064
1065 #[test]
1066 fn test_split_arg_string_single_quotes() {
1067 let args = split_arg_string("foo 'bar baz' qux");
1068 assert_eq!(args, vec!["foo", "bar baz", "qux"]);
1069
1070 let args = split_arg_string("foo '' bar");
1072 assert_eq!(args, vec!["foo", "bar"]);
1073 }
1074
1075 #[test]
1076 fn test_split_arg_string_double_quotes() {
1077 let args = split_arg_string("foo \"bar baz\" qux");
1078 assert_eq!(args, vec!["foo", "bar baz", "qux"]);
1079
1080 let args = split_arg_string(r#"foo "bar \"quoted\"" baz"#);
1082 assert_eq!(args, vec!["foo", r#"bar "quoted""#, "baz"]);
1083 }
1084
1085 #[test]
1086 fn test_split_arg_string_backslash_escape() {
1087 let args = split_arg_string(r"foo\ bar baz");
1089 assert_eq!(args, vec!["foo bar", "baz"]);
1090
1091 let args = split_arg_string(r"foo\\bar");
1093 assert_eq!(args, vec![r"foo\bar"]);
1094 }
1095
1096 #[test]
1097 fn test_split_arg_string_mixed_quotes() {
1098 let args = split_arg_string("foo 'bar baz' \"quoted\" plain");
1099 assert_eq!(args, vec!["foo", "bar baz", "quoted", "plain"]);
1100 }
1101
1102 #[test]
1103 fn test_split_arg_string_empty() {
1104 let args = split_arg_string("");
1105 assert!(args.is_empty());
1106
1107 let args = split_arg_string(" ");
1108 assert!(args.is_empty());
1109 }
1110
1111 #[test]
1112 fn test_split_arg_string_complex() {
1113 let args = split_arg_string("foo 'bar baz' \"quoted\"");
1115 assert_eq!(args, vec!["foo", "bar baz", "quoted"]);
1116 }
1117
1118 #[test]
1119 fn test_split_arg_string_no_escapes_in_single_quotes() {
1120 let args = split_arg_string(r"'foo\\bar'");
1122 assert_eq!(args, vec![r"foo\\bar"]);
1123
1124 let args = split_arg_string(r#"'foo\"bar'"#);
1125 assert_eq!(args, vec![r#"foo\"bar"#]);
1126 }
1127
1128 #[test]
1133 fn test_has_glob_pattern() {
1134 assert!(has_glob_pattern("*.txt"));
1135 assert!(has_glob_pattern("file?.txt"));
1136 assert!(has_glob_pattern("file[ab].txt"));
1137 assert!(!has_glob_pattern("normal.txt"));
1138 assert!(!has_glob_pattern("/path/to/file"));
1139 }
1140
1141 #[test]
1142 fn test_glob_pattern_literal() {
1143 let parts = compile_glob_pattern("hello");
1144 let is_match = matches_pattern("hello", &parts);
1145 assert!(is_match);
1146
1147 let is_match = matches_pattern("world", &parts);
1148 assert!(!is_match);
1149 }
1150
1151 #[test]
1152 fn test_glob_pattern_star() {
1153 let parts = compile_glob_pattern("*.txt");
1154 assert!(matches_pattern("file.txt", &parts));
1155 assert!(matches_pattern("hello.txt", &parts));
1156 assert!(matches_pattern(".txt", &parts));
1157 assert!(!matches_pattern("file.rs", &parts));
1158 }
1159
1160 #[test]
1161 fn test_glob_pattern_question() {
1162 let parts = compile_glob_pattern("file?.txt");
1163 assert!(matches_pattern("file1.txt", &parts));
1164 assert!(matches_pattern("filea.txt", &parts));
1165 assert!(!matches_pattern("file12.txt", &parts));
1166 assert!(!matches_pattern("file.txt", &parts));
1167 }
1168
1169 #[test]
1170 fn test_glob_pattern_char_class() {
1171 let parts = compile_glob_pattern("file[abc].txt");
1172 assert!(matches_pattern("filea.txt", &parts));
1173 assert!(matches_pattern("fileb.txt", &parts));
1174 assert!(matches_pattern("filec.txt", &parts));
1175 assert!(!matches_pattern("filed.txt", &parts));
1176 }
1177
1178 #[test]
1179 fn test_glob_pattern_negated_char_class() {
1180 let parts = compile_glob_pattern("file[!abc].txt");
1181 assert!(!matches_pattern("filea.txt", &parts));
1182 assert!(!matches_pattern("fileb.txt", &parts));
1183 assert!(matches_pattern("filed.txt", &parts));
1184 assert!(matches_pattern("file1.txt", &parts));
1185 }
1186
1187 #[test]
1188 fn test_expand_args_no_patterns() {
1189 let args = vec!["file.txt".to_string(), "other.rs".to_string()];
1190 let expanded = expand_args(&args);
1191 assert_eq!(expanded, args);
1192 }
1193
1194 #[test]
1195 fn test_expand_args_pattern_no_matches() {
1196 let args = vec!["__nonexistent_pattern_xyz_*.abc".to_string()];
1198 let expanded = expand_args(&args);
1199 assert_eq!(expanded, args);
1201 }
1202
1203 #[test]
1204 fn test_split_pattern_path() {
1205 let (dir, pattern) = split_pattern_path("*.txt");
1206 assert_eq!(dir, "");
1207 assert_eq!(pattern, "*.txt");
1208
1209 let (dir, pattern) = split_pattern_path("src/*.rs");
1210 assert_eq!(dir, "src");
1211 assert_eq!(pattern, "*.rs");
1212
1213 let (dir, pattern) = split_pattern_path("/home/user/*.txt");
1214 assert_eq!(dir, "/home/user");
1215 assert_eq!(pattern, "*.txt");
1216 }
1217}