shelldon 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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