/
sfl.rb
214 lines (194 loc) · 4.42 KB
/
sfl.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
class SFL
VERSION = "2.2".freeze
SHELL_SPECIALS = %r([\*\?\{\}\[\]<>\(\)~&\|\\\$;'`"\n])m.freeze
attr_reader :command, :environment, :argument, :option
# SFL.new('ls', '-a') becomes
# @environment = {}
# @command = ['ls', 'ls']
# @argument = ['-a']
# @option = {}
def initialize(*cmdandarg)
raise ArgumentError if cmdandarg.size == 0
cmdandarg = cmdandarg.dup
@environment =
if Hash === cmdandarg.first
cmdandarg.shift
else
{}
end
@option =
if Hash === cmdandarg.last
cmdandarg.pop
else
{}
end
if cmdandarg.size == 1
cmdandarg = cmdandarg.first
if String === cmdandarg
if SHELL_SPECIALS === cmdandarg
@command = cmdandarg
@argument = []
else
cmd, *arg = self.class.parse_command_with_arg(cmdandarg)
@command = [cmd, cmd]
@argument = arg
end
else
@command = cmdandarg
@argument = []
end
else
# 'ls', '.' -> [['ls', 'ls'], '.']
cmd = cmdandarg.shift
cmd = (String === cmd) ? [cmd, cmd] : cmd
@command = cmd
@argument = cmdandarg
end
end
def run
fork {
@environment.each do |k, v|
ENV[k] = v
end
self.class.option_parser(@option).each do |ast|
self.class.eval_ast ast
end
exec(@command, *@argument)
}
end
def ==(o) # Mostly for rspec
instance_variables.all? do |i|
i = i[1..-1] # '@a' -> 'a'
eval "self.#{i} == o.#{i}"
end
end
class << self
REDIRECTION_MAPPING = {
:in => STDIN,
:out => STDOUT,
:err => STDERR,
}
def redirection_ast(v, what_for = :out)
case v
when Integer
raise NotImplementedError, "Redirection to integer FD not yet implemented"
when :close
nil
when :in, :out, :err
REDIRECTION_MAPPING[v]
when String # filename
[File, :open, v, (what_for == :in ? 'r' : 'w')]
when Array # filename with option
[File, :open, v[0], v[1]]
when IO
v
end
end
def option_parser(hash)
result = []
# changing dir has high priority
chdir = hash.delete(:chdir)
if chdir
result[0] = [Dir, :chdir, chdir]
end
# other options
result += hash.map {|k, v|
case k
when :in, :out, :err
if right = redirection_ast(v, k)
[[REDIRECTION_MAPPING[k], :reopen, right]]
else
[[REDIRECTION_MAPPING[k], :close]]
end
when Array
# assuming k is like [:out, :err]
raise NotImplementedError if k.size > 2
left1, left2 = *k.map {|i| REDIRECTION_MAPPING[i] }
if right = redirection_ast(v)
[
[left1, :reopen, right],
[left2, :reopen, left1],
]
else
[
[left1, :close],
[left2, :close],
]
end
end
}.flatten(1)
result
end
def eval_ast(ast)
case ast
when Array
if ast.size > 2
eval_ast(ast[0]).send(ast[1], *ast[2..-1].map {|i| eval_ast(i) })
else
eval_ast(ast[0]).send(ast[1])
end
else
ast
end
end
def parse_command_with_arg(x)
in_squote = false
in_dquote = false
tmp = ''
cmdargs = []
x.strip.split(//).each do |c|
case c
when '"'
if in_dquote
in_dquote = false
else
in_dquote = true
end
when "'"
if in_squote
in_squote = false
else
in_squote = true
end
when ' '
if in_dquote || in_squote
tmp << ' '
else
cmdargs << tmp
tmp = ''
end
else
tmp << c
end
end
cmdargs << tmp
end
end
end
if RUBY_VERSION < "1.9"
def Kernel.spawn(*x)
SFL.new(*x).run
end
def spawn(*x)
Kernel.spawn(*x)
end
def Process.spawn(*x)
SFL.new(*x).run
end
end
if RUBY_VERSION <= '1.8.6'
class Array
alias orig_flatten flatten
def flatten(depth = -1)
if depth < 0
orig_flatten
elsif depth == 0
self
else
inject([]) {|m, i|
Array === i ? m + i : m << i
}.flatten(depth - 1)
end
end
end
end