File: sheller.rb

package info (click to toggle)
ruby-shellany 0.0.1-5
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 152 kB
  • sloc: ruby: 218; sh: 4; makefile: 3
file content (144 lines) | stat: -rw-r--r-- 3,712 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
require "open3"

module Shellany
  # The Guard sheller abstract the actual subshell
  # calls and allow easier stubbing.
  #
  class Sheller
    attr_reader :status

    # Creates a new Guard::Sheller object.
    #
    # @param [String] args a command to run in a subshell
    # @param [Array<String>] args an array of command parts to run in a subshell
    # @param [*String] args a list of command parts to run in a subshell
    #
    def initialize(*args)
      fail ArgumentError, "no command given" if args.empty?
      @command = args
      @ran = false
    end

    # Shortcut for new(command).run
    #
    def self.run(*args)
      new(*args).run
    end

    # Shortcut for new(command).run.stdout
    #
    def self.stdout(*args)
      new(*args).stdout
    end

    # Shortcut for new(command).run.stderr
    #
    def self.stderr(*args)
      new(*args).stderr
    end

    # Runs the command.
    #
    # @return [Boolean] whether or not the command succeeded.
    #
    def run
      unless ran?
        status, output, errors = self.class._system_with_capture(*@command)
        @ran = true
        @stdout = output
        @stderr = errors
        @status = status
      end

      ok?
    end

    # Returns true if the command has already been run, false otherwise.
    #
    # @return [Boolean] whether or not the command has already been run
    #
    def ran?
      @ran
    end

    # Returns true if the command succeeded, false otherwise.
    #
    # @return [Boolean] whether or not the command succeeded
    #
    def ok?
      run unless ran?

      @status && @status.success?
    end

    # Returns the command's output.
    #
    # @return [String] the command output
    #
    def stdout
      run unless ran?

      @stdout
    end

    # Returns the command's error output.
    #
    # @return [String] the command output
    #
    def stderr
      run unless ran?

      @stderr
    end

    # No output capturing
    #
    # NOTE: `$stdout.puts system('cls')` on Windows won't work like
    # it does for on systems with ansi terminals, so we need to be
    # able to call Kernel.system directly.
    def self.system(*args)
      _system_with_no_capture(*args)
    end

    def self._system_with_no_capture(*args)
      Kernel.system(*args)
      result = $?
      errors = (result == 0) || "Guard failed to run: #{args.inspect}"
      [result, nil, errors]
    end

    def self._system_with_capture(*args)
      # We use popen3, because it started working on recent versions
      # of JRuby, while JRuby doesn't handle options to Kernel.system
      args = _shellize_if_needed(args)

      stdout, stderr, status = nil
      Open3.popen3(*args) do |_stdin, _stdout, _stderr, _thr|
        stdout = _stdout.read
        stderr = _stderr.read
        status = _thr.value
      end

      [status, stdout, stderr]
    rescue Errno::ENOENT, IOError => e
      [nil, nil, "Guard::Sheller failed (#{e.inspect})"]
    end

    # Only needed on JRUBY, because MRI properly detects ';' and metachars
    def self._shellize_if_needed(args)
      return args unless RUBY_PLATFORM == "java"
      return args unless args.size == 1
      return args unless /[;<>]/ =~ args.first

      # NOTE: Sheller was originally meant for Guard (which basically only uses
      # UNIX commands anyway) and JRuby doesn't support options to
      # Kernel.system (and doesn't automatically shell when there's a
      # metacharacter in the command).
      #
      # So ... I'm assuming /bin/sh exists - if not, PRs are welcome,
      # because I have no clue what to do if /bin/sh doesn't exist.
      # (use ENV["RUBYSHELL"] ? Detect cmd.exe ?)
      ["/bin/sh", "-c", args.first]
    end
  end
end