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 +7 -0
- data/README.rdoc +31 -12
- data/bin/rubsh +9 -7
- data/lib/alias.rb +23 -13
- data/lib/commands.rb +64 -66
- data/lib/completion.rb +111 -0
- data/lib/rub_readline.rb +35 -10
- data/lib/rubsh.rb +460 -123
- data/lib/shell_parser.rb +141 -0
- data/lib/shell_pipeline.rb +131 -0
- data/rubsh.gemspec +6 -4
- data/spec/completion_spec.rb +45 -0
- data/spec/rubsh_spec.rb +177 -0
- data/test/rubsh_test.rb +45 -0
- metadata +48 -49
- data/Manifest +0 -11
data/lib/rubsh.rb
CHANGED
|
@@ -4,143 +4,480 @@ require 'rub_readline'
|
|
|
4
4
|
require 'prompt'
|
|
5
5
|
require 'etc'
|
|
6
6
|
require 'alias'
|
|
7
|
+
require 'shell_parser'
|
|
8
|
+
require 'shell_pipeline'
|
|
9
|
+
require 'completion'
|
|
10
|
+
|
|
7
11
|
|
|
8
12
|
def ralias(str)
|
|
9
|
-
|
|
13
|
+
name, value = str.to_s.strip.split(/\s+/, 2)
|
|
14
|
+
[name, value]
|
|
10
15
|
end
|
|
11
16
|
|
|
12
17
|
class Rubsh
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
attr_reader :commands
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@commands = Commands.new
|
|
22
|
+
@binding = @commands.get_binding
|
|
23
|
+
@aliases = Alias.new
|
|
24
|
+
@block = []
|
|
25
|
+
@block_depth = 0
|
|
26
|
+
@function_sources = {}
|
|
27
|
+
@completions = CompletionRegistry.new
|
|
28
|
+
|
|
29
|
+
@binding.eval('$PATH = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@commands.shell_executor = proc { |name, args| execute_ruby_command(name, args) }
|
|
33
|
+
@commands.shell_source_executor = proc { |source| execute_pipeline(ShellParser.parse(source)) }
|
|
34
|
+
@commands.completion_executor = proc { |name, &block| @completions.complete(name, &block) }
|
|
35
|
+
$rubsh_completion_registry = @completions
|
|
36
|
+
$rubsh_aliases = @aliases
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
Signal.trap('INT') { warn 'Enter exit or quit to exit shell' }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.iscmd?(cmd = nil)
|
|
45
|
+
return false unless cmd
|
|
46
|
+
executable?(cmd.to_s.split.first)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.executable?(name)
|
|
50
|
+
return false if name.nil? || name.empty?
|
|
51
|
+
return File.executable?(name) if name.include?(File::SEPARATOR)
|
|
52
|
+
ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? do |dir|
|
|
53
|
+
path = File.join(dir, name)
|
|
54
|
+
File.executable?(path) && !File.directory?(path)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def alias(*args)
|
|
59
|
+
return @aliases.to_h if args.empty?
|
|
60
|
+
define_alias(args.shift, args.join(' '))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def source(filename)
|
|
64
|
+
ruby_lines = []
|
|
65
|
+
File.foreach(filename) do |line|
|
|
66
|
+
directive = line.strip
|
|
67
|
+
if directive =~ /\Aalias\s+(\S+)\s+(.+)\z/
|
|
68
|
+
define_alias($1, $2)
|
|
69
|
+
else
|
|
70
|
+
ruby_lines << line
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
source = ruby_lines.join
|
|
74
|
+
source.scan(/^\s*def\s+([A-Za-z_]\w*)/).each { |match| @function_sources[match.first] = source }
|
|
75
|
+
eval(source, @binding, filename) unless source.empty?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def parse_cmd(line)
|
|
79
|
+
return exit if line.nil?
|
|
80
|
+
return if line.to_s.strip.empty?
|
|
81
|
+
line = line.to_s
|
|
82
|
+
if line.match?(/\A\s*def\b.*\bend\s*\|/)
|
|
83
|
+
warn 'rubsh: cannot pipe a function definition; define it before running the pipeline'
|
|
84
|
+
return 2
|
|
85
|
+
end
|
|
86
|
+
if line =~ /\A(.+?)\s*\|\s*complete\s+--from-fish\s*\z/
|
|
87
|
+
imported = execute_pipeline(ShellParser.parse(Regexp.last_match(1)), capture: true)
|
|
88
|
+
@completions.import_fish(imported)
|
|
89
|
+
return 0
|
|
90
|
+
end
|
|
91
|
+
if line =~ /\A(.+?)\s*\|\s*source\s*\z/
|
|
92
|
+
generated = execute_pipeline(ShellParser.parse(Regexp.last_match(1)), capture: true)
|
|
93
|
+
eval(generated, @binding, '(pipeline source)')
|
|
94
|
+
return 0
|
|
95
|
+
end
|
|
96
|
+
if line =~ /\A(def\s+[A-Za-z_]\w*\s*;.*?;\s*end;?)\s*(.+)\z/
|
|
97
|
+
accept_ruby_line(Regexp.last_match(1))
|
|
98
|
+
rest = Regexp.last_match(2).sub(/\A\s*;\s*/, '')
|
|
99
|
+
return parse_cmd(rest) unless rest.empty?
|
|
100
|
+
return 0
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
return accept_ruby_line(line) if collecting_ruby?
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if line =~ /\Acomplete\s+-e\s+(\S+)\s*\z/
|
|
108
|
+
@completions.erase($1)
|
|
109
|
+
return 0
|
|
110
|
+
elsif line =~ /\Acomplete\s+(\S+)\s*\z/
|
|
111
|
+
puts @completions.show($1)
|
|
112
|
+
return 0
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if ruby_expression?(line)
|
|
116
|
+
expression = line.match?(/\$(?:PATH|argv|rubsh_status)/) ? line : expand_environment(line)
|
|
117
|
+
eval(expression, @binding)
|
|
118
|
+
return 0
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if line =~ /\Asource\s+(.+)\z/
|
|
122
|
+
path = expand_tilde(Regexp.last_match(1).strip)
|
|
123
|
+
unless File.file?(path)
|
|
124
|
+
warn "rubsh: source file not found: #{path}"
|
|
125
|
+
return 1
|
|
27
126
|
end
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
source(path)
|
|
128
|
+
return 0
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if line =~ /\A(?:alias|ralias)\s+(\S+)\s+(.+)\z/
|
|
132
|
+
define_alias($1, $2)
|
|
133
|
+
return 0
|
|
134
|
+
end
|
|
135
|
+
if line =~ /\Adef\s+-([esr])\s+([A-Za-z_]\w*)\s*\z/
|
|
136
|
+
return edit_function($2) if $1 == 'e'
|
|
137
|
+
return remove_function($2) if $1 == 'r'
|
|
138
|
+
return save_function($2)
|
|
139
|
+
end
|
|
140
|
+
if line =~ /\Atype\s+([A-Za-z_]\w*)\s*\z/
|
|
141
|
+
return report_type($1)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
return accept_ruby_line(line) if ruby_block_start?(line)
|
|
145
|
+
|
|
146
|
+
if line.strip =~ /\A\$([A-Za-z_]\w*)\z/
|
|
147
|
+
puts ENV.fetch($1, '')
|
|
148
|
+
return 0
|
|
149
|
+
end
|
|
150
|
+
if line.strip =~ /\A(puts|print|p)\s+\$([A-Za-z_]\w*)\z/
|
|
151
|
+
value = ENV.fetch($2, '')
|
|
152
|
+
Kernel.public_send($1, value)
|
|
153
|
+
return 0
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
return exit if line.strip =~ /\A(?:exit|quit)\z/
|
|
157
|
+
|
|
158
|
+
execute_pipeline(expand_alias(ShellParser.parse(line)))
|
|
159
|
+
rescue SyntaxError, StandardError => error
|
|
160
|
+
warn "rubsh: #{error.message}"
|
|
161
|
+
1
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def execute_pipeline(pipeline, capture: false)
|
|
165
|
+
stages = pipeline.stages.map { |stage| expand_stage(stage) }
|
|
166
|
+
sync_path_environment
|
|
167
|
+
|
|
168
|
+
if stages.length == 1 && stages.first.first == 'cd'
|
|
169
|
+
return @commands.call('cd', stages.first.drop(1), stderr: $stderr)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
runtime = stages.map do |argv|
|
|
173
|
+
callable = callable_for(argv.first)
|
|
174
|
+
ShellPipelineStage.new(argv, callable: callable)
|
|
175
|
+
end
|
|
176
|
+
target = capture ? StringIO.new : $stdout
|
|
177
|
+
result = ShellPipeline.new(runtime, stdout: target, stderr: $stderr).run
|
|
178
|
+
capture ? result.stdout.sub(/\n\z/, '') : result.returncode
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def run(command = nil)
|
|
182
|
+
ENV['PR1'] ||= '[%h:%w] %u%% '
|
|
183
|
+
directory = File.join(ENV['HOME'], '.config', 'rubsh')
|
|
184
|
+
defs = File.join(directory, 'defs')
|
|
185
|
+
completions = File.join(directory, 'completions')
|
|
186
|
+
|
|
187
|
+
FileUtils.mkdir_p(completions)
|
|
188
|
+
Dir[File.join(completions, '*.rb')].sort.each { |file| source(file) }
|
|
189
|
+
local_completions = File.expand_path('../completions', __dir__)
|
|
190
|
+
Dir[File.join(local_completions, '*.rb')].sort.each { |file| source(file) }
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
FileUtils.mkdir_p(defs)
|
|
194
|
+
Dir[File.join(defs, '*.rb')].sort.each { |file| source(file) }
|
|
195
|
+
config = File.join(directory, 'config.rb')
|
|
196
|
+
source(config) if File.exist?(config)
|
|
197
|
+
return parse_cmd(command) unless command.nil?
|
|
198
|
+
init_readline
|
|
199
|
+
loop do
|
|
200
|
+
prompt = @block.empty? ? prompt_for_readline : '> '
|
|
201
|
+
line = Reline.readline(prompt, true)
|
|
202
|
+
break if line.nil?
|
|
203
|
+
Reline::HISTORY.pop if line.strip.empty?
|
|
204
|
+
log_cmd(line)
|
|
205
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
206
|
+
status = parse_cmd(line)
|
|
207
|
+
ENV['CMD_DURATION'] = (((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round).to_s
|
|
208
|
+
$rubsh_status = status
|
|
209
|
+
$stdout.write("\n") if $stdout.respond_to?(:tty?) && $stdout.tty?
|
|
210
|
+
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def prompt_text
|
|
215
|
+
return Prompt.parse(ENV['PR1']) unless @commands.command?('sh_prompt')
|
|
216
|
+
@binding.eval('sh_prompt').to_s
|
|
217
|
+
rescue StandardError => error
|
|
218
|
+
warn "rubsh: sh_prompt: #{error.message}"
|
|
219
|
+
Prompt.parse(ENV['PR1'])
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def prompt_for_readline
|
|
223
|
+
lines = prompt_text.split("\n", -1)
|
|
224
|
+
unless lines.length == 1
|
|
225
|
+
$stdout.write(lines[0...-1].join("\n"))
|
|
226
|
+
$stdout.write("\n")
|
|
227
|
+
end
|
|
228
|
+
lines[-1]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def history
|
|
232
|
+
file = File.join(ENV['HOME'], '.rubsh', 'history')
|
|
233
|
+
return unless File.exist?(file)
|
|
234
|
+
File.foreach(file) { |line| puts line }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def log_cmd(command)
|
|
238
|
+
return if command.nil? || command.strip.empty?
|
|
239
|
+
@fh ||= File.open(File.join(ENV['HOME'], '.rubsh', 'history'), 'a+')
|
|
240
|
+
@fh.puts(command)
|
|
241
|
+
@fh.flush
|
|
242
|
+
rescue SystemCallError
|
|
243
|
+
@fh = nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def init_readline
|
|
247
|
+
file = File.join(ENV['HOME'], '.rubsh', 'history')
|
|
248
|
+
return unless File.exist?(file)
|
|
249
|
+
File.foreach(file) { |line| Reline::HISTORY.push(line.chomp) }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
private
|
|
253
|
+
|
|
254
|
+
def collecting_ruby?
|
|
255
|
+
!@block.empty?
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def ruby_block_start?(line)
|
|
259
|
+
line.lstrip.start_with?('def ')
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def accept_ruby_line(line)
|
|
263
|
+
@block << line
|
|
264
|
+
@block_depth += line.scan(/\bdef\b/).length
|
|
265
|
+
@block_depth -= line.scan(/\bend\b/).length
|
|
266
|
+
return if @block_depth > 0
|
|
267
|
+
original_source = @block.join("\n")
|
|
268
|
+
source = transform_ruby_source(original_source)
|
|
269
|
+
function_name = original_source[/\A\s*def\s+([A-Za-z_]\w*)/, 1]
|
|
270
|
+
@function_sources[function_name] = original_source if function_name
|
|
271
|
+
@block = []
|
|
272
|
+
eval(source, @binding)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def execute_ruby_command(name, args)
|
|
276
|
+
words = [ShellParser::Word.new([name.to_s])] + args.map { |arg| ShellParser::Word.new([arg.to_s]) }
|
|
277
|
+
execute_pipeline(ShellParser::Pipeline.new([ShellParser::Stage.new(words)]))
|
|
278
|
+
nil
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def transform_ruby_source(source)
|
|
282
|
+
source.lines.map do |line|
|
|
283
|
+
inline = line.match(/^(\s*)def\s+([A-Za-z_]\w*)\s*;\s*([A-Za-z_]\w*(?:\s+.+?))\s*;\s*end\s*;?\s*$/)
|
|
284
|
+
if inline && self.class.executable?(inline[3].split.first)
|
|
285
|
+
"#{inline[1]}def #{inline[2]}; __rubsh_shell__(#{inline[3].inspect}); end\n"
|
|
286
|
+
else
|
|
287
|
+
match = line.match(/^(\s*)([A-Za-z_]\w*)(?:\s+(.+?))?\s*$/)
|
|
288
|
+
if match && self.class.executable?(match[2])
|
|
289
|
+
"#{match[1]}__rubsh_shell__(#{line.strip.inspect})\n"
|
|
290
|
+
else
|
|
291
|
+
line
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end.join
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def save_function(name)
|
|
298
|
+
source = @function_sources[name]
|
|
299
|
+
alias_definition = @aliases[name]
|
|
300
|
+
unless source || alias_definition
|
|
301
|
+
warn "rubsh: function not found: #{name}"
|
|
302
|
+
return 1
|
|
303
|
+
end
|
|
304
|
+
directory = File.join(ENV['HOME'], '.config', 'rubsh', 'defs')
|
|
305
|
+
FileUtils.mkdir_p(directory)
|
|
306
|
+
path = File.join(directory, "#{name}.rb")
|
|
307
|
+
if alias_definition
|
|
308
|
+
File.write(path, "alias #{name} #{alias_definition.inspect}\n")
|
|
309
|
+
else
|
|
310
|
+
File.write(path, format_ruby_definition(source, color: false) + "\n")
|
|
311
|
+
end
|
|
312
|
+
0
|
|
313
|
+
rescue SystemCallError => error
|
|
314
|
+
warn "rubsh: #{error.message}"
|
|
315
|
+
1
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def remove_function(name)
|
|
319
|
+
@function_sources.delete(name)
|
|
320
|
+
@aliases.delete(name)
|
|
321
|
+
method = name.to_sym
|
|
322
|
+
if @commands.singleton_class.instance_methods(false).include?(method)
|
|
323
|
+
@commands.singleton_class.send(:remove_method, method)
|
|
324
|
+
elsif @commands.class.instance_methods(false).include?(method)
|
|
325
|
+
@commands.class.send(:remove_method, method)
|
|
326
|
+
end
|
|
327
|
+
path = File.join(ENV['HOME'], '.config', 'rubsh', 'defs', "#{name}.rb")
|
|
328
|
+
File.delete(path) if File.file?(path)
|
|
329
|
+
0
|
|
330
|
+
rescue SystemCallError => error
|
|
331
|
+
warn "rubsh: #{error.message}"
|
|
332
|
+
1
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def edit_function(name)
|
|
336
|
+
directory = File.join(ENV['HOME'], '.config', 'rubsh', 'defs')
|
|
337
|
+
FileUtils.mkdir_p(directory)
|
|
338
|
+
path = File.join(directory, "#{name}.rb")
|
|
339
|
+
source = @function_sources[name] || "def #{name}\nend\n"
|
|
340
|
+
File.write(path, format_ruby_definition(source, color: false) + "\n") unless File.exist?(path)
|
|
341
|
+
editor = ENV['EDITOR'] || ENV['VISUAL']
|
|
342
|
+
unless editor && !editor.empty?
|
|
343
|
+
warn 'rubsh: $EDITOR is not set'
|
|
344
|
+
return 1
|
|
345
|
+
end
|
|
346
|
+
return 1 unless system(editor, path)
|
|
347
|
+
source(path)
|
|
348
|
+
0
|
|
349
|
+
rescue SystemCallError => error
|
|
350
|
+
warn "rubsh: #{error.message}"
|
|
351
|
+
1
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def report_type(name)
|
|
355
|
+
if (definition = @aliases[name])
|
|
356
|
+
puts "#{name} is a function with definition"
|
|
357
|
+
puts '# Defined via alias'
|
|
358
|
+
puts format_shell_definition(name, definition)
|
|
359
|
+
elsif @commands.command?(name)
|
|
360
|
+
puts "#{name} is a function with definition"
|
|
361
|
+
puts '# Defined via Ruby'
|
|
362
|
+
puts format_ruby_definition(@function_sources[name] || "def #{name}\nend")
|
|
363
|
+
elsif self.class.executable?(name)
|
|
364
|
+
path = ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).map { |dir| File.join(dir, name) }.find { |candidate| File.executable?(candidate) }
|
|
365
|
+
puts "#{name} is #{path}"
|
|
366
|
+
else
|
|
367
|
+
puts "#{name} not found"
|
|
368
|
+
return 1
|
|
369
|
+
end
|
|
370
|
+
0
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def format_ruby_definition(source, color: true)
|
|
374
|
+
lines = source.lines.map(&:rstrip)
|
|
375
|
+
lines.each_with_index.map do |line, index|
|
|
376
|
+
formatted = index > 0 && index < lines.length - 1 && !line.empty? ? " #{line.lstrip}" : line
|
|
377
|
+
highlight_ruby(formatted, color: color)
|
|
378
|
+
end.join("\n")
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def format_shell_definition(name, definition)
|
|
382
|
+
["function #{name}", " #{definition} $argv", 'end'].map { |line| highlight_shell(line) }.join("\n")
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def highlight_ruby(line, color: true)
|
|
386
|
+
return line unless color && $stdout.respond_to?(:tty?) && $stdout.tty?
|
|
387
|
+
line.gsub(/\b(def|end|return)\b/, "\e[35m\\1\e[0m").gsub(/([\"'].*?[\"'])/, "\e[32m\\1\e[0m")
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def highlight_shell(line)
|
|
391
|
+
return line unless $stdout.respond_to?(:tty?) && $stdout.tty?
|
|
392
|
+
line.gsub(/\b(function|end)\b/, "\e[36m\\1\e[0m")
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def define_alias(name, value)
|
|
396
|
+
value = value.strip
|
|
397
|
+
value = value[1...-1] if value.length >= 2 && value[0] == value[-1] && ['"', "'"].include?(value[0])
|
|
398
|
+
@aliases[name.to_s] = value
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def expand_alias(pipeline)
|
|
402
|
+
first = pipeline.stages.first
|
|
403
|
+
return pipeline unless first && first.words.first&.literal?
|
|
404
|
+
name = first.words.first.value
|
|
405
|
+
replacement = @aliases[name]
|
|
406
|
+
return pipeline unless replacement
|
|
407
|
+
raise RuntimeError, "alias recursion: #{name}" if replacement.include?(name)
|
|
408
|
+
replacement_stage = ShellParser.parse(replacement).stages.first
|
|
409
|
+
ShellParser::Pipeline.new([ShellParser::Stage.new(replacement_stage.words + first.words.drop(1))] + pipeline.stages.drop(1))
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def expand_stage(stage)
|
|
413
|
+
stage.words.flat_map do |word|
|
|
414
|
+
value = word.parts.map do |part|
|
|
415
|
+
if part.is_a?(ShellParser::Substitution)
|
|
416
|
+
execute_substitution(part.source)
|
|
417
|
+
else
|
|
418
|
+
expand_tilde(expand_environment(part))
|
|
419
|
+
end
|
|
420
|
+
end.join
|
|
421
|
+
expand_glob(value)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def expand_glob(value)
|
|
426
|
+
return [value] unless value.match?(/[\*\?\[]/)
|
|
427
|
+
matches = Dir.glob(value)
|
|
428
|
+
matches.empty? ? [value] : matches
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def expand_environment(value)
|
|
434
|
+
value.gsub(/\$([A-Za-z_]\w*)/) { ENV.fetch(Regexp.last_match(1), '') }
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def sync_path_environment
|
|
438
|
+
path = @binding.eval('$PATH')
|
|
439
|
+
ENV['PATH'] = path.map(&:to_s).join(File::PATH_SEPARATOR) if path.is_a?(Array)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def expand_tilde(value)
|
|
443
|
+
return value unless value == '~' || value.start_with?('~/')
|
|
444
|
+
File.expand_path(value, ENV['HOME'])
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def ruby_expression?(line)
|
|
448
|
+
return true if line.match?(/\A\s*[A-Za-z_]\w*\s*=\s*[^=]/)
|
|
449
|
+
return true if line.match?(/\A\s*\$PATH\b/)
|
|
450
|
+
return true if line.match?(/\A\s*(?:puts|print|p)\s+\$(?:PATH|argv|rubsh_status)\s*\z/)
|
|
451
|
+
|
|
452
|
+
if line =~ /\A\s*(?:puts|print|p)\s+([a-z_]\w*)\s*\z/
|
|
453
|
+
return @binding.local_variable_defined?($1.to_sym)
|
|
454
|
+
end
|
|
455
|
+
line =~ /\A\s*([a-z_]\w*)\s*\z/ && @binding.local_variable_defined?($1.to_sym)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def execute_substitution(source)
|
|
459
|
+
execute_pipeline(expand_alias(ShellParser.parse(source)), capture: true).to_s
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def callable_for(name)
|
|
463
|
+
return nil if self.class.executable?(name)
|
|
464
|
+
return nil unless @commands.command?(name)
|
|
465
|
+
proc { |argv| @commands.call(name, argv.drop(1)) }
|
|
466
|
+
end
|
|
128
467
|
end
|
|
129
468
|
|
|
130
469
|
class String
|
|
131
470
|
def ls
|
|
132
|
-
Dir.glob
|
|
471
|
+
Dir.glob(self)
|
|
133
472
|
end
|
|
134
473
|
end
|
|
135
474
|
|
|
136
475
|
class Array
|
|
137
|
-
def du(opt='')
|
|
138
|
-
switch =
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
switch = opt
|
|
476
|
+
def du(opt = '')
|
|
477
|
+
switch = opt.is_a?(Symbol) ? "-#{opt}" : opt.to_s
|
|
478
|
+
each do |file|
|
|
479
|
+
pid = Process.spawn('du', switch, file.to_s)
|
|
480
|
+
Process.wait(pid)
|
|
143
481
|
end
|
|
144
|
-
self.each { |f| system("du #{switch} '#{f}'"); }
|
|
145
482
|
end
|
|
146
483
|
end
|