Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/thoughtbot/terrapin
Run shell commands safely, even with user-supplied values
https://github.com/thoughtbot/terrapin
Last synced: 2 days ago
JSON representation
Run shell commands safely, even with user-supplied values
- Host: GitHub
- URL: https://github.com/thoughtbot/terrapin
- Owner: thoughtbot
- License: other
- Created: 2018-01-12T15:50:55.000Z (almost 7 years ago)
- Default Branch: main
- Last Pushed: 2024-12-17T21:57:48.000Z (25 days ago)
- Last Synced: 2025-01-04T07:44:16.668Z (7 days ago)
- Language: Ruby
- Size: 212 KB
- Stars: 251
- Watchers: 8
- Forks: 19
- Open Issues: 7
-
Metadata Files:
- Readme: README.md
- Changelog: NEWS.md
- License: LICENSE
- Codeowners: CODEOWNERS
- Security: SECURITY.md
Awesome Lists containing this project
- awesome-ruby - Terrapin - A small command line library (Formerly Cocaine). (CLI Builder)
README
# Terrapin [![Build Status](https://secure.travis-ci.org/thoughtbot/terrapin.png?branch=master)](http://travis-ci.org/thoughtbot/terrapin)
Run shell commands safely, even with user-supplied values
[API reference](http://rubydoc.info/gems/terrapin/)
## Usage
The basic, normal stuff:
```ruby
line = Terrapin::CommandLine.new("echo", "hello 'world'")
line.command # => "echo hello 'world'"
line.run # => "hello world\n"
```Interpolated arguments:
```ruby
line = Terrapin::CommandLine.new("convert", ":in -scale :resolution :out")
line.command(in: "omg.jpg",
resolution: "32x32",
out: "omg_thumb.jpg")
# => "convert 'omg.jpg' -scale '32x32' 'omg_thumb.jpg'"
```It prevents attempts at being bad:
```ruby
line = Terrapin::CommandLine.new("cat", ":file")
line.command(file: "haha`rm -rf /`.txt") # => "cat 'haha`rm -rf /`.txt'"line = Terrapin::CommandLine.new("cat", ":file")
line.command(file: "ohyeah?'`rm -rf /`.ha!") # => "cat 'ohyeah?'\\''`rm -rf /`.ha!'"
```NOTE: It only does that for arguments interpolated via `run`, NOT arguments
passed into `new` (see 'Security' below):```ruby
line = Terrapin::CommandLine.new("echo", "haha`whoami`")
line.command # => "echo haha`whoami`"
line.run # => "hahawebserver\n"
```This is the right way:
```ruby
line = Terrapin::CommandLine.new("echo", "haha:whoami")
line.command(whoami: "`whoami`") # => "echo haha'`whoami`'"
line.run(whoami: "`whoami`") # => "haha`whoami`\n"
```You can ignore the result:
```ruby
line = Terrapin::CommandLine.new("noisy", "--extra-verbose", swallow_stderr: true)
line.command # => "noisy --extra-verbose 2>/dev/null"# ... and on Windows...
line.command # => "noisy --extra-verbose 2>NUL"
```If your command errors, you get an exception:
```ruby
line = Terrapin::CommandLine.new("git", "commit")
begin
line.run
rescue Terrapin::ExitStatusError => e
e.message # => "Command 'git commit' returned 1. Expected 0"
end
```If your command might return something non-zero, and you expect that, it's cool:
```ruby
line = Terrapin::CommandLine.new("/usr/bin/false", "", expected_outcodes: [0, 1])
begin
line.run
rescue Terrapin::ExitStatusError => e
# => You never get here!
end
```You don't have the command? You get an exception:
```ruby
line = Terrapin::CommandLine.new("lolwut")
begin
line.run
rescue Terrapin::CommandNotFoundError => e
e # => the command isn't in the $PATH for this process.
end
```But don't fear, you can specify where to look for the command:
```ruby
Terrapin::CommandLine.path = "/opt/bin"
line = Terrapin::CommandLine.new("lolwut")
line.command # => "lolwut", but it looks in /opt/bin for it.
```You can even give it a bunch of places to look:
```ruby
FileUtils.rm("/opt/bin/lolwut")
File.open('/usr/local/bin/lolwut') { |f| f.write('echo Hello') }
Terrapin::CommandLine.path = ["/opt/bin", "/usr/local/bin"]
line = Terrapin::CommandLine.new("lolwut")
line.run # => prints 'Hello', because it searches the path
```Or just put it in the command:
```ruby
line = Terrapin::CommandLine.new("/opt/bin/lolwut")
line.command # => "/opt/bin/lolwut"
```You can see what's getting run. The 'Command' part it logs is in green for
visibility! (where applicable)```ruby
line = Terrapin::CommandLine.new("echo", ":var", logger: Logger.new(STDOUT))
line.run(var: "LOL!") # => Logs this with #info -> Command :: echo 'LOL!'
```Or log every command:
```ruby
Terrapin::CommandLine.logger = Logger.new(STDOUT)
Terrapin::CommandLine.new("date").run # => Logs this -> Command :: date
```## Security
Short version: Only pass user-generated data into the `run` method and NOT
`new`.As shown in examples above, Terrapin will only shell-escape what is passed in as
interpolations to the `run` method. It WILL NOT escape what is passed in to the
second argument of `new`. Terrapin assumes that you will not be manually
passing user-generated data to that argument and will be using it as a template
for your command line's structure.## Runners
Terrapin will choose from among a couple different ways of running commands.
The simplest is `Process.spawn`, which is also the default. Terrapin can also just use [backticks], so if for some reason you'd prefer that, you can ask Terrapin to use that:```ruby
Terrapin::CommandLine.runner = Terrapin::CommandLine::BackticksRunner.new
```And if you really want to, you can define your own Runner, though I can't imagine why you would.
[backticks]: https://ruby-doc.org/3.2.1/Kernel.html#method-i-60
```ruby
Terrapin::CommandLine.runner = Terrapin::CommandLine::BackticksRunner.new
```And if you really want to, you can define your own Runner, though I can't
imagine why you would.### JRuby issues
#### Caveat
If you get `Error::ECHILD` errors and are using JRuby, there is a very good
chance that the error is actually in JRuby. This was brought to our attention
in https://github.com/thoughtbot/terrapin/issues/24 and probably fixed in
http://jira.codehaus.org/browse/JRUBY-6162. You *will* want to use the
`BackticksRunner` if you are unable to update JRuby.#### Spawn warning
If you get `unsupported spawn option: out` warning (like in [issue
38](https://github.com/thoughtbot/terrapin/issues/38)), try to use
`PopenRunner`:```ruby
Terrapin::CommandLine.runner = Terrapin::CommandLine::PopenRunner.new
```## Thread Safety
Terrapin should be thread safe. As discussed [here, in this climate_control
thread](https://github.com/thoughtbot/climate_control/pull/11), climate_control,
which modifies the environment under which commands are run for the
BackticksRunner and PopenRunner, is thread-safe but not reentrant. Please let us
know if you find this is ever not the case.## Feedback
*Security* concerns must be privately emailed to
[[email protected]]([email protected]).Question? Idea? Problem? Bug? Comment? Concern? Like using question marks?
[GitHub Issues For All!](https://github.com/thoughtbot/terrapin/issues)
## Credits
Thank you to all [the
contributors](https://github.com/thoughtbot/terrapin/graphs/contributors)!## License
Copyright © 2011 Jon Yurek and thoughtbot, inc. This is free software, and
may be redistributed under the terms specified in the
[LICENSE](https://github.com/thoughtbot/terrapin/blob/master/LICENSE)
file.## About thoughtbot
![thoughtbot](https://thoughtbot.com/thoughtbot-logo-for-readmes.svg)
This repo is maintained and funded by thoughtbot, inc.
The names and logos for thoughtbot are trademarks of thoughtbot, inc.We love open source software!
See [our other projects][community].
We are [available for hire][hire].[community]: https://thoughtbot.com/community?utm_source=github
[hire]: https://thoughtbot.com/hire-us?utm_source=github