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.
Files changed (9) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -3
  3. data/cc.rb +1 -0
  4. data/monkey-patch.rb +375 -1
  5. data/text-client.rb +297 -0
  6. data/text-server.rb +403 -0
  7. data/text-tools.rb +418 -0
  8. data/tree.rb +184 -184
  9. 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