diff --git a/src/lib/ast.ml b/src/lib/ast.ml index 51976d1..ba0d26e 100644 --- a/src/lib/ast.ml +++ b/src/lib/ast.ml @@ -742,6 +742,7 @@ let rec word_component_to_string : word_component -> string = function | WordSingleQuoted s -> word_components_to_string s | WordGlobAll -> "*" | WordGlobAny -> "?" + | WordEmpty -> "" | WordAssignmentWord (Name p, v) -> p ^ "=" ^ word_components_to_string v | WordSubshell _ -> Fmt.failwith diff --git a/src/lib/dune b/src/lib/dune index daf3842..5a7e4b6 100644 --- a/src/lib/dune +++ b/src/lib/dune @@ -15,4 +15,4 @@ linenoise fpath cmdliner - globlon)) + merry.glob)) diff --git a/src/lib/eval.ml b/src/lib/eval.ml index 475aed4..670e54d 100644 --- a/src/lib/eval.ml +++ b/src/lib/eval.ml @@ -89,32 +89,6 @@ module Make (S : Types.State) (E : Types.Exec) = struct Ast.WordName (S.expand ctx.state `Tilde) :: tilde_expansion ctx rest | v :: rest -> v :: tilde_expansion ctx rest - let parameter_expansion' ctx = - let rec expand = function - | [] -> [] - | Ast.WordVariable v :: rest -> ( - match v with - | Ast.VariableAtom ("!", NoAttribute) -> - Ast.WordName ctx.last_background_process :: expand rest - | Ast.VariableAtom (n, NoAttribute) - when Option.is_some (int_of_string_opt n) -> ( - let n = int_of_string n in - match Array.get ctx.argv n with - | v -> Ast.WordName v :: expand rest - | exception Invalid_argument _ -> Ast.WordName "" :: expand rest) - | Ast.VariableAtom (s, NoAttribute) -> ( - match S.lookup ctx.state ~param:s with - | None -> Ast.WordName "" :: expand rest - | Some cst -> cst @ expand rest) - | _ -> Fmt.failwith "No support for variable attributes yet!") - | Ast.WordDoubleQuoted cst :: rest -> - Ast.WordDoubleQuoted (expand cst) :: expand rest - | Ast.WordSingleQuoted cst :: rest -> - Ast.WordSingleQuoted (expand cst) :: expand rest - | v :: rest -> v :: expand rest - in - (ctx, expand) - let stdout_for_pipeline ~sw ctx = function | [] -> (None, `Global ctx.stdout) | _ -> @@ -257,7 +231,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct | None -> (ctx, []) | Some suffix -> expand_redirects (ctx, []) suffix in - let args = args ctx suffix in + let ctx, args = args ctx suffix in let args_as_strings = List.map Ast.word_components_to_string args in let some_read, some_write = stdout_for_pipeline ~sw:pipeline_switch ctx rest @@ -365,6 +339,144 @@ module Make (S : Types.State) (E : Types.Exec) = struct Exit.zero { ctx with background_jobs = job :: ctx.background_jobs } end + and parameter_expansion' ctx ast = + let get_prefix ~pattern ~kind param = + let _, prefix = + String.fold_left + (fun (so_far, acc) c -> + match acc with + | Some s when kind = `Smallest -> (so_far, Some s) + | _ -> ( + let s = so_far ^ String.make 1 c in + match Glob.test ~pattern [ s ] with + | [ s ] -> (s, Some s) + | _ -> (s, acc))) + ("", None) param + in + prefix + in + let get_suffix ~pattern ~kind param = + let _, prefix = + String.fold_left + (fun (so_far, acc) c -> + match acc with + | Some s when kind = `Smallest -> (so_far, Some s) + | _ -> ( + let s = String.make 1 c ^ so_far in + match Glob.test ~pattern [ s ] with + | [ s ] -> (s, Some s) + | _ -> (s, acc))) + ("", None) + (String.fold_left (fun acc c -> String.make 1 c ^ acc) "" param) + in + prefix + in + let rec expand acc ctx = function + | [] -> (ctx, List.rev acc |> List.concat) + | Ast.WordVariable v :: rest -> ( + match v with + | Ast.VariableAtom ("!", NoAttribute) -> + expand + ([ Ast.WordName ctx.last_background_process ] :: acc) + ctx rest + | Ast.VariableAtom (n, NoAttribute) + when Option.is_some (int_of_string_opt n) -> ( + let n = int_of_string n in + match Array.get ctx.argv n with + | v -> expand ([ Ast.WordName v ] :: acc) ctx rest + | exception Invalid_argument _ -> + expand ([ Ast.WordName "" ] :: acc) ctx rest) + | Ast.VariableAtom (s, NoAttribute) -> ( + match S.lookup ctx.state ~param:s with + | None -> expand ([ Ast.WordName "" ] :: acc) ctx rest + | Some cst -> expand (cst :: acc) ctx rest) + | Ast.VariableAtom (s, ParameterLength) -> ( + match S.lookup ctx.state ~param:s with + | None -> expand ([ Ast.WordLiteral "0" ] :: acc) ctx rest + | Some cst -> + expand + ([ + Ast.WordLiteral + (string_of_int + (String.length (Ast.word_components_to_string cst))); + ] + :: acc) + ctx rest) + | Ast.VariableAtom (s, UseDefaultValues (_, cst)) -> ( + match S.lookup ctx.state ~param:s with + | None -> expand (cst :: acc) ctx rest + | Some cst -> expand (cst :: acc) ctx rest) + | Ast.VariableAtom + ( s, + (( RemoveSmallestPrefixPattern cst + | RemoveLargestPrefixPattern cst ) as v) ) -> ( + let ctx, spp = expand_cst ctx cst in + let pattern = Ast.word_components_to_string spp in + match S.lookup ctx.state ~param:s with + | None -> expand (cst :: acc) ctx rest + | Some cst -> ( + let kind = + match v with + | RemoveSmallestPrefixPattern _ -> `Smallest + | RemoveLargestPrefixPattern _ -> `Largest + | _ -> assert false + in + let param = Ast.word_components_to_string cst in + let prefix = get_prefix ~pattern ~kind param in + match prefix with + | None -> expand ([ Ast.WordName param ] :: acc) ctx rest + | Some s -> ( + match String.cut_prefix ~prefix:s param with + | Some s -> expand ([ Ast.WordName s ] :: acc) ctx rest + | None -> expand ([ Ast.WordName param ] :: acc) ctx rest) + )) + | Ast.VariableAtom + ( s, + (( RemoveSmallestSuffixPattern cst + | RemoveLargestSuffixPattern cst ) as v) ) -> ( + let ctx, spp = expand_cst ctx cst in + let pattern = Ast.word_components_to_string spp in + match S.lookup ctx.state ~param:s with + | None -> expand (cst :: acc) ctx rest + | Some cst -> ( + let kind = + match v with + | RemoveSmallestSuffixPattern _ -> `Smallest + | RemoveLargestSuffixPattern _ -> `Largest + | _ -> assert false + in + let param = Ast.word_components_to_string cst in + let suffix = get_suffix ~pattern ~kind param in + match suffix with + | None -> expand ([ Ast.WordName param ] :: acc) ctx rest + | Some s -> ( + match String.cut_suffix ~suffix:s param with + | Some s -> expand ([ Ast.WordName s ] :: acc) ctx rest + | None -> expand ([ Ast.WordName param ] :: acc) ctx rest) + )) + | Ast.VariableAtom (s, UseAlternativeValue (_, alt)) -> ( + match S.lookup ctx.state ~param:s with + | Some cst -> expand (cst :: acc) ctx rest + | None -> expand (alt :: acc) ctx rest) + | Ast.VariableAtom (s, AssignDefaultValues (_, value)) -> ( + match S.lookup ctx.state ~param:s with + | Some cst -> expand (cst :: acc) ctx rest + | None -> + let state = S.update ctx.state ~param:s value in + let new_ctx = { ctx with state } in + expand (value :: acc) new_ctx rest) + | Ast.VariableAtom (_, IndicateErrorifNullorUnset (_, _)) -> + Fmt.failwith "TODO: Indicate Error") + | Ast.WordDoubleQuoted cst :: rest -> + let new_ctx, cst_acc = expand [] ctx cst in + expand ([ Ast.WordDoubleQuoted cst_acc ] :: acc) new_ctx rest + | Ast.WordSingleQuoted cst :: rest -> + let new_ctx, cst_acc = expand [] ctx cst in + expand ([ Ast.WordSingleQuoted cst_acc ] :: acc) new_ctx rest + | v :: rest -> expand ([ v ] :: acc) ctx rest + in + expand [] ctx ast + and handle_export ctx (assignments : Ast.word_cst list) = let rec loop acc_ctx = function | [] -> Exit.zero acc_ctx @@ -383,8 +495,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct and expand_cst (ctx : ctx) cst : ctx * Ast.word_cst = let cst = tilde_expansion ctx cst in - let _, o = parameter_expansion' ctx in - (ctx, o cst) + parameter_expansion' ctx cst and expand_redirects ((ctx, acc) : ctx * Ast.cmd_suffix_item list) (c : Ast.cmd_suffix_item list) = @@ -546,8 +657,8 @@ module Make (S : Types.State) (E : Types.Exec) = struct and glob_expand ctx wc = let wc = handle_word_cst_subshell ctx wc in if Ast.has_glob wc then - Ast.word_components_to_string wc - |> Globlon.glob |> Array.to_list + Ast.word_components_to_string wc |> fun pattern -> + Glob.glob_dir ~pattern (cwd_of_ctx ctx) |> List.map (fun w -> [ Ast.WordName w ]) else [ wc ] @@ -573,14 +684,14 @@ module Make (S : Types.State) (E : Types.Exec) = struct | _ -> ctx) ctx - and args ctx swc : Ast.word_cst list = - List.concat_map - (function - | Ast.Suffix_redirect _ -> [] + and args ctx swc : ctx * Ast.word_cst list = + List.fold_left + (fun (ctx, acc) -> function + | Ast.Suffix_redirect _ -> (ctx, acc) | Suffix_word wc -> let ctx, cst = expand_cst ctx wc in - word_glob_expand ctx cst) - swc + (ctx, acc @ word_glob_expand ctx cst)) + (ctx, []) swc and handle_built_in (ctx : ctx) = function | Built_ins.Cd { path } -> diff --git a/src/lib/glob/dune b/src/lib/glob/dune new file mode 100644 index 0000000..6799ec9 --- /dev/null +++ b/src/lib/glob/dune @@ -0,0 +1,6 @@ +(library + (public_name merry.glob) + (name glob) + (libraries re)) + +(ocamllex lexer) diff --git a/src/lib/glob/glob.ml b/src/lib/glob/glob.ml new file mode 100644 index 0000000..849a0cf --- /dev/null +++ b/src/lib/glob/glob.ml @@ -0,0 +1,47 @@ +(* From dune-glob library + +The MIT License + +Copyright (c) 2016 Jane Street Group, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. *) + +type t = Re of { re : Re.re; repr : string } | Literal of string + +let test t s = + match t with + | Literal t -> String.equal t s + | Re { re; repr = _ } -> Re.execp re s + +let empty = Re { re = Re.compile Re.empty; repr = "\000" } +let universal = Re { re = Re.compile (Re.rep Re.any); repr = "**" } + +let of_string_result repr = + Lexer.parse_string repr + |> Result.map (function + | Lexer.Literal s -> Literal s + | Re re -> Re { re = Re.compile re; repr }) + +let of_string repr = + match of_string_result repr with + | Error (_, msg) -> invalid_arg (Printf.sprintf "invalid glob: :%s" msg) + | Ok t -> t + +let to_string t = match t with Re { repr; re = _ } -> repr | Literal s -> s +let hash t = String.hash (to_string t) diff --git a/src/lib/glob/glob.mli b/src/lib/glob/glob.mli new file mode 100644 index 0000000..1f0d942 --- /dev/null +++ b/src/lib/glob/glob.mli @@ -0,0 +1,21 @@ +(** Simple glob support library. *) + +type t + +val empty : t +(** A glob that matches nothing *) + +val universal : t +(** A glob that matches anything (including the strings starting with a ".") *) + +val test : t -> string -> bool +(** Tests if string matches the glob. *) + +val to_string : t -> string +(** Returns textual representation of a glob. *) + +val of_string : string -> t +(** Converts string to glob. Throws [Invalid_argument] exception if string is + not a valid glob. *) + +val hash : t -> int diff --git a/src/lib/glob/lexer.mll b/src/lib/glob/lexer.mll new file mode 100644 index 0000000..4a1584b --- /dev/null +++ b/src/lib/glob/lexer.mll @@ -0,0 +1,91 @@ +{ +open Re + +let string_of_list chars = + let s = Bytes.make (List.length chars) '0' in + List.iteri (fun i c -> Bytes.set s i c) chars; + Bytes.to_string s + +type t = + | Literal of string + | Re of Re.t + +let no_slash = diff any (char '/') +let no_slash_no_dot = diff any (set "./") + +type stack = + | Bottom + | Lbrace of stack + | Char of char * stack + | Re of Re.t * stack + | Comma of stack + +let make_group st = + let rec loop current_re full_res st = + match st with + | Bottom -> failwith "'}' without opening '{'" + | Re (re, st) -> loop (re :: current_re) full_res st + | Char (c, st) -> loop (char c :: current_re) full_res st + | Comma st -> loop [] (seq current_re :: full_res) st + | Lbrace st -> Re (alt (seq current_re :: full_res), st) + in + loop [] [] st + +let finalize st = + let rec loop acc st = + match st with + | Bottom -> seq (start :: acc) + | Re (re, st) -> loop (re :: acc) st + | Char (c, st) -> loop (char c :: acc) st + | Comma st -> loop (char ',' :: acc) st + | Lbrace _ -> failwith "unclosed '{'" + in + let rec try_str (acc : char list) st = + match st with + | Bottom -> Literal (string_of_list acc) + | Comma st -> try_str (',' :: acc) st + | Char (c, st) -> try_str (c :: acc) st + | st -> + let re = + let re = [stop] in + match acc with + | [] -> re + | _ :: _ -> str (string_of_list acc) :: re + in + Re (loop re st) + in + try_str [] st +} + +rule initial = parse + (* | "**" { glob (Re (rep any, Bottom)) lexbuf } *) + | "*" { glob (Re (rep any, Bottom)) lexbuf } + | "" { glob Bottom lexbuf } + +and glob st = parse + | eof + | '\\' eof { finalize st } + | '\\' (_ as c) { glob (Char (c , st)) lexbuf } + | "**" { glob (Re (seq [no_slash_no_dot; rep no_slash] , st)) lexbuf } + | '*' { glob (Re (rep no_slash , st)) lexbuf } + | '?' { glob (Re (no_slash , st)) lexbuf } + | '{' { glob (Lbrace st ) lexbuf } + | ',' { glob (Comma st ) lexbuf } + | '}' { glob (make_group st) lexbuf } + | '[' { char_set st lexbuf } + | ']' { failwith "']' without opening '['" } + | _ as c { glob (Char (c , st)) lexbuf } + +and char_set st = parse + | '!' ([^ ']']* as s) "]" { glob (Re (diff any (set s) , st)) lexbuf } + | ([^ ']']* as s) "]" { glob (Re (set s , st)) lexbuf } + | "" { failwith "unclosed character set" } + +{ + let parse_string s = + let lb = Lexing.from_string s in + match initial lb with + | re -> Result.Ok re + | exception Failure msg -> + Error (Lexing.lexeme_start lb, msg) +} diff --git a/src/lib/import.ml b/src/lib/import.ml index b8e6b09..c22b088 100644 --- a/src/lib/import.ml +++ b/src/lib/import.ml @@ -59,3 +59,22 @@ module Nslist = struct end let yojson_pp = Yojson.Safe.pretty_print ~std:true + +module String = struct + include String + + let cut_prefix ~prefix s = + match Astring.String.cut ~sep:prefix s with + | Some ("", rest) -> Some rest + | _ -> None + + let cut_suffix ~suffix s = + match Astring.String.cut ~sep:suffix s with + | Some (start, "") -> Some start + | _ -> None +end + +module Glob = struct + let test ~pattern s = List.filter Glob.(test (of_string pattern)) s + let glob_dir ~pattern dir = test ~pattern (Eio.Path.read_dir dir) +end diff --git a/test/attr.t b/test/attr.t new file mode 100644 index 0000000..a382357 --- /dev/null +++ b/test/attr.t @@ -0,0 +1,94 @@ +Attributes on expansions and params. + +Using default values: + + $ sh -c "echo \${FOOBAR-hello}" + hello + $ msh -c "echo \${FOOBAR-hello}" + hello + +Smallest prefix pattern: + + $ cat > test.sh << EOF + > x="foobar" + > echo "\${x#foo}" + > echo "\${x#f*}" + > echo "\${x#bar}" + > EOF + + $ sh test.sh + bar + oobar + foobar + $ msh test.sh + bar + oobar + foobar + +Largest Prefix Pattern: + + $ cat > test.sh << EOF + > x=/path/to/my/script.sh + > echo "\${x##*/}" + > EOF + + $ sh test.sh + script.sh + $ msh test.sh + script.sh + +Smallest suffix pattern: + + $ cat > test.sh << EOF + > x="foobar" + > echo "\${x%bar}" + > echo "\${x%o*}" + > echo "\${x%foo}" + > EOF + + $ sh test.sh + foo + fo + foobar + $ msh test.sh + foo + fo + foobar + +Largest suffix pattern: + + $ cat > test.sh << EOF + > x=/path/to/my/script.sh + > echo "\${x%%*/}" + > EOF + + $ sh test.sh + /path/to/my/script.sh + $ msh test.sh + /path/to/my/script.sh + +Length of param: + + $ cat > test.sh < foo=hello + > echo \${#foo} + > EOF + + $ sh test.sh + 5 + $ msh test.sh + 5 + +Assigning on the fly: + + $ cat > test.sh << EOF + > echo \${FOO:=bar} + > echo \$FOO + > EOF + + $ sh test.sh + bar + bar + $ msh test.sh + bar + bar