toys-core 0.13.1 → 0.14.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +18 -16
- data/docs/guide.md +111 -6
- data/lib/toys/acceptor.rb +64 -58
- data/lib/toys/arg_parser.rb +4 -4
- data/lib/toys/cli.rb +3 -3
- data/lib/toys/completion.rb +5 -1
- data/lib/toys/context.rb +4 -3
- data/lib/toys/core.rb +1 -1
- data/lib/toys/errors.rb +14 -3
- data/lib/toys/flag.rb +10 -2
- data/lib/toys/input_file.rb +8 -8
- data/lib/toys/loader.rb +160 -92
- data/lib/toys/middleware.rb +24 -5
- data/lib/toys/settings.rb +2 -2
- data/lib/toys/source_info.rb +9 -9
- data/lib/toys/standard_middleware/show_help.rb +4 -16
- data/lib/toys/standard_mixins/exec.rb +86 -22
- data/lib/toys/standard_mixins/pager.rb +57 -0
- data/lib/toys/standard_mixins/xdg.rb +7 -7
- data/lib/toys/tool_definition.rb +10 -8
- data/lib/toys/utils/exec.rb +218 -37
- data/lib/toys/utils/help_text.rb +4 -1
- data/lib/toys/utils/pager.rb +138 -0
- data/lib/toys/wrappable_string.rb +12 -0
- metadata +6 -4
data/lib/toys/utils/exec.rb
CHANGED
@@ -54,54 +54,88 @@ module Toys
|
|
54
54
|
# options.
|
55
55
|
#
|
56
56
|
# Three general strategies are available for custom stream handling. First,
|
57
|
-
# you
|
57
|
+
# you can redirect to other streams such as files, IO objects, or Ruby
|
58
58
|
# strings. Some of these options map directly to options provided by the
|
59
|
-
# `Process#spawn` method. Second, you
|
60
|
-
# the streams programmatically. Third, you
|
59
|
+
# `Process#spawn` method. Second, you can use a controller to manipulate
|
60
|
+
# the streams programmatically. Third, you can capture output stream data
|
61
61
|
# and make it available in the result.
|
62
62
|
#
|
63
63
|
# Following is a full list of the stream handling options, along with how
|
64
64
|
# to specify them using the `:in`, `:out`, and `:err` options.
|
65
65
|
#
|
66
|
-
# * **Inherit parent stream:** You
|
66
|
+
# * **Inherit parent stream:** You can inherit the corresponding stream
|
67
67
|
# in the parent process by passing `:inherit` as the option value. This
|
68
68
|
# is the default if the subprocess is *not* run in the background.
|
69
|
-
#
|
69
|
+
#
|
70
|
+
# * **Redirect to null:** You can redirect to a null stream by passing
|
70
71
|
# `:null` as the option value. This connects to a stream that is not
|
71
72
|
# closed but contains no data, i.e. `/dev/null` on unix systems. This
|
72
73
|
# is the default if the subprocess is run in the background.
|
73
|
-
#
|
74
|
+
#
|
75
|
+
# * **Close the stream:** You can close the stream by passing `:close` as
|
74
76
|
# the option value. This is the same as passing `:close` to
|
75
77
|
# `Process#spawn`.
|
76
|
-
#
|
78
|
+
#
|
79
|
+
# * **Redirect to a file:** You can redirect to a file. This reads from
|
77
80
|
# an existing file when connected to `:in`, and creates or appends to a
|
78
81
|
# file when connected to `:out` or `:err`. To specify a file, use the
|
79
|
-
# setting `[:file, "/path/to/file"]`. You
|
82
|
+
# setting `[:file, "/path/to/file"]`. You can also, when writing a
|
80
83
|
# file, append an optional mode and permission code to the array. For
|
81
84
|
# example, `[:file, "/path/to/file", "a", 0644]`.
|
82
|
-
#
|
83
|
-
#
|
85
|
+
#
|
86
|
+
# * **Redirect to an IO object:** You can redirect to an IO object in the
|
87
|
+
# parent process, by passing the IO object as the option value. You can
|
84
88
|
# use any IO object. For example, you could connect the child's output
|
85
89
|
# to the parent's error using `out: $stderr`, or you could connect to
|
86
90
|
# an existing File stream. Unlike `Process#spawn`, this works for IO
|
87
91
|
# objects that do not have a corresponding file descriptor (such as
|
88
92
|
# StringIO objects). In such a case, a thread will be spawned to pipe
|
89
93
|
# the IO data through to the child process.
|
90
|
-
#
|
94
|
+
#
|
95
|
+
# * **Redirect to a pipe:** You can redirect to a pipe created using
|
96
|
+
# `IO.pipe` (i.e. a two-element array of read and write IO objects) by
|
97
|
+
# passing the array as the option value. This will connect the
|
98
|
+
# appropriate IO (either read or write), and close it in the parent.
|
99
|
+
# Thus, you can connect only one process to each end. If you want more
|
100
|
+
# direct control over IO closing behavior, pass the IO object (i.e. the
|
101
|
+
# element of the pipe array) directly.
|
102
|
+
#
|
103
|
+
# * **Combine with another child stream:** You can redirect one child
|
91
104
|
# output stream to another, to combine them. To merge the child's error
|
92
105
|
# stream into its output stream, use `err: [:child, :out]`.
|
93
|
-
#
|
106
|
+
#
|
107
|
+
# * **Read from a string:** You can pass a string to the input stream by
|
94
108
|
# setting `[:string, "the string"]`. This works only for `:in`.
|
95
|
-
#
|
109
|
+
#
|
110
|
+
# * **Capture output stream:** You can capture a stream and make it
|
96
111
|
# available on the {Toys::Utils::Exec::Result} object, using the
|
97
112
|
# setting `:capture`. This works only for the `:out` and `:err`
|
98
113
|
# streams.
|
99
|
-
#
|
114
|
+
#
|
115
|
+
# * **Use the controller:** You can hook a stream to the controller using
|
100
116
|
# the setting `:controller`. You can then manipulate the stream via the
|
101
117
|
# controller. If you pass a block to {Toys::Utils::Exec#exec}, it
|
102
118
|
# yields the {Toys::Utils::Exec::Controller}, giving you access to
|
103
119
|
# streams.
|
104
120
|
#
|
121
|
+
# * **Make copies of an output stream:** You can "tee," or duplicate the
|
122
|
+
# `:out` or `:err` stream and redirect those copies to various
|
123
|
+
# destinations. To specify a tee, use the setting `[:tee, ...]` where
|
124
|
+
# the additional array elements include two or more of the following.
|
125
|
+
# See the corresponding documentation above for more detail.
|
126
|
+
# * `:inherit` to direct to the parent process's stream.
|
127
|
+
# * `:capture` to capture the stream and store it in the result.
|
128
|
+
# * `:controller` to direct the stream to the controller.
|
129
|
+
# * `[:file, "/path/to/file"]` to write to a file.
|
130
|
+
# * An `IO` or `StringIO` object.
|
131
|
+
# * An array of two `IO` objects representing a pipe
|
132
|
+
#
|
133
|
+
# Additionally, the last element of the array can be a hash of options.
|
134
|
+
# Supported options include:
|
135
|
+
# * `:buffer_size` The size of the memory buffer for each element of
|
136
|
+
# the tee. Larger buffers may allow higher throughput. The default
|
137
|
+
# is 65536.
|
138
|
+
#
|
105
139
|
# ### Result handling
|
106
140
|
#
|
107
141
|
# A subprocess result is represented by a {Toys::Utils::Exec::Result}
|
@@ -171,7 +205,7 @@ module Toys
|
|
171
205
|
# not present, the command is not logged.
|
172
206
|
#
|
173
207
|
# * `:log_level` (Integer,false) Level for logging the actual command.
|
174
|
-
# Defaults to Logger::INFO if not present. You
|
208
|
+
# Defaults to Logger::INFO if not present. You can also pass `false` to
|
175
209
|
# disable logging of the command.
|
176
210
|
#
|
177
211
|
# * `:log_cmd` (String) The string logged for the actual command.
|
@@ -227,7 +261,7 @@ module Toys
|
|
227
261
|
end
|
228
262
|
|
229
263
|
##
|
230
|
-
# Execute a command. The command
|
264
|
+
# Execute a command. The command can be given as a single string to pass
|
231
265
|
# to a shell, or an array of strings indicating a posix command.
|
232
266
|
#
|
233
267
|
# If the process is not set to run in the background, and a block is
|
@@ -310,7 +344,7 @@ module Toys
|
|
310
344
|
end
|
311
345
|
|
312
346
|
##
|
313
|
-
# Execute a command. The command
|
347
|
+
# Execute a command. The command can be given as a single string to pass
|
314
348
|
# to a shell, or an array of strings indicating a posix command.
|
315
349
|
#
|
316
350
|
# Captures standard out and returns it as a string.
|
@@ -400,7 +434,7 @@ module Toys
|
|
400
434
|
# An object that controls a subprocess. This object is returned from an
|
401
435
|
# execution running in the background, or is yielded to a control block
|
402
436
|
# for an execution running in the foreground.
|
403
|
-
# You
|
437
|
+
# You can use this object to interact with the subcommand's streams,
|
404
438
|
# send signals to the process, and get its result.
|
405
439
|
#
|
406
440
|
class Controller
|
@@ -502,8 +536,8 @@ module Toys
|
|
502
536
|
##
|
503
537
|
# Redirects the remainder of the given stream.
|
504
538
|
#
|
505
|
-
# You
|
506
|
-
# specified by its path. If specifying a file, you
|
539
|
+
# You can specify the stream as an IO or IO-like object, or as a file
|
540
|
+
# specified by its path. If specifying a file, you can optionally
|
507
541
|
# provide the mode and permissions for the call to `File#open`. You can
|
508
542
|
# also specify the value `:null` to indicate the null file.
|
509
543
|
#
|
@@ -540,8 +574,8 @@ module Toys
|
|
540
574
|
##
|
541
575
|
# Redirects the remainder of the standard input stream.
|
542
576
|
#
|
543
|
-
# You
|
544
|
-
# specified by its path. If specifying a file, you
|
577
|
+
# You can specify the stream as an IO or IO-like object, or as a file
|
578
|
+
# specified by its path. If specifying a file, you can optionally
|
545
579
|
# provide the mode and permissions for the call to `File#open`. You can
|
546
580
|
# also specify the value `:null` to indicate the null file.
|
547
581
|
#
|
@@ -559,8 +593,8 @@ module Toys
|
|
559
593
|
##
|
560
594
|
# Redirects the remainder of the standard output stream.
|
561
595
|
#
|
562
|
-
# You
|
563
|
-
# specified by its path. If specifying a file, you
|
596
|
+
# You can specify the stream as an IO or IO-like object, or as a file
|
597
|
+
# specified by its path. If specifying a file, you can optionally
|
564
598
|
# provide the mode and permissions for the call to `File#open`. You can
|
565
599
|
# also specify the value `:null` to indicate the null file.
|
566
600
|
#
|
@@ -578,8 +612,8 @@ module Toys
|
|
578
612
|
##
|
579
613
|
# Redirects the remainder of the standard error stream.
|
580
614
|
#
|
581
|
-
# You
|
582
|
-
# specified by its path. If specifying a file, you
|
615
|
+
# You can specify the stream as an IO or IO-like object, or as a file
|
616
|
+
# specified by its path. If specifying a file, you can optionally
|
583
617
|
# provide the mode and permissions for the call to `File#open`.
|
584
618
|
#
|
585
619
|
# After calling this, do not interact directly with the stream.
|
@@ -594,7 +628,7 @@ module Toys
|
|
594
628
|
end
|
595
629
|
|
596
630
|
##
|
597
|
-
# Send the given signal to the process. The signal
|
631
|
+
# Send the given signal to the process. The signal can be specified
|
598
632
|
# by name or number.
|
599
633
|
#
|
600
634
|
# @param sig [Integer,String] The signal to send.
|
@@ -1091,7 +1125,7 @@ module Toys
|
|
1091
1125
|
when :close
|
1092
1126
|
:close
|
1093
1127
|
else
|
1094
|
-
stream if stream.respond_to?(:
|
1128
|
+
stream if stream.respond_to?(:read)
|
1095
1129
|
end
|
1096
1130
|
if in_stream == :close
|
1097
1131
|
stdstream.close
|
@@ -1163,16 +1197,23 @@ module Toys
|
|
1163
1197
|
end
|
1164
1198
|
|
1165
1199
|
def interpret_in_array(setting)
|
1166
|
-
|
1167
|
-
when ::Symbol
|
1200
|
+
if setting.first.is_a?(::Symbol)
|
1168
1201
|
setup_in_stream_of_type(setting.first, setting[1..-1])
|
1169
|
-
|
1202
|
+
elsif setting.first.is_a?(::String)
|
1170
1203
|
setup_in_stream_of_type(:file, setting)
|
1204
|
+
elsif setting.size == 2 && setting.first.is_a?(::IO) && setting.last.is_a?(::IO)
|
1205
|
+
interpret_in_pipe(*setting)
|
1171
1206
|
else
|
1172
1207
|
raise "Unknown value for in: #{setting.inspect}"
|
1173
1208
|
end
|
1174
1209
|
end
|
1175
1210
|
|
1211
|
+
def interpret_in_pipe(reader, writer)
|
1212
|
+
@spawn_opts[:in] = reader
|
1213
|
+
@child_streams << reader
|
1214
|
+
@parent_streams << writer
|
1215
|
+
end
|
1216
|
+
|
1176
1217
|
def setup_in_stream_of_type(type, args)
|
1177
1218
|
case type
|
1178
1219
|
when :controller
|
@@ -1199,7 +1240,7 @@ module Toys
|
|
1199
1240
|
end
|
1200
1241
|
|
1201
1242
|
def interpret_in_file(args)
|
1202
|
-
raise "Expected only file name" unless args.size == 1 && args.first.is_a?(::String)
|
1243
|
+
raise "Expected only file name for in" unless args.size == 1 && args.first.is_a?(::String)
|
1203
1244
|
@spawn_opts[:in] = args + [::File::RDONLY]
|
1204
1245
|
end
|
1205
1246
|
|
@@ -1230,16 +1271,23 @@ module Toys
|
|
1230
1271
|
end
|
1231
1272
|
|
1232
1273
|
def interpret_out_array(key, setting)
|
1233
|
-
|
1234
|
-
when ::Symbol
|
1274
|
+
if setting.first.is_a?(::Symbol)
|
1235
1275
|
setup_out_stream_of_type(key, setting.first, setting[1..-1])
|
1236
|
-
|
1276
|
+
elsif setting.first.is_a?(::String)
|
1237
1277
|
setup_out_stream_of_type(key, :file, setting)
|
1278
|
+
elsif setting.size == 2 && setting.first.is_a?(::IO) && setting.last.is_a?(::IO)
|
1279
|
+
interpret_out_pipe(key, *setting)
|
1238
1280
|
else
|
1239
1281
|
raise "Unknown value for #{key}: #{setting.inspect}"
|
1240
1282
|
end
|
1241
1283
|
end
|
1242
1284
|
|
1285
|
+
def interpret_out_pipe(key, reader, writer)
|
1286
|
+
@spawn_opts[key] = writer
|
1287
|
+
@child_streams << writer
|
1288
|
+
@parent_streams << reader
|
1289
|
+
end
|
1290
|
+
|
1243
1291
|
def setup_out_stream_of_type(key, type, args)
|
1244
1292
|
case type
|
1245
1293
|
when :controller
|
@@ -1260,17 +1308,150 @@ module Toys
|
|
1260
1308
|
copy_from_out_thread(key, args.first)
|
1261
1309
|
when :file
|
1262
1310
|
interpret_out_file(key, args)
|
1311
|
+
when :tee
|
1312
|
+
interpret_out_tee(key, args)
|
1263
1313
|
else
|
1264
1314
|
raise "Unknown type for #{key}: #{type.inspect}"
|
1265
1315
|
end
|
1266
1316
|
end
|
1267
1317
|
|
1268
1318
|
def interpret_out_file(key, args)
|
1269
|
-
raise "Expected file name" if args.empty? || !args.first.is_a?(::String)
|
1270
|
-
raise "Too many file arguments" if args.size > 3
|
1319
|
+
raise "Expected file name for #{key}" if args.empty? || !args.first.is_a?(::String)
|
1320
|
+
raise "Too many file arguments for #{key}" if args.size > 3
|
1271
1321
|
@spawn_opts[key] = args.size == 1 ? args.first : args
|
1272
1322
|
end
|
1273
1323
|
|
1324
|
+
def interpret_out_tee(key, args)
|
1325
|
+
opts = args.last.is_a?(::Hash) ? args.pop : {}
|
1326
|
+
reader = make_out_pipe(key)
|
1327
|
+
sinks = interpret_out_tee_arguments(key, args)
|
1328
|
+
tee_runner(key, reader, sinks, opts[:buffer_size] || 65_536)
|
1329
|
+
end
|
1330
|
+
|
1331
|
+
def interpret_out_tee_arguments(key, args)
|
1332
|
+
args.map do |arg|
|
1333
|
+
case arg
|
1334
|
+
when :inherit
|
1335
|
+
[key == :err ? $stderr : $stdout, nil]
|
1336
|
+
when :capture
|
1337
|
+
[::StringIO.new, :capture]
|
1338
|
+
when :controller
|
1339
|
+
tee_sink_for_controller(key)
|
1340
|
+
when ::IO, ::StringIO
|
1341
|
+
[arg, nil]
|
1342
|
+
when ::String
|
1343
|
+
[::File.open(arg, "w"), :close]
|
1344
|
+
when ::Array
|
1345
|
+
tee_sink_for_array(key, arg)
|
1346
|
+
else
|
1347
|
+
raise "Unknown value for #{key} tee argument: #{arg.inspect}"
|
1348
|
+
end
|
1349
|
+
end
|
1350
|
+
end
|
1351
|
+
|
1352
|
+
def tee_sink_for_controller(key)
|
1353
|
+
@controller_streams[key], writer = ::IO.pipe
|
1354
|
+
writer.sync = true
|
1355
|
+
[writer, :close]
|
1356
|
+
end
|
1357
|
+
|
1358
|
+
def tee_sink_for_array(key, arg)
|
1359
|
+
if arg.size == 2 &&
|
1360
|
+
arg.last.is_a?(::IO) &&
|
1361
|
+
(arg.first == :autoclose || arg.first.is_a?(::IO))
|
1362
|
+
[arg.last, :close]
|
1363
|
+
else
|
1364
|
+
arg = arg[1..-1] if arg.first == :file
|
1365
|
+
if arg.empty? || !arg.first.is_a?(::String)
|
1366
|
+
raise "Expected file name for #{key} tee argument"
|
1367
|
+
end
|
1368
|
+
raise "Too many file arguments for #{key} tee argument" if arg.size > 3
|
1369
|
+
arg += ["w"] if arg.size == 1
|
1370
|
+
[::File.open(*arg), :close]
|
1371
|
+
end
|
1372
|
+
end
|
1373
|
+
|
1374
|
+
def tee_runner(key, reader, sinks, buffer_size)
|
1375
|
+
@join_threads << ::Thread.new do
|
1376
|
+
sinks.map! { |io, on_done| [io, ::String.new, :write_nonblock, on_done] }
|
1377
|
+
until sinks.empty?
|
1378
|
+
tee_wait_for_streams(reader, sinks)
|
1379
|
+
reader = tee_read_stream(reader, sinks, buffer_size)
|
1380
|
+
tee_write_streams(sinks, key, reader.nil?)
|
1381
|
+
end
|
1382
|
+
end
|
1383
|
+
end
|
1384
|
+
|
1385
|
+
def tee_wait_for_streams(reader, sinks)
|
1386
|
+
read_select = reader && [reader]
|
1387
|
+
write_select = []
|
1388
|
+
sinks.each do |io, buffer, _write_method, _on_done|
|
1389
|
+
write_select << io unless buffer.empty?
|
1390
|
+
end
|
1391
|
+
::IO.select(read_select, write_select)
|
1392
|
+
end
|
1393
|
+
|
1394
|
+
def tee_read_stream(reader, sinks, buffer_size)
|
1395
|
+
return nil if reader.nil?
|
1396
|
+
max = tee_amount_to_read(sinks, buffer_size)
|
1397
|
+
return reader unless max.positive?
|
1398
|
+
begin
|
1399
|
+
data = reader.read_nonblock(max)
|
1400
|
+
unless data.empty?
|
1401
|
+
sinks.each { |_io, buffer, _write_method, _on_done| buffer << data }
|
1402
|
+
end
|
1403
|
+
reader
|
1404
|
+
rescue ::IO::WaitReadable
|
1405
|
+
reader
|
1406
|
+
rescue ::StandardError
|
1407
|
+
reader.close rescue nil # rubocop:disable Style/RescueModifier
|
1408
|
+
nil
|
1409
|
+
end
|
1410
|
+
end
|
1411
|
+
|
1412
|
+
def tee_write_streams(sinks, key, read_complete)
|
1413
|
+
sinks.delete_if do |sink|
|
1414
|
+
io, buffer, write_method, on_done = sink
|
1415
|
+
done, write_method = tee_write_one_stream(io, buffer, write_method, read_complete)
|
1416
|
+
sink[2] = write_method
|
1417
|
+
if done
|
1418
|
+
case on_done
|
1419
|
+
when :close
|
1420
|
+
io.close rescue nil # rubocop:disable Style/RescueModifier
|
1421
|
+
when :capture
|
1422
|
+
@mutex.synchronize do
|
1423
|
+
@captures[key] = io.string
|
1424
|
+
end
|
1425
|
+
end
|
1426
|
+
end
|
1427
|
+
done
|
1428
|
+
end
|
1429
|
+
end
|
1430
|
+
|
1431
|
+
def tee_write_one_stream(io, buffer, write_method, read_complete)
|
1432
|
+
return [read_complete, write_method] if buffer.empty?
|
1433
|
+
begin
|
1434
|
+
bytes = io.send(write_method, buffer)
|
1435
|
+
buffer.slice!(0, bytes)
|
1436
|
+
[false, write_method]
|
1437
|
+
rescue ::IO::WaitWritable, ::Errno::EINTR
|
1438
|
+
[false, write_method]
|
1439
|
+
rescue ::Errno::EBADF, ::NoMethodError
|
1440
|
+
raise if write_method == :write
|
1441
|
+
[false, :write]
|
1442
|
+
rescue ::StandardError
|
1443
|
+
[true, write_method]
|
1444
|
+
end
|
1445
|
+
end
|
1446
|
+
|
1447
|
+
def tee_amount_to_read(sink_info, buffer_size)
|
1448
|
+
maxbuff = 0
|
1449
|
+
sink_info.each do |_sink, buffer, _meth|
|
1450
|
+
maxbuff = buffer.size if buffer.size > maxbuff
|
1451
|
+
end
|
1452
|
+
buffer_size - maxbuff
|
1453
|
+
end
|
1454
|
+
|
1274
1455
|
def make_null_stream(key, mode)
|
1275
1456
|
f = ::File.open(::File::NULL, mode)
|
1276
1457
|
@spawn_opts[key] = f
|
data/lib/toys/utils/help_text.rb
CHANGED
@@ -171,7 +171,10 @@ module Toys
|
|
171
171
|
([@tool] + @delegates).each do |tool|
|
172
172
|
name_len = tool.full_name.length
|
173
173
|
subtools = @loader.list_subtools(tool.full_name,
|
174
|
-
recursive: recursive,
|
174
|
+
recursive: recursive,
|
175
|
+
include_hidden: include_hidden,
|
176
|
+
include_namespaces: include_hidden,
|
177
|
+
include_non_runnable: include_hidden)
|
175
178
|
subtools.each do |subtool|
|
176
179
|
local_name = subtool.full_name.slice(name_len..-1).join(" ")
|
177
180
|
subtools_by_name[local_name] = subtool
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "toys/utils/exec"
|
4
|
+
|
5
|
+
module Toys
|
6
|
+
module Utils
|
7
|
+
##
|
8
|
+
# A class that invokes an external pager.
|
9
|
+
#
|
10
|
+
# @example Using a pager for regular output
|
11
|
+
#
|
12
|
+
# Toys::Utils::Pager.start do |io|
|
13
|
+
# io.puts "A long string\n"
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# @example Piping output from a command
|
17
|
+
#
|
18
|
+
# exec_service = Toys::Utils::Exec.new
|
19
|
+
# Toys::Utils::Pager.start(exec_service: exec_service) do |io|
|
20
|
+
# exec_service.exec(["/bin/ls", "-alF"], out: io)
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
class Pager
|
24
|
+
##
|
25
|
+
# Creates a new pager.
|
26
|
+
#
|
27
|
+
# @param command [String,Array<String>,boolean] The command to use to
|
28
|
+
# invoke the pager. May be specified as a string to be passed to the
|
29
|
+
# shell, an array of strings representing a posix command, the value
|
30
|
+
# `true` to use the default (normally `less -FIRX`), or the value
|
31
|
+
# `false` to disable the pager and write directly to the output
|
32
|
+
# stream. Default is `true`.
|
33
|
+
# @param exec_service [Toys::Utils::Exec] The service to use for
|
34
|
+
# executing commands, or `nil` (the default) to use a default.
|
35
|
+
# @param fallback_io [IO] An IO-like object to write to if the pager is
|
36
|
+
# disabled. Defaults to `$stdout`.
|
37
|
+
#
|
38
|
+
def initialize(command: true, exec_service: nil, fallback_io: nil)
|
39
|
+
@command = command == true ? Pager.default_command : command
|
40
|
+
@command ||= nil
|
41
|
+
@exec_service = exec_service || Pager.default_exec_service
|
42
|
+
@fallback_io = fallback_io || $stdout
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Runs the pager. Takes a block and yields an IO-like object that passes
|
47
|
+
# text to the pager. Can be called multiple times on the same pager.
|
48
|
+
#
|
49
|
+
# @yieldparam io [IO] An object that can be written to, to pass data to
|
50
|
+
# the pager.
|
51
|
+
# @return [Integer] The exit code of the pager process.
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
#
|
55
|
+
# pager = Toys::Utils::Pager.new
|
56
|
+
# pager.start do |io|
|
57
|
+
# io.puts "A long string\n"
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
def start
|
61
|
+
if @command
|
62
|
+
result = @exec_service.exec(@command, in: :controller) do |controller|
|
63
|
+
yield controller.in if controller.pid
|
64
|
+
end
|
65
|
+
return result.exit_code unless result.failed?
|
66
|
+
end
|
67
|
+
yield @fallback_io
|
68
|
+
0
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# The command for running the pager process. May be specified as a string
|
73
|
+
# to be passed to the shell, an array of strings representing a posix
|
74
|
+
# command, or `nil` to disable the pager and write directly to an output
|
75
|
+
# stream.
|
76
|
+
#
|
77
|
+
# @return [String,Array<String>,nil]
|
78
|
+
#
|
79
|
+
attr_accessor :command
|
80
|
+
|
81
|
+
##
|
82
|
+
# The IO stream used if the pager is disabled or could not be executed.
|
83
|
+
#
|
84
|
+
# @return [IO]
|
85
|
+
#
|
86
|
+
attr_accessor :fallback_io
|
87
|
+
|
88
|
+
class << self
|
89
|
+
##
|
90
|
+
# A convenience method that creates a pager and runs it once by calling
|
91
|
+
# {Pager#start}.
|
92
|
+
#
|
93
|
+
# @param command [String,Array<String>,boolean] The command to use to
|
94
|
+
# invoke the pager. May be specified as a string to be passed to the
|
95
|
+
# shell, an array of strings representing a posix command, the value
|
96
|
+
# `true` to use the default (normally `less -FIRX`), or the value
|
97
|
+
# `false` to disable the pager and write directly to the output
|
98
|
+
# stream. Default is `true`.
|
99
|
+
# @param exec_service [Toys::Utils::Exec] The service to use for
|
100
|
+
# executing commands, or `nil` (the default) to use a default.
|
101
|
+
# @param fallback_io [IO] An IO-like object to write to if the pager is
|
102
|
+
# disabled. Defaults to `$stdout`.
|
103
|
+
# @return [Integer] The exit code of the pager process.
|
104
|
+
#
|
105
|
+
# @example
|
106
|
+
#
|
107
|
+
# Toys::Utils::Pager.start do |io|
|
108
|
+
# io.puts "A long string\n"
|
109
|
+
# end
|
110
|
+
def start(command: true, exec_service: nil, fallback_io: nil, &block)
|
111
|
+
pager = new(command: command, exec_service: exec_service, fallback_io: fallback_io)
|
112
|
+
pager.start(&block)
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# @private
|
117
|
+
#
|
118
|
+
def default_command
|
119
|
+
unless defined? @default_command
|
120
|
+
@default_command = ::ENV["PAGER"]
|
121
|
+
unless @default_command
|
122
|
+
path = `which less`.strip
|
123
|
+
@default_command = [path, "-FIRX"] unless path.empty?
|
124
|
+
end
|
125
|
+
end
|
126
|
+
@default_command
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# @private
|
131
|
+
#
|
132
|
+
def default_exec_service
|
133
|
+
@default_exec_service ||= Exec.new
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -4,9 +4,19 @@ module Toys
|
|
4
4
|
##
|
5
5
|
# A string intended for word-wrapped display.
|
6
6
|
#
|
7
|
+
# A WrappableString is an array of string "fragments" representing the atomic
|
8
|
+
# units that should not be split when word-wrapping. It should be possible to
|
9
|
+
# reconstruct the original string by joining these fragments with whitespace.
|
10
|
+
#
|
7
11
|
class WrappableString
|
8
12
|
##
|
9
13
|
# Create a wrapped string.
|
14
|
+
#
|
15
|
+
# You can pass either:
|
16
|
+
#
|
17
|
+
# * A single String, which will be split into fragments by whitespace.
|
18
|
+
# * An array of Strings representing the fragments explicitly.
|
19
|
+
#
|
10
20
|
# @param string [String,Array<String>] The string or array of string
|
11
21
|
# fragments
|
12
22
|
#
|
@@ -35,6 +45,7 @@ module Toys
|
|
35
45
|
|
36
46
|
##
|
37
47
|
# Returns true if the string is empty (i.e. has no fragments)
|
48
|
+
#
|
38
49
|
# @return [Boolean]
|
39
50
|
#
|
40
51
|
def empty?
|
@@ -43,6 +54,7 @@ module Toys
|
|
43
54
|
|
44
55
|
##
|
45
56
|
# Returns the string without any wrapping
|
57
|
+
#
|
46
58
|
# @return [String]
|
47
59
|
#
|
48
60
|
def string
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: toys-core
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.14.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Azuma
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-03
|
11
|
+
date: 2022-10-03 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Toys-Core is the command line tool framework underlying Toys. It can
|
14
14
|
be used to create command line executables using the Toys DSL and classes.
|
@@ -60,6 +60,7 @@ files:
|
|
60
60
|
- lib/toys/standard_mixins/gems.rb
|
61
61
|
- lib/toys/standard_mixins/git_cache.rb
|
62
62
|
- lib/toys/standard_mixins/highline.rb
|
63
|
+
- lib/toys/standard_mixins/pager.rb
|
63
64
|
- lib/toys/standard_mixins/terminal.rb
|
64
65
|
- lib/toys/standard_mixins/xdg.rb
|
65
66
|
- lib/toys/template.rb
|
@@ -69,6 +70,7 @@ files:
|
|
69
70
|
- lib/toys/utils/gems.rb
|
70
71
|
- lib/toys/utils/git_cache.rb
|
71
72
|
- lib/toys/utils/help_text.rb
|
73
|
+
- lib/toys/utils/pager.rb
|
72
74
|
- lib/toys/utils/terminal.rb
|
73
75
|
- lib/toys/utils/xdg.rb
|
74
76
|
- lib/toys/wrappable_string.rb
|
@@ -76,10 +78,10 @@ homepage: https://github.com/dazuma/toys
|
|
76
78
|
licenses:
|
77
79
|
- MIT
|
78
80
|
metadata:
|
79
|
-
changelog_uri: https://dazuma.github.io/toys/gems/toys-core/v0.
|
81
|
+
changelog_uri: https://dazuma.github.io/toys/gems/toys-core/v0.14.1/file.CHANGELOG.html
|
80
82
|
source_code_uri: https://github.com/dazuma/toys/tree/main/toys-core
|
81
83
|
bug_tracker_uri: https://github.com/dazuma/toys/issues
|
82
|
-
documentation_uri: https://dazuma.github.io/toys/gems/toys-core/v0.
|
84
|
+
documentation_uri: https://dazuma.github.io/toys/gems/toys-core/v0.14.1
|
83
85
|
post_install_message:
|
84
86
|
rdoc_options: []
|
85
87
|
require_paths:
|