lightning 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/CHANGELOG.rdoc +9 -0
  2. data/README.rdoc +53 -125
  3. data/Rakefile +14 -40
  4. data/bin/lightning +4 -0
  5. data/bin/lightning-complete +1 -10
  6. data/bin/lightning-translate +4 -0
  7. data/lib/lightning.rb +36 -50
  8. data/lib/lightning/bolt.rb +53 -26
  9. data/lib/lightning/builder.rb +87 -0
  10. data/lib/lightning/commands.rb +92 -69
  11. data/lib/lightning/commands/bolt.rb +63 -0
  12. data/lib/lightning/commands/core.rb +57 -0
  13. data/lib/lightning/commands/function.rb +76 -0
  14. data/lib/lightning/commands/shell_command.rb +38 -0
  15. data/lib/lightning/commands_util.rb +75 -0
  16. data/lib/lightning/completion.rb +72 -28
  17. data/lib/lightning/completion_map.rb +42 -39
  18. data/lib/lightning/config.rb +92 -57
  19. data/lib/lightning/function.rb +70 -0
  20. data/lib/lightning/generator.rb +77 -43
  21. data/lib/lightning/generators.rb +53 -0
  22. data/lib/lightning/generators/misc.rb +12 -0
  23. data/lib/lightning/generators/ruby.rb +32 -0
  24. data/lib/lightning/util.rb +70 -0
  25. data/lib/lightning/version.rb +3 -0
  26. data/test/bolt_test.rb +16 -28
  27. data/test/builder_test.rb +54 -0
  28. data/test/commands_test.rb +98 -0
  29. data/test/completion_map_test.rb +31 -54
  30. data/test/completion_test.rb +106 -36
  31. data/test/config_test.rb +22 -56
  32. data/test/function_test.rb +90 -0
  33. data/test/generator_test.rb +73 -0
  34. data/test/lightning.yml +26 -34
  35. data/test/test_helper.rb +80 -15
  36. metadata +42 -20
  37. data/VERSION.yml +0 -4
  38. data/bin/lightning-full_path +0 -18
  39. data/bin/lightning-install +0 -7
  40. data/lib/lightning/bolts.rb +0 -12
  41. data/lightning.yml.example +0 -87
  42. data/lightning_completions.example +0 -147
  43. 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
@@ -1,48 +1,82 @@
1
- #This class generates shell scripts from a configuration.
2
- class Lightning
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
- class<<self
5
- def generate_completions(generated_file=nil)
6
- generated_file ||= Lightning.config[:generated_file]
7
- output = generate(Lightning.config[:shell], Lightning.config[:commands])
8
- File.open(generated_file, 'w'){|f| f.write(output) }
9
- output
10
- end
11
-
12
- def generate(*args)
13
- shell = args.shift
14
- send("#{shell}_generator", *args)
15
- end
16
-
17
- def bash_generator(commands)
18
- body = <<-INIT
19
- #### This file was generated by Lightning. ####
20
- #LBIN_PATH="$PWD/bin/" #only use for development
21
- LBIN_PATH=""
22
-
23
- INIT
24
- commands.each do |e|
25
- body += <<-EOS
26
-
27
- #{'#' + e['description'] if e['description']}
28
- #{e['name']} () {
29
- if [ -z "$1" ]; then
30
- echo "No arguments given"
31
- return
32
- fi
33
- FULL_PATH="`${LBIN_PATH}lightning-full_path #{e['name']} $@`#{e['post_path'] if e['post_path']}"
34
- if [ $1 == '#{Lightning::TEST_FLAG}' ]; then
35
- CMD="#{e['map_to']} '$FULL_PATH'#{' '+ e['add_to_command'] if e['add_to_command']}"
36
- echo $CMD
37
- else
38
- #{e['map_to']} "$FULL_PATH"#{' '+ e['add_to_command'] if e['add_to_command']}
39
- fi
40
- }
41
- complete -o default -C "${LBIN_PATH}lightning-complete #{e['name']}" #{e['name']}
42
- EOS
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
@@ -0,0 +1,3 @@
1
+ module Lightning
2
+ VERSION = '0.3.0'
3
+ end
data/test/bolt_test.rb CHANGED
@@ -1,36 +1,24 @@
1
1
  require File.join(File.dirname(__FILE__), 'test_helper')
2
2
 
3
- class Lightning::BoltTest < Test::Unit::TestCase
4
- context "Bolt" do
5
- before(:each) do
6
- @completion_map = {'path1'=>'/dir/path1','path2'=>'/dir/path2'}
7
- @bolt = Lightning::Bolt.new('blah')
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
- test "resolves completion" do
16
- assert_equal @completion_map['path1'], @bolt.resolve_completion('path1')
17
- end
9
+ assert "command hash" do
10
+ Lightning.functions['oa'].is_a?(Function)
11
+ end
18
12
 
19
- test "resolves completion with test flag" do
20
- assert_equal @completion_map['path1'], @bolt.resolve_completion('-test path1')
21
- end
13
+ assert "global shell command" do
14
+ Lightning.functions['grep-app'].is_a?(Function)
15
+ end
22
16
 
23
- test "creates completion_map only once" do
24
- assert_equal @bolt.completion_map.object_id, @bolt.completion_map.object_id
25
- end
17
+ assert "aliased global shell command in config" do
18
+ Lightning.functions['v-app'].is_a?(Function)
26
19
  end
27
-
28
- test "Bolt's completion_map sets up alias map with options" do
29
- old_config = Lightning.config
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