cc 1.2.1 → 1.4.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 +4 -4
- data/README.md +29 -3
- data/cc.rb +1 -0
- data/monkey-patch.rb +375 -1
- data/text-client.rb +297 -0
- data/text-server.rb +403 -0
- data/text-tools.rb +418 -0
- data/tree.rb +184 -184
- metadata +5 -2
data/text-client.rb
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#coding:utf-8
|
|
3
|
+
|
|
4
|
+
############################################################################################################
|
|
5
|
+
# text-client —— C 端:textmind 命令行传入解析 #
|
|
6
|
+
# #
|
|
7
|
+
# 移植改造自 imdoc/TextUtils 的 tm CLI 与 config.ru 使用方式: #
|
|
8
|
+
# - 默认本地执行(加载同目录 text-tools.rb,不依赖任何网络与服务) #
|
|
9
|
+
# - --remote URL 时走 S 端(text-server.rb)服务解析,本地/远程结果一致 #
|
|
10
|
+
# - 操作名与参数名和 S 端 URL 路由一一对应,Postman 集合里的用法可原样翻译成命令行 #
|
|
11
|
+
# #
|
|
12
|
+
# 用法: #
|
|
13
|
+
# ruby text-client.rb <namespace> <op> [参数...] #
|
|
14
|
+
# namespace: type | text | list | tree #
|
|
15
|
+
# op: 操作名(可含子路径,如 match/paragraph、draw/lines) #
|
|
16
|
+
# #
|
|
17
|
+
# 数据输入: #
|
|
18
|
+
# --text "字符串" 文本/类型操作输入(或 --file PATH,或管道 stdin) #
|
|
19
|
+
# --list '[1,2,3]' 列表操作输入(JSON 数组) #
|
|
20
|
+
# --tree '{"a":1}' 树操作输入(JSON 对象) #
|
|
21
|
+
# 布尔开关(如 --strip)出现即置 true;--patterns/--attaches/--oprands/--oprand 若以 [ 或 { 开头按 JSON #
|
|
22
|
+
# 解析,与 S 端 JSON 请求体语义对齐 #
|
|
23
|
+
# #
|
|
24
|
+
# 通用: #
|
|
25
|
+
# --remote http://127.0.0.1:9292 改走 S 端服务解析 #
|
|
26
|
+
# --help 帮助 #
|
|
27
|
+
# #
|
|
28
|
+
# 输出:JSON(与 S 端响应一致);用法/连接错误输出到 stderr 并返回 1 #
|
|
29
|
+
# #
|
|
30
|
+
# 示例: #
|
|
31
|
+
# ruby text-client.rb type int --radix 2 --text 100 # => 4 #
|
|
32
|
+
# ruby text-client.rb text head --prefix aa --text aabbcc # => true #
|
|
33
|
+
# ruby text-client.rb text match/paragraph --start a --finish b --text a1a2b3 #
|
|
34
|
+
# echo "abedef" | ruby text-client.rb text exchange --oldtext e --newtext z #
|
|
35
|
+
# ruby text-client.rb list map --func "proc{|c| c * 2}" --list '[1,2,3]' # => [2,4,6] #
|
|
36
|
+
# ruby text-client.rb tree + --oprand '[1,3,5]' --tree '{"a":1}' # => {"a":1,"0":1,"1":3,"2":5} #
|
|
37
|
+
# ruby text-client.rb --remote http://127.0.0.1:9292 text head --prefix aa --text aabbcc #
|
|
38
|
+
############################################################################################################
|
|
39
|
+
|
|
40
|
+
require_relative 'text-tools'
|
|
41
|
+
require 'json'
|
|
42
|
+
require 'net/http'
|
|
43
|
+
|
|
44
|
+
module TextClient
|
|
45
|
+
# 结构化参数键:这些键的值以 [ 或 { 开头时按 JSON 解析(与 S 端 JSON 请求体语义对齐),
|
|
46
|
+
# 其余一律保持字符串(数字/布尔由操作端自行 to_i/判断,与 S 端一致)。
|
|
47
|
+
STRUCTURED_KEYS = %w{list tree oprand oprands patterns attaches item val}
|
|
48
|
+
|
|
49
|
+
module_function
|
|
50
|
+
|
|
51
|
+
# 结构化参数解析:结构化键的值尝试按 JSON 解析(数字/布尔/数组/对象),解析失败保持原串。
|
|
52
|
+
#
|
|
53
|
+
# @param name [String] 参数名
|
|
54
|
+
# @param value [String] 命令行参数值
|
|
55
|
+
# @return [Object] 解析结果
|
|
56
|
+
def coerce name, value
|
|
57
|
+
return value unless STRUCTURED_KEYS.include?(name) && value.is_a?(String)
|
|
58
|
+
JSON.parse(value)
|
|
59
|
+
rescue JSON::ParserError
|
|
60
|
+
value
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 解析命令行参数:--key value 成对(也接受 --key=value 写法),
|
|
64
|
+
# 值缺失或下一个也是 --开关 时视为布尔 true。
|
|
65
|
+
#
|
|
66
|
+
# @param argv [Array<String>] 参数(不含 namespace/op)
|
|
67
|
+
# @return [Hash{String => Object}] 参数表
|
|
68
|
+
def parse_params argv
|
|
69
|
+
params = {}
|
|
70
|
+
i = 0
|
|
71
|
+
while i < argv.size
|
|
72
|
+
key = argv[i]
|
|
73
|
+
if key.start_with?('--')
|
|
74
|
+
name, inline = key[2..-1].split('=', 2)
|
|
75
|
+
value = inline || argv[i+1]
|
|
76
|
+
if value.nil? || (!inline && value.start_with?('--'))
|
|
77
|
+
params[name] = true
|
|
78
|
+
else
|
|
79
|
+
params[name] = coerce(name, value)
|
|
80
|
+
i += 1 unless inline
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
i += 1
|
|
84
|
+
end
|
|
85
|
+
params
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# 取文本输入:--text > --file > 管道 stdin。
|
|
89
|
+
#
|
|
90
|
+
# @param params [Hash] 参数表
|
|
91
|
+
# @return [String, nil] 文本输入
|
|
92
|
+
def fetch_text params
|
|
93
|
+
return params['text'] if params['text']
|
|
94
|
+
return File.read(params['file']) if params['file']
|
|
95
|
+
return $stdin.read unless $stdin.tty?
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# 本地执行:直接调用 text-tools 的操作符(与 S 端路由同参数同缺省)。
|
|
100
|
+
#
|
|
101
|
+
# @param ns [String] namespace(type/text/list/tree)
|
|
102
|
+
# @param op [String] 操作名(可含子路径)
|
|
103
|
+
# @param params [Hash] 参数表
|
|
104
|
+
# @return [Object] 计算结果
|
|
105
|
+
def dispatch_local ns, op, params
|
|
106
|
+
case "#{ns}/#{op}"
|
|
107
|
+
# ---- type ----
|
|
108
|
+
when 'type/int'
|
|
109
|
+
fetch_text(params).int(params['radix'] ? params['radix'].to_i : 10)
|
|
110
|
+
when 'type/float'
|
|
111
|
+
fetch_text(params).float(params['tail'] ? params['tail'].to_i : 2)
|
|
112
|
+
when 'type/precent'
|
|
113
|
+
fetch_text(params).precent(params['tail'] ? params['tail'].to_i : 2)
|
|
114
|
+
when 'type/text'
|
|
115
|
+
fetch_text(params).text((params['format'] || 'raw').to_sym)
|
|
116
|
+
when 'type/bool'
|
|
117
|
+
fetch_text(params).bool
|
|
118
|
+
# ---- text ----
|
|
119
|
+
when 'text/head'
|
|
120
|
+
fetch_text(params).head?(params['prefix'], params['strip'] || false)
|
|
121
|
+
when 'text/tail'
|
|
122
|
+
fetch_text(params).tail?(params['postfix'], params['strip'] || false)
|
|
123
|
+
when 'text/include'
|
|
124
|
+
fetch_text(params).include?(params['pattern'])
|
|
125
|
+
when 'text/empty'
|
|
126
|
+
fetch_text(params).empty?
|
|
127
|
+
when 'text/length'
|
|
128
|
+
fetch_text(params).length
|
|
129
|
+
when 'text/strip'
|
|
130
|
+
flag = (params['flag'] || 'both').to_s.downcase.to_sym
|
|
131
|
+
text = fetch_text(params)
|
|
132
|
+
flag==:both ? text.strip : flag==:left ? text.lstrip : flag==:right ? text.rstrip : text
|
|
133
|
+
when 'text/join'
|
|
134
|
+
fetch_text(params).join(params['attaches'] || [])
|
|
135
|
+
when 'text/+'
|
|
136
|
+
fetch_text(params) + (params['oprand'] || '')
|
|
137
|
+
when 'text/-'
|
|
138
|
+
fetch_text(params) - (params['oprand'] || '')
|
|
139
|
+
when 'text/*'
|
|
140
|
+
fetch_text(params) * (params['oprand'] || 1).to_s.to_i
|
|
141
|
+
when 'text/split'
|
|
142
|
+
fetch_text(params) / (params['oprand'] || ' ')
|
|
143
|
+
when 'text/cut'
|
|
144
|
+
fetch_text(params).cut(params['lidx'] ? params['lidx'].to_i : 1, params['ridx'] ? params['ridx'].to_i : 0)
|
|
145
|
+
when 'text/match'
|
|
146
|
+
text = fetch_text(params)
|
|
147
|
+
patterns = params['patterns'] || []
|
|
148
|
+
mode = (params['mode'] || 'one').to_s.downcase.to_sym
|
|
149
|
+
[:one, :all].include?(mode) ? (!patterns.first.to_s.empty? ? text.matches(patterns.first.to_s,mode) : text) :
|
|
150
|
+
text.matches(patterns,mode)
|
|
151
|
+
when 'text/exchange'
|
|
152
|
+
repeat = (params['repeat'] || 'all').to_s
|
|
153
|
+
repeat = repeat.include?('all') ? repeat.to_sym : repeat.to_i
|
|
154
|
+
fetch_text(params).exchange(params['oldtext'], params['newtext'], repeat)
|
|
155
|
+
when 'text/draw/lines'
|
|
156
|
+
rests, content = fetch_text(params).draw_lines(Regexp.new(params['preflag'].to_s), Regexp.new(params['postflag'].to_s))
|
|
157
|
+
[content, rests]
|
|
158
|
+
when 'text/match/paragraph'
|
|
159
|
+
fetch_text(params).match_paragraph(params['start'].to_s, params['finish'].to_s)
|
|
160
|
+
when 'text/match/cascade'
|
|
161
|
+
fetch_text(params).match_cascade(params['start'].to_s, params['finish'].to_s)
|
|
162
|
+
when 'text/match/xml'
|
|
163
|
+
fetch_text(params).match_xml(params['tag'].to_s)
|
|
164
|
+
# ---- list ----
|
|
165
|
+
when 'list/pop'
|
|
166
|
+
params['list'].pop
|
|
167
|
+
when 'list/push'
|
|
168
|
+
params['list'].push(params['item'])
|
|
169
|
+
when 'list/shift'
|
|
170
|
+
params['list'].shift
|
|
171
|
+
when 'list/unshift'
|
|
172
|
+
params['list'].unshift(params['item'])
|
|
173
|
+
when 'list/+'
|
|
174
|
+
params['list'] + (params['oprand'] || [])
|
|
175
|
+
when 'list/-'
|
|
176
|
+
params['list'] - (params['oprand'] || [])
|
|
177
|
+
when 'list/catch'
|
|
178
|
+
params['list'].catch(params['oprands'] || [])
|
|
179
|
+
when 'list/sub'
|
|
180
|
+
params['list'].sub(params['lidx'] ? params['lidx'].to_i : 1, params['ridx'] ? params['ridx'].to_i : 0)
|
|
181
|
+
when 'list/match'
|
|
182
|
+
repeat = params['repeat'] || 1
|
|
183
|
+
repeat = :all if repeat.to_s == 'all'
|
|
184
|
+
repeat = repeat.to_i unless repeat == :all
|
|
185
|
+
params['list'].match(repeat, &eval(params['func']))
|
|
186
|
+
when 'list/map'
|
|
187
|
+
params['list'].map(&eval(params['func']))
|
|
188
|
+
when 'list/reduce'
|
|
189
|
+
sectors = params['init'].to_s.split(":")
|
|
190
|
+
type = sectors.last.strip
|
|
191
|
+
init = sectors[0..-2].join(':')
|
|
192
|
+
iv = case type
|
|
193
|
+
when 'text'; init.strip.to_s
|
|
194
|
+
when 'integer'; init.strip.to_i
|
|
195
|
+
when 'float'; init.strip.to_f
|
|
196
|
+
when 'list'; eval(init)
|
|
197
|
+
when 'tree'; eval(init)
|
|
198
|
+
end
|
|
199
|
+
params['list'].reduce(iv, &eval(params['func']))
|
|
200
|
+
# ---- tree ----
|
|
201
|
+
when 'tree/keys'
|
|
202
|
+
params['tree'].keys
|
|
203
|
+
when 'tree/values'
|
|
204
|
+
params['tree'].values
|
|
205
|
+
when 'tree/get'
|
|
206
|
+
params['tree'][params['key'].to_s]
|
|
207
|
+
when 'tree/set'
|
|
208
|
+
params['tree'][params['key']] = params['val']
|
|
209
|
+
params['tree']
|
|
210
|
+
when 'tree/+'
|
|
211
|
+
params['tree'] + params['oprand']
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# 远程执行:POST JSON 参数到 S 端(路径与操作名一致)。
|
|
216
|
+
#
|
|
217
|
+
# @param base [String] S 端地址(如 http://127.0.0.1:9292)
|
|
218
|
+
# @param ns [String] namespace
|
|
219
|
+
# @param op [String] 操作名
|
|
220
|
+
# @param params [Hash] 参数表
|
|
221
|
+
# @return [Object] 解析结果
|
|
222
|
+
def dispatch_remote base, ns, op, params
|
|
223
|
+
uri = URI("#{base}/#{ns}/#{op}")
|
|
224
|
+
body = params.reject{|k,_| %w{remote help}.include?(k) }.to_json
|
|
225
|
+
response = Net::HTTP.post(uri, body, 'Content-Type' => 'application/json')
|
|
226
|
+
raise "S 端响应 #{response.code}" unless response.code == '200'
|
|
227
|
+
JSON.parse(response.body)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# 帮助文本。
|
|
231
|
+
#
|
|
232
|
+
# @return [String]
|
|
233
|
+
def help
|
|
234
|
+
<<~HELP
|
|
235
|
+
text-client —— textmind 命令行解析(C 端)
|
|
236
|
+
|
|
237
|
+
用法: ruby text-client.rb <namespace> <op> [参数...]
|
|
238
|
+
namespace: type | text | list | tree
|
|
239
|
+
op: 操作名(可含子路径,如 match/paragraph、draw/lines)
|
|
240
|
+
|
|
241
|
+
数据输入:
|
|
242
|
+
--text "字符串" 文本/类型操作输入(或 --file PATH,或管道 stdin)
|
|
243
|
+
--list '[...]' 列表操作输入(JSON 数组)
|
|
244
|
+
--tree '{...}' 树操作输入(JSON 对象)
|
|
245
|
+
|
|
246
|
+
通用:
|
|
247
|
+
--remote URL 改走 S 端服务解析(默认本地执行)
|
|
248
|
+
--help 本帮助
|
|
249
|
+
|
|
250
|
+
示例:
|
|
251
|
+
ruby text-client.rb type int --radix 2 --text 100
|
|
252
|
+
ruby text-client.rb text head --prefix aa --text aabbcc
|
|
253
|
+
ruby text-client.rb text exchange --oldtext e --newtext z --text abedef
|
|
254
|
+
ruby text-client.rb list map --func "proc{|c| c * 2}" --list '[1,2,3]'
|
|
255
|
+
ruby text-client.rb tree + --oprand '[1,3,5]' --tree '{"a":1}'
|
|
256
|
+
echo "a1a2b3" | ruby text-client.rb text match/paragraph --start a --finish b
|
|
257
|
+
ruby text-client.rb --remote http://127.0.0.1:9292 text head --prefix aa --text aabbcc
|
|
258
|
+
HELP
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# C 端命令行入口(可测试:传入 argv 与 io)。
|
|
262
|
+
#
|
|
263
|
+
# --remote 可出现在任意位置(--remote URL 或 --remote=URL 两种写法),
|
|
264
|
+
# 抽取后剩余参数的前两个位置量即 namespace 与 op。
|
|
265
|
+
#
|
|
266
|
+
# @param argv [Array<String>] 命令行参数
|
|
267
|
+
# @param io [IO] 结果输出流,默认 $stdout
|
|
268
|
+
# @return [Integer] 进程退出码
|
|
269
|
+
def run argv, io: $stdout
|
|
270
|
+
return (io.puts help; 0) if argv.empty? || argv.include?('--help')
|
|
271
|
+
|
|
272
|
+
# 归一化 --remote=URL 写法,再整对抽取 --remote,避免挤占 ns/op 位置
|
|
273
|
+
argv = argv.flat_map{|arg| arg.start_with?('--remote=') ? ['--remote', arg.split('=', 2)[1]] : [arg] }
|
|
274
|
+
remote = nil
|
|
275
|
+
if i = argv.index('--remote')
|
|
276
|
+
remote = argv[i+1]
|
|
277
|
+
argv = argv.dup.tap{|a| a.slice!(i, 2) }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
ns, op, *raw_params = argv
|
|
281
|
+
return (io.puts help; 1) unless ns && op && !ns.start_with?('--')
|
|
282
|
+
params = parse_params(raw_params)
|
|
283
|
+
|
|
284
|
+
begin
|
|
285
|
+
result = remote ? dispatch_remote(remote, ns, op, params) : dispatch_local(ns, op, params)
|
|
286
|
+
io.puts result.to_json
|
|
287
|
+
return 0
|
|
288
|
+
rescue => e
|
|
289
|
+
warn "text-client: #{e.class}: #{e.message}"
|
|
290
|
+
return 1
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
if $PROGRAM_NAME == __FILE__
|
|
296
|
+
exit(TextClient.run(ARGV))
|
|
297
|
+
end
|