ocaml/stdlib/filename.ml

360 lines
13 KiB
OCaml

(**************************************************************************)
(* *)
(* OCaml *)
(* *)
(* Xavier Leroy and Damien Doligez, INRIA Rocquencourt *)
(* *)
(* Copyright 1996 Institut National de Recherche en Informatique et *)
(* en Automatique. *)
(* *)
(* All rights reserved. This file is distributed under the terms of *)
(* the GNU Lesser General Public License version 2.1, with the *)
(* special exception on linking described in the file LICENSE. *)
(* *)
(**************************************************************************)
let generic_quote quotequote s =
let l = String.length s in
let b = Buffer.create (l + 20) in
Buffer.add_char b '\'';
for i = 0 to l - 1 do
if s.[i] = '\''
then Buffer.add_string b quotequote
else Buffer.add_char b s.[i]
done;
Buffer.add_char b '\'';
Buffer.contents b
(* This function implements the Open Group specification found here:
[[1]] http://pubs.opengroup.org/onlinepubs/9699919799/utilities/basename.html
In step 1 of [[1]], we choose to return "." for empty input.
(for compatibility with previous versions of OCaml)
In step 2, we choose to process "//" normally.
Step 6 is not implemented: we consider that the [suffix] operand is
always absent. Suffixes are handled by [chop_suffix] and [chop_extension].
*)
let generic_basename is_dir_sep current_dir_name name =
let rec find_end n =
if n < 0 then String.sub name 0 1
else if is_dir_sep name n then find_end (n - 1)
else find_beg n (n + 1)
and find_beg n p =
if n < 0 then String.sub name 0 p
else if is_dir_sep name n then String.sub name (n + 1) (p - n - 1)
else find_beg (n - 1) p
in
if name = ""
then current_dir_name
else find_end (String.length name - 1)
(* This function implements the Open Group specification found here:
[[2]] http://pubs.opengroup.org/onlinepubs/9699919799/utilities/dirname.html
In step 6 of [[2]], we choose to process "//" normally.
*)
let generic_dirname is_dir_sep current_dir_name name =
let rec trailing_sep n =
if n < 0 then String.sub name 0 1
else if is_dir_sep name n then trailing_sep (n - 1)
else base n
and base n =
if n < 0 then current_dir_name
else if is_dir_sep name n then intermediate_sep n
else base (n - 1)
and intermediate_sep n =
if n < 0 then String.sub name 0 1
else if is_dir_sep name n then intermediate_sep (n - 1)
else String.sub name 0 (n + 1)
in
if name = ""
then current_dir_name
else trailing_sep (String.length name - 1)
module type SYSDEPS = sig
val null : string
val current_dir_name : string
val parent_dir_name : string
val dir_sep : string
val is_dir_sep : string -> int -> bool
val is_relative : string -> bool
val is_implicit : string -> bool
val check_suffix : string -> string -> bool
val chop_suffix_opt : suffix:string -> string -> string option
val temp_dir_name : string
val quote : string -> string
val quote_command :
string -> ?stdin: string -> ?stdout: string -> ?stderr: string
-> string list -> string
val basename : string -> string
val dirname : string -> string
end
module Unix : SYSDEPS = struct
let null = "/dev/null"
let current_dir_name = "."
let parent_dir_name = ".."
let dir_sep = "/"
let is_dir_sep s i = s.[i] = '/'
let is_relative n = String.length n < 1 || n.[0] <> '/'
let is_implicit n =
is_relative n
&& (String.length n < 2 || String.sub n 0 2 <> "./")
&& (String.length n < 3 || String.sub n 0 3 <> "../")
let check_suffix name suff =
String.ends_with ~suffix:suff name
let chop_suffix_opt ~suffix filename =
let len_s = String.length suffix and len_f = String.length filename in
if len_f >= len_s then
let r = String.sub filename (len_f - len_s) len_s in
if r = suffix then
Some (String.sub filename 0 (len_f - len_s))
else
None
else
None
let temp_dir_name =
try Sys.getenv "TMPDIR" with Not_found -> "/tmp"
let quote = generic_quote "'\\''"
let quote_command cmd ?stdin ?stdout ?stderr args =
String.concat " " (List.map quote (cmd :: args))
^ (match stdin with None -> "" | Some f -> " <" ^ quote f)
^ (match stdout with None -> "" | Some f -> " >" ^ quote f)
^ (match stderr with None -> "" | Some f -> if stderr = stdout
then " 2>&1"
else " 2>" ^ quote f)
let basename = generic_basename is_dir_sep current_dir_name
let dirname = generic_dirname is_dir_sep current_dir_name
end
module Win32 : SYSDEPS = struct
let null = "NUL"
let current_dir_name = "."
let parent_dir_name = ".."
let dir_sep = "\\"
let is_dir_sep s i = let c = s.[i] in c = '/' || c = '\\' || c = ':'
let is_relative n =
(String.length n < 1 || n.[0] <> '/')
&& (String.length n < 1 || n.[0] <> '\\')
&& (String.length n < 2 || n.[1] <> ':')
let is_implicit n =
is_relative n
&& (String.length n < 2 || String.sub n 0 2 <> "./")
&& (String.length n < 2 || String.sub n 0 2 <> ".\\")
&& (String.length n < 3 || String.sub n 0 3 <> "../")
&& (String.length n < 3 || String.sub n 0 3 <> "..\\")
let check_suffix name suff =
String.length name >= String.length suff &&
(let s = String.sub name (String.length name - String.length suff)
(String.length suff) in
String.lowercase_ascii s = String.lowercase_ascii suff)
let chop_suffix_opt ~suffix filename =
let len_s = String.length suffix and len_f = String.length filename in
if len_f >= len_s then
let r = String.sub filename (len_f - len_s) len_s in
if String.lowercase_ascii r = String.lowercase_ascii suffix then
Some (String.sub filename 0 (len_f - len_s))
else
None
else
None
let temp_dir_name =
try Sys.getenv "TEMP" with Not_found -> "."
let quote s =
let l = String.length s in
let b = Buffer.create (l + 20) in
Buffer.add_char b '\"';
let rec loop i =
if i = l then Buffer.add_char b '\"' else
match s.[i] with
| '\"' -> loop_bs 0 i;
| '\\' -> loop_bs 0 i;
| c -> Buffer.add_char b c; loop (i+1);
and loop_bs n i =
if i = l then begin
Buffer.add_char b '\"';
add_bs n;
end else begin
match s.[i] with
| '\"' -> add_bs (2*n+1); Buffer.add_char b '\"'; loop (i+1);
| '\\' -> loop_bs (n+1) (i+1);
| _ -> add_bs n; loop i
end
and add_bs n = for _j = 1 to n do Buffer.add_char b '\\'; done
in
loop 0;
Buffer.contents b
(*
Quoting commands for execution by cmd.exe is difficult.
1- Each argument is first quoted using the "quote" function above, to
protect it against the processing performed by the C runtime system,
then cmd.exe's special characters are escaped with '^', using
the "quote_cmd" function below. For more details, see
https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23
2- The command and the redirection files, if any, must be double-quoted
in case they contain spaces. This quoting is interpreted by cmd.exe,
not by the C runtime system, hence the "quote" function above
cannot be used. The two characters we don't know how to quote
inside a double-quoted cmd.exe string are double-quote and percent.
We just fail if the command name or the redirection file names
contain a double quote (not allowed in Windows file names, anyway)
or a percent. See function "quote_cmd_filename" below.
3- The whole string passed to Sys.command is then enclosed in double
quotes, which are immediately stripped by cmd.exe. Otherwise,
some of the double quotes from step 2 above can be misparsed.
See e.g. https://stackoverflow.com/a/9965141
*)
let quote_cmd s =
let b = Buffer.create (String.length s + 20) in
String.iter
(fun c ->
match c with
| '(' | ')' | '!' | '^' | '%' | '\"' | '<' | '>' | '&' | '|' ->
Buffer.add_char b '^'; Buffer.add_char b c
| _ ->
Buffer.add_char b c)
s;
Buffer.contents b
let quote_cmd_filename f =
if String.contains f '\"' || String.contains f '%' then
failwith ("Filename.quote_command: bad file name " ^ f)
else if String.contains f ' ' then
"\"" ^ f ^ "\""
else
f
(* Redirections in cmd.exe: see https://ss64.com/nt/syntax-redirection.html
and https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-xp/bb490982(v=technet.10)
*)
let quote_command cmd ?stdin ?stdout ?stderr args =
String.concat "" [
"\"";
quote_cmd_filename cmd;
" ";
quote_cmd (String.concat " " (List.map quote args));
(match stdin with None -> "" | Some f -> " <" ^ quote_cmd_filename f);
(match stdout with None -> "" | Some f -> " >" ^ quote_cmd_filename f);
(match stderr with None -> "" | Some f ->
if stderr = stdout
then " 2>&1"
else " 2>" ^ quote_cmd_filename f);
"\""
]
let has_drive s =
let is_letter = function
| 'A' .. 'Z' | 'a' .. 'z' -> true
| _ -> false
in
String.length s >= 2 && is_letter s.[0] && s.[1] = ':'
let drive_and_path s =
if has_drive s
then (String.sub s 0 2, String.sub s 2 (String.length s - 2))
else ("", s)
let dirname s =
let (drive, path) = drive_and_path s in
let dir = generic_dirname is_dir_sep current_dir_name path in
drive ^ dir
let basename s =
let (_drive, path) = drive_and_path s in
generic_basename is_dir_sep current_dir_name path
end
module Cygwin : SYSDEPS = struct
let null = "/dev/null"
let current_dir_name = "."
let parent_dir_name = ".."
let dir_sep = "/"
let is_dir_sep = Win32.is_dir_sep
let is_relative = Win32.is_relative
let is_implicit = Win32.is_implicit
let check_suffix = Win32.check_suffix
let chop_suffix_opt = Win32.chop_suffix_opt
let temp_dir_name = Unix.temp_dir_name
let quote = Unix.quote
let quote_command = Unix.quote_command
let basename = generic_basename is_dir_sep current_dir_name
let dirname = generic_dirname is_dir_sep current_dir_name
end
module Sysdeps =
(val (match Sys.os_type with
| "Win32" -> (module Win32: SYSDEPS)
| "Cygwin" -> (module Cygwin: SYSDEPS)
| _ -> (module Unix: SYSDEPS)))
include Sysdeps
let concat dirname filename =
let l = String.length dirname in
if l = 0 || is_dir_sep dirname (l-1)
then dirname ^ filename
else dirname ^ dir_sep ^ filename
let chop_suffix name suff =
let n = String.length name - String.length suff in
if n < 0 then invalid_arg "Filename.chop_suffix" else String.sub name 0 n
let extension_len name =
let rec check i0 i =
if i < 0 || is_dir_sep name i then 0
else if name.[i] = '.' then check i0 (i - 1)
else String.length name - i0
in
let rec search_dot i =
if i < 0 || is_dir_sep name i then 0
else if name.[i] = '.' then check i (i - 1)
else search_dot (i - 1)
in
search_dot (String.length name - 1)
let extension name =
let l = extension_len name in
if l = 0 then "" else String.sub name (String.length name - l) l
let chop_extension name =
let l = extension_len name in
if l = 0 then invalid_arg "Filename.chop_extension"
else String.sub name 0 (String.length name - l)
let remove_extension name =
let l = extension_len name in
if l = 0 then name else String.sub name 0 (String.length name - l)
external open_desc: string -> open_flag list -> int -> int = "caml_sys_open"
external close_desc: int -> unit = "caml_sys_close"
let prng = lazy(Random.State.make_self_init ())
let temp_file_name temp_dir prefix suffix =
let rnd = (Random.State.bits (Lazy.force prng)) land 0xFFFFFF in
concat temp_dir (Printf.sprintf "%s%06x%s" prefix rnd suffix)
let current_temp_dir_name = ref temp_dir_name
let set_temp_dir_name s = current_temp_dir_name := s
let get_temp_dir_name () = !current_temp_dir_name
let temp_file ?(temp_dir = !current_temp_dir_name) prefix suffix =
let rec try_name counter =
let name = temp_file_name temp_dir prefix suffix in
try
close_desc(open_desc name [Open_wronly; Open_creat; Open_excl] 0o600);
name
with Sys_error _ as e ->
if counter >= 1000 then raise e else try_name (counter + 1)
in try_name 0
let open_temp_file ?(mode = [Open_text]) ?(perms = 0o600)
?(temp_dir = !current_temp_dir_name) prefix suffix =
let rec try_name counter =
let name = temp_file_name temp_dir prefix suffix in
try
(name,
open_out_gen (Open_wronly::Open_creat::Open_excl::mode) perms name)
with Sys_error _ as e ->
if counter >= 1000 then raise e else try_name (counter + 1)
in try_name 0