shelldon 0.0.1
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 +7 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +43 -0
- data/LICENSE.txt +21 -0
- data/README.md +138 -0
- data/Rakefile +1 -0
- data/bin/shelldon +8 -0
- data/build_and_install.sh +3 -0
- data/lib/Exceptions/error_factory.rb +26 -0
- data/lib/cli.rb +19 -0
- data/lib/command/command.rb +134 -0
- data/lib/command/command_list.rb +72 -0
- data/lib/config/config.rb +85 -0
- data/lib/config/config_factory.rb +41 -0
- data/lib/config/param.rb +54 -0
- data/lib/config/param/boolean_param.rb +28 -0
- data/lib/config/param/number_param.rb +7 -0
- data/lib/config/param/string_param.rb +8 -0
- data/lib/config/param_factory.rb +75 -0
- data/lib/defaults/commands.rb +0 -0
- data/lib/dsl.rb +21 -0
- data/lib/file_management/config_file_manager.rb +34 -0
- data/lib/file_management/file_manager.rb +20 -0
- data/lib/file_management/history_file.rb +34 -0
- data/lib/file_management/yaml_manager.rb +16 -0
- data/lib/helpers/timer.rb +17 -0
- data/lib/opts/opt_factory.rb +30 -0
- data/lib/opts/opts.rb +13 -0
- data/lib/shell/shell.rb +167 -0
- data/lib/shell/shell_factory.rb +38 -0
- data/lib/shell/shell_index.rb +33 -0
- data/lib/shelldon.rb +26 -0
- data/lib/shelldon/version.rb +3 -0
- data/shelldon.gemspec +28 -0
- data/test_shell/Gemfile +3 -0
- data/test_shell/Gemfile.lock +20 -0
- data/test_shell/run.sh +3 -0
- data/test_shell/simple_shell.rb +106 -0
- data/test_shell/test_shell.rb +82 -0
- data/test_shell/useful_commands.rb +11 -0
- metadata +130 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module Shelldon
|
2
|
+
class Timer
|
3
|
+
def initialize(strftime = '%s')
|
4
|
+
@strftime = strftime
|
5
|
+
end
|
6
|
+
|
7
|
+
def start
|
8
|
+
@start = Time.now
|
9
|
+
end
|
10
|
+
|
11
|
+
def stop
|
12
|
+
res = (Time.now - @start) * 1000
|
13
|
+
@start = nil
|
14
|
+
Time.at(res).strftime(@strftime)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Shelldon
|
2
|
+
class OptFactory
|
3
|
+
def initialize(&block)
|
4
|
+
@opt_arr = []
|
5
|
+
instance_eval(&block)
|
6
|
+
Shelldon.opts = get_opts
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_opts
|
10
|
+
@opt_arr.empty? ? [] : Getopt::Long.getopts(*@opt_arr)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def opt(*args, type)
|
16
|
+
@opt_arr << [args.first, args[1] || '', getopt_constant(type)]
|
17
|
+
end
|
18
|
+
|
19
|
+
def getopt_constant(sym)
|
20
|
+
case sym.to_sym
|
21
|
+
when :boolean
|
22
|
+
Getopt::BOOLEAN
|
23
|
+
when :required
|
24
|
+
Getopt::REQUIRED
|
25
|
+
else
|
26
|
+
fail StandardError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/opts/opts.rb
ADDED
data/lib/shell/shell.rb
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'byebug'
|
2
|
+
require 'readline'
|
3
|
+
|
4
|
+
module Shelldon
|
5
|
+
class Shell
|
6
|
+
attr_accessor :command_list, :config,
|
7
|
+
:accept_errors, :reject_errors
|
8
|
+
attr_reader :home, :name
|
9
|
+
attr_writer :history, :history_file
|
10
|
+
|
11
|
+
def initialize(name, &block)
|
12
|
+
@accept_errors = {}
|
13
|
+
@reject_errors = {}
|
14
|
+
@name = name
|
15
|
+
@errors = {}
|
16
|
+
@history = true
|
17
|
+
@command_list = CommandList.new(self)
|
18
|
+
@config = Config.new(self)
|
19
|
+
setup(&block) if block_given?
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.method_missing(meth_name, *args, &block)
|
23
|
+
Shelldon::ShellIndex[:default].send(meth_name, *args, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def setup(&block)
|
27
|
+
instance_eval(&block)
|
28
|
+
FileUtils.mkdir_p(@home.to_s) unless File.exist?(@home)
|
29
|
+
Dir.chdir(@home) if @home
|
30
|
+
Readline.completion_proc = proc { [] }
|
31
|
+
if @history_path && @history
|
32
|
+
@history_helper = Shelldon::HistoryFile.new(@history_path)
|
33
|
+
@history_helper.load if @history_helper
|
34
|
+
end
|
35
|
+
@config.load_config_file
|
36
|
+
run
|
37
|
+
end
|
38
|
+
|
39
|
+
def quit
|
40
|
+
@history_helper.save if @history_helper
|
41
|
+
puts "\n"
|
42
|
+
exit 0
|
43
|
+
end
|
44
|
+
|
45
|
+
def run
|
46
|
+
instance_eval(&@startup) if @startup
|
47
|
+
begin
|
48
|
+
run_repl
|
49
|
+
rescue *@accept_errors.keys => e
|
50
|
+
print_error(e)
|
51
|
+
on_error(e, @accept_errors[e.class])
|
52
|
+
retry
|
53
|
+
rescue *@reject_errors.keys => e
|
54
|
+
print_error(e)
|
55
|
+
on_error(e, @reject_errors[e.class])
|
56
|
+
rescue StandardError => e
|
57
|
+
print_error(e)
|
58
|
+
puts "Reached fatal error. G'bye!"
|
59
|
+
raise e
|
60
|
+
ensure
|
61
|
+
instance_eval(&@shutdown) if @shutdown
|
62
|
+
@history_helper.save if @history_helper
|
63
|
+
quit
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def on_error(e, proc)
|
68
|
+
instance_exec(e, &proc)
|
69
|
+
end
|
70
|
+
|
71
|
+
def print_error(e)
|
72
|
+
return false unless @config[:debug_mode]
|
73
|
+
puts e.message
|
74
|
+
puts e.backtrace.join("\n")
|
75
|
+
end
|
76
|
+
|
77
|
+
def get_prompt
|
78
|
+
if @prompt_setter
|
79
|
+
instance_eval(&@prompt_setter)
|
80
|
+
else
|
81
|
+
@prompt || '> '
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def run_repl
|
86
|
+
while cmd = Readline.readline(get_prompt, true)
|
87
|
+
run_commands(cmd)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def opts
|
92
|
+
@config.opts
|
93
|
+
end
|
94
|
+
|
95
|
+
def opts=(arr)
|
96
|
+
@config.opts = arr
|
97
|
+
end
|
98
|
+
|
99
|
+
def run_commands(commands)
|
100
|
+
commands.split(';').each { |cmd| run_command(cmd) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def run_command(cmd)
|
104
|
+
@history_helper << cmd
|
105
|
+
run_precommand(cmd)
|
106
|
+
@command_list.run(cmd)
|
107
|
+
run_postcommand(cmd)
|
108
|
+
end
|
109
|
+
|
110
|
+
def run_precommand(cmd)
|
111
|
+
instance_exec(cmd, &@pre_command) if @pre_command
|
112
|
+
end
|
113
|
+
|
114
|
+
def run_postcommand(cmd)
|
115
|
+
instance_exec(cmd, &@post_command) if @post_command
|
116
|
+
end
|
117
|
+
|
118
|
+
def init(&block)
|
119
|
+
instance_eval(&block)
|
120
|
+
end
|
121
|
+
|
122
|
+
# DSL
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def history(history)
|
127
|
+
@history = history
|
128
|
+
end
|
129
|
+
|
130
|
+
def history_file(file)
|
131
|
+
@history_path = file
|
132
|
+
end
|
133
|
+
|
134
|
+
def prompt(str = '> ', &block)
|
135
|
+
if block_given?
|
136
|
+
@prompt_setter = block
|
137
|
+
else
|
138
|
+
@prompt = str
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def shutdown(&block)
|
143
|
+
@shutdown = block
|
144
|
+
end
|
145
|
+
|
146
|
+
def startup(&block)
|
147
|
+
@startup = block
|
148
|
+
end
|
149
|
+
|
150
|
+
def pre_command(&block)
|
151
|
+
@pre_command = block
|
152
|
+
end
|
153
|
+
|
154
|
+
def post_command(&block)
|
155
|
+
@post_command = block
|
156
|
+
end
|
157
|
+
|
158
|
+
def home(path)
|
159
|
+
@home = Pathname.new(path).expand_path
|
160
|
+
end
|
161
|
+
|
162
|
+
def errors(&block)
|
163
|
+
@accept_errors, @reject_errors =
|
164
|
+
Shelldon::ErrorFactory.new(&block).get
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Shelldon
|
2
|
+
class ShellFactory
|
3
|
+
def initialize(name, &block)
|
4
|
+
if Shelldon[name]
|
5
|
+
@shell = Shelldon[name]
|
6
|
+
else
|
7
|
+
@shell = Shell.new(name)
|
8
|
+
end
|
9
|
+
instance_eval(&block)
|
10
|
+
end
|
11
|
+
|
12
|
+
def register
|
13
|
+
Shelldon::ShellIndex << @shell
|
14
|
+
end
|
15
|
+
|
16
|
+
def shell(&block)
|
17
|
+
@shell.setup(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def command(name, &block)
|
21
|
+
cmd = Shelldon::Command.new(name, @shell.command_list, &block)
|
22
|
+
@shell.command_list.register(cmd)
|
23
|
+
end
|
24
|
+
|
25
|
+
def config(&block)
|
26
|
+
Shelldon::ConfigFactory.create(@shell, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def opts(&block)
|
30
|
+
OptFactory.new(&block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def command_missing(&block)
|
34
|
+
cmd = Shelldon::Command.new(:not_found, @shell, &block)
|
35
|
+
@shell.command_list.register_default(cmd)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# This allows subshelling - You can define multiple shells in one program and nest them.
|
2
|
+
# This is the opposite approach to shell-as-singleton, which makes access easy but prevents multiple shells.
|
3
|
+
# This is why all of the factories pass the shell parent into their respective products.
|
4
|
+
|
5
|
+
module Shelldon
|
6
|
+
class ShellIndex
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
def self.method_missing(meth_name, *args, &block)
|
10
|
+
if block_given?
|
11
|
+
instance.send(meth_name, *args, &block)
|
12
|
+
else
|
13
|
+
instance.send(meth_name, *args)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@shell_index = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def [](key)
|
22
|
+
@shell_index[key.to_sym]
|
23
|
+
end
|
24
|
+
|
25
|
+
def <<(shell)
|
26
|
+
if shell.is_a?(Shelldon::Shell)
|
27
|
+
@shell_index[shell.name] = shell
|
28
|
+
else
|
29
|
+
fail StandardError
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/shelldon.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'shelldon/version'
|
2
|
+
require 'singleton'
|
3
|
+
require 'readline'
|
4
|
+
require 'getopt/long'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
require 'shell/shell'
|
8
|
+
require 'config/config'
|
9
|
+
require 'config/param'
|
10
|
+
require 'config/param/string_param'
|
11
|
+
require 'config/param/boolean_param'
|
12
|
+
require 'config/param/number_param'
|
13
|
+
require 'dsl'
|
14
|
+
require 'shell/shell_index'
|
15
|
+
require 'shell/shell_factory'
|
16
|
+
require 'config/config_factory'
|
17
|
+
require 'opts/opt_factory'
|
18
|
+
require 'config/param_factory'
|
19
|
+
require 'command/command'
|
20
|
+
require 'command/command_list'
|
21
|
+
require 'file_management/file_manager'
|
22
|
+
require 'file_management/yaml_manager'
|
23
|
+
require 'file_management/config_file_manager'
|
24
|
+
require 'exceptions/error_factory'
|
25
|
+
require 'opts/opts'
|
26
|
+
require 'file_management/history_file'
|
data/shelldon.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'shelldon/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'shelldon'
|
8
|
+
spec.version = Shelldon::VERSION
|
9
|
+
spec.authors = ['Wesley Boynton']
|
10
|
+
spec.email = ['wes@boynton.io']
|
11
|
+
spec.homepage = "https://github.com/wwboynton/shelldon"
|
12
|
+
|
13
|
+
spec.summary = 'An expressive DSL for building interactive command-line apps'
|
14
|
+
spec.description = "Shelldon is an expressive DSL for build interactive command-line apps (REPLs)\
|
15
|
+
with minimal effort. It supports all kinds of fun features, like config/history management, \
|
16
|
+
error handling, subcommands, subshells, and more!"
|
17
|
+
spec.license = 'MIT'
|
18
|
+
|
19
|
+
|
20
|
+
spec.files = Dir["#{File.dirname(__FILE__)}/**/**/**/**/*"].reject { |f| f.match(%r{^(test|spec|features)/}) }
|
21
|
+
spec.bindir = 'bin'
|
22
|
+
spec.executables = 'shelldon'
|
23
|
+
spec.require_paths = %w(lib bin)
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.10'
|
26
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
27
|
+
spec.add_development_dependency 'rubocop', '~> 0.33.0'
|
28
|
+
end
|
data/test_shell/Gemfile
ADDED
data/test_shell/run.sh
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'shelldon'
|
2
|
+
require 'pp'
|
3
|
+
|
4
|
+
Shelldon.shell do
|
5
|
+
opts do
|
6
|
+
opt '--myopt', '-m', :boolean
|
7
|
+
end
|
8
|
+
|
9
|
+
config do
|
10
|
+
# Set the config file, which can hold values one level higher than their default
|
11
|
+
# The order of precedence for params is: Set in-session > set by command-line flag > set by config file > default
|
12
|
+
config_file '.my-config-file'
|
13
|
+
|
14
|
+
param :myparam do # Create a config option (a 'param')
|
15
|
+
type :boolean # Make it a boolean
|
16
|
+
default false # Make its default value false
|
17
|
+
opt 'myopt' # Override it with the value of command-line opt '--myopt' if present
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Define a command that sets a config option
|
22
|
+
command :set do
|
23
|
+
# Set up some help information for the command
|
24
|
+
help 'Set a configuration option for the remainder of the session.'
|
25
|
+
examples ['set myparam']
|
26
|
+
usage 'set [config_option]'
|
27
|
+
|
28
|
+
# Define the command's action - this has access to some helpers, like 'config'
|
29
|
+
# You can also give the block access to the remaining (unusued) tokens of the command
|
30
|
+
# "Unused" meaning tokens that weren't used up to call the command in the first place
|
31
|
+
action do |args|
|
32
|
+
tokens = args.split(' ')
|
33
|
+
config[tokens[0].to_sym] = tokens[1]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Here's a simplification of grabbing args for use in an action
|
39
|
+
command :arg do
|
40
|
+
help 'Show your args off!'
|
41
|
+
action { |args| puts args }
|
42
|
+
end
|
43
|
+
|
44
|
+
# This command will show the active config if called witout args, or
|
45
|
+
# show the value of a specific option if called with an argument
|
46
|
+
command :config do
|
47
|
+
help 'Show the configuration of the current session.'
|
48
|
+
usage 'config'
|
49
|
+
action do |args|
|
50
|
+
if args.empty?
|
51
|
+
pp config.to_a
|
52
|
+
else
|
53
|
+
param = config.find(args.to_sym)
|
54
|
+
puts "#{param.name}: #{param.val}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# This is a subcommand - it will automatically take precedence
|
59
|
+
# if you call a command beginning with "config save"
|
60
|
+
subcommand :save do
|
61
|
+
help 'Save your current configuration'
|
62
|
+
usage 'config save'
|
63
|
+
action { config.save }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# This will show all that nice help information we've been defining.
|
69
|
+
# This produces a two-dimensional array, so you could make it into a table with some
|
70
|
+
# table-printing gem if you wanted.
|
71
|
+
command :help do
|
72
|
+
action { |args| pp command_list.help(args) }
|
73
|
+
help 'Show help. Optionally specify specific command for more information.'
|
74
|
+
usage 'help [cmd]'
|
75
|
+
examples ['help', 'help quit']
|
76
|
+
end
|
77
|
+
|
78
|
+
# Define a default command - This is what happens when a command doesn't match up
|
79
|
+
command_missing do
|
80
|
+
action { |cmd| puts "No such command \"#{cmd}\"" }
|
81
|
+
end
|
82
|
+
|
83
|
+
# LASTLY, define some basic shell properties. The shell will run at the end of this block.
|
84
|
+
shell do
|
85
|
+
# You can make your prompt a string or a block
|
86
|
+
prompt 'shelldon> ' # This is okay
|
87
|
+
prompt { "shelldon#{4+2}>" } # This is okay too
|
88
|
+
|
89
|
+
# This is the "home" directory of your shell, used for config files, history files, etc.
|
90
|
+
home '~/.my-test'
|
91
|
+
|
92
|
+
# Enable in-session history (enabled by default)
|
93
|
+
history true
|
94
|
+
|
95
|
+
# Enable history logging and reloading between sessions
|
96
|
+
history_file '.my-history'
|
97
|
+
|
98
|
+
# Error handling - You can 'accept' an error or 'reject' it.
|
99
|
+
# The only difference is an accepted error won't kill the shell.
|
100
|
+
# You can also pass a block to run when that specific command is caught.
|
101
|
+
errors do
|
102
|
+
reject StandardError
|
103
|
+
accept(Interrupt) { puts '^C' }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|