Class: Aidp::ShellExecutor
- Inherits:
-
Object
- Object
- Aidp::ShellExecutor
- Defined in:
- lib/aidp/shell_executor.rb
Overview
Shell command executor wrapper for testability
Commands are always executed in argument form so shell metacharacters in any value are treated as literal data:
run_argv(*command)- Captures output without shell interpretationsystem(*args)- Wraps Kernel.system() with optional output suppression
In tests, set ShellExecutor.suppress_output = true to suppress all
system() output without changing any production code behavior.
Defined Under Namespace
Classes: Result
Class Attribute Summary collapse
-
.suppress_output ⇒ Object
When true, system() calls will have output redirected to /dev/null Default: false (output shown normally).
Instance Method Summary collapse
-
#run_argv(*command, **opts) ⇒ Result
Run a command without shell interpretation and capture its output.
-
#run_line(command, **opts) ⇒ Result
Run a command line without shell interpretation and capture its output.
-
#system(*args, **opts) ⇒ Boolean?
Run a command via system(), optionally suppressing output.
Class Attribute Details
.suppress_output ⇒ Object
When true, system() calls will have output redirected to /dev/null Default: false (output shown normally)
25 26 27 |
# File 'lib/aidp/shell_executor.rb', line 25 def suppress_output @suppress_output end |
Instance Method Details
#run_argv(*command, **opts) ⇒ Result
Run a command without shell interpretation and capture its output
The command and its arguments are passed directly to the operating system, so shell metacharacters in any argument are treated as literal data. Prefer this over #run whenever an argument may contain externally supplied values.
64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/aidp/shell_executor.rb', line 64 def run_argv(*command, **opts) require "open3" program, *args = command.map(&:to_s) raise ArgumentError, "command must not be blank" if program.nil? || program.empty? # The [program, argv0] form forces direct execution even when a command # has no arguments. Passing one string to Open3 can otherwise invoke a # shell when that string contains shell metacharacters. stdout, stderr, status = Open3.capture3([program, program], *args, **opts) Result.new(stdout: stdout, stderr: stderr, exit_status: status.exitstatus) end |
#run_line(command, **opts) ⇒ Result
Run a command line without shell interpretation and capture its output
The line is tokenized with Shellwords, so quoting in the command string is honored as argument boundaries, but the resulting argv is passed directly to the operating system. Shell metacharacters (operators, redirections, substitutions) are never interpreted, which makes this safe for command lines built from untrusted input.
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 |
# File 'lib/aidp/shell_executor.rb', line 89 def run_line(command, **opts) require "shellwords" argv = Shellwords.split(command.to_s) raise ArgumentError, "command must not be blank" if argv.empty? # A leading NAME=value token is a shell environment assignment; without # a shell it would be treated as the program name and fail to run. if argv.first.match?(/\A[A-Za-z_][A-Za-z0-9_]*=/) raise ArgumentError, "environment variable assignments are not supported; " \ "prefix the command with env (e.g. env VAR=value command)" end # Operator and redirection tokens signal an intent to use shell # features, which are intentionally unsupported here; reject them # loudly rather than running them as literal arguments. A leading file # descriptor digit (e.g. `2>`) still introduces a shell redirect. if argv.any? { |token| %w[&& || ; |].include?(token) || token.match?(/\A\d*[<>&]/) } raise ArgumentError, "shell operators are not supported in configured commands; " \ "split into separate commands instead: #{command.inspect}" end # A single token that still contains shell metacharacters would be # handed to Process.spawn as a lone string, which Ruby routes through # sh -c; reject it rather than let the shell interpret it. if argv.length == 1 && argv.first.match?(/[*?\[\]{}()<>|;&$\\"'`\r\n~#=]/) raise ArgumentError, "shell metacharacters in a single-token command are not supported; " \ "split into program and arguments" end run_argv(*argv, **opts) end |
#system(*args, **opts) ⇒ Boolean?
Run a command via system(), optionally suppressing output
When suppress_output is true, output is redirected to /dev/null unless explicit out:/err: options are provided.
Commands are always executed in argument form so a shell never interprets any value. A leading environment hash is forwarded unchanged; otherwise the [program, argv0] form forces direct execution even when a single command string is supplied.
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/aidp/shell_executor.rb', line 137 def system(*args, **opts) if self.class.suppress_output && !opts.key?(:out) && !opts.key?(:err) opts = opts.merge(out: File::NULL, err: File::NULL) end if args.first.is_a?(Hash) environment, *command = args program, *rest = command.map(&:to_s) raise ArgumentError, "command must not be blank" if program.nil? || program.empty? Kernel.system(environment, [program, program], *rest, **opts) else program, *rest = args.map(&:to_s) raise ArgumentError, "command must not be blank" if program.nil? || program.empty? Kernel.system([program, program], *rest, **opts) end end |