cocaine-0.5.8/0000755000175000017500000000000013132653355012515 5ustar amruthamruthcocaine-0.5.8/LICENSE0000644000175000017500000000212413132653355013521 0ustar amruthamruth 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. cocaine-0.5.8/Gemfile0000644000175000017500000000012313132653355014004 0ustar amruthamruthsource 'https://rubygems.org' gemspec platforms :ruby do gem "posix-spawn" end cocaine-0.5.8/cocaine.gemspec0000644000175000017500000000213013132653355015457 0ustar amruthamruth$LOAD_PATH.push File.expand_path("../lib", __FILE__) require 'cocaine/version' Gem::Specification.new do |s| s.name = "cocaine" s.version = Cocaine::VERSION.dup s.platform = Gem::Platform::RUBY s.author = "Jon Yurek" s.email = "jyurek@thoughtbot.com" s.homepage = "https://github.com/thoughtbot/cocaine" s.summary = "A small library for doing (command) lines" s.description = "A small library for doing (command) lines" 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 cocaine-0.5.8/spec/0000755000175000017500000000000013132653355013447 5ustar amruthamruthcocaine-0.5.8/spec/spec_helper.rb0000644000175000017500000000134313132653355016266 0ustar amruthamruthrequire 'rspec' require 'mocha/api' require 'bourne' require 'cocaine' 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 cocaine-0.5.8/spec/support/0000755000175000017500000000000013132653355015163 5ustar amruthamruthcocaine-0.5.8/spec/support/have_output.rb0000644000175000017500000000035113132653355020052 0ustar amruthamruthRSpec::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 cocaine-0.5.8/spec/support/with_exitstatus.rb0000644000175000017500000000041313132653355020756 0ustar amruthamruthmodule 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 cocaine-0.5.8/spec/support/unsetting_exitstatus.rb0000644000175000017500000000020613132653355022023 0ustar amruthamruthmodule UnsettingExitstatus def assuming_no_processes_have_been_run class << $? undef_method :exitstatus end end end cocaine-0.5.8/spec/support/nonblocking_examples.rb0000644000175000017500000000065313132653355021715 0ustar amruthamruthshared_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 cocaine-0.5.8/spec/support/stub_os.rb0000644000175000017500000000111113132653355017160 0ustar amruthamruthmodule StubOS def on_windows! stub_os('mswin') Cocaine::OS.stubs(:path_separator).returns(";") end def on_unix! stub_os('darwin11.0.0') Cocaine::OS.stubs(:path_separator).returns(":") end def on_mingw! stub_os('mingw') Cocaine::OS.stubs(:path_separator).returns(";") end def on_java! Cocaine::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 cocaine-0.5.8/spec/support/fake_logger.rb0000644000175000017500000000031713132653355017756 0ustar amruthamruthclass FakeLogger def initialize(options = {}) @tty = options[:tty] @entries = [] end def info(text) @entries << text end def entries @entries end def tty? @tty end end cocaine-0.5.8/spec/cocaine/0000755000175000017500000000000013132653355015050 5ustar amruthamruthcocaine-0.5.8/spec/cocaine/errors_spec.rb0000644000175000017500000000371713132653355017733 0ustar amruthamruthrequire 'spec_helper' describe "When an error happens" do it "raises a CommandLineError if the result code command isn't expected" do cmd = Cocaine::CommandLine.new("echo", "hello") cmd.stubs(:execute) with_exitstatus_returning(1) do lambda { cmd.run }.should raise_error(Cocaine::CommandLineError) end end it "does not raise if the result code is expected, even if nonzero" do cmd = Cocaine::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 = Cocaine::CommandLine.new("echo", "hello") error_output = "Error 315" cmd. stubs(:execute). returns(Cocaine::CommandLine::Output.new("", error_output)) with_exitstatus_returning(1) do begin cmd.run rescue Cocaine::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 = Cocaine::CommandLine.new('test', '') cmd.stubs(:execute).raises(Errno::ENOENT.new("not found")) begin cmd.run rescue Cocaine::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 = Cocaine::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 = Cocaine::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 cocaine-0.5.8/spec/cocaine/os_detector_spec.rb0000644000175000017500000000071713132653355020726 0ustar amruthamruthrequire 'spec_helper' describe Cocaine::OSDetector do it "detects that the system is unix" do on_unix! Cocaine::OS.should be_unix end it "detects that the system is windows" do on_windows! Cocaine::OS.should be_windows end it "detects that the system is windows (mingw)" do on_mingw! Cocaine::OS.should be_windows end it "detects that the current Ruby is on Java" do on_java! Cocaine::OS.should be_java end end cocaine-0.5.8/spec/cocaine/runners_spec.rb0000644000175000017500000000643413132653355020112 0ustar amruthamruthrequire 'spec_helper' describe "When picking a Runner" do it "uses the BackticksRunner by default" do Cocaine::CommandLine::ProcessRunner.stubs(:supported?).returns(false) Cocaine::CommandLine::PosixRunner.stubs(:supported?).returns(false) cmd = Cocaine::CommandLine.new("echo", "hello") cmd.runner.class.should == Cocaine::CommandLine::BackticksRunner end it "uses the ProcessRunner on 1.9 and it's available" do Cocaine::CommandLine::ProcessRunner.stubs(:supported?).returns(true) Cocaine::CommandLine::PosixRunner.stubs(:supported?).returns(false) cmd = Cocaine::CommandLine.new("echo", "hello") cmd.runner.class.should == Cocaine::CommandLine::ProcessRunner end it "uses the PosixRunner if the PosixRunner is available" do Cocaine::CommandLine::PosixRunner.stubs(:supported?).returns(true) cmd = Cocaine::CommandLine.new("echo", "hello") cmd.runner.class.should == Cocaine::CommandLine::PosixRunner end it "uses the BackticksRunner if the PosixRunner is available, but we told it to use Backticks all the time" do Cocaine::CommandLine::PosixRunner.stubs(:supported?).returns(true) Cocaine::CommandLine.runner = Cocaine::CommandLine::BackticksRunner.new cmd = Cocaine::CommandLine.new("echo", "hello") cmd.runner.class.should == Cocaine::CommandLine::BackticksRunner end it "uses the BackticksRunner if the PosixRunner is available, but we told it to use Backticks" do Cocaine::CommandLine::PosixRunner.stubs(:supported?).returns(true) cmd = Cocaine::CommandLine.new("echo", "hello", :runner => Cocaine::CommandLine::BackticksRunner.new) cmd.runner.class.should == Cocaine::CommandLine::BackticksRunner end it "can go into 'Fake' mode" do Cocaine::CommandLine.fake! cmd = Cocaine::CommandLine.new("echo", "hello") cmd.runner.class.should eq Cocaine::CommandLine::FakeRunner end it "can turn off Fake mode" do Cocaine::CommandLine.fake! Cocaine::CommandLine.unfake! cmd = Cocaine::CommandLine.new("echo", "hello") cmd.runner.class.should_not eq Cocaine::CommandLine::FakeRunner end it "can use a FakeRunner even if not in Fake mode" do Cocaine::CommandLine.unfake! cmd = Cocaine::CommandLine.new("echo", "hello", :runner => Cocaine::CommandLine::FakeRunner.new) cmd.runner.class.should eq Cocaine::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') Cocaine::CommandLine.path = path end after do FileUtils.rm_f("#{Cocaine::CommandLine.path}/ls") end [ Cocaine::CommandLine::BackticksRunner, Cocaine::CommandLine::PopenRunner, Cocaine::CommandLine::PosixRunner, Cocaine::CommandLine::ProcessRunner ].each do |runner_class| if runner_class.supported? describe runner_class do describe '#run' do it 'finds the correct executable' do Cocaine::CommandLine.runner = runner_class.new command = Cocaine::CommandLine.new('ls') result = command.run expect(result.strip).to eq('overridden-ls') end end end end end end cocaine-0.5.8/spec/cocaine/command_line_spec.rb0000644000175000017500000001716413132653355021045 0ustar amruthamruthrequire 'spec_helper' describe Cocaine::CommandLine do before do Cocaine::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 = Cocaine::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 Cocaine::CommandLine.path = ["/path/to/command/dir", "/"] cmd = Cocaine::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! Cocaine::CommandLine.path = ['C:\system32', 'D:\\'] cmd = Cocaine::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 Cocaine::CommandLine.path = ["/path/to/command/dir", "/some/other/path"] cmd = Cocaine::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 Cocaine::CommandLine.environment['TEST'] = 'Hello, world!' cmd = Cocaine::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 Cocaine::CommandLine.environment['TEST'] = 'Hello, world!' cmd = Cocaine::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 = Cocaine::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 = Cocaine::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 = Cocaine::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 = Cocaine::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 = Cocaine::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 = Cocaine::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 = Cocaine::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 = Cocaine::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 = Cocaine::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 = Cocaine::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 = Cocaine::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 = Cocaine::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 = Cocaine::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 = Cocaine::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 = Cocaine::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 = Cocaine::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) Cocaine::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?) Cocaine::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 Cocaine::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 Cocaine::CommandLine.logger = FakeLogger.new Cocaine::CommandLine.new("echo", "'Logging!'").run Cocaine::CommandLine.logger.entries.should include("Command :: echo 'Logging!'") end it "is fine if no logger is supplied" do Cocaine::CommandLine.logger = nil cmd = Cocaine::CommandLine.new("echo", "'Logging!'", :logger => nil) lambda { cmd.run }.should_not raise_error end end cocaine-0.5.8/spec/cocaine/command_line/0000755000175000017500000000000013132653355017475 5ustar amruthamruthcocaine-0.5.8/spec/cocaine/command_line/runners/0000755000175000017500000000000013132653355021171 5ustar amruthamruthcocaine-0.5.8/spec/cocaine/command_line/runners/backticks_runner_spec.rb0000644000175000017500000000134413132653355026061 0ustar amruthamruthrequire 'spec_helper' describe Cocaine::CommandLine::BackticksRunner do if Cocaine::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 cocaine-0.5.8/spec/cocaine/command_line/runners/fake_runner_spec.rb0000644000175000017500000000133413132653355025030 0ustar amruthamruthrequire 'spec_helper' describe Cocaine::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 cocaine-0.5.8/spec/cocaine/command_line/runners/process_runner_spec.rb0000644000175000017500000000235613132653355025605 0ustar amruthamruthrequire 'spec_helper' describe Cocaine::CommandLine::ProcessRunner do if Cocaine::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 = Cocaine::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 cocaine-0.5.8/spec/cocaine/command_line/runners/popen_runner_spec.rb0000644000175000017500000000133413132653355025243 0ustar amruthamruthrequire 'spec_helper' describe Cocaine::CommandLine::PopenRunner do if Cocaine::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 cocaine-0.5.8/spec/cocaine/command_line/runners/posix_runner_spec.rb0000644000175000017500000000235213132653355025265 0ustar amruthamruthrequire 'spec_helper' describe Cocaine::CommandLine::PosixRunner do if Cocaine::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 = Cocaine::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 cocaine-0.5.8/spec/cocaine/command_line/output_spec.rb0000644000175000017500000000062313132653355022375 0ustar amruthamruthrequire 'spec_helper' describe Cocaine::CommandLine::Output do it 'holds an input and error stream' do output = Cocaine::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 = Cocaine::CommandLine::Output.new(:a, :b) expect(output.to_s).to eq 'a' end end cocaine-0.5.8/Rakefile0000644000175000017500000000043113132653355014160 0ustar amruthamruthrequire '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/cocaine/**/*_spec.rb' end end cocaine-0.5.8/lib/0000755000175000017500000000000013132653355013263 5ustar amruthamruthcocaine-0.5.8/lib/cocaine.rb0000644000175000017500000000041113132653355015205 0ustar amruthamruth# coding: UTF-8 require 'rbconfig' require 'cocaine/os_detector' require 'cocaine/command_line' require 'cocaine/command_line/output' require 'cocaine/command_line/multi_pipe' require 'cocaine/command_line/runners' require 'cocaine/exceptions' module Cocaine end cocaine-0.5.8/lib/cocaine/0000755000175000017500000000000013132653355014664 5ustar amruthamruthcocaine-0.5.8/lib/cocaine/version.rb0000644000175000017500000000007713132653355016702 0ustar amruthamruth# coding: UTF-8 module Cocaine VERSION = "0.5.8".freeze end cocaine-0.5.8/lib/cocaine/exceptions.rb0000644000175000017500000000035213132653355017372 0ustar amruthamruth# coding: UTF-8 module Cocaine class CommandLineError < StandardError; end class CommandNotFoundError < CommandLineError; end class ExitStatusError < CommandLineError; end class InterpolationError < CommandLineError; end end cocaine-0.5.8/lib/cocaine/command_line/0000755000175000017500000000000013132653355017311 5ustar amruthamruthcocaine-0.5.8/lib/cocaine/command_line/runners/0000755000175000017500000000000013132653355021005 5ustar amruthamruthcocaine-0.5.8/lib/cocaine/command_line/runners/backticks_runner.rb0000644000175000017500000000076513132653355024671 0ustar amruthamruth# coding: UTF-8 require 'climate_control' module Cocaine 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 cocaine-0.5.8/lib/cocaine/command_line/runners/process_runner.rb0000644000175000017500000000137113132653355024403 0ustar amruthamruth# coding: UTF-8 module Cocaine 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 cocaine-0.5.8/lib/cocaine/command_line/runners/posix_runner.rb0000644000175000017500000000164513132653355024073 0ustar amruthamruth# coding: UTF-8 module Cocaine 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 cocaine-0.5.8/lib/cocaine/command_line/runners/fake_runner.rb0000644000175000017500000000102213132653355023624 0ustar amruthamruth# coding: UTF-8 module Cocaine 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 cocaine-0.5.8/lib/cocaine/command_line/runners/popen_runner.rb0000644000175000017500000000102613132653355024043 0ustar amruthamruth# coding: UTF-8 module Cocaine 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 cocaine-0.5.8/lib/cocaine/command_line/runners.rb0000644000175000017500000000043213132653355021331 0ustar amruthamruth# coding: UTF-8 require 'cocaine/command_line/runners/backticks_runner' require 'cocaine/command_line/runners/process_runner' require 'cocaine/command_line/runners/posix_runner' require 'cocaine/command_line/runners/popen_runner' require 'cocaine/command_line/runners/fake_runner' cocaine-0.5.8/lib/cocaine/command_line/output.rb0000644000175000017500000000033613132653355021200 0ustar amruthamruthclass Cocaine::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 cocaine-0.5.8/lib/cocaine/command_line/multi_pipe.rb0000644000175000017500000000164413132653355022012 0ustar amruthamruthmodule Cocaine 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 cocaine-0.5.8/lib/cocaine/command_line.rb0000644000175000017500000001073513132653355017644 0ustar amruthamruth# coding: UTF-8 module Cocaine 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 Cocaine::CommandNotFoundError, e.message ensure @exit_status = $?.respond_to?(:exitstatus) ? $?.exitstatus : 0 end if @exit_status == 127 raise Cocaine::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 Cocaine::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 cocaine-0.5.8/lib/cocaine/os_detector.rb0000644000175000017500000000052513132653355017525 0ustar amruthamruth# coding: UTF-8 module Cocaine 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 cocaine-0.5.8/NEWS.md0000644000175000017500000000525713132653355013624 0ustar amruthamruthNew 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. cocaine-0.5.8/.travis.yml0000644000175000017500000000007613132653355014631 0ustar amruthamruthrvm: - 1.9.3 - 2.0.0 - 2.1.5 - jruby-19mode - rbx-2 cocaine-0.5.8/GOALS0000644000175000017500000000046013132653355013305 0ustar amruthamruthCocaine 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. cocaine-0.5.8/.gitignore0000644000175000017500000000006613132653355014507 0ustar amruthamruth*.rbc .rbx *.gem Gemfile.lock *.swp *.swo .bundle bin cocaine-0.5.8/README.md0000644000175000017500000001463013132653355014000 0ustar amruthamruth# Cocaine [![Build Status](https://secure.travis-ci.org/thoughtbot/cocaine.png?branch=master)](http://travis-ci.org/thoughtbot/cocaine) A small library for doing (command) lines. [API reference](http://rubydoc.info/gems/cocaine/) ## Usage The basic, normal stuff: ```ruby line = Cocaine::CommandLine.new("echo", "hello 'world'") line.command # => "echo hello 'world'" line.run # => "hello world\n" ``` Interpolated arguments: ```ruby line = Cocaine::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 = Cocaine::CommandLine.new("cat", ":file") line.command(file: "haha`rm -rf /`.txt") # => "cat 'haha`rm -rf /`.txt'" line = Cocaine::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 = Cocaine::CommandLine.new("echo", "haha`whoami`") line.command # => "echo haha`whoami`" line.run # => "hahawebserver" ``` You can ignore the result: ```ruby line = Cocaine::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 = Cocaine::CommandLine.new("git", "commit") begin line.run rescue Cocaine::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 = Cocaine::CommandLine.new("/usr/bin/false", "", expected_outcodes: [0, 1]) begin line.run rescue Cocaine::ExitStatusError => e # => You never get here! end ``` You don't have the command? You get an exception: ```ruby line = Cocaine::CommandLine.new("lolwut") begin line.run rescue Cocaine::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 Cocaine::CommandLine.path = "/opt/bin" line = Cocaine::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') } Cocaine::CommandLine.path = ["/opt/bin", "/usr/local/bin"] line = Cocaine::CommandLine.new("lolwut") line.run # => prints 'Hello', because it searches the path ``` Or just put it in the command: ```ruby line = Cocaine::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! ```ruby line = Cocaine::CommandLine.new("echo", ":var", logger: Logger.new(STDOUT)) line.run(var: "LOL!") # => Logs this with #info -> Command :: echo 'LOL!' ``` Or log every command: ```ruby Cocaine::CommandLine.logger = Logger.new(STDOUT) Cocaine::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, Cocaine 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`. Cocaine 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 Cocaine 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 Cocaine::CommandLine.runner = Cocaine::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/cocaine/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/cocaine/issues/38)), try to use `PopenRunner`: ```ruby Cocaine::CommandLine.runner = Cocaine::CommandLine::PopenRunner.new ``` ## Thread Safety Cocaine 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/cocaine/issues) ## Credits Thank you to all [the contributors](https://github.com/thoughtbot/cocaine/graphs/contributors)! ![thoughtbot](http://thoughtbot.com/logo.png) Cocaine 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-2014 Jon Yurek and thoughtbot, inc. This is free software, and may be redistributed under the terms specified in the [LICENSE](https://github.com/thoughtbot/cocaine/blob/master/LICENSE) file.