Though Standard ML provides the OS.Process.system function to execute an arbitrary command using the default system shell, and the Posix.Process structure for fork and the exec variants, it doesn’t (as far as I know) provide a mechanism to run a process and capture that process’s standard out.

I learned from my officemate that I was essentially looking for an SML implementation of popen(3) and pclose(3). Here’s what I came up with:

structure Popen :>
	  sig
		  (* Parent wants to write, read stdout, or read stdout + stderr *)
		  datatype pipe_type = PIPE_W | PIPE_R | PIPE_RE
		  val popen : string * pipe_type -> Posix.IO.file_desc
		  val pclose : Posix.IO.file_desc -> Posix.Process.exit_status option
	  end =
struct

datatype pipe_type = PIPE_W | PIPE_R | PIPE_RE

type pinfo = { fd : Posix.ProcEnv.file_desc, pid : Posix.Process.pid }

val pids : pinfo list ref = ref []

(* Implements popen(3) *)
fun popen (cmd, t) =
  let val { infd = readfd, outfd = writefd } = Posix.IO.pipe ()
  in case (Posix.Process.fork (), t)
	  of (NONE, t) => (* Child *)
	 (( case t
		 of PIPE_W => Posix.IO.dup2 { old = readfd, new = Posix.FileSys.stdin }
		  | PIPE_R => Posix.IO.dup2 { old = writefd, new = Posix.FileSys.stdout }
		  | PIPE_RE => ( Posix.IO.dup2 { old = writefd, new = Posix.FileSys.stdout }
			   ; Posix.IO.dup2 { old = writefd, new = Posix.FileSys.stderr })
	  ; Posix.IO.close writefd
	  ; Posix.IO.close readfd
	  ; Posix.Process.execp ("/bin/sh", ["sh", "-c", cmd]))
	  handle OS.SysErr (err, _) =>
		 ( print ("Fatal error in child: " ^ err ^ "\n")
		 ; OS.Process.exit OS.Process.failure ))
	   | (SOME pid, t) => (* Parent *)
	 let val fd = case t of PIPE_W => (Posix.IO.close readfd; writefd)
				  | PIPE_R => (Posix.IO.close writefd; readfd)
				  | PIPE_RE => (Posix.IO.close writefd; readfd)
		 val _ = pids := ({ fd = fd, pid = pid } :: !pids)
	 in fd end
  end

(* Implements pclose(3) *)
fun pclose fd =
  case List.partition (fn { fd = f, pid = _ } => f = fd) (!pids)
   of ([], _) => NONE
	| ([{ fd = _, pid = pid }], pids') =>
	  let val _ = pids := pids'
	  val (_, status) = Posix.Process.waitpid (Posix.Process.W_CHILD pid, [])
	  val _ = Posix.IO.close fd
	  in SOME status end
	| _ => raise Bind (* This should be impossible. *)
end

val f = Popen.popen("ls", Popen.PIPE_R);
val g = Popen.popen("read line; echo $line>/tmp/foo", Popen.PIPE_W);
val _ = Posix.IO.writeVec (g, Word8VectorSlice.full (Byte.stringToBytes "Hello World! I was written by g\n"));
val h = Popen.popen("cat /tmp/foo", Popen.PIPE_R);
val i = Popen.popen("echo 'to stderr i' 1>&2", Popen.PIPE_R);
val j = Popen.popen("echo 'to stderr j' 1>&2", Popen.PIPE_RE);
val _ = app (fn fd => print (Byte.bytesToString (Posix.IO.readVec (fd, 1000)))) [f, h, i, j];
val _ = map Popen.pclose [f, g, h, i, j];
val _ = OS.Process.exit OS.Process.success;

and the corresponding output is:

rak@zeta:~/popen$ rm /tmp/foo && ls && sml popen.sml
popen.sml
Standard ML of New Jersey v110.79 [built: Tue Aug  8 16:57:33 2017]
[opening popen.sml]
[autoloading]
[library $SMLNJ-BASIS/basis.cm is stable]
[library $SMLNJ-BASIS/(basis.cm):basis-common.cm is stable]
[autoloading done]
popen.sml:42.52 Warning: calling polyEqual
structure Popen :
  sig
	datatype pipe_type = PIPE_R | PIPE_RE | PIPE_W
	val popen : string * pipe_type -> ?.POSIX_IO.file_desc
	val pclose : ?.POSIX_IO.file_desc -> ?.POSIX_Process.exit_status option
  end
val f = FD {fd=4} : ?.POSIX_IO.file_desc
val g = FD {fd=6} : ?.POSIX_IO.file_desc
[autoloading]
[autoloading done]
val h = FD {fd=5} : ?.POSIX_IO.file_desc
to stderr i
val i = FD {fd=7} : ?.POSIX_IO.file_desc
val j = FD {fd=8} : ?.POSIX_IO.file_desc
popen.sml
Hello World! I was written by g
to stderr j