(* $Id$ *)

(** Shell filesystem *)

(** This module emulates a filesystem by submitting shell commands.
    For example, a directory listing is retrieved via the [ls]
    utility instead of opening the directory directly. This also
    works when logging in to a remote machine, e.g. via [ssh].

    The following standard POSIX commands are used:
    - [dd] with options [if], [of], [bs], [skip], [conv=notrunc], 
      and optionally [excl] (the latter is a GNU extension)
    - [test] with options [-d], [-e], [-f], [-r], [-s], [-w], [-x]
    - [ls] with options [-1], [-n], [-d], [-a], [-L]
    - [rm] with options [-r] and [-f]
    - [mv] with option [-f]
    - [ln] with option [-s]
    - [mkdir] with options [-p]
    - [rmdir]
    - [cp] with option [-p]

    In addition to this, the commands may be embedded in one-line shell
    scripts.

    Filenames with leading minus chars are not supported.

    Error reporting is less accurate than for a local filesystem.

    {b Example.} List a directory on a remote system:

    {[
      let fs = 
        Shell_fs.shell_fs 
          (Shell_fs.ssh_interpreter ~host:"moon" ())
      let files =
        fs # readdir [] "/this/is/a/directory/on/moon"
    ]}
 *)


type command_context =
    { sfs_command : string;          (** The command line *)
      sfs_stdin : Shell.producer;    (** stdin from here *)
      sfs_stdout : Shell.consumer;   (** stdout goes here *)
      sfs_stderr : Shell.consumer;   (** stderr goes here *)
      mutable sfs_status : Unix.process_status option; (** The exit code is put here *)
    }

type command_interpreter
  (** The interpreter runs the command, and fills in [sfs_status] *)

val local_interpreter : unit -> command_interpreter
  (** Executes commands on the local machine *)

val cmd_interpreter : (command_context -> Shell_sys.command list) -> 
                       command_interpreter
  (** Creates a command interpreter from a function that creates the
      real command (as pipeline) to execute
   *)

val ssh_interpreter : ?options:string list -> ?user:string -> host:string ->
                      unit -> command_interpreter
  (** Executes commands via ssh on the machine [host] as [user] (defaults
      to current user). [options] are further command-line options.
      By default, only [-o BatchMode yes] is passed.
   *)

class type shell_stream_fs =
object
  inherit Netfs.stream_fs

  method last_stderr : string
    (** The stderr output of the last operation *)
end


class shell_fs : ?encoding:Netconversion.encoding -> ?root:string -> 
                 ?dd_has_excl:bool ->
                 ?tmp_directory:string -> ?tmp_prefix:string ->
                 command_interpreter -> shell_stream_fs
val shell_fs : ?encoding:Netconversion.encoding -> ?root:string -> 
               ?dd_has_excl:bool ->
               ?tmp_directory:string -> ?tmp_prefix:string ->
               command_interpreter -> shell_stream_fs
  (** The shell filesystem.

      - [encoding]: the assumed character encoding of the filenames.
        [None] by default.
      - [root]: the root of the file tree that is accessed. This can
        be an absolute path, or a relative path. 
      - [dd_has_excl]: whether the [dd] command support "conv=excl".
        Default is [false]; this is a GNU extension.
      - [tmp_directory] and [tmp_prefix] are only meaningful for
        the [write_file] method which creates a temporary file.
        See {!Netchannels.make_temporary_file} for more information.
     
   *)


(** {2 Utility functions for developing other implementations of 
    [shell_stream_fs]} *)

val execute : command_interpreter -> command_context -> unit
  (** Starts this command. It is not waited until the command is finished.
      One can either call [wait] for this, or one of the adapter
      functions below.

      One can only start one command at a time.
   *)

val wait : command_interpreter -> unit
  (** Waits until the running command is finished *)

val output_stream_adapter : ci:command_interpreter -> 
                           close_in:(unit -> unit) ->
                           skip:int64 ->
                             Shell.consumer * Netchannels.in_obj_channel
  (** Arranges that the output of a shell command is made available as
      an [in_obj_channel]:

      {[ let (c, ch) = output_stream_adapter ~ci ~close_in ~skip ]}

      The consumer [p] can be used in a [command_context] for either
      [sfs_stdout] or [sfs_stderr]. The channel [ch] is an input
      channel, and when reading from it will return the bytes of
      stdout or stderr.

      [close_in] is called as post-hook when the [close_in] method of
      [ch] is called. 

      [skip] bytes of stdout/stderr are skipped at the beginning of the
      stream.
   *)

val input_stream_adapter : ci:command_interpreter ->
                           close_out:(unit -> unit) ->
                             Shell.producer * Netchannels.out_obj_channel
  (** Arranges that the input of a shell command is made available as
      an [out_obj_channel]:

      {[ let (p, ch) = input_stream_adapter ~ci ~close_in ]}

      The producer [p] can be used in a [command_context] for 
      [sfs_stdin]. The channel [ch] is an output
      channel, and bytes written to it will appear in stdin of the
      executed command.

      [close_out] is called as post-hook when the [close_out] method of
      [ch] is called. 
   *)
