rubsh 0.0.1 → 0.0.3

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ca63773e13fa92b984e44144945aa43955b9f4468881e819581a65b8015f7b4d
4
+ data.tar.gz: 4090f30591db271c5f50230c93270e542d59a2f19c4b3e60e3f09fbc8160b56f
5
+ SHA512:
6
+ metadata.gz: 9717cb3327ac4176352ed8dc3ac818cac44f5d0a77a5b9775d0f992902b8723af461c93a66b02acd730471c77c94d11c9d3d390824ac1c1fde41e65f58492899
7
+ data.tar.gz: 54fbae882195e3851d33c9eadef7d2dfbd4181f2fcef701574d38b0ccaf2a4811350a78e8f94e7fa1da7cfb29a6c7258b9aef802e4edabe38829616046ba44ba
data/README.rdoc CHANGED
@@ -0,0 +1,103 @@
1
+ = Rubsh
2
+
3
+ Rubsh was created to be a native shell for ruby users. A irb which can also be
4
+ used as a normal shell without the akwardness of some of the features of bash
5
+ and other shells, and with the power of ruby.
6
+
7
+ = Installation
8
+
9
+ sudo gem source -a http://gemcutter.org
10
+ sudo gem install rubsh
11
+
12
+ Run rubsh on the commandline. Type help for a quick overview of usage.
13
+
14
+ = Features
15
+
16
+ * readline support
17
+ * tab completion
18
+ * aliases
19
+ * commandline history
20
+
21
+ = Usage
22
+
23
+ == aliases
24
+
25
+ Aliases use shell syntax and remain available for the current session:
26
+
27
+ alias ls "ls -Gh"
28
+
29
+ Put the same line in <tt>~/.config/rubsh/config.rb</tt> to load it at startup.
30
+ Alias values are parsed again as shell commands, so quoting and pipeline arguments are preserved.
31
+
32
+ == ~/.config/rubsh/config.rb
33
+
34
+ Define Ruby functions and persistent aliases in this file. Rubsh loads it when the shell starts.
35
+
36
+ == Shortcuts
37
+
38
+ While normal commands as 'ls' etc work transparantly, you can also do things like:
39
+
40
+ '*'.ls
41
+
42
+ shortcut for Dir.glob(...)
43
+
44
+ '*'.ls.du
45
+
46
+ Does du on all the results
47
+
48
+ It also takes arguments:
49
+
50
+ '*'.ls.du(:sh)
51
+
52
+ is equivalent to du -sh *
53
+
54
+ == Prompt
55
+
56
+ use ENV['PR1'] to set your prompt. see 'help prompt' for options from within the shell.
57
+
58
+ %h - hostname
59
+ %u - the username of the current user
60
+ %w - the current working directory, with $HOME abbreviated with a tilde
61
+ %W - the basename of the current working directory, with $HOME abbreviated
62
+ with a tilde
63
+ %Cb - blue color
64
+ %Cc - cyan color
65
+ %Cg - green color
66
+ %CC - color reset
67
+ %t - the current time in 24-hour HH:MM:SS format
68
+ %% - literal %
69
+ %$ - if the effective UID is 0, a #, otherwise a $
70
+
71
+ Example: ENV['PR1'] = "[%u@%h]--(%t)\n\r[%w]%$ "
72
+
73
+ == Ruby command functions
74
+
75
+ Rubsh keeps one Ruby context for the shell session. Define a function over multiple lines,
76
+ then use it as a command or inside backtick command substitution:
77
+
78
+ def foo
79
+ return "ok"
80
+ end
81
+
82
+ echo `foo` | tee out.txt
83
+
84
+ The example prints <tt>ok</tt> and writes <tt>ok\n</tt> to <tt>out.txt</tt>. Function arguments
85
+ are the command arguments. A returned string becomes command output; <tt>puts</tt> writes
86
+ directly to the command's output stream.
87
+
88
+ Ruby functions can also call shell commands:
89
+
90
+ def merp
91
+ ls
92
+ end
93
+
94
+ == Pipelines
95
+
96
+ Commands are parsed into argv arrays, so quotes and escaped spaces remain one argument.
97
+ The pipe operator connects command stages, including Ruby functions and external programs.
98
+ Backticks run a nested Rubsh command and substitute its captured output, with a trailing
99
+ newline removed.
100
+
101
+ Rubsh intentionally does not implement job control, background jobs, heredocs, redirects,
102
+ or <tt>&amp;&amp;</tt>/<tt>||</tt> conditionals. Stateful commands such as <tt>cd</tt> run in the
103
+ shell process so directory changes persist.
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'rubygems'
2
2
  require 'echoe'
3
- Echoe.new('rubsh','0.0.1') do |gem|
3
+ Echoe.new('rubsh','0.0.2') do |gem|
4
4
  gem.description = 'A ruby shell'
5
5
  gem.url = 'http://github.com/danielb2/rubsh'
6
6
  gem.author = 'Daniel Bretoi'
data/bin/rubsh CHANGED
@@ -7,12 +7,14 @@ require 'optparse'
7
7
  $DEBUG = false
8
8
 
9
9
  def parse_options
10
- parser = OptionParser.new()
11
- parser.on("--debug", "If included run in DEBUG mode") do
12
- $DEBUG = true
13
- end
14
- parser.banner "rubsh"
15
- parser.parse(ARGV)
10
+ options = {}
11
+ parser = OptionParser.new
12
+ parser.on('-c COMMAND', '--command COMMAND', 'Run one command and exit') { |command| options[:command] = command }
13
+ parser.on('--debug', 'If included run in DEBUG mode') { $DEBUG = true }
14
+ parser.banner = 'rubsh'
15
+ parser.parse!(ARGV)
16
+ options
16
17
  end
17
18
 
18
- Rubsh.new.run
19
+ options = parse_options
20
+ exit(Rubsh.new.run(options[:command]) || 0)
data/lib/alias.rb CHANGED
@@ -1,19 +1,29 @@
1
1
  class Alias
2
- @@aliases = Hash.new
3
- def self.parse(str)
4
- str.scan(/^\s*([^\s]+)(.*)$/) do |name,val|
5
- @@aliases[name] = val
6
- end
2
+ def initialize
3
+ @values = {}
7
4
  end
8
- def self.[]=(name,value)
9
- @@aliases[name] = value
5
+
6
+ def []=(name, value)
7
+ @values[name.to_s] = value.to_s
10
8
  end
11
- def self.[](name)
12
- @@aliases[name]
9
+
10
+ def [](name)
11
+ @values[name.to_s]
13
12
  end
14
- def self.show
15
- @@aliases.each do |k,v|
16
- puts "alias #{k} = #{v}"
17
- end
13
+
14
+ def delete(name)
15
+ @values.delete(name.to_s)
16
+ end
17
+
18
+ def empty?
19
+ @values.empty?
20
+ end
21
+
22
+ def to_h
23
+ @values.dup
24
+ end
25
+
26
+ def show(output = $stdout)
27
+ @values.each { |name, value| output.puts "alias #{name}=#{value}" }
18
28
  end
19
29
  end
data/lib/commands.rb CHANGED
@@ -1,86 +1,84 @@
1
1
  class Commands
2
2
  @@oldpwd = nil
3
+
4
+ attr_writer :shell_executor, :shell_source_executor, :completion_executor
5
+
3
6
  def get_binding
4
7
  binding
5
8
  end
6
9
 
7
- def help(arg)
8
- case arg
9
- when /help.*prompt/
10
- puts %|
11
- %h - hostname
12
- %u - the username of the current user
13
- %w - the current working directory, with $HOME abbreviated with a tilde
14
- %W - the basename of the current working directory, with $HOME abbreviated
15
- with a tilde
16
- %Cb - blue color
17
- %Cc - cyan color
18
- %Cg - green color
19
- %CC - color reset
20
- %t - the current time in 24-hour HH:MM:SS format
21
- %% - literal %
22
- %$ - if the effective UID is 0, a #, otherwise a $
23
-
24
- Example: ENV['PR1'] = "[%u@%h]--(%t)\\n\\r[%w]%$ "
25
- |
26
- else
27
- puts %|
28
- ralias - alias command
29
- example: ralias 'ls ls -Gh'
30
- aliases ls to 'ls -Gh'
10
+ def method_missing(name, *args)
11
+ return super unless @shell_executor
12
+ @shell_executor.call(name.to_s, args)
13
+ end
31
14
 
32
- shortcuts:
33
- '...'.ls - shortcut for Dir.glob(...)
34
- '...'.ls.du is available to do `du` on the files
15
+ def respond_to_missing?(_name, _include_private = false)
16
+ true
17
+ end
35
18
 
36
- prompt:
37
- use ENV['PR1'] to set your prompt.
38
- see 'help prompt' for options
19
+ def complete(name, &block)
20
+ @completion_executor.call(name.to_s, &block)
21
+ end
39
22
 
40
- ~/.rubsh/rc.rb
41
- you can define your aliases, prompt and define
42
- your own functions in here.
23
+ def __rubsh_shell__(source)
24
+ @shell_source_executor.call(source)
25
+ end
43
26
 
44
- commandline functions:
45
- * it gets sent it's own invocation
46
- * must be a one liner
47
- example: def test(*a); p a;end # test foo #=> ["test foo"]
27
+ def command?(name)
28
+ symbol = name.to_sym
29
+ singleton_methods.include?(symbol) ||
30
+ self.class.public_instance_methods(true).include?(symbol) ||
31
+ self.class.protected_instance_methods(true).include?(symbol) ||
32
+ self.class.private_instance_methods(true).include?(symbol)
33
+ end
48
34
 
49
- |
35
+ def call(name, args, stdin: $stdin, stdout: $stdout, stderr: $stderr)
36
+ method = method(name.to_sym)
37
+ if method.parameters.any? { |_kind, key| key == :stdin }
38
+ method.call(args, stdin: stdin, stdout: stdout, stderr: stderr)
39
+ elsif method.arity == 0
40
+ method.call
41
+ else
42
+ method.call(*args)
50
43
  end
51
44
  end
52
- def cd(dir)
53
- dir.gsub! /\s*cd\s*/, ''
54
- dir.gsub! /\s*$/, ''
55
- dir.gsub! /("|')/, ''
56
- if dir.empty?
57
- @@oldpwd = Dir.pwd
58
- Dir.chdir ENV['HOME']
59
- return
60
- end
61
45
 
46
+ def help(*_args)
47
+ puts <<~HELP
48
+ Rubsh runs Ruby expressions and external commands.
49
+ Define commands with a multiline Ruby function:
50
+
51
+ def foo
52
+ return "ok"
53
+ end
54
+
55
+ Backticks capture a command, and | connects pipeline stages.
56
+ HELP
57
+ end
58
+
59
+ def cd(args, stdin: $stdin, stdout: $stdout, stderr: $stderr)
60
+ dir = args.first
61
+ dir = ENV['HOME'] if dir.nil? || dir.empty?
62
62
  if dir == '-'
63
- if @@oldpwd
64
- curdir = Dir.pwd
65
- Dir.chdir @@oldpwd
66
- @@oldpwd = curdir
67
- else
68
- puts "OLDPWD not set"
63
+ unless @@oldpwd
64
+ stderr.puts 'OLDPWD not set'
65
+ return 1
69
66
  end
70
- return
71
- end
72
-
73
- if dir =~ /^~\/?/
74
- dir.gsub!('~',ENV['HOME'])
67
+ dir, @@oldpwd = @@oldpwd, Dir.pwd
68
+ else
69
+ dir = File.expand_path(dir, ENV['HOME']) if dir.start_with?('~/') || dir == '~'
70
+ @@oldpwd = Dir.pwd
75
71
  end
76
- @@oldpwd = Dir.pwd
77
- Dir.chdir dir
72
+ Dir.chdir(dir)
73
+ 0
74
+ rescue SystemCallError => error
75
+ stderr.puts error.message
76
+ 1
78
77
  end
79
- def method_missing(m,*args)
80
- if ::Rubsh::iscmd? m.to_s
81
- system(m.to_s)
82
- return
83
- end
84
- raise "rubsh: #{m}: command not found"
78
+
79
+ def history(*_args)
80
+ file = File.join(ENV['HOME'], '.rubsh', 'history')
81
+ return unless File.exist?(file)
82
+ File.foreach(file) { |line| puts line }
85
83
  end
86
84
  end
data/lib/completion.rb ADDED
@@ -0,0 +1,111 @@
1
+ class CompletionNode
2
+ attr_reader :options, :subcommands, :arguments
3
+ attr_accessor :description, :wrapped_command
4
+
5
+ def initialize
6
+ @options = {}
7
+ @subcommands = {}
8
+ @arguments = []
9
+ end
10
+
11
+ def option(name, description: nil, argument: nil)
12
+ @options[name.to_s] = { description: description, argument: argument }
13
+ end
14
+
15
+ def argument(name = nil, complete: nil)
16
+ @arguments << { name: name, complete: complete }
17
+ end
18
+
19
+ def subcommand(name, description: nil, &block)
20
+ child = (@subcommands[name.to_s] ||= CompletionNode.new)
21
+ child.description = description if description
22
+ child.instance_eval(&block) if block
23
+ child
24
+ end
25
+
26
+ def wraps(command)
27
+ @wrapped_command = command.to_s
28
+ end
29
+ end
30
+
31
+ class CompletionRegistry
32
+ def initialize
33
+ @commands = {}
34
+ end
35
+
36
+ def complete(name, &block)
37
+ node = (@commands[name.to_s] ||= CompletionNode.new)
38
+ node.instance_eval(&block) if block
39
+ node
40
+ end
41
+
42
+ def erase(name)
43
+ @commands.delete(name.to_s)
44
+ end
45
+ def import_fish(text)
46
+ text.each_line do |line|
47
+ next unless line =~ /\Acomplete(?:\s+--no-files)?\s+(\S+)(.*)\z/
48
+ command, rest = Regexp.last_match(1), Regexp.last_match(2)
49
+ complete(command) do
50
+ if rest =~ /\s+-s\s+(\S+)/
51
+ option("-#{Regexp.last_match(1)}", description: rest[/\s+-d\s+'([^']+)'/, 1])
52
+ elsif rest =~ /\s+-a\s+'([^']+)'/
53
+ Regexp.last_match(1).split.each { |value| subcommand(value) }
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+
60
+ def [](name)
61
+ @commands[name.to_s]
62
+ end
63
+
64
+ def suggestions(argv, prefix)
65
+ node = @commands[argv.first]
66
+ node = @commands[node.wrapped_command] if node && node.wrapped_command
67
+ return [] unless node
68
+ argv.drop(1).each { |word| node = node.subcommands[word] if node.subcommands.key?(word) }
69
+ (node.subcommands.keys + node.options.keys).grep(/^#{Regexp.escape(prefix)}/)
70
+ end
71
+
72
+ def show(name = nil)
73
+ return @commands.keys.sort unless name
74
+ node = @commands[name.to_s]
75
+ node ? ruby_definition(name.to_s, node) : []
76
+ end
77
+
78
+ private
79
+
80
+ def ruby_definition(name, node, indent = 0)
81
+ pad = ' ' * indent
82
+ lines = ["#{pad}complete #{name.inspect} do"]
83
+ node.options.each do |option, details|
84
+ args = [option.inspect]
85
+ args << "description: #{details[:description].inspect}" if details[:description]
86
+ args << "argument: #{details[:argument].inspect}" if details[:argument]
87
+ lines << "#{pad} option #{args.join(', ')}"
88
+ end
89
+ node.arguments.each { |arg| lines << "#{pad} argument #{arg[:name].inspect}" }
90
+ node.subcommands.each do |subcommand, child|
91
+ description = child.description ? ", description: #{child.description.inspect}" : ''
92
+ lines.concat(ruby_subcommand(subcommand, child, indent + 1, description))
93
+ end
94
+ lines << "#{pad}end"
95
+ lines.join("\n")
96
+ end
97
+
98
+ def ruby_subcommand(name, node, indent, description)
99
+ pad = ' ' * indent
100
+ lines = ["#{pad}subcommand #{name.inspect}#{description} do"]
101
+ node.options.each do |option, details|
102
+ args = [option.inspect]
103
+ args << "description: #{details[:description].inspect}" if details[:description]
104
+ args << "argument: #{details[:argument].inspect}" if details[:argument]
105
+ lines << "#{pad} option #{args.join(', ')}"
106
+ end
107
+ node.subcommands.each { |child_name, child| lines.concat(ruby_subcommand(child_name, child, indent + 1, child.description ? ", description: #{child.description.inspect}" : '')) }
108
+ lines << "#{pad}end"
109
+ lines
110
+ end
111
+ end
data/lib/rub_readline.rb CHANGED
@@ -1,10 +1,56 @@
1
- require 'readline'
2
- Readline.completion_proc = Proc.new do |str|
3
- Dir[str+'*'].
4
- grep( /^#{Regexp.escape(str)}/ ). #only return dirs which start with str
5
- map { |f| f =~ /\s/ ? "\"#{f}\"" : f } # if names have spaces, then quote
6
- #Dir[str+'*'].select { |f| File.stat(f).executable? }.grep( /^#{Regexp.escape(str)}/ )
7
- # + commands_in_path.grep( /^#{Regexp.escape(str)}/ )
1
+ require 'reline'
2
+ def get_local_dirs(str)
3
+ Dir[str+'*'].
4
+ grep( /^#{Regexp.escape(str)}/ ). #only return dirs which start with str
5
+ map { |f| f =~ /\s/ ? "\"#{f}\"" : f } # if names have spaces, then quote
6
+ end
7
+
8
+ # tab completion here is bad because we don't know if the str has a 'cd' before it.
9
+ # That makes us go through the executables as well when we're doing cd str[TAB]
10
+ def get_executables(str)
11
+ commands = []
12
+ ENV["PATH"].split(':').each do |dir|
13
+ Dir.glob("#{dir}/*").grep( /#{File::SEPARATOR}?#{Regexp.escape(str)}/ ).each do |file|
14
+ begin
15
+ stat = File.stat(file)
16
+ commands << File::basename(file) if stat.executable? and not stat.directory?
17
+ rescue
18
+ end
19
+ end
20
+ end
21
+ return commands
22
+ end
23
+ Reline.autocompletion = false if Reline.respond_to?(:autocompletion=)
24
+ Reline.completion_proc = Proc.new do |str|
25
+
26
+ line = Reline.line_buffer
27
+ str = str.to_s
28
+
29
+ custom = []
30
+ registered = false
31
+ if defined?($rubsh_completion_registry) && defined?(ShellParser)
32
+ begin
33
+ if line =~ /\A\s*([^\s|]+)\s*\z/ && !line.end_with?(' ')
34
+ command = Regexp.last_match(1)
35
+ command = $rubsh_aliases[command].split.first if defined?($rubsh_aliases) && $rubsh_aliases[command]
36
+ custom = $rubsh_completion_registry.suggestions([command], '')
37
+ registered = !$rubsh_completion_registry[command].nil?
38
+
39
+ else
40
+ before = str.empty? ? line : line[0...-str.length]
41
+ words = ShellParser.parse(before.empty? ? 'x' : before).stages.first.words.map(&:value)
42
+ command = words.first
43
+ command = $rubsh_aliases[command].split.first if defined?($rubsh_aliases) && $rubsh_aliases[command]
44
+ words[0] = command
45
+ custom = $rubsh_completion_registry.suggestions(words, str)
46
+ registered = !$rubsh_completion_registry[command].nil?
47
+
48
+ end
49
+ rescue SyntaxError
50
+ custom = []
51
+ end
52
+ end
53
+ registered ? custom.uniq : (custom + get_local_dirs(str) + get_executables(str)).uniq
8
54
  end
9
55
 
10
56
  def commands_in_path
@@ -20,3 +66,7 @@ def commands_in_path
20
66
  end
21
67
  return commands
22
68
  end
69
+ COMMANDS_IN_PATH = commands_in_path.freeze
70
+
71
+
72
+ # Reline's output modifier also receives subprocess output. Do not rewrite it as input.