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.
@@ -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
@@ -0,0 +1,13 @@
1
+ module Shelldon
2
+ class Opts
3
+ include Singleton
4
+ attr_reader :opts
5
+ def initialize
6
+ end
7
+
8
+ def set(opts_arr)
9
+ fail StandardError unless @opts.nil?
10
+ @opts ||= opts_arr
11
+ end
12
+ end
13
+ end
@@ -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'
@@ -0,0 +1,3 @@
1
+ module Shelldon
2
+ VERSION = '0.0.1'
3
+ end
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
@@ -0,0 +1,3 @@
1
+ gem 'shelldon', path: '/Users/wwboynton/repositories/shelldon/'
2
+ gem 'getopt'
3
+ gem 'byebug'
@@ -0,0 +1,20 @@
1
+ PATH
2
+ remote: /Users/wwboynton/repositories/shelldon
3
+ specs:
4
+ shelldon (0.0.1)
5
+
6
+ GEM
7
+ specs:
8
+ byebug (8.2.1)
9
+ getopt (1.4.2)
10
+
11
+ PLATFORMS
12
+ ruby
13
+
14
+ DEPENDENCIES
15
+ byebug
16
+ getopt
17
+ shelldon!
18
+
19
+ BUNDLED WITH
20
+ 1.10.6
data/test_shell/run.sh ADDED
@@ -0,0 +1,3 @@
1
+ #! /usr/bin/env bash
2
+ bundle exec ruby test_shell.rb "$@"
3
+
@@ -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