lightning 0.2.1 → 0.3.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.
- data/CHANGELOG.rdoc +9 -0
- data/README.rdoc +53 -125
- data/Rakefile +14 -40
- data/bin/lightning +4 -0
- data/bin/lightning-complete +1 -10
- data/bin/lightning-translate +4 -0
- data/lib/lightning.rb +36 -50
- data/lib/lightning/bolt.rb +53 -26
- data/lib/lightning/builder.rb +87 -0
- data/lib/lightning/commands.rb +92 -69
- data/lib/lightning/commands/bolt.rb +63 -0
- data/lib/lightning/commands/core.rb +57 -0
- data/lib/lightning/commands/function.rb +76 -0
- data/lib/lightning/commands/shell_command.rb +38 -0
- data/lib/lightning/commands_util.rb +75 -0
- data/lib/lightning/completion.rb +72 -28
- data/lib/lightning/completion_map.rb +42 -39
- data/lib/lightning/config.rb +92 -57
- data/lib/lightning/function.rb +70 -0
- data/lib/lightning/generator.rb +77 -43
- data/lib/lightning/generators.rb +53 -0
- data/lib/lightning/generators/misc.rb +12 -0
- data/lib/lightning/generators/ruby.rb +32 -0
- data/lib/lightning/util.rb +70 -0
- data/lib/lightning/version.rb +3 -0
- data/test/bolt_test.rb +16 -28
- data/test/builder_test.rb +54 -0
- data/test/commands_test.rb +98 -0
- data/test/completion_map_test.rb +31 -54
- data/test/completion_test.rb +106 -36
- data/test/config_test.rb +22 -56
- data/test/function_test.rb +90 -0
- data/test/generator_test.rb +73 -0
- data/test/lightning.yml +26 -34
- data/test/test_helper.rb +80 -15
- metadata +42 -20
- data/VERSION.yml +0 -4
- data/bin/lightning-full_path +0 -18
- data/bin/lightning-install +0 -7
- data/lib/lightning/bolts.rb +0 -12
- data/lightning.yml.example +0 -87
- data/lightning_completions.example +0 -147
- data/test/lightning_test.rb +0 -58
@@ -0,0 +1,70 @@
|
|
1
|
+
module Lightning
|
2
|
+
# A Function object represents a shell function which wraps around a shell command and a {Bolt}.
|
3
|
+
# This shell function autocompletes bolt paths by their basenames and translates arguments that
|
4
|
+
# are these basenames to their full paths.
|
5
|
+
#
|
6
|
+
# == Argument Translation
|
7
|
+
# Before executing its shell command, a function checks each argument to see if it can translate it.
|
8
|
+
# Translation is done if the argument matches the basename of one its bolt's paths.
|
9
|
+
# $ echo-ruby irb.rb
|
10
|
+
# /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/irb.rb.
|
11
|
+
#
|
12
|
+
# For translation to occur, the full basename must match. The only exception to this is when using
|
13
|
+
# lightning's own filename expansion syntax: a '..' at the end of an argument expands the argument
|
14
|
+
# with all completions that matched up to '..'. For example:
|
15
|
+
# $ echo-ruby ad[TAB]
|
16
|
+
# address.rb addressbook.rb
|
17
|
+
# $ echo-ruby ad..
|
18
|
+
# /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/osx/addressbook.rb
|
19
|
+
# /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/wsdl/soap/address.rb
|
20
|
+
#
|
21
|
+
# This expansion of any bolt paths combined with regex completion makes for a powerfully quick
|
22
|
+
# way of typing paths.
|
23
|
+
class Function
|
24
|
+
ATTRIBUTES = :name, :post_path, :shell_command, :bolt, :desc
|
25
|
+
attr_accessor *ATTRIBUTES
|
26
|
+
def initialize(hash)
|
27
|
+
raise ArgumentError, "Function must have a name and bolt" unless hash['name'] && hash['bolt']
|
28
|
+
hash.each do |k,v|
|
29
|
+
instance_variable_set("@#{k}", v)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Array] All possible completions
|
34
|
+
def completions
|
35
|
+
completion_map.keys
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Array] Globs used to create {Function#completion_map completion_map}
|
39
|
+
def globs
|
40
|
+
@globs ||= @bolt.globs
|
41
|
+
end
|
42
|
+
|
43
|
+
# User-defined aliases for any path. Defaults to its bolt's aliases.
|
44
|
+
# @return [Hash] Maps aliases to full paths
|
45
|
+
def aliases
|
46
|
+
@aliases ||= @bolt.aliases
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [CompletionMap] Map of basenames to full paths used in completion
|
50
|
+
def completion_map
|
51
|
+
@completion_map ||= CompletionMap.new(globs, :aliases=>aliases)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Array] Translates function's arguments
|
55
|
+
def translate(args)
|
56
|
+
translated = Array(args).map {|arg|
|
57
|
+
!completion_map[arg] && (new_arg = arg[/^(.*)\.\.$/,1]) ?
|
58
|
+
Completion.complete(new_arg, self, false) : arg
|
59
|
+
}.flatten.map {|arg|
|
60
|
+
new_arg = completion_map[arg] || arg.dup
|
61
|
+
new_arg << @post_path if @post_path && new_arg != arg
|
62
|
+
if new_arg == arg && (dir = new_arg[/^([^\/]+)\//,1]) && (full_dir = completion_map[dir])
|
63
|
+
new_arg.sub!(dir, full_dir)
|
64
|
+
new_arg = File.expand_path(new_arg)
|
65
|
+
end
|
66
|
+
new_arg
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/lightning/generator.rb
CHANGED
@@ -1,48 +1,82 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Lightning
|
2
|
+
# Generates globs for bolts using methods defined in {Generators}.
|
3
|
+
# Generated bolts are inserted under Lightning.config[:bolts].
|
4
|
+
# Users can define their own generators with {Generators generator plugins}.
|
3
5
|
class Generator
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
end
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
6
|
+
DEFAULT_GENERATORS = %w{gem ruby local_ruby wild}
|
7
|
+
|
8
|
+
# @return [Hash] Maps generators to their descriptions
|
9
|
+
def self.generators
|
10
|
+
load_plugins
|
11
|
+
Generators.generators
|
12
|
+
end
|
13
|
+
|
14
|
+
# Runs generators
|
15
|
+
# @param [Array<String>] Generators instance methods
|
16
|
+
# @param [Hash] options
|
17
|
+
# @option options [String] :once Generator to run once
|
18
|
+
# @option options [Boolean] :test Runs generators in test mode which only displays
|
19
|
+
# generated globs and doesn't save them
|
20
|
+
def self.run(gens=[], options={})
|
21
|
+
load_plugins
|
22
|
+
new.run(gens, options)
|
23
|
+
rescue
|
24
|
+
$stderr.puts "Error: #{$!.message}"
|
25
|
+
end
|
26
|
+
|
27
|
+
# Loads default and user generator plugins
|
28
|
+
def self.load_plugins
|
29
|
+
@loaded ||= begin
|
30
|
+
Util.load_plugins File.dirname(__FILE__), 'generators'
|
31
|
+
Util.load_plugins Lightning.dir, 'generators'
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Object used to call generator(s)
|
37
|
+
attr_reader :underling
|
38
|
+
def initialize
|
39
|
+
@underling = Object.new.extend(Generators)
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [nil, true] Main method which runs generators
|
43
|
+
def run(gens, options)
|
44
|
+
if options.key?(:once)
|
45
|
+
run_once(gens, options)
|
46
|
+
else
|
47
|
+
gens = DEFAULT_GENERATORS if Array(gens).empty?
|
48
|
+
gens = Hash[*gens.zip(gens).flatten] if gens.is_a?(Array)
|
49
|
+
generate_bolts gens
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
def run_once(bolt, options)
|
55
|
+
generator = options[:once] || bolt
|
56
|
+
if options[:test]
|
57
|
+
puts Config.bolt(Array(call_generator(generator)))['globs']
|
58
|
+
else
|
59
|
+
if generate_bolts(bolt=>generator)
|
60
|
+
puts "Generated following globs for bolt '#{bolt}':"
|
61
|
+
puts Lightning.config.bolts[bolt]['globs'].map {|e| " "+e }
|
62
|
+
true
|
63
|
+
end
|
43
64
|
end
|
44
|
-
body.gsub(/^\s{6,10}/, '')
|
45
65
|
end
|
66
|
+
|
67
|
+
def generate_bolts(bolts)
|
68
|
+
results = bolts.map {|bolt, gen|
|
69
|
+
(globs = call_generator(gen)) && Lightning.config.bolts[bolt.to_s] = Config.bolt(globs)
|
70
|
+
}
|
71
|
+
Lightning.config.save if results.any?
|
72
|
+
results.all?
|
73
|
+
end
|
74
|
+
|
75
|
+
def call_generator(gen)
|
76
|
+
raise "Generator method doesn't exist." unless @underling.respond_to?(gen)
|
77
|
+
Array(@underling.send(gen)).map {|e| e.to_s }
|
78
|
+
rescue
|
79
|
+
$stdout.puts "Generator '#{gen}' failed with: #{$!.message}"
|
46
80
|
end
|
47
81
|
end
|
48
|
-
end
|
82
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Lightning
|
2
|
+
# This module contains methods which are used to generate bolts with 'lightning bolt generate'.
|
3
|
+
# Each method should return an array of bolt globs. The name of the method is the name given to the bolt.
|
4
|
+
#
|
5
|
+
# == Generator Plugins
|
6
|
+
# Generator plugins are a way for users to define and share generators.
|
7
|
+
# A generator plugin is a .rb file in ~/.lightning/generators/. Each plugin can have multiple
|
8
|
+
# generators since a generator is just a method in Lightning::Generators.
|
9
|
+
#
|
10
|
+
# A sample generator plugin looks like this:
|
11
|
+
# module Lightning::Generators
|
12
|
+
# desc "Files in $PATH"
|
13
|
+
# def bin
|
14
|
+
# ENV['PATH'].split(":").uniq.map {|e| "#{e}/*" }
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# To register a generator, {Generators.desc desc} must be placed before a method and given a generator
|
19
|
+
# description. A generator should produce an array of globs. If a generator is to be shared with others
|
20
|
+
# it should dynamically generate filesystem-specific globs based on environment variables and commands.
|
21
|
+
# Generated globs don't have to expand '~' as lightning expands that automatically to the user's home.
|
22
|
+
#
|
23
|
+
# For generator plugin examples
|
24
|
+
# {read the source}[http://github.com/cldwalker/lightning/tree/master/lib/lightning/generators/].
|
25
|
+
module Generators
|
26
|
+
# @return [Hash] Maps generators to their descriptions
|
27
|
+
def self.generators
|
28
|
+
@desc ||= {}
|
29
|
+
end
|
30
|
+
|
31
|
+
# Used before a generator method to give it a description
|
32
|
+
def self.desc(arg)
|
33
|
+
@next_desc = arg
|
34
|
+
end
|
35
|
+
|
36
|
+
# Overridden for generators to error elegantly when a generator calls a shell command that
|
37
|
+
# doesn't exist
|
38
|
+
def `(*args)
|
39
|
+
cmd = args[0].split(/\s+/)[0] || ''
|
40
|
+
if Util.shell_command_exists?(cmd)
|
41
|
+
Kernel.`(*args)
|
42
|
+
else
|
43
|
+
raise "Command '#{cmd}' doesn't exist."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def self.method_added(meth)
|
49
|
+
generators[meth.to_s] = @next_desc if @next_desc
|
50
|
+
@next_desc = nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Lightning::Generators
|
2
|
+
protected
|
3
|
+
desc "*ALL* files and directories under the current directory. Careful where you do this."
|
4
|
+
def wild
|
5
|
+
["**/*"]
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "Files in $PATH"
|
9
|
+
def bin
|
10
|
+
ENV['PATH'].split(":").uniq.map {|e| "#{e}/*" }
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Lightning::Generators
|
2
|
+
protected
|
3
|
+
desc "Directories of gems"
|
4
|
+
def gem
|
5
|
+
`gem environment path`.chomp.split(":").map {|e| e +"/gems/*" }
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "System ruby files"
|
9
|
+
def ruby
|
10
|
+
system_ruby.map {|e| e +"/**/*.{rb,bundle,so,c}"}
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Files in a rails project"
|
14
|
+
def rails
|
15
|
+
["{app,config,lib}/**/*", "{db}/**/*.rb"]
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "*ALL* local ruby files. Careful where you do this."
|
19
|
+
def local_ruby
|
20
|
+
["**/*.rb", "bin/*"]
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "Test or spec files in a ruby project"
|
24
|
+
def test_ruby
|
25
|
+
['{spec,test}/**/*_{test,spec}.rb', '{spec,test}/**/{test,spec}_*.rb', 'spec/**/*.spec']
|
26
|
+
end
|
27
|
+
|
28
|
+
def system_ruby
|
29
|
+
require 'rbconfig'
|
30
|
+
[RbConfig::CONFIG['rubylibdir'], RbConfig::CONFIG['sitelibdir']].compact.uniq
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Lightning
|
2
|
+
module Util
|
3
|
+
extend self
|
4
|
+
|
5
|
+
if RUBY_VERSION < '1.9.1'
|
6
|
+
|
7
|
+
# From Ruby 1.9's Shellwords#shellescape
|
8
|
+
def shellescape(str)
|
9
|
+
# An empty argument will be skipped, so return empty quotes.
|
10
|
+
return "''" if str.empty?
|
11
|
+
|
12
|
+
str = str.dup
|
13
|
+
|
14
|
+
# Process as a single byte sequence because not all shell
|
15
|
+
# implementations are multibyte aware.
|
16
|
+
str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
|
17
|
+
|
18
|
+
# A LF cannot be escaped with a backslash because a backslash + LF
|
19
|
+
# combo is regarded as line continuation and simply ignored.
|
20
|
+
str.gsub!(/\n/, "'\n'")
|
21
|
+
|
22
|
+
return str
|
23
|
+
end
|
24
|
+
else
|
25
|
+
|
26
|
+
require 'shellwords'
|
27
|
+
def shellescape(str)
|
28
|
+
Shellwords.shellescape(str)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [String] Cross-platform way to determine a user's home. From Rubygems.
|
33
|
+
def find_home
|
34
|
+
['HOME', 'USERPROFILE'].each {|e| return ENV[e] if ENV[e] }
|
35
|
+
return "#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}" if ENV['HOMEDRIVE'] && ENV['HOMEPATH']
|
36
|
+
File.expand_path("~")
|
37
|
+
rescue
|
38
|
+
File::ALT_SEPARATOR ? "C:/" : "/"
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Boolean] Determines if a shell command exists by searching for it in ENV['PATH'].
|
42
|
+
def shell_command_exists?(command)
|
43
|
+
(@path ||= ENV['PATH'].split(File::PATH_SEPARATOR)).
|
44
|
+
any? {|d| File.exists? File.join(d, command) }
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Hash] Symbolizes keys of given hash
|
48
|
+
def symbolize_keys(hash)
|
49
|
+
hash.inject({}) do |h, (key, value)|
|
50
|
+
h[key.to_sym] = value; h
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Loads *.rb plugins in given directory and sub directory under it
|
55
|
+
def load_plugins(base_dir, sub_dir)
|
56
|
+
if File.exists?(dir = File.join(base_dir, sub_dir))
|
57
|
+
plugin_type = sub_dir.sub(/s$/, '')
|
58
|
+
Dir[dir + '/*.rb'].each {|file| load_plugin(file, plugin_type) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
protected
|
63
|
+
def load_plugin(file, plugin_type)
|
64
|
+
require file
|
65
|
+
rescue Exception => e
|
66
|
+
puts "Error: #{plugin_type.capitalize} plugin '#{File.basename(file)}'"+
|
67
|
+
" failed to load:", e.message
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/test/bolt_test.rb
CHANGED
@@ -1,36 +1,24 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@bolt.completion_map.map = @completion_map
|
9
|
-
end
|
10
|
-
|
11
|
-
test "fetches correct completions" do
|
12
|
-
assert_equal @bolt.completions, @completion_map.keys
|
13
|
-
end
|
3
|
+
# depends on test/lightning.yml
|
4
|
+
context "Bolt generates correct command from" do
|
5
|
+
assert "shell command" do
|
6
|
+
Lightning.functions['less-app'].is_a?(Function)
|
7
|
+
end
|
14
8
|
|
15
|
-
|
16
|
-
|
17
|
-
|
9
|
+
assert "command hash" do
|
10
|
+
Lightning.functions['oa'].is_a?(Function)
|
11
|
+
end
|
18
12
|
|
19
|
-
|
20
|
-
|
21
|
-
|
13
|
+
assert "global shell command" do
|
14
|
+
Lightning.functions['grep-app'].is_a?(Function)
|
15
|
+
end
|
22
16
|
|
23
|
-
|
24
|
-
|
25
|
-
end
|
17
|
+
assert "aliased global shell command in config" do
|
18
|
+
Lightning.functions['v-app'].is_a?(Function)
|
26
19
|
end
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
Lightning.stub!(:current_command, :return=>'blah')
|
31
|
-
Lightning.config = {:aliases=>{'path1'=>'/dir1/path1'}, :commands=>[{'name'=>'blah'}], :paths=>{}}
|
32
|
-
@bolt = Lightning::Bolt.new('blah')
|
33
|
-
assert_equal({'path1'=>'/dir1/path1'}, @bolt.completion_map.alias_map)
|
34
|
-
Lightning.config = old_config
|
20
|
+
|
21
|
+
assert "global shell command which has a local config" do
|
22
|
+
Lightning.functions['c'].is_a?(Function)
|
35
23
|
end
|
36
24
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
context "Builder" do
|
4
|
+
def build
|
5
|
+
Lightning.config.source_file = source_file
|
6
|
+
Builder.run
|
7
|
+
end
|
8
|
+
|
9
|
+
def source_file
|
10
|
+
@source_file ||= File.dirname(__FILE__) + '/lightning_completions'
|
11
|
+
end
|
12
|
+
|
13
|
+
test "prints error when unable to build" do
|
14
|
+
Lightning.config[:shell] = 'blah'
|
15
|
+
capture_stdout { build }.should =~ /No.*exists.*blah shell/
|
16
|
+
Lightning.config[:shell] = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
test "with non-default shell builds" do
|
20
|
+
Lightning.config[:shell] = 'zsh'
|
21
|
+
mock(Builder).zsh_builder(anything) { '' }
|
22
|
+
build
|
23
|
+
Lightning.config[:shell] = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
test "warns about existing commands being overridden" do
|
27
|
+
mock(Util).shell_command_exists?('bling') { true }
|
28
|
+
stub(Util).shell_command_exists?(anything) { false }
|
29
|
+
capture_stdout { build }.should =~ /following.*exist.*: bling$/
|
30
|
+
end
|
31
|
+
|
32
|
+
context "with default shell" do
|
33
|
+
before_all { build }
|
34
|
+
|
35
|
+
test "builds file in expected location" do
|
36
|
+
File.exists?(source_file).should == true
|
37
|
+
end
|
38
|
+
|
39
|
+
# depends on test/lightning.yml
|
40
|
+
test "builds expected output for a command" do
|
41
|
+
expected = <<-EOS.gsub(/^\s{6}/,'')
|
42
|
+
oa () {
|
43
|
+
local IFS=$'\\n'
|
44
|
+
local arr=( $(${LBIN_PATH}lightning-translate oa $@) )
|
45
|
+
open -a "${arr[@]}"
|
46
|
+
}
|
47
|
+
complete -o default -C "${LBIN_PATH}lightning-complete oa" oa
|
48
|
+
EOS
|
49
|
+
File.read(source_file).include?(expected).should == true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
after_all { FileUtils.rm_f(source_file) }
|
54
|
+
end
|