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