Idiomatic calling other process with stdin, stdout, stderr stdlib

Hello,

I want to call a process and provide stdin, get stdout and if something goes wrong exit and print stderr with stdlib.

Can anyone give me feedback if this is the most idiomatic way to do this:

open Unix

let cmd_input prog args input_data =
  let environment = environment () in
  let output_channel, input_channel, error_channel = open_process_args_full prog args environment in
  let concatted = String.concat "\n" input_data in
  output_string input_channel concatted;
  close_out input_channel;
  let rec read_result_lines acc =
      try
        read_result_lines (input_line output_channel :: acc)
      with End_of_file ->
        List.rev acc
    in
  let rec read_error_lines acc =
  	  try
  	  	read_error_lines (input_line error_channel :: acc)
  	  with End_of_file ->
  	  	List.rev acc
    in
  let result_lines = read_result_lines [] in
  let error_lines = String.concat "\n" (read_error_lines []) in
  let status = close_process_full (output_channel, input_channel, error_channel) in
  match status with
  | WEXITED 0 -> result_lines
  | WEXITED n -> failwith (Printf.sprintf "Command failed with exit code %d and errors: %s" n error_lines)
  | WSIGNALED s -> failwith (Printf.sprintf "Command killed by signal %d and errors: %s" s error_lines)
  | WSTOPPED s -> failwith (Printf.sprintf "Command stopped by signal %d and erros: %s" s error_lines)

let () = 
  let input = [ "some data"; "line 2" ] in
  cmd_input "/bin/cat" [| "/bin/cat" |] input |> List.iter print_endline
1 Like

As an addition, these are my examples if not everything is needed:

open Unix

let cmd_output prog args =
  let input_channel = open_process_args_in prog args in
  let rec read_lines acc =
    try
      read_lines (input_line input_channel :: acc)
    with End_of_file ->
      List.rev acc
  in
  let lines = read_lines [] in
  let status = close_process_in input_channel in
  match status with
  | WEXITED 0 -> lines
  | WEXITED n -> failwith (Printf.sprintf "Command failed with exit code %d" n)
  | WSIGNALED s -> failwith (Printf.sprintf "Command killed by signal %d" s)
  | WSTOPPED s -> failwith (Printf.sprintf "Command stopped by signal %d" s)

let () = cmd_output "/bin/ls" [| "/bin/ls"; "-l"; "/tmp/" |] |> List.iter print_endline

-------

open Unix

let cmd_input prog args input_data =
  let output_channel = open_process_args_out prog args in
  let concatted = String.concat "\n" input_data in
  output_string output_channel concatted;
  close_out output_channel;
  let status = close_process_out output_channel in  
  match status with
  | WEXITED 0 -> [ ]
  | WEXITED n -> failwith (Printf.sprintf "Command failed with exit code %d" n)
  | WSIGNALED s -> failwith (Printf.sprintf "Command killed by signal %d" s)
  | WSTOPPED s -> failwith (Printf.sprintf "Command stopped by signal %d" s)

let () = 
  let input = [ "some data"; "line 2" ] in
  cmd_input "/bin/cat" [| "/bin/cat" |] input |> List.iter print_endline

------

open Unix

let cmd_input prog args input_data =
  let input_channel, output_channel = open_process_args prog args in
  let concatted = String.concat "\n" input_data in
  output_string output_channel concatted;
  close_out output_channel;
  let rec read_lines acc =
      try
        read_lines (input_line input_channel :: acc)
      with End_of_file ->
        List.rev acc
    in
  let output_lines = read_lines [] in
  let status = close_process (input_channel, output_channel) in
  match status with
  | WEXITED 0 -> output_lines
  | WEXITED n -> failwith (Printf.sprintf "Command failed with exit code %d" n)
  | WSIGNALED s -> failwith (Printf.sprintf "Command killed by signal %d" s)
  | WSTOPPED s -> failwith (Printf.sprintf "Command stopped by signal %d" s)

let () = 
  let input = [ "some data"; "line 2" ] in
  cmd_input "/bin/cat" [| "/bin/cat" |] input |> List.iter print_endline

------

open Unix

let cmd_input prog args input_data =
  let environment = environment () in
  let output_channel, input_channel, error_channel = open_process_args_full prog args environment in
  let concatted = String.concat "\n" input_data in
  output_string input_channel concatted;
  close_out input_channel;
  let rec read_result_lines acc =
      try
        read_result_lines (input_line output_channel :: acc)
      with End_of_file ->
        List.rev acc
    in
  let rec read_error_lines acc =
  	  try
  	  	read_error_lines (input_line error_channel :: acc)
  	  with End_of_file ->
  	  	List.rev acc
    in
  let result_lines = read_result_lines [] in
  let error_lines = String.concat "\n" (read_error_lines []) in
  let status = close_process_full (output_channel, input_channel, error_channel) in
  match status with
  | WEXITED 0 -> result_lines
  | WEXITED n -> failwith (Printf.sprintf "Command failed with exit code %d\n and errors: %s" n error_lines)
  | WSIGNALED s -> failwith (Printf.sprintf "Command killed by signal %d and errors: %s" s error_lines)
  | WSTOPPED s -> failwith (Printf.sprintf "Command stopped by signal %d and erros: %s" s error_lines)

let () = 
  let input = [ "some data"; "line 2" ] in
  cmd_input "/bin/cat" [| "/bin/cat" |] input |> List.iter print_endline

---- to test error: 

let () = 
  let input = [ "b"; "a" ] in
  cmd_input "sort" [| "sort"; "-c" |] input |> List.iter print_endline

I didn’t look at the details, but the code looks more-or-less reasonable from a distance. You can also do it with the just the standard library (ie without using Unix) by using temporary files. Something like (not tested!):

let cmd_input prog args input_data =
  let stdin = Filename.temp_file "" "stdin" in
  let stdout = Filename.temp_file "" "stdout" in
  let stderr = Filename.temp_file "" "stderr" in
  Out_channel.with_open_out stdin
    (fun oc -> Out_channel.output_string oc input_data);
  let n =
    Sys.command (Filename.quote_command prog ~stdin ~stdout ~stderr args) in
  if n <> 0 then
    Printf.ksprintf failwith "Command failed with exit code %d and errors: %s"
      n (In_channel.with_open_bin stderr In_channel.input_all);
  In_channel.with_open_bin stdout In_channel.input_all

(In actual code, you should clean up the temporary files after using them.)

Cheers,
Nicolas

1 Like

I think this can lead to a deadlock if your called process wants to dump a lot of data on stderr and blocks until that data can be written (assuming buffers are too small) before continuing writing to stdout and closing stdout.


To demonstrate:

let () = 
  let input = [ "some data"; "line 2" ] in
  cmd_input
    "/bin/sh" [|
      "/bin/sh";
      "-c";
      "dd if=/dev/zero bs=1k count=256 > /dev/stderr; cat"
    |]
    input
    |> List.iter print_endline

This command would lead to a deadlock. You could circumvent it (in this case) by switching the processing order of stderr and stdout:

   	  	List.rev acc
     in
-  let result_lines = read_result_lines [] in
   let error_lines = String.concat "\n" (read_error_lines []) in
+  let result_lines = read_result_lines [] in
   let status = close_process_full (output_channel, input_channel, error_channel) in
   match status with

But that doesn’t solve the problem but makes it even worse if there is too much data on stdout that needs to be read (while stderr is not closed yet).

Unless you are sure that either stdout or stderr fits in the I/O buffers, you would need some sort of threading/polling/async etc. to read on both channels without blocking if either of them has no data.

The alternative would be to not use something that resembles “popen3” (aka open_process_args_full) but use something like popen (aka open_process_args_in or open_process_args_out), where you only connect a single stream (either reading or writing), or to ensure that only programs are called for which you know when and if they write data to stderr, and the order in which stdin and stdout are processed.

See also this StackOverflow question on the same subject. You can also have a look at how Feather does it using threads. It would be nice to have a convenience function for this in the stdlib.