yarsh 0.1.1 → 0.3.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/AGENTS.md +105 -20
- data/CHANGES.md +101 -12
- data/README.md +66 -2
- data/lib/yarsh/binding.rb +14 -0
- data/lib/yarsh/completion/bash_autocomplete.rb +139 -0
- data/lib/yarsh/completion/engine.rb +17 -0
- data/lib/yarsh/completion/gem_completion.rb +281 -0
- data/lib/yarsh/completion/help_completion.rb +105 -0
- data/lib/yarsh/completion/registry.rb +39 -0
- data/lib/yarsh/completion.rb +16 -0
- data/lib/yarsh/config.rb +3 -1
- data/lib/yarsh/help.rb +72 -0
- data/lib/yarsh/instance_methods.rb +23 -0
- data/lib/yarsh/multiline.rb +147 -0
- data/lib/yarsh/prompt.rb +9 -2
- data/lib/yarsh/shell.rb +243 -50
- data/lib/yarsh/version.rb +1 -1
- data/lib/yarsh.rb +22 -2
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 149709074192906833f0d530ba07da00c9a6591ea79864fd5d5f2e9c39019ad8
|
|
4
|
+
data.tar.gz: 4c7336c81d2531dd94052c05344d186e97878b94c92e1ad5852955d10df07da3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 030f6d98caa7410911fd8cff2e55e9062dda276c6f45f27d2e29073b53717aa273a0b58ad3b761833c1a955fa0b8ade8501c7c0fcab0af8095eff09a906451ce
|
|
7
|
+
data.tar.gz: 85d5081c0f07e9ddea3e0ea5202abea2d2adbea4b6cedf22f8d9a81c4a34fad0b1a9c037de8da601579b2e5bbbea4f649e9a2661b31c3496bf5f8f91d65f727f
|
data/AGENTS.md
CHANGED
|
@@ -15,13 +15,23 @@ A hybrid Ruby/shell interactive REPL. Input is evaluated as Ruby first; on
|
|
|
15
15
|
|
|
16
16
|
## Architecture
|
|
17
17
|
|
|
18
|
-
- **
|
|
19
|
-
|
|
20
|
-
`bin/console`
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
- **Entry point** is `exe/yarsh`: it `extend`s `Yarsh::InstanceMethods` onto the
|
|
19
|
+
top-level object, creates `Yarsh::Shell.new(binding)`, and calls `.shell`
|
|
20
|
+
(the REPL loop). `bin/console` is for development only (adds `bundler/setup`
|
|
21
|
+
and starts an IRB session via `Yarsh.console`).
|
|
22
|
+
- **`lib/yarsh.rb`** defines the module-level helpers: constants (`YARSH_DIR`,
|
|
23
|
+
`HISTORY_FILE`, `CONFIG_FILE`), `ExecOutput`, `self.current_shell`,
|
|
24
|
+
`self.expand_aliases`, `shell_path?` (absolute/relative/home path detection,
|
|
25
|
+
mixed into `Shell`), `self.execute_system_command` (Open3, dead code), and
|
|
26
|
+
`self.console`.
|
|
27
|
+
- **`Yarsh::Shell`** (`lib/yarsh/shell.rb`) holds the REPL loop (`shell`),
|
|
28
|
+
the prompt/history/completion wiring (`setup_reline_*`), the evaluation
|
|
29
|
+
pipeline (`execute_multiline`, `yarsh_eval`, `execute_pipe`, `__sh_exec`),
|
|
30
|
+
`cd`, `shell_command?`, and `execute` (`system(cmd, exception: true)`).
|
|
31
|
+
- Multiline input lives in `lib/yarsh/multiline.rb` (`Yarsh::Multiline`):
|
|
32
|
+
`incomplete?` decides when to keep reading, `transform` rewrites shell lines
|
|
33
|
+
into `__sh__`/`__yarsh__` calls, `classify_line` classifies each line. The
|
|
34
|
+
active shell is exposed as `Yarsh.current_shell` for `__sh__` dispatch.
|
|
25
35
|
|
|
26
36
|
## `-->` pipe operator
|
|
27
37
|
|
|
@@ -39,15 +49,72 @@ Examples:
|
|
|
39
49
|
| `ls --> lines.count` | (file count) |
|
|
40
50
|
| `cat data.txt --> _.split("\n").grep(/error/)` | matching lines |
|
|
41
51
|
|
|
42
|
-
##
|
|
52
|
+
## Multiline input
|
|
53
|
+
|
|
54
|
+
Input is read via `Reline.readmultiline`; the REPL keeps reading while the
|
|
55
|
+
buffer is syntactically incomplete (`Yarsh::Multiline.incomplete?` checks the
|
|
56
|
+
`SyntaxError` from `RubyVM::InstructionSequence.compile` for unterminated
|
|
57
|
+
constructs: missing `end`, strings, heredocs, regexps, operators, `case`
|
|
58
|
+
clauses). Known-bad syntax (`@@@`) is not incomplete. Shell-command-shaped
|
|
59
|
+
lines (`cd /tmp`, `grep foo /etc/passwd`) never enter multiline mode even
|
|
60
|
+
though they fail to compile as Ruby (unterminated regexp) — `shell_command?`
|
|
61
|
+
is checked first. An empty line forces submission; continuation lines get a
|
|
62
|
+
` > ` prompt via `Reline.prompt_proc`.
|
|
63
|
+
|
|
64
|
+
The finished buffer is rewritten by `Yarsh::Multiline.transform` (each line
|
|
65
|
+
classified 1:1 by `classify_line`), then evaluated via `@bind.eval`:
|
|
66
|
+
|
|
67
|
+
- `:structural` — lines whose `Ripper.lex` tokens include a structural
|
|
68
|
+
keyword (`do`, `if`, `def`, `class`, `when`, …) are kept verbatim, but an
|
|
69
|
+
opening structural line gets `bind = binding` appended (nested structures
|
|
70
|
+
get `bind = binding + bind`) so inner lines can reach outer locals
|
|
71
|
+
- `:yarsh` — every other non-blank line (shell lines *and* Ruby expressions
|
|
72
|
+
inside a structural block) is rewritten to
|
|
73
|
+
`__yarsh__("...", bind|nil)`; local variables assigned in an outer
|
|
74
|
+
structural line are wrapped as `__yarsh__("name", bind)` references
|
|
75
|
+
(`IdentifierSurround`)
|
|
76
|
+
- `:skip` — blank lines are dropped; heredoc bodies are never rewritten
|
|
77
|
+
- `:shell` — reserved for `__sh__("...")` calls (currently unemitted, see
|
|
78
|
+
Known issues)
|
|
79
|
+
|
|
80
|
+
`__yarsh__` (`Yarsh::InstanceMethods`, extended onto the top-level binding in
|
|
81
|
+
`exe/yarsh`) temporarily swaps `Shell#bind` for the block binding and calls
|
|
82
|
+
`Shell#yarsh_eval`, which handles `-->` pipes, shell paths, `cd`, and Ruby
|
|
83
|
+
evaluation with the shell fallback (see REPL evaluation flow). Shell lines
|
|
84
|
+
therefore work inside Ruby blocks, defs, and classes; the whole buffer is
|
|
85
|
+
evaluated in the shared REPL binding so methods/variables persist. Examples:
|
|
86
|
+
|
|
87
|
+
| Input | Result |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `Dir['*.rb'].each do \|f|` … `ls -l` … `puts f` … `end` | runs `ls -l` for each file |
|
|
90
|
+
| `def hi` … `puts 'yo'` … `end` then `hi` | `yo` (def persists) |
|
|
43
91
|
|
|
44
|
-
|
|
92
|
+
Known limitations:
|
|
93
|
+
|
|
94
|
+
- Pure-bash multiline (e.g. bash `if ...; then`) is not supported; shell
|
|
95
|
+
lines only work inside Ruby structures.
|
|
96
|
+
- Shell execution inside a block relies on the `NameError` fallback of
|
|
97
|
+
`yarsh_eval`, so a Ruby expression that raises a real `NameError` may be
|
|
98
|
+
shell-executed instead of reported.
|
|
99
|
+
|
|
100
|
+
## REPL evaluation flow
|
|
45
101
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
102
|
+
All steps below are wrapped in a single `begin..rescue` in `Shell#shell` so no
|
|
103
|
+
error crashes the REPL:
|
|
104
|
+
|
|
105
|
+
1. Input is read via `Reline.readmultiline` (see Multiline input above);
|
|
106
|
+
`read_input` keeps reading while `incomplete_line?` (incomplete and not a
|
|
107
|
+
shell command) or until a blank line ends the buffer
|
|
108
|
+
2. The whole input goes through `execute_multiline`: `Yarsh::Multiline.transform`
|
|
109
|
+
rewrites non-structural lines to `__yarsh__(...)` calls (structural lines
|
|
110
|
+
kept verbatim), then `@bind.eval` runs the buffer; per-line execution
|
|
111
|
+
happens inside `Shell#yarsh_eval` / `Shell#__sh_exec`
|
|
112
|
+
3. `yarsh_eval`: `-->` pipe → `execute_pipe`; shell path (`shell_path?`) →
|
|
113
|
+
`execute`; `cd`/`cd <path>` → `Dir.chdir`; otherwise `bind.eval` — on
|
|
114
|
+
`NameError`/`SyntaxError`/`ArgumentError` it falls back to aliases +
|
|
115
|
+
shell execution (unless the input looks like a Ruby error, then it is
|
|
116
|
+
reported)
|
|
117
|
+
4. Other exceptions → print `Error: ExceptionClass: message`
|
|
51
118
|
|
|
52
119
|
## Error handling
|
|
53
120
|
|
|
@@ -60,12 +127,30 @@ All steps below are wrapped in a single `begin..rescue` so no error crashes the
|
|
|
60
127
|
|
|
61
128
|
## Known issues
|
|
62
129
|
|
|
63
|
-
- **
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- **
|
|
67
|
-
|
|
68
|
-
|
|
130
|
+
- **Dead code:** `Yarsh.execute_system_command` (Open3 version) in
|
|
131
|
+
`lib/yarsh.rb` is defined but never called; shell execution goes through
|
|
132
|
+
`Shell#execute` (`system(cmd, exception: true)`).
|
|
133
|
+
- **Bug risk:** `setup_reline_history` replays history with
|
|
134
|
+
`@core.readmultiline`-independent heuristics (`incomplete_line?`),
|
|
135
|
+
which may mishandle historical shell-command lines (see Multiline input).
|
|
136
|
+
- **Unemitted `:shell`:** `Multiline.transform` never emits `:shell`/`__sh__`
|
|
137
|
+
(the `classify_line` branches are commented out behind FIXMEs); shell lines
|
|
138
|
+
and Ruby expressions both go through `__yarsh__`, with the shell fallback of
|
|
139
|
+
`yarsh_eval` deciding. `__sh__`/`__sh_exec` still exist for potential
|
|
140
|
+
re-enabling.
|
|
141
|
+
|
|
142
|
+
## Bash autocomplete
|
|
143
|
+
|
|
144
|
+
`Yarsh::Completion::BashAutocomplete` provides tab-completion by invoking bash
|
|
145
|
+
completion functions:
|
|
146
|
+
|
|
147
|
+
- For commands supporting the `__complete` subcommand (docker, kubectl, etc.),
|
|
148
|
+
it calls `<command> __complete <word>` and parses tab-separated output.
|
|
149
|
+
- For all other commands, it sources the bash completion script and runs the
|
|
150
|
+
registered completion function with `COMP_WORDS`/`COMP_CWORD` set.
|
|
151
|
+
- Privileged commands (`sudo`, `doas`, `pkexec`, `su`) skip the `__complete`
|
|
152
|
+
path to avoid password prompts; they always use the bash completion function.
|
|
153
|
+
- Results are cached in a bounded in-memory `CACHE` (max 100 entries).
|
|
69
154
|
|
|
70
155
|
## Style
|
|
71
156
|
|
data/CHANGES.md
CHANGED
|
@@ -26,19 +26,19 @@ a shell command, matching the existing `NameError` fallback behavior.
|
|
|
26
26
|
|
|
27
27
|
### REPL history persisted to `~/.yarsh/history`
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Commands are saved to `~/.yarsh/history` across sessions. Max 1000 lines;
|
|
30
30
|
concurrent-session safe via `File.flock`.
|
|
31
|
-
|
|
31
|
+
Add tests for append_history: creation, append, trim, concurrent safety.
|
|
32
32
|
|
|
33
33
|
### `bin/console` launches an IRB session
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
`bin/console` now starts a standard IRB session with the gem loaded
|
|
36
36
|
(`Yarsh.console`). The hybrid REPL remains available via `bundle exec ruby exe/yarsh`
|
|
37
37
|
(`Yarsh.shell`).
|
|
38
38
|
|
|
39
39
|
### Test suite adapted to multi-file architecture
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Tests split per class: `test_yarsh.rb` (module-level: constants,
|
|
42
42
|
`ExecOutput`, `console`), `test_shell.rb` (`Yarsh::Shell`: `shell_path?`, `cd`,
|
|
43
43
|
completion, history, `execute_pipe` incl. bind-var feature), `test_config.rb`
|
|
44
44
|
(`Yarsh::Config`: prompt, `log_level`), `test_prompt.rb` (`Prompt`,
|
|
@@ -47,14 +47,14 @@ validation in `Config#prompt=`.
|
|
|
47
47
|
|
|
48
48
|
### Console test works under minitest 6
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
`test_console_launches_irb_session` no longer uses
|
|
51
51
|
`IRB.stub` (`minitest/mock` was removed in minitest 6, breaking plain
|
|
52
52
|
`rake test`); it now stubs `IRB.start` via `define_singleton_method`.
|
|
53
53
|
README documents running tests with `bundle exec rake test`.
|
|
54
54
|
|
|
55
55
|
### `expand_aliases` no longer strips whitespace
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Registering the first alias via `sh_alias`/`add_alias` ran the
|
|
58
58
|
command through `expand_aliases` with an empty alias hash; the empty
|
|
59
59
|
alternation degraded the regex to `(^| +)() *` and every space was
|
|
60
60
|
deleted (`exa -l --icons=always` was stored as `exa-l--icons=always`).
|
|
@@ -66,7 +66,7 @@ with a regression test for the empty-alias case.
|
|
|
66
66
|
|
|
67
67
|
### `Yarsh::InvalidPromptError` for invalid prompt configuration
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
`Config#prompt=` raises the new `Yarsh::InvalidPromptError`
|
|
70
70
|
(subclass of `Yarsh::Error`) instead of a bare `StandardError`. The
|
|
71
71
|
message is a constructor argument with the documented default.
|
|
72
72
|
`Yarsh::Error` is now defined before sub-files are required, fixing a
|
|
@@ -75,7 +75,7 @@ assert the error class, the default message, and custom messages.
|
|
|
75
75
|
|
|
76
76
|
### Logging methods in `InstanceMethods`
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
Besides the existing `debug`, the REPL now exposes `info`,
|
|
79
79
|
`warn`, `error`, `fatal`, and `unknown` — each delegating to
|
|
80
80
|
`Config.instance.logger`, covering all `Logger` severities. Methods are
|
|
81
81
|
defined dynamically from the `SEVERITIES` constant
|
|
@@ -83,7 +83,7 @@ defined dynamically from the `SEVERITIES` constant
|
|
|
83
83
|
|
|
84
84
|
### 256-color and text styles in `AnsiColorFormatter`
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
New `fg_color256`/`bg_color256` emit 8-bit indexed color
|
|
87
87
|
codes (`38;5;N` / `48;5;N`) and raise `Yarsh::Error` for indices outside
|
|
88
88
|
0..255. Text styles are defined dynamically from `STYLE_CODES`: `bold`,
|
|
89
89
|
`dim`, `italic`, `underline`, `blink`, `reverse`, `hidden`,
|
|
@@ -93,7 +93,7 @@ now tolerates calls with neither text nor a block, so bare style calls
|
|
|
93
93
|
|
|
94
94
|
### Real `ArgumentError`s are no longer shell-executed
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
Shell commands like `bundle exec rake test` raise
|
|
97
97
|
`ArgumentError` (the innermost call resolves to `Kernel#test`, which
|
|
98
98
|
requires 2 args). The fallback previously treated every `ArgumentError`
|
|
99
99
|
as a shell command, so genuine Ruby errors (`"x".sub`) were
|
|
@@ -105,10 +105,99 @@ keep their unconditional shell fallback.
|
|
|
105
105
|
|
|
106
106
|
### Renamed from `rsh` to `yarsh`
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
The project is now called **yarsh** throughout: module `Rsh`
|
|
109
109
|
→ `Yarsh`, gem name `yarsh` (`spec.name`, require paths), data dir
|
|
110
110
|
`~/.rsh` → `~/.yarsh`, executable `exe/ruby-shell` → `exe/yarsh`.
|
|
111
111
|
Files renamed: `rsh.gemspec`, `lib/rsh.rb`, `lib/rsh/`,
|
|
112
112
|
`sig/rsh.rbs`, `test/test_rsh.rb`. All references updated (tests,
|
|
113
113
|
`bin/console`, README, AGENTS.md, CHANGES.md); `Gemfile.lock`
|
|
114
|
-
regenerated.
|
|
114
|
+
regenerated.
|
|
115
|
+
|
|
116
|
+
## 0.1.0
|
|
117
|
+
|
|
118
|
+
First development release!
|
|
119
|
+
|
|
120
|
+
* `eee1b3e` Initial commit for a clean git project
|
|
121
|
+
* `da76411` and `b02a1b8` Updated README for better clarity and fix repository links
|
|
122
|
+
|
|
123
|
+
## 0.1.1
|
|
124
|
+
|
|
125
|
+
Small correction in the gem description
|
|
126
|
+
|
|
127
|
+
* `5741a00` update the gem description
|
|
128
|
+
|
|
129
|
+
## 0.1.2
|
|
130
|
+
|
|
131
|
+
Bug fix and small improvement for the prompt type **powerline**
|
|
132
|
+
|
|
133
|
+
See commits:
|
|
134
|
+
|
|
135
|
+
* `d59834b`
|
|
136
|
+
* `d81ffcb`
|
|
137
|
+
* `3e93937`
|
|
138
|
+
* `4b77901`
|
|
139
|
+
|
|
140
|
+
## 0.2.0
|
|
141
|
+
|
|
142
|
+
### Multi-line input with shell lines inside Ruby blocks
|
|
143
|
+
|
|
144
|
+
Input is now read via `Reline.readmultiline` — the REPL keeps reading while the
|
|
145
|
+
buffer is syntactically incomplete (`Yarsh::Multiline.incomplete?` checks the
|
|
146
|
+
`SyntaxError` from `RubyVM::InstructionSequence.compile` for unterminated
|
|
147
|
+
constructs: missing `end`, strings, heredocs, regexps, operators, `case`
|
|
148
|
+
clauses). Known-bad syntax (`@@@`) is not treated as incomplete, and
|
|
149
|
+
shell-command-shaped lines (`cd /tmp`, `grep foo /etc/passwd`) never enter
|
|
150
|
+
multiline mode even though they fail to compile as Ruby (unterminated regexp).
|
|
151
|
+
An empty line forces submission; continuation lines get a ` > ` prompt via
|
|
152
|
+
`Reline.prompt_proc`.
|
|
153
|
+
|
|
154
|
+
The finished buffer is rewritten by `Yarsh::Multiline.transform` (each line
|
|
155
|
+
classified 1:1 by `classify_line`), then evaluated via `@bind.eval`:
|
|
156
|
+
|
|
157
|
+
- Lines whose `Ripper.lex` tokens include a structural keyword (`do`, `if`,
|
|
158
|
+
`def`, `class`, `when`, …) are kept verbatim; an opening structural line
|
|
159
|
+
gets `bind = binding` appended (nested structures get `bind = binding +
|
|
160
|
+
bind`) so inner lines can reach outer locals
|
|
161
|
+
- Every other non-blank line — shell lines *and* Ruby expressions inside a
|
|
162
|
+
structural block — is rewritten to `__yarsh__(...)` with the surrounding
|
|
163
|
+
binding; local variables assigned in an outer structural line are wrapped
|
|
164
|
+
as `__yarsh__("name", bind)` references (`IdentifierSurround`)
|
|
165
|
+
- Blank lines are dropped; heredoc bodies are never rewritten
|
|
166
|
+
|
|
167
|
+
`__yarsh__` (`Yarsh::InstanceMethods`, extended onto the top-level binding in
|
|
168
|
+
`exe/yarsh`) temporarily swaps `Yarsh::Shell#bind` for the block binding and
|
|
169
|
+
calls `Shell#yarsh_eval`, which handles `-->` pipes, shell paths, `cd`, and
|
|
170
|
+
Ruby evaluation with the shell fallback. Shell lines therefore work inside
|
|
171
|
+
Ruby blocks, defs, and classes; the whole buffer is evaluated in the shared
|
|
172
|
+
REPL binding so methods and variables persist. Examples:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
Dir['*.rb'].each do |f|
|
|
176
|
+
ls -l
|
|
177
|
+
puts f
|
|
178
|
+
end
|
|
179
|
+
# runs `ls -l` for each file
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
def hi
|
|
184
|
+
puts 'yo'
|
|
185
|
+
end
|
|
186
|
+
hi
|
|
187
|
+
#=> yo (def persists)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Known limitations:
|
|
191
|
+
|
|
192
|
+
- Pure-bash multiline (e.g. bash `if ...; then`) is not supported; shell
|
|
193
|
+
lines only work inside Ruby structures.
|
|
194
|
+
- Shell execution inside a block relies on the `NameError` fallback of
|
|
195
|
+
`yarsh_eval`, so a Ruby expression that raises a real `NameError` may be
|
|
196
|
+
shell-executed instead of reported.
|
|
197
|
+
|
|
198
|
+
See commits: `bcc25b4`, `ea92742`, `51c9db8`, `2218f29`, `0fceeb8`. Tests in
|
|
199
|
+
`test/test_multiline.rb` (completeness detection, line classification,
|
|
200
|
+
transformation, heredoc handling, binding threading, integration with
|
|
201
|
+
`Yarsh::Shell`).
|
|
202
|
+
|
|
203
|
+
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Yet Another Ruby SHell is a mix of Ruby and standard Shell REPL. Priority is giv
|
|
|
4
4
|
|
|
5
5
|
This project was made just for fun, to remember the basis of Ruby and to discover new features. To provide a good user experience, it uses `reline` under the hood which already implement all we want for a REPL: History, completion, etc.
|
|
6
6
|
|
|
7
|
-
I wanted a simple version but already working version. If I have time, more features will come. For example,
|
|
7
|
+
I wanted a simple version but already working version. If I have time, more features will come. For example, non interactive with a file as argument. I dream of a shell syntax we can do:
|
|
8
8
|
|
|
9
9
|
```ruby
|
|
10
10
|
def install_rails
|
|
@@ -16,8 +16,15 @@ end
|
|
|
16
16
|
install_rails
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
And this is now a reality: multi-line editing is supported, and shell commands
|
|
20
|
+
work inside Ruby blocks, defs, and classes (see [Multiline](#multiline)).
|
|
21
|
+
|
|
19
22
|
I developed this project half of the time offline, another part with the help of AI. It was a way for me to try to develop with AI. I used `opencode` with DeepSeek V4 Flash Free.
|
|
20
23
|
|
|
24
|
+
## install
|
|
25
|
+
|
|
26
|
+
Run `gem install yarsh`
|
|
27
|
+
|
|
21
28
|
## Usage
|
|
22
29
|
|
|
23
30
|
`yarsh` start the shell.
|
|
@@ -55,9 +62,66 @@ ls -al . -->(sorted) lines.sort {|a, b| a.length <=> b.length }
|
|
|
55
62
|
sorted.each { |file| puts "What ever you wanna do" }
|
|
56
63
|
```
|
|
57
64
|
|
|
65
|
+
### Multiline
|
|
66
|
+
|
|
67
|
+
Multi-line input is supported: the REPL keeps reading while the buffer is
|
|
68
|
+
syntactically incomplete (missing `end`, unterminated string/heredoc/regexp,
|
|
69
|
+
trailing operator, ...) and submits when the syntax is complete or on an empty
|
|
70
|
+
line. Continuation lines get a ` > ` prompt.
|
|
71
|
+
|
|
72
|
+
The best part: you can mix Ruby and shell lines in the same block, def, or
|
|
73
|
+
class:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
def install_rails
|
|
77
|
+
sudo apt update
|
|
78
|
+
sudo apt install ruby
|
|
79
|
+
gem install rails
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
Dir['*.rb'].each do |f|
|
|
85
|
+
ls -l
|
|
86
|
+
puts "Processed: #{f}"
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The whole buffer is evaluated in the shared REPL binding, so methods,
|
|
91
|
+
variables, and shell side-effects persist between lines and across inputs.
|
|
92
|
+
Shell lines are executed via the usual fallback (they are evaluated as Ruby
|
|
93
|
+
first, then as a shell command). Heredoc bodies are kept verbatim:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
x = <<~EOF
|
|
97
|
+
not evaluated, just a string
|
|
98
|
+
EOF
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Known limitations:
|
|
102
|
+
|
|
103
|
+
- Pure-bash multiline (e.g. bash `if ...; then`) is not supported; shell
|
|
104
|
+
lines only work inside Ruby structures.
|
|
105
|
+
- A Ruby expression that raises a real `NameError` inside a block may be
|
|
106
|
+
executed as a shell command instead of being reported.
|
|
107
|
+
|
|
58
108
|
### Completion
|
|
59
109
|
|
|
60
|
-
|
|
110
|
+
Tab completion is powered by a multi-engine system:
|
|
111
|
+
|
|
112
|
+
- **Command completion** — completes executables in PATH and shell builtins
|
|
113
|
+
- **Path completion** — completes file and directory paths (including `~` expansion)
|
|
114
|
+
- **Ruby completion** — completes Ruby methods and constants (can be disabled via `Config.instance.ruby_completion = false`)
|
|
115
|
+
- **Bash completion** — invokes bash completion functions for commands that have them (e.g. `git`, `docker`, `systemctl`), including multi-word commands like `systemctl restart <service>`
|
|
116
|
+
- **Gem completion** — provides subcommand and option completion for `gem`, plus installed gem names for subcommands that operate on installed gems (`info`, `uninstall`, `open`, `contents`, `check`, `cleanup`, `pristine`, `rdoc`, `specification`, `dependency`)
|
|
117
|
+
|
|
118
|
+
Completion is suppressed when the cursor is inside a quoted string, so typing
|
|
119
|
+
`git commit -m ""` won't have options inserted between the quotes.
|
|
120
|
+
|
|
121
|
+
Privileged commands (`sudo`, `doas`, `pkexec`, `su`) are handled transparently:
|
|
122
|
+
`sudo systemctl restart <Tab>` completes the same as `systemctl restart <Tab>`.
|
|
123
|
+
|
|
124
|
+
A base completion engine is provided in order to add more completions with ease. See `lib/yarsh/completion/base.rb` for more information and `lib/yarsh/completion/gem.rb` for an example of a completion for the gem command.
|
|
61
125
|
|
|
62
126
|
### Errors
|
|
63
127
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class Binding
|
|
2
|
+
def +(other)
|
|
3
|
+
return self if other.nil?
|
|
4
|
+
|
|
5
|
+
other.local_variables.each do |var|
|
|
6
|
+
local_variable_set(var, other.local_variable_get(var))
|
|
7
|
+
end
|
|
8
|
+
self
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def inspect
|
|
12
|
+
"#<Binding:>\n local_vars: #{local_variables.inspect}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
require_relative 'engine'
|
|
6
|
+
|
|
7
|
+
module Yarsh
|
|
8
|
+
module Completion
|
|
9
|
+
class BashAutocomplete < Engine
|
|
10
|
+
BASH_COMPLETION_MAIN = '/usr/share/bash-completion/bash_completion'
|
|
11
|
+
COMPLETION_DIR = '/usr/share/bash-completion/completions'
|
|
12
|
+
CACHE = {}
|
|
13
|
+
|
|
14
|
+
def supports?(command)
|
|
15
|
+
return false if command.nil? || command.empty?
|
|
16
|
+
command = command.strip.split(/\s+/).first
|
|
17
|
+
File.exist?("#{COMPLETION_DIR}/#{command}") ||
|
|
18
|
+
File.exist?("#{COMPLETION_DIR}/#{command}.bash") ||
|
|
19
|
+
bash_completion_defined?(command)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def complete(command, word, args = [])
|
|
23
|
+
command = command.strip.split(/\s+/).first
|
|
24
|
+
return [] unless supports?(command)
|
|
25
|
+
return [] unless find_script(command)
|
|
26
|
+
|
|
27
|
+
# Try __complete subcommand first (works for docker and others)
|
|
28
|
+
sub_output = bash_invoke_complete_subcommand(command, word)
|
|
29
|
+
return sub_output unless sub_output.nil? || sub_output.empty?
|
|
30
|
+
|
|
31
|
+
# Fall back to bash completion function
|
|
32
|
+
func = completion_function(command)
|
|
33
|
+
return [] if func.nil? || func.empty?
|
|
34
|
+
|
|
35
|
+
output = bash_invoke_completion(func, command, word)
|
|
36
|
+
return [] if output.nil? || output.empty?
|
|
37
|
+
|
|
38
|
+
has_trailing_space = word.end_with?(' ')
|
|
39
|
+
prefix = has_trailing_space ? '' : word.strip.split(/\s+/).last
|
|
40
|
+
return output.split if prefix == command
|
|
41
|
+
|
|
42
|
+
output.split.select { |r| r.start_with?(prefix) }
|
|
43
|
+
rescue
|
|
44
|
+
[]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def find_script(command)
|
|
50
|
+
return "#{COMPLETION_DIR}/#{command}" if File.exist?("#{COMPLETION_DIR}/#{command}")
|
|
51
|
+
return "#{COMPLETION_DIR}/#{command}.bash" if File.exist?("#{COMPLETION_DIR}/#{command}.bash")
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def bash_completion_defined?(command)
|
|
56
|
+
func = completion_function(command)
|
|
57
|
+
!func.nil? && !func.empty?
|
|
58
|
+
rescue
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def completion_function(command)
|
|
63
|
+
return '' unless File.exist?(BASH_COMPLETION_MAIN)
|
|
64
|
+
script = find_script(command)
|
|
65
|
+
return '' unless script
|
|
66
|
+
|
|
67
|
+
`bash -c 'source #{Shellwords.shellescape(BASH_COMPLETION_MAIN)} 2>/dev/null; source #{Shellwords.shellescape(script)} 2>/dev/null; complete -p #{Shellwords.shellescape(command)} 2>/dev/null | grep -oP "(?<= -F )\\S+"' 2>/dev/null`.strip
|
|
68
|
+
rescue
|
|
69
|
+
''
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def bash_invoke_completion(func, command, partial)
|
|
73
|
+
has_trailing_space = partial.end_with?(' ')
|
|
74
|
+
words = if has_trailing_space
|
|
75
|
+
partial.strip.split(/\s+/) << ''
|
|
76
|
+
else
|
|
77
|
+
partial.strip.split(/\s+/)
|
|
78
|
+
end
|
|
79
|
+
cword = [words.size - 1, 0].max
|
|
80
|
+
comp_words = words.map { |w| Shellwords.shellescape(w) }.join(' ')
|
|
81
|
+
comp_words += ' ""' unless has_trailing_space
|
|
82
|
+
script = Shellwords.shellescape(find_script(command))
|
|
83
|
+
cmd = "source #{Shellwords.shellescape(BASH_COMPLETION_MAIN)} 2>/dev/null; source #{script} 2>/dev/null; COMP_WORDS=(#{comp_words}); COMP_CWORD=#{cword}; COMPREPLY=(); #{Shellwords.shellescape(func)} 2>/dev/null; IFS=; printf '%s\\n' \"\${COMPREPLY[@]}\""
|
|
84
|
+
return CACHE[cmd] if CACHE.key?(cmd)
|
|
85
|
+
raw = nil
|
|
86
|
+
Tempfile.create('yarsh_completion') do |f|
|
|
87
|
+
f.write(cmd)
|
|
88
|
+
f.close
|
|
89
|
+
raw = `timeout 4 bash #{f.path} 2>/dev/null`.strip
|
|
90
|
+
end
|
|
91
|
+
CACHE[cmd] = raw
|
|
92
|
+
CACHE.delete_if { |_, v| v.nil? || v.empty? } if CACHE.size > 100
|
|
93
|
+
raw
|
|
94
|
+
rescue
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def bash_invoke_complete_subcommand(command, partial)
|
|
99
|
+
has_trailing_space = partial.end_with?(' ')
|
|
100
|
+
words = partial.strip.split(/\s+/)
|
|
101
|
+
return nil if words.size > 2
|
|
102
|
+
|
|
103
|
+
# Don't invoke __complete for privileged commands (prompts for password)
|
|
104
|
+
return nil if PRIVILEGED_COMMANDS.include?(command)
|
|
105
|
+
|
|
106
|
+
# Don't invoke __complete when only the command + space is typed (no arg yet)
|
|
107
|
+
return nil if has_trailing_space && words.size == 1
|
|
108
|
+
|
|
109
|
+
last_word = has_trailing_space ? '' : (words.last || '')
|
|
110
|
+
raw = `timeout 4 #{Shellwords.shellescape(command)} __complete #{Shellwords.shellescape(last_word)} 2>/dev/null`.strip
|
|
111
|
+
return nil if raw.empty?
|
|
112
|
+
|
|
113
|
+
# Filter to only lines that contain tab-separated data
|
|
114
|
+
tab_lines = raw.each_line.select { |line| line.include?("\t") }
|
|
115
|
+
return nil if tab_lines.empty?
|
|
116
|
+
|
|
117
|
+
results = tab_lines.map { |line| line.split("\t").first }.reject(&:empty?)
|
|
118
|
+
return nil if results.empty?
|
|
119
|
+
|
|
120
|
+
# If the only result is the last_word itself, try subcommand completion
|
|
121
|
+
if results.size == 1 && results.first == last_word
|
|
122
|
+
sub_cmd = words[1..-1].join(' ')
|
|
123
|
+
if sub_cmd && !sub_cmd.empty?
|
|
124
|
+
sub_raw = `timeout 4 #{Shellwords.shellescape(command)} #{Shellwords.shellescape(sub_cmd)} __complete 2>/dev/null`.strip
|
|
125
|
+
return nil if sub_raw.empty? || !sub_raw.include?("\t")
|
|
126
|
+
sub_tab_lines = sub_raw.each_line.select { |line| line.include?("\t") }
|
|
127
|
+
return nil if sub_tab_lines.empty?
|
|
128
|
+
sub_results = sub_tab_lines.map { |line| line.split("\t").first }.reject(&:empty?)
|
|
129
|
+
return nil if sub_results.empty?
|
|
130
|
+
results = sub_results
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
results
|
|
134
|
+
rescue
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yarsh
|
|
4
|
+
module Completion
|
|
5
|
+
class Engine
|
|
6
|
+
PRIVILEGED_COMMANDS = %w[sudo doas pkexec su].freeze
|
|
7
|
+
|
|
8
|
+
def complete(command, word, args = [])
|
|
9
|
+
raise NotImplementedError, "Subclasses must implement #complete"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def supports?(command)
|
|
13
|
+
false
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|