shell_shock 0.0.4 → 0.0.5
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.
- data/README.rdoc +2 -62
- data/example/adventure.rb +79 -0
- data/example/cat.rb +66 -0
- data/example/file_system.rb +95 -0
- data/lib/shell_shock/context.rb +6 -6
- data/lib/shell_shock/logger.rb +3 -2
- metadata +11 -14
data/README.rdoc
CHANGED
|
@@ -14,70 +14,10 @@ A library for creating command line shell application.
|
|
|
14
14
|
|
|
15
15
|
== Usage
|
|
16
16
|
|
|
17
|
-
The basic idea is that you create classes
|
|
17
|
+
The basic idea is that you create classes and include the ShellShock::Context mixin. Start a shell by instantiating the class and executing the push method. Any number of nested shells may be pushed and exit falls back to the previous shell.
|
|
18
18
|
|
|
19
19
|
These classes represent a shell context with a number of commands registered. These commands are matched with tab completion. In addition to registered commands, 'exit', 'quit', 'help' and '?' are always available.
|
|
20
20
|
|
|
21
21
|
Each command must have an execute method that accepts a string (the remaining content after the command name). Commands may also implement usage and help methods. They may also implement a completion method so may have arbitrarily complex completion rules per parameter.
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
require 'shell_shock/context'
|
|
26
|
-
|
|
27
|
-
class CatFileCommand
|
|
28
|
-
def initialize path
|
|
29
|
-
@path = path
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def usage
|
|
33
|
-
'<file name>'
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def help
|
|
37
|
-
'displays the content of a file'
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def completion text
|
|
41
|
-
Dir.glob("#{@path}/#{text}*").select {|f| File.file?(f) }
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def execute path
|
|
45
|
-
File.open(path) {|f| f.each_with_index {|l,i| puts "#{i+1}: #{l}" } }
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
class ChangeDirectoryCommand
|
|
50
|
-
def initialize path
|
|
51
|
-
@path = path
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def usage
|
|
55
|
-
'<directory name>'
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def help
|
|
59
|
-
'switches to a new shell context in the specified directory'
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def completion text
|
|
63
|
-
Dir.glob("#{@path}/#{text}*").select {|f| File.directory?(f) }
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def execute text
|
|
67
|
-
DirectoryContext.new(text).push
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
class DirectoryContext
|
|
72
|
-
include ShellShock::Context
|
|
73
|
-
|
|
74
|
-
def initialize path
|
|
75
|
-
@prompt_text = "#{path} > "
|
|
76
|
-
@commands = {
|
|
77
|
-
'cd' => ChangeDirectoryCommand.new(path),
|
|
78
|
-
'cat' => CatFileCommand.new(path)
|
|
79
|
-
}
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
DirectoryContext.new('.').push
|
|
23
|
+
Refer to the examples directory for some sample shells.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
$: << File.dirname(__FILE__)+'/../lib'
|
|
2
|
+
|
|
3
|
+
require 'shell_shock/context'
|
|
4
|
+
|
|
5
|
+
class MoveCommand
|
|
6
|
+
attr_reader :usage, :help
|
|
7
|
+
|
|
8
|
+
def initialize room
|
|
9
|
+
@room = room
|
|
10
|
+
@usage = '<direction>'
|
|
11
|
+
@help = 'moves to the adjoining room in the specified direction'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def completion text
|
|
15
|
+
@room.connections.keys.grep(/^#{text}/).sort
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def execute direction
|
|
19
|
+
room = @room.connections[direction]
|
|
20
|
+
if room
|
|
21
|
+
AdventureContext.new(room).push
|
|
22
|
+
else
|
|
23
|
+
puts "there is no adjoining room to the #{direction}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class AdventureContext
|
|
29
|
+
include ShellShock::Context
|
|
30
|
+
|
|
31
|
+
def initialize room
|
|
32
|
+
puts room.description
|
|
33
|
+
@prompt = "#{room.name} > "
|
|
34
|
+
add_command MoveCommand.new(room), 'go'
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Room
|
|
39
|
+
attr_reader :name, :description, :connections
|
|
40
|
+
def initialize name, description
|
|
41
|
+
@name, @description = name, description
|
|
42
|
+
@connections = {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def add direction, room
|
|
46
|
+
@connections[direction] = room
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
START = Room.new 'clearing', <<EOF
|
|
51
|
+
You have entered a clearing.
|
|
52
|
+
|
|
53
|
+
A dead goat lies on the ground in front of you
|
|
54
|
+
EOF
|
|
55
|
+
|
|
56
|
+
CAVE_ENTRANCE = Room.new 'cave entrance', <<EOF
|
|
57
|
+
You have arrived at the entrance to a cave.
|
|
58
|
+
|
|
59
|
+
A foul smell is emitting from the cave. Some smoke
|
|
60
|
+
can be seen off off to the east.
|
|
61
|
+
EOF
|
|
62
|
+
|
|
63
|
+
CAMP_SITE = Room.new 'camp site', <<EOF
|
|
64
|
+
You have arrived in a camp site.
|
|
65
|
+
|
|
66
|
+
There is a fire that has been recently put out.
|
|
67
|
+
EOF
|
|
68
|
+
|
|
69
|
+
CAVE = Room.new 'cave', <<EOF
|
|
70
|
+
You have entered a dark cave.
|
|
71
|
+
|
|
72
|
+
A faint growling sound can be heard.
|
|
73
|
+
EOF
|
|
74
|
+
|
|
75
|
+
START.add 'north', CAVE_ENTRANCE
|
|
76
|
+
CAVE_ENTRANCE.add 'east', CAMP_SITE
|
|
77
|
+
CAVE_ENTRANCE.add 'north', CAVE
|
|
78
|
+
|
|
79
|
+
AdventureContext.new(START).push
|
data/example/cat.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require 'shell_shock/context'
|
|
2
|
+
|
|
3
|
+
class Prompt
|
|
4
|
+
def say text=nil
|
|
5
|
+
puts text ? text : ''
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class CatFileCommand
|
|
10
|
+
def initialize path
|
|
11
|
+
@path = path
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def usage
|
|
15
|
+
'<file name>'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def help
|
|
19
|
+
'displays the content of a file'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def completion text
|
|
23
|
+
Dir.glob("#{@path}/#{text}*").select {|f| File.file?(f) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def execute path
|
|
27
|
+
File.open(path) {|f| f.each_with_index {|l,i| puts "#{i+1}: #{l}" } }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class ChangeDirectoryCommand
|
|
32
|
+
def initialize path
|
|
33
|
+
@path = path
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def usage
|
|
37
|
+
'<directory name>'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def help
|
|
41
|
+
'switches to a new shell context in the specified directory'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def completion text
|
|
45
|
+
Dir.glob("#{@path}/#{text}*").select {|f| File.directory?(f) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def execute text
|
|
49
|
+
DirectoryContext.new(text).push
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class DirectoryContext
|
|
54
|
+
include ShellShock::Context
|
|
55
|
+
|
|
56
|
+
def initialize path
|
|
57
|
+
@io = Prompt.new
|
|
58
|
+
@prompt_text = "#{path} > "
|
|
59
|
+
@commands = {
|
|
60
|
+
'cd' => ChangeDirectoryCommand.new(path),
|
|
61
|
+
'cat' => CatFileCommand.new(path)
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
DirectoryContext.new('.').push
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
$: << File.dirname(__FILE__)+'/../lib'
|
|
2
|
+
|
|
3
|
+
require 'shell_shock/context'
|
|
4
|
+
require 'find'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
|
|
7
|
+
class Finder
|
|
8
|
+
attr_reader :files
|
|
9
|
+
|
|
10
|
+
def initialize path
|
|
11
|
+
@path = path
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def refresh pattern
|
|
15
|
+
files = []
|
|
16
|
+
here = Pathname.new(File.expand_path(@path))
|
|
17
|
+
Find.find(File.expand_path(@path)) do |p|
|
|
18
|
+
if File.basename(p).start_with?('.')
|
|
19
|
+
Find.prune
|
|
20
|
+
else
|
|
21
|
+
path = Pathname.new(p).relative_path_from(here).to_s
|
|
22
|
+
files << path if path.start_with?(pattern) and yield(p)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
files
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class CatFileCommand
|
|
30
|
+
include ShellShock::Logger
|
|
31
|
+
|
|
32
|
+
def initialize path
|
|
33
|
+
@finder = Finder.new path
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def usage
|
|
37
|
+
'<file name>'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def help
|
|
41
|
+
'displays the content of a file'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def completion text
|
|
45
|
+
log { "cat command completing \"#{text}\"" }
|
|
46
|
+
@finder.refresh(text) {|path| File.file? path }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def execute path=nil
|
|
50
|
+
return unless path
|
|
51
|
+
File.open(path) {|f| f.each_with_index {|l,i| puts "#{i+1}: #{l}" } }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class ChangeDirectoryCommand
|
|
56
|
+
include ShellShock::Logger
|
|
57
|
+
|
|
58
|
+
def initialize path
|
|
59
|
+
@path = path
|
|
60
|
+
@finder = Finder.new path
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def usage
|
|
64
|
+
'<directory name>'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def help
|
|
68
|
+
'switches to a new shell context in the specified directory'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def completion text
|
|
72
|
+
log { "cd command completing \"#{text}\"" }
|
|
73
|
+
@finder.refresh(text) {|path| path != '.' and File.directory? path }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def execute text=nil
|
|
77
|
+
return unless text
|
|
78
|
+
log { "pushing new shell in \"#{text}\"" }
|
|
79
|
+
DirectoryContext.new(@path+'/'+text).push
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class DirectoryContext
|
|
84
|
+
include ShellShock::Context
|
|
85
|
+
|
|
86
|
+
def initialize path
|
|
87
|
+
@prompt_text = "#{path} > "
|
|
88
|
+
@commands = {
|
|
89
|
+
'cd' => ChangeDirectoryCommand.new(path),
|
|
90
|
+
'cat' => CatFileCommand.new(path)
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
DirectoryContext.new('.').push
|
data/lib/shell_shock/context.rb
CHANGED
|
@@ -20,24 +20,24 @@ module ShellShock
|
|
|
20
20
|
refresh_commands if respond_to?(:refresh_commands)
|
|
21
21
|
Readline.completer_word_break_characters = ''
|
|
22
22
|
Readline.completion_proc = lambda do |string|
|
|
23
|
-
log "trying completion for \"#{string}\""
|
|
23
|
+
log { "trying completion for \"#{string}\"" }
|
|
24
24
|
first, rest = head_tail(string)
|
|
25
|
-
log "split \"#{first}\" from \"#{rest}\""
|
|
25
|
+
log { "split \"#{first}\" from \"#{rest}\"" }
|
|
26
26
|
if first
|
|
27
27
|
command = @commands[first]
|
|
28
28
|
if command
|
|
29
|
-
log "matched #{first} command"
|
|
29
|
+
log { "matched #{first} command" }
|
|
30
30
|
if command.respond_to?(:completion)
|
|
31
31
|
completions = command.completion(rest).map {|c| "#{first} #{c}" }
|
|
32
32
|
else
|
|
33
|
-
log "#{first} has no completion proc"
|
|
33
|
+
log { "#{first} has no completion proc" }
|
|
34
34
|
completions = []
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
completions ||= @commands.keys.grep( /^#{Regexp.escape(first)}/ ).sort
|
|
40
|
-
log "returning #{completions.inspect} completions"
|
|
40
|
+
log { "returning #{completions.inspect} completions" }
|
|
41
41
|
completions
|
|
42
42
|
end
|
|
43
43
|
end
|
|
@@ -65,7 +65,7 @@ module ShellShock
|
|
|
65
65
|
line = Readline.readline(@prompt, true)
|
|
66
66
|
if line
|
|
67
67
|
first, rest = head_tail(line)
|
|
68
|
-
log "looking for command \"#{first}\" with parameter \"#{rest}\""
|
|
68
|
+
log { "looking for command \"#{first}\" with parameter \"#{rest}\"" }
|
|
69
69
|
if @commands[first]
|
|
70
70
|
@commands[first].execute rest
|
|
71
71
|
else
|
data/lib/shell_shock/logger.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shell_shock
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
hash: 23
|
|
5
4
|
prerelease: false
|
|
6
5
|
segments:
|
|
7
6
|
- 0
|
|
8
7
|
- 0
|
|
9
|
-
-
|
|
10
|
-
version: 0.0.
|
|
8
|
+
- 5
|
|
9
|
+
version: 0.0.5
|
|
11
10
|
platform: ruby
|
|
12
11
|
authors:
|
|
13
12
|
- Mark Ryall
|
|
@@ -15,7 +14,7 @@ autorequire:
|
|
|
15
14
|
bindir: bin
|
|
16
15
|
cert_chain: []
|
|
17
16
|
|
|
18
|
-
date:
|
|
17
|
+
date: 2011-01-10 00:00:00 +10:00
|
|
19
18
|
default_executable:
|
|
20
19
|
dependencies:
|
|
21
20
|
- !ruby/object:Gem::Dependency
|
|
@@ -26,7 +25,6 @@ dependencies:
|
|
|
26
25
|
requirements:
|
|
27
26
|
- - ~>
|
|
28
27
|
- !ruby/object:Gem::Version
|
|
29
|
-
hash: 49
|
|
30
28
|
segments:
|
|
31
29
|
- 0
|
|
32
30
|
- 8
|
|
@@ -42,7 +40,6 @@ dependencies:
|
|
|
42
40
|
requirements:
|
|
43
41
|
- - ~>
|
|
44
42
|
- !ruby/object:Gem::Version
|
|
45
|
-
hash: 23
|
|
46
43
|
segments:
|
|
47
44
|
- 0
|
|
48
45
|
- 0
|
|
@@ -58,12 +55,11 @@ dependencies:
|
|
|
58
55
|
requirements:
|
|
59
56
|
- - ~>
|
|
60
57
|
- !ruby/object:Gem::Version
|
|
61
|
-
hash: 13
|
|
62
58
|
segments:
|
|
63
59
|
- 2
|
|
60
|
+
- 4
|
|
64
61
|
- 0
|
|
65
|
-
|
|
66
|
-
version: 2.0.1
|
|
62
|
+
version: 2.4.0
|
|
67
63
|
type: :development
|
|
68
64
|
version_requirements: *id003
|
|
69
65
|
description: |
|
|
@@ -79,11 +75,14 @@ extensions: []
|
|
|
79
75
|
extra_rdoc_files: []
|
|
80
76
|
|
|
81
77
|
files:
|
|
82
|
-
-
|
|
78
|
+
- example/adventure.rb
|
|
79
|
+
- example/cat.rb
|
|
80
|
+
- example/file_system.rb
|
|
81
|
+
- lib/shell_shock/command_spec.rb
|
|
82
|
+
- lib/shell_shock/context.rb
|
|
83
83
|
- lib/shell_shock/exit_command.rb
|
|
84
84
|
- lib/shell_shock/help_command.rb
|
|
85
|
-
- lib/shell_shock/
|
|
86
|
-
- lib/shell_shock/command_spec.rb
|
|
85
|
+
- lib/shell_shock/logger.rb
|
|
87
86
|
- README.rdoc
|
|
88
87
|
- MIT-LICENSE
|
|
89
88
|
has_rdoc: true
|
|
@@ -100,7 +99,6 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
100
99
|
requirements:
|
|
101
100
|
- - ">="
|
|
102
101
|
- !ruby/object:Gem::Version
|
|
103
|
-
hash: 3
|
|
104
102
|
segments:
|
|
105
103
|
- 0
|
|
106
104
|
version: "0"
|
|
@@ -109,7 +107,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
109
107
|
requirements:
|
|
110
108
|
- - ">="
|
|
111
109
|
- !ruby/object:Gem::Version
|
|
112
|
-
hash: 3
|
|
113
110
|
segments:
|
|
114
111
|
- 0
|
|
115
112
|
version: "0"
|