shell 0.0.1 → 0.7
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/.gitignore +8 -4
- data/.travis.yml +6 -0
- data/Gemfile +4 -2
- data/LICENSE.txt +22 -0
- data/README.md +90 -8
- data/Rakefile +7 -5
- data/bin/console +7 -0
- data/bin/setup +6 -0
- data/lib/shell.rb +462 -0
- data/lib/shell/builtin-command.rb +147 -0
- data/lib/shell/command-processor.rb +668 -0
- data/lib/shell/error.rb +26 -0
- data/lib/shell/filter.rb +138 -0
- data/lib/shell/process-controller.rb +309 -0
- data/lib/shell/system-command.rb +159 -0
- data/lib/shell/version.rb +17 -0
- data/shell.gemspec +21 -20
- metadata +46 -41
- data/lib/shell_utils.rb +0 -58
- data/lib/shell_utils/version.rb +0 -3
- data/test/shell_utils_test.rb +0 -25
- data/test/test_helper.rb +0 -4
data/lib/shell/error.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
#
|
3
|
+
# shell/error.rb -
|
4
|
+
# $Release Version: 0.7 $
|
5
|
+
# $Revision$
|
6
|
+
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
|
7
|
+
#
|
8
|
+
# --
|
9
|
+
#
|
10
|
+
#
|
11
|
+
#
|
12
|
+
|
13
|
+
require "e2mmap"
|
14
|
+
|
15
|
+
class Shell
|
16
|
+
module Error
|
17
|
+
extend Exception2MessageMapper
|
18
|
+
def_e2message TypeError, "wrong argument type %s (expected %s)"
|
19
|
+
|
20
|
+
def_exception :DirStackEmpty, "Directory stack empty."
|
21
|
+
def_exception :CantDefine, "Can't define method(%s, %s)."
|
22
|
+
def_exception :CantApplyMethod, "This method(%s) does not apply to this type(%s)."
|
23
|
+
def_exception :CommandNotFound, "Command not found(%s)."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
data/lib/shell/filter.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
#
|
3
|
+
# shell/filter.rb -
|
4
|
+
# $Release Version: 0.7 $
|
5
|
+
# $Revision$
|
6
|
+
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
|
7
|
+
#
|
8
|
+
# --
|
9
|
+
#
|
10
|
+
#
|
11
|
+
#
|
12
|
+
|
13
|
+
class Shell #:nodoc:
|
14
|
+
# Any result of command execution is a Filter.
|
15
|
+
#
|
16
|
+
# This class includes Enumerable, therefore a Filter object can use all
|
17
|
+
# Enumerable
|
18
|
+
# facilities.
|
19
|
+
#
|
20
|
+
class Filter
|
21
|
+
include Enumerable
|
22
|
+
|
23
|
+
def initialize(sh)
|
24
|
+
@shell = sh # parent shell
|
25
|
+
@input = nil # input filter
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :input
|
29
|
+
|
30
|
+
def input=(filter)
|
31
|
+
@input = filter
|
32
|
+
end
|
33
|
+
|
34
|
+
# call-seq:
|
35
|
+
# each(record_separator=nil) { block }
|
36
|
+
#
|
37
|
+
# Iterates a block for each line.
|
38
|
+
def each(rs = nil)
|
39
|
+
rs = @shell.record_separator unless rs
|
40
|
+
if @input
|
41
|
+
@input.each(rs){|l| yield l}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# call-seq:
|
46
|
+
# < source
|
47
|
+
#
|
48
|
+
# Inputs from +source+, which is either a string of a file name or an IO
|
49
|
+
# object.
|
50
|
+
def <(src)
|
51
|
+
case src
|
52
|
+
when String
|
53
|
+
cat = Cat.new(@shell, src)
|
54
|
+
cat | self
|
55
|
+
when IO
|
56
|
+
self.input = src
|
57
|
+
self
|
58
|
+
else
|
59
|
+
Shell.Fail Error::CantApplyMethod, "<", src.class
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# call-seq:
|
64
|
+
# > source
|
65
|
+
#
|
66
|
+
# Outputs from +source+, which is either a string of a file name or an IO
|
67
|
+
# object.
|
68
|
+
def >(to)
|
69
|
+
case to
|
70
|
+
when String
|
71
|
+
dst = @shell.open(to, "w")
|
72
|
+
begin
|
73
|
+
each(){|l| dst << l}
|
74
|
+
ensure
|
75
|
+
dst.close
|
76
|
+
end
|
77
|
+
when IO
|
78
|
+
each(){|l| to << l}
|
79
|
+
else
|
80
|
+
Shell.Fail Error::CantApplyMethod, ">", to.class
|
81
|
+
end
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
# call-seq:
|
86
|
+
# >> source
|
87
|
+
#
|
88
|
+
# Appends the output to +source+, which is either a string of a file name
|
89
|
+
# or an IO object.
|
90
|
+
def >>(to)
|
91
|
+
begin
|
92
|
+
Shell.cd(@shell.pwd).append(to, self)
|
93
|
+
rescue CantApplyMethod
|
94
|
+
Shell.Fail Error::CantApplyMethod, ">>", to.class
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# call-seq:
|
99
|
+
# | filter
|
100
|
+
#
|
101
|
+
# Processes a pipeline.
|
102
|
+
def |(filter)
|
103
|
+
filter.input = self
|
104
|
+
if active?
|
105
|
+
@shell.process_controller.start_job filter
|
106
|
+
end
|
107
|
+
filter
|
108
|
+
end
|
109
|
+
|
110
|
+
# call-seq:
|
111
|
+
# filter1 + filter2
|
112
|
+
#
|
113
|
+
# Outputs +filter1+, and then +filter2+ using Join.new
|
114
|
+
def +(filter)
|
115
|
+
Join.new(@shell, self, filter)
|
116
|
+
end
|
117
|
+
|
118
|
+
def to_a
|
119
|
+
ary = []
|
120
|
+
each(){|l| ary.push l}
|
121
|
+
ary
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_s
|
125
|
+
str = ""
|
126
|
+
each(){|l| str.concat l}
|
127
|
+
str
|
128
|
+
end
|
129
|
+
|
130
|
+
def inspect
|
131
|
+
if @shell.debug.kind_of?(Integer) && @shell.debug > 2
|
132
|
+
super
|
133
|
+
else
|
134
|
+
to_s
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,309 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
#
|
3
|
+
# shell/process-controller.rb -
|
4
|
+
# $Release Version: 0.7 $
|
5
|
+
# $Revision$
|
6
|
+
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
|
7
|
+
#
|
8
|
+
# --
|
9
|
+
#
|
10
|
+
#
|
11
|
+
#
|
12
|
+
require "forwardable"
|
13
|
+
require "sync"
|
14
|
+
|
15
|
+
class Shell
|
16
|
+
class ProcessController
|
17
|
+
|
18
|
+
@ProcessControllers = {}
|
19
|
+
@ProcessControllersMonitor = Thread::Mutex.new
|
20
|
+
@ProcessControllersCV = Thread::ConditionVariable.new
|
21
|
+
|
22
|
+
@BlockOutputMonitor = Thread::Mutex.new
|
23
|
+
@BlockOutputCV = Thread::ConditionVariable.new
|
24
|
+
|
25
|
+
class << self
|
26
|
+
extend Forwardable
|
27
|
+
|
28
|
+
def_delegator("@ProcessControllersMonitor",
|
29
|
+
"synchronize", "process_controllers_exclusive")
|
30
|
+
|
31
|
+
def active_process_controllers
|
32
|
+
process_controllers_exclusive do
|
33
|
+
@ProcessControllers.dup
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def activate(pc)
|
38
|
+
process_controllers_exclusive do
|
39
|
+
@ProcessControllers[pc] ||= 0
|
40
|
+
@ProcessControllers[pc] += 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def inactivate(pc)
|
45
|
+
process_controllers_exclusive do
|
46
|
+
if @ProcessControllers[pc]
|
47
|
+
if (@ProcessControllers[pc] -= 1) == 0
|
48
|
+
@ProcessControllers.delete(pc)
|
49
|
+
@ProcessControllersCV.signal
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def each_active_object
|
56
|
+
process_controllers_exclusive do
|
57
|
+
for ref in @ProcessControllers.keys
|
58
|
+
yield ref
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def block_output_synchronize(&b)
|
64
|
+
@BlockOutputMonitor.synchronize(&b)
|
65
|
+
end
|
66
|
+
|
67
|
+
def wait_to_finish_all_process_controllers
|
68
|
+
process_controllers_exclusive do
|
69
|
+
while !@ProcessControllers.empty?
|
70
|
+
Shell::notify("Process finishing, but active shell exists",
|
71
|
+
"You can use Shell#transact or Shell#check_point for more safe execution.")
|
72
|
+
if Shell.debug?
|
73
|
+
for pc in @ProcessControllers.keys
|
74
|
+
Shell::notify(" Not finished jobs in "+pc.shell.to_s)
|
75
|
+
for com in pc.jobs
|
76
|
+
com.notify(" Jobs: %id")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
@ProcessControllersCV.wait(@ProcessControllersMonitor)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# for shell-command complete finish at this process exit.
|
87
|
+
USING_AT_EXIT_WHEN_PROCESS_EXIT = true
|
88
|
+
at_exit do
|
89
|
+
wait_to_finish_all_process_controllers unless $@
|
90
|
+
end
|
91
|
+
|
92
|
+
def initialize(shell)
|
93
|
+
@shell = shell
|
94
|
+
@waiting_jobs = []
|
95
|
+
@active_jobs = []
|
96
|
+
@jobs_sync = Sync.new
|
97
|
+
|
98
|
+
@job_monitor = Thread::Mutex.new
|
99
|
+
@job_condition = Thread::ConditionVariable.new
|
100
|
+
end
|
101
|
+
|
102
|
+
attr_reader :shell
|
103
|
+
|
104
|
+
def jobs
|
105
|
+
jobs = []
|
106
|
+
@jobs_sync.synchronize(:SH) do
|
107
|
+
jobs.concat @waiting_jobs
|
108
|
+
jobs.concat @active_jobs
|
109
|
+
end
|
110
|
+
jobs
|
111
|
+
end
|
112
|
+
|
113
|
+
def active_jobs
|
114
|
+
@active_jobs
|
115
|
+
end
|
116
|
+
|
117
|
+
def waiting_jobs
|
118
|
+
@waiting_jobs
|
119
|
+
end
|
120
|
+
|
121
|
+
def jobs_exist?
|
122
|
+
@jobs_sync.synchronize(:SH) do
|
123
|
+
@active_jobs.empty? or @waiting_jobs.empty?
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def active_jobs_exist?
|
128
|
+
@jobs_sync.synchronize(:SH) do
|
129
|
+
@active_jobs.empty?
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def waiting_jobs_exist?
|
134
|
+
@jobs_sync.synchronize(:SH) do
|
135
|
+
@waiting_jobs.empty?
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# schedule a command
|
140
|
+
def add_schedule(command)
|
141
|
+
@jobs_sync.synchronize(:EX) do
|
142
|
+
ProcessController.activate(self)
|
143
|
+
if @active_jobs.empty?
|
144
|
+
start_job command
|
145
|
+
else
|
146
|
+
@waiting_jobs.push(command)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# start a job
|
152
|
+
def start_job(command = nil)
|
153
|
+
@jobs_sync.synchronize(:EX) do
|
154
|
+
if command
|
155
|
+
return if command.active?
|
156
|
+
@waiting_jobs.delete command
|
157
|
+
else
|
158
|
+
command = @waiting_jobs.shift
|
159
|
+
|
160
|
+
return unless command
|
161
|
+
end
|
162
|
+
@active_jobs.push command
|
163
|
+
command.start
|
164
|
+
|
165
|
+
# start all jobs that input from the job
|
166
|
+
for job in @waiting_jobs.dup
|
167
|
+
start_job(job) if job.input == command
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def waiting_job?(job)
|
173
|
+
@jobs_sync.synchronize(:SH) do
|
174
|
+
@waiting_jobs.include?(job)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def active_job?(job)
|
179
|
+
@jobs_sync.synchronize(:SH) do
|
180
|
+
@active_jobs.include?(job)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# terminate a job
|
185
|
+
def terminate_job(command)
|
186
|
+
@jobs_sync.synchronize(:EX) do
|
187
|
+
@active_jobs.delete command
|
188
|
+
ProcessController.inactivate(self)
|
189
|
+
if @active_jobs.empty?
|
190
|
+
command.notify("start_job in terminate_job(%id)", Shell::debug?)
|
191
|
+
start_job
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# kill a job
|
197
|
+
def kill_job(sig, command)
|
198
|
+
@jobs_sync.synchronize(:EX) do
|
199
|
+
if @waiting_jobs.delete command
|
200
|
+
ProcessController.inactivate(self)
|
201
|
+
return
|
202
|
+
elsif @active_jobs.include?(command)
|
203
|
+
begin
|
204
|
+
r = command.kill(sig)
|
205
|
+
ProcessController.inactivate(self)
|
206
|
+
rescue
|
207
|
+
print "Shell: Warn: $!\n" if @shell.verbose?
|
208
|
+
return nil
|
209
|
+
end
|
210
|
+
@active_jobs.delete command
|
211
|
+
r
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# wait for all jobs to terminate
|
217
|
+
def wait_all_jobs_execution
|
218
|
+
@job_monitor.synchronize do
|
219
|
+
begin
|
220
|
+
while !jobs.empty?
|
221
|
+
@job_condition.wait(@job_monitor)
|
222
|
+
for job in jobs
|
223
|
+
job.notify("waiting job(%id)", Shell::debug?)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
ensure
|
227
|
+
redo unless jobs.empty?
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# simple fork
|
233
|
+
def sfork(command)
|
234
|
+
pipe_me_in, pipe_peer_out = IO.pipe
|
235
|
+
pipe_peer_in, pipe_me_out = IO.pipe
|
236
|
+
|
237
|
+
|
238
|
+
pid = nil
|
239
|
+
pid_mutex = Thread::Mutex.new
|
240
|
+
pid_cv = Thread::ConditionVariable.new
|
241
|
+
|
242
|
+
Thread.start do
|
243
|
+
ProcessController.block_output_synchronize do
|
244
|
+
STDOUT.flush
|
245
|
+
ProcessController.each_active_object do |pc|
|
246
|
+
for jobs in pc.active_jobs
|
247
|
+
jobs.flush
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
pid = fork {
|
252
|
+
Thread.list.each do |th|
|
253
|
+
th.kill unless Thread.current == th
|
254
|
+
end
|
255
|
+
|
256
|
+
STDIN.reopen(pipe_peer_in)
|
257
|
+
STDOUT.reopen(pipe_peer_out)
|
258
|
+
|
259
|
+
ObjectSpace.each_object(IO) do |io|
|
260
|
+
if ![STDIN, STDOUT, STDERR].include?(io)
|
261
|
+
io.close
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
yield
|
266
|
+
}
|
267
|
+
end
|
268
|
+
pid_cv.signal
|
269
|
+
|
270
|
+
pipe_peer_in.close
|
271
|
+
pipe_peer_out.close
|
272
|
+
command.notify "job(%name:##{pid}) start", @shell.debug?
|
273
|
+
|
274
|
+
begin
|
275
|
+
_pid = nil
|
276
|
+
command.notify("job(%id) start to waiting finish.", @shell.debug?)
|
277
|
+
_pid = Process.waitpid(pid, nil)
|
278
|
+
rescue Errno::ECHILD
|
279
|
+
command.notify "warn: job(%id) was done already waitpid."
|
280
|
+
_pid = true
|
281
|
+
ensure
|
282
|
+
command.notify("Job(%id): Wait to finish when Process finished.", @shell.debug?)
|
283
|
+
# when the process ends, wait until the command terminates
|
284
|
+
if USING_AT_EXIT_WHEN_PROCESS_EXIT or _pid
|
285
|
+
else
|
286
|
+
command.notify("notice: Process finishing...",
|
287
|
+
"wait for Job[%id] to finish.",
|
288
|
+
"You can use Shell#transact or Shell#check_point for more safe execution.")
|
289
|
+
redo
|
290
|
+
end
|
291
|
+
|
292
|
+
@job_monitor.synchronize do
|
293
|
+
terminate_job(command)
|
294
|
+
@job_condition.signal
|
295
|
+
command.notify "job(%id) finish.", @shell.debug?
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
pid_mutex.synchronize do
|
301
|
+
while !pid
|
302
|
+
pid_cv.wait(pid_mutex)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
return pid, pipe_me_in, pipe_me_out
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|