class Process
  # A struct representing the CPU current times of the process,
  # in fractions of seconds.
  #
  # * *utime*: CPU time a process spent in userland.
  # * *stime*: CPU time a process spent in the kernel.
  # * *cutime*: CPU time a processes terminated children (and their terminated children) spent in the userland.
  # * *cstime*: CPU time a processes terminated children (and their terminated children) spent in the kernel.
  record Tms, utime : Float64, stime : Float64, cutime : Float64, cstime : Float64
end

require "crystal/system/process"

class Process
  # Terminate the current process immediately. All open files, pipes and sockets
  # are flushed and closed, all child processes are inherited by PID 1. This does
  # not run any handlers registered with `at_exit`, use `::exit` for that.
  #
  # *status* is the exit status of the current process.
  def self.exit(status : Int32 | Process::Status = 0) : NoReturn
    Crystal::System::Process.exit(status)
  end

  # Returns the process identifier of the current process.
  def self.pid : Int64
    Crystal::System::Process.pid.to_i64
  end

  # Returns the process group identifier of the current process.
  def self.pgid : Int64
    Crystal::System::Process.pgid.to_i64
  end

  # Returns the process group identifier of the process identified by *pid*.
  def self.pgid(pid : Int) : Int64
    Crystal::System::Process.pgid(pid).to_i64
  end

  # Returns the process identifier of the parent process of the current process.
  #
  # On Windows, the parent is associated only at process creation time, and the
  # system does not re-parent the current process if the parent terminates; thus
  # `Process.exists?(Process.ppid)` is not guaranteed to be true.
  def self.ppid : Int64
    Crystal::System::Process.ppid.to_i64
  end

  # Sends *signal* to the process identified by *pid*.
  def self.signal(signal : Signal, pid : Int) : Nil
    Crystal::System::Process.signal(pid, signal.value)
  end

  # Installs *handler* as the new handler for interrupt requests. Removes any
  # previously set interrupt handler.
  #
  # The handler is executed on a fresh fiber every time an interrupt occurs.
  #
  # * On Unix-like systems, this traps `SIGINT`.
  # * On Windows, this captures <kbd>Ctrl</kbd> + <kbd>C</kbd> and
  #   <kbd>Ctrl</kbd> + <kbd>Break</kbd> signals sent to a console application.
  @[Deprecated("Use `#on_terminate` instead")]
  def self.on_interrupt(&handler : ->) : Nil
    Crystal::System::Process.on_interrupt(&handler)
  end

  # Installs *handler* as the new handler for termination requests. Removes any
  # previously set termination handler.
  #
  # The handler is executed on a fresh fiber every time an interrupt occurs.
  #
  # * On Unix-like systems, this traps `SIGINT`, `SIGHUP` and `SIGTERM`.
  # * On Windows, this captures <kbd>Ctrl</kbd> + <kbd>C</kbd>,
  #   <kbd>Ctrl</kbd> + <kbd>Break</kbd>, terminal close, windows logoff
  #   and shutdown signals sent to a console application.
  #
  # ```
  # wait_channel = Channel(Nil).new
  #
  # Process.on_terminate do |reason|
  #   case reason
  #   when .interrupted?
  #     puts "terminating gracefully"
  #     wait_channel.close
  #   when .terminal_disconnected?
  #     puts "reloading configuration"
  #   when .session_ended?
  #     puts "terminating forcefully"
  #     Process.exit
  #   end
  # end
  #
  # wait_channel.receive?
  # puts "bye"
  # ```
  def self.on_terminate(&handler : ::Process::ExitReason ->) : Nil
    Crystal::System::Process.on_terminate(&handler)
  end

  # Ignores all interrupt requests. Removes any custom interrupt handler set
  # with `#on_terminate`.
  #
  # * On Windows, interrupts generated by <kbd>Ctrl</kbd> + <kbd>Break</kbd>
  #   cannot be ignored in this way.
  def self.ignore_interrupts! : Nil
    Crystal::System::Process.ignore_interrupts!
  end

  # Restores default handling of interrupt requests.
  def self.restore_interrupts! : Nil
    Crystal::System::Process.restore_interrupts!
  end

  # Returns whether a debugger is attached to the current process.
  #
  # Currently supported on Windows and Linux. Always returns `false` on other
  # systems.
  @[Experimental]
  def self.debugger_present? : Bool
    Crystal::System::Process.debugger_present?
  end

  # Returns `true` if the process identified by *pid* is valid for
  # a currently registered process, `false` otherwise. Note that this
  # returns `true` for a process in the zombie or similar state.
  def self.exists?(pid : Int) : Bool
    Crystal::System::Process.exists?(pid)
  end

  # Returns a `Tms` for the current process. For the children times, only those
  # of terminated children are returned on Unix; they are zero on Windows.
  def self.times : Tms
    Crystal::System::Process.times
  end

  # :nodoc:
  #
  # Runs the given block inside a new process and
  # returns a `Process` representing the new child process.
  #
  # Available only on Unix-like operating systems.
  @[Deprecated("Fork is no longer supported.")]
  def self.fork(&) : Process
    new Crystal::System::Process.fork { yield }
  end

  # :nodoc:
  #
  # Duplicates the current process.
  # Returns a `Process` representing the new child process in the current process
  # and `nil` inside the new child process.
  #
  # Available only on Unix-like operating systems.
  @[Deprecated("Fork is no longer supported.")]
  def self.fork : Process?
    {% raise("Process fork is unsupported with multithread mode") if flag?(:preview_mt) %}

    if pid = Crystal::System::Process.fork
      new pid
    end
  end

  # How to redirect the standard input, output and error IO of a process.
  enum Redirect
    # Pipe the IO so the parent process can read (or write) to the process IO
    # through `#input`, `#output` or `#error`.
    Pipe

    # Discards the IO.
    Close

    # Use the IO of the parent process.
    Inherit
  end

  # The standard `IO` configuration of a process.
  alias Stdio = Redirect | IO
  alias ExecStdio = Redirect | IO::FileDescriptor
  alias Env = Nil | Hash(String, Nil) | Hash(String, String?) | Hash(String, String)

  # Executes a child process and waits for it to complete, returning its status.
  #
  # See `Process.new` for the meaning of the parameters.
  #
  # Returns a `Process::Status` representing the child process' exit status.
  #
  # Raises `IO::Error` if the execution itself fails (for example because the
  # executable does not exist or is not executable).
  #
  # Example:
  #
  # ```
  # io = IO::Memory.new
  # status = Process.run(%w[echo hello], output: io)
  # io.to_s # => "hello\n"
  # status  # => Process::Status[0]
  # ```
  @[Experimental]
  def self.run(args : Enumerable(String), *, env : Env = nil, clear_env : Bool = false,
               input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : Path | String? = nil) : Process::Status
    new(args, env: env, clear_env: clear_env, input: input, output: output, error: error, chdir: chdir).wait
  end

  # Executes a child process and waits for it to complete, returning its status.
  #
  # See `Process.new` for the meaning of the parameters.
  #
  # Returns a `Process::Status` representing the child process' exit status.
  # The global `$?` variable is set to the returned status.
  #
  # Raises `IO::Error` if the execution itself fails (for example because the
  # executable does not exist or is not executable).
  #
  # Example:
  #
  # ```
  # status = Process.run("echo", ["hello"], output: Process::Redirect::Inherit)
  # # outputs "hello\n"
  # $?     # => Process::Status[0]
  # status # => Process::Status[0]
  # ```
  def self.run(command : String, args : Enumerable(String)? = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false,
               input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : Path | String? = nil) : Process::Status
    status = new(command, args, env, clear_env, shell, input, output, error, chdir).wait
    $? = status
    status
  end

  # Executes a child process and waits for it to complete, returning its status.
  #
  # See `Process.new` for the meaning of the parameters.
  #
  # Returns a `Process::Status` representing the child process' exit status.
  # The global `$?` variable is set to the returned status.
  #
  # Returns `nil` if the execution itself fails (for example because the
  # executable does not exist or is not executable).
  #
  # Example:
  #
  # ```
  # Process.run?(["true"])        # => Process::Status[0]
  # Process.run?(["nonexistent"]) # => nil
  # ```
  @[Experimental]
  def self.run?(args : Enumerable(String), *,
                env : Env = nil, clear_env : Bool = false,
                input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close,
                chdir : Path | String? = nil) : Process::Status?
    status = new(args, env: env, clear_env: clear_env, input: input, output: output, error: error, chdir: chdir) { return nil }.wait
    status
  end

  # Executes a child process, yields the block, and then waits for it to finish.
  #
  # See `Process.new` for the meaning of the parameters.
  #
  # By default the process is configured to use pipes for input, output and error.
  # These will be closed automatically at the end of the block.
  #
  # Returns a tuple with the process' exit status and the block's output value.
  #
  # Raises `IO::Error` if the execution itself fails (for example because the
  # executable does not exist or is not executable).
  #
  # Example:
  #
  # ```
  # status, result = Process.run(%w[echo hello]) do |process|
  #   process.output.gets_to_end
  # end
  # status # => Process::Status[0]
  # result # => "hello\n"
  # ```
  @[Experimental]
  def self.run(args : Enumerable(String), *, env : Env = nil, clear_env : Bool = false,
               input : Stdio = Redirect::Pipe, output : Stdio = Redirect::Pipe, error : Stdio = Redirect::Pipe, chdir : Path | String? = nil, & : Process -> _)
    process = new(args, env: env, clear_env: clear_env, input: input, output: output, error: error, chdir: chdir)
    begin
      value = yield process

      process.close
      status = process.wait
      {status, value}
    rescue ex
      process.terminate
      raise ex
    end
  end

  # Executes a child process, yields the block, and then waits for it to finish.
  #
  # See `Process.new` for the meaning of the parameters.
  #
  # By default the process is configured to use pipes for input, output and error.
  # These will be closed automatically at the end of the block.
  #
  # Returns the block's value.
  #
  # Raises `IO::Error` if the execution itself fails (for example because the
  # executable does not exist or is not executable).
  #
  # Example:
  #
  # ```
  # output = Process.run("echo", ["hello"]) do |process|
  #   process.output.gets_to_end
  # end
  # $?     # => Process::Status[0]
  # output # => "hello\n"
  # ```
  def self.run(command : String, args : Enumerable(String)? = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false,
               input : Stdio = Redirect::Pipe, output : Stdio = Redirect::Pipe, error : Stdio = Redirect::Pipe, chdir : Path | String? = nil, &)
    process = new(command, args, env, clear_env, shell, input, output, error, chdir)
    begin
      value = yield process

      process.close
      $? = process.wait
      value
    rescue ex
      process.terminate
      raise ex
    end
  end

  # Replaces the current process with a new one. This function never returns.
  #
  # Raises `IO::Error` if executing the command fails (for example if the executable doesn't exist).
  def self.exec(command : String, args : Enumerable(String)? = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false,
                input : ExecStdio = Redirect::Inherit, output : ExecStdio = Redirect::Inherit, error : ExecStdio = Redirect::Inherit, chdir : Path | String? = nil) : NoReturn
    input = exec_stdio_to_fd(input, for: STDIN)
    output = exec_stdio_to_fd(output, for: STDOUT)
    error = exec_stdio_to_fd(error, for: STDERR)

    Crystal::System::Process.replace(command, args, shell, env, clear_env, input, output, error, chdir)
  end

  private def self.exec_stdio_to_fd(stdio : ExecStdio, for dst_io : IO::FileDescriptor) : IO::FileDescriptor
    case stdio
    when IO::FileDescriptor
      stdio
    when Redirect::Pipe
      raise "Cannot use Process::Redirect::Pipe for Process.exec"
    when Redirect::Inherit
      dst_io
    when Redirect::Close
      if dst_io == STDIN
        File.open(File::NULL, "r")
      else
        File.open(File::NULL, "w")
      end
    else
      raise "BUG: Impossible type in ExecStdio #{stdio.class}"
    end
  end

  # Returns the process identifier of this process.
  def pid : Int64
    @process_info.pid.to_i64
  end

  # A pipe to this process' input. Raises if a pipe wasn't asked when creating the process.
  getter! input : IO::FileDescriptor

  # A pipe to this process' output. Raises if a pipe wasn't asked when creating the process.
  getter! output : IO::FileDescriptor

  # A pipe to this process' error. Raises if a pipe wasn't asked when creating the process.
  getter! error : IO::FileDescriptor

  @process_info : Crystal::System::Process
  @wait_count = 0

  # Creates and executes a child process.
  #
  # This starts a new process for the command given in *args[0]*.
  #
  # The command is either a path to the executable to run, or the name of an
  # executable which is then looked up by the operating system.
  # The lookup uses the `PATH` variable of the current process environment
  # (i.e. `ENV["PATH"]).
  # In order to resolve to a specific executable, provide a path instead of
  # only a command name. `Process.find_executable` can help with looking up a
  # command in a custom `PATH`.
  #
  # The following arguments in *args* are passed as arguments to the child process.
  #
  # Raises `IO::Error` if executing *args[0]* fails, for example because the
  # executable doesn't exist or is not executable.
  #
  # *env* provides a mapping of environment variables for the child process.
  # If *clear_env* is `true`, only these explicit variables are used; if `false`,
  # the child inherits the parent's environment with *env*  merged.
  #
  # *input*, *output*, *error* configure the child process's standard streams.
  # * `Redirect::Close` passes the null device
  # * `Redirect::Pipe` creates a pipe that's accessible via `#input`, `#output`
  #    or `#error`.
  # * `Redirect::Inherit` to share the parent's streams (`STDIN`, `STDOUT`, `STDERR`).
  # * An `IO` instance creates a pipe that reads/writes into the given IO.
  #
  # *chdir* changes the working directory of the child process. If `nil`, uses
  # the current working directory of the parent process.
  #
  # Example:
  #
  # ```
  # process = Process.new(["echo", "Hello"], output: Process::Redirect::Pipe)
  # process.output.gets_to_end # => "Hello\n"
  # process.wait               # => Process::Status[0]
  # ```
  #
  # Similar methods:
  #
  # * `Process.run` is a convenient short cut if you just want to run a command
  #    and wait for it to finish.
  # * `Process.exec` replaces the current process.
  @[Experimental]
  def self.new(args : Enumerable(String), *, env : Env = nil, clear_env : Bool = false,
               input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : Path | String? = nil)
    new(args, env: env, clear_env: clear_env, input: input, output: output, error: error, chdir: chdir) do |error, command|
      raise ::File::Error.from_os_error("Error executing process", error, file: command)
    end
  end

  # :nodoc:
  protected def initialize(args : Enumerable(String), *, env : Env = nil, clear_env : Bool = false,
                           input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : Path | String? = nil, &)
    raise File::NotFoundError.new("Error executing process: No command", file: "") if args.empty?

    fork_input = stdio_to_fd(input, for: STDIN)
    fork_output = stdio_to_fd(output, for: STDOUT)
    fork_error = stdio_to_fd(error, for: STDERR)

    prepared_args = Crystal::System::Process.prepare_args(args)
    pid = Crystal::System::Process.spawn(prepared_args, false, env, clear_env, fork_input, fork_output, fork_error, chdir.try &.to_s) do |error, command|
      yield error, command
    end
    @process_info = Crystal::System::Process.new(pid)

    fork_input.close unless fork_input.in?(input, STDIN)
    fork_output.close unless fork_output.in?(output, STDOUT)
    fork_error.close unless fork_error.in?(error, STDERR)
  end

  # Creates and executes a child process.
  #
  # This starts a new process for `command`.
  #
  # ## `shell: false` (the default)
  #
  # *command* is either a path to the executable to run, or the name of an
  # executable which is then looked up by the operating system.
  # The lookup uses the `PATH` variable of the current process environment
  # (i.e. `ENV["PATH"]).
  # In order to resolve to a specific executable, provide a path instead of
  # only a command name. `Process.find_executable` can help with looking up a
  # command in a custom `PATH`.
  #
  # The arguments in *args* are passed as arguments to the child process.
  #
  # Raises `IO::Error` if executing *command*  fails, for example because the
  # executable doesn't exist or is not executable.
  #
  # ## `shell: true`
  #
  # *command* is a shell script executed in the system shell (`/bin/sh` on Unix
  # systems, `cmd.exe` on Windows).
  # Command names are looked up by the shell itself, using the `PATH` variable
  # of the shell process (i.e. `env["PATH"]`).
  #
  # *args* is unsupported on Windows.
  # On Unix it's passed as additional arguments to the shell process and can be
  # used in the shell script with `"${@}"` to safely insert them there. If the
  # script is a single command (no whitespace), `"${@}"` is appended implicitly.
  #
  # The returned instance represents the shell process, not the process executed
  # for *command*.
  #
  # If executing *command*  fails, for example because the executable doesn't
  # exist or is not executable, it may raise `IO::Error` (on Windows) or return
  # an unsuccessful exit status (on Unix).
  #
  # ## Shared parameters
  #
  # *env* provides a mapping of environment variables for the child process.
  # If *clear_env* is `true`, only these explicit variables are used; if `false`,
  # the child inherits the parent's environment with *env*  merged.
  #
  # *input*, *output*, *error* configure the child process's standard streams.
  # * `Redirect::Close` passes the null device
  # * `Redirect::Pipe` creates a pipe that's accessible via `#input`, `#output`
  #    or `#error`.
  # * `Redirect::Inherit` to share the parent's streams (`STDIN`, `STDOUT`, `STDERR`).
  # * An `IO` instance creates a pipe that reads/writes into the given IO.
  #
  # *chdir* changes the working directory of the child process. If `nil`, uses
  # the current working directory of the parent process.
  #
  # Example:
  #
  # ```
  # process = Process.new("echo", ["Hello"], output: Process::Redirect::Pipe)
  # process.output.gets_to_end # => "Hello\n"
  # process.wait               # => Process::Status[0]
  # ```
  #
  # Similar methods:
  #
  # * `Process.run` is a convenient short cut if you just want to run a command
  #    and wait for it to finish.
  # * `Process.exec` replaces the current process.
  def initialize(command : String, args : Enumerable(String)? = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false,
                 input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : Path | String? = nil)
    fork_input = stdio_to_fd(input, for: STDIN)
    fork_output = stdio_to_fd(output, for: STDOUT)
    fork_error = stdio_to_fd(error, for: STDERR)

    prepared_args = Crystal::System::Process.prepare_args(command, args, shell)
    pid = Crystal::System::Process.spawn(prepared_args, shell, env, clear_env, fork_input, fork_output, fork_error, chdir.try &.to_s) do |error, command|
      raise ::File::Error.from_os_error("Error executing process", error, file: command)
    end
    @process_info = Crystal::System::Process.new(pid)

    fork_input.close unless fork_input.in?(input, STDIN)
    fork_output.close unless fork_output.in?(output, STDOUT)
    fork_error.close unless fork_error.in?(error, STDERR)
  end

  def finalize : Nil
    @process_info.release
  end

  private def stdio_to_fd(stdio : Stdio, for dst_io : IO::FileDescriptor) : IO::FileDescriptor
    case stdio
    in IO::FileDescriptor
      # on Windows, only async pipes can be passed to child processes, async
      # regular files will report an error and those require a separate pipe
      # (https://github.com/crystal-lang/crystal/pull/13362#issuecomment-1519082712)
      {% if flag?(:win32) %}
        unless stdio.system_blocking? || stdio.info.type.pipe?
          return io_to_fd(stdio, for: dst_io)
        end
      {% end %}

      stdio
    in IO
      io_to_fd(stdio, for: dst_io)
    in Redirect::Pipe
      case dst_io
      when STDIN
        fork_io, @input = IO.pipe(read_blocking: true)
      when STDOUT
        @output, fork_io = IO.pipe(write_blocking: true)
      when STDERR
        @error, fork_io = IO.pipe(write_blocking: true)
      else
        raise "BUG: Unknown destination io #{dst_io}"
      end

      fork_io
    in Redirect::Inherit
      dst_io
    in Redirect::Close
      if dst_io == STDIN
        File.open(File::NULL, "r")
      else
        File.open(File::NULL, "w")
      end
    end
  end

  private def io_to_fd(stdio : Stdio, for dst_io : IO::FileDescriptor) : IO::FileDescriptor
    if stdio.closed?
      if dst_io == STDIN
        return File.open(File::NULL, "r").tap(&.close)
      else
        return File.open(File::NULL, "w").tap(&.close)
      end
    end

    if dst_io == STDIN
      fork_io, process_io = IO.pipe(read_blocking: true)

      @wait_count += 1
      ensure_channel
      spawn { copy_io(stdio, process_io, channel, close_dst: true) }
    else
      process_io, fork_io = IO.pipe(write_blocking: true)

      @wait_count += 1
      ensure_channel
      spawn { copy_io(process_io, stdio, channel, close_src: true) }
    end

    fork_io
  end

  {% unless flag?(:interpreted) %}
    # :nodoc:
    def initialize(pid : LibC::PidT)
      @process_info = Crystal::System::Process.new(pid)
    end
  {% end %}

  # Sends *signal* to this process.
  #
  # NOTE: `#terminate` is preferred over `signal(Signal::TERM)` and
  # `signal(Signal::KILL)` as a portable alternative which also works on
  # Windows.
  def signal(signal : Signal) : Nil
    Crystal::System::Process.signal(@process_info.pid, signal)
  end

  # Waits for this process to complete and closes any pipes.
  def wait : Process::Status
    @wait_count.times do
      ex = channel.receive
      raise ex if ex
    end
    @wait_count = 0

    Process::Status.new(@process_info.wait)
  ensure
    close
    @process_info.release
  end

  # Whether the process is still registered in the system.
  # Note that this returns `true` for processes in the zombie or similar state.
  def exists? : Bool
    @process_info.exists?
  end

  # Whether this process is already terminated.
  def terminated? : Bool
    !exists?
  end

  # Closes any system resources (e.g. pipes) held for the child process.
  def close : Nil
    close_io @input
    close_io @output
    close_io @error
  end

  # Asks this process to terminate.
  #
  # If *graceful* is true, prefers graceful termination over abrupt termination
  # if supported by the system.
  #
  # * On Unix-like systems, this causes `Signal::TERM` to be sent to the process
  #   instead of `Signal::KILL`.
  # * On Windows, this parameter has no effect and graceful termination is
  #   unavailable. The terminated process has an exit status of 1.
  def terminate(*, graceful : Bool = true) : Nil
    @process_info.terminate(graceful: graceful)
  end

  private def channel
    if channel = @channel
      channel
    else
      raise "BUG: Notification channel was not initialized for this process"
    end
  end

  private def ensure_channel
    @channel ||= Channel(Exception?).new
  end

  private def copy_io(src, dst, channel, close_src = false, close_dst = false)
    return unless src.is_a?(IO) && dst.is_a?(IO)

    begin
      IO.copy(src, dst)

      # close is called here to trigger exceptions
      # close must be called before channel.send or the process may deadlock
      src.close if close_src
      close_src = false
      dst.close if close_dst
      close_dst = false

      channel.send nil
    rescue ex
      channel.send ex
    ensure
      # any exceptions are silently ignored because of spawn
      src.close if close_src
      dst.close if close_dst
    end
  end

  private def close_io(io)
    io.close if io
  end

  # Changes the root directory and the current working directory for the current
  # process.
  #
  # Available only on Unix-like operating systems.
  #
  # Security: `chroot` on its own is not an effective means of mitigation. At minimum
  # the process needs to also drop privileges as soon as feasible after the `chroot`.
  # Changes to the directory hierarchy or file descriptors passed via `recvmsg(2)` from
  # outside the `chroot` jail may allow a restricted process to escape, even if it is
  # unprivileged.
  #
  # ```
  # Process.chroot("/var/empty")
  # ```
  def self.chroot(path : String) : Nil
    Crystal::System::Process.chroot(path)
  end
end

# Executes the given command in a subshell.
# Standard input, output and error are inherited.
# Returns `true` if the command gives zero exit code, `false` otherwise.
# The special `$?` variable is set to a `Process::Status` associated with this execution.
#
# If *command* contains no spaces and *args* is given, it will become
# its argument list.
#
# If *command* contains spaces and *args* is given, *command* must include
# `"${@}"` (including the quotes) to receive the argument list.
#
# No shell interpretation is done in *args*.
#
# Example:
#
# ```
# system("echo *")
# ```
#
# Produces:
#
# ```text
# LICENSE shard.yml Readme.md spec src
# ```
def system(command : String, args : Enumerable(String)? = nil) : Bool
  status = Process.run(command, args, shell: true, input: Process::Redirect::Inherit, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit)
  $? = status
  status.success?
end

# Returns the standard output of executing *command* in a subshell.
# Standard input, and error are inherited.
# The special `$?` variable is set to a `Process::Status` associated with this execution.
#
# It is impossible to call this method with any regular call syntax. There is an associated literal type which calls the method with the literal content as command:
#
# ```
# `echo hi`   # => "hi\n"
# $?.success? # => true
# ```
#
# See [`Command` literals](https://crystal-lang.org/reference/syntax_and_semantics/literals/command.html) in the language reference.
def `(command : String) : String
  process = Process.new(command, shell: true, input: Process::Redirect::Inherit, output: Process::Redirect::Pipe, error: Process::Redirect::Inherit)
  output = process.output.gets_to_end
  status = process.wait
  $? = status
  output
end

require "./process/*"
