rubsh 0.0.2 → 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
@@ -22,17 +22,16 @@ Run rubsh on the commandline. Type help for a quick overview of usage.
22
22
 
23
23
  == aliases
24
24
 
25
- ralias aliases a command.
25
+ Aliases use shell syntax and remain available for the current session:
26
26
 
27
- Example:
28
- ralias 'ls ls -Gh'
27
+ alias ls "ls -Gh"
29
28
 
30
- Currently an alias will not be parsed to include other aliases. i.e.
31
- doing <tt>ralias 'ls ls -Gh'</tt> followed by <tt>ralias 'll ls -l'</tt> will not get you 'ls -lGh'
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.
32
31
 
33
- == .rubsh/rc.rb
32
+ == ~/.config/rubsh/config.rb
34
33
 
35
- Define your own functions, aliases etc in the rc file. It's included to the environment.
34
+ Define Ruby functions and persistent aliases in this file. Rubsh loads it when the shell starts.
36
35
 
37
36
  == Shortcuts
38
37
 
@@ -71,14 +70,34 @@ use ENV['PR1'] to set your prompt. see 'help prompt' for options from within th
71
70
 
72
71
  Example: ENV['PR1'] = "[%u@%h]--(%t)\n\r[%w]%$ "
73
72
 
74
- == Commandline functions
73
+ == Ruby command functions
75
74
 
76
- Commandline functions must currently be defined as one-liners. They also take an argument, which
77
- is what was typed to invoke the command.
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:
78
77
 
79
- def test(*a); p a;end
80
- $ test foo #=> ["test foo"]
78
+ def foo
79
+ return "ok"
80
+ end
81
81
 
82
+ echo `foo` | tee out.txt
82
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.
83
87
 
88
+ Ruby functions can also call shell commands:
84
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/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,4 +1,4 @@
1
- require 'readline'
1
+ require 'reline'
2
2
  def get_local_dirs(str)
3
3
  Dir[str+'*'].
4
4
  grep( /^#{Regexp.escape(str)}/ ). #only return dirs which start with str
@@ -20,16 +20,37 @@ def get_executables(str)
20
20
  end
21
21
  return commands
22
22
  end
23
- Readline.completion_proc = Proc.new do |str|
24
- completions = get_local_dirs(str)
25
-
26
- # if it's the first thing we're typing in, assume this to be a command and not just a dir.
27
- # iow, we don't do this for second arg allowing executables not to be completed when
28
- # prefixed by example 'cd'.
29
- if Readline.line_buffer =~ /^\s*#{Regexp.escape(str)}/
30
- completions += get_executables(str)
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
31
52
  end
32
- completions
53
+ registered ? custom.uniq : (custom + get_local_dirs(str) + get_executables(str)).uniq
33
54
  end
34
55
 
35
56
  def commands_in_path
@@ -45,3 +66,7 @@ def commands_in_path
45
66
  end
46
67
  return commands
47
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.