interactive_s3 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 22a4ce5049308ee26440e48c45a0471bcbb6c55b
4
+ data.tar.gz: 19e81f547625f94a1af33cb8565d267f1be378cb
5
+ SHA512:
6
+ metadata.gz: c2b434692269b5a03a1841494263344baf717b9d2e9ec4c063705aa89bd0f3cc3228bd9ce5a7a6f160fbef2c3c64861b81eeea4f8a3a4cd47df819484bf545c0
7
+ data.tar.gz: 12e44e37783b6fa98f7c169dad16548226c122d583ebe729f8c92f2e1402bd5b98fab9075d4fecdcc994b6e96c9b82950147e3b7a220fce5980d8b88aaedc5cd
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 2.1
5
+ - 2.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in interactive_s3.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 yamayo
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # InteractiveS3 [![Build Status](https://travis-ci.org/yamayo/interactive_s3.svg?branch=master)](https://travis-ci.org/yamayo/interactive_s3)
2
+ InteractiveS3 is an interactive shell that can be easily extended with commands using a `aws s3` in the [AWS Command Line Interface](http://docs.aws.amazon.com/cli/latest/index.html).
3
+
4
+ ## Requirements
5
+ * Ruby (=> 2.0.0)
6
+ * AWS CLI (~> 1.5.1)
7
+
8
+ ## Installation
9
+ ```sh
10
+ $ gem install interactive_s3
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Start
16
+ InteractiveS3 is run from the command line.
17
+
18
+ ```sh
19
+ $ is3
20
+ ```
21
+
22
+ ### Available commands
23
+
24
+ - **cd** - Change the current path.
25
+ - **pwd** - Show the current path.
26
+ - **lls** - Show local file list.
27
+ - **exit** - Quit InteractiveS3.
28
+
29
+ And `aws s3` commands.
30
+
31
+ ### Examples
32
+
33
+ ```sh
34
+ s3> ls
35
+ 2014-08-07 00:22:58 my-bucket
36
+ s3> cd my-bucket
37
+ s3://my-bucket> ls
38
+ 2014-08-11 00:23:56 4 .
39
+ 2014-08-20 01:31:34 5 foo
40
+ s3://my-bucket> lls
41
+ bar.txt
42
+ ```
43
+
44
+ #### `:` - local file prefix
45
+
46
+ ```sh
47
+ s3://my-bucket> cp :bar.txt .
48
+ upload: ./bar.txt to s3://mybucket/bar.txt
49
+ s3://my-bucket> ls
50
+ 2014-08-11 00:23:56 4 .
51
+ 2014-08-20 01:31:34 5 foo
52
+ 2014-10-19 22:37:43 31 bar.txt
53
+ ```
54
+
55
+ ## Contributing
56
+
57
+ 1. Fork it ( http://github.com/<my-github-username>/interactive_s3/fork )
58
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
59
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
60
+ 4. Push to the branch (`git push origin my-new-feature`)
61
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/is3 ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path('../../lib', __FILE__)
3
+ require 'interactive_s3'
4
+
5
+ InteractiveS3::CLI.new.start
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'interactive_s3/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "interactive_s3"
8
+ spec.version = InteractiveS3::VERSION
9
+ spec.executables = ["is3"]
10
+ spec.authors = ["yamayo"]
11
+ spec.email = ["noorthaven@gmail.com"]
12
+ spec.summary = %q{An interactive shell for AWS CLI (aws s3)}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ end
@@ -0,0 +1,11 @@
1
+ require 'interactive_s3/cli'
2
+ require 'interactive_s3/command_builder'
3
+ require 'interactive_s3/commands/base'
4
+ require 'interactive_s3/commands/internal_command'
5
+ require 'interactive_s3/commands/s3_command'
6
+ require 'interactive_s3/errors'
7
+ require 'interactive_s3/history'
8
+ require 'interactive_s3/interpreter'
9
+ require 'interactive_s3/s3'
10
+ require 'interactive_s3/s3_path'
11
+ require 'interactive_s3/version'
@@ -0,0 +1,49 @@
1
+ require 'readline'
2
+
3
+ module InteractiveS3
4
+ class CLI
5
+ def start
6
+ history.load
7
+ show_version
8
+ run
9
+ ensure
10
+ history.save
11
+ end
12
+
13
+ private
14
+
15
+ def run
16
+ while line = readline
17
+ next if line.empty?
18
+ interpreter.execute(line)
19
+ end
20
+ puts "\n"
21
+ rescue Interrupt
22
+ puts "\n"
23
+ retry
24
+ end
25
+
26
+ def readline
27
+ if line = Readline.readline(prompt, true)
28
+ Readline::HISTORY.pop if /^\s*$/ =~ line.strip
29
+ end
30
+ line
31
+ end
32
+
33
+ def show_version
34
+ puts "InteractiveS3 #{InteractiveS3::VERSION}"
35
+ end
36
+
37
+ def prompt
38
+ interpreter.root? ? 's3> ' : "#{interpreter.current_path}> "
39
+ end
40
+
41
+ def interpreter
42
+ @interpreter ||= Interpreter.new
43
+ end
44
+
45
+ def history
46
+ @history ||= History.new
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,20 @@
1
+ module InteractiveS3
2
+ class CommandBuilder
3
+ def initialize(context, params)
4
+ @context = context
5
+ @name, *@arguments = params.split(/\s/)
6
+ end
7
+
8
+ def build
9
+ command_class.new(context, name, arguments)
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :context, :name, :arguments
15
+
16
+ def command_class
17
+ Commands::InternalCommand.fetch(name.to_sym, Commands::S3Command)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ module InteractiveS3::Commands
2
+ class Base
3
+ attr_accessor :state, :arguments
4
+
5
+ attr_reader :s3, :name
6
+
7
+ def initialize(context, name, arguments = [])
8
+ @s3 = context.s3
9
+ @state = context.state
10
+ @name = name
11
+ @arguments = arguments
12
+ end
13
+
14
+ def execute
15
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
16
+ end
17
+
18
+ private
19
+
20
+ def argument_size
21
+ arguments.size
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,62 @@
1
+ module InteractiveS3::Commands
2
+ module InternalCommand
3
+ def self.fetch(name, default = nil)
4
+ COMMAND_CLASSES.fetch(name, default)
5
+ end
6
+
7
+ class Chdir < Base
8
+ def execute
9
+ state[:previous_stack] ||= []
10
+
11
+ if target.nil?
12
+ state[:previous_stack] = s3.stack
13
+ s3.reset
14
+ return
15
+ end
16
+
17
+ if target.strip == '-'
18
+ s3.stack, state[:previous_stack] = state[:previous_stack], s3.stack
19
+ else
20
+ target.sub!('s3://', '/')
21
+ state[:previous_stack] = s3.stack
22
+ s3.stack = InteractiveS3::S3Path.new(target, s3.stack).resolve
23
+ end
24
+ ensure
25
+ unless s3.exist?
26
+ s3.stack = state[:previous_stack]
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def target
33
+ @target ||= arguments.first
34
+ end
35
+ end
36
+
37
+ class LocalList < Base
38
+ def execute
39
+ puts Dir.entries('.').delete_if {|file| file =~ /^(.|..)$/ }
40
+ end
41
+ end
42
+
43
+ class Pwd < Base
44
+ def execute
45
+ puts s3.root? ? '/' : s3.current_path
46
+ end
47
+ end
48
+
49
+ class Exit < Base
50
+ def execute
51
+ exit
52
+ end
53
+ end
54
+
55
+ COMMAND_CLASSES = {
56
+ cd: Chdir,
57
+ lls: LocalList,
58
+ pwd: Pwd,
59
+ exit: Exit,
60
+ }.freeze
61
+ end
62
+ end
@@ -0,0 +1,97 @@
1
+ module InteractiveS3::Commands
2
+ class S3Command < Base
3
+ S3_PATH_PREFIX = /^s3:\/\//
4
+ LOCAL_PATH_PREFIX = /^:/
5
+ OPTION_PREFIX = /^--/
6
+ HELP_SUB_COMMAND = /^help$/
7
+ TARGET_SUB_COMMANDS = %w(mv rb rm).freeze
8
+
9
+ def initialize(context, name, arguments = [])
10
+ super
11
+ parse_arguments
12
+ end
13
+
14
+ def execute
15
+ pid = Process.spawn(
16
+ *command_with_arguments,
17
+ out: $stdout,
18
+ err: $stderr
19
+ )
20
+ wait_for_process(pid)
21
+ $? && $?.success?
22
+ rescue SystemCallError => e
23
+ puts e.message
24
+ false
25
+ ensure
26
+ if target_sub_command_and_s3_not_exist?
27
+ s3.reset
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ alias sub_command name
34
+
35
+ def parse_arguments
36
+ arguments[target] = arguments[target].map {|argument|
37
+ case argument
38
+ when S3_PATH_PREFIX, HELP_SUB_COMMAND, OPTION_PREFIX
39
+ argument
40
+ when LOCAL_PATH_PREFIX
41
+ argument.sub(LOCAL_PATH_PREFIX, '')
42
+ else
43
+ stack = InteractiveS3::S3Path.new(argument, s3.stack).resolve
44
+ "s3://#{stack.join('/')}"
45
+ end
46
+ }
47
+ end
48
+
49
+ def target
50
+ @target ||= if option_first?
51
+ argument_size-2..argument_size-1
52
+ else
53
+ 0..1
54
+ end
55
+ end
56
+
57
+ def option_first?
58
+ !!(arguments.first =~ OPTION_PREFIX)
59
+ end
60
+
61
+ def command_with_arguments
62
+ arguments << "#{s3.current_path}/" if list_command_with_no_s3_path?
63
+ ['aws', 's3', sub_command, arguments].flatten
64
+ end
65
+
66
+ def list_command_with_no_s3_path?
67
+ list_command? && !help_sub_command? && !include_s3_path?
68
+ end
69
+
70
+ def list_command?
71
+ sub_command == 'ls'
72
+ end
73
+
74
+ def help_sub_command?
75
+ arguments.include?('help')
76
+ end
77
+
78
+ def include_s3_path?
79
+ arguments.any? {|argument| argument.match(S3_PATH_PREFIX) }
80
+ end
81
+
82
+ def target_sub_command?
83
+ TARGET_SUB_COMMANDS.include?(sub_command)
84
+ end
85
+
86
+ def target_sub_command_and_s3_not_exist?
87
+ target_sub_command? && !s3.exist?
88
+ end
89
+
90
+ def wait_for_process(pid)
91
+ Process.wait(pid)
92
+ rescue Interrupt
93
+ Process.kill('INT', pid)
94
+ retry
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,3 @@
1
+ module InteractiveS3
2
+ class CommandError < StandardError; end
3
+ end
@@ -0,0 +1,32 @@
1
+ module InteractiveS3
2
+ class History
3
+ HISTORY_FILE = "#{Dir.home}/.is3_history"
4
+ HISTORY_SIZE = 500
5
+
6
+ def load
7
+ if file_exist?
8
+ File.read(file_path).each_line do |line|
9
+ Readline::HISTORY << line.chomp
10
+ end
11
+ end
12
+ end
13
+
14
+ def save
15
+ File.write(file_path, Readline::HISTORY.to_a.last(history_size).join("\n"))
16
+ end
17
+
18
+ private
19
+
20
+ def file_exist?
21
+ File.exist?(file_path)
22
+ end
23
+
24
+ def file_path
25
+ HISTORY_FILE
26
+ end
27
+
28
+ def history_size
29
+ HISTORY_SIZE.to_i
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ require 'forwardable'
2
+
3
+ module InteractiveS3
4
+ class Interpreter
5
+ extend Forwardable
6
+
7
+ attr_reader :s3, :state
8
+
9
+ def_delegators :s3, :current_path, :root?
10
+
11
+ def initialize
12
+ @s3 = S3.new
13
+ @state = {}
14
+ end
15
+
16
+ def execute(input)
17
+ build_command(input).execute
18
+ rescue CommandError => e
19
+ puts e.class, e.message, e.backtrace.join("\n")
20
+ end
21
+
22
+ private
23
+
24
+ def build_command(input)
25
+ CommandBuilder.new(self, input).build
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,34 @@
1
+ require 'open3'
2
+
3
+ module InteractiveS3
4
+ class S3
5
+ attr_accessor :stack
6
+
7
+ def initialize
8
+ @stack = []
9
+ end
10
+
11
+ def current_path
12
+ empty? ? '' : "s3://#{stack.join('/')}"
13
+ end
14
+
15
+ def empty?
16
+ stack.empty?
17
+ end
18
+ alias root? empty?
19
+
20
+ def reset
21
+ self.stack = []
22
+ end
23
+
24
+ def bucket?
25
+ stack.size == 1
26
+ end
27
+
28
+ def exist?
29
+ return true if root?
30
+ output, error, status = Open3.capture3('aws', 's3', 'ls', current_path)
31
+ status.success? && (bucket? || output != '')
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,40 @@
1
+ module InteractiveS3
2
+ class S3Path
3
+ def initialize(target_path, current_stack)
4
+ @target_path = target_path
5
+ @current_stack = current_stack
6
+ end
7
+
8
+ def resolve
9
+ scanner = StringScanner.new(target_path.strip)
10
+ stack = current_stack.dup
11
+
12
+ begin
13
+ bol = scanner.bol?
14
+ segment = scanner.scan(/[^\/]*/)
15
+ scanner.getch
16
+
17
+ case segment.chomp
18
+ when ''
19
+ stack = [] if bol
20
+ when '.'
21
+ next
22
+ when '..'
23
+ stack.pop
24
+ else
25
+ stack << segment
26
+ end
27
+ rescue => e
28
+ raise CommandError.new(e).tap {|ex|
29
+ ex.set_backtrace(e.backtrace)
30
+ }
31
+ end until scanner.eos?
32
+
33
+ stack
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :current_stack, :target_path
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module InteractiveS3
2
+ VERSION = '0.0.2'
3
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe InteractiveS3::CommandBuilder do
4
+ let(:builder) { described_class.new(context, params) }
5
+ let(:context) { double('context') }
6
+
7
+ before do
8
+ allow(context).to receive(:s3)
9
+ allow(context).to receive(:state)
10
+ end
11
+
12
+ describe '#build' do
13
+ let(:command_class) { double('command_class') }
14
+ let(:params) { 'cd -' }
15
+
16
+ before do
17
+ allow(InteractiveS3::Commands::InternalCommand).to receive(:fetch) { command_class }
18
+ end
19
+
20
+ it 'creates a command class and initializes it' do
21
+ expect(command_class).to receive(:new).with(context, 'cd', ['-'])
22
+ builder.build
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe InteractiveS3::Commands::Base do
4
+ let(:context) { double('context') }
5
+ let(:name) { double('name') }
6
+
7
+ before do
8
+ allow(context).to receive(:s3)
9
+ allow(context).to receive(:state)
10
+ end
11
+
12
+ describe '#execute' do
13
+ context 'when implements the method' do
14
+ let(:instance) do
15
+ Class.new(described_class) {
16
+ def execute; end
17
+ }.new(context, name)
18
+ end
19
+
20
+ it { expect { instance.execute }.not_to raise_error }
21
+ end
22
+
23
+ context 'when does not implements the method' do
24
+ let(:instance) do
25
+ described_class.new(context, name)
26
+ end
27
+
28
+ it { expect { instance.execute }.to raise_error(NotImplementedError) }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,133 @@
1
+ require 'spec_helper'
2
+
3
+ describe InteractiveS3::Commands::InternalCommand do
4
+ let(:context) { double('context') }
5
+ let(:s3) { InteractiveS3::S3.new }
6
+ let(:state) { {} }
7
+
8
+ before do
9
+ allow(context).to receive(:s3).and_return(s3)
10
+ allow(context).to receive(:state).and_return(state)
11
+ end
12
+
13
+ describe '.fetch' do
14
+ subject { described_class.fetch(name.to_sym, InteractiveS3::Commands::S3Command) }
15
+
16
+ context 'given the params `cd`' do
17
+ let(:name) { 'cd' }
18
+
19
+ it { is_expected.to be described_class::Chdir }
20
+ end
21
+
22
+ context 'given the params `pwd`' do
23
+ let(:name) { 'pwd' }
24
+
25
+ it { is_expected.to be described_class::Pwd }
26
+ end
27
+
28
+ context 'given the params `lls`' do
29
+ let(:name) { 'lls' }
30
+
31
+ it { is_expected.to be described_class::LocalList }
32
+ end
33
+
34
+ context 'given the params `exit`' do
35
+ let(:name) { 'exit' }
36
+
37
+ it { is_expected.to be described_class::Exit }
38
+ end
39
+
40
+ context 'given the params `ls`' do
41
+ let(:name) { 'ls' }
42
+
43
+ it { is_expected.to be InteractiveS3::Commands::S3Command }
44
+ end
45
+ end
46
+
47
+ describe described_class::Chdir do
48
+ describe '#execute' do
49
+ before do
50
+ allow(s3).to receive(:exist?).and_return(true)
51
+ s3.stack << 'mybucket' << 'foo' << 'bar'
52
+ end
53
+
54
+ let(:command) { described_class.new(context, 'cd', arguments) }
55
+
56
+ context 'target is nil' do
57
+ let(:arguments) { [] }
58
+
59
+ before { command.execute }
60
+
61
+ it { expect(s3.stack).to be_empty }
62
+ it { expect(state[:previous_stack]).to eq %w(mybucket foo bar) }
63
+ end
64
+
65
+ context 'target is `-`' do
66
+ let(:arguments) { ['-'] }
67
+
68
+ before do
69
+ state[:previous_stack] = %w(xxx yyy)
70
+ command.execute
71
+ end
72
+
73
+ it { expect(s3.stack).to eq %w(xxx yyy) }
74
+ it { expect(state[:previous_stack]).to eq %w(mybucket foo bar) }
75
+ end
76
+
77
+ context 'target is `xxx/yyy`' do
78
+ let(:arguments) { ['xxx', 'yyy'] }
79
+
80
+ before do
81
+ allow_any_instance_of(InteractiveS3::S3Path).to receive(:resolve).and_return(['foo'])
82
+ command.execute
83
+ end
84
+
85
+ it { expect(s3.stack).to eq %w(foo) }
86
+ it { expect(state[:previous_stack]).to eq %w(mybucket foo bar) }
87
+ end
88
+ end
89
+ end
90
+
91
+ describe described_class::LocalList do
92
+ describe '#execute' do
93
+ let(:command) { described_class.new(context, 'lls') }
94
+
95
+ before do
96
+ allow(Dir).to receive(:entries).with('.').and_return(['.', '..', 'foo', 'bar'])
97
+ end
98
+
99
+ it { expect { command.execute }.to output("foo\nbar\n").to_stdout }
100
+ end
101
+ end
102
+
103
+ describe described_class::Pwd do
104
+ describe '#execute' do
105
+ let(:command) { described_class.new(context, 'pwd') }
106
+
107
+ context 'when the s3 path is root' do
108
+ before do
109
+ allow(s3).to receive(:root?).and_return(true)
110
+ end
111
+
112
+ it { expect { command.execute }.to output("/\n").to_stdout }
113
+ end
114
+
115
+ context 'when the s3 path isn\'t root' do
116
+ before do
117
+ allow(s3).to receive(:root?).and_return(false)
118
+ allow(s3).to receive(:current_path).and_return("s3://mybucket/foo/bar")
119
+ end
120
+
121
+ it { expect { command.execute }.to output("s3://mybucket/foo/bar\n").to_stdout }
122
+ end
123
+ end
124
+ end
125
+
126
+ describe described_class::Exit do
127
+ describe '#execute' do
128
+ let(:command) { described_class.new(context, 'exit') }
129
+
130
+ it { expect { command.execute }.to raise_exception(SystemExit) }
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ describe InteractiveS3::Commands::S3Command do
4
+ let(:s3_command) { described_class.new(context, name, arguments) }
5
+ let(:context) { double('context') }
6
+ let(:s3) { InteractiveS3::S3.new }
7
+ let(:state) { {} }
8
+
9
+ before do
10
+ allow(context).to receive(:s3).and_return(s3)
11
+ allow(context).to receive(:state).and_return(state)
12
+ allow(Process).to receive(:spawn)
13
+ allow(Process).to receive(:wait)
14
+ allow_any_instance_of(InteractiveS3::S3Path).to receive(:resolve).and_return(['mybucket', 'bar'])
15
+ end
16
+
17
+ shared_examples_for 'parses commands with arguments and executes it' do |command_with_arguments|
18
+ it do
19
+ expect(Process).to receive(:spawn).with(*command_with_arguments, out: $stdout, err: $stderr)
20
+ expect(s3_command.execute).to be_truthy
21
+ end
22
+ end
23
+
24
+ describe '#execute' do
25
+ context 'ls' do
26
+ let(:name) { 'ls' }
27
+
28
+ context 'with no sub command' do
29
+ let(:arguments) { [] }
30
+
31
+ it_behaves_like 'parses commands with arguments and executes it', ['aws', 's3', 'ls', '/']
32
+ end
33
+
34
+ context 'with help sub command' do
35
+ let(:arguments) { ['help'] }
36
+
37
+ it_behaves_like 'parses commands with arguments and executes it', ['aws', 's3', 'ls', 'help']
38
+ end
39
+ end
40
+
41
+ context 'cp' do
42
+ let(:name) { 'cp' }
43
+
44
+ context 'with local path and s3 path and option' do
45
+ let(:arguments) { [':foo', 'bar', '--option'] }
46
+
47
+ it_behaves_like 'parses commands with arguments and executes it', ['aws', 's3', 'cp', 'foo', 's3://mybucket/bar', '--option']
48
+ end
49
+
50
+ context 'with help sub command' do
51
+ let(:arguments) { ['help'] }
52
+
53
+ it_behaves_like 'parses commands with arguments and executes it', ['aws', 's3', 'cp', 'help']
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+ require 'tempfile'
3
+
4
+ describe InteractiveS3::History do
5
+ let(:history) { described_class.new }
6
+ let(:file) { Tempfile.new('history') }
7
+
8
+ before do
9
+ stub_const('Readline::HISTORY', [])
10
+ stub_const('InteractiveS3::History::HISTORY_FILE', file.path)
11
+ end
12
+
13
+ after do
14
+ file.delete
15
+ end
16
+
17
+ describe '#load' do
18
+ context 'when the file exists' do
19
+ before do
20
+ file.write("pwd\n")
21
+ file.write("ls\n")
22
+ file.rewind
23
+ history.load
24
+ end
25
+
26
+ it 'load into the Readline from the file' do
27
+ expect(Readline::HISTORY.to_a).to eq %w(pwd ls)
28
+ end
29
+ end
30
+
31
+ context 'when the file is empty' do
32
+ before { history.load }
33
+
34
+ it 'does not load into the Readline' do
35
+ expect(Readline::HISTORY.to_a).to be_empty
36
+ end
37
+ end
38
+ end
39
+
40
+ describe '#save' do
41
+ context 'when the file exists' do
42
+ before do
43
+ Readline::HISTORY << 'ls' << 'exit'
44
+ history.save
45
+ end
46
+
47
+ it 'saves the history from Readline to file' do
48
+ expect(file.each_line.to_a.each(&:chomp!)).to eq %w(ls exit)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe InteractiveS3::Interpreter do
4
+ let(:interpreter) { described_class.new }
5
+ let(:input) { double('input') }
6
+ let(:command) { double('command') }
7
+
8
+ describe '#execute' do
9
+ before do
10
+ allow(InteractiveS3::CommandBuilder).to receive_message_chain(:new, :build) { command }
11
+ end
12
+
13
+ it 'build the command and executes it' do
14
+ expect(command).to receive(:execute)
15
+ interpreter.execute(input)
16
+ end
17
+
18
+ it 'handles command error' do
19
+ allow(command).to receive(:execute).and_raise(InteractiveS3::CommandError)
20
+ expect { interpreter.execute(input) }.to output.to_stdout
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe InteractiveS3::S3Path do
4
+ describe '#resolve' do
5
+ subject { described_class.new(path, stack).resolve }
6
+
7
+ let(:stack) { ['foo', 'bar'] }
8
+
9
+ context 'with path is `baz`' do
10
+ let(:path) { 'baz' }
11
+
12
+ it { is_expected.to eq %w(foo bar baz) }
13
+ end
14
+
15
+ context 'with path is `baz/qux`' do
16
+ let(:path) { 'baz/qux' }
17
+
18
+ it { is_expected.to eq %w(foo bar baz qux) }
19
+ end
20
+
21
+ context 'with path is `../..`' do
22
+ let(:path) { '../..' }
23
+
24
+ it { is_expected.to be_empty }
25
+ end
26
+
27
+ context 'with path is empty' do
28
+ let(:path) { '' }
29
+
30
+ it { is_expected.to be_empty }
31
+ end
32
+
33
+ context 'with path is `./baz`' do
34
+ let(:path) { './baz' }
35
+
36
+ it { is_expected.to eq %w(foo bar baz) }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,117 @@
1
+ require 'spec_helper'
2
+
3
+ describe InteractiveS3::S3 do
4
+ let(:s3) { described_class.new }
5
+
6
+ def stack_to_s3
7
+ s3.stack << 'mybucket' << 'foo' << 'bar'
8
+ end
9
+
10
+ describe '#current_path' do
11
+ context 'when the stack exists' do
12
+ before { stack_to_s3 }
13
+
14
+ it { expect(s3.current_path).to eq('s3://mybucket/foo/bar') }
15
+ end
16
+
17
+ context 'when the stack does not exists' do
18
+ it { expect(s3.current_path).to be_empty }
19
+ end
20
+ end
21
+
22
+ describe '#empty?' do
23
+ context 'when the stack exists' do
24
+ before { stack_to_s3 }
25
+
26
+ it { expect(s3.empty?).to be_falsey }
27
+ end
28
+
29
+ context 'when the stack does not exists' do
30
+ it { expect(s3.empty?).to be_truthy }
31
+ end
32
+ end
33
+
34
+ describe '#reset' do
35
+ before do
36
+ stack_to_s3
37
+ s3.reset
38
+ end
39
+
40
+ it { expect(s3.stack).to be_empty }
41
+ end
42
+
43
+ describe '#bucket?' do
44
+ context 'when size of the stack is 1' do
45
+ before { s3.stack << 'mybucket' }
46
+
47
+ it { expect(s3.bucket?).to be_truthy }
48
+ end
49
+
50
+ context 'when size of the stack is greater than 1' do
51
+ before { stack_to_s3 }
52
+
53
+ it { expect(s3.bucket?).to be_falsey }
54
+ end
55
+
56
+ context 'when the stack does not exists' do
57
+ it { expect(s3.bucket?).to be_falsey }
58
+ end
59
+ end
60
+
61
+ describe '#exist?' do
62
+ subject { s3.exist? }
63
+
64
+ context 'when the s3 path is root' do
65
+ it { is_expected.to be_truthy }
66
+ end
67
+
68
+ context 'when the s3 path isn\'t root' do
69
+ let(:error) { double('error') }
70
+ let(:status) { double('status') }
71
+
72
+ before do
73
+ allow(Open3).to receive(:capture3).and_return([output, error, status])
74
+ allow(s3).to receive(:root?).and_return(false)
75
+ end
76
+
77
+ context 'status is success and output exists' do
78
+ let(:output) { 'output' }
79
+
80
+ before do
81
+ allow(status).to receive(:success?).and_return(true)
82
+ end
83
+
84
+ it { is_expected.to be_truthy }
85
+ end
86
+
87
+ context 'status is success and output does not exists' do
88
+ let(:output) { '' }
89
+
90
+ before do
91
+ allow(status).to receive(:success?).and_return(true)
92
+ end
93
+
94
+ it { is_expected.to be_falsey }
95
+ end
96
+
97
+ context 'status is success and current_path is bucket and output exists' do
98
+ let(:output) { '' }
99
+
100
+ before do
101
+ allow(status).to receive(:success?).and_return(true)
102
+ allow(s3).to receive(:bucket?).and_return(true)
103
+ end
104
+
105
+ it { is_expected.to be_truthy }
106
+ end
107
+
108
+ context 'status is not success' do
109
+ before do
110
+ allow(status).to receive(:success?).and_return(false)
111
+ end
112
+
113
+ it { is_expected.to be_falsey }
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'interactive_s3'
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: interactive_s3
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - yamayo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - noorthaven@gmail.com
58
+ executables:
59
+ - is3
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".gitignore"
64
+ - ".rspec"
65
+ - ".travis.yml"
66
+ - Gemfile
67
+ - LICENSE.txt
68
+ - README.md
69
+ - Rakefile
70
+ - bin/is3
71
+ - interactive_s3.gemspec
72
+ - lib/interactive_s3.rb
73
+ - lib/interactive_s3/cli.rb
74
+ - lib/interactive_s3/command_builder.rb
75
+ - lib/interactive_s3/commands/base.rb
76
+ - lib/interactive_s3/commands/internal_command.rb
77
+ - lib/interactive_s3/commands/s3_command.rb
78
+ - lib/interactive_s3/errors.rb
79
+ - lib/interactive_s3/history.rb
80
+ - lib/interactive_s3/interpreter.rb
81
+ - lib/interactive_s3/s3.rb
82
+ - lib/interactive_s3/s3_path.rb
83
+ - lib/interactive_s3/version.rb
84
+ - spec/interactive_s3/command_builder_spec.rb
85
+ - spec/interactive_s3/commands/base_spec.rb
86
+ - spec/interactive_s3/commands/internal_command_spec.rb
87
+ - spec/interactive_s3/commands/s3_command_spec.rb
88
+ - spec/interactive_s3/history_spec.rb
89
+ - spec/interactive_s3/interpreter_spec.rb
90
+ - spec/interactive_s3/s3_path_spec.rb
91
+ - spec/interactive_s3/s3_spec.rb
92
+ - spec/spec_helper.rb
93
+ homepage: ''
94
+ licenses:
95
+ - MIT
96
+ metadata: {}
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 2.2.2
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: An interactive shell for AWS CLI (aws s3)
117
+ test_files:
118
+ - spec/interactive_s3/command_builder_spec.rb
119
+ - spec/interactive_s3/commands/base_spec.rb
120
+ - spec/interactive_s3/commands/internal_command_spec.rb
121
+ - spec/interactive_s3/commands/s3_command_spec.rb
122
+ - spec/interactive_s3/history_spec.rb
123
+ - spec/interactive_s3/interpreter_spec.rb
124
+ - spec/interactive_s3/s3_path_spec.rb
125
+ - spec/interactive_s3/s3_spec.rb
126
+ - spec/spec_helper.rb