run_tasks 3.2.9 → 4.0.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 +474 -0
- data/src/bootstrap.rb +3 -6
- data/src/run/core.rb +57 -39
- data/src/run/error/duplicate_parameter.rb +12 -0
- data/src/run/error/unexpected_named_parameters.rb +13 -0
- data/src/run/helper/question_helper.rb +7 -1
- data/src/run/helper/yesno_helper.rb +43 -0
- data/src/run/task/block_task.rb +6 -5
- data/src/run/version.rb +5 -0
- metadata +6 -16
- data/CHEATSHEET.md +0 -218
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 18567c31d63e5cacb71de0cf93a2a988a36161add471ba464e0c76fc80e1a5ae
|
|
4
|
+
data.tar.gz: 5e1a9ca0f82fe57c4ebab2bafe9487f588fa436f0c2a14b4e2555bace0874000
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b70d57d950528c6182766218104938627b2b9469baccebd1e4472f3ef8ec1363283dd2432d898b4d89bc8173e3a249acfdc02b7fa7eeed99e5598323599cd5eb
|
|
7
|
+
data.tar.gz: 8ba35fe28b3ef9346ff031041b342d641975503d068f9e817df178a429a663a11af423b1f3e1f1c03622043013513603b5b98b57cf10cfbbc5cbf759d9fefd2f
|
data/README.md
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
# Run
|
|
2
|
+
|
|
3
|
+
Manage your Ruby projects with a straightforward syntax.
|
|
4
|
+
|
|
5
|
+
## Compatibility
|
|
6
|
+
|
|
7
|
+
Run is known to work with Ruby >=2.4.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
gem install "run_tasks"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
To enable ZSH completions:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
run install_completions
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Running Run
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
run your_task
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If you want to display the available tasks:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
run help
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> The help provides Run's version, so `run version` will display the same screen.
|
|
34
|
+
|
|
35
|
+
> Note that the command `run` displays the help screen by default when no task is specified.
|
|
36
|
+
|
|
37
|
+
## Writing tasks
|
|
38
|
+
|
|
39
|
+
```rb
|
|
40
|
+
# Displays hello.
|
|
41
|
+
task :hello do
|
|
42
|
+
puts "hello!"
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The comment is used by the Run to display the help. It must be placed directly above the task and can be multiline.
|
|
47
|
+
|
|
48
|
+
> Note that help verbatims support Markdown syntax.
|
|
49
|
+
|
|
50
|
+
`task` also support aliases, so you can call a task with different names:
|
|
51
|
+
|
|
52
|
+
```rb
|
|
53
|
+
task [:console, :c] do
|
|
54
|
+
run "bundle exec rails c"
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Running `run c` or `run console` will then start the Rails console.
|
|
59
|
+
|
|
60
|
+
## Running shell commands
|
|
61
|
+
|
|
62
|
+
```rb
|
|
63
|
+
# Accessing a non-existent file.
|
|
64
|
+
task :boom do
|
|
65
|
+
run "stat foo"
|
|
66
|
+
run "echo 'hello!'"
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
It outputs:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
$ run boom
|
|
74
|
+
|
|
75
|
+
> stat foo
|
|
76
|
+
|
|
77
|
+
stat: cannot stat 'foo': No such file or directory
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
> As you can see, `run` captures errors and stops when something bad happens.
|
|
81
|
+
|
|
82
|
+
## Calling other tasks
|
|
83
|
+
|
|
84
|
+
You can run tasks arbitrarily from any other task by passing a symbol:
|
|
85
|
+
|
|
86
|
+
```rb
|
|
87
|
+
task :eslint do
|
|
88
|
+
run "npx eslint"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
task :flow do
|
|
92
|
+
run "npx flow"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
task :lint_js do
|
|
96
|
+
run :eslint
|
|
97
|
+
run :flow
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
By running `run lint_js` you will call `eslint` and `flow` tasks.
|
|
102
|
+
|
|
103
|
+
## Passing arguments
|
|
104
|
+
|
|
105
|
+
You can pass arguments to tasks from the CLI:
|
|
106
|
+
|
|
107
|
+
```rb
|
|
108
|
+
task :hello do |name, age|
|
|
109
|
+
puts "Hello #{name}, you are #{age}."
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
$ run hello 'John Doe' 30
|
|
115
|
+
Hello John Doe, you are 30.
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
But also from the Runfile itself:
|
|
119
|
+
|
|
120
|
+
```rb
|
|
121
|
+
task :test do
|
|
122
|
+
run :hello, "John Doe", 30
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Every argument passed from the CLI is auto-converted, as a whole:
|
|
127
|
+
|
|
128
|
+
| Argument | Result |
|
|
129
|
+
|-----------------------------------------------------------|-----------|
|
|
130
|
+
| an integer: `42`, `-3`, `0` | `Integer` |
|
|
131
|
+
| a decimal: `1.5`, `0.5`, `-0.5` | `Float` |
|
|
132
|
+
| `true` / `false` | boolean |
|
|
133
|
+
| anything else, including superfluous zeros: `007`, `00.5` | `String` |
|
|
134
|
+
|
|
135
|
+
> Arguments never become symbols: if your task needs one, call `.to_sym`.
|
|
136
|
+
|
|
137
|
+
### Named parameters
|
|
138
|
+
|
|
139
|
+
Tasks also support named parameters, from the Runfile:
|
|
140
|
+
|
|
141
|
+
```rb
|
|
142
|
+
task :hello do |name:, age:|
|
|
143
|
+
puts "Hello #{name}, you are #{age}."
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
task :test do
|
|
147
|
+
run :hello, name: "John Doe", age: 30
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
And from the CLI, with `name=value`:
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
$ run hello name='John Doe' age=30
|
|
155
|
+
Hello John Doe, you are 30.
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The value is auto-converted like any other argument. Separate values with commas to pass an array, each element being auto-converted:
|
|
159
|
+
|
|
160
|
+
| Command | Received by the task |
|
|
161
|
+
|----------------------------------------|-------------------------------------|
|
|
162
|
+
| `run review reviewer=clement` | `reviewer: "clement"` |
|
|
163
|
+
| `run review reviewer=clement,morisset` | `reviewer: ["clement", "morisset"]` |
|
|
164
|
+
| `run build retries=3` | `retries: 3` |
|
|
165
|
+
| `run build ids=1,2,x` | `ids: [1, 2, "x"]` |
|
|
166
|
+
| `run build dry=true` | `dry: true` |
|
|
167
|
+
| `run build tag=` | `tag: ""` |
|
|
168
|
+
|
|
169
|
+
> A single value gives a scalar, not an array: if your task expects a list, use `Array(value)`. Commas cannot be escaped.
|
|
170
|
+
|
|
171
|
+
### Named booleans
|
|
172
|
+
|
|
173
|
+
We also support named booleans. For example, imagine you want to run your server in tunnel mode:
|
|
174
|
+
|
|
175
|
+
```rb
|
|
176
|
+
task :server do |tunnel: false|
|
|
177
|
+
run "my_server #{tunnel ? "--tunnel" : ""}"
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
When you call...
|
|
182
|
+
|
|
183
|
+
```sh
|
|
184
|
+
run server +tunnel
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
...the parameter `tunnel: true` will be passed to your task. The same if you pass `-tunnel`, it will pass `false`.
|
|
188
|
+
|
|
189
|
+
### Additional notes
|
|
190
|
+
|
|
191
|
+
- Hyphens in names are replaced by underscores: `+dry-run` and `dry-run=1` give `dry_run:`.
|
|
192
|
+
- A name cannot be given twice (`a=1 a=2`, `+a -a`, `+a a=2`).
|
|
193
|
+
- A task receiving named parameters or booleans must declare keywords (`reviewer:`, `**options`…). A task declaring keywords and receiving an unknown one fails too, as with any Ruby block.
|
|
194
|
+
|
|
195
|
+
## Quiet mode
|
|
196
|
+
|
|
197
|
+
If you want to run a task (or a command) without it to being able to write on STDOUT/STDERR, pass the quiet flag to true :
|
|
198
|
+
|
|
199
|
+
```rb
|
|
200
|
+
task :echo do
|
|
201
|
+
run "echo 'test'", quiet: true
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
It's useful, for example, when you want to delete a file at the beginning of a task without it to being verbose when the file does not exist.
|
|
206
|
+
|
|
207
|
+
## Colorization & styles
|
|
208
|
+
|
|
209
|
+
It is often needed to colorize/stylize the messages sent to the user with `puts`. For this matter, Run is natively shipped with basic styles.
|
|
210
|
+
|
|
211
|
+
For example:
|
|
212
|
+
|
|
213
|
+
```rb
|
|
214
|
+
puts "Please wait...".yellow.bold
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The available styles are :
|
|
218
|
+
|
|
219
|
+
- bold
|
|
220
|
+
- dim
|
|
221
|
+
- italic
|
|
222
|
+
- underline
|
|
223
|
+
- inverse
|
|
224
|
+
- strikethrough
|
|
225
|
+
|
|
226
|
+
And the available colors are :
|
|
227
|
+
|
|
228
|
+
- black
|
|
229
|
+
- red
|
|
230
|
+
- green
|
|
231
|
+
- yellow
|
|
232
|
+
- blue
|
|
233
|
+
- magenta
|
|
234
|
+
- cyan
|
|
235
|
+
- white
|
|
236
|
+
- bright_black
|
|
237
|
+
- bright_red
|
|
238
|
+
- bright_green
|
|
239
|
+
- bright_yellow
|
|
240
|
+
- bright_blue
|
|
241
|
+
- bright_magenta
|
|
242
|
+
- bright_cyan
|
|
243
|
+
- bright_white
|
|
244
|
+
|
|
245
|
+
## Helpers
|
|
246
|
+
|
|
247
|
+
Run comes with a few helpers to help writing tasks quicker.
|
|
248
|
+
|
|
249
|
+
### are_you_sure
|
|
250
|
+
|
|
251
|
+
When called, it asks a question to the user which has the choice to enter `y`, `yes`, `n` or `no` (the default answer). When `no` is selected, the task will exit with a `9` code.
|
|
252
|
+
|
|
253
|
+
```rb
|
|
254
|
+
task :dangerous_task do
|
|
255
|
+
are_you_sure "This task will delete your hard drive. Are you sure about that?"
|
|
256
|
+
# Some dangerous actions.
|
|
257
|
+
end
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### bind
|
|
261
|
+
|
|
262
|
+
`bind` lets you group several tasks/processes so when one stops all the other will stop as well.
|
|
263
|
+
|
|
264
|
+
```rb
|
|
265
|
+
task :dev do
|
|
266
|
+
bind :rails, :vite
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
task :rails do
|
|
270
|
+
run "bundle exec unicorn -c config/unicorn.rb -p 3000"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
task :vite do
|
|
274
|
+
run "bin/vite dev"
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### catch_interruption
|
|
279
|
+
|
|
280
|
+
Alternatively, if you have a command that stays open until you hit `CTRL+C` (like a Docker container) and you need to catch the binding, you probably want to use `catch_interruption` instead so it lets the command handle the interruption gracefully and executes some actions afterwards.
|
|
281
|
+
|
|
282
|
+
```rb
|
|
283
|
+
# Run server in development mode.
|
|
284
|
+
task :server do
|
|
285
|
+
catch_interruption("docker run -d -p 8080:80 web_server") do
|
|
286
|
+
# Run some clean up actions here after the command has been interrupted.
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### exists
|
|
292
|
+
|
|
293
|
+
Handy helper to verify if a program exists.
|
|
294
|
+
|
|
295
|
+
```rb
|
|
296
|
+
task :man do
|
|
297
|
+
command = exists("bat") ? "bat" : "cat"
|
|
298
|
+
run "#{command} #{__dir__}/README.md"
|
|
299
|
+
end
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### expand
|
|
303
|
+
|
|
304
|
+
This one takes a glob string and process it to return a list of files.
|
|
305
|
+
|
|
306
|
+
```rb
|
|
307
|
+
task :scan do |glob|
|
|
308
|
+
expand(glob).each do |file_path|
|
|
309
|
+
# Do something.
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### menu
|
|
315
|
+
|
|
316
|
+
It displays an advanced menu where you can choose an element that will be returned by the function.
|
|
317
|
+
|
|
318
|
+
```rb
|
|
319
|
+
# Array syntax
|
|
320
|
+
task :deploy_aws do
|
|
321
|
+
region = menu "Location?", ["us‑east‑2", "us-west-1", "eu-west-1"]
|
|
322
|
+
end
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
```rb
|
|
326
|
+
# Hash syntax
|
|
327
|
+
task :deploy_aws do
|
|
328
|
+
region = menu "Location?", {
|
|
329
|
+
"US East (Ohio)" => "us‑east‑2",
|
|
330
|
+
"US West (N. California)" => "us-west-1",
|
|
331
|
+
"Europe (Ireland)" => "eu-west-1",
|
|
332
|
+
}
|
|
333
|
+
end
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### pause
|
|
337
|
+
|
|
338
|
+
You can pause a task if needed, waiting for the user to press any key.
|
|
339
|
+
|
|
340
|
+
```rb
|
|
341
|
+
task :deploy do
|
|
342
|
+
puts "Press enter to continue.".yellow
|
|
343
|
+
pause
|
|
344
|
+
run :deploy_production
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### question
|
|
349
|
+
|
|
350
|
+
Displays a question and returns the answer.
|
|
351
|
+
|
|
352
|
+
```rb
|
|
353
|
+
task :survey do
|
|
354
|
+
comment = question "Did your meal was good?"
|
|
355
|
+
end
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
You can also pass a regex to validation the entered value:
|
|
359
|
+
|
|
360
|
+
```rb
|
|
361
|
+
task :age do
|
|
362
|
+
age = question "What is your age?", /^\d+$/
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
A default value can be set, it will be returned if the user enters nothing:
|
|
367
|
+
|
|
368
|
+
```rb
|
|
369
|
+
task :age do
|
|
370
|
+
age = question "What is your age?", /^\d+$/, default: "30"
|
|
371
|
+
end
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### yesno
|
|
375
|
+
|
|
376
|
+
Asks a yes/no question and returns a boolean. The user can answer `y`, `yes`, `n` or `no`.
|
|
377
|
+
|
|
378
|
+
```rb
|
|
379
|
+
task :deploy do
|
|
380
|
+
run :migrate if yesno "Run migrations?"
|
|
381
|
+
end
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Without a default value, an answer is required. With one, it is returned if the user enters nothing:
|
|
385
|
+
|
|
386
|
+
```rb
|
|
387
|
+
task :deploy do
|
|
388
|
+
run :migrate if yesno "Run migrations?", default: true
|
|
389
|
+
end
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### wait_for_interruption
|
|
393
|
+
|
|
394
|
+
If you need to wait for the user to interrupt a task of, for example, several running servers, you can use the `wait_for_interruption` helper.
|
|
395
|
+
|
|
396
|
+
```rb
|
|
397
|
+
# Run server in development mode.
|
|
398
|
+
task :server do
|
|
399
|
+
Thread.new do
|
|
400
|
+
# Run first server.
|
|
401
|
+
end
|
|
402
|
+
Thread.new do
|
|
403
|
+
# Run second server.
|
|
404
|
+
end
|
|
405
|
+
# Wait for CTRL+C.
|
|
406
|
+
wait_for_interruption do
|
|
407
|
+
# Some actions to handle before terminating the task, like killing the servers.
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Base tasks
|
|
413
|
+
|
|
414
|
+
Run also comes with some generic tasks for recurring tasks.
|
|
415
|
+
|
|
416
|
+
### publish
|
|
417
|
+
|
|
418
|
+
You can publish your library with:
|
|
419
|
+
|
|
420
|
+
```sh
|
|
421
|
+
run publish
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
> At the moment, this is only compatible with gems.
|
|
425
|
+
|
|
426
|
+
### rspec
|
|
427
|
+
|
|
428
|
+
You can call directly `run rspec` or abstract the task like this:
|
|
429
|
+
|
|
430
|
+
```rb
|
|
431
|
+
# Run the tests.
|
|
432
|
+
task [:specs, :s, :tests, :t] do |path = "spec/src"|
|
|
433
|
+
run :rspec, path
|
|
434
|
+
end
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
> The `rspec` task will look into `spec/src` by default, so the above task is just explanatory is you want overload default behaviour.
|
|
438
|
+
|
|
439
|
+
## Advanced usage
|
|
440
|
+
|
|
441
|
+
### Specifying Runfile path
|
|
442
|
+
|
|
443
|
+
If you want to load Run from another directory, you have two choices :
|
|
444
|
+
|
|
445
|
+
- change the working directory in your script,
|
|
446
|
+
- or pass the Runfile path with `RUNFILE=/path/to/Runfile.rb run my_task`
|
|
447
|
+
|
|
448
|
+
### Exit codes
|
|
449
|
+
|
|
450
|
+
Run has specific exit codes so you can handle it better in some environments, like in CI or in deployments:
|
|
451
|
+
|
|
452
|
+
- 1: Run has exited abnormally.
|
|
453
|
+
- 2: The required task does not exist.
|
|
454
|
+
- 3: Unhandled Interrupt error coming from a failed shell command.
|
|
455
|
+
- 4: An error has been raised in the Runfile.
|
|
456
|
+
- 5: The Runfile contains a syntax error.
|
|
457
|
+
- 6: Some parameters are invalid.
|
|
458
|
+
- 7: Runfile not found.
|
|
459
|
+
- 8: Unable to load a remote file.
|
|
460
|
+
- 9: The user has answered "No" to an "Are you sure?" question.
|
|
461
|
+
- 10: The required task already exists.
|
|
462
|
+
- 11: The required command is not running.
|
|
463
|
+
- 12: The required task does not exist.
|
|
464
|
+
|
|
465
|
+
## Development
|
|
466
|
+
|
|
467
|
+
For test purposes, it's often needed to install Run from a development branch. For this, you'll need to follow these steps:
|
|
468
|
+
|
|
469
|
+
```sh
|
|
470
|
+
gem install specific_install
|
|
471
|
+
gem specific_install http://github.com/pyrsmk/run.git <branch>
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Do not forget to uninstall other `run_tasks` packages if needed, to avoid conflicts. And if you're under Rbenv, you'll probably need to do `rbenv rehash` too.
|
data/src/bootstrap.rb
CHANGED
|
@@ -7,7 +7,6 @@ require "readline"
|
|
|
7
7
|
require "rubygems"
|
|
8
8
|
require "securerandom"
|
|
9
9
|
require "rb_monkey"
|
|
10
|
-
require "rb_gemspec"
|
|
11
10
|
require "rb_markdown"
|
|
12
11
|
|
|
13
12
|
# Require all files.
|
|
@@ -77,11 +76,6 @@ task :publish do
|
|
|
77
76
|
`rm #{gemspec.name}-#{gemspec.version}.gem`
|
|
78
77
|
end
|
|
79
78
|
|
|
80
|
-
# Display the manual.
|
|
81
|
-
task :man do
|
|
82
|
-
puts Markdown::Engine.new(File.read("#{__dir__}/../CHEATSHEET.md")).to_ansi
|
|
83
|
-
end
|
|
84
|
-
|
|
85
79
|
# Expose helpers.
|
|
86
80
|
Dir.glob(File.join(__dir__, "run", "helper", "*.rb")) do |path|
|
|
87
81
|
filename = File.basename(path, ".rb")
|
|
@@ -117,6 +111,9 @@ rescue Interrupt
|
|
|
117
111
|
rescue SyntaxError => error
|
|
118
112
|
format_error error
|
|
119
113
|
exit 5
|
|
114
|
+
rescue Run::Error::DuplicateParameter, Run::Error::UnexpectedNamedParameters => error
|
|
115
|
+
puts error.message.red
|
|
116
|
+
exit 6
|
|
120
117
|
rescue ArgumentError => error
|
|
121
118
|
format_error error
|
|
122
119
|
exit 6
|
data/src/run/core.rb
CHANGED
|
@@ -4,6 +4,9 @@ module Run
|
|
|
4
4
|
module Core
|
|
5
5
|
RUNFILE_FILENAME = ENV.fetch('RUNFILE', 'Runfile.rb')
|
|
6
6
|
RESERVED_TASK_NAMES = ["help", "version"]
|
|
7
|
+
NAME_PATTERN = /[a-zA-Z_]\w*(?:-\w+)*/
|
|
8
|
+
OPTION_PATTERN = /\A([+-])(#{NAME_PATTERN})\z/
|
|
9
|
+
NAMED_PARAMETER_PATTERN = /\A(#{NAME_PATTERN})=(.*)\z/m
|
|
7
10
|
@@tasks = []
|
|
8
11
|
@@runfile_version = nil
|
|
9
12
|
|
|
@@ -22,7 +25,7 @@ module Run
|
|
|
22
25
|
end
|
|
23
26
|
raise Run::Error::NonExistingRunfile.new if !File.file?(RUNFILE_FILENAME)
|
|
24
27
|
if ARGV.size == 1 && ARGV[0] == "version"
|
|
25
|
-
puts
|
|
28
|
+
puts VERSION
|
|
26
29
|
elsif ARGV.size == 0 || (ARGV.size == 1 && ARGV[0] == "help")
|
|
27
30
|
require File.expand_path(RUNFILE_FILENAME)
|
|
28
31
|
check_runfile_version!
|
|
@@ -73,16 +76,11 @@ module Run
|
|
|
73
76
|
|
|
74
77
|
# @return [void]
|
|
75
78
|
def self.check_runfile_version!
|
|
76
|
-
if (@@runfile_version || 2) !=
|
|
79
|
+
if (@@runfile_version || 2) != VERSION.split(".").first.to_i
|
|
77
80
|
raise Run::Error::RunfileVersionMismatch.new
|
|
78
81
|
end
|
|
79
82
|
end
|
|
80
83
|
|
|
81
|
-
# @return [void]
|
|
82
|
-
def self.display_version
|
|
83
|
-
puts Gemspec::Metadata.new("run_tasks").read.version
|
|
84
|
-
end
|
|
85
|
-
|
|
86
84
|
# @return [void]
|
|
87
85
|
def self.completions
|
|
88
86
|
(@@tasks.flat_map{ |t| t[:names] }.map(&:to_s) + RESERVED_TASK_NAMES).sort.uniq
|
|
@@ -91,19 +89,13 @@ module Run
|
|
|
91
89
|
# @return [void]
|
|
92
90
|
def self.display_help
|
|
93
91
|
puts
|
|
94
|
-
puts " Run v#{
|
|
92
|
+
puts " Run v#{VERSION}".bright_blue
|
|
95
93
|
contents = Run::Core::Help.run(File.read(File.join(Dir.pwd, RUNFILE_FILENAME)))
|
|
96
94
|
if contents.strip.size > 0
|
|
97
|
-
puts
|
|
98
|
-
puts " Project tasks:".magenta
|
|
99
95
|
puts
|
|
100
96
|
puts contents
|
|
101
97
|
end
|
|
102
98
|
puts
|
|
103
|
-
puts " Global tasks:".magenta
|
|
104
|
-
puts
|
|
105
|
-
puts Run::Core::Help.run(File.read(File.join(__dir__, "..", "bootstrap.rb")))
|
|
106
|
-
puts
|
|
107
99
|
end
|
|
108
100
|
|
|
109
101
|
# @return [void]
|
|
@@ -111,32 +103,60 @@ module Run
|
|
|
111
103
|
name = ARGV[0].gsub('-', '_').to_sym # Auto-replace hyphens to underscores.
|
|
112
104
|
raise Run::Error::UnknownTask.new(name) if !task_exist?(name)
|
|
113
105
|
|
|
106
|
+
args, options = parse_arguments(ARGV.drop(1))
|
|
107
|
+
run name, *args, **options
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @param words [Array<String>]
|
|
111
|
+
# @return [Array(Array, Hash)]
|
|
112
|
+
def self.parse_arguments(words)
|
|
114
113
|
args = []
|
|
115
114
|
options = {}
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
if (match =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
elsif (match =
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
args <<
|
|
126
|
-
next
|
|
127
|
-
elsif arg.match?(/^-?\d+\.\d+$/)
|
|
128
|
-
args << Float(arg)
|
|
129
|
-
next
|
|
116
|
+
words.each do |word|
|
|
117
|
+
if (match = word.match(OPTION_PATTERN))
|
|
118
|
+
sign, name = match.captures
|
|
119
|
+
add_option(options, name, sign == "+")
|
|
120
|
+
elsif (match = word.match(NAMED_PARAMETER_PATTERN))
|
|
121
|
+
name, value = match.captures
|
|
122
|
+
add_option(options, name, convert_named_value(value))
|
|
123
|
+
else
|
|
124
|
+
args << convert_argument(word)
|
|
130
125
|
end
|
|
131
|
-
raise
|
|
132
|
-
rescue
|
|
133
|
-
next args << true if arg == "true"
|
|
134
|
-
next args << false if arg == "false"
|
|
135
|
-
next args << arg.to_sym if arg.match?(/^\w+$/)
|
|
136
|
-
args << arg
|
|
137
126
|
end
|
|
138
127
|
|
|
139
|
-
|
|
128
|
+
[args, options]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @param value [String]
|
|
132
|
+
# @return [Integer, Float, Boolean, String]
|
|
133
|
+
def self.convert_argument(value)
|
|
134
|
+
case value
|
|
135
|
+
when /\A-?(?:0|[1-9]\d*)\z/ then Integer(value, 10)
|
|
136
|
+
when /\A-?(?:0|[1-9]\d*)\.\d+\z/ then Float(value)
|
|
137
|
+
when "true" then true
|
|
138
|
+
when "false" then false
|
|
139
|
+
else value
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @param value [String]
|
|
144
|
+
# @return [Object, Array]
|
|
145
|
+
def self.convert_named_value(value)
|
|
146
|
+
return convert_argument(value) if !value.include?(",")
|
|
147
|
+
|
|
148
|
+
value.split(",", -1).map { |item| convert_argument(item) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# @param options [Hash]
|
|
152
|
+
# @param name [String]
|
|
153
|
+
# @param value [Object]
|
|
154
|
+
# @return [void]
|
|
155
|
+
def self.add_option(options, name, value)
|
|
156
|
+
key = name.tr("-", "_").to_sym
|
|
157
|
+
raise Run::Error::DuplicateParameter.new(key) if options.key?(key)
|
|
158
|
+
|
|
159
|
+
options[key] = value
|
|
140
160
|
end
|
|
141
161
|
|
|
142
162
|
# @param name [Symbol]
|
|
@@ -152,6 +172,9 @@ module Run
|
|
|
152
172
|
def self.run_block_task(name, *args, **options)
|
|
153
173
|
task = @@tasks.find{ |item| item[:names].include? name }
|
|
154
174
|
raise Error::NonExistingTask.new(name) if task.nil?
|
|
175
|
+
if options.any? && !task[:task].accepts_keywords?
|
|
176
|
+
raise Error::UnexpectedNamedParameters.new(name, options.keys)
|
|
177
|
+
end
|
|
155
178
|
|
|
156
179
|
task[:task].run(*args, **options)
|
|
157
180
|
end
|
|
@@ -162,11 +185,6 @@ module Run
|
|
|
162
185
|
Run::Task::SystemTask.new(command).run
|
|
163
186
|
end
|
|
164
187
|
|
|
165
|
-
# @return [String]
|
|
166
|
-
def self.version
|
|
167
|
-
Gemspec::Metadata.new("run_tasks").read.version
|
|
168
|
-
end
|
|
169
|
-
|
|
170
188
|
# @return [Boolean]
|
|
171
189
|
def self.quiet?
|
|
172
190
|
!!Thread.current[:quiet]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Run
|
|
4
|
+
module Error
|
|
5
|
+
class UnexpectedNamedParameters < StandardError
|
|
6
|
+
# @param task_name [Symbol]
|
|
7
|
+
# @param names [Array<Symbol>]
|
|
8
|
+
def initialize(task_name, names)
|
|
9
|
+
super "Task '#{task_name}' does not accept named parameters: #{names.join(", ")}"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -7,17 +7,23 @@ module Run
|
|
|
7
7
|
class QuestionHelper
|
|
8
8
|
# @param question [String]
|
|
9
9
|
# @param regex [Regexp, Nil]
|
|
10
|
-
|
|
10
|
+
# @param default [String, Nil]
|
|
11
|
+
def initialize(question, regex = nil, default: nil)
|
|
11
12
|
raise ArgumentError.new("'question' must be a String") if !question.is_a?(String)
|
|
13
|
+
raise ArgumentError.new("'default' must be a String") if !default.nil? && !default.is_a?(String)
|
|
12
14
|
|
|
13
15
|
@question = question
|
|
14
16
|
@regex = regex
|
|
17
|
+
@default = default
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
# @return [any]
|
|
18
21
|
def run
|
|
19
22
|
TTY::Prompt.new.ask(@question) do |config|
|
|
20
23
|
config.required true
|
|
24
|
+
if @default
|
|
25
|
+
config.default @default
|
|
26
|
+
end
|
|
21
27
|
if @regex
|
|
22
28
|
config.validate @regex
|
|
23
29
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-prompt"
|
|
4
|
+
|
|
5
|
+
module Run
|
|
6
|
+
module Helper
|
|
7
|
+
class YesnoHelper
|
|
8
|
+
VALIDATION = /\A(y(es)?|no?)?\z/i
|
|
9
|
+
|
|
10
|
+
# @param question [String]
|
|
11
|
+
# @param default [Boolean, Nil]
|
|
12
|
+
def initialize(question, default: nil)
|
|
13
|
+
raise ArgumentError.new("'question' must be a String") if !question.is_a?(String)
|
|
14
|
+
raise ArgumentError.new("'default' must be a Boolean") if ![nil, true, false].include?(default)
|
|
15
|
+
|
|
16
|
+
@question = question
|
|
17
|
+
@default = default
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Boolean]
|
|
21
|
+
def run
|
|
22
|
+
answer = TTY::Prompt.new.ask("#{@question} #{suffix}") do |config|
|
|
23
|
+
config.required @default.nil?
|
|
24
|
+
config.validate VALIDATION, "Please answer 'y' or 'n'."
|
|
25
|
+
end
|
|
26
|
+
return @default if answer.nil?
|
|
27
|
+
|
|
28
|
+
answer.downcase.start_with?("y")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# @return [String]
|
|
34
|
+
def suffix
|
|
35
|
+
case @default
|
|
36
|
+
when true then "[Yn]"
|
|
37
|
+
when false then "[yN]"
|
|
38
|
+
else "[yn]"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/src/run/task/block_task.rb
CHANGED
|
@@ -8,15 +8,16 @@ module Run
|
|
|
8
8
|
@block = block
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# @return [Boolean]
|
|
12
|
+
def accepts_keywords?
|
|
13
|
+
@block.parameters.any?{ |type, _| [:key, :keyreq, :keyrest].include?(type) }
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
# @param arguments [Array]
|
|
12
17
|
# @param options [Hash]
|
|
13
18
|
# @return [void]
|
|
14
19
|
def run(*arguments, **options)
|
|
15
|
-
|
|
16
|
-
@block.call *arguments
|
|
17
|
-
else
|
|
18
|
-
@block.call *arguments, **options
|
|
19
|
-
end
|
|
20
|
+
@block.call(*arguments, **options)
|
|
20
21
|
end
|
|
21
22
|
end
|
|
22
23
|
end
|
data/src/run/version.rb
ADDED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: run_tasks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aurélien Delogu
|
|
@@ -37,20 +37,6 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: 0.1.0
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: rb_gemspec
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: 0.2.1
|
|
47
|
-
type: :runtime
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: 0.2.1
|
|
54
40
|
- !ruby/object:Gem::Dependency
|
|
55
41
|
name: rb_markdown
|
|
56
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -71,18 +57,20 @@ executables:
|
|
|
71
57
|
extensions: []
|
|
72
58
|
extra_rdoc_files: []
|
|
73
59
|
files:
|
|
74
|
-
-
|
|
60
|
+
- README.md
|
|
75
61
|
- bin/run
|
|
76
62
|
- completions/_run
|
|
77
63
|
- src/bootstrap.rb
|
|
78
64
|
- src/run/core.rb
|
|
79
65
|
- src/run/core/help.rb
|
|
80
66
|
- src/run/error/aborted.rb
|
|
67
|
+
- src/run/error/duplicate_parameter.rb
|
|
81
68
|
- src/run/error/existing_task.rb
|
|
82
69
|
- src/run/error/non_existing_runfile.rb
|
|
83
70
|
- src/run/error/non_existing_task.rb
|
|
84
71
|
- src/run/error/reserved_task_name.rb
|
|
85
72
|
- src/run/error/runfile_version_mismatch.rb
|
|
73
|
+
- src/run/error/unexpected_named_parameters.rb
|
|
86
74
|
- src/run/error/unknown_task.rb
|
|
87
75
|
- src/run/helper/are_you_sure_helper.rb
|
|
88
76
|
- src/run/helper/bind_helper.rb
|
|
@@ -93,8 +81,10 @@ files:
|
|
|
93
81
|
- src/run/helper/pause_helper.rb
|
|
94
82
|
- src/run/helper/question_helper.rb
|
|
95
83
|
- src/run/helper/wait_for_interruption_helper.rb
|
|
84
|
+
- src/run/helper/yesno_helper.rb
|
|
96
85
|
- src/run/task/block_task.rb
|
|
97
86
|
- src/run/task/system_task.rb
|
|
87
|
+
- src/run/version.rb
|
|
98
88
|
homepage: https://github.com/pyrsmk/run
|
|
99
89
|
licenses:
|
|
100
90
|
- MIT
|
data/CHEATSHEET.md
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
# Defining a task
|
|
2
|
-
|
|
3
|
-
```rb
|
|
4
|
-
task :eslint do
|
|
5
|
-
end
|
|
6
|
-
```
|
|
7
|
-
|
|
8
|
-
# Defining a task with aliases
|
|
9
|
-
|
|
10
|
-
```rb
|
|
11
|
-
task [:console, :c] do
|
|
12
|
-
end
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
# Defining a task with parameters
|
|
16
|
-
|
|
17
|
-
```rb
|
|
18
|
-
task :hello do |name, age|
|
|
19
|
-
puts "Hello #{name}, you are #{age}."
|
|
20
|
-
end
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
# Running commands
|
|
24
|
-
|
|
25
|
-
```rb
|
|
26
|
-
task :clear_all do
|
|
27
|
-
run "rm -rf /*"
|
|
28
|
-
end
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
# Running subtasks
|
|
32
|
-
|
|
33
|
-
```rb
|
|
34
|
-
task :linting do
|
|
35
|
-
run :eslint
|
|
36
|
-
run :rubocop
|
|
37
|
-
end
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
# Running a task/command in quiet mode
|
|
41
|
-
|
|
42
|
-
```rb
|
|
43
|
-
task :clear_cache do
|
|
44
|
-
run "rm -rf cache/*", quiet: true
|
|
45
|
-
end
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
# Using named booleans
|
|
49
|
-
|
|
50
|
-
```sh
|
|
51
|
-
run server +tunnel
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
```rb
|
|
55
|
-
task :server do |tunnel: false|
|
|
56
|
-
run "http_server #{tunnel ? "--tunnel" : ""}"
|
|
57
|
-
end
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
# Helpers
|
|
61
|
-
|
|
62
|
-
## are_you_sure
|
|
63
|
-
|
|
64
|
-
```rb
|
|
65
|
-
task :dangerous_task do
|
|
66
|
-
are_you_sure "This task will delete your hard drive. Are you sure about that?"
|
|
67
|
-
# Some dangerous actions.
|
|
68
|
-
end
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## bind
|
|
72
|
-
|
|
73
|
-
```rb
|
|
74
|
-
task :rails do
|
|
75
|
-
run "bundle exec unicorn -c config/unicorn.rb -p 3000"
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
task :vite do
|
|
79
|
-
run "bin/vite dev"
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
task :dev do
|
|
83
|
-
# Bind `rails` and `vite` tasks together, if one closes, the other terminates too.
|
|
84
|
-
bind :rails, :vite
|
|
85
|
-
end
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
```rb
|
|
89
|
-
task :dev do
|
|
90
|
-
# Plug STDIN to the `rails` task.
|
|
91
|
-
bind :rails, :vite, stdin: :rails
|
|
92
|
-
end
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## catch_interruption
|
|
96
|
-
|
|
97
|
-
```rb
|
|
98
|
-
task :server do
|
|
99
|
-
catch_interruption("docker run -d -p 8080:80 web_server") do
|
|
100
|
-
run :clear_cache
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## exists
|
|
106
|
-
|
|
107
|
-
```rb
|
|
108
|
-
task :man do
|
|
109
|
-
command = exists("bat") ? "bat" : "cat"
|
|
110
|
-
run "#{command} #{__dir__}/README.md"
|
|
111
|
-
end
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
## expand
|
|
115
|
-
|
|
116
|
-
```rb
|
|
117
|
-
task :scan do |glob|
|
|
118
|
-
expand(glob).each do |file_path|
|
|
119
|
-
puts file_path
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
## menu
|
|
125
|
-
|
|
126
|
-
```rb
|
|
127
|
-
task :deploy_aws do
|
|
128
|
-
region = menu "Location?", ["us‑east‑2", "us-west-1", "eu-west-1"]
|
|
129
|
-
end
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
```rb
|
|
133
|
-
task :deploy_aws do
|
|
134
|
-
region = menu "Location?", {
|
|
135
|
-
"US East (Ohio)" => "us‑east‑2",
|
|
136
|
-
"US West (N. California)" => "us-west-1",
|
|
137
|
-
"Europe (Ireland)" => "eu-west-1",
|
|
138
|
-
}
|
|
139
|
-
end
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## pause
|
|
143
|
-
|
|
144
|
-
```rb
|
|
145
|
-
task :deploy do
|
|
146
|
-
puts "Press enter to continue.".yellow
|
|
147
|
-
pause
|
|
148
|
-
run :deploy_production
|
|
149
|
-
end
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## question
|
|
153
|
-
|
|
154
|
-
```rb
|
|
155
|
-
task :survey do
|
|
156
|
-
comment = question "Did your meal was good?"
|
|
157
|
-
end
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
```rb
|
|
161
|
-
task :age do
|
|
162
|
-
age = question "What is your age?", /^\d+$/
|
|
163
|
-
end
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
## wait_for_interruption
|
|
167
|
-
|
|
168
|
-
```rb
|
|
169
|
-
task :server do
|
|
170
|
-
Thread.new do
|
|
171
|
-
run :rails
|
|
172
|
-
end
|
|
173
|
-
Thread.new do
|
|
174
|
-
run :vite
|
|
175
|
-
end
|
|
176
|
-
wait_for_interruption do
|
|
177
|
-
run :kill_rails
|
|
178
|
-
run :kill_vite
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
# Colorization
|
|
184
|
-
|
|
185
|
-
```rb
|
|
186
|
-
puts "hello".bold
|
|
187
|
-
puts "hello".dim
|
|
188
|
-
puts "hello".italic
|
|
189
|
-
puts "hello".underline
|
|
190
|
-
puts "hello".inverse
|
|
191
|
-
puts "hello".strikethrough
|
|
192
|
-
puts "hello".black
|
|
193
|
-
puts "hello".red
|
|
194
|
-
puts "hello".green
|
|
195
|
-
puts "hello".yellow
|
|
196
|
-
puts "hello".blue
|
|
197
|
-
puts "hello".magenta
|
|
198
|
-
puts "hello".cyan
|
|
199
|
-
puts "hello".white
|
|
200
|
-
puts "hello".bright_black
|
|
201
|
-
puts "hello".bright_red
|
|
202
|
-
puts "hello".bright_green
|
|
203
|
-
puts "hello".bright_yellow
|
|
204
|
-
puts "hello".bright_blue
|
|
205
|
-
puts "hello".bright_magenta
|
|
206
|
-
puts "hello".bright_cyan
|
|
207
|
-
puts "hello".bright_white
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
```rb
|
|
211
|
-
puts "hello".green.bold.italic
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
# Advanced usage
|
|
215
|
-
|
|
216
|
-
```sh
|
|
217
|
-
RUNFILE=/path/to/Runfile.rb run my_task
|
|
218
|
-
```
|