tabry 0.1.2 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/tabry-bash +2 -17
- data/lib/tabry/cli/util.rb +13 -2
- data/lib/tabry/options_finder.rb +4 -0
- data/lib/tabry/shell_splitter.rb +32 -0
- data/lib/tabry/usage_generator.rb +5 -5
- data/sh/fish/README.md +16 -0
- data/sh/fish/tabry_fish.fish +95 -0
- data/spec/lib/tabry/cli/util_spec.rb +58 -0
- data/spec/tabry/options_finder_spec.rb +36 -0
- data/spec/tabry/shell_splitter_spec.rb +20 -0
- data/tabry.gemspec +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 238e670dd915a52c21ff0af689fd3885261deba3caee10dae8972f06cbf90ccb
|
4
|
+
data.tar.gz: ff161d2cda396309d243a8f1dd5f4a75a6b154f6bdacedb7ee7e59c4bae5d8f5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66d5af76b745d7cb56c1905f095c7b137acfd3d816f4e8fb070c0ccf8c76c5be123c85ebc5b1fc29480e73485a7f9dacb438d98c0f514009830afabb5ec13426
|
7
|
+
data.tar.gz: 30545a229fc13939596285535d84a6e69cc73c08e248f0d3a3bf18cbba18a379dbc26a1756c9f9266081503d277da15738d0608e573a219715b8d46211eb8772
|
data/bin/tabry-bash
CHANGED
@@ -8,27 +8,12 @@ require "shellwords"
|
|
8
8
|
require "yaml"
|
9
9
|
require_relative "../lib/tabry/util"
|
10
10
|
require_relative "../lib/tabry/runner"
|
11
|
+
require_relative "../lib/tabry/shell_splitter"
|
11
12
|
|
12
13
|
# Bash-specific entrypoint, taking COMP_WORDS and COMP_CWORDS and returning possible options
|
13
14
|
|
14
|
-
COMP_POINT_SENTINEL = "\uFFFF"
|
15
|
-
|
16
|
-
# TODO: in weird scenarios this acts weird: namely, special shell operators like <(ls), $$((1 + 1))
|
17
|
-
# Also it crashed on unbalanced quotes, like: foo "bar<TAB>
|
18
|
-
# however, this will handle the common scenarios of escaping with quotes, single quotes, and backslashes
|
19
|
-
# Split up args and put the argument that comp_point is in in the `last_arg` variable.
|
20
|
-
# Just cutting off everything after comp_point might have worked, although
|
21
|
-
# maybe we wanted the whole arg? Not sure this is the best.
|
22
15
|
cmd_line, comp_point = ARGV
|
23
|
-
|
24
|
-
cmd_line[comp_point.to_i...comp_point.to_i] = COMP_POINT_SENTINEL
|
25
|
-
cmd, *all_args = Shellwords.split(cmd_line)
|
26
|
-
last_arg_index = all_args.index { |arg| arg.include?(COMP_POINT_SENTINEL) }
|
27
|
-
args = all_args[0..last_arg_index]
|
28
|
-
last_arg = args.pop.gsub! COMP_POINT_SENTINEL, ""
|
29
|
-
|
30
|
-
cmd_name = cmd.gsub(%r{.*/}, "")
|
31
|
-
|
16
|
+
cmd_name, args, last_arg = Tabry::ShellSplitter.split(cmd_line, comp_point)
|
32
17
|
opts = Tabry::Runner.new(config_name: cmd_name).options(args, last_arg)
|
33
18
|
|
34
19
|
if Tabry::Util.debug?
|
data/lib/tabry/cli/util.rb
CHANGED
@@ -9,8 +9,19 @@ module Tabry
|
|
9
9
|
module_function
|
10
10
|
|
11
11
|
def make_cmdline(cmdline, *args, echo: false, echo_only: false)
|
12
|
-
|
13
|
-
|
12
|
+
# Allow to pass in an array, or varargs:
|
13
|
+
args = args.first if args.length == 1 && args.first.is_a?(Array)
|
14
|
+
|
15
|
+
args = args.map do |arg|
|
16
|
+
if arg.is_a?(Array)
|
17
|
+
# array of arguments get escaped separately and joined with strings (see specs)
|
18
|
+
arg.map { |a| Shellwords.escape(a) }.join(" ")
|
19
|
+
else
|
20
|
+
Shellwords.escape(arg)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
cmdline = cmdline % args
|
24
|
+
|
14
25
|
warn cmdline if echo || echo_only
|
15
26
|
return nil if echo_only
|
16
27
|
|
data/lib/tabry/options_finder.rb
CHANGED
@@ -18,14 +18,18 @@ module Tabry
|
|
18
18
|
def options(token)
|
19
19
|
# Bit of a hack: send this down to autocomplete shell commands
|
20
20
|
# TODO: set this only in ShellOption -- would require passing state down on thru
|
21
|
+
before_env = ENV.fetch("TABRY_AUTOCOMPLETE_STATE", nil)
|
21
22
|
ENV["TABRY_AUTOCOMPLETE_STATE"] = {
|
22
23
|
cmd: result.config.cmd,
|
23
24
|
flags: result.state.flags,
|
24
25
|
args: result.state.args,
|
26
|
+
current_token: token,
|
25
27
|
current_flag: result.state.current_flag
|
26
28
|
}.to_json
|
27
29
|
|
28
30
|
send(:"options_#{state.mode}", token || "")
|
31
|
+
ensure
|
32
|
+
ENV["TABRY_AUTOCOMPLETE_STATE"] = before_env
|
29
33
|
end
|
30
34
|
|
31
35
|
private
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Use Shellwords.split() to split a command line + comp point (index of the
|
2
|
+
# cursor in the command line) into the args up to the current token plus
|
3
|
+
# current token
|
4
|
+
module Tabry
|
5
|
+
module ShellSplitter
|
6
|
+
module_function
|
7
|
+
|
8
|
+
COMP_POINT_SENTINEL = "\uFFFF"
|
9
|
+
|
10
|
+
def split(cmd_line, comp_point)
|
11
|
+
# Returns [cmd_name, args, last_arg]
|
12
|
+
# cmd_name is the basename of the command run
|
13
|
+
#
|
14
|
+
# TODO: in weird scenarios this acts weird: namely, special shell operators like <(ls), $$((1 + 1))
|
15
|
+
# Also it crashed on unbalanced quotes, like: foo "bar<TAB>
|
16
|
+
# however, this will handle the common scenarios of escaping with quotes, single quotes, and backslashes
|
17
|
+
# Split up args and put the argument that comp_point is in in the `last_arg` variable.
|
18
|
+
# Just cutting off everything after comp_point might have worked, although
|
19
|
+
# maybe we wanted the whole arg? Not sure this is the best.
|
20
|
+
cmd_line = cmd_line.dup
|
21
|
+
cmd_line[comp_point.to_i...comp_point.to_i] = COMP_POINT_SENTINEL
|
22
|
+
cmd, *all_args = Shellwords.split(cmd_line)
|
23
|
+
last_arg_index = all_args.index { |arg| arg.include?(COMP_POINT_SENTINEL) }
|
24
|
+
args = all_args[0..last_arg_index]
|
25
|
+
last_arg = args.pop.gsub! COMP_POINT_SENTINEL, ""
|
26
|
+
|
27
|
+
cmd_name = cmd.gsub(%r{.*/}, "")
|
28
|
+
|
29
|
+
[cmd_name, args, last_arg]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -97,11 +97,11 @@ module Tabry
|
|
97
97
|
lines << ""
|
98
98
|
lines << "FLAGS".green.bold
|
99
99
|
if sub != sub_stack.last
|
100
|
-
lines
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
100
|
+
lines[-1] += if full_sub_name == ""
|
101
|
+
" (global)".green.bold
|
102
|
+
else
|
103
|
+
" (#{full_sub_name})".green.bold
|
104
|
+
end
|
105
105
|
end
|
106
106
|
|
107
107
|
sub.flags.map(&:name).sort.each do |name|
|
data/sh/fish/README.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
This directory includes fish completions for Tabry
|
2
|
+
|
3
|
+
There are two steps to add to your ~/.config/fish/config.fish to use:
|
4
|
+
|
5
|
+
1. Source this file
|
6
|
+
2. Add a call to `tabry_completion_init`, for each command
|
7
|
+
|
8
|
+
```sh
|
9
|
+
source tabry_fish.fish
|
10
|
+
tabry_completion_init "aws"
|
11
|
+
tabry_completion_init "rapture"
|
12
|
+
```
|
13
|
+
|
14
|
+
Note: Currently, the fish completion support doesn't distinguish between directory completion and file completion. The native fish complete command doesn't seem to support it (https://fishshell.com/docs/current/completions.html), so something custom would have to be written.
|
15
|
+
|
16
|
+
Also note: the bash completion code can be used to create completely separate tabry bash functions for each tabry-based CLI, so multiple tabry-based CLIs can use distinct tabry versions. The fish completion does not yet support this (see sh/bash/README.md).
|
@@ -0,0 +1,95 @@
|
|
1
|
+
|
2
|
+
function tabry_completion_init
|
3
|
+
# Three separate complete calls here are needed to support files.
|
4
|
+
# One provides completions in the case where tabry offers _only_ arguments,
|
5
|
+
# another provides completions where tabry offers _only_ files,
|
6
|
+
# the third handles the case where tabry offers both files and arguments
|
7
|
+
set cmd $argv[1]
|
8
|
+
complete -f -c "$cmd" -n "__fish_tabry_check_only_args" -a "(__fish_tabry_completions)"
|
9
|
+
complete -c "$cmd" -n "__fish_tabry_check_only_file"
|
10
|
+
complete -c "$cmd" -n "__fish_tabry_check_args_and_file" -a "(__fish_tabry_completions)"
|
11
|
+
end
|
12
|
+
|
13
|
+
# return true if tabry only reports commands
|
14
|
+
function __fish_tabry_internal_invoke
|
15
|
+
set SCRIPT (status --current-filename)
|
16
|
+
set SCRIPT_DIR (dirname $SCRIPT)
|
17
|
+
|
18
|
+
# -C "Get cursor position"
|
19
|
+
set cursor_position (commandline -C)
|
20
|
+
set cmd (commandline)
|
21
|
+
set result ($SCRIPT_DIR/../../bin/tabry-bash "$cmd" "$cursor_position")
|
22
|
+
echo $result
|
23
|
+
end
|
24
|
+
|
25
|
+
# return true if tabry only reports file
|
26
|
+
function __fish_tabry_check_only_args
|
27
|
+
set result (__fish_tabry_internal_invoke)
|
28
|
+
|
29
|
+
set args (echo "$result"|sed 's/ .*//')
|
30
|
+
set specials (echo "$result"|grep -o ' file')
|
31
|
+
|
32
|
+
# https://github.com/fish-shell/fish-shell/issues/5186#issuecomment-421244106
|
33
|
+
if test "x$args" != "x" -a "$specials" != " file"
|
34
|
+
# echo "confirming only args: [$result,$args,$specials]" 1>&2
|
35
|
+
return 0;
|
36
|
+
else
|
37
|
+
# echo "rejecting only args: [$result,$args,$specials]" 1>&2
|
38
|
+
return 1;
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# return true if tabry reports file _and_ commands
|
43
|
+
function __fish_tabry_check_only_file
|
44
|
+
set result (__fish_tabry_internal_invoke)
|
45
|
+
|
46
|
+
set args (echo "$result"|sed 's/ .*//')
|
47
|
+
|
48
|
+
if test "x$args" = "x" -a (string match -ra ' file' $result)
|
49
|
+
# echo "confirming only file" 1>&2
|
50
|
+
return 0;
|
51
|
+
else
|
52
|
+
# echo "rejecting only file: [$args,$specials]" 1>&2
|
53
|
+
return 1;
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
function __fish_tabry_check_args_and_file
|
58
|
+
set result (__fish_tabry_internal_invoke)
|
59
|
+
|
60
|
+
set args (echo "$result"|sed 's/ .*//')
|
61
|
+
|
62
|
+
if test "x$args" != "x" -a (string match -ra ' file' $result)
|
63
|
+
# echo "confirming args and file" 1>&2
|
64
|
+
return 0;
|
65
|
+
else
|
66
|
+
# echo "rejecting args and file: [$args,$specials]" 1>&2
|
67
|
+
return 1;
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
function __fish_tabry_completions
|
72
|
+
set result (__fish_tabry_internal_invoke)
|
73
|
+
|
74
|
+
set args (echo "$result"|sed 's/ .*//')
|
75
|
+
|
76
|
+
set args_parsed (string split ' ' $args)
|
77
|
+
|
78
|
+
if test "x$args" = "x"
|
79
|
+
# Don't offer anything if we don't have any completions
|
80
|
+
return 1;
|
81
|
+
else
|
82
|
+
# $args_parsed will be something like: ["foo" "bar" "baz" "" "file"]
|
83
|
+
# where "file" is special, since it's after the space.
|
84
|
+
for arg in $args_parsed
|
85
|
+
if test "x$arg" != "x"
|
86
|
+
echo "$arg"
|
87
|
+
else
|
88
|
+
break;
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# echo $args
|
93
|
+
return 0;
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require_relative "../../../../lib/tabry/cli/util"
|
2
|
+
|
3
|
+
describe Tabry::CLI::Util do
|
4
|
+
describe ".make_cmdline" do
|
5
|
+
it "escapes a single string" do
|
6
|
+
expect(described_class.make_cmdline(
|
7
|
+
"hello %s", "world;abc"
|
8
|
+
)).to eq(
|
9
|
+
"hello world\\;abc"
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "escapes multiple string arguments" do
|
14
|
+
expect(described_class.make_cmdline(
|
15
|
+
"hello %s hi %s", "world;abc", "def;ghi"
|
16
|
+
)).to eq(
|
17
|
+
"hello world\\;abc hi def\\;ghi"
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "escapes array arguments" do
|
22
|
+
# args is an array of strings and arrays
|
23
|
+
expect(described_class.make_cmdline(
|
24
|
+
"hello %s hi %s", "world;abc", ["foo;bar", "waz;ok"]
|
25
|
+
)).to eq(
|
26
|
+
"hello world\\;abc hi foo\\;bar waz\\;ok"
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "treats a single array of strings as a list of args, not as one array arg" do
|
31
|
+
# args is an array with one array of strings
|
32
|
+
# an improvement might be to figure out how
|
33
|
+
# many '%s's there are in the cmdline
|
34
|
+
expect(described_class.make_cmdline(
|
35
|
+
"hello %s hi %s", ["world;abc", "foo;bar"]
|
36
|
+
)).to eq(
|
37
|
+
"hello world\\;abc hi foo\\;bar"
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "escapes arrays if given one array of mixed strings and arrays" do
|
42
|
+
expect(described_class.make_cmdline(
|
43
|
+
"hello %s hi %s", [["world;abc", "foo;bar"], "waz;ok"]
|
44
|
+
)).to eq(
|
45
|
+
"hello world\\;abc foo\\;bar hi waz\\;ok"
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "can send a single array argument by wrapping in an array" do
|
50
|
+
# args is an array with one array of one array
|
51
|
+
expect(described_class.make_cmdline(
|
52
|
+
"hello %s hi", [["foo;bar", "waz;ok"]]
|
53
|
+
)).to eq(
|
54
|
+
"hello foo\\;bar waz\\;ok hi"
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -88,4 +88,40 @@ describe Tabry::OptionsFinder do
|
|
88
88
|
expect(described_class.options(result, token)).to match_array expected_options
|
89
89
|
end
|
90
90
|
end
|
91
|
+
|
92
|
+
describe "TABRY_AUTOCOMPLETE_STATE" do
|
93
|
+
before { @env_before = ENV.fetch("TABRY_AUTOCOMPLETE_STATE", nil) }
|
94
|
+
|
95
|
+
after { ENV["TABRY_AUTOCOMPLETE_STATE"] = @env_before }
|
96
|
+
|
97
|
+
let(:state) do
|
98
|
+
{
|
99
|
+
mode: :flagarg, current_flag: "speed",
|
100
|
+
subcommand_stack: %w[move crash],
|
101
|
+
flags: {}, args: []
|
102
|
+
}
|
103
|
+
end
|
104
|
+
let(:result) { Tabry::Result.new(config, Tabry::State.new(state)) }
|
105
|
+
let(:flag) { config.dig_sub(%w[move crash]).flags["speed"] }
|
106
|
+
|
107
|
+
it "sets TABRY_AUTOCOMPLETE_STATE with information about the state" do
|
108
|
+
expect(flag.options).to receive(:options) do
|
109
|
+
expect(JSON.parse(ENV.fetch("TABRY_AUTOCOMPLETE_STATE", nil), symbolize_names: true)).to eq(
|
110
|
+
args: [],
|
111
|
+
cmd: "vehicles",
|
112
|
+
current_flag: "speed",
|
113
|
+
current_token: "abcd",
|
114
|
+
flags: {}
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
described_class.options(result, "abcd")
|
119
|
+
end
|
120
|
+
|
121
|
+
it "sets TABRY_AUTOCOMPLETE_STATE back to what it was before" do
|
122
|
+
ENV["TABRY_AUTOCOMPLETE_STATE"] = "foobar1234"
|
123
|
+
described_class.options(result, "foo")
|
124
|
+
expect(ENV.fetch("TABRY_AUTOCOMPLETE_STATE", nil)).to eq("foobar1234")
|
125
|
+
end
|
126
|
+
end
|
91
127
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../lib/tabry/shell_splitter"
|
4
|
+
|
5
|
+
describe Tabry::ShellSplitter do
|
6
|
+
describe '.split' do
|
7
|
+
it "returns the command basename, argument, and last argument" do
|
8
|
+
str = 'foo/bar abc def ghi'
|
9
|
+
expect(described_class.split(str, 13)).to eq(["bar", ["abc"], "def"])
|
10
|
+
end
|
11
|
+
|
12
|
+
it "it handles quotes and backslashes like a shell" do
|
13
|
+
expect(described_class.split('"/foo bar/waz" a\'b \'c\\ d "ef g" "hi jk" lmn', 38)).to eq([
|
14
|
+
"waz",
|
15
|
+
["ab c d", "ef g"],
|
16
|
+
"hi jk"
|
17
|
+
])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/tabry.gemspec
CHANGED
@@ -6,7 +6,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "tabry"
|
9
|
-
s.version = "0.1.
|
9
|
+
s.version = "0.1.4"
|
10
10
|
s.summary = "Tab completion and CLIs extraordinaire"
|
11
11
|
s.authors = ["Evan Battaglia"]
|
12
12
|
s.email = "battaglia.evan@gmail.com"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tabry
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Evan Battaglia
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-09-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pry-byebug
|
@@ -133,6 +133,7 @@ files:
|
|
133
133
|
- lib/tabry/options_finder.rb
|
134
134
|
- lib/tabry/result.rb
|
135
135
|
- lib/tabry/runner.rb
|
136
|
+
- lib/tabry/shell_splitter.rb
|
136
137
|
- lib/tabry/shells/bash.rb
|
137
138
|
- lib/tabry/state.rb
|
138
139
|
- lib/tabry/usage_generator.rb
|
@@ -141,12 +142,15 @@ files:
|
|
141
142
|
- sh/bash/tabry_bash.sh
|
142
143
|
- sh/bash/tabry_bash_core.sh
|
143
144
|
- sh/bash/tabry_bash_help.sh
|
145
|
+
- sh/fish/README.md
|
146
|
+
- sh/fish/tabry_fish.fish
|
144
147
|
- spec/fixtures/basiccli.json
|
145
148
|
- spec/fixtures/basiccli.tabry
|
146
149
|
- spec/fixtures/basiccli2.tabry
|
147
150
|
- spec/fixtures/basiccli2.yml
|
148
151
|
- spec/fixtures/vehicles.tabry
|
149
152
|
- spec/fixtures/vehicles.yaml
|
153
|
+
- spec/lib/tabry/cli/util_spec.rb
|
150
154
|
- spec/spec_helper.rb
|
151
155
|
- spec/tabry/cli/arg_proxy_spec.rb
|
152
156
|
- spec/tabry/cli/builder_spec.rb
|
@@ -163,6 +167,7 @@ files:
|
|
163
167
|
- spec/tabry/options_finder_spec.rb
|
164
168
|
- spec/tabry/result_spec.rb
|
165
169
|
- spec/tabry/runner_spec.rb
|
170
|
+
- spec/tabry/shell_splitter_spec.rb
|
166
171
|
- spec/tabry/usage_generator_spec.rb
|
167
172
|
- tabry.gemspec
|
168
173
|
- treesitter/Cargo.toml
|