Class: Shellout

Inherits:
Object
  • Object
show all
Defined in:
lib/shellout.rb

Overview

Controls execution of commands delegated to the running shell

Constant Summary collapse

DEFAULT_EXECUTE_DISPLAY_OUTPUT =
true
DEFAULT_EXECUTE_RETRY_ATTEMPTS =
0
DEFAULT_EXECUTE_RETRY_DELAY_SECS =
2
BLOCK_SIZE =
1024
ShelloutBaseError =
Class.new(StandardError)
ExecuteCommandFailedError =
Class.new(ShelloutBaseError)
StreamCommandFailedError =
Class.new(ShelloutBaseError)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args, **opts) ⇒ Shellout

Returns a new instance of Shellout.



20
21
22
23
24
# File 'lib/shellout.rb', line 20

def initialize(*args, **opts)
  @args = args.flatten
  @env = opts.delete(:env) || {}
  @opts = opts
end

Instance Attribute Details

#argsObject (readonly)

Returns the value of attribute args.



9
10
11
# File 'lib/shellout.rb', line 9

def args
  @args
end

#envObject (readonly)

Returns the value of attribute env.



9
10
11
# File 'lib/shellout.rb', line 9

def env
  @env
end

#optsObject (readonly)

Returns the value of attribute opts.



9
10
11
# File 'lib/shellout.rb', line 9

def opts
  @opts
end

#stderr_strObject (readonly)

Returns the value of attribute stderr_str.



9
10
11
# File 'lib/shellout.rb', line 9

def stderr_str
  @stderr_str
end

Instance Method Details

#commandObject



26
27
28
# File 'lib/shellout.rb', line 26

def command
  @command ||= args.join(' ')
end

#execute(display_output: true, display_error: true, retry_attempts: DEFAULT_EXECUTE_RETRY_ATTEMPTS, retry_delay_secs: DEFAULT_EXECUTE_RETRY_DELAY_SECS) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/shellout.rb', line 30

def execute(display_output: true, display_error: true, retry_attempts: DEFAULT_EXECUTE_RETRY_ATTEMPTS, retry_delay_secs: DEFAULT_EXECUTE_RETRY_DELAY_SECS)
  retried ||= false
  GDK::Output.debug("command=[#{command}], opts=[#{opts}], display_output=[#{display_output}], retry_attempts=[#{retry_attempts}]")

  duration = Benchmark.realtime do
    display_output ? stream : try_run
  end

  GDK::Output.debug("result: success?=[#{success?}], stdout=[#{read_stdout}], stderr=[#{read_stderr}], duration=[#{duration.round(2)} seconds]")

  raise ExecuteCommandFailedError, command unless success?

  if retried
    retry_success_message = "'#{command}' succeeded after retry."
    GDK::Output.success(retry_success_message)
  end

  self
rescue StreamCommandFailedError, ExecuteCommandFailedError => e
  error_message = "'#{command}' failed."

  if (retry_attempts -= 1).negative?
    GDK::Output.error(error_message, e) if display_error

    self
  else
    retried = true
    error_message += " Retrying in #{retry_delay_secs} secs.."
    GDK::Output.error(error_message, e) if display_error

    sleep(retry_delay_secs)
    retry
  end
end

#exit_codeInteger

Exit code from last run command

Returns:

  • (Integer)

    exit code



135
136
137
138
139
# File 'lib/shellout.rb', line 135

def exit_code
  return nil unless @status

  @status.exitstatus
end

#read_stderrObject



119
120
121
# File 'lib/shellout.rb', line 119

def read_stderr
  clean_string(@stderr_str.to_s.chomp)
end

#read_stdoutObject



115
116
117
# File 'lib/shellout.rb', line 115

def read_stdout
  clean_string(@stdout_str.to_s.chomp)
end

#readlines(limit = -1)) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/shellout.rb', line 84

def readlines(limit = -1)
  @stdout_str = ''
  @stderr_str = ''
  lines = []

  Open3.popen2(env, *args, opts) do |_stdin, stdout, thread|
    stdout.each_line do |line|
      lines << line.chomp if limit == -1 || lines.count < limit
    end

    thread.join
    @status = thread.value
  end

  @stdout_str = lines.join("\n")

  lines
end

#runObject



103
104
105
106
# File 'lib/shellout.rb', line 103

def run
  capture
  read_stdout
end

#stream(extra_options = {}) ⇒ Object

Executes the command while printing the output from both stdout and stderr

This command will stream each individual character from a separate thread making it possible to visualize interactive progress bar.



69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/shellout.rb', line 69

def stream(extra_options = {})
  @stdout_str = ''
  @stderr_str = ''

  # Inspiration: https://nickcharlton.net/posts/ruby-subprocesses-with-stdout-stderr-streams.html
  Open3.popen3(env, *args, opts.merge(extra_options)) do |_stdin, stdout, stderr, thread|
    @status = print_output_from_thread(thread, stdout, stderr)
  end

  read_stdout
rescue Errno::ENOENT => e
  print_err(e.message)
  raise StreamCommandFailedError, e
end

#success?Boolean

Return whether last run command was successful (exit 0)

Returns:

  • (Boolean)

    whether last run command was successful



126
127
128
129
130
# File 'lib/shellout.rb', line 126

def success?
  return false unless @status

  @status.success?
end

#try_runObject



108
109
110
111
112
113
# File 'lib/shellout.rb', line 108

def try_run
  capture(err: '/dev/null')
  read_stdout
rescue Errno::ENOENT
  ''
end