peruby 0.1.0
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/.rubocop.yml +35 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +51 -0
- data/Rakefile +92 -0
- data/bin/peruby +9 -0
- data/doc/COMPAT.md +54 -0
- data/doc/CONTRIBUTING.md +21 -0
- data/doc/DESIGN.md +25 -0
- data/doc/INCOMPATIBILITIES.md +30 -0
- data/doc/PERF.md +51 -0
- data/doc/ROADMAP.md +18 -0
- data/examples/hello.pl +1 -0
- data/examples/json.pl +2 -0
- data/examples/object.pl +5 -0
- data/examples/word_count.pl +6 -0
- data/lib/peruby/cli.rb +224 -0
- data/lib/peruby/compile_unit.rb +103 -0
- data/lib/peruby/compiler.rb +224 -0
- data/lib/peruby/errors.rb +40 -0
- data/lib/peruby/lexer/heredoc.rb +8 -0
- data/lib/peruby/lexer/keywords.rb +63 -0
- data/lib/peruby/lexer/number.rb +32 -0
- data/lib/peruby/lexer/quote_like.rb +72 -0
- data/lib/peruby/lexer/source_scanner.rb +104 -0
- data/lib/peruby/lexer/state.rb +46 -0
- data/lib/peruby/lexer/structure_scanner.rb +149 -0
- data/lib/peruby/lexer/term_scanner.rb +301 -0
- data/lib/peruby/lexer/token.rb +16 -0
- data/lib/peruby/lexer.rb +123 -0
- data/lib/peruby/node.rb +88 -0
- data/lib/peruby/op/assign.rb +128 -0
- data/lib/peruby/op/builtin.rb +903 -0
- data/lib/peruby/op/call.rb +378 -0
- data/lib/peruby/op/control.rb +256 -0
- data/lib/peruby/op/element.rb +136 -0
- data/lib/peruby/op/expression.rb +342 -0
- data/lib/peruby/op/io.rb +113 -0
- data/lib/peruby/op/list.rb +102 -0
- data/lib/peruby/op/literal.rb +84 -0
- data/lib/peruby/op/loop.rb +158 -0
- data/lib/peruby/op/regexp.rb +288 -0
- data/lib/peruby/op/variable.rb +534 -0
- data/lib/peruby/op.rb +47 -0
- data/lib/peruby/parser/grammar.rb +5797 -0
- data/lib/peruby/parser/grammar.y +576 -0
- data/lib/peruby/parser.rb +14 -0
- data/lib/peruby/runtime/code.rb +21 -0
- data/lib/peruby/runtime/conv.rb +140 -0
- data/lib/peruby/runtime/directory_handle.rb +18 -0
- data/lib/peruby/runtime/env.rb +129 -0
- data/lib/peruby/runtime/glob.rb +24 -0
- data/lib/peruby/runtime/interpolation.rb +223 -0
- data/lib/peruby/runtime/io_handle.rb +37 -0
- data/lib/peruby/runtime/local_stack.rb +90 -0
- data/lib/peruby/runtime/match_state.rb +62 -0
- data/lib/peruby/runtime/module_loader.rb +133 -0
- data/lib/peruby/runtime/mro.rb +94 -0
- data/lib/peruby/runtime/perl_array.rb +81 -0
- data/lib/peruby/runtime/perl_hash.rb +57 -0
- data/lib/peruby/runtime/ref.rb +43 -0
- data/lib/peruby/runtime/regexp_compiler.rb +75 -0
- data/lib/peruby/runtime/scalar.rb +27 -0
- data/lib/peruby/runtime/sprintf.rb +54 -0
- data/lib/peruby/runtime/stash.rb +50 -0
- data/lib/peruby/runtime/test_builder.rb +47 -0
- data/lib/peruby/runtime.rb +325 -0
- data/lib/peruby/validator.rb +236 -0
- data/lib/peruby/version.rb +5 -0
- data/lib/peruby.rb +31 -0
- data/t/00-basic.t +5 -0
- data/t/lib/MiniTest.pm +22 -0
- metadata +130 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
|
|
6
|
+
module Peruby
|
|
7
|
+
module Op
|
|
8
|
+
# Standard-library-backed Perl built-ins that do not need custom syntax.
|
|
9
|
+
class Builtin < Base # rubocop:disable Metrics/ClassLength
|
|
10
|
+
NAMES = %w[
|
|
11
|
+
open close binmode seek tell fileno eof select opendir readdir closedir rewinddir stat lstat chmod chown utime
|
|
12
|
+
flock
|
|
13
|
+
glob
|
|
14
|
+
length substr index rindex uc lc ucfirst lcfirst reverse join sprintf
|
|
15
|
+
chomp chop chr ord quotemeta pop shift unshift splice keys values each sort defined scalar wantarray
|
|
16
|
+
abs int sqrt sin
|
|
17
|
+
cos atan2 exp log hex oct rand srand time localtime gmtime sleep times pack unpack mkdir rmdir unlink rename
|
|
18
|
+
link
|
|
19
|
+
symlink readlink truncate system exec fork wait waitpid kill exit getppid warn die first sum sum0 min max
|
|
20
|
+
minstr maxstr reduce any all none shuffle uniq pairs encode decode gettimeofday tempfile tempdir GetOptions
|
|
21
|
+
INT_MAX O_RDONLY O_WRONLY O_RDWR O_CREAT O_TRUNC SEEK_SET SEEK_CUR SEEK_END EACCES ENOENT
|
|
22
|
+
plan ok is isnt like is_deeply pass fail subtest done_testing diag
|
|
23
|
+
blessed reftype refaddr weaken dualvar looks_like_number floor ceil fmod strftime Dumper
|
|
24
|
+
basename dirname catfile catdir rel2abs getcwd abs_path mkpath rmtree encode_json decode_json
|
|
25
|
+
md5_hex sha1_hex sha256_hex dclone carp croak confess cluck
|
|
26
|
+
].freeze
|
|
27
|
+
MODULE_NAMES = %w[
|
|
28
|
+
first sum sum0 min max minstr maxstr reduce any all none shuffle uniq pairs encode decode gettimeofday
|
|
29
|
+
tempfile tempdir GetOptions INT_MAX O_RDONLY O_WRONLY O_RDWR O_CREAT O_TRUNC SEEK_SET SEEK_CUR SEEK_END
|
|
30
|
+
EACCES ENOENT
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
def self.supports?(name) = NAMES.include?(name)
|
|
34
|
+
|
|
35
|
+
def initialize(name, arguments, file: '-e', line: 1)
|
|
36
|
+
@name = name
|
|
37
|
+
@arguments = arguments
|
|
38
|
+
@file = file
|
|
39
|
+
@line = line
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def run(env, context)
|
|
43
|
+
code = env.runtime.stash.fetch(@name)&.code if MODULE_NAMES.include?(@name)
|
|
44
|
+
return env.runtime.call(code, @arguments.flat_map { |argument| argument.argument_cells(env) }, context) if code
|
|
45
|
+
|
|
46
|
+
result = dispatch(env, context)
|
|
47
|
+
return result if result.is_a?(Array) && result.all?(Scalar)
|
|
48
|
+
|
|
49
|
+
context == :list ? Array(result).map { |value| Scalar.new(value) } : result
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def lvalue(env, context)
|
|
53
|
+
return super unless @name == 'substr'
|
|
54
|
+
|
|
55
|
+
cell = @arguments.first.lvalue(env, :scalar)
|
|
56
|
+
offset = Conv.to_num(@arguments[1].run(env, :scalar)).to_i
|
|
57
|
+
length = @arguments[2] && Conv.to_num(@arguments[2].run(env, :scalar)).to_i
|
|
58
|
+
SubstringCell.new(cell, offset, length)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
64
|
+
def dispatch(env, context)
|
|
65
|
+
args = scalar_values(env)
|
|
66
|
+
case @name
|
|
67
|
+
when 'length' then Conv.to_str(args[0]).length
|
|
68
|
+
when 'uc' then Conv.to_str(args[0]).upcase
|
|
69
|
+
when 'lc' then Conv.to_str(args[0]).downcase
|
|
70
|
+
when 'ucfirst' then Conv.to_str(args[0]).sub(/./, &:upcase)
|
|
71
|
+
when 'lcfirst' then Conv.to_str(args[0]).sub(/./, &:downcase)
|
|
72
|
+
when 'chr' then Conv.to_num(args[0]).to_i.chr(Encoding::UTF_8)
|
|
73
|
+
when 'ord' then Conv.to_str(args[0]).codepoints.first || 0
|
|
74
|
+
when 'quotemeta' then Regexp.escape(Conv.to_str(args[0]))
|
|
75
|
+
when 'index', 'rindex' then string_index(args)
|
|
76
|
+
when 'substr' then substring(env, args)
|
|
77
|
+
when 'join' then list_values(env).drop(1).map { |value| Conv.to_str(value) }.join(Conv.to_str(args[0]))
|
|
78
|
+
when 'sprintf' then Sprintf.format(Conv.to_str(args[0]), list_values(env).drop(1))
|
|
79
|
+
when 'reverse' then reverse(env, context)
|
|
80
|
+
when 'defined' then args[0].nil? ? '' : 1
|
|
81
|
+
when 'scalar' then @arguments.first&.run(env, :scalar)
|
|
82
|
+
when 'wantarray' then { list: 1, scalar: '', void: nil }.fetch(env.want)
|
|
83
|
+
when 'abs' then overloaded_abs(env, args[0])
|
|
84
|
+
when 'int' then Conv.to_num(args[0]).to_i
|
|
85
|
+
when 'sqrt' then Math.sqrt(Conv.to_num(args[0]))
|
|
86
|
+
when 'sin', 'cos', 'exp', 'log' then Math.public_send(@name, Conv.to_num(args[0]))
|
|
87
|
+
when 'atan2' then Math.atan2(Conv.to_num(args[0]), Conv.to_num(args[1]))
|
|
88
|
+
when 'first', 'reduce', 'any', 'all', 'none' then list_utility_block(env, context)
|
|
89
|
+
when 'sum', 'sum0' then list_sum(env)
|
|
90
|
+
when 'min' then list_values(env).min_by { |value| Conv.to_num(value) }
|
|
91
|
+
when 'max' then list_values(env).max_by { |value| Conv.to_num(value) }
|
|
92
|
+
when 'minstr' then list_values(env).min_by { |value| Conv.to_str(value) }
|
|
93
|
+
when 'maxstr' then list_values(env).max_by { |value| Conv.to_str(value) }
|
|
94
|
+
when 'shuffle' then list_values(env).shuffle
|
|
95
|
+
when 'uniq'
|
|
96
|
+
seen = {}
|
|
97
|
+
list_values(env).select { |value| !seen.key?(Conv.to_str(value)) && (seen[Conv.to_str(value)] = true) }
|
|
98
|
+
when 'pairs' then list_values(env).each_slice(2).map { |pair| pair_reference(pair) }
|
|
99
|
+
when 'plan' then env.runtime.test_builder.plan(Conv.to_num(args.last).to_i)
|
|
100
|
+
when 'ok' then env.runtime.test_builder.ok(Conv.truthy?(args[0]), Conv.to_str(args[1]))
|
|
101
|
+
when 'pass' then env.runtime.test_builder.ok(true, Conv.to_str(args[0]))
|
|
102
|
+
when 'fail' then env.runtime.test_builder.ok(false, Conv.to_str(args[0]))
|
|
103
|
+
when 'is' then env.runtime.test_builder.ok(equal?(args[0], args[1]), Conv.to_str(args[2]))
|
|
104
|
+
when 'isnt' then env.runtime.test_builder.ok(!equal?(args[0], args[1]), Conv.to_str(args[2]))
|
|
105
|
+
when 'like' then env.runtime.test_builder.ok(like?(args[0], args[1]), Conv.to_str(args[2]))
|
|
106
|
+
when 'is_deeply'
|
|
107
|
+
env.runtime.test_builder.ok(deep_value(args[0]) == deep_value(args[1]), Conv.to_str(args[2]))
|
|
108
|
+
when 'done_testing' then env.runtime.test_builder.done_testing
|
|
109
|
+
when 'subtest' then subtest(env, args)
|
|
110
|
+
when 'diag' then env.runtime.stderr.puts("# #{Conv.to_str(args[0])}") && 1
|
|
111
|
+
when 'blessed' then args[0].is_a?(Ref) ? args[0].blessed.to_s : ''
|
|
112
|
+
when 'reftype' then args[0].is_a?(Ref) ? args[0].kind : ''
|
|
113
|
+
when 'refaddr' then args[0].is_a?(Ref) ? args[0].target.object_id : nil
|
|
114
|
+
when 'weaken' then args[0].is_a?(Ref) ? args[0].weaken! : nil
|
|
115
|
+
when 'dualvar' then Dualvar.new(Conv.to_num(args[0]), Conv.to_str(args[1]))
|
|
116
|
+
when 'looks_like_number' then Conv::NUM_RE.match?(Conv.to_str(args[0])) ? 1 : ''
|
|
117
|
+
when 'floor', 'ceil' then Conv.to_num(args[0]).public_send(@name)
|
|
118
|
+
when 'fmod' then Conv.to_num(args[0]) % Conv.to_num(args[1])
|
|
119
|
+
when 'strftime' then posix_strftime(args)
|
|
120
|
+
when 'INT_MAX' then (2**31) - 1
|
|
121
|
+
when 'Dumper' then data_dumper(env, list_values(env))
|
|
122
|
+
when 'basename' then File.basename(Conv.to_str(args[0]))
|
|
123
|
+
when 'dirname' then File.dirname(Conv.to_str(args[0]))
|
|
124
|
+
when 'catfile', 'catdir' then File.join(*args.map { |value| Conv.to_str(value) })
|
|
125
|
+
when 'rel2abs' then File.expand_path(Conv.to_str(args[0]), Conv.to_str(args[1] || Dir.pwd))
|
|
126
|
+
when 'getcwd' then Dir.pwd
|
|
127
|
+
when 'abs_path' then File.realpath(Conv.to_str(args[0]))
|
|
128
|
+
when 'tempfile', 'tempdir' then temporary_value(args, context)
|
|
129
|
+
when 'mkpath' then file_utils.mkdir_p(Conv.to_str(args[0])).last
|
|
130
|
+
when 'rmtree' then file_utils.rm_rf(Conv.to_str(args[0])).length
|
|
131
|
+
when 'encode_json' then json.generate(deep_value(args[0]))
|
|
132
|
+
when 'decode_json' then ruby_value(json.parse(Conv.to_str(args[0])))
|
|
133
|
+
when 'encode' then Conv.to_str(args[1]).encode(Conv.to_str(args[0]))
|
|
134
|
+
when 'decode'
|
|
135
|
+
Conv.to_str(args[1]).dup.force_encoding(Conv.to_str(args[0])).encode(Encoding::UTF_8)
|
|
136
|
+
when 'md5_hex' then digest::MD5.hexdigest(Conv.to_str(args[0]))
|
|
137
|
+
when 'sha1_hex' then digest::SHA1.hexdigest(Conv.to_str(args[0]))
|
|
138
|
+
when 'sha256_hex' then digest::SHA256.hexdigest(Conv.to_str(args[0]))
|
|
139
|
+
when 'dclone' then ruby_value(deep_value(args[0]))
|
|
140
|
+
when 'carp', 'cluck'
|
|
141
|
+
env.runtime.emit_warning(Conv.to_str(args[0]), file: @file, line: @line)
|
|
142
|
+
1
|
|
143
|
+
when 'croak', 'confess', 'die' then env.runtime.die(list_values(env), file: @file, line: @line)
|
|
144
|
+
when 'hex' then Conv.to_str(args[0]).to_i(16)
|
|
145
|
+
when 'oct' then Conv.to_str(args[0]).to_i(8)
|
|
146
|
+
when 'rand' then rand(args[0] ? Conv.to_num(args[0]) : 1.0)
|
|
147
|
+
when 'srand' then seed_random(args[0])
|
|
148
|
+
when 'time' then Time.now.to_i
|
|
149
|
+
when 'gettimeofday' then high_resolution_time(context)
|
|
150
|
+
when 'localtime', 'gmtime' then time_parts(args[0], utc: @name == 'gmtime', context:)
|
|
151
|
+
when 'sleep' then sleep(Conv.to_num(args[0] || 0))
|
|
152
|
+
when 'times' then process_times(context)
|
|
153
|
+
when 'pack' then list_values(env).drop(1).pack(Conv.to_str(args[0]))
|
|
154
|
+
when 'unpack' then unpack_values(args, context)
|
|
155
|
+
when 'keys', 'values' then hash_values(env, @name, context)
|
|
156
|
+
when 'each' then hash_each(env, context)
|
|
157
|
+
when 'sort' then list_values(env).sort_by { |value| Conv.to_str(value) }
|
|
158
|
+
when 'pop', 'shift' then array_remove(env, @name)
|
|
159
|
+
when 'unshift' then array_unshift(env)
|
|
160
|
+
when 'splice' then array_splice(env, context)
|
|
161
|
+
when 'chomp', 'chop' then trim(env, @name)
|
|
162
|
+
when 'open' then open_handle(env, args)
|
|
163
|
+
when 'close' then handle(env, @arguments.first, args[0])&.close || ''
|
|
164
|
+
when 'binmode' then binmode(env, args)
|
|
165
|
+
when 'seek'
|
|
166
|
+
handle(env, @arguments.first, args[0]).io.seek(Conv.to_num(args[1]), Conv.to_num(args[2]).to_i) && 1
|
|
167
|
+
when 'tell' then handle(env, @arguments.first, args[0]).io.tell
|
|
168
|
+
when 'fileno' then handle(env, @arguments.first, args[0]).io.fileno
|
|
169
|
+
when 'eof' then eof_value(env, args)
|
|
170
|
+
when 'select' then select_output(env, args)
|
|
171
|
+
when 'opendir' then open_directory(env, args)
|
|
172
|
+
when 'readdir' then read_directory(env, args, context)
|
|
173
|
+
when 'closedir' then directory_handle(env, args).close
|
|
174
|
+
when 'rewinddir' then directory_handle(env, args).rewind
|
|
175
|
+
when 'stat', 'lstat' then stat_values(env, args, context)
|
|
176
|
+
when 'chmod' then File.chmod(Conv.to_num(args.shift).to_i, *args.map { |value| Conv.to_str(value) })
|
|
177
|
+
when 'chown' then File.chown(Conv.to_num(args.shift).to_i, Conv.to_num(args.shift).to_i, *args.map(&:to_s))
|
|
178
|
+
when 'utime' then file_utime(args)
|
|
179
|
+
when 'flock' then handle(env, @arguments.first, args[0]).io.flock(Conv.to_num(args[1]).to_i) ? 1 : ''
|
|
180
|
+
when 'glob' then glob_values(args, context)
|
|
181
|
+
when 'mkdir', 'rmdir', 'unlink', 'truncate' then file_operation(args)
|
|
182
|
+
when 'rename', 'link', 'symlink' then File.public_send(@name, *args.map { |value| Conv.to_str(value) }) && 1
|
|
183
|
+
when 'readlink' then File.readlink(Conv.to_str(args[0]))
|
|
184
|
+
when 'system' then run_system(env)
|
|
185
|
+
when 'exec' then Process.exec(*list_values(env).map { |value| Conv.to_str(value) })
|
|
186
|
+
when 'fork' then fork_process(env)
|
|
187
|
+
when 'wait', 'waitpid' then wait_process(env, args)
|
|
188
|
+
when 'kill' then Process.kill(args.shift, *args.map { |value| Conv.to_num(value).to_i })
|
|
189
|
+
when 'exit'
|
|
190
|
+
status = Conv.to_num(args[0] || 0).to_i
|
|
191
|
+
env.fetch(:scalar, '?').set(status)
|
|
192
|
+
raise PerlExit, status
|
|
193
|
+
when 'getppid' then Process.ppid
|
|
194
|
+
when 'GetOptions' then get_options(env)
|
|
195
|
+
when 'O_RDONLY', 'O_WRONLY', 'O_RDWR', 'O_CREAT', 'O_TRUNC' then file_constant(@name)
|
|
196
|
+
when 'SEEK_SET', 'SEEK_CUR', 'SEEK_END' then IO.const_get(@name)
|
|
197
|
+
when 'EACCES', 'ENOENT' then Errno.const_get(@name)::Errno
|
|
198
|
+
when 'warn' then warn_message(env)
|
|
199
|
+
end
|
|
200
|
+
rescue SystemCallError, IOError => e
|
|
201
|
+
env.fetch(:scalar, '!').set(Dualvar.new(e.errno || 0, e.message))
|
|
202
|
+
''
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def scalar_values(env) = @arguments.map { |argument| argument.run(env, :scalar) }
|
|
206
|
+
def list_values(env) = @arguments.flat_map { |argument| argument.run(env, :list) }.map(&:get)
|
|
207
|
+
|
|
208
|
+
def file_utils
|
|
209
|
+
require 'fileutils'
|
|
210
|
+
FileUtils
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def json
|
|
214
|
+
require 'json'
|
|
215
|
+
JSON
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def digest
|
|
219
|
+
require 'digest'
|
|
220
|
+
Digest
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def equal?(left, right)
|
|
224
|
+
return Conv.to_num(left) == Conv.to_num(right) if left.is_a?(Numeric) || right.is_a?(Numeric)
|
|
225
|
+
|
|
226
|
+
Conv.to_str(left) == Conv.to_str(right)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def list_sum(env)
|
|
230
|
+
values = list_values(env)
|
|
231
|
+
return nil if values.empty? && @name == 'sum'
|
|
232
|
+
|
|
233
|
+
values.sum { |value| Conv.to_num(value) }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def overloaded_abs(env, value)
|
|
237
|
+
found, overloaded = env.runtime.overloaded(value, 'abs')
|
|
238
|
+
found ? overloaded : Conv.to_num(value).abs
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def list_utility_block(env, context)
|
|
242
|
+
code = @arguments.first&.run(env, :scalar)
|
|
243
|
+
code = code.target if code.is_a?(Ref) && code.kind == 'CODE'
|
|
244
|
+
raise PerlError, "#{@name} requires a code reference" unless code.is_a?(Code)
|
|
245
|
+
|
|
246
|
+
cells = @arguments.drop(1).flat_map { |argument| argument.argument_cells(env) }
|
|
247
|
+
return reduce_cells(env, code, cells) if @name == 'reduce'
|
|
248
|
+
|
|
249
|
+
matches = cells.select { |cell| predicate(env, code, cell) }
|
|
250
|
+
return matches.first&.get if @name == 'first'
|
|
251
|
+
|
|
252
|
+
results = { 'any' => !matches.empty?, 'all' => matches.length == cells.length, 'none' => matches.empty? }
|
|
253
|
+
result = results.fetch(@name)
|
|
254
|
+
value = result ? 1 : ''
|
|
255
|
+
context == :list ? [Scalar.new(value)] : value
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def predicate(env, code, cell)
|
|
259
|
+
code.environment.with_scope do
|
|
260
|
+
code.environment.bind(:scalar, '_', cell)
|
|
261
|
+
Conv.truthy?(env.runtime.call(code, [], :scalar))
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def reduce_cells(env, code, cells)
|
|
266
|
+
return nil if cells.empty?
|
|
267
|
+
|
|
268
|
+
a = env.runtime.stash.glob("#{env.package}::a").scalar
|
|
269
|
+
b = env.runtime.stash.glob("#{env.package}::b").scalar
|
|
270
|
+
previous = [a.get, b.get]
|
|
271
|
+
result = cells.first.get
|
|
272
|
+
cells.drop(1).each do |cell|
|
|
273
|
+
a.set(result)
|
|
274
|
+
b.set(cell.get)
|
|
275
|
+
result = env.runtime.call(code, [], :scalar)
|
|
276
|
+
end
|
|
277
|
+
result
|
|
278
|
+
ensure
|
|
279
|
+
a&.set(previous&.first)
|
|
280
|
+
b&.set(previous&.last)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def pair_reference(pair)
|
|
284
|
+
Ref.new(PerlArray.new(pair.map { |value| Scalar.new(value) }))
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def high_resolution_time(context)
|
|
288
|
+
now = Time.now
|
|
289
|
+
context == :list ? [now.to_i, now.usec] : now.to_f
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def temporary_value(args, context)
|
|
293
|
+
if @name == 'tempdir'
|
|
294
|
+
require 'tmpdir'
|
|
295
|
+
return Dir.mktmpdir(Conv.to_str(args[0] || 'peruby'))
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
require 'tempfile'
|
|
299
|
+
file = Tempfile.new(Conv.to_str(args[0] || 'peruby'))
|
|
300
|
+
values = [IOHandle.new(file), file.path]
|
|
301
|
+
context == :list ? values : values.first
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def file_constant(name)
|
|
305
|
+
require 'fcntl'
|
|
306
|
+
Fcntl.const_get(name)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def get_options(env)
|
|
310
|
+
specifications = list_values(env).each_slice(2).to_h
|
|
311
|
+
cells = env.fetch(:array, 'ARGV').cells
|
|
312
|
+
index = 0
|
|
313
|
+
while index < cells.length
|
|
314
|
+
consumed = consume_option(cells, index, specifications)
|
|
315
|
+
consumed ? cells.slice!(index, consumed) : index += 1
|
|
316
|
+
end
|
|
317
|
+
1
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
321
|
+
def consume_option(cells, index, specifications)
|
|
322
|
+
argument = Conv.to_str(cells[index].get)
|
|
323
|
+
return unless argument.start_with?('--')
|
|
324
|
+
|
|
325
|
+
option, inline = argument.delete_prefix('--').split('=', 2)
|
|
326
|
+
spec, target = specifications.find do |name, _value|
|
|
327
|
+
name.to_s.sub(/[=!:].*\z/, '').split('|').include?(option.delete_prefix('no-'))
|
|
328
|
+
end
|
|
329
|
+
return unless spec && target.is_a?(Ref) && target.target.is_a?(Scalar)
|
|
330
|
+
|
|
331
|
+
takes_value = spec.to_s.match?(/[=:][sif]\z/)
|
|
332
|
+
following = cells[index + 1]&.get if takes_value && inline.nil?
|
|
333
|
+
return if takes_value && inline.nil? && following.nil?
|
|
334
|
+
|
|
335
|
+
value = inline || following || (option.start_with?('no-') ? '' : 1)
|
|
336
|
+
value = Conv.to_num(value).to_i if spec.to_s.end_with?('=i', ':i')
|
|
337
|
+
value = Conv.to_num(value) if spec.to_s.end_with?('=f', ':f')
|
|
338
|
+
target.target.set(value)
|
|
339
|
+
takes_value && inline.nil? ? 2 : 1
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def posix_strftime(args)
|
|
343
|
+
format = Conv.to_str(args[0])
|
|
344
|
+
return Time.at(Conv.to_num(args[1] || Time.now.to_i)).strftime(format) if args.length < 7
|
|
345
|
+
|
|
346
|
+
second, minute, hour, day, month, year = args.drop(1).first(6).map { |value| Conv.to_num(value).to_i }
|
|
347
|
+
Time.local(year + 1900, month + 1, day, hour, minute, second).strftime(format)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def data_dumper(env, values)
|
|
351
|
+
terse = Conv.truthy?(env.fetch(:scalar, 'Data::Dumper::Terse').get)
|
|
352
|
+
seen = {}.compare_by_identity
|
|
353
|
+
values.each_with_index.map do |value, index|
|
|
354
|
+
label = "$VAR#{index + 1}"
|
|
355
|
+
prefix = terse ? '' : "$VAR#{index + 1} = "
|
|
356
|
+
suffix = terse ? "\n" : ";\n"
|
|
357
|
+
"#{prefix}#{dump_value(value, prefix.length, seen, label)}#{suffix}"
|
|
358
|
+
end.join
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def dump_value(value, indent, seen, label)
|
|
362
|
+
target = value.is_a?(Ref) ? value.target : value
|
|
363
|
+
if target.is_a?(PerlArray) || target.is_a?(PerlHash)
|
|
364
|
+
return seen[target] if seen.key?(target)
|
|
365
|
+
|
|
366
|
+
seen[target] = label
|
|
367
|
+
end
|
|
368
|
+
dumped = if target.is_a?(PerlArray)
|
|
369
|
+
dump_array(target, indent, seen, label)
|
|
370
|
+
elsif target.is_a?(PerlHash)
|
|
371
|
+
dump_hash(target, indent, seen, label)
|
|
372
|
+
else
|
|
373
|
+
dump_scalar(target)
|
|
374
|
+
end
|
|
375
|
+
value.is_a?(Ref) && value.blessed ? "bless( #{dumped}, '#{value.blessed}' )" : dumped
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def dump_array(array, indent, seen, label)
|
|
379
|
+
values = array.cells.each_with_index.map do |cell, index|
|
|
380
|
+
dump_value(cell&.value, indent + 2, seen, "#{label}->[#{index}]")
|
|
381
|
+
end
|
|
382
|
+
return '[]' if values.empty?
|
|
383
|
+
|
|
384
|
+
"[\n#{values.map { |value| "#{' ' * (indent + 2)}#{value}" }.join(",\n")}\n#{' ' * indent}]"
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def dump_hash(hash, indent, seen, label)
|
|
388
|
+
values = hash.cells.map do |key, cell|
|
|
389
|
+
quoted = dump_scalar(key)
|
|
390
|
+
value = dump_value(cell.value, indent + quoted.length + 6, seen, "#{label}->{#{quoted}}")
|
|
391
|
+
"#{' ' * (indent + 2)}#{quoted} => #{value}"
|
|
392
|
+
end
|
|
393
|
+
return '{}' if values.empty?
|
|
394
|
+
|
|
395
|
+
"{\n#{values.join(",\n")}\n#{' ' * indent}}"
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def dump_scalar(value)
|
|
399
|
+
return 'undef' if value.nil?
|
|
400
|
+
return Conv.to_str(value) if value.is_a?(Numeric)
|
|
401
|
+
|
|
402
|
+
"'#{Conv.to_str(value).gsub('\\', '\\\\').gsub("'", "\\\\'")}'"
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def like?(value, pattern)
|
|
406
|
+
regexp = pattern.is_a?(Ref) ? pattern.target : pattern
|
|
407
|
+
regexp.is_a?(Regexp) && regexp.match?(Conv.to_str(value))
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def deep_value(value)
|
|
411
|
+
if value.is_a?(Ref) && value.target.is_a?(PerlArray)
|
|
412
|
+
return value.target.cells.map { |cell| deep_value(cell.get) }
|
|
413
|
+
end
|
|
414
|
+
if value.is_a?(Ref) && value.target.is_a?(PerlHash)
|
|
415
|
+
return value.target.cells.to_h { |key, cell| [key, deep_value(cell.get)] }
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
value
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def subtest(env, args)
|
|
422
|
+
code = args[1]
|
|
423
|
+
code = code.target if code.is_a?(Ref) && code.kind == 'CODE'
|
|
424
|
+
raise PerlError, 'subtest requires a code reference' unless code.is_a?(Code)
|
|
425
|
+
|
|
426
|
+
env.runtime.test_builder.subtest(Conv.to_str(args[0])) { env.runtime.call(code, [], :void) }
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def ruby_value(value)
|
|
430
|
+
case value
|
|
431
|
+
when Array then Ref.new(PerlArray.new(value.map { |item| Scalar.new(ruby_value(item)) }))
|
|
432
|
+
when Hash
|
|
433
|
+
hash = PerlHash.new
|
|
434
|
+
value.each { |key, item| hash.slot(key).set(ruby_value(item)) }
|
|
435
|
+
Ref.new(hash)
|
|
436
|
+
else value
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def substring(env, args)
|
|
441
|
+
string = Conv.to_str(args[0])
|
|
442
|
+
offset = Conv.to_num(args[1]).to_i
|
|
443
|
+
length = args[2] && Conv.to_num(args[2]).to_i
|
|
444
|
+
value = length ? string.slice(offset, length) : string.slice(offset..)
|
|
445
|
+
return value unless args.length > 3
|
|
446
|
+
|
|
447
|
+
cell = @arguments.first.lvalue(env, :scalar)
|
|
448
|
+
string = string.dup
|
|
449
|
+
string[offset, length] = Conv.to_str(args[3])
|
|
450
|
+
cell.set(string)
|
|
451
|
+
value
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def string_index(args)
|
|
455
|
+
string = Conv.to_str(args[0])
|
|
456
|
+
needle = Conv.to_str(args[1])
|
|
457
|
+
offset = args[2] && Conv.to_num(args[2]).to_i
|
|
458
|
+
(offset ? string.public_send(@name, needle, offset) : string.public_send(@name, needle)) || -1
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def warn_message(env)
|
|
462
|
+
message = list_values(env).map { |value| Conv.to_str(value) }.join
|
|
463
|
+
env.runtime.stderr.print(env.runtime.error_message(message))
|
|
464
|
+
1
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def reverse(env, context)
|
|
468
|
+
values = list_values(env)
|
|
469
|
+
return values.reverse.map { |value| Scalar.new(value) } if context == :list
|
|
470
|
+
|
|
471
|
+
values.map { |value| Conv.to_str(value) }.join.reverse
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def time_parts(value, utc:, context:)
|
|
475
|
+
time = Time.at(Conv.to_num(value || Time.now.to_i))
|
|
476
|
+
time = time.utc if utc
|
|
477
|
+
return time.strftime('%a %b %e %H:%M:%S %Y') unless context == :list
|
|
478
|
+
|
|
479
|
+
daylight = time.dst? ? 1 : 0
|
|
480
|
+
[time.sec, time.min, time.hour, time.day, time.month - 1, time.year - 1900,
|
|
481
|
+
time.wday, time.yday - 1, daylight]
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def seed_random(value)
|
|
485
|
+
seed = value.nil? ? Random.new_seed : Conv.to_num(value).to_i
|
|
486
|
+
Kernel.srand(seed)
|
|
487
|
+
seed
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def process_times(context)
|
|
491
|
+
times = Process.times
|
|
492
|
+
values = [times.utime, times.stime, times.cutime, times.cstime]
|
|
493
|
+
context == :list ? values : values.first
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def unpack_values(args, context)
|
|
497
|
+
values = Conv.to_str(args[1]).unpack(Conv.to_str(args[0]))
|
|
498
|
+
context == :list ? values : values.first
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def run_system(env)
|
|
502
|
+
result = Kernel.system(*list_values(env).map { |value| Conv.to_str(value) })
|
|
503
|
+
status = result.nil? ? -1 : $CHILD_STATUS.to_i
|
|
504
|
+
env.fetch(:scalar, '?').set(status)
|
|
505
|
+
status
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def fork_process(env)
|
|
509
|
+
pid = Process.fork
|
|
510
|
+
env.fetch(:scalar, '$').set(Process.pid) unless pid
|
|
511
|
+
pid || 0
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def wait_process(env, args)
|
|
515
|
+
pid = if @name == 'wait'
|
|
516
|
+
Process.wait
|
|
517
|
+
else
|
|
518
|
+
Process.waitpid(Conv.to_num(args[0]).to_i, Conv.to_num(args[1] || 0).to_i)
|
|
519
|
+
end
|
|
520
|
+
env.fetch(:scalar, '?').set($CHILD_STATUS.to_i)
|
|
521
|
+
pid
|
|
522
|
+
rescue Errno::ECHILD
|
|
523
|
+
-1
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def hash_values(env, name, context)
|
|
527
|
+
hash = @arguments.first.lvalue(env, :scalar)
|
|
528
|
+
raise PerlError, "Type of arg 1 to #{name} must be hash" unless hash.is_a?(PerlHash)
|
|
529
|
+
|
|
530
|
+
values = name == 'keys' ? hash.keys : hash.keys.map { |key| hash.fetch(key) }
|
|
531
|
+
context == :list ? values : values.length
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def hash_each(env, context)
|
|
535
|
+
hash = @arguments.first.lvalue(env, :scalar)
|
|
536
|
+
raise PerlError, 'Type of arg 1 to each must be hash' unless hash.is_a?(PerlHash)
|
|
537
|
+
|
|
538
|
+
pair = hash.each_pair
|
|
539
|
+
return context == :list ? [] : nil unless pair
|
|
540
|
+
|
|
541
|
+
key, cell = pair
|
|
542
|
+
context == :list ? [Scalar.new(key), cell] : key
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def array_remove(env, name)
|
|
546
|
+
array = if @arguments.empty?
|
|
547
|
+
env.fetch(:array, env.state_owner ? '_' : 'ARGV')
|
|
548
|
+
else
|
|
549
|
+
@arguments.first.lvalue(env, :scalar)
|
|
550
|
+
end
|
|
551
|
+
cell = name == 'pop' ? array.cells.pop : array.cells.shift
|
|
552
|
+
cell&.get
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def array_unshift(env)
|
|
556
|
+
array = @arguments.first.lvalue(env, :scalar)
|
|
557
|
+
array.cells.unshift(*@arguments.drop(1).flat_map { |arg| arg.argument_cells(env) }.map(&:copy))
|
|
558
|
+
array.size
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
562
|
+
def array_splice(env, context)
|
|
563
|
+
array = @arguments.first.lvalue(env, :scalar)
|
|
564
|
+
args = scalar_values(env)
|
|
565
|
+
offset = Conv.to_num(args[1] || 0).to_i
|
|
566
|
+
offset += array.size if offset.negative?
|
|
567
|
+
length = args[2] ? Conv.to_num(args[2]).to_i : array.size - offset
|
|
568
|
+
length = array.size - offset + length if length.negative?
|
|
569
|
+
replacement = @arguments.drop(3).flat_map { |arg| arg.argument_cells(env) }.map(&:copy)
|
|
570
|
+
removed = array.cells.slice!(offset, length) || []
|
|
571
|
+
array.cells.insert(offset, *replacement)
|
|
572
|
+
context == :list ? removed : removed.last&.get
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def trim(env, name)
|
|
576
|
+
cell = @arguments.first.lvalue(env, :scalar)
|
|
577
|
+
string = Conv.to_str(cell.get)
|
|
578
|
+
removed = if name == 'chomp'
|
|
579
|
+
string.sub!(/\R\z/, '') ? 1 : 0
|
|
580
|
+
else
|
|
581
|
+
string.slice!(-1).to_s.length
|
|
582
|
+
end
|
|
583
|
+
cell.set(string)
|
|
584
|
+
removed
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def open_handle(env, args)
|
|
588
|
+
target = @arguments.first
|
|
589
|
+
values = @arguments.drop(1).flat_map { |argument| argument.run(env, :list) }.map(&:get)
|
|
590
|
+
marker, layers, source = open_spec(values)
|
|
591
|
+
io = open_io(marker, source, values.drop(2))
|
|
592
|
+
apply_layers(io, layers)
|
|
593
|
+
assign_handle(env, target, args[0], IOHandle.new(io, layers:))
|
|
594
|
+
1
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def open_spec(values)
|
|
598
|
+
if values.length == 1
|
|
599
|
+
source = Conv.to_str(values[0])
|
|
600
|
+
marker = source.slice!(/\A(?:\+?[<>]{1,2}|-\||\|-)/) || '<'
|
|
601
|
+
marker, layers = marker.split(':', 2)
|
|
602
|
+
return [marker, layers, source]
|
|
603
|
+
end
|
|
604
|
+
marker, layers = Conv.to_str(values[0]).split(':', 2)
|
|
605
|
+
[marker, layers, values[1]]
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def ruby_mode(marker)
|
|
609
|
+
{ '<' => 'r', '>' => 'w', '>>' => 'a', '+<' => 'r+', '+>' => 'w+', '+>>' => 'a+' }.fetch(marker, marker)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def open_io(marker, source, command_tail)
|
|
613
|
+
if %w[-| |-].include?(marker)
|
|
614
|
+
command = [Conv.to_str(source), *command_tail.map { |item| Conv.to_str(item) }]
|
|
615
|
+
return IO.popen(command, marker == '-|' ? 'r' : 'w')
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
if source.is_a?(Ref) && source.target.is_a?(Scalar)
|
|
619
|
+
return StringIO.new(Conv.to_str(source.target.get), ruby_mode(marker))
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
File.open(Conv.to_str(source), ruby_mode(marker))
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def apply_layers(io, layers)
|
|
626
|
+
return unless layers
|
|
627
|
+
|
|
628
|
+
encoding = layers[/encoding\(([^)]+)\)/, 1] || ('UTF-8' if layers == 'utf8')
|
|
629
|
+
io.set_encoding(encoding) if encoding && io.respond_to?(:set_encoding)
|
|
630
|
+
io.binmode if layers == 'raw'
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def assign_handle(env, operation, value, io_handle)
|
|
634
|
+
if operation.respond_to?(:lvalue)
|
|
635
|
+
operation.lvalue(env, :scalar).set(io_handle)
|
|
636
|
+
else
|
|
637
|
+
env.runtime.stash.glob(Conv.to_str(value)).io = io_handle
|
|
638
|
+
end
|
|
639
|
+
rescue PerlError
|
|
640
|
+
env.runtime.stash.glob(Conv.to_str(value)).io = io_handle
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def handle(env, operation, value)
|
|
644
|
+
candidate = operation&.run(env, :scalar)
|
|
645
|
+
return candidate if candidate.is_a?(IOHandle)
|
|
646
|
+
|
|
647
|
+
env.runtime.stash.glob(Conv.to_str(value)).io
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def binmode(env, args)
|
|
651
|
+
io_handle = handle(env, @arguments.first, args[0])
|
|
652
|
+
layer = Conv.to_str(args[1] || ':raw')
|
|
653
|
+
apply_layers(io_handle.io, layer.delete_prefix(':'))
|
|
654
|
+
io_handle.layers << layer
|
|
655
|
+
1
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
def eof_value(env, args)
|
|
659
|
+
io_handle = args.empty? ? env.runtime.last_input_handle : handle(env, @arguments.first, args[0])
|
|
660
|
+
io_handle ||= env.runtime.stash.glob('STDIN').io
|
|
661
|
+
io_handle.io.eof? ? 1 : ''
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def select_output(env, args)
|
|
665
|
+
io_handle = args.empty? ? nil : handle(env, @arguments.first, args[0])
|
|
666
|
+
env.runtime.select_output(io_handle)
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def open_directory(env, args)
|
|
670
|
+
assign_handle(env, @arguments.first, args[0], DirectoryHandle.new(Conv.to_str(args[1])))
|
|
671
|
+
1
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def directory_handle(env, args)
|
|
675
|
+
value = @arguments.first&.run(env, :scalar)
|
|
676
|
+
return value if value.is_a?(DirectoryHandle)
|
|
677
|
+
|
|
678
|
+
handle = env.runtime.stash.glob(Conv.to_str(args[0])).io
|
|
679
|
+
raise PerlError, 'readdir() attempted on invalid dirhandle' unless handle.is_a?(DirectoryHandle)
|
|
680
|
+
|
|
681
|
+
handle
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def read_directory(env, args, context)
|
|
685
|
+
directory = directory_handle(env, args)
|
|
686
|
+
return directory.read if context != :list
|
|
687
|
+
|
|
688
|
+
entries = []
|
|
689
|
+
loop do
|
|
690
|
+
entry = directory.read
|
|
691
|
+
break unless entry
|
|
692
|
+
|
|
693
|
+
entries << entry
|
|
694
|
+
end
|
|
695
|
+
entries
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def stat_values(env, args, context)
|
|
699
|
+
stat = env.runtime.file_stat(Conv.to_str(args[0]), lstat: @name == 'lstat')
|
|
700
|
+
return 1 unless context == :list
|
|
701
|
+
|
|
702
|
+
[stat.dev, stat.ino, stat.mode, stat.nlink, stat.uid, stat.gid, stat.rdev, stat.size,
|
|
703
|
+
stat.atime.to_i, stat.mtime.to_i, stat.ctime.to_i, stat.blksize, stat.blocks]
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def file_utime(args)
|
|
707
|
+
access = Time.at(Conv.to_num(args.shift))
|
|
708
|
+
modify = Time.at(Conv.to_num(args.shift))
|
|
709
|
+
File.utime(access, modify, *args.map { |value| Conv.to_str(value) })
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def glob_values(args, context)
|
|
713
|
+
return args.flat_map { |pattern| Dir.glob(Conv.to_str(pattern)) } if context == :list
|
|
714
|
+
|
|
715
|
+
@glob_values = Dir.glob(Conv.to_str(args[0])) if @glob_values.nil? || @glob_values.empty?
|
|
716
|
+
@glob_values.shift
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def file_operation(args)
|
|
720
|
+
case @name
|
|
721
|
+
when 'mkdir' then Dir.mkdir(Conv.to_str(args[0]), Conv.to_num(args[1] || 0o777).to_i)
|
|
722
|
+
when 'rmdir' then Dir.rmdir(Conv.to_str(args[0]))
|
|
723
|
+
when 'unlink' then File.unlink(*args.map { |value| Conv.to_str(value) })
|
|
724
|
+
when 'truncate' then File.truncate(Conv.to_str(args[0]), Conv.to_num(args[1]).to_i)
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# Mutable view used when substr appears on the left of an assignment.
|
|
729
|
+
class SubstringCell
|
|
730
|
+
def initialize(cell, offset, length)
|
|
731
|
+
@cell = cell
|
|
732
|
+
@offset = offset
|
|
733
|
+
@length = length
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def get
|
|
737
|
+
string = Conv.to_str(@cell.get)
|
|
738
|
+
@length ? string.slice(@offset, @length) : string.slice(@offset..)
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def set(value)
|
|
742
|
+
string = Conv.to_str(@cell.get).dup
|
|
743
|
+
@length ? string[@offset, @length] = Conv.to_str(value) : string[@offset..] = Conv.to_str(value)
|
|
744
|
+
@cell.set(string)
|
|
745
|
+
self
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# Perl file-test operators backed by File::Stat predicates.
|
|
751
|
+
class FileTest < Base
|
|
752
|
+
def initialize(operator, expression)
|
|
753
|
+
@operator = operator.delete_prefix('-')
|
|
754
|
+
@expression = expression
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
def run(env, context)
|
|
758
|
+
path = Conv.to_str(@expression.run(env, :scalar))
|
|
759
|
+
result = test(path, env)
|
|
760
|
+
context == :list ? [Scalar.new(result)] : result
|
|
761
|
+
rescue SystemCallError
|
|
762
|
+
context == :list ? [Scalar.new('')] : ''
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
private
|
|
766
|
+
|
|
767
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
768
|
+
def test(path, env)
|
|
769
|
+
return tty(path, env) if @operator == 't'
|
|
770
|
+
|
|
771
|
+
stat = env.runtime.file_stat(path, lstat: @operator == 'l')
|
|
772
|
+
actual_path = path == '_' ? env.runtime.file_stat_path : path
|
|
773
|
+
predicates = { 'e' => true, 'f' => stat.file?, 'd' => stat.directory?, 'l' => stat.symlink?,
|
|
774
|
+
'r' => File.readable?(actual_path), 'w' => File.writable?(actual_path),
|
|
775
|
+
'x' => File.executable?(actual_path), 'z' => stat.zero?, 'p' => stat.pipe?, 'S' => stat.socket?,
|
|
776
|
+
'b' => stat.blockdev?, 'c' => stat.chardev?, 'u' => stat.setuid?, 'g' => stat.setgid?,
|
|
777
|
+
'k' => stat.sticky? }
|
|
778
|
+
return stat.size if @operator == 's'
|
|
779
|
+
return text_file?(actual_path) == (@operator == 'T') ? 1 : '' if %w[T B].include?(@operator)
|
|
780
|
+
return predicates.fetch(@operator) ? 1 : '' if predicates.key?(@operator)
|
|
781
|
+
|
|
782
|
+
return (Time.now - stat.mtime) / 86_400 if @operator == 'M'
|
|
783
|
+
return (Time.now - stat.atime) / 86_400 if @operator == 'A'
|
|
784
|
+
return (Time.now - stat.ctime) / 86_400 if @operator == 'C'
|
|
785
|
+
|
|
786
|
+
''
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
def tty(path, env)
|
|
790
|
+
handle = env.runtime.stash.glob(path).io
|
|
791
|
+
io = handle&.io
|
|
792
|
+
io&.tty? ? 1 : ''
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
def text_file?(path)
|
|
796
|
+
bytes = File.binread(path, 1024).bytes
|
|
797
|
+
return true if bytes.empty?
|
|
798
|
+
return false if bytes.include?(0)
|
|
799
|
+
|
|
800
|
+
controls = bytes.count { |byte| byte < 32 && ![9, 10, 12, 13].include?(byte) }
|
|
801
|
+
controls.fdiv(bytes.length) < 0.3
|
|
802
|
+
end
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
# Compile-time pragmas and registered core modules; loading expands in P12.
|
|
806
|
+
class Use < Base
|
|
807
|
+
CORE = ModuleLoader::INTERNAL_MODULES
|
|
808
|
+
|
|
809
|
+
def initialize(module_name, imports, disable, package)
|
|
810
|
+
@module_name = module_name
|
|
811
|
+
@imports = imports
|
|
812
|
+
@disable = disable
|
|
813
|
+
@package = package
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
def run(env, context) # rubocop:disable Metrics/AbcSize
|
|
817
|
+
imports, version, suppress = options
|
|
818
|
+
env.configure_strict(imports, disable: @disable) if @module_name == 'strict'
|
|
819
|
+
env.runtime.configure_warnings(imports, disable: @disable) if @module_name == 'warnings'
|
|
820
|
+
configure_inc(env, imports) if @module_name == 'lib' && !@disable
|
|
821
|
+
env.runtime.module_loader.require_module(@module_name) unless @disable
|
|
822
|
+
configure_inheritance(env, imports) if %w[parent base].include?(@module_name) && !@disable
|
|
823
|
+
configure_overload(env, imports) if @module_name == 'overload'
|
|
824
|
+
check_version(env, version) if version
|
|
825
|
+
invoke_import(env, imports, suppress)
|
|
826
|
+
alias_imports(env, imports) unless @disable || suppress
|
|
827
|
+
if %w[Test::More Test::Simple].include?(@module_name) && imports.first == 'tests'
|
|
828
|
+
env.runtime.test_builder.plan(imports.last)
|
|
829
|
+
end
|
|
830
|
+
context == :list ? [Scalar.new(1)] : 1
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
def warning_directive? = @module_name == 'warnings'
|
|
834
|
+
|
|
835
|
+
private
|
|
836
|
+
|
|
837
|
+
def options
|
|
838
|
+
imports = @imports.dup
|
|
839
|
+
suppress = imports.delete('__NO_IMPORT__')
|
|
840
|
+
version = imports.shift(2).last if imports.first == '__VERSION__'
|
|
841
|
+
[imports, version, suppress]
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
def check_version(env, required)
|
|
845
|
+
actual = env.runtime.stash.glob("#{@module_name}::VERSION").scalar.get
|
|
846
|
+
return if actual && Conv.to_num(actual) >= Conv.to_num(required)
|
|
847
|
+
|
|
848
|
+
raise PerlError, "#{@module_name} version #{required} required"
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
def invoke_import(env, imports, suppress)
|
|
852
|
+
return if suppress
|
|
853
|
+
|
|
854
|
+
method = @disable ? 'unimport' : 'import'
|
|
855
|
+
code = env.runtime.stash.fetch("#{@module_name}::#{method}")&.code
|
|
856
|
+
return unless code
|
|
857
|
+
|
|
858
|
+
arguments = [Scalar.new(@module_name), *imports.map { |name| Scalar.new(name) }]
|
|
859
|
+
env.runtime.call(code, arguments, :void)
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
def alias_imports(env, imports)
|
|
863
|
+
names = if imports.empty?
|
|
864
|
+
env.runtime.stash.glob("#{@module_name}::EXPORT").array.cells.map(&:get)
|
|
865
|
+
else
|
|
866
|
+
imports
|
|
867
|
+
end
|
|
868
|
+
names.each do |name|
|
|
869
|
+
code = env.runtime.stash.fetch("#{@module_name}::#{name}")&.code
|
|
870
|
+
env.runtime.stash.glob("#{@package}::#{name}").code = code if code
|
|
871
|
+
end
|
|
872
|
+
env.runtime.stash.touch unless names.empty?
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def configure_inc(env, imports)
|
|
876
|
+
imports.reverse_each { |path| env.fetch(:array, 'INC').cells.unshift(Scalar.new(path)) }
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
def configure_inheritance(env, imports)
|
|
880
|
+
parents = imports.reject { |name| name == '-norequire' }
|
|
881
|
+
parents.each { |parent| env.runtime.module_loader.require_module(parent) } unless imports.include?('-norequire')
|
|
882
|
+
env.runtime.stash.glob("#{@package}::ISA").array.cells.concat(parents.map { |name| Scalar.new(name) })
|
|
883
|
+
env.runtime.stash.touch
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
def configure_overload(env, imports)
|
|
887
|
+
env.runtime.register_overload(@package, imports.each_slice(2).to_h, disable: @disable)
|
|
888
|
+
end
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
# Runtime require expression.
|
|
892
|
+
class Require < Base
|
|
893
|
+
def initialize(expression)
|
|
894
|
+
@expression = expression
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
def run(env, context)
|
|
898
|
+
result = env.runtime.module_loader.require_module(@expression.run(env, :scalar))
|
|
899
|
+
context == :list ? [Scalar.new(result)] : result
|
|
900
|
+
end
|
|
901
|
+
end
|
|
902
|
+
end
|
|
903
|
+
end
|