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,38 @@
1
+ module Lightning::Commands
2
+ protected
3
+ desc '(list [-a|--alias] | create SHELL_COMMAND [alias]| delete SHELL_COMMAND)',
4
+ 'Commands for managing shell commands. Defaults to listing them.'
5
+ def shell_command(argv)
6
+ subcommand = argv.shift || 'list'
7
+ subcommand = %w{create delete list}.find {|e| e[/^#{subcommand}/]} || subcommand
8
+ shell_command_subcommand(subcommand, argv) if subcommand_has_required_args(subcommand, argv)
9
+ end
10
+
11
+ def shell_command_subcommand(subcommand, argv)
12
+ case subcommand
13
+ when 'list' then list_subcommand(:shell_commands, argv)
14
+ when 'create' then create_shell_command(argv[0], argv[1])
15
+ when 'delete' then delete_shell_command(argv[0])
16
+ else puts "Invalid subcommand '#{subcommand}'", command_usage
17
+ end
18
+ end
19
+
20
+ def create_shell_command(scmd, scmd_alias=nil)
21
+ scmd_alias ||= scmd
22
+ if config.shell_commands.values.include?(scmd_alias)
23
+ puts "Alias '#{scmd_alias}' already exists for shell command '#{config.unaliased_command(scmd_alias)}'"
24
+ else
25
+ config.shell_commands[scmd] = scmd_alias
26
+ save_and_say "Added shell command '#{scmd}'"
27
+ end
28
+ end
29
+
30
+ def delete_shell_command(scmd)
31
+ if config.shell_commands[scmd]
32
+ config.shell_commands.delete scmd
33
+ save_and_say "Deleted shell command '#{scmd}' and its functions"
34
+ else
35
+ puts "Can't find shell command '#{scmd}'"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,75 @@
1
+ module Lightning
2
+ # Utility methods to be used inside lightning commands.
3
+ module CommandsUtil
4
+ # Yields a block for an existing bolt or prints an error message
5
+ def if_bolt_found(bolt)
6
+ bolt = config.unalias_bolt(bolt)
7
+ config.bolts[bolt] ? yield(bolt) : puts("Can't find bolt '#{bolt}'")
8
+ end
9
+
10
+ # Prints a hash as a 2 column table sorted by keys
11
+ def print_sorted_hash(hash, indent=false)
12
+ offset = hash.keys.map {|e| e.size }.max + 2
13
+ offset += 1 unless offset % 2 == 0
14
+ indent_char = indent ? ' ' : ''
15
+ hash.sort.each do |k,v|
16
+ puts "#{indent_char}#{k}" << ' ' * (offset - k.size) << (v || '')
17
+ end
18
+ end
19
+
20
+ # Saves config and prints message
21
+ def save_and_say(message)
22
+ config.save
23
+ puts message
24
+ end
25
+
26
+ # @return [nil, true] Determines if command has required arguments
27
+ def command_has_required_args(argv, required)
28
+ return true if argv.size >= required
29
+ puts "'lightning #{@command}' was called incorrectly.", command_usage
30
+ end
31
+
32
+ # @return [nil, true] Determines if subcommand has required arguments
33
+ def subcommand_has_required_args(subcommand, argv)
34
+ return true if argv.size >= (subcommand_required_args[subcommand] || 0)
35
+ puts "'lightning #{@command} #{subcommand}' was called incorrectly.", command_usage
36
+ end
37
+
38
+ # Parses arguments into non-option arguments and hash of options. Options can have
39
+ # values with an equal sign i.e. '--option=value'. Options without a value are set to true.
40
+ # @param [Array]
41
+ # @return [Array<Array, Hash>] Hash of options has symbolic keys
42
+ def parse_args(args)
43
+ options, args = args.partition {|e| e =~ /^-/ }
44
+ options = options.inject({}) do |hash, flag|
45
+ key, value = flag.split('=')
46
+ hash[key.sub(/^--?/,'').intern] = value.nil? ? true : value
47
+ hash
48
+ end
49
+ [args, options]
50
+ end
51
+
52
+ # Shortcut to Lightning.config
53
+ def config; Lightning.config; end
54
+
55
+ # Lists a bolt or shell_command with optional --alias
56
+ def list_subcommand(list_type, argv)
57
+ if %w{-a --alias}.include?(argv[0])
58
+ hash = config.send(list_type)
59
+ hash = hash.inject({}) {|a,(k,v)| a[k] = v['alias']; a } if list_type == :bolts
60
+ print_sorted_hash hash
61
+ else
62
+ puts config.send(list_type).keys.sort
63
+ end
64
+ end
65
+
66
+ private
67
+ def subcommand_required_args
68
+ desc_array[0].split('|').inject({}) {|a,e|
69
+ cmd, *args = e.strip.split(/\s+/)
70
+ a[cmd] = args.select {|e| e[/^[A-Z]/]}.size
71
+ a
72
+ }
73
+ end
74
+ end
75
+ end
@@ -1,43 +1,87 @@
1
- # derived from http://github.com/ryanb/dotfiles/tree/master/bash/completion_scripts/project_completion
2
- #This class handles completions given a path key and the text already typed.
3
- class Lightning
1
+ require 'shellwords'
2
+
3
+ module Lightning
4
+ # This class returns completions for the last word typed for a given lightning function and its {Function} object.
5
+ # Inspired loosely by ryanb[http://github.com/ryanb/dotfiles/tree/master/bash/completion_scripts/project_completion].
6
+ #
7
+ # == Regex Completion
8
+ # By default, regular expressions can be used while completing to filter/match possible completions. For duplicate
9
+ # paths that offer their full paths in completion, this means their full paths can also match. One non-regexp
10
+ # shorthand is that a '*' is converted to '.*' for glob-like behavior.To revert to standard completion, toggle
11
+ # Lightning.config[:complete_regex].
4
12
  class Completion
5
- def self.complete(text_to_complete, bolt_key)
6
- new(text_to_complete, bolt_key).matches
13
+ # @return [Array] Returns completions that match last word typed
14
+ def self.complete(text_to_complete, function, shellescape=true)
15
+ return error_array("No function found to complete.") unless function
16
+ new(text_to_complete, function, shellescape).matches
7
17
  end
8
-
9
- def initialize(text_typed, bolt_key)
18
+
19
+ # @return [Array] Constructs completion error message. More than one element long to ensure error message
20
+ # gets displayed and not completed
21
+ def self.error_array(message)
22
+ ["#Error: #{message}", "Please open an issue."]
23
+ end
24
+
25
+ def initialize(text_typed, function, shellescape=true)
10
26
  @text_typed = text_typed
11
- @bolt_key = bolt_key
27
+ @function = function
28
+ @shellescape = shellescape
12
29
  end
13
30
 
31
+ # @return [Array] Main method to determine and return completions that match
14
32
  def matches
15
- if Lightning.config[:complete_regex]
16
- begin
17
- possible_completions.grep(/#{blob_to_regex(typed)}/)
18
- rescue RegexpError
19
- ['#Error: Invalid regular expression']
20
- end
21
- else
22
- possible_completions.select do |e|
23
- e[0, typed.length] == typed
33
+ matched = get_matches(possible_completions)
34
+ matched = match_when_completing_subdirectories(matched)
35
+ @shellescape ? matched.map {|e| Util.shellescape(e) } : matched
36
+ rescue SystemCallError
37
+ self.class.error_array("Nonexistent directory.")
38
+ rescue RegexpError
39
+ self.class.error_array("Invalid regular expression.")
40
+ end
41
+
42
+ # @param [Array]
43
+ # @return [Array] Selects possible completions that match using {Completion#typed typed}
44
+ def get_matches(possible)
45
+ Lightning.config[:complete_regex] ? possible.grep(/^#{blob_to_regex(typed)}/) :
46
+ possible.select {|e| e[0, typed.length] == typed }
47
+ end
48
+
49
+ # @param [Array]
50
+ # @return [Array] Generates completions when completing a directory above or below a basename
51
+ def match_when_completing_subdirectories(matched)
52
+ if matched.empty? && (top_dir = typed[/^([^\/]+)\//,1]) && !typed.include?('//')
53
+ matched = possible_completions.grep(/^#{top_dir}/)
54
+
55
+ # for typed = some/dir/file, top_dir = path and translated_dir = /full/bolt/path
56
+ if matched.size == 1 && (translated_dir = @function.translate([top_dir])[0])
57
+ short_dir = typed.sub(/\/([^\/]+)?$/, '') # some/dir
58
+ completed_dir = short_dir.sub(top_dir, translated_dir) #/full/bolt/path/some/dir
59
+ completed_dir = File.expand_path(completed_dir) if completed_dir[/\/\.\.($|\/)/]
60
+ matched = Dir.entries(completed_dir).delete_if {|e| %w{. ..}.include?(e) }.map {|f|
61
+ File.directory?(completed_dir+'/'+f) ? File.join(short_dir,f) +'/' : File.join(short_dir,f)
62
+ }
63
+ matched = get_matches(matched)
24
64
  end
25
65
  end
66
+ matched
67
+ end
68
+
69
+ # @return [String] Last word typed by user
70
+ def typed
71
+ @typed ||= begin
72
+ args = Shellwords.shellwords(@text_typed)
73
+ !args[-1][/\s+/] && @text_typed[/\s+$/] ? '' : args[-1]
74
+ end
26
75
  end
27
-
28
- #just converts * to .* to make a glob-like regex
76
+
77
+ # @private Converts * to .* to make a glob-like regex when in regex completion mode
29
78
  def blob_to_regex(string)
30
79
  string.gsub(/^\*|([^\.])\*/) {|e| $1 ? $1 + ".*" : ".*" }
31
80
  end
32
-
33
- def typed
34
- # @text_typed[/\s(.+?)$/, 1] || ''
35
- text = @text_typed[/^(\S+)\s+(#{Lightning::TEST_FLAG})?\s*(.+?)$/, 3] || ''
36
- text.strip
37
- end
38
-
81
+
82
+ protected
39
83
  def possible_completions
40
- Lightning.bolts[@bolt_key].completions
41
- end
84
+ @function.completions
85
+ end
42
86
  end
43
87
  end
@@ -1,59 +1,62 @@
1
- #This class maps completions to their full paths for the given blobs
2
- class Lightning
1
+ module Lightning
2
+ # Maps completions (file basenames) and aliases to their full paths given a {Bolt} object's globs.
3
3
  class CompletionMap
4
+ DUPLICATE_DELIMITER = '//'
5
+ # @return [Array] Regular expression paths to ignore. By default paths ending in . or .. are ignored.
6
+ def self.ignore_paths
7
+ @ignore_paths ||= (Lightning.config[:ignore_paths] || []) + %w{\.\.?$}
8
+ end
9
+
10
+ # Sets ignore_paths
11
+ def self.ignore_paths=(val)
12
+ @ignore_paths = val
13
+ end
14
+
15
+ # @return [Hash] Maps file basenames to full paths
4
16
  attr_accessor :map
17
+ # @return [Hash] Maps aliases to full paths
5
18
  attr_reader :alias_map
6
19
 
7
20
  def initialize(*globs)
8
21
  options = globs[-1].is_a?(Hash) ? globs.pop : {}
9
22
  globs.flatten!
10
- @map = create_map_for_globs(globs)
11
- @alias_map = (options[:global_aliases] || {}).merge(options[:aliases] || {})
23
+ @map = create_globbed_map(globs)
24
+ @alias_map = options[:aliases] || {}
12
25
  end
13
-
26
+
27
+ # @return [String] Fetches full path of file or alias
14
28
  def [](completion)
15
29
  @map[completion] || @alias_map[completion]
16
30
  end
17
-
31
+
32
+ # @return [Array] List of unique basenames and aliases
18
33
  def keys
19
34
  (@map.keys + @alias_map.keys).uniq
20
35
  end
21
-
22
- #should return hash
23
- def create_map_for_globs(globs)
24
- path_hash = {}
25
- ignore_paths = ['.', '..'] + Lightning.ignore_paths
26
- globs.each do |d|
27
- Dir.glob(d, File::FNM_DOTMATCH).each do |e|
28
- basename = File.basename(e)
29
- unless ignore_paths.include?(basename)
30
- #save paths of duplicate basenames to process later
31
- if path_hash.has_key?(basename)
32
- if path_hash[basename].is_a?(Array)
33
- path_hash[basename] << e
34
- else
35
- path_hash[basename] = [path_hash[basename], e]
36
- end
37
- else
38
- path_hash[basename] = e
39
- end
40
- end
36
+
37
+ protected
38
+ def create_globbed_map(globs)
39
+ duplicates = {}
40
+ ignore_regexp = /(#{self.class.ignore_paths.join('|')})/
41
+ globs.map {|e| Dir.glob(e, File::FNM_DOTMATCH)}.flatten.uniq.
42
+ inject({}) do |acc, file|
43
+ basename = File.basename file
44
+ next acc if file =~ ignore_regexp
45
+ if duplicates[basename]
46
+ duplicates[basename] << file
47
+ elsif acc.key?(basename)
48
+ duplicates[basename] = [acc.delete(basename), file]
49
+ else
50
+ acc[basename] = file
41
51
  end
42
- end
43
- map_duplicate_basenames(path_hash)
44
- path_hash
52
+ acc
53
+ end.merge create_resolved_duplicates(duplicates)
45
54
  end
46
-
47
- #map saved duplicates
48
- def map_duplicate_basenames(path_hash)
49
- path_hash.select {|k,v| v.is_a?(Array)}.each do |key,paths|
50
- paths.each do |e|
51
- new_key = "#{key}/#{File.dirname(e)}"
52
- path_hash[new_key] = e
53
- end
54
- path_hash.delete(key)
55
+
56
+ def create_resolved_duplicates(duplicates)
57
+ duplicates.inject({}) do |hash, (basename, paths)|
58
+ paths.each {|e| hash["#{basename}#{DUPLICATE_DELIMITER}#{File.dirname(e)}"] = e }; hash
55
59
  end
56
60
  end
57
-
58
61
  end
59
62
  end
@@ -1,72 +1,107 @@
1
- class Lightning
1
+ require 'yaml'
2
+ module Lightning
3
+ # Handles config file used to generate bolts and functions.
4
+ # The config file is in YAML so it's easy to manually edit. Nevertheless, it's recommended to modify
5
+ # the config through lightning commands since their API is more stable than the config file's API.
6
+ #
7
+ # == Config File Format
8
+ #
9
+ # Top level keys:
10
+ #
11
+ # * *:source_file*: Location of shell file generated by {Builder}. Defaults to ~/.lightning/functions.sh
12
+ # * *:ignore_paths*: Array of paths to globally ignore when generating possible completions
13
+ # * *:complete_regex*: true or false (default is true). When true, regular expressions can be used while
14
+ # completing. See {Completion} for more details.
15
+ # * *:shell*: Specifies shell Builder uses to generate completions. Defaults to bash.
16
+ # * *:bolts*: Array of bolts with each bolt being a hash with the following keys:
17
+ # * *name*(required): Unique name
18
+ # * *alias*: Abbreviated name which can be used to reference bolt in most lightning commands. This is used
19
+ # over name when generating function names.
20
+ # * *global*: true or false (default is false). When set, uses bolt to generate functions with each command
21
+ # in :shell_commands.
22
+ # * *globs*(required): Array of globs which defines group of paths bolt handles
23
+ # * *functions*: Array of lightning functions. A function can either be a string (shell command with default
24
+ # options) or a hash with the following keys:
25
+ # * *name*: Name of the lightning function
26
+ # * *shell_command*(required): Shell command with default options which this function wraps
27
+ # * *aliases*: A hash of custom aliases and full paths only for this function
28
+ # * *post_path*: String to add immediately after a resolved path
29
+ # * *:shell_commands*: Hash of global shell commands which are combined with all global bolts to generate functions
2
30
  class Config < ::Hash
3
31
  class <<self
4
32
  attr_accessor :config_file
33
+ # @return [String] ~/.lightningrc
5
34
  def config_file
6
- @config_file ||= (File.exists?('lightning.yml') ? 'lightning.yml' : File.expand_path(File.join("~",".lightning.yml")))
35
+ @config_file ||= File.join(Lightning.home,".lightningrc")
7
36
  end
8
37
 
9
- def create(options={})
10
- hash = read_config_file
11
- obj = new(hash)
12
- configure_commands_and_paths(obj) if options[:read_only]
13
- obj
14
- end
15
-
16
- def read_config_file(file=nil)
17
- default_config = {'shell'=>'bash', 'generated_file'=>File.expand_path(File.join('~', '.lightning_completions')),
18
- 'complete_regex'=>true}
19
- @config_file = file if file
20
- hash = YAML::load_file(config_file)
21
- default_config.merge(hash)
22
- end
23
-
24
- def commands_to_bolt_key(map_to_command, new_command)
25
- "#{map_to_command}-#{new_command}"
26
- end
27
-
28
- def configure_commands_and_paths(hash)
29
- hash[:paths] ||= {}
30
- hash[:commands].each do |e|
31
- #mapping a referenced path
32
- if e['paths'].is_a?(String)
33
- e['bolt_key'] = e['paths'].dup
34
- end
35
- #create a path entry + key if none exists
36
- if e['bolt_key'].nil?
37
- #extract command in case it has options after it
38
- e['map_to'] =~ /\s*(\w+)/
39
- bolt_key = commands_to_bolt_key($1, e['name'])
40
- e['bolt_key'] = bolt_key
41
- hash[:paths][bolt_key] = e['paths'] || []
42
- end
43
- end
44
- hash
38
+ # @return [Hash] Creates a bolt hash given globs
39
+ def bolt(globs)
40
+ {'globs'=>globs.map {|e| e.sub(/^~/, Lightning.home) }}
45
41
  end
46
42
  end
47
-
48
- def save
49
- File.open(self.class.config_file, "w") { |f| f.puts self.to_hash.to_yaml }
50
- end
51
43
 
52
- def initialize(hash)
44
+ DEFAULT = {:complete_regex=>true, :bolts=>{}, :shell_commands=>{'cd'=>'cd', 'echo'=>'echo'} }
45
+ attr_accessor :source_file
46
+ def initialize(hash=read_config_file)
47
+ hash = DEFAULT.merge hash
53
48
  super
54
49
  replace(hash)
55
- self.replace(self.symbolize_keys)
56
50
  end
57
-
58
- def to_hash
59
- hash = Hash.new
60
- hash.replace(self)
51
+
52
+ # @return [String] Shell file generated by Builder. Defaults to ~/.lightning/functions.sh
53
+ def source_file
54
+ @source_file ||= self[:source_file] || File.join(Lightning.dir, 'functions.sh')
61
55
  end
62
-
63
- #from Rails' ActiveSupport
64
- def symbolize_keys
65
- inject({}) do |options, (key, value)|
66
- options[(key.to_sym rescue key) || key] = value
67
- options
68
- end
56
+
57
+ # @return [Array] Global shell commands used to generate Functions for all Bolts.
58
+ def global_commands
59
+ shell_commands.keys
60
+ end
61
+
62
+ # @return [Hash] Maps shell command names to their aliases
63
+ def shell_commands
64
+ self[:shell_commands]
65
+ end
66
+
67
+ # @return [Hash] Maps bolt names to their config hashes
68
+ def bolts
69
+ self[:bolts]
70
+ end
71
+
72
+ # @return [String] Converts shell command alias to shell command
73
+ def unaliased_command(cmd)
74
+ shell_commands.invert[cmd] || cmd
75
+ end
76
+
77
+ # @return [String] Converts bolt alias to bolt's name
78
+ def unalias_bolt(bolt)
79
+ bolts[bolt] ? bolt : (Array(bolts.find {|k,v| v['alias'] == bolt })[0] || bolt)
80
+ end
81
+
82
+ # @return [String] Extracts shell command from a shell_command string
83
+ def only_command(shell_command)
84
+ shell_command[/\w+/]
85
+ end
86
+
87
+ # @return [String] Creates a command name from its shell command and bolt in the form "#{command}-#{bolt}".
88
+ # Uses aliases for either if they exist.
89
+ def function_name(shell_command, bolt)
90
+ cmd = only_command shell_command
91
+ "#{shell_commands[cmd] || cmd}-#{bolt}"
92
+ end
93
+
94
+ # Saves config to Config.config_file
95
+ def save
96
+ File.open(self.class.config_file, "w") {|f| f.puts Hash.new.replace(self).to_yaml }
97
+ end
98
+
99
+ protected
100
+ def read_config_file
101
+ File.exists?(self.class.config_file) ?
102
+ Util.symbolize_keys(YAML::load_file(self.class.config_file)) : {}
103
+ rescue
104
+ raise $!.message.sub('syntax error', "Syntax error in '#{Config.config_file}'")
69
105
  end
70
-
71
106
  end
72
- end
107
+ end