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.
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