instacli 1.0.0
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/LICENSE.txt +21 -0
- data/README.org +135 -0
- data/instacli.gemspec +16 -0
- data/lib/instacli/cli.rb +77 -0
- data/lib/instacli/demuxing.rb +26 -0
- data/lib/instacli/exception_variant.rb +8 -0
- data/lib/instacli/help.rb +48 -0
- data/lib/instacli.rb +12 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 50a85c046d7c6a005f5ae3fb995afb467b83e462
|
4
|
+
data.tar.gz: 16832c6490f6822773c851a5d84f86394a89ae82
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: af9511d7f1c66621428c30f3346b84efff385d8f7eeb4648011c297fa40fead434a12ab53c5575e0593ca6ba1c83f3015dd4f299af3283fbcba95c798159f3b9
|
7
|
+
data.tar.gz: 8d33afe6a36f0e4a1445659cff0b1bb31ce0ef2890e8f40003ee1ba28a98e3acda27cecaf423c51080d2b1ecfb0df3ee36be5291d7790173fa35c34bbebee3ca
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
The MIT License (MIT)
|
3
|
+
Copyright © 2016 Chris Olstrom <chris@olstrom.com>
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the “Software”), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.org
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
#+TITLE: Instant CLI
|
2
|
+
#+LATEX: \pagebreak
|
3
|
+
|
4
|
+
* Overview (just add objects!)
|
5
|
+
|
6
|
+
An unopinionated universal commandline interface for Ruby. Given a collection
|
7
|
+
of Objects, provides a reasonable CLI with automatic help generation.
|
8
|
+
|
9
|
+
* Rationale (why you care)
|
10
|
+
|
11
|
+
Ruby has a number of excellent gems for building commandline applications.
|
12
|
+
They range in complexity, but share a common approach: defining an interface
|
13
|
+
to some objects.
|
14
|
+
|
15
|
+
But... your objects already have interfaces: their =public methods=.
|
16
|
+
|
17
|
+
~instacli~ adopts this perspective when building a CLI.
|
18
|
+
|
19
|
+
* Installation (I can haz?)
|
20
|
+
|
21
|
+
#+BEGIN_SRC shell
|
22
|
+
gem install instacli
|
23
|
+
#+END_SRC
|
24
|
+
|
25
|
+
* Usage (How to use it)
|
26
|
+
|
27
|
+
** Building a CLI for an Object
|
28
|
+
|
29
|
+
Imagine we have an =EchoClass= that implements an =echo(string)= method that
|
30
|
+
returns ~string~. Something like this:
|
31
|
+
|
32
|
+
#+BEGIN_SRC ruby
|
33
|
+
class EchoClass
|
34
|
+
def echo(string)
|
35
|
+
string
|
36
|
+
end
|
37
|
+
end
|
38
|
+
#+END_SRC
|
39
|
+
|
40
|
+
We can add a CLI for it like so:
|
41
|
+
|
42
|
+
#+BEGIN_SRC ruby
|
43
|
+
require 'instacli'
|
44
|
+
|
45
|
+
boring_object = EchoClass.new
|
46
|
+
|
47
|
+
cli = InstaCLI.new boring: boring_object
|
48
|
+
|
49
|
+
cli.execute(*ARGV)
|
50
|
+
#+END_SRC
|
51
|
+
|
52
|
+
Assuming this code is in ~cli.rb~, and ~cli.rb~ is executable, we can use this
|
53
|
+
CLI as follows:
|
54
|
+
|
55
|
+
#+BEGIN_SRC shell
|
56
|
+
./cli.rb boring echo foo
|
57
|
+
#+END_SRC
|
58
|
+
|
59
|
+
And it will output =foo= to =STDOUT=.
|
60
|
+
|
61
|
+
** Autogenerating Documentation
|
62
|
+
|
63
|
+
Using the same ~EchoClass~, we can add help like so:
|
64
|
+
|
65
|
+
#+BEGIN_SRC ruby
|
66
|
+
class CLI < InstaCLI::CLI
|
67
|
+
include InstaCLI::Help
|
68
|
+
end
|
69
|
+
|
70
|
+
cli = CLI.new boring: boring_object
|
71
|
+
|
72
|
+
cli.execute(*ARGV)
|
73
|
+
#+END_SRC
|
74
|
+
|
75
|
+
Now running ~cli.rb~ with ~--help~, like so:
|
76
|
+
|
77
|
+
#+BEGIN_SRC shell
|
78
|
+
./cli.rb --help boring echo
|
79
|
+
#+END_SRC
|
80
|
+
|
81
|
+
Will print the following:
|
82
|
+
|
83
|
+
#+BEGIN_EXAMPLE
|
84
|
+
NAME:
|
85
|
+
cli boring echo - CommandLine Interface
|
86
|
+
|
87
|
+
USAGE:
|
88
|
+
cli boring echo <string>
|
89
|
+
#+END_EXAMPLE
|
90
|
+
|
91
|
+
* How does it work?
|
92
|
+
|
93
|
+
** Mapping Commands to Objects
|
94
|
+
|
95
|
+
A single ~InstaCLI~ can expose as many objects as you like. Just give it a
|
96
|
+
hash, mapping the command names you want to use, to the objects they expose.
|
97
|
+
|
98
|
+
#+BEGIN_SRC ruby
|
99
|
+
mapping = {
|
100
|
+
foo: SomeObject.new,
|
101
|
+
bar: AnotherObject.new
|
102
|
+
}
|
103
|
+
|
104
|
+
InstaCLI::CLI.new mapping
|
105
|
+
#+END_SRC
|
106
|
+
|
107
|
+
** Automatic Subcommands
|
108
|
+
|
109
|
+
Any =public methods= on an object will be exposed as subcommands (ignoring
|
110
|
+
any generic methods from ~Object~ itself).
|
111
|
+
|
112
|
+
** Automatic Help
|
113
|
+
|
114
|
+
Disabled by default, can be enabled with ~include InstaCLI::Help~.
|
115
|
+
|
116
|
+
The parameters of a method are used to generate usage documentation,
|
117
|
+
following the common convention of =<required>= and =[optional]=.
|
118
|
+
|
119
|
+
If a method raises an ~ArugmentError~, a usage notice will be displayed,
|
120
|
+
outlining the correct usage.
|
121
|
+
|
122
|
+
** Command Demultiplexing (aka "that thing =busybox= does")
|
123
|
+
|
124
|
+
Disabled by default, can be enabled with ~include InstaCLI::Demuxing~.
|
125
|
+
|
126
|
+
When enabled, the name the program was invoked with is read as the command to
|
127
|
+
execute.
|
128
|
+
|
129
|
+
* License
|
130
|
+
|
131
|
+
~instacli~ is available under the [[https://tldrlegal.com/license/mit-license][MIT License]]. See ~LICENSE.txt~ for the full text.
|
132
|
+
|
133
|
+
* Contributors
|
134
|
+
|
135
|
+
- [[https://colstrom.github.io/][Chris Olstrom]] | [[mailto:chris@olstrom.com][e-mail]] | [[https://twitter.com/ChrisOlstrom][Twitter]]
|
data/instacli.gemspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
Gem::Specification.new do |gem|
|
2
|
+
gem.name = 'instacli'
|
3
|
+
gem.version = `git describe --tags --abbrev=0`.chomp
|
4
|
+
gem.licenses = 'MIT'
|
5
|
+
gem.authors = ['Chris Olstrom']
|
6
|
+
gem.email = 'chris@olstrom.com'
|
7
|
+
gem.homepage = 'https://github.com/colstrom/instacli'
|
8
|
+
gem.summary = 'Instant CommandLine Interfaces - just add Objects!'
|
9
|
+
|
10
|
+
gem.files = `git ls-files`.split("\n")
|
11
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
12
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
13
|
+
gem.require_paths = ['lib']
|
14
|
+
|
15
|
+
gem.add_runtime_dependency 'contracts', '~> 0.14', '>= 0.14.0'
|
16
|
+
end
|
data/lib/instacli/cli.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'contracts'
|
4
|
+
require_relative 'exception_variant'
|
5
|
+
|
6
|
+
module InstaCLI
|
7
|
+
# Instant CLI - just add Objects!
|
8
|
+
class CLI
|
9
|
+
include ::Contracts::Core
|
10
|
+
include ::Contracts::Builtin
|
11
|
+
|
12
|
+
Contract None => ArrayOf[ExceptionVariant]
|
13
|
+
def rescues
|
14
|
+
[ArgumentError]
|
15
|
+
end
|
16
|
+
|
17
|
+
Contract Maybe[HashOf[Symbol, Object]] => InstaCLI::CLI
|
18
|
+
def initialize(**mapping)
|
19
|
+
@mapping = mapping
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
Contract None => ArrayOf[Symbol]
|
24
|
+
def objects
|
25
|
+
@mapping.keys
|
26
|
+
end
|
27
|
+
|
28
|
+
Contract RespondTo[:to_sym] => Bool
|
29
|
+
def object?(o)
|
30
|
+
object.include? o.to_sym
|
31
|
+
end
|
32
|
+
|
33
|
+
Contract RespondTo[:to_sym] => Object
|
34
|
+
def object(o)
|
35
|
+
@mapping.fetch(o.to_sym) { Object.new }
|
36
|
+
end
|
37
|
+
|
38
|
+
Contract RespondTo[:to_sym] => ArrayOf[Symbol]
|
39
|
+
def methods(o)
|
40
|
+
object(o).methods - Object.methods
|
41
|
+
end
|
42
|
+
|
43
|
+
Contract RespondTo[:to_sym], RespondTo[:to_sym] => Bool
|
44
|
+
def method?(o, m)
|
45
|
+
methods(o).include? m.to_sym
|
46
|
+
end
|
47
|
+
|
48
|
+
Contract RespondTo[:to_sym], RespondTo[:to_sym] => Any
|
49
|
+
def method(o, m)
|
50
|
+
raise ArgumentError unless method? o, m
|
51
|
+
object(o).method(m)
|
52
|
+
end
|
53
|
+
|
54
|
+
Contract RespondTo[:to_sym], RespondTo[:to_sym] => ArrayOf[String]
|
55
|
+
def parameters(o, m)
|
56
|
+
method(o, m)
|
57
|
+
.parameters
|
58
|
+
.map { |p| p.first == :req ? "<#{p.last}>" : "[#{p.last}]" }
|
59
|
+
end
|
60
|
+
|
61
|
+
def help(*)
|
62
|
+
''
|
63
|
+
end
|
64
|
+
|
65
|
+
def execute(*args)
|
66
|
+
return STDERR.puts help(*args[1..-1]) if %w(--help -h).include?(args.first)
|
67
|
+
|
68
|
+
o, m, *rest = *args
|
69
|
+
|
70
|
+
begin
|
71
|
+
STDOUT.puts method(o, m).call(*rest)
|
72
|
+
rescue *rescues
|
73
|
+
STDERR.puts help(*args)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'contracts'
|
2
|
+
|
3
|
+
module InstaCLI
|
4
|
+
# Mixin for routing by invocation (like busybox)
|
5
|
+
module Demuxing
|
6
|
+
include ::Contracts::Core
|
7
|
+
include ::Contracts::Builtin
|
8
|
+
|
9
|
+
Contract None => String
|
10
|
+
def invoked_as
|
11
|
+
File.basename $PROGRAM_NAME
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(*args)
|
15
|
+
return STDERR.puts help(invoked_as, args[1..-1]) if %w(--help -h).include?(args.first)
|
16
|
+
|
17
|
+
m, *rest = *args
|
18
|
+
|
19
|
+
begin
|
20
|
+
STDOUT.puts method(invoked_as, m).call(*rest)
|
21
|
+
rescue *rescues
|
22
|
+
STDERR.puts help(invoked_as, *args)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'contracts'
|
2
|
+
|
3
|
+
module InstaCLI
|
4
|
+
# Usage documentation for --help
|
5
|
+
module Help
|
6
|
+
include ::Contracts::Core
|
7
|
+
include ::Contracts::Builtin
|
8
|
+
|
9
|
+
def header(*args)
|
10
|
+
"NAME:\n\t#{[program, *args].join ' '} - #{description}"
|
11
|
+
end
|
12
|
+
|
13
|
+
Contract None => String
|
14
|
+
def commands
|
15
|
+
['COMMANDS:', *objects].join("\n\t")
|
16
|
+
end
|
17
|
+
|
18
|
+
Contract RespondTo[:to_sym] => String
|
19
|
+
def commands(o)
|
20
|
+
['COMMANDS:', *methods(o)].join("\n\t")
|
21
|
+
end
|
22
|
+
|
23
|
+
Contract RespondTo[:to_sym], RespondTo[:to_sym] => String
|
24
|
+
def usage(o, m)
|
25
|
+
"USAGE:\n\t #{[program, o, m, *parameters(o, m)].join(' ')}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def help(*args)
|
29
|
+
if args.length < 2
|
30
|
+
[header(*args), commands(*args)].join("\n\n")
|
31
|
+
else
|
32
|
+
[header(*args), usage(*args)].join("\n\n")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
Contract None => String
|
39
|
+
def program
|
40
|
+
File.basename $PROGRAM_NAME
|
41
|
+
end
|
42
|
+
|
43
|
+
Contract None => String
|
44
|
+
def description
|
45
|
+
'CommandLine Interface'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/instacli.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
|
2
|
+
require_relative 'instacli/cli'
|
3
|
+
require_relative 'instacli/exception_variant'
|
4
|
+
require_relative 'instacli/help'
|
5
|
+
require_relative 'instacli/demuxing'
|
6
|
+
|
7
|
+
# Convenience Wrapper
|
8
|
+
module InstaCLI
|
9
|
+
def self.new(*args)
|
10
|
+
InstaCLI::CLI.new(*args)
|
11
|
+
end
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: instacli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Olstrom
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-06-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: contracts
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.14'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.14.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.14'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.14.0
|
33
|
+
description:
|
34
|
+
email: chris@olstrom.com
|
35
|
+
executables: []
|
36
|
+
extensions: []
|
37
|
+
extra_rdoc_files: []
|
38
|
+
files:
|
39
|
+
- LICENSE.txt
|
40
|
+
- README.org
|
41
|
+
- instacli.gemspec
|
42
|
+
- lib/instacli.rb
|
43
|
+
- lib/instacli/cli.rb
|
44
|
+
- lib/instacli/demuxing.rb
|
45
|
+
- lib/instacli/exception_variant.rb
|
46
|
+
- lib/instacli/help.rb
|
47
|
+
homepage: https://github.com/colstrom/instacli
|
48
|
+
licenses:
|
49
|
+
- MIT
|
50
|
+
metadata: {}
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 2.5.1
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: Instant CommandLine Interfaces - just add Objects!
|
71
|
+
test_files: []
|
72
|
+
has_rdoc:
|