(* * Glob expansion. * * There is a dilemma here for Win32. * Since \ is the pathname separator, we will * often see names like "dir\*.c". This is not * and escape sequence in the DOS shell, and in * fact * is not a valid character in Win32 * filenames. * * So, we could turn off escape sequences in Win32, * but doing this globally is a bad idea. For example, * what about the sequence dir\[a-z]*.c? In this case, * the [ and ] characters _are_ valid in Win32 filenames. * * For now, we punt. The \ character is considered to * be an escape character. If you want globbing in * Win32, use forward slashes. The result will still * use backslashes, so no worries. * *) (************************************************************************ * Tilde expansion. *) (* * Keep a table of entries. * Whenever we look a value in the passwd file, * add it to the table. *) let tilde_table = ref [||] (* * Keep the table sorted for quick lookup. *) let tilde_insert (dir : string) (name : string) = let table = !tilde_table in let len = Array.length table in if len = 0 then tilde_table := [|dir, name|] else (* Binary search *) let rec search i j (dir : string) table = if i < j - 1 then let k = (i + j) / 2 in let dir', _ = table.(k) in if dir' > dir then search i k dir table else search k j dir table else i in let i = search (-1) len dir table in if i >= 0 && fst table.(i) = dir then (if snd table.(i) <> "" then table.(i) <- dir, name) else let i = succ i in let ntable = Array.create (len + 1) table.(0) in Array.blit table 0 ntable 0 i; ntable.(i) <- dir, name; Array.blit table i ntable (i + 1) (len - i); tilde_table := ntable (* * Find an entry in the table. *) let rec tilde_matches dir1 dir2 len i = i = len || dir1.[i] = dir2.[i] && tilde_matches dir1 dir2 len (succ i) let tilde_collapse dir = let table = !tilde_table in let len = Array.length table in let rec search i j = if i < j - 1 then let k = (i + j) / 2 in let dir', _ = table.(k) in if dir' > dir then search i k else search k j else i in let i = search (-1) len in if i < 0 then dir else let dir', name = table.(i) in let len' = String.length dir' in let len = String.length dir in if len' <= len && tilde_matches dir' dir len' 0 then let namelen = String.length name in let length = len - len' + namelen + 1 in let s = String.make length ' ' in s.[0] <- '~'; String.blit name 0 s 1 namelen; String.blit dir len' s (namelen + 1) (len - len'); s else dir (* * Here is the caching getpwnam. *) let getpwnam user = let passwd = Unix.getpwnam user in let dir = passwd.Unix.pw_dir in tilde_insert dir user; dir let gethomedir = getpwnam (* * Try to figure out the home directory as best as possible. *) let home_dir = let home = Lm_unix_util.home_dir in tilde_insert home ""; home (* * Get a list of all the users. *) let getusers () = let users = Lm_unix_util.getpwents () in List.map (fun entry -> let { Unix.pw_name = name; Unix.pw_dir = dir; _ } = entry in tilde_insert dir name; name) users (************************************************************************ * Glob expansion. *) type glob_option = GlobNoBraces (* Do not perform csh-style brace expansion *) | GlobNoTilde (* Do not perform tilde-expansion *) | GlobNoEscape (* The \ character does not escape special characters *) | GlobNoCheck (* If an expansion fails, return the expansion literally *) | GlobIgnoreCheck (* If an expansion fails, it expands to nothing *) | GlobDot (* Allow wildcards to match filenames with a leading . *) | GlobOnlyFiles (* Return only non-directories in the result *) | GlobOnlyDirs (* Return only directories in the result *) | GlobCVSIgnore (* Ignore files as specified by .cvsignore files *) | GlobIgnore of string list (* Ignore the files that match the pattern *) | GlobAllow of string list (* Allow only files that match the pattern *) | GlobIgnoreFun of (string -> bool) (* Ignore the files specified by the function *) | GlobAllowFun of (string -> bool) (* Allow only the files specified by the function *) | GlobHomeDir of string (* Home directory for ~ expansion *) | GlobProperSubdirs (* Include only proper subdirs in listing *) type glob_check = | NoMatchError | NoMatchPreserve | NoMatchIgnore type glob_options = { glob_braces : bool; glob_tilde : bool; glob_escape : bool; glob_check : glob_check; glob_dot : bool; glob_files : bool; glob_dirs : bool; glob_cvs : bool; glob_ignore : (string -> bool); glob_allow : (string -> bool); glob_cvsignore : (string -> bool); glob_home : string; glob_proper : bool } let default_glob_options = { glob_braces = true; glob_tilde = true; glob_escape = true; glob_check = NoMatchError; glob_dot = false; glob_files = false; glob_dirs = false; glob_cvs = false; glob_ignore = (fun _ -> false); glob_allow = (fun _ -> true); glob_cvsignore = (fun _ -> false); glob_home = home_dir; glob_proper = false } (************************************************************************ * Utilities. *) (* * Determine if a string contains glob characters. *) let is_glob_string options name = let len = String.length name in let rec search lbrack i = if i >= len then false else match name.[i] with '*' | '?' -> true | '~' when i = 0 -> true | '[' -> search true (succ i) | ']' -> lbrack || search lbrack (succ i) | '\\' when options.glob_escape -> search lbrack (i + 2) | _ -> search lbrack (succ i) in search false 0 let glob_add_escaped options buf s = let len = String.length s in let rec collect i = if i < len then let c = String.unsafe_get s i in match c with '*' | '?' | '[' | ']' -> Buffer.add_char buf '\\'; Buffer.add_char buf c; collect (succ i) | '~' when i = 0 -> Buffer.add_char buf '\\'; Buffer.add_char buf c; collect (succ i) | '{' | '}' when options.glob_braces -> Buffer.add_char buf '\\'; Buffer.add_char buf c; collect (succ i) | '\\' when options.glob_escape -> Buffer.add_char buf '\\'; if i < len - 1 then begin Buffer.add_char buf s.[i + 1]; collect (i + 2) end | c -> Buffer.add_char buf c; collect (succ i) in collect 0 (* * Unescape a name. *) let unescape options s = if options.glob_escape then let len = String.length s in let buf = Buffer.create len in let rec collect i = if i = len then Buffer.contents buf else let c = s.[i] in if c = '\\' && i < len - 1 then let c = s.[i + 1] in match c with '*' | '?' | '[' | ']' | '~' | '{' | '}' -> Buffer.add_char buf c; collect (i + 2) | _ -> Buffer.add_char buf '\\'; collect (i + 1) else begin Buffer.add_char buf c; collect (i + 1) end in collect 0 else s (* * Don't add unnecessary separators. *) let filename_concat dir name = match dir, name with "", _ -> name | _, "" -> dir | _ -> Filename.concat dir name (* * Split the path into root part, and the rest. * If escaping is enabled, do not split at escape sequences, * but split everywhere else. *) let filename_split options s = let len = String.length s in let add_name names start i = if start < i then String.sub s start (i - start) :: names else names in let rec collect names start i = if i = len then add_name names start i else let c = s.[i] in match c with '/' -> collect (add_name names start i) (succ i) (succ i) | '\\' -> if options.glob_escape && i < len - 1 then let c = s.[i + 1] in match c with '*' | '?' | '[' | ']' | '~' -> collect names start (i + 2) | _ -> collect (add_name names start i) (succ i) (succ i) else collect (add_name names start i) (succ i) (succ i) | _ -> collect names start (succ i) in let names = collect [] 0 0 in List.rev names (* * Split the rest into parts. *) let filename_path options name : string list Lm_filename_util.path = match Lm_filename_util.filename_string name with AbsolutePath (root, path) -> Lm_filename_util.AbsolutePath (root, filename_split options path) | RelativePath path -> RelativePath (filename_split options path) (************************************************************************ * Shell regular expressions. * *) let add_shell_pattern options buf s = let len = String.length s in let rec collect i = if i >= len then Buffer.add_char buf '$' else let c = s.[i] in match s.[i] with '*' -> Buffer.add_string buf ".*"; collect (succ i) | '?' -> Buffer.add_string buf "."; collect (succ i) | '.' | '+' | '^' | '$' | '|' | '(' | ')' | '{' | '}' -> Buffer.add_char buf '\\'; Buffer.add_char buf c; collect (succ i) | '\\' -> if options.glob_escape && i < len - 1 then let c = s.[i + 1] in match c with '*' | '?' | '[' | ']' | '~' -> Buffer.add_char buf '\\'; Buffer.add_char buf c; collect (i + 2) | _ -> Buffer.add_string buf "\\\\"; collect (succ i) else begin Buffer.add_string buf "\\\\"; collect (succ i) end | _ -> Buffer.add_char buf c; collect (succ i) in collect 0 let add_shell_disjunct options buf s = Buffer.add_string buf "|"; add_shell_pattern options buf s let regexp_of_shell_pattern options s = let buf = Buffer.create 32 in add_shell_pattern options buf s; Lm_lexer.LmStr.regexp (Buffer.contents buf) let make_filter options sl default = let buf = Buffer.create 32 in match sl with s :: sl -> add_shell_pattern options buf s; List.iter (add_shell_disjunct options buf) sl; let pattern = Lm_lexer.LmStr.regexp (Buffer.contents buf) in (fun name -> Lm_lexer.LmStr.string_match pattern name 0) | [] -> (fun _ -> default) (* * These are the files that CVS ignores by default. * https://www.cvshome.org/docs/manual/cvs-1.11.16/cvs_18.html#IDX266 *) let default_patterns = ["RCS"; "SCCS"; "CVS"; "CVS.adm"; "RCSLOG"; "cvslog.*"; "tags"; "TAGS"; ".make.state"; ".nse_depinfo"; ".svn"; "*~"; "#*"; ".#*"; ",*"; "_$*"; "*$"; "*.old"; "*.bak"; "*.BAK"; "*.orig"; "*.rej"; ".del-*"; "*.a"; "*.olb"; "*.o"; "*.obj"; "*.so"; "*.exe"; "*.Z"; "*.elc"; "*.ln"; "core.*"] let stdignore = let buf = Buffer.create 256 in Buffer.add_string buf "^\\.cvsignore$"; List.iter (add_shell_disjunct default_glob_options buf) default_patterns; Lm_lexer.LmStr.regexp (Buffer.contents buf) (* * Load the ignore expression from .cvsignore. *) let load_cvsignore dirname = let filename = filename_concat dirname ".cvsignore" in (* Get the patterns from the file *) let inx = open_in filename in let rec collect patterns = try collect (Lm_string_util.tokens_std (input_line inx) @ patterns) with End_of_file -> patterns in let patterns = collect [] in let () = close_in inx in (* Concatenate them into a large regular expression *) let buf = Buffer.create 256 in Buffer.add_string buf "^\\.cvsignore$"; List.iter (add_shell_disjunct default_glob_options buf) default_patterns; List.iter (add_shell_disjunct default_glob_options buf) patterns; Lm_lexer.LmStr.regexp (Buffer.contents buf) let load_cvsignore dirname = let pattern = try load_cvsignore dirname with Sys_error _ -> stdignore in (fun name -> Lm_lexer.LmStr.string_match pattern name 0) (* * Check if a filename refers to a directory. *) let is_dir filename = try (Unix.lstat filename).Unix.st_kind = Unix.S_DIR with Unix.Unix_error _ -> false (************************************************************************ * Globbing. *) (* * Collect glob options. *) let create_options l = let rec collect options l = match l with option :: l -> let options = match option with GlobNoBraces -> { options with glob_braces = false } | GlobNoTilde -> { options with glob_tilde = false } | GlobNoEscape -> { options with glob_escape = false } | GlobNoCheck -> { options with glob_check = NoMatchPreserve } | GlobIgnoreCheck -> { options with glob_check = NoMatchIgnore } | GlobDot -> { options with glob_dot = true } | GlobOnlyFiles -> { options with glob_files = true } | GlobOnlyDirs -> { options with glob_dirs = true } | GlobCVSIgnore -> { options with glob_cvs = true } | GlobIgnoreFun f -> { options with glob_ignore = f } | GlobAllowFun f -> { options with glob_allow = f } | GlobIgnore sl -> { options with glob_ignore = make_filter options sl false } | GlobAllow sl -> { options with glob_allow = make_filter options sl true } | GlobHomeDir dir -> { options with glob_home = dir } | GlobProperSubdirs -> { options with glob_proper = true } in collect options l | [] -> options in collect default_glob_options l (* * Perform brace expansion. *) let rec expand_braces options expanded_names unexpanded_names = match unexpanded_names with name :: unexpanded_names -> let len = String.length name in let expanded_names, unexpanded_names = (* Search for the first brace *) let rec search_brace i = if i >= len then name :: expanded_names, unexpanded_names else match name.[i] with '\\' when options.glob_escape -> search_brace (i + 2) | '{' -> search_found 0 i (i + 1) [] (i + 1) | _ -> search_brace (i + 1) (* Found a brace, search for the parts *) and search_found level start last names i = if i >= len then raise (Failure (name ^ ": brace mismatch")); match name.[i] with '\\' when options.glob_escape -> search_found level start last names (i + 2) | ',' when level = 0 -> let name = String.sub name last (i - last) in search_found level start (i + 1) (name :: names) (i + 1) | '{' -> search_found (succ level) start last names (i + 1) | '}' when level = 0 -> let pref = String.sub name 0 start in let suf = String.sub name (i + 1) (len - i - 1) in let name = String.sub name last (i - last) in let names = name :: names in let names = List.map (fun s -> pref ^ s ^ suf) names in expanded_names, List.append names unexpanded_names | '}' -> search_found (pred level) start last names (i + 1) | _ -> search_found level start last names (i + 1) in search_brace 0 in expand_braces options expanded_names unexpanded_names | [] -> expanded_names let glob_braces options names = if options.glob_braces then expand_braces options [] (List.rev names) else names (* * Expand a glob pattern. * The dir is a fully-expanded directory name. *) let glob_dir_pattern options root dirs names dir pattern = let options = if options.glob_cvs then { options with glob_cvsignore = load_cvsignore dir } else options in let root_dir = filename_concat root dir in let dirx = Unix.opendir root_dir in let rec collect dirs names = let name = try Some (Unix.readdir dirx) with End_of_file -> None in match name with None -> dirs, names | Some "" | Some "." | Some ".." -> collect dirs names | Some name -> let root_name = filename_concat root_dir name in let file_name = filename_concat dir name in let dir_flag = is_dir root_name in let dirs, names = if (options.glob_dot || name.[0] <> '.') && (not options.glob_files || not dir_flag) && (not options.glob_dirs || dir_flag) && Lm_lexer.LmStr.string_match pattern name 0 && not (options.glob_ignore name) && (options.glob_allow name) && not (options.glob_cvsignore name) then if dir_flag then file_name :: dirs, names else dirs, file_name :: names else dirs, names in collect dirs names in let dirs_names = collect dirs names in Unix.closedir dirx; dirs_names let glob_dirs_pattern options root dirs pattern = let rec collect dirs' names' dirs = match dirs with dir :: dirs -> let dirs', names' = glob_dir_pattern options root dirs' names' dir pattern in collect dirs' names' dirs | [] -> dirs', names' in collect [] [] dirs let glob_dirs_name options root dirs name = if is_glob_string options name then let options = if name <> "" && name.[0] = '.' then { options with glob_dot = true } else options in let pattern = regexp_of_shell_pattern options name in glob_dirs_pattern options root dirs pattern else let name = unescape options name in List.fold_left (fun (dirs, names) dir -> let root_dir = filename_concat root dir in let root_name = filename_concat root_dir name in let file_name = filename_concat dir name in try let stat = Unix.LargeFile.stat root_name in if stat.Unix.LargeFile.st_kind = Unix.S_DIR then file_name :: dirs, names else dirs, file_name :: names with Unix.Unix_error _ -> dirs, names) ([], []) dirs (* * Perform tilde expansion. *) let null_root = "" let glob_tilde options root dir path = if options.glob_tilde then match path with name :: rest -> let len = String.length name in if len > 0 && name.[0] = '~' then if len = 1 then null_root, options.glob_home, rest else let user = String.sub name 1 (len - 1) in let dir = try getpwnam user with Not_found -> raise (Failure ("Unknown user: " ^ user)) in null_root, dir, rest else if len > 1 && name.[0] = '\\' && name.[1] = '~' then root, dir, String.sub name 1 (len - 1) :: rest else root, dir, path | [] -> root, dir, path else root, dir, path (* * Perform a glob expansion on a single path. *) let glob_match options root dir name = (* Split the path into components *) let root, dir, path = match filename_path options name with RelativePath path -> root, dir, path | AbsolutePath (root, path) -> null_root, Lm_filename_util.string_of_root root, path in (* Do ~ expansion *) let root, dir, path = glob_tilde options root dir path in (* Walk through the path *) let rec glob dirs path = match path with [] -> dirs, [] | [name] -> glob_dirs_name options root dirs name | name :: path -> let options = { options with glob_dirs = true } in let dirs, _ = glob_dirs_name options root dirs name in glob dirs path in glob [dir] path (* * Don't glob-expand unless it is a glob pattern. *) let glob_name options root dir name = if is_glob_string options name then let dirs, names = glob_match options root dir name in if dirs = [] && names = [] then match options.glob_check with NoMatchError -> raise (Failure (name ^ ": bad match")) | NoMatchPreserve -> [], [name] | NoMatchIgnore -> [], [] else dirs, names else if Filename.is_relative name then let name = unescape options name in let root_dir = filename_concat root dir in let root_name = filename_concat root_dir name in let file_name = filename_concat dir name in if is_dir root_name then [file_name], [] else [], [file_name] else let file_name = unescape options name in if is_dir file_name then [file_name], [] else [], [file_name] (* * Perform the actual glob. *) let glob options dir names = let names = glob_braces options names in List.fold_left (fun (dirs, names) name -> let dirs', names' = glob_name options dir "" name in let dirs = List.rev_append dirs' dirs in let names = List.rev_append names' names in dirs, names) ([], []) names (* * Don't glob-expand unless it is a glob pattern. * For argv expansion, we don't care about what is a directory * and what is not. *) let glob_argv_name options root dir name = if is_glob_string options name then let dirs, names = glob_match options root dir name in if dirs = [] then if names = [] then match options.glob_check with NoMatchError -> raise (Failure (name ^ ": bad match")) | NoMatchPreserve -> [name] | NoMatchIgnore -> [] else names else if names = [] then dirs else let names = List.append dirs names in List.sort Pervasives.compare names else let name = unescape options name in let file_name = filename_concat dir name in [file_name] (* * Glob an argv list. * We have to be a little more careful to preserve the order. *) let glob_argv options dir names = let names = glob_braces options names in let names = List.fold_left (fun names name -> let names' = glob_argv_name options dir "" name in List.rev_append names' names) [] names in List.rev names (************************************************************************ * Directory listings. *) (* * Get all the names in the directory. *) let list_dir_exn options root hidden_dirs dirs names dirname = let inx = Unix.opendir (filename_concat root dirname) in let rec read hidden_dirs dirs names = let name = try Some (Unix.readdir inx) with End_of_file -> None in match name with Some "." | Some ".." -> read hidden_dirs dirs names | None -> hidden_dirs, dirs, names | Some name -> let hidden_dirs, dirs, names = let filename = filename_concat dirname name in let dir_flag = is_dir filename in if (options.glob_dot || name.[0] <> '.') && (dir_flag || not options.glob_dirs) && not (options.glob_ignore name) && not (options.glob_cvsignore name) then if dir_flag then if options.glob_allow name then hidden_dirs, filename :: dirs, names else filename :: hidden_dirs, dirs, names else if options.glob_allow name then hidden_dirs, dirs, filename :: names else hidden_dirs, dirs, names else hidden_dirs, dirs, names in read hidden_dirs dirs names in let hidden_dirs_names = read hidden_dirs dirs names in Unix.closedir inx; hidden_dirs_names let list_dir_aux options root hidden_dirs dirs names dirname = let options = if options.glob_cvs then { options with glob_cvsignore = load_cvsignore (filename_concat root dirname) } else options in try list_dir_exn options root hidden_dirs dirs names dirname with Unix.Unix_error _ | Sys_error _ | Failure _ -> hidden_dirs, dirs, names (* * Perform a directory listing. *) let list_dirs options root dirs = let rec collect dirs names l = match l with dir :: l -> let _, dirs, names = list_dir_aux options root [] dirs names dir in collect dirs names l | [] -> dirs, names in collect [] [] dirs (* * Recursive directory listing. *) let list_dirs_rec options root dirs = let rec collect examined_dirs hidden_dirs unexamined_dirs names = match hidden_dirs, unexamined_dirs with dir :: hidden_dirs, _ -> let hidden_dirs, unexamined_dirs, names = list_dir_aux options root hidden_dirs unexamined_dirs names dir in collect examined_dirs hidden_dirs unexamined_dirs names | [], dir :: unexamined_dirs -> let examined_dirs = dir :: examined_dirs in let hidden_dirs, unexamined_dirs, names = list_dir_aux options root hidden_dirs unexamined_dirs names dir in collect examined_dirs hidden_dirs unexamined_dirs names | [], [] -> examined_dirs, names in let hidden_dirs, unexamined_dirs = List.fold_left (fun (hidden_dirs, unexamined_dirs) dir -> if options.glob_allow dir then hidden_dirs, dir :: unexamined_dirs else dir :: hidden_dirs, unexamined_dirs) ([], []) dirs in collect [] hidden_dirs unexamined_dirs [] (* * Recursively expand all subdirectories. *) let subdirs_of_dirs options root dirs = let options = { options with glob_dirs = true } in let rec collect listing hidden_dirs dirs = match hidden_dirs, dirs with dir :: hidden_dirs, _ -> let hidden_dirs, dirs, _ = list_dir_aux options root hidden_dirs dirs [] dir in collect listing hidden_dirs dirs | [], dir :: dirs -> let listing = dir :: listing in let hidden_dirs, dirs, _ = list_dir_aux options root hidden_dirs dirs [] dir in collect listing hidden_dirs dirs | [], [] -> listing in let hidden_dirs, dirs = if options.glob_proper then dirs, [] else [], dirs in collect [] hidden_dirs dirs (* * Regular expression export. *) let regex_of_shell_pattern = regexp_of_shell_pattern