run_kit 0.1.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 +7 -0
- data/.github/workflows/ci.yml +27 -0
- data/.gitignore +3 -0
- data/.justfile +60 -0
- data/.mise.toml +11 -0
- data/.rubocop.yml +61 -0
- data/AGENTS.md +29 -0
- data/Gemfile +13 -0
- data/README.md +107 -0
- data/Rakefile +14 -0
- data/demo.rb +11 -0
- data/lib/run_kit/core_ext.rb +116 -0
- data/lib/run_kit/options/color.rb +30 -0
- data/lib/run_kit/options/config.rb +123 -0
- data/lib/run_kit/options/flag.rb +135 -0
- data/lib/run_kit/options/help.rb +85 -0
- data/lib/run_kit/options/main.rb +82 -0
- data/lib/run_kit/options/parser.rb +142 -0
- data/lib/run_kit/options/positional.rb +23 -0
- data/lib/run_kit/options.rb +7 -0
- data/lib/run_kit/shell.rb +307 -0
- data/lib/run_kit/term.rb +201 -0
- data/lib/run_kit.rb +22 -0
- data/prek.toml +46 -0
- data/run_kit.gemspec +23 -0
- metadata +93 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f4f28d8c9977c242536eb737cdde4f7723df1cdaf7b094efcf724562c2fd0f1e
|
|
4
|
+
data.tar.gz: fe8f3c3a1479f61483137e0ae96a72b2adc0e78b0ccb611130c0d13afbcdd627
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 176389146afcdc0646319c3911024ff6903dcfb4836ac54f86e6f0d7ca9557c71b3a00e3d4295f7ea53c6e5751dcc8f55eb45713da71e6d32083fcbd9feb7b9e
|
|
7
|
+
data.tar.gz: 784fc1c1c4702adca3504ca13ea1c713172df14791adfb355755dd97ea9e89d570f8f664d28cdd7b8bfcdf08a18d9c520b43dc2e6219a3a9df5285d8cea0877f
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
paths-ignore: ["**/*.md"]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions: { contents: read } # (zizmor)
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ${{ github.workflow }}-${{ github.ref_name }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
strategy:
|
|
19
|
+
fail-fast: false
|
|
20
|
+
matrix: { platform: [ubuntu-latest, macos-latest] }
|
|
21
|
+
runs-on: ${{ matrix.platform }}
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 (zizmor)
|
|
24
|
+
with: { persist-credentials: false } # (zizmor)
|
|
25
|
+
- uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4 (zizmor)
|
|
26
|
+
with: { experimental: true }
|
|
27
|
+
- run: just check
|
data/.gitignore
ADDED
data/.justfile
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
default: test
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# check and friends
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
check:
|
|
8
|
+
if [ "{{os()}}" != "windows" ]; then just lint ; fi
|
|
9
|
+
just test
|
|
10
|
+
just banner "✓ check ✓"
|
|
11
|
+
|
|
12
|
+
fmt: (lint "-a")
|
|
13
|
+
|
|
14
|
+
outdated:
|
|
15
|
+
just banner Here are the easy ones:
|
|
16
|
+
bundle outdated --filter-minor || true
|
|
17
|
+
just banner The full list:
|
|
18
|
+
bundle outdated || true
|
|
19
|
+
|
|
20
|
+
pry:
|
|
21
|
+
bundle exec pry -I lib -r run_kit.rb
|
|
22
|
+
|
|
23
|
+
lint *ARGS:
|
|
24
|
+
just banner lint...
|
|
25
|
+
rubocop {{ARGS}}
|
|
26
|
+
|
|
27
|
+
test *ARGS:
|
|
28
|
+
just banner rake test {{ARGS}}
|
|
29
|
+
rake test {{ARGS}}
|
|
30
|
+
|
|
31
|
+
# run tests repeatedly
|
|
32
|
+
test-watch *ARGS:
|
|
33
|
+
watchexec --stop-timeout=0 --clear clear just test "{{ARGS}}"
|
|
34
|
+
|
|
35
|
+
#
|
|
36
|
+
# publish
|
|
37
|
+
#
|
|
38
|
+
|
|
39
|
+
gem-local:
|
|
40
|
+
just banner rake install:local...
|
|
41
|
+
rake install:local
|
|
42
|
+
|
|
43
|
+
publish: check
|
|
44
|
+
just banner rake release...
|
|
45
|
+
rake release
|
|
46
|
+
just banner "✓ publish ✓"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
# banner
|
|
51
|
+
#
|
|
52
|
+
|
|
53
|
+
set quiet
|
|
54
|
+
|
|
55
|
+
banner +ARGS: (_banner '\e[48;2;064;160;043m' ARGS)
|
|
56
|
+
warning +ARGS: (_banner '\e[48;2;251;100;011m' ARGS)
|
|
57
|
+
fatal +ARGS: (_banner '\e[48;2;210;015;057m' ARGS)
|
|
58
|
+
exit 1
|
|
59
|
+
_banner BG +ARGS:
|
|
60
|
+
printf '\e[38;5;231m{{BOLD+BG}}[%s] %-72s {{NORMAL}}\n' "$(date +%H:%M:%S)" "{{ARGS}}"
|
data/.mise.toml
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require:
|
|
2
|
+
- standard
|
|
3
|
+
|
|
4
|
+
inherit_gem:
|
|
5
|
+
standard: config/ruby-3.3.yml
|
|
6
|
+
standard-custom: config/base.yml
|
|
7
|
+
standard-performance: config/base.yml
|
|
8
|
+
|
|
9
|
+
plugins:
|
|
10
|
+
- standard-custom
|
|
11
|
+
- standard-performance
|
|
12
|
+
- rubocop-performance
|
|
13
|
+
|
|
14
|
+
AllCops:
|
|
15
|
+
# We completely override Exclude rather than using `inherit_mode: merge` because
|
|
16
|
+
# we want to undo the exclusion of `bin/*` from rubocop-rails. So we must recreate the
|
|
17
|
+
# full exclusion list from all of our parent configs.
|
|
18
|
+
Exclude:
|
|
19
|
+
- .git/**/*
|
|
20
|
+
- tmp/**/*
|
|
21
|
+
- vendor/**/*
|
|
22
|
+
NewCops: enable
|
|
23
|
+
SuggestExtensions: false
|
|
24
|
+
|
|
25
|
+
#
|
|
26
|
+
# fight with standardrb!
|
|
27
|
+
#
|
|
28
|
+
|
|
29
|
+
Bundler/OrderedGems: { Enabled: true } # sort Gemfile
|
|
30
|
+
Lint/MissingSuper: { Enabled: true } # must call `super`
|
|
31
|
+
Lint/NonLocalExitFromIterator: { Enabled: false } # allow return in iterators
|
|
32
|
+
Lint/RedundantDirGlobSort: { Enabled: true } # glob already sorted
|
|
33
|
+
Naming/FileName: { Enabled: true } # duh
|
|
34
|
+
Naming/MemoizedInstanceVariableName: { Enabled: true } # clean memo ivars
|
|
35
|
+
Naming/MethodName: { Enabled: true } # keep method names conventional
|
|
36
|
+
Performance/MapCompact: { Enabled: true } # filter_map-ish style
|
|
37
|
+
Performance/RegexpMatch: { Enabled: false } # local style is fine
|
|
38
|
+
Performance/SelectMap: { Enabled: true } # filter_map-ish style
|
|
39
|
+
Style/BlockDelimiters: { Enabled: true } # do/end vs braces
|
|
40
|
+
Style/ClassAndModuleChildren: { Enabled: true, EnforcedStyle: nested } # explicit nesting
|
|
41
|
+
Style/ClassMethodsDefinitions: { Enabled: true } # avoid "class << self"
|
|
42
|
+
Style/CollectionCompact: { Enabled: true } # compact over reject
|
|
43
|
+
Style/CollectionMethods: { Enabled: true } # modern collection helpers
|
|
44
|
+
Style/FrozenStringLiteralComment: { Enabled: true, EnforcedStyle: never } # nope
|
|
45
|
+
Style/HashEachMethods: { Enabled: true } # each_key/value helpers
|
|
46
|
+
Style/HashSyntax: { EnforcedShorthandSyntax: always } # modern hash syntax
|
|
47
|
+
Style/HashTransformKeys: { Enabled: true } # transform_keys
|
|
48
|
+
Style/HashTransformValues: { Enabled: true } # transform_values
|
|
49
|
+
Style/MinMax: { Enabled: true } # minmax when fitting
|
|
50
|
+
Style/NestedTernaryOperator: { Enabled: false } # we do this sometimes
|
|
51
|
+
Style/NonNilCheck: { Enabled: false } # allow x != nil for clarity
|
|
52
|
+
Style/PreferredHashMethods: { Enabled: true } # modern hash helpers
|
|
53
|
+
Style/RedundantAssignment: { Enabled: false } # allows s=xxx;s=yyy;s
|
|
54
|
+
Style/RedundantInitialize: { Enabled: true } # remove pointless initialize
|
|
55
|
+
Style/RedundantReturn: { Enabled: false } # sometimes we do this while working on something
|
|
56
|
+
Style/SelectByRegexp: { Enabled: true } # grep-like selection
|
|
57
|
+
Style/StringConcatenation: { Enabled: true } # interpolation
|
|
58
|
+
Style/SymbolArray: { Enabled: true } # %i for symbol arrays
|
|
59
|
+
Style/TrailingCommaInArrayLiteral: { EnforcedStyleForMultiline: consistent_comma } # commas!!
|
|
60
|
+
Style/TrailingCommaInHashLiteral: { EnforcedStyleForMultiline: consistent_comma } # commas!!
|
|
61
|
+
Style/WordArray: { Enabled: true } # %w for word arrays
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Style
|
|
2
|
+
|
|
3
|
+
1. Keep code/docs/output simple and concise.
|
|
4
|
+
2. Comment major methods and tricky code.
|
|
5
|
+
3. Put only trivial one-liners under `# one-liners`.
|
|
6
|
+
4. Prefer readers over ivars except for assignment or type hints.
|
|
7
|
+
5. Prefer `each`/ranges over `while`; counters are `idx`.
|
|
8
|
+
6. Treat String as immutable: use `StringIO buf`, arrays + `join`, substitution, or interpolation. Never add frozen-string comments.
|
|
9
|
+
7. Prefer truthy checks over `nil?` when `false` is not distinct.
|
|
10
|
+
8. Always nest module/class declarations; never declare them with constant paths such as `module RunKit::Options`.
|
|
11
|
+
9. Do not change `.rubocop.yml` without asking.
|
|
12
|
+
10. Keep PR/commit text succinct: one or two sentences tops. PR titles should be only a few words.
|
|
13
|
+
|
|
14
|
+
# Tests
|
|
15
|
+
|
|
16
|
+
1. Run `just test` after every change.
|
|
17
|
+
2. Mirror each source file with one `*_test.rb`; generally use one test method per public method.
|
|
18
|
+
3. Namespace tests to mirror production modules.
|
|
19
|
+
4. Use `new.tap { _1... }` to set up important objects.
|
|
20
|
+
5. Assert observable behavior or state, not incidental mutator return values.
|
|
21
|
+
6. Prefer table cases, shared helpers, and custom assertions.
|
|
22
|
+
7. Derive table messages from inputs; hand-name only opaque proc cases.
|
|
23
|
+
8. Inline one-use table literals directly into `each`; do not name them.
|
|
24
|
+
9. Give complicated classes a rich `test_basic` covering their main behavior end to end.
|
|
25
|
+
10. Prefer a few end-to-end behavior tests over exhaustive trivial method coverage.
|
|
26
|
+
11. Fixtures such as `test/smoke.rb` are exempt from test style conventions.
|
|
27
|
+
12. Use `just test-regen` only for intentional snapshot updates; review generated `*.expected` files.
|
|
28
|
+
13. Stdout/stderr are fine when relevant. Prefer `assert_equal`/`assert_raises`; never use `refute`.
|
|
29
|
+
14. Usually assert only the exception class; check messages only when wording is user-facing behavior.
|
data/Gemfile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
source "https://rubygems.org"
|
|
2
|
+
gemspec
|
|
3
|
+
|
|
4
|
+
group :development, :test do
|
|
5
|
+
gem "amazing_print"
|
|
6
|
+
gem "minitest", "~> 6.0"
|
|
7
|
+
gem "minitest-hooks"
|
|
8
|
+
gem "mocha"
|
|
9
|
+
gem "pry", require: false
|
|
10
|
+
gem "rake", require: false
|
|
11
|
+
gem "ruby-lsp", require: false
|
|
12
|
+
gem "standard", "~> 1.56", require: false, platform: :mri
|
|
13
|
+
end
|
data/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# RunKit [](https://github.com/gurgeous/run_kit/actions/workflows/ci.yml)
|
|
2
|
+
|
|
3
|
+
RunKit is a small toolkit for cli. It provides option parsing, shell and file helpers, term colors, and a handful of core extensions.
|
|
4
|
+
|
|
5
|
+
### Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# install gem
|
|
9
|
+
$ gem install run_key
|
|
10
|
+
|
|
11
|
+
# or add to your Gemfile
|
|
12
|
+
gem "run_key"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## RunKit::Options
|
|
16
|
+
|
|
17
|
+
Featureful cli arg parser based on [spinel-slap](https://github.com/gurgeous/spinel-slap), which combines the best bits from [slop rb](https://github.com/leejarvis/slop) and [clap rs](https://github.com/clap-rs/clap).
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
options = RunKit.parse do |o|
|
|
21
|
+
o.int "-n", "--count <n>", "How many times to run", default: 1
|
|
22
|
+
o.str "--mode <mode>", "Run quickly, or not", choices: %w[fast slow]
|
|
23
|
+
o.positional "<url>", "url to fetch"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# #<data count=1, mode="slow", url="https://demo.org", _args=[]>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Automatic `--help` for the above. Uses color and wraps to terminal:
|
|
30
|
+
|
|
31
|
+
<img width="379" height="108" alt="image" src="https://github.com/user-attachments/assets/3d11c8ca-6b57-4dec-9c11-28bf23162ded" />
|
|
32
|
+
|
|
33
|
+
## RunKit::Shell
|
|
34
|
+
|
|
35
|
+
`RunKit::Shell` is a mixin with many helpers for bin scripts:
|
|
36
|
+
|
|
37
|
+
| Function | Description |
|
|
38
|
+
| ------------------------------ | --------------------------------------------------- |
|
|
39
|
+
| `csv_read` / `csv_write` | Read/write CSV (add .gz for gzip) |
|
|
40
|
+
| `file_read` / `file_write` | Atomic read/write files (add .gz for gzip) |
|
|
41
|
+
| `json_read` / `json_write` | Atomic read/write json (add .gz for gzip) |
|
|
42
|
+
| `jsonl_read / `jsonl_write` | Atomic read/write jsonl (add .gz for gzip) |
|
|
43
|
+
| |
|
|
44
|
+
| `csv_write_stdout` | Write CSV to stdout |
|
|
45
|
+
| `gunzip` / `gzip` | (De)compress a string |
|
|
46
|
+
| `atomic_write` | Atomically replace a file |
|
|
47
|
+
| `cache_fetch` | Populate/fetch from file cache w/ block |
|
|
48
|
+
| `cp_metadata` | Copy file metadata from src to dst |
|
|
49
|
+
| `glob` | Find sorted/uniq paths |
|
|
50
|
+
| `lines_in_file` | Count lines in file using `wc` |
|
|
51
|
+
| |
|
|
52
|
+
| `shell` | Run command and return status |
|
|
53
|
+
| `shell!` | Run command or raise |
|
|
54
|
+
| `shell_transform!` | Atomically transform from src => dst via cmd |
|
|
55
|
+
| `installed?` | Check command availability |
|
|
56
|
+
| `kill_process` | Kill process if present |
|
|
57
|
+
| |
|
|
58
|
+
| `banner` / `warning` / `fatal` | Pretty banner in green, orange or red (fatal exits) |
|
|
59
|
+
| `program_name` | Return executable name |
|
|
60
|
+
| `prompt?` | Ask use for confirmation |
|
|
61
|
+
| `md5` / `sha256` | Hash strings |
|
|
62
|
+
|
|
63
|
+
### RunKit CoreExt
|
|
64
|
+
|
|
65
|
+
RunKit also installs a small set of core extensions to assist with bin scripts.
|
|
66
|
+
|
|
67
|
+
| Function | Description |
|
|
68
|
+
| -------------------------------------------- | ------------------------- |
|
|
69
|
+
| `Enumerable#mean` | Mean |
|
|
70
|
+
| `Enumerable#stddev` | Standard deviation |
|
|
71
|
+
| `Enumerable#variance` | Statistical variance |
|
|
72
|
+
| `Enumerable#tally_sorted` | Tally and sort values |
|
|
73
|
+
| |
|
|
74
|
+
| `Enumerator#with_progressbar` | Iterate with progress bar |
|
|
75
|
+
| |
|
|
76
|
+
| `Hash#hash_sort_by` | Sort hash |
|
|
77
|
+
| `Hash#sort_by_key` | Sort hash, by keys |
|
|
78
|
+
| `Hash#sort_by_value` | Sort hash, by values |
|
|
79
|
+
| `Hash#to_struct` | Convert hash to Struct |
|
|
80
|
+
| |
|
|
81
|
+
| `Numeric#sqrt` | Square root |
|
|
82
|
+
| |
|
|
83
|
+
| `Pathname#abs` | Expand to absolute path |
|
|
84
|
+
| `Pathname#cd` | cd |
|
|
85
|
+
| `Pathname#chmod` | chmod |
|
|
86
|
+
| `Pathname#chmod_r` | chmod recursively |
|
|
87
|
+
| `Pathname#chown` | chown |
|
|
88
|
+
| `Pathname#chown_r` | chown recursively |
|
|
89
|
+
| `Pathname#cp` | cp -rf |
|
|
90
|
+
| `Pathname#ln` | ln |
|
|
91
|
+
| `Pathname#mkdir` | mkdir -p |
|
|
92
|
+
| `Pathname#mv` | mv |
|
|
93
|
+
| `Pathname#rm` | rm |
|
|
94
|
+
| `Pathname#rm_rf` | rm -rf |
|
|
95
|
+
| `Pathname#touch` | touch |
|
|
96
|
+
| `Pathname#escape` | Escape path for shell |
|
|
97
|
+
| |
|
|
98
|
+
| `String#blue` (also green/organge/red/muted) | White text on color bg |
|
|
99
|
+
| `String#without_curly` | Replace curly quotes |
|
|
100
|
+
|
|
101
|
+
Note: There has been some effort to get the Pathname helpers into Ruby itself, without much success.
|
|
102
|
+
|
|
103
|
+
### Changelog
|
|
104
|
+
|
|
105
|
+
#### 0.1.0 (unreleased)
|
|
106
|
+
|
|
107
|
+
- first
|
data/Rakefile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
require "rake/testtask"
|
|
3
|
+
require "bundler"
|
|
4
|
+
Bundler::GemHelper.install_tasks
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new do
|
|
7
|
+
# let the user pass *.rb on the command line
|
|
8
|
+
files = ARGV.grep(%r{^test/.*\.rb$}).sort
|
|
9
|
+
_1.test_files = files.empty? ? FileList["test/**/*_test.rb"] : files
|
|
10
|
+
# autoload test_helper
|
|
11
|
+
_1.ruby_opts = ["-w", "-r#{File.realpath("test/test_helper.rb")}"]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
task default: :test
|
data/demo.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require_relative "lib/run_kit"
|
|
4
|
+
|
|
5
|
+
options = RunKit.parse do |o|
|
|
6
|
+
o.int "-n", "--count <n>", "How many times to run", default: 1
|
|
7
|
+
o.str "--mode <mode>", "Run quickly, or not", choices: %w[fast slow]
|
|
8
|
+
o.positional "<url>", "url to fetch"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
p options
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module RunKit
|
|
2
|
+
module CoreExt
|
|
3
|
+
module Enumerable
|
|
4
|
+
def tally_sorted
|
|
5
|
+
keyfn = case find { _1 }
|
|
6
|
+
when ::String then -> { _1&.downcase || "\uffff" }
|
|
7
|
+
when ::Numeric then -> { _1 || Float::MAX }
|
|
8
|
+
when ::Array then -> { _1 || [] }
|
|
9
|
+
when TrueClass, FalseClass then -> { (_1 != nil) ? (_1 ? 0 : 1) : Float::MAX }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
unsorted = tally
|
|
13
|
+
begin
|
|
14
|
+
unsorted.sort_by { [-_2, keyfn&.call(_1)] }
|
|
15
|
+
rescue ArgumentError, NoMethodError
|
|
16
|
+
keyfn = nil # mixed types, don't sort on key
|
|
17
|
+
retry
|
|
18
|
+
end.to_h
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def variance(sample: true)
|
|
22
|
+
n = length
|
|
23
|
+
return if (sample && n < 2) || (!sample && n.zero?)
|
|
24
|
+
mu = mean
|
|
25
|
+
denom = sample ? (n - 1) : n
|
|
26
|
+
sum(0.0) { (_1 - mu)**2 } / denom
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# one-liners
|
|
30
|
+
def mean = empty? ? nil : sum(0.0) / length
|
|
31
|
+
def stddev(sample: true) = variance(sample:)&.sqrt
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# each.with_progresbar
|
|
35
|
+
module Enumerator
|
|
36
|
+
def with_progressbar(options = {}, &block)
|
|
37
|
+
defaults = {
|
|
38
|
+
format: "%j%% %B #{RunKit::Term.paint_ansi("%c/%u %e", RunKit::Term.ansi256_fg(242))}",
|
|
39
|
+
progress_mark: RunKit::Term.paint_ansi("━", RunKit::Term.ansi256_fg(46)),
|
|
40
|
+
remainder_mark: RunKit::Term.paint_ansi("━", RunKit::Term.ansi256_fg(237)),
|
|
41
|
+
output: $stdout.isatty ? $stdout : $stderr,
|
|
42
|
+
total: size,
|
|
43
|
+
length: 72,
|
|
44
|
+
}
|
|
45
|
+
options = defaults.merge(options)
|
|
46
|
+
|
|
47
|
+
return enum_for(__method__) if !block
|
|
48
|
+
|
|
49
|
+
if !options[:hide]
|
|
50
|
+
bar = ProgressBar.create(options)
|
|
51
|
+
end
|
|
52
|
+
RunKit::Term.with_hidden_cursor(options[:output]) do
|
|
53
|
+
each do
|
|
54
|
+
bar&.increment
|
|
55
|
+
yield(_1)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
module Hash
|
|
62
|
+
def hash_sort_by(&) = sort_by(&).to_h
|
|
63
|
+
def sort_by_key = hash_sort_by { _1 }
|
|
64
|
+
def sort_by_value = hash_sort_by { _2 }
|
|
65
|
+
def to_struct = Struct.new(*keys.map(&:to_sym)).new(*values)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
module Numeric
|
|
69
|
+
def sqrt = Math.sqrt(self)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
module Pathname
|
|
73
|
+
# one-liners
|
|
74
|
+
def abs = expand_path
|
|
75
|
+
def cd(...) = FileUtils.cd(self, ...)
|
|
76
|
+
def chmod_r(mode, **kwargs) = FileUtils.chmod_R(mode, self, **kwargs)
|
|
77
|
+
def chown_r(user, group, **kwargs) = FileUtils.chown_R(user, group, self, **kwargs)
|
|
78
|
+
def cp(dest, **kwargs) = FileUtils.cp_r(self, dest, **kwargs.merge(preserve: true))
|
|
79
|
+
def escape = Shellwords.escape(to_s)
|
|
80
|
+
def ln(...) = FileUtils.ln_sf(self, ...)
|
|
81
|
+
def mv(...) = FileUtils.mv(self, ...)
|
|
82
|
+
def rm(...) = FileUtils.rm_f(self, ...)
|
|
83
|
+
def rm_rf(...) = FileUtils.rm_rf(self, ...)
|
|
84
|
+
def touch(...) = FileUtils.touch(self, ...)
|
|
85
|
+
|
|
86
|
+
# Replacements for Pathname built-ins removed below.
|
|
87
|
+
def self.included(base)
|
|
88
|
+
base.remove_method :mkdir, :chown, :chmod
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def chmod(mode, **kwargs) = FileUtils.chmod(mode, self, **kwargs)
|
|
92
|
+
def chown(user, group, **kwargs) = FileUtils.chown(user, group, self, **kwargs)
|
|
93
|
+
def mkdir(...) = FileUtils.mkdir_p(self, ...)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
module String
|
|
97
|
+
# remove smart quotes
|
|
98
|
+
def without_curly = tr("“”‘’", "\"\"''")
|
|
99
|
+
|
|
100
|
+
# don't judge
|
|
101
|
+
def blue = RunKit::Term.paint_banner(self, :blue)
|
|
102
|
+
def green = RunKit::Term.paint_banner(self, :green)
|
|
103
|
+
def orange = RunKit::Term.paint_banner(self, :peach)
|
|
104
|
+
def red = RunKit::Term.paint_banner(self, :red)
|
|
105
|
+
def muted = RunKit::Term.paint_muted(self)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# mix them in
|
|
111
|
+
::Enumerable.include RunKit::CoreExt::Enumerable
|
|
112
|
+
::Enumerator.include RunKit::CoreExt::Enumerator
|
|
113
|
+
::Hash.include RunKit::CoreExt::Hash
|
|
114
|
+
::Numeric.include RunKit::CoreExt::Numeric
|
|
115
|
+
::Pathname.include RunKit::CoreExt::Pathname
|
|
116
|
+
::String.include RunKit::CoreExt::String
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#
|
|
2
|
+
# A simple color toggle that can format text. If unset infer from stdout.tty.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
module RunKit
|
|
6
|
+
module Options
|
|
7
|
+
class Color
|
|
8
|
+
attr_reader :enabled
|
|
9
|
+
alias_method :enabled?, :enabled
|
|
10
|
+
|
|
11
|
+
def initialize(enabled)
|
|
12
|
+
@enabled = enabled.nil? ? $stdout.tty? : enabled
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# one-liners
|
|
16
|
+
def blue(str) = paint8(str, :blue)
|
|
17
|
+
def cyan(str) = paint8(str, :cyan)
|
|
18
|
+
def green(str) = paint8(str, :green)
|
|
19
|
+
def magenta(str) = paint8(str, :magenta)
|
|
20
|
+
def red(str) = paint8(str, :red)
|
|
21
|
+
def yellow(str) = paint8(str, :yellow)
|
|
22
|
+
|
|
23
|
+
protected
|
|
24
|
+
|
|
25
|
+
def paint8(str, ansi8)
|
|
26
|
+
enabled? ? Term.paint8(str, ansi8) : str
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Config users setup with `RunKit::Options.parse`. Records flags, positionals, help text,
|
|
3
|
+
# etc.
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
module RunKit
|
|
7
|
+
module Options
|
|
8
|
+
class Config
|
|
9
|
+
attr_accessor :app_name, :banner, :color, :exit, :help, :naked, :version
|
|
10
|
+
attr_reader :flags, :help_flag, :lookup, :positionals, :separators, :version_flag
|
|
11
|
+
alias_method :naked?, :naked
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@app_name = File.basename($PROGRAM_NAME)
|
|
15
|
+
@naked = true
|
|
16
|
+
@lookup = {}
|
|
17
|
+
@flags, @positionals, @separators = [], [], []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Add a positional param declared as `<url>`.
|
|
21
|
+
def pos(meta, help = "")
|
|
22
|
+
Positional.new(meta:, help:).tap do
|
|
23
|
+
raise ArgumentError, "duplicate positional #{_1.key}" if key?(_1.key)
|
|
24
|
+
positionals << _1
|
|
25
|
+
lookup[_1.key] = _1
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Add separator text at the current point in generated help.
|
|
30
|
+
def sep(text = "")
|
|
31
|
+
[flags.length, text].tap do
|
|
32
|
+
separators << _1
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#
|
|
37
|
+
# flags
|
|
38
|
+
#
|
|
39
|
+
|
|
40
|
+
def bool(*opts, default: nil, required: false)
|
|
41
|
+
add_flag(Flag.new(:bool, opts, default:, required:))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def float(*opts, default: nil, required: false, choices: nil)
|
|
45
|
+
add_flag(Flag.new(:float, opts, default:, required:, choices:))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def int(*opts, default: nil, required: false, choices: nil)
|
|
49
|
+
add_flag(Flag.new(:int, opts, default:, required:, choices:))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def path(*opts, default: nil, required: false, choices: nil)
|
|
53
|
+
add_flag(Flag.new(:path, opts, default:, required:, choices:))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def str(*opts, default: nil, required: false, choices: nil)
|
|
57
|
+
add_flag(Flag.new(:str, opts, default:, required:, choices:))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def sym(*opts, default: nil, required: false, choices: nil)
|
|
61
|
+
add_flag(Flag.new(:sym, opts, default:, required:, choices:))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# long-form aliases
|
|
65
|
+
alias_method :boolean, :bool
|
|
66
|
+
alias_method :integer, :int
|
|
67
|
+
alias_method :pathname, :path
|
|
68
|
+
alias_method :positional, :pos
|
|
69
|
+
alias_method :separator, :sep
|
|
70
|
+
alias_method :string, :str
|
|
71
|
+
alias_method :symbol, :sym
|
|
72
|
+
|
|
73
|
+
# handy helpers
|
|
74
|
+
def defaults
|
|
75
|
+
flags.filter_map do
|
|
76
|
+
next if _1.required || _1 == help_flag || _1 == version_flag
|
|
77
|
+
[_1.key, _1.default]
|
|
78
|
+
end.to_h
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# one-liners
|
|
82
|
+
def flag(switch) = lookup[switch]
|
|
83
|
+
def flag?(switch) = lookup.key?(switch)
|
|
84
|
+
def key?(key) = lookup.key?(key)
|
|
85
|
+
def required = flags.select(&:required?)
|
|
86
|
+
|
|
87
|
+
# Complete one-time setup after the caller has declared overrides.
|
|
88
|
+
def prepare!
|
|
89
|
+
return if @prepared
|
|
90
|
+
@prepared = true
|
|
91
|
+
@exit ||= lambda { |status| Kernel.exit(status) }
|
|
92
|
+
@help_flag = add_builtin(["-h", "--help"], "Show this message")
|
|
93
|
+
@version_flag = add_builtin(["-v", "--version"], "Show version") if version
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
protected
|
|
97
|
+
|
|
98
|
+
# Add help/version flags, but only for switches the user did not override.
|
|
99
|
+
def add_builtin(switches, help_text)
|
|
100
|
+
unused = switches.select { !flag?(_1) }
|
|
101
|
+
return if unused.empty?
|
|
102
|
+
add_flag(Flag.new(:bool, unused + [help_text]))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def add_flag(flag)
|
|
106
|
+
# dup check
|
|
107
|
+
raise ArgumentError, "reserved flag key: _args" if flag.key == :_args
|
|
108
|
+
raise ArgumentError, "dup flag key: #{flag.key}" if key?(flag.key)
|
|
109
|
+
flag.switches.each do
|
|
110
|
+
raise ArgumentError, "dup flag switch: #{_1}" if flag?(_1)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# append
|
|
114
|
+
flags << flag
|
|
115
|
+
lookup[flag.key] = flag
|
|
116
|
+
flag.switches.each { lookup[_1] = flag }
|
|
117
|
+
|
|
118
|
+
# return flag
|
|
119
|
+
flag
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|