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