toys-core 0.13.1 → 0.14.1
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/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:
|