terrapin-0.6.0/0000755000175000017500000000000013503711060014073 5ustar utkarsh2102utkarsh2102terrapin-0.6.0/spec/0000755000175000017500000000000013503711060015025 5ustar utkarsh2102utkarsh2102terrapin-0.6.0/spec/spec_helper.rb0000644000175000017500000000134413503711060017645 0ustar utkarsh2102utkarsh2102require 'rspec' require 'mocha/api' require 'bourne' require 'terrapin' require 'timeout' require 'tempfile' require 'pry' require 'thread' require 'pathname' begin; require 'active_support/logger'; rescue LoadError; end begin; require 'active_support/buffered_logger'; rescue LoadError; end Dir[File.dirname(__FILE__) + "/support/**.rb"].each{|support_file| require support_file } RSpec.configure do |config| config.mock_with :mocha config.include WithExitstatus config.include StubOS config.include UnsettingExitstatus end def best_logger if ActiveSupport.const_defined?("Logger") ActiveSupport::Logger elsif ActiveSupport.const_defined?("BufferedLogger") ActiveSupport::BufferedLogger else Logger end end terrapin-0.6.0/spec/support/0000755000175000017500000000000013503711060016541 5ustar utkarsh2102utkarsh2102terrapin-0.6.0/spec/support/stub_os.rb0000644000175000017500000000111513503711060020542 0ustar utkarsh2102utkarsh2102module StubOS def on_windows! stub_os('mswin') Terrapin::OS.stubs(:path_separator).returns(";") end def on_unix! stub_os('darwin11.0.0') Terrapin::OS.stubs(:path_separator).returns(":") end def on_mingw! stub_os('mingw') Terrapin::OS.stubs(:path_separator).returns(";") end def on_java! Terrapin::OS.stubs(:arch).returns("universal-java1.7") end def stub_os(host_string) # http://blog.emptyway.com/2009/11/03/proper-way-to-detect-windows-platform-in-ruby/ RbConfig::CONFIG.stubs(:[]).with('host_os').returns(host_string) end end terrapin-0.6.0/spec/support/fake_logger.rb0000644000175000017500000000031713503711060021334 0ustar utkarsh2102utkarsh2102class FakeLogger def initialize(options = {}) @tty = options[:tty] @entries = [] end def info(text) @entries << text end def entries @entries end def tty? @tty end end terrapin-0.6.0/spec/support/with_exitstatus.rb0000644000175000017500000000041313503711060022334 0ustar utkarsh2102utkarsh2102module WithExitstatus def with_exitstatus_returning(code) saved_exitstatus = $?.respond_to?(:exitstatus) ? $?.exitstatus : 0 begin `ruby -e "exit #{code.to_i}"` yield ensure `ruby -e "exit #{saved_exitstatus.to_i}"` end end end terrapin-0.6.0/spec/support/unsetting_exitstatus.rb0000644000175000017500000000020613503711060023401 0ustar utkarsh2102utkarsh2102module UnsettingExitstatus def assuming_no_processes_have_been_run class << $? undef_method :exitstatus end end end terrapin-0.6.0/spec/support/nonblocking_examples.rb0000644000175000017500000000065313503711060023273 0ustar utkarsh2102utkarsh2102shared_examples_for "a command that does not block" do it 'does not block if the command outputs a lot of data' do garbage_file = Tempfile.new("garbage") 10.times{ garbage_file.write("A" * 1024 * 1024) } Timeout.timeout(5) do output = subject.call("cat '#{garbage_file.path}'") $?.exitstatus.should == 0 output.output.length.should == 10 * 1024 * 1024 end garbage_file.close end end terrapin-0.6.0/spec/support/have_output.rb0000644000175000017500000000035113503711060021430 0ustar utkarsh2102utkarsh2102RSpec::Matchers.define :have_output do |expected| match do |actual| actual.output == expected end end RSpec::Matchers.define :have_error_output do |expected| match do |actual| actual.error_output == expected end end terrapin-0.6.0/spec/terrapin/0000755000175000017500000000000013503711060016651 5ustar utkarsh2102utkarsh2102terrapin-0.6.0/spec/terrapin/errors_spec.rb0000644000175000017500000000373113503711060021530 0ustar utkarsh2102utkarsh2102require 'spec_helper' describe "When an error happens" do it "raises a CommandLineError if the result code command isn't expected" do cmd = Terrapin::CommandLine.new("echo", "hello") cmd.stubs(:execute) with_exitstatus_returning(1) do lambda { cmd.run }.should raise_error(Terrapin::CommandLineError) end end it "does not raise if the result code is expected, even if nonzero" do cmd = Terrapin::CommandLine.new("echo", "hello", expected_outcodes: [0, 1]) cmd.stubs(:execute) with_exitstatus_returning(1) do lambda { cmd.run }.should_not raise_error end end it "adds command output to exception message if the result code is nonzero" do cmd = Terrapin::CommandLine.new("echo", "hello") error_output = "Error 315" cmd. stubs(:execute). returns(Terrapin::CommandLine::Output.new("", error_output)) with_exitstatus_returning(1) do begin cmd.run rescue Terrapin::ExitStatusError => e e.message.should =~ /STDERR:\s+#{error_output}/ end end end it 'passes the error message to the exception when command is not found' do cmd = Terrapin::CommandLine.new('test', '') cmd.stubs(:execute).raises(Errno::ENOENT.new("not found")) begin cmd.run rescue Terrapin::CommandNotFoundError => e expect(e.message).to eq 'No such file or directory - not found' end end it "should keep result code in #exitstatus" do cmd = Terrapin::CommandLine.new("convert") cmd.stubs(:execute).with("convert").returns(:correct_value) with_exitstatus_returning(1) do cmd.run rescue nil end cmd.exit_status.should == 1 end it "does not blow up if running the command errored before execution" do assuming_no_processes_have_been_run command = Terrapin::CommandLine.new("echo", ":hello_world") command.stubs(:command).raises("An Error") lambda{ command.run }.should raise_error("An Error") command.exit_status.should eq 0 end end terrapin-0.6.0/spec/terrapin/command_line/0000755000175000017500000000000013503711060021276 5ustar utkarsh2102utkarsh2102terrapin-0.6.0/spec/terrapin/command_line/runners/0000755000175000017500000000000013503711060022772 5ustar utkarsh2102utkarsh2102terrapin-0.6.0/spec/terrapin/command_line/runners/popen_runner_spec.rb0000644000175000017500000000133613503711060027046 0ustar utkarsh2102utkarsh2102require 'spec_helper' describe Terrapin::CommandLine::PopenRunner do if Terrapin::CommandLine::PopenRunner.supported? it_behaves_like 'a command that does not block' it 'runs the command given and captures the output in an Output' do output = subject.call("echo hello") expect(output).to have_output "hello\n" end it 'modifies the environment and runs the command given' do output = subject.call("echo $yes", {"yes" => "no"}) expect(output).to have_output "no\n" end it 'sets the exitstatus when a command completes' do subject.call("ruby -e 'exit 0'") $?.exitstatus.should == 0 subject.call("ruby -e 'exit 5'") $?.exitstatus.should == 5 end end end terrapin-0.6.0/spec/terrapin/command_line/runners/fake_runner_spec.rb0000644000175000017500000000133513503711060026632 0ustar utkarsh2102utkarsh2102require 'spec_helper' describe Terrapin::CommandLine::FakeRunner do it 'records commands' do subject.call("some command", :environment) subject.call("other command", :other_environment) subject.commands.should eq [["some command", :environment], ["other command", :other_environment]] end it 'can tell if a command was run' do subject.call("some command", :environment) subject.call("other command", :other_environment) subject.ran?("some command").should eq true subject.ran?("no command").should eq false end it 'can tell if a command was run even if shell options were set' do subject.call("something 2>/dev/null", :environment) subject.ran?("something").should eq true end end terrapin-0.6.0/spec/terrapin/command_line/runners/posix_runner_spec.rb0000644000175000017500000000235513503711060027071 0ustar utkarsh2102utkarsh2102require 'spec_helper' describe Terrapin::CommandLine::PosixRunner do if Terrapin::CommandLine::PosixRunner.supported? it_behaves_like 'a command that does not block' it 'runs the command given and captures the output' do output = subject.call("echo hello") expect(output).to have_output "hello\n" end it 'runs the command given and captures the error output' do output = subject.call("echo hello 1>&2") expect(output).to have_error_output "hello\n" end it 'modifies the environment and runs the command given' do output = subject.call("echo $yes", {"yes" => "no"}) expect(output).to have_output "no\n" end it 'sets the exitstatus when a command completes' do subject.call("ruby -e 'exit 0'") $?.exitstatus.should == 0 subject.call("ruby -e 'exit 5'") $?.exitstatus.should == 5 end it "runs the command it's given and allows access to stderr afterwards" do cmd = Terrapin::CommandLine.new( "ruby", "-e '$stdout.puts %{hello}; $stderr.puts %{goodbye}'", :swallow_stderr => false ) cmd.run expect(cmd.command_output).to eq "hello\n" expect(cmd.command_error_output).to eq "goodbye\n" end end end terrapin-0.6.0/spec/terrapin/command_line/runners/process_runner_spec.rb0000644000175000017500000000236113503711060027402 0ustar utkarsh2102utkarsh2102require 'spec_helper' describe Terrapin::CommandLine::ProcessRunner do if Terrapin::CommandLine::ProcessRunner.supported? it_behaves_like "a command that does not block" it 'runs the command given and captures the output' do output = subject.call("echo hello") expect(output).to have_output "hello\n" end it 'runs the command given and captures the error output' do output = subject.call("echo hello 1>&2") expect(output).to have_error_output "hello\n" end it 'modifies the environment and runs the command given' do output = subject.call("echo $yes", {"yes" => "no"}) expect(output).to have_output "no\n" end it 'sets the exitstatus when a command completes' do subject.call("ruby -e 'exit 0'") $?.exitstatus.should == 0 subject.call("ruby -e 'exit 5'") $?.exitstatus.should == 5 end it "runs the command it's given and allows access to stderr afterwards" do cmd = Terrapin::CommandLine.new( "ruby", "-e '$stdout.puts %{hello}; $stderr.puts %{goodbye}'", :swallow_stderr => false ) cmd.run expect(cmd.command_output).to eq "hello\n" expect(cmd.command_error_output).to eq "goodbye\n" end end end terrapin-0.6.0/spec/terrapin/command_line/runners/backticks_runner_spec.rb0000644000175000017500000000134613503711060027664 0ustar utkarsh2102utkarsh2102require 'spec_helper' describe Terrapin::CommandLine::BackticksRunner do if Terrapin::CommandLine::BackticksRunner.supported? it_behaves_like 'a command that does not block' it 'runs the command given and captures the output in an Output' do output = subject.call("echo hello") expect(output).to have_output "hello\n" end it 'modifies the environment and runs the command given' do output = subject.call("echo $yes", {"yes" => "no"}) expect(output).to have_output "no\n" end it 'sets the exitstatus when a command completes' do subject.call("ruby -e 'exit 0'") $?.exitstatus.should == 0 subject.call("ruby -e 'exit 5'") $?.exitstatus.should == 5 end end end terrapin-0.6.0/spec/terrapin/command_line/output_spec.rb0000644000175000017500000000062613503711060024201 0ustar utkarsh2102utkarsh2102require 'spec_helper' describe Terrapin::CommandLine::Output do it 'holds an input and error stream' do output = Terrapin::CommandLine::Output.new(:a, :b) expect(output.output).to eq :a expect(output.error_output).to eq :b end it 'calls #to_s on the output when you call #to_s on it' do output = Terrapin::CommandLine::Output.new(:a, :b) expect(output.to_s).to eq 'a' end end terrapin-0.6.0/spec/terrapin/runners_spec.rb0000644000175000017500000000650313503711060021710 0ustar utkarsh2102utkarsh2102require 'spec_helper' describe "When picking a Runner" do it "uses the BackticksRunner by default" do Terrapin::CommandLine::ProcessRunner.stubs(:supported?).returns(false) Terrapin::CommandLine::PosixRunner.stubs(:supported?).returns(false) cmd = Terrapin::CommandLine.new("echo", "hello") cmd.runner.class.should == Terrapin::CommandLine::BackticksRunner end it "uses the ProcessRunner on 1.9 and it's available" do Terrapin::CommandLine::ProcessRunner.stubs(:supported?).returns(true) Terrapin::CommandLine::PosixRunner.stubs(:supported?).returns(false) cmd = Terrapin::CommandLine.new("echo", "hello") cmd.runner.class.should == Terrapin::CommandLine::ProcessRunner end it "uses the PosixRunner if the PosixRunner is available" do Terrapin::CommandLine::PosixRunner.stubs(:supported?).returns(true) cmd = Terrapin::CommandLine.new("echo", "hello") cmd.runner.class.should == Terrapin::CommandLine::PosixRunner end it "uses the BackticksRunner if the PosixRunner is available, but we told it to use Backticks all the time" do Terrapin::CommandLine::PosixRunner.stubs(:supported?).returns(true) Terrapin::CommandLine.runner = Terrapin::CommandLine::BackticksRunner.new cmd = Terrapin::CommandLine.new("echo", "hello") cmd.runner.class.should == Terrapin::CommandLine::BackticksRunner end it "uses the BackticksRunner if the PosixRunner is available, but we told it to use Backticks" do Terrapin::CommandLine::PosixRunner.stubs(:supported?).returns(true) cmd = Terrapin::CommandLine.new("echo", "hello", :runner => Terrapin::CommandLine::BackticksRunner.new) cmd.runner.class.should == Terrapin::CommandLine::BackticksRunner end it "can go into 'Fake' mode" do Terrapin::CommandLine.fake! cmd = Terrapin::CommandLine.new("echo", "hello") cmd.runner.class.should eq Terrapin::CommandLine::FakeRunner end it "can turn off Fake mode" do Terrapin::CommandLine.fake! Terrapin::CommandLine.unfake! cmd = Terrapin::CommandLine.new("echo", "hello") cmd.runner.class.should_not eq Terrapin::CommandLine::FakeRunner end it "can use a FakeRunner even if not in Fake mode" do Terrapin::CommandLine.unfake! cmd = Terrapin::CommandLine.new("echo", "hello", :runner => Terrapin::CommandLine::FakeRunner.new) cmd.runner.class.should eq Terrapin::CommandLine::FakeRunner end end describe 'When running an executable in the supplemental path' do before do path = Pathname.new(File.dirname(__FILE__)) + '..' + 'support' File.open(path + 'ls', 'w'){|f| f.puts "#!/bin/sh\necho overridden-ls\n" } FileUtils.chmod(0755, path + 'ls') Terrapin::CommandLine.path = path end after do FileUtils.rm_f("#{Terrapin::CommandLine.path}/ls") end [ Terrapin::CommandLine::BackticksRunner, Terrapin::CommandLine::PopenRunner, Terrapin::CommandLine::PosixRunner, Terrapin::CommandLine::ProcessRunner ].each do |runner_class| if runner_class.supported? describe runner_class do describe '#run' do it 'finds the correct executable' do Terrapin::CommandLine.runner = runner_class.new command = Terrapin::CommandLine.new('ls') result = command.run expect(result.strip).to eq('overridden-ls') end end end end end end terrapin-0.6.0/spec/terrapin/command_line_spec.rb0000644000175000017500000001723113503711060022641 0ustar utkarsh2102utkarsh2102require 'spec_helper' describe Terrapin::CommandLine do before do Terrapin::CommandLine.path = nil on_unix! # Assume we're on unix unless otherwise specified. end it "takes a command and parameters and produces a Bash command line" do cmd = Terrapin::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => false) cmd.command.should == "convert a.jpg b.png" end it "specifies the $PATH where the command can be found on unix" do Terrapin::CommandLine.path = ["/path/to/command/dir", "/"] cmd = Terrapin::CommandLine.new("ls") cmd.command.should == "PATH=/path/to/command/dir:/:$PATH; ls" end it "specifies the %PATH% where the command can be found on windows" do on_windows! Terrapin::CommandLine.path = ['C:\system32', 'D:\\'] cmd = Terrapin::CommandLine.new("dir") cmd.command.should == 'SET PATH=C:\system32;D:\;%PATH% & dir' end it "specifies more than one path where the command can be found" do Terrapin::CommandLine.path = ["/path/to/command/dir", "/some/other/path"] cmd = Terrapin::CommandLine.new("ruby", "-e 'puts ENV[%{PATH}]'") output = cmd.run output.should match(%r{/path/to/command/dir}) output.should match(%r{/some/other/path}) end it "temporarily changes specified environment variables" do Terrapin::CommandLine.environment['TEST'] = 'Hello, world!' cmd = Terrapin::CommandLine.new("ruby", "-e 'puts ENV[%{TEST}]'") output = cmd.run output.should match(%r{Hello, world!}) end it 'changes environment variables for the command line' do Terrapin::CommandLine.environment['TEST'] = 'Hello, world!' cmd = Terrapin::CommandLine.new("ruby", "-e 'puts ENV[%{TEST}]'", :environment => {'TEST' => 'Hej hej'}) output = cmd.run output.should match(%r{Hej hej}) end it 'passes the existing environment variables through to the runner' do command = Terrapin::CommandLine.new('echo', '$HOME') output = command.run output.chomp.should_not == '' end it "can interpolate quoted variables into the command line's parameters" do cmd = Terrapin::CommandLine.new("convert", ":one :{two}", :swallow_stderr => false) command_string = cmd.command(:one => "a.jpg", :two => "b.png") command_string.should == "convert 'a.jpg' 'b.png'" end it 'does not over-interpolate in a command line' do cmd = Terrapin::CommandLine.new("convert", ":hell :{two} :hello", :swallow_stderr => false) command_string = cmd.command(:hell => "a.jpg", :two => "b.png", :hello => "c.tiff") command_string.should == "convert 'a.jpg' 'b.png' 'c.tiff'" end it "interpolates when running a command" do command = Terrapin::CommandLine.new("echo", ":hello_world") command.run(:hello_world => "Hello, world").should match(/Hello, world/) end it "interpolates any Array arguments when running a command" do command = Terrapin::CommandLine.new("echo", "Hello :worlds and :dwarfs") command.command(:worlds => %w[mercury venus earth], :dwarfs => "pluto").should == "echo Hello 'mercury' 'venus' 'earth' and 'pluto'" end it "quotes command line options differently if we're on windows" do on_windows! cmd = Terrapin::CommandLine.new("convert", ":one :{two}", :swallow_stderr => false) command_string = cmd.command(:one => "a.jpg", :two => "b.png") command_string.should == 'convert "a.jpg" "b.png"' end it "can quote and interpolate dangerous variables" do cmd = Terrapin::CommandLine.new("convert", ":one :two", :swallow_stderr => false) command_string = cmd.command(:one => "`rm -rf`.jpg", :two => "ha'ha.png'") command_string.should == "convert '`rm -rf`.jpg' 'ha'\\''ha.png'\\'''" end it 'cannot recursively introduce a place where user-supplied commands can run' do cmd = Terrapin::CommandLine.new('convert', ':foo :bar') cmd.command(:foo => ':bar', :bar => '`rm -rf`').should == 'convert \':bar\' \'`rm -rf`\'' end it "can quote and interpolate dangerous variables even on windows" do on_windows! cmd = Terrapin::CommandLine.new("convert", ":one :two", :swallow_stderr => false) command_string = cmd.command(:one => "`rm -rf`.jpg", :two => "ha'ha.png") command_string.should == %{convert "`rm -rf`.jpg" "ha'ha.png"} end it "quotes blank values into the command line's parameters" do cmd = Terrapin::CommandLine.new("curl", "-X POST -d :data :url", :swallow_stderr => false) command_string = cmd.command(:data => "", :url => "http://localhost:9000") command_string.should == "curl -X POST -d '' 'http://localhost:9000'" end it "allows colons in parameters" do cmd = Terrapin::CommandLine.new("convert", "'a.jpg' xc:black 'b.jpg'", :swallow_stderr => false) cmd.command.should == "convert 'a.jpg' xc:black 'b.jpg'" end it 'handles symbols in user supplied values' do cmd = Terrapin::CommandLine.new("echo", ":foo") command_string = cmd.command(:foo => :bar) command_string.should == "echo 'bar'" end it "can redirect stderr to the bit bucket if requested" do cmd = Terrapin::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => true) cmd.command.should == "convert a.jpg b.png 2>/dev/null" end it "can redirect stderr to the bit bucket on windows" do on_windows! cmd = Terrapin::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => true) cmd.command.should == "convert a.jpg b.png 2>NUL" end it "runs the command it's given and returns the output" do cmd = Terrapin::CommandLine.new("echo", "hello", :swallow_stderr => false) expect(cmd.run).to eq "hello\n" end it "runs the command it's given and allows access to stdout afterwards" do cmd = Terrapin::CommandLine.new("echo", "hello", :swallow_stderr => false) cmd.run expect(cmd.command_output).to eq "hello\n" end it "colorizes the output to a tty" do logger = FakeLogger.new(:tty => true) Terrapin::CommandLine.new("echo", "'Logging!' :foo", :logger => logger).run(:foo => "bar") logger.entries.should include("\e[32mCommand\e[0m :: echo 'Logging!' 'bar'") end it 'can still take something that does not respond to tty as a logger' do output_buffer = StringIO.new logger = best_logger.new(output_buffer) logger.should_not respond_to(:tty?) Terrapin::CommandLine.new("echo", "'Logging!' :foo", :logger => logger).run(:foo => "bar") output_buffer.rewind output_buffer.read.should == "Command :: echo 'Logging!' 'bar'\n" end it "logs the command to a supplied logger" do logger = FakeLogger.new Terrapin::CommandLine.new("echo", "'Logging!' :foo", :logger => logger).run(:foo => "bar") logger.entries.should include("Command :: echo 'Logging!' 'bar'") end it "logs the command to a default logger" do Terrapin::CommandLine.logger = FakeLogger.new Terrapin::CommandLine.new("echo", "'Logging!'").run Terrapin::CommandLine.logger.entries.should include("Command :: echo 'Logging!'") end it "is fine if no logger is supplied" do Terrapin::CommandLine.logger = nil cmd = Terrapin::CommandLine.new("echo", "'Logging!'", :logger => nil) lambda { cmd.run }.should_not raise_error end end terrapin-0.6.0/spec/terrapin/os_detector_spec.rb0000644000175000017500000000072413503711060022525 0ustar utkarsh2102utkarsh2102require 'spec_helper' describe Terrapin::OSDetector do it "detects that the system is unix" do on_unix! Terrapin::OS.should be_unix end it "detects that the system is windows" do on_windows! Terrapin::OS.should be_windows end it "detects that the system is windows (mingw)" do on_mingw! Terrapin::OS.should be_windows end it "detects that the current Ruby is on Java" do on_java! Terrapin::OS.should be_java end end terrapin-0.6.0/LICENSE0000644000175000017500000000212413503711060015077 0ustar utkarsh2102utkarsh2102 LICENSE The MIT License Copyright (c) 2011-2014 Jon Yurek and thoughtbot, inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. terrapin-0.6.0/lib/0000755000175000017500000000000013503711060014641 5ustar utkarsh2102utkarsh2102terrapin-0.6.0/lib/terrapin.rb0000644000175000017500000000042013503711060017006 0ustar utkarsh2102utkarsh2102# coding: UTF-8 require 'rbconfig' require 'terrapin/os_detector' require 'terrapin/command_line' require 'terrapin/command_line/output' require 'terrapin/command_line/multi_pipe' require 'terrapin/command_line/runners' require 'terrapin/exceptions' module Terrapin end terrapin-0.6.0/lib/terrapin/0000755000175000017500000000000013503711060016465 5ustar utkarsh2102utkarsh2102terrapin-0.6.0/lib/terrapin/os_detector.rb0000644000175000017500000000052613503711060021327 0ustar utkarsh2102utkarsh2102# coding: UTF-8 module Terrapin class OSDetector def java? arch =~ /java/ end def unix? RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ end def windows? !unix? end def path_separator File::PATH_SEPARATOR end def arch RUBY_PLATFORM end end OS = OSDetector.new end terrapin-0.6.0/lib/terrapin/exceptions.rb0000644000175000017500000000035313503711060021174 0ustar utkarsh2102utkarsh2102# coding: UTF-8 module Terrapin class CommandLineError < StandardError; end class CommandNotFoundError < CommandLineError; end class ExitStatusError < CommandLineError; end class InterpolationError < CommandLineError; end end terrapin-0.6.0/lib/terrapin/command_line/0000755000175000017500000000000013503711060021112 5ustar utkarsh2102utkarsh2102terrapin-0.6.0/lib/terrapin/command_line/runners/0000755000175000017500000000000013503711060022606 5ustar utkarsh2102utkarsh2102terrapin-0.6.0/lib/terrapin/command_line/runners/process_runner.rb0000644000175000017500000000137213503711060026205 0ustar utkarsh2102utkarsh2102# coding: UTF-8 module Terrapin class CommandLine class ProcessRunner def self.available? Process.respond_to?(:spawn) end def self.supported? available? && !OS.java? end def supported? self.class.supported? end def call(command, env = {}, options = {}) pipe = MultiPipe.new pid = spawn(env, command, options.merge(pipe.pipe_options)) pipe.read_and_then do waitpid(pid) end pipe.output end private def spawn(*args) Process.spawn(*args) end def waitpid(pid) Process.waitpid(pid) rescue Errno::ECHILD # In JRuby, waiting on a finished pid raises. end end end end terrapin-0.6.0/lib/terrapin/command_line/runners/backticks_runner.rb0000644000175000017500000000076613503711060026473 0ustar utkarsh2102utkarsh2102# coding: UTF-8 require 'climate_control' module Terrapin class CommandLine class BackticksRunner def self.supported? true end def supported? self.class.supported? end def call(command, env = {}, options = {}) with_modified_environment(env) do Output.new(`#{command}`) end end private def with_modified_environment(env, &block) ClimateControl.modify(env, &block) end end end end terrapin-0.6.0/lib/terrapin/command_line/runners/fake_runner.rb0000644000175000017500000000102313503711060025426 0ustar utkarsh2102utkarsh2102# coding: UTF-8 module Terrapin class CommandLine class FakeRunner def self.supported? false end def supported? self.class.supported? end attr_reader :commands def initialize @commands = [] end def call(command, env = {}, options = {}) commands << [command, env] Output.new("") end def ran?(predicate_command) @commands.any?{|(command, _)| command =~ Regexp.new(predicate_command) } end end end end terrapin-0.6.0/lib/terrapin/command_line/runners/popen_runner.rb0000644000175000017500000000102713503711060025645 0ustar utkarsh2102utkarsh2102# coding: UTF-8 module Terrapin class CommandLine class PopenRunner def self.supported? true end def supported? self.class.supported? end def call(command, env = {}, options = {}) with_modified_environment(env) do IO.popen(command, "r", options) do |pipe| Output.new(pipe.read) end end end private def with_modified_environment(env, &block) ClimateControl.modify(env, &block) end end end end terrapin-0.6.0/lib/terrapin/command_line/runners/posix_runner.rb0000644000175000017500000000164613503711060025675 0ustar utkarsh2102utkarsh2102# coding: UTF-8 module Terrapin class CommandLine class PosixRunner def self.available? return @available unless @available.nil? @available = posix_spawn_gem_available? end def self.supported? available? && !OS.java? end def supported? self.class.supported? end def call(command, env = {}, options = {}) pipe = MultiPipe.new pid = spawn(env, command, options.merge(pipe.pipe_options)) pipe.read_and_then do waitpid(pid) end pipe.output end private def spawn(*args) POSIX::Spawn.spawn(*args) end def waitpid(pid) Process.waitpid(pid) end def self.posix_spawn_gem_available? require 'posix/spawn' true rescue LoadError false end private_class_method :posix_spawn_gem_available? end end end terrapin-0.6.0/lib/terrapin/command_line/multi_pipe.rb0000644000175000017500000000164513503711060023614 0ustar utkarsh2102utkarsh2102module Terrapin class CommandLine class MultiPipe def initialize @stdout_in, @stdout_out = IO.pipe @stderr_in, @stderr_out = IO.pipe end def pipe_options { out: @stdout_out, err: @stderr_out } end def output Output.new(@stdout_output, @stderr_output) end def read_and_then(&block) close_write read block.call close_read end private def close_write @stdout_out.close @stderr_out.close end def read @stdout_output = read_stream(@stdout_in) @stderr_output = read_stream(@stderr_in) end def close_read @stdout_in.close @stderr_in.close end def read_stream(io) result = "" while partial_result = io.read(8192) result << partial_result end result end end end end terrapin-0.6.0/lib/terrapin/command_line/output.rb0000644000175000017500000000033713503711060023002 0ustar utkarsh2102utkarsh2102class Terrapin::CommandLine::Output def initialize(output = nil, error_output = nil) @output = output @error_output = error_output end attr_reader :output, :error_output def to_s output.to_s end end terrapin-0.6.0/lib/terrapin/command_line/runners.rb0000644000175000017500000000043713503711060023137 0ustar utkarsh2102utkarsh2102# coding: UTF-8 require 'terrapin/command_line/runners/backticks_runner' require 'terrapin/command_line/runners/process_runner' require 'terrapin/command_line/runners/posix_runner' require 'terrapin/command_line/runners/popen_runner' require 'terrapin/command_line/runners/fake_runner' terrapin-0.6.0/lib/terrapin/version.rb0000644000175000017500000000007713503711060020503 0ustar utkarsh2102utkarsh2102# coding: UTF-8 module Terrapin VERSION = "0.6.0".freeze end terrapin-0.6.0/lib/terrapin/command_line.rb0000644000175000017500000001074113503711060021442 0ustar utkarsh2102utkarsh2102# coding: UTF-8 module Terrapin class CommandLine class << self attr_accessor :logger, :runner def path @supplemental_path end def path=(supplemental_path) @supplemental_path = Array(supplemental_path). flatten. join(OS.path_separator) end def environment @supplemental_environment ||= {} end def runner @runner || best_runner end def runner_options @default_runner_options ||= {} end def fake! @runner = FakeRunner.new end def unfake! @runner = nil end private def best_runner [PosixRunner, ProcessRunner, BackticksRunner].detect do |runner| runner.supported? end.new end end @environment = {} attr_reader :exit_status, :runner def initialize(binary, params = "", options = {}) @binary = binary.dup @params = params.dup @options = options.dup @runner = @options.delete(:runner) || self.class.runner @logger = @options.delete(:logger) || self.class.logger @swallow_stderr = @options.delete(:swallow_stderr) @expected_outcodes = @options.delete(:expected_outcodes) || [0] @environment = @options.delete(:environment) || {} @runner_options = @options.delete(:runner_options) || {} end def command(interpolations = {}) cmd = [path_prefix, @binary, interpolate(@params, interpolations)] cmd << bit_bucket if @swallow_stderr cmd.join(" ").strip end def run(interpolations = {}) @exit_status = nil begin full_command = command(interpolations) log("#{colored("Command")} :: #{full_command}") @output = execute(full_command) rescue Errno::ENOENT => e raise Terrapin::CommandNotFoundError, e.message ensure @exit_status = $?.respond_to?(:exitstatus) ? $?.exitstatus : 0 end if @exit_status == 127 raise Terrapin::CommandNotFoundError end unless @expected_outcodes.include?(@exit_status) message = [ "Command '#{full_command}' returned #{@exit_status}. Expected #{@expected_outcodes.join(", ")}", "Here is the command output: STDOUT:\n", command_output, "\nSTDERR:\n", command_error_output ].join("\n") raise Terrapin::ExitStatusError, message end command_output end def command_output output.output end def command_error_output output.error_output end def output @output || Output.new end private def colored(text, ansi_color = "\e[32m") if @logger && @logger.respond_to?(:tty?) && @logger.tty? "#{ansi_color}#{text}\e[0m" else text end end def log(text) if @logger @logger.info(text) end end def path_prefix if !self.class.path.nil? && !self.class.path.empty? os_path_prefix end end def os_path_prefix if OS.unix? unix_path_prefix else windows_path_prefix end end def unix_path_prefix "PATH=#{self.class.path}#{OS.path_separator}$PATH;" end def windows_path_prefix "SET PATH=#{self.class.path}#{OS.path_separator}%PATH% &" end def execute(command) runner.call(command, environment, runner_options) end def environment self.class.environment.merge(@environment) end def runner_options self.class.runner_options.merge(@runner_options) end def interpolate(pattern, interpolations) interpolations = stringify_keys(interpolations) pattern.gsub(/:\{?(\w+)\b\}?/) do |match| key = match.tr(":{}", "") if interpolations.key?(key) shell_quote_all_values(interpolations[key]) else match end end end def stringify_keys(hash) Hash[hash.map{ |k, v| [k.to_s, v] }] end def shell_quote_all_values(values) Array(values).map(&method(:shell_quote)).join(" ") end def shell_quote(string) return "" if string.nil? string = string.to_s if string.respond_to? :to_s if OS.unix? if string.empty? "''" else string.split("'", -1).map{|m| "'#{m}'" }.join("\\'") end else %{"#{string}"} end end def bit_bucket OS.unix? ? "2>/dev/null" : "2>NUL" end end end terrapin-0.6.0/Rakefile0000644000175000017500000000043213503711060015537 0ustar utkarsh2102utkarsh2102require 'bundler/setup' require 'bundler/gem_tasks' require 'rspec/core/rake_task' desc 'Default: Run specs.' task :default => 'spec:unit' namespace :spec do desc "Run unit specs" RSpec::Core::RakeTask.new('unit') do |t| t.pattern = 'spec/terrapin/**/*_spec.rb' end end terrapin-0.6.0/README.md0000644000175000017500000001525613503711060015363 0ustar utkarsh2102utkarsh2102# 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. ## POSIX Spawn You can potentially increase performance by installing [the posix-spawn gem](https://rubygems.org/gems/posix-spawn). This gem can keep your application's heap from being copied when forking command line processes. For applications with large heaps the gain can be significant. To include `posix-spawn`, simply add it to your `Gemfile` or, if you don't use bundler, install the gem. ## Runners Terrapin will attempt to choose from among 3 different ways of running commands. The simplest is using backticks, and is the default in 1.8. In Ruby 1.9, it will attempt to use `Process.spawn`. And, as mentioned above, if the `posix-spawn` gem is installed, it will attempt to use that. If for some reason one of the `.spawn` runners don't work for you, you can override them manually by setting a new runner, like so: ```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 [security@thoughtbot.com](security@thoughtbot.com). 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)! ![thoughtbot](http://thoughtbot.com/logo.png) Terrapin is maintained and funded by [thoughtbot, inc](http://thoughtbot.com/community) The names and logos for thoughtbot are trademarks of thoughtbot, inc. ## License Copyright 2011-2018 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. terrapin-0.6.0/GOALS0000644000175000017500000000046113503711060014664 0ustar utkarsh2102utkarsh2102Terrapin hits 1.0.0 when: [*] It can run command lines across all major unix platforms. [*] It can run command lines on Windows. [*] It handles quoting. [*] It takes advantage of OS-specific functionality to be thread-safe, when possible. [ ] It has a consistent and functioning API that pleases us. terrapin-0.6.0/NEWS.md0000644000175000017500000000570313503711060015176 0ustar utkarsh2102utkarsh2102New for 0.6.0: * Rename the project to Terrapin New for 0.5.8: * Improvement: Ensure that argument interpolations can be turned into Strings * Feature: Save STDOUT and STDERR for inspection when the command completes * Bug fix: Properly interpolate at the end of the line New for 0.5.7: * Feature: Allow collection of both STDOUT and STDERR. * Improvement: Convert arguments to strings when possible New for 0.5.6: * Bug Fix: Java does not need to run commands with `env` * Bug Fix: Found out we were rescuing the wrong error New for 0.5.5: * Bug Fix: Posix- and ProcessRunner respect paths *and* are thread safe! * Bug Fix: `exitstatus` should always be set, even if command doesn't run. * Test Fix: Do not try to test Runners if they don't run on this system. * Improvement: Pass the Errno::ENOENT message through to the exception. * Improvement: Improve documentation New for 0.5.4: * Bug Fix: PosixRunner and ProcessRunner respect supplemental paths now. New for 0.5.3: * SECURITY: Fix exploitable bug that could allow arbitrary command execution. See CVE-2013-4457 for more details. Thanks to Holger Just for report and fix! * Bug fix: Sub-word interpolations can be confused for the longer version New for 0.5.2: * Improvement: Close all the IO objects! * Feature: Add an Runner that uses IO.popen, so JRuby can play * Improvement: Officially drop Ruby 1.8 support, add Ruby 2.0 support * Bug fix: Prevent a crash if no command was actually run * Improvement: Add security cautions to the README New for 0.5.1: * Fixed a bug preventing running on 1.8.7 for no good reason. New for 0.5.0: * Updated the copyrights to 2013 * Added UTF encoding markers on code files to ensure they're interpreted as UTF-8 instead of ASCII. * Swapped the ordering of the PATH and supplemental path. A binary in the supplemental path will take precedence, now. * Errors contain the output of the erroring command, for inspection. * Use climate_control instead for environment management. New for 0.4.2: * Loggers that don't understand `tty?`, like `ActiveSupport::BufferedLogger` will still work. New for 0.4.1: * Introduce FakeRunner for testing, so you don't really run commands. * Fix logging: output the actual command, not the un-interpolated pattern. * Prevent color codes from being output if log destination isn't a TTY. New for 0.4.0: * Moved interpolation to the `run` method, instead of interpolating on `new`. * Remove official support for REE. New for 0.3.2: * Fix a hang when processes wait for IO. New for 0.3.1: * Made the `Runner` manually swappable, in case `ProcessRunner` doesn't work for some reason. * Fixed copyright years. New for 0.3.0: * Support blank arguments. * Add `CommandLine#unix?`. * Add `CommandLine#exit_status`. * Automatically use `POSIX::Spawn` if available. * Add `CommandLine#environment` as a hash of extra `ENV` data.. * Add `CommandLine#runner` which produces an object that responds to `#call`. * Fix a race condition but only on Ruby 1.9. terrapin-0.6.0/.travis.yml0000644000175000017500000000007613503711060016207 0ustar utkarsh2102utkarsh2102rvm: - 1.9.3 - 2.0.0 - 2.1.5 - jruby-19mode - rbx-2 terrapin-0.6.0/terrapin.gemspec0000644000175000017500000000217213503711060017266 0ustar utkarsh2102utkarsh2102$LOAD_PATH.push File.expand_path("../lib", __FILE__) require 'terrapin/version' Gem::Specification.new do |s| s.name = "terrapin" s.version = Terrapin::VERSION.dup s.platform = Gem::Platform::RUBY s.author = "Jon Yurek" s.email = "jyurek@thoughtbot.com" s.homepage = "https://github.com/thoughtbot/terrapin" s.summary = "Run shell commands safely, even with user-supplied values" s.description = "Run shell commands safely, even with user-supplied values" s.license = "MIT" s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f)} s.require_paths = ["lib"] s.add_dependency('climate_control', '>= 0.0.3', '< 1.0') s.add_development_dependency('rspec') s.add_development_dependency('bourne') s.add_development_dependency('mocha') s.add_development_dependency('rake') s.add_development_dependency('activesupport', ">= 3.0.0", "< 5.0") s.add_development_dependency('pry') end terrapin-0.6.0/.gitignore0000644000175000017500000000006613503711060016065 0ustar utkarsh2102utkarsh2102*.rbc .rbx *.gem Gemfile.lock *.swp *.swo .bundle bin terrapin-0.6.0/Gemfile0000644000175000017500000000012313503711060015362 0ustar utkarsh2102utkarsh2102source 'https://rubygems.org' gemspec platforms :ruby do gem "posix-spawn" end