Terrapin

repository·main·Indexed 43 days ago

https://github.com/thoughtbot/terrapin

A Ruby library for safely running shell commands. It prevents command injection attacks by automatically shell-escaping interpolated arguments in command templates. Features include configurable runners (ProcessRunner, PopenRunner, BackticksRunner), custom search paths, environment variable management, and a FakeRunner for testing.

Tokens
3.9K
Snippets
17
Records
20
Agent score
54%

What's inside Terrapin

  1. Securely handle user-supplied values

    main

    To prevent security vulnerabilities, only pass user-generated data into the run method, not the new method.

    Terrapin escapes arguments passed to run (interpolations), but it does not escape the command template passed to new. If you pass user input directly into new, it will be executed as part of the raw shell command.

    Incorrect (Unsafe):

    line = Terrapin::CommandLine.new("echo", "haha`whoami`")
    line.run # Executes whoami

    Correct (Safe):

    line = Terrapin::CommandLine.new("echo", "haha:whoami")
    line.run(whoami: "`whoami`") # Escapes the input safely
  2. Handle command errors and missing executables

    main

    Terrapin raises specific exceptions based on the outcome of the command:

    • Terrapin::ExitStatusError: Raised if the command returns a non-zero exit status. You can prevent this by providing expected_outcodes (an array of acceptable exit codes).
    • Terrapin::CommandNotFoundError: Raised if the command is not found in the system $PATH.

    To customize where Terrapin looks for commands, set Terrapin::CommandLine.path to a string or an array of paths.

    # Handling non-zero exit codes
    line = Terrapin::CommandLine.new("/usr/bin/false", "", expected_outcodes: [0, 1])
    line.run # Does not raise error because 1 is expected
    
    # Handling missing commands
    begin
      Terrapin::CommandLine.new("lolwut").run
    rescue Terrapin::CommandNotFoundError => e
      # Handle error
    end
    
    # Configuring the search path
    Terrapin::CommandLine.path = ["/opt/bin", "/usr/local/bin"]
  3. Configure command runners and handle JRuby/Spawn issues

    main

    Terrapin uses Process.spawn as the default runner. You can switch runners if you encounter specific environment issues:

    • BackticksRunner: Use this if you are using JRuby and encountering Error::ECHILD errors, or if you simply prefer backtick execution.
    • PopenRunner: Use this if you encounter unsupported spawn option: out warnings.

    To change the runner, set Terrapin::CommandLine.runner.

    # Use BackticksRunner (recommended for JRuby issues)
    Terrapin::CommandLine.runner = Terrapin::CommandLine::BackticksRunner.new
    
    # Use PopenRunner (recommended for 'unsupported spawn option: out' warnings)
    Terrapin::CommandLine.runner = Terrapin::CommandLine::PopenRunner.new
  4. Use Terrapin::CommandLine to run shell commands

    main

    Use Terrapin::CommandLine to construct and execute shell commands. You can define a command template and then provide arguments via interpolation. Terrapin automatically shell-escapes interpolated arguments to prevent command injection.

    # Basic usage
    line = Terrapin::CommandLine.new("echo", "hello 'world'")
    line.run # => "hello world\n"
    
    # Using interpolated arguments
    line = Terrapin::CommandLine.new("convert", ":in -scale :resolution :out")
    line.run(in: "omg.jpg", resolution: "32x32", out: "omg_thumb.jpg")
  5. Suppress stderr output

    main

    You can ignore stderr by passing swallow_stderr: true to the command initialization. Terrapin will redirect stderr to the appropriate null device (/dev/null on Unix, NUL on Windows).

    line = Terrapin::CommandLine.new("noisy", "--extra-verbose", swallow_stderr: true)
    line.command # => "noisy --extra-verbose 2>/dev/null" (on Unix)
  6. Configure command logging

    main

    You can log the commands being executed by providing a logger to an individual instance or setting a global Terrapin::CommandLine.logger.

    # Log a specific command instance
    line = Terrapin::CommandLine.new("echo", ":var", logger: Logger.new(STDOUT))
    line.run(var: "LOL!")
    
    # Log all commands globally
    Terrapin::CommandLine.logger = Logger.new(STDOUT)
    Terrapin::CommandLine.new("date").run
  7. Initialize and run a Terrapin::CommandLine

    main

    To run a shell command, instantiate Terrapin::CommandLine with the binary name, a parameter string (which can include interpolation placeholders), and an options hash.

    Interpolation uses the syntax :key or :{key}. Terrapin automatically shell-quotes the interpolated values to prevent injection.

    Calling .run(interpolations) executes the command. If the command's exit status is not included in the @expected_outcodes array (which defaults to [0]), a Terrapin::ExitStatusError is raised. If the binary is not found, a Terrapin::CommandNotFoundError is raised.

    # Example: Running a command with interpolation
    cmd = Terrapin::CommandLine.new("convert", ":{input} -scale :scale :output", {
      expected_outcodes: [0, 1],
      swallow_stderr: true
    })
    
    output = cmd.run(input: "omg.jpg", scale: "32x32", output: "omg_thumb.jpg")
    puts output
  8. Configure global Terrapin::CommandLine settings

    main

    You can configure global settings for all Terrapin::CommandLine instances via the class methods. These settings are used as defaults if not provided in the instance options.

    • Terrapin::CommandLine.path=: Sets a supplemental path that will be prepended to the system PATH when executing commands. Accepts a single string or an array of strings.
    • Terrapin::CommandLine.environment=: Sets a global environment hash that is merged with instance-specific environments.
    • Terrapin::CommandLine.runner=: Sets the default runner used for execution.
    • Terrapin::CommandLine.runner_options=: Sets default options for the runner.
    • Terrapin::CommandLine.logger=: Sets a global logger.
    Terrapin::CommandLine.path = ["/usr/local/bin", "/opt/bin"]
    Terrapin::CommandLine.environment = { "MY_VAR" => "value" }
  9. Check if ProcessRunner is supported

    main

    The Terrapin::CommandLine::ProcessRunner uses Process.spawn to execute commands. You can check if this runner is available and supported in your current environment using available? and supported?.

    • available?: Returns true if Process.spawn is available on the current Ruby implementation.
    • supported?: Returns true if the runner is available and the environment is not JRuby. Note that in JRuby, waiting on a finished PID can raise errors, so this runner is explicitly marked as unsupported there.
    Terrapin::CommandLine::ProcessRunner.supported? # returns true or false
  10. Use PopenRunner for command execution

    main

    The Terrapin::CommandLine::PopenRunner is a runner implementation that uses IO.popen to execute shell commands. It is always supported. When calling a command, you can provide an environment hash and an options hash (which are passed directly to IO.popen). The runner uses ClimateControl to manage environment variable modifications during the command execution.

    runner = Terrapin::CommandLine::PopenRunner.new
    output = runner.call('ls -la', { 'MY_VAR' => 'value' }, { err: :out })
  11. Use FakeRunner for testing

    main

    To test code that uses Terrapin without actually executing shell commands, you can use Terrapin::CommandLine.fake!. This replaces the global runner with a FakeRunner that does not touch the system shell.

    To restore the original runner behavior, call Terrapin::CommandLine.unfake!.

    Terrapin::CommandLine.fake!
    # ... run tests that call Terrapin::CommandLine ...
    Terrapin::CommandLine.unfake!
  12. Initialize Terrapin::CommandLine::Output with custom streams

    main

    The Terrapin::CommandLine::Output class is used to encapsulate the standard output (stdout) and standard error (stderr) streams of a command. When initializing a command, you can provide specific IO objects (such as StringIO or file handles) to capture or redirect the command's output and error streams.

    # Example of manually creating an Output object with custom streams
    require 'stringio'
    
    stdout = StringIO.new
    stderr = StringIO.new
    output_wrapper = Terrapin::CommandLine::Output.new(stdout, stderr)
    
    # Access the streams via readers
    puts output_wrapper.output
    puts output_wrapper.error_output