Skip to content

Commit 4c70ef1

Browse files
Portable sed-like functionality (#13008)
Using `sed` is somewhat temperamental: despite POSIX, the CLI is notably different between BSD and GNU systems making it tedious to use as other systems don't support the options, the regular expression support is dependant on arguments, `sed` scripts are fairly difficult to read (with `/` and `#` separators for paths instead of just command line arguments) and mostly only used to do simple replacements (so often just overkill). This PR adds a few new commands to `dune_cmd` to implement `sed`-like functionality but in a way that is consistent between GNU/Linux, BSDs, macOS and potentially Windows. It supports in-place replacement, since this is what we usually wants in our cram tests as well as interactions via stdio. Other `sed` functionality could be added, it's implemented in somewhat extensible way by parsing into a `Sed.t` action and evaluating that. I've changed some of the helpers from the `dune pkg` tests to use it. Signed-off-by: Marek Kubica <marek@tarides.com>
1 parent 18bbc94 commit 4c70ef1

File tree

2 files changed

+120
-11
lines changed

2 files changed

+120
-11
lines changed

test/blackbox-tests/test-cases/pkg/helpers.sh

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,16 @@ build_pkg() {
4141

4242
show_pkg() {
4343
prefix="$(get_build_pkg_dir $1)"
44-
find "$prefix" | sort | sed "s#$prefix##"
44+
find "$prefix" | sort | dune_cmd subst "$prefix" ""
4545
}
4646

4747
strip_sandbox() {
48-
sed -E 's#[^ ]*.sandbox/[^/]+#$SANDBOX#g'
48+
dune_cmd subst '[^ ]*.sandbox/[^/]+' '$SANDBOX'
4949
}
5050

5151
show_pkg_targets() {
5252
prefix="$(get_build_pkg_dir $1)/target"
53-
find "$prefix" | sort | sed "s#$prefix##"
53+
find "$prefix" | sort | dune_cmd subst "$prefix" ""
5454
}
5555

5656
show_pkg_cookie() {
@@ -117,7 +117,7 @@ EOF
117117
set_pkg_to () {
118118
local value="${1}"
119119
if grep "(pkg .*)" dune-workspace > /dev/null; then
120-
sed -i.bak "s/(pkg .*)/(pkg ${value})/" dune-workspace
120+
dune_cmd substitute "(pkg .*)" "(pkg ${value})" dune-workspace
121121
else
122122
echo "(pkg ${value})" >> dune-workspace
123123
fi
@@ -132,7 +132,7 @@ disable_pkg() {
132132
}
133133

134134
unset_pkg() {
135-
sed -i.bak "/(pkg/d" dune-workspace
135+
dune_cmd delete "\(pkg" dune-workspace
136136
}
137137

138138
add_mock_repo_if_needed() {
@@ -210,7 +210,7 @@ dune_pkg_lock_normalized() {
210210
else
211211
cat solve-stderr.txt \
212212
| awk '/Solution/{printf"%s:\n",$0;f=0};f{print};/Dependencies.*:/{f=1}' \
213-
| sed 's/(none)/(no dependencies to lock)/'
213+
| dune_cmd subst '\(none\)' '(no dependencies to lock)'
214214
fi
215215
else
216216
cat solve-stderr.txt | sed '/The dependency solver failed to find a solution for the following platforms:/,/\.\.\.with this error:/d'
@@ -243,7 +243,7 @@ EOF
243243
}
244244

245245
print_source() {
246-
cat "${default_lock_dir}"/"$1".pkg | sed -n "/source/,//p" | sed "s#$PWD#PWD#g" | tr '\n' ' '| tr -s " "
246+
cat "${default_lock_dir}"/"$1".pkg | sed -n "/source/,//p" | dune_cmd subst "$PWD" "PWD" | tr '\n' ' '| tr -s " "
247247
}
248248

249249
solve() {
@@ -256,6 +256,6 @@ solve() {
256256
# on different machines, such as lockfiles generated by expanding the "$PWD"
257257
# variable.
258258
sanitize_pkg_digest() {
259-
local pkg_name_and_version="${1}"
260-
sed "s#$pkg_name_and_version-[0-9a-f]*#$pkg_name_and_version-DIGEST_HASH#"
259+
local pkg_name_and_version="${1}"
260+
dune_cmd subst "$pkg_name_and_version-[0-9a-f]*" "$pkg_name_and_version-DIGEST_HASH"
261261
}

test/blackbox-tests/utils/dune_cmd.ml

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ module Wait_for_file_to_appear = struct
322322
| [ file ] ->
323323
let file = Path.of_filename_relative_to_initial_cwd file in
324324
{ file }
325-
| _ -> raise (Arg.Bad (sprintf "1 argument must be provided"))
325+
| _ -> raise (Arg.Bad "1 argument must be provided")
326326
;;
327327

328328
let run { file } =
@@ -357,6 +357,115 @@ module Exec_a = struct
357357
let () = register name of_args run
358358
end
359359

360+
module Sed = struct
361+
type io =
362+
| Inplace of Path.t
363+
| Stdio
364+
365+
type action =
366+
| Subst of
367+
{ rex : Re.re
368+
; replacement : string
369+
}
370+
| Delete of Re.re
371+
372+
type t =
373+
{ io : io
374+
; action : action
375+
}
376+
377+
let io = function
378+
| Some p -> Inplace p
379+
| None -> Stdio
380+
;;
381+
382+
let subst ?file ~rex ~replacement () =
383+
let io = io file in
384+
let action = Subst { rex; replacement } in
385+
{ io; action }
386+
;;
387+
388+
let delete ?file ~rex () =
389+
let io = io file in
390+
let action = Delete rex in
391+
{ io; action }
392+
;;
393+
394+
let run_action inputs = function
395+
| Subst { rex; replacement } ->
396+
List.map inputs ~f:(fun line ->
397+
Re.Pcre.substitute ~rex ~subst:(fun _ -> replacement) line)
398+
| Delete rex -> List.filter inputs ~f:(fun line -> not @@ Re.Pcre.pmatch ~rex line)
399+
;;
400+
401+
let run { io; action } =
402+
let inputs, output =
403+
match io with
404+
| Inplace p ->
405+
let inputs = p |> Io.read_file |> String.split_on_char ~sep:'\n' in
406+
let output outputs =
407+
let temp = p |> Path.to_string |> sprintf "%s.tmp" |> Path.of_string in
408+
Io.write_lines temp outputs;
409+
Unix.rename (Path.to_string temp) (Path.to_string p)
410+
in
411+
inputs, output
412+
| Stdio ->
413+
let inputs = Io.input_lines stdin in
414+
let output outputs =
415+
List.iter outputs ~f:(fun line ->
416+
output_string stdout line;
417+
output_char stdout '\n')
418+
in
419+
inputs, output
420+
in
421+
let outputs = run_action inputs action in
422+
output outputs
423+
;;
424+
end
425+
426+
module Run_sed (C : sig
427+
val name : string
428+
val of_args : string list -> Sed.t
429+
end) =
430+
struct
431+
let () = register C.name C.of_args Sed.run
432+
end
433+
434+
module Subst = Run_sed (struct
435+
let name = "subst"
436+
437+
let of_args = function
438+
| pattern :: replacement :: optional ->
439+
let rex = Re.Pcre.regexp pattern in
440+
let file =
441+
match optional with
442+
| [] -> None
443+
| [ filename ] -> Some (Path.of_filename_relative_to_initial_cwd filename)
444+
| _ :: _ :: _ -> raise (Arg.Bad "Too many arguments")
445+
in
446+
Sed.subst ?file ~rex ~replacement ()
447+
| _ -> raise (Arg.Bad "Required arguments are <pattern> <replacement> [file]")
448+
;;
449+
end)
450+
451+
module Delete = Run_sed (struct
452+
let name = "delete"
453+
454+
let of_args = function
455+
| pattern :: optional ->
456+
let rex = Re.Pcre.regexp pattern in
457+
Printexc.record_backtrace true;
458+
let file =
459+
match optional with
460+
| [] -> None
461+
| [ filename ] -> Some (Path.of_filename_relative_to_initial_cwd filename)
462+
| _ :: _ :: _ -> raise (Arg.Bad "Too many arguments")
463+
in
464+
Sed.delete ?file ~rex ()
465+
| _ -> raise (Arg.Bad "Required arguments are <pattern> [file]")
466+
;;
467+
end)
468+
360469
let () =
361470
let name, args =
362471
match Array.to_list Sys.argv with
@@ -368,7 +477,7 @@ let () =
368477
in
369478
match Table.find commands name with
370479
| None ->
371-
Format.eprintf "No command %S name found" name;
480+
Format.eprintf "No command named %S found" name;
372481
exit 1
373482
| Some run -> run args
374483
;;

0 commit comments

Comments
 (0)