piotrmurach/tty-command

View on GitHub
lib/tty/command/cmd.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# frozen_string_literal: true

require "securerandom"
require "shellwords"

module TTY
  class Command
    # Encapsulates the executed command
    #
    # @api private
    class Cmd
      # A string command name, or shell program
      # @api public
      attr_reader :command

      # A string arguments
      # @api public
      attr_reader :argv

      # Hash of operations to peform
      # @api public
      attr_reader :options

      # Unique identifier
      # @api public
      attr_reader :uuid

      # Flag that controls whether to print the output only on error or not
      attr_reader :only_output_on_error

      # Initialize a new Cmd object
      #
      # @api private
      def initialize(env_or_cmd, *args)
        opts = args.last.respond_to?(:to_hash) ? args.pop : {}
        if env_or_cmd.respond_to?(:to_hash)
          @env = env_or_cmd
          unless command = args.shift
            raise ArgumentError, "Cmd requires command argument"
          end
        else
          command = env_or_cmd
        end

        if args.empty? && cmd = command.to_s
          raise ArgumentError, "No command provided" if cmd.empty?

          @command = sanitize(cmd)
          @argv = []
        else
          if command.respond_to?(:to_ary)
            @command = sanitize(command[0])
            args.unshift(*command[1..-1])
          else
            @command = sanitize(command)
          end
          @argv = args.map { |i| Shellwords.escape(i) }
        end
        @env ||= {}
        @options = opts

        @uuid = SecureRandom.uuid.split("-")[0]
        @only_output_on_error = opts.fetch(:only_output_on_error) { false }
        freeze
      end

      # Extend command options if keys don't already exist
      #
      # @api public
      def update(options)
        @options.update(options.merge(@options))
      end

      # The shell environment variables
      #
      # @api public
      def environment
        @env.merge(options.fetch(:env, {}))
      end

      def environment_string
        environment.map do |key, val|
          converted_key = key.is_a?(Symbol) ? key.to_s.upcase : key.to_s
          escaped_val = val.to_s.gsub(/"/, '\"')
          %(#{converted_key}="#{escaped_val}")
        end.join(" ")
      end

      def evars(value, &block)
        return (value || block) unless environment.any?

        "( export #{environment_string} ; #{value || block.call} )"
      end

      def umask(value)
        return value unless options[:umask]

        %(umask #{options[:umask]} && %s) % [value]
      end

      def chdir(value)
        return value unless options[:chdir]

        %(cd #{Shellwords.escape(options[:chdir])} && #{value})
      end

      def user(value)
        return value unless options[:user]

        vars = environment.any? ? "#{environment_string} " : ""
        %(sudo -u #{options[:user]} #{vars}-- sh -c '%s') % [value]
      end

      def group(value)
        return value unless options[:group]

        %(sg #{options[:group]} -c \\\"%s\\\") % [value]
      end

      # Clear environment variables except specified by env
      #
      # @api public
      def with_clean_env
      end

      # Assemble full command
      #
      # @api public
      def to_command
        chdir(umask(evars(user(group(to_s)))))
      end

      # @api public
      def to_s
        [command.to_s, *Array(argv)].join(" ")
      end

      # @api public
      def to_hash
        {
          command: command,
          argv: argv,
          uuid: uuid
        }
      end

      private

      # Coerce to string
      #
      # @api private
      def sanitize(value)
        value.to_s.dup
      end
    end # Cmd
  end # Command
end # TTY