yarsh 0.1.1 → 0.2.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 +92 -20
- data/CHANGES.md +101 -12
- data/README.md +51 -1
- data/lib/yarsh/binding.rb +10 -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 +100 -39
- data/lib/yarsh/version.rb +1 -1
- data/lib/yarsh.rb +12 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c0badb7f9f245222eb3ff552fff43848240d14e3e8d8efddae83004a77d72ab
|
|
4
|
+
data.tar.gz: d2920f423aac65f6dec454ebf84104fb97c60923358790440463bee0ada9cd8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '097af7754f04c9f8ec210fcd844d745b01e5aa681f36619e47ec3ae979ac00f162e0579085acb8c7f5cbb7a6515a38fad5ee973456618bc32b8cd78f732f3f26'
|
|
7
|
+
data.tar.gz: 891cd97331a879529d3ad545fba07590c0d3ca1d09e295550d96444bda867bd69f9f10cf494ec0cf61944978813831422e9162826b0415c339434022c44db15e
|
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,17 @@ 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.
|
|
69
141
|
|
|
70
142
|
## Style
|
|
71
143
|
|
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,6 +62,49 @@ 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
|
For the moment, there is autocompletion for exectuables in PATH and for file and dir path for shell commands
|
|
@@ -8,6 +8,29 @@ module Yarsh
|
|
|
8
8
|
Config.instance.add_alias(new_name, command)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# Executes a shell line from inside a multiline Ruby buffer. Shell lines
|
|
12
|
+
# are rewritten to __sh__(...) by Yarsh::Multiline.transform.
|
|
13
|
+
def __sh__(command)
|
|
14
|
+
shell = Yarsh.current_shell
|
|
15
|
+
raise Error, 'No active Yarsh shell' if shell.nil?
|
|
16
|
+
|
|
17
|
+
shell.send(:__sh_exec, command)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def __yarsh__(exp, binding = nil)
|
|
21
|
+
shell = Yarsh.current_shell
|
|
22
|
+
raise Error, 'No active Yarsh shell' if shell.nil?
|
|
23
|
+
|
|
24
|
+
current_bind = nil
|
|
25
|
+
if binding
|
|
26
|
+
current_bind = shell.bind
|
|
27
|
+
shell.bind = binding
|
|
28
|
+
end
|
|
29
|
+
result = shell.send(:yarsh_eval, exp)
|
|
30
|
+
shell.bind = current_bind if binding
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
|
|
11
34
|
def source(filename)
|
|
12
35
|
load(filename, Yarsh::InstanceMethods)
|
|
13
36
|
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yarsh
|
|
4
|
+
# Multiline input support: decides when a line is syntactically incomplete
|
|
5
|
+
# (keep reading) and transforms a finished buffer so shell lines can live
|
|
6
|
+
# inside Ruby blocks/defs/classes: shell lines are rewritten into __sh__ calls.
|
|
7
|
+
module Multiline
|
|
8
|
+
INCOMPLETE_MESSAGES = [
|
|
9
|
+
/unexpected end-of-input/,
|
|
10
|
+
/unterminated string/,
|
|
11
|
+
/unterminated heredoc/,
|
|
12
|
+
/unterminated regexp/,
|
|
13
|
+
/can't find string ".*" anywhere before EOF/,
|
|
14
|
+
/expected a `when` or `in` clause after `case`/,
|
|
15
|
+
/expected an `end` to close the/
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
# Keywords that must never be treated as shell, even though some of them
|
|
19
|
+
# (break, next, return, times, ...) are also bash builtins.
|
|
20
|
+
STRUCTURAL_KEYWORDS = %w[do end if else elsif when rescue ensure then begin
|
|
21
|
+
case class def module while until for unless
|
|
22
|
+
return break next yield].freeze
|
|
23
|
+
KEYWORD_REGEX = Regexp.new("\\A(?:#{STRUCTURAL_KEYWORDS.join('|')})\\b")
|
|
24
|
+
|
|
25
|
+
class IdentifierSurround
|
|
26
|
+
def initialize
|
|
27
|
+
@local_vars = {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# detect if the line assign a new local variable
|
|
31
|
+
# if yes, save it's name
|
|
32
|
+
def save_local_vars(line)
|
|
33
|
+
tokens = Prism.lex(line).value.map { |token, _| token }
|
|
34
|
+
return unless tokens.any? { |token| token.type == :EQUAL }
|
|
35
|
+
|
|
36
|
+
tokens
|
|
37
|
+
.take_while { |token| token.type != :EQUAL }
|
|
38
|
+
.filter { |token| token.type == :IDENTIFIER }
|
|
39
|
+
.each do |token|
|
|
40
|
+
@local_vars[token.value] =
|
|
41
|
+
token
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# surroud the local variable with 'bind.eval()'
|
|
46
|
+
# a local variable is an identifier who was registred
|
|
47
|
+
# in a precedent line
|
|
48
|
+
def surrond_with_bind(line, bind_name)
|
|
49
|
+
Prism.lex(line).value
|
|
50
|
+
.map { |token, _| token }
|
|
51
|
+
.filter { |token| token.type == :IDENTIFIER }
|
|
52
|
+
.each do |token|
|
|
53
|
+
next unless @local_vars[token.value]
|
|
54
|
+
|
|
55
|
+
location = token.location
|
|
56
|
+
line[location.start_column..(location.end_column - 1)] =
|
|
57
|
+
"__yarsh__(\"#{token.value}\", #{bind_name})"
|
|
58
|
+
end
|
|
59
|
+
line
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.incomplete?(src)
|
|
64
|
+
RubyVM::InstructionSequence.compile(src)
|
|
65
|
+
false
|
|
66
|
+
rescue SyntaxError => e
|
|
67
|
+
INCOMPLETE_MESSAGES.any? { |re| e.message =~ re }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# :ruby - keep the line verbatim
|
|
71
|
+
# :shell - rewrite the line into a __sh__ call
|
|
72
|
+
# :skip - drop the line (blank)
|
|
73
|
+
def self.classify_line(line)
|
|
74
|
+
stripped = line.strip
|
|
75
|
+
return :skip if stripped.empty?
|
|
76
|
+
# return :ruby if structural?(stripped)
|
|
77
|
+
# return :shell if stripped.include?('-->') #FIXME: Should not be treated as a shell --> should allow multiline too
|
|
78
|
+
# return :shell if shell_command && shell_command.call(stripped) #FIXME: pwd = Dir.pwd is evaluated as a shell command instead of a ruby
|
|
79
|
+
# return :ruby if incomplete?(stripped)
|
|
80
|
+
# return :ruby if compiles?(stripped)
|
|
81
|
+
# #TODO: check what happend when it do not compile with another error message catched by incomplete? ?
|
|
82
|
+
|
|
83
|
+
# :shell
|
|
84
|
+
return :structural if structural?(stripped)
|
|
85
|
+
|
|
86
|
+
# return :shell if !compiles?(stripped) && shell_command&.call(stripped)
|
|
87
|
+
|
|
88
|
+
:yarsh
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.transform(buffer, &shell_command)
|
|
92
|
+
heredoc_delim = nil
|
|
93
|
+
is_structural = false
|
|
94
|
+
print_binding = 'bind = binding'
|
|
95
|
+
ident_surround = IdentifierSurround.new
|
|
96
|
+
buffer.split("\n").map do |line|
|
|
97
|
+
if heredoc_delim
|
|
98
|
+
heredoc_delim = nil if line.strip == heredoc_delim
|
|
99
|
+
next line
|
|
100
|
+
end
|
|
101
|
+
if (delim = heredoc_delimiter(line))
|
|
102
|
+
heredoc_delim = delim
|
|
103
|
+
next line
|
|
104
|
+
end
|
|
105
|
+
case classify_line(line, &shell_command)
|
|
106
|
+
when :skip then nil
|
|
107
|
+
when :structural
|
|
108
|
+
next line if line.include? 'end'
|
|
109
|
+
|
|
110
|
+
if is_structural
|
|
111
|
+
line = ident_surround.surrond_with_bind(line, 'bind')
|
|
112
|
+
print_binding = 'bind = binding + bind'
|
|
113
|
+
end
|
|
114
|
+
is_structural = true
|
|
115
|
+
line + "\n" + print_binding
|
|
116
|
+
when :shell then "__sh__(#{line.strip.inspect})"
|
|
117
|
+
when :yarsh
|
|
118
|
+
ident_surround.save_local_vars(line)
|
|
119
|
+
"__yarsh__(#{line.strip.inspect}, #{is_structural ? 'bind' : 'nil'})"
|
|
120
|
+
end
|
|
121
|
+
end.join("\n")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.structural?(line)
|
|
125
|
+
# line.strip.match?(KEYWORD_REGEX)
|
|
126
|
+
Ripper.lex(line).any? { |token| STRUCTURAL_KEYWORDS.include?(token[2]) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def self.compiles?(src)
|
|
130
|
+
RubyVM::InstructionSequence.compile(src)
|
|
131
|
+
true
|
|
132
|
+
rescue SyntaxError
|
|
133
|
+
false
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def self.heredoc_delimiter(line)
|
|
137
|
+
return nil if line.include?('-->')
|
|
138
|
+
return nil unless line.include?('<<')
|
|
139
|
+
|
|
140
|
+
m = line.match(/<<(-|~)?(['"])([^'"]+)\2(?:\s*\.\w+)?\s*\z/)
|
|
141
|
+
return m[3] if m
|
|
142
|
+
|
|
143
|
+
m = line.match(/<<(-|~)?([A-Za-z_]\w*)(?:\s*\.\w+)?\s*\z/)
|
|
144
|
+
m && m[2]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
data/lib/yarsh/prompt.rb
CHANGED
|
@@ -8,8 +8,15 @@ module Yarsh
|
|
|
8
8
|
seg = Dir.pwd.gsub(/^#{Dir.home}/, '~').split('/')
|
|
9
9
|
seg = (seg.size > 3 ? [' …'] + seg[-3..-1] : seg)
|
|
10
10
|
git_branch = ''
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
if File.directory?('.git')
|
|
12
|
+
git_branch = `git status --porcelain -b`
|
|
13
|
+
.split("\n")
|
|
14
|
+
&.first
|
|
15
|
+
&.gsub('## ', '')
|
|
16
|
+
&.split('...', 2)
|
|
17
|
+
&.first || ''
|
|
18
|
+
end
|
|
19
|
+
git_branch = ' ' + git_branch unless git_branch.empty?
|
|
13
20
|
c.bg_color256(base_color)
|
|
14
21
|
.fg_white("#{seg.join(' ')}#{git_branch}")
|
|
15
22
|
.reset
|
data/lib/yarsh/shell.rb
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Yarsh
|
|
3
4
|
class Shell
|
|
4
5
|
include Yarsh
|
|
5
6
|
include InstanceMethods
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
attr_accessor :bind
|
|
9
|
+
|
|
10
|
+
SHELL_BUILTINS = %w[alias bg bind break builtin command echo eval exec exit
|
|
8
11
|
export false fc fg getopts hash help history jobs kill let
|
|
9
12
|
local logout mapfile popd printf pushd pwd read readonly
|
|
10
13
|
return set shift shopt source suspend test times trap true
|
|
@@ -16,8 +19,10 @@ module Yarsh
|
|
|
16
19
|
@history_file = File.expand_path(HISTORY_FILE)
|
|
17
20
|
@config_file = File.expand_path(CONFIG_FILE)
|
|
18
21
|
@cf = Yarsh::AnsiColorFormatter.new
|
|
22
|
+
Yarsh.current_shell = self
|
|
19
23
|
setup_reline_history
|
|
20
24
|
setup_reline_completion
|
|
25
|
+
setup_reline_prompt
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
def shell
|
|
@@ -25,7 +30,7 @@ module Yarsh
|
|
|
25
30
|
load @config_file, InstanceMethods if File.exist? @config_file
|
|
26
31
|
loop do
|
|
27
32
|
input = begin
|
|
28
|
-
|
|
33
|
+
read_input
|
|
29
34
|
rescue Interrupt
|
|
30
35
|
next
|
|
31
36
|
end
|
|
@@ -37,42 +42,7 @@ module Yarsh
|
|
|
37
42
|
append_history(input, @history_file)
|
|
38
43
|
|
|
39
44
|
begin
|
|
40
|
-
|
|
41
|
-
fp execute_pipe(input, @bind)
|
|
42
|
-
next
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
if shell_path?(input)
|
|
46
|
-
execute(input)
|
|
47
|
-
next
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
if input == 'cd' || input.start_with?('cd ')
|
|
51
|
-
cd(input.split(' ', 2)[1])
|
|
52
|
-
next
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
begin
|
|
56
|
-
result = @bind.eval(input)
|
|
57
|
-
fp result
|
|
58
|
-
rescue NameError, SyntaxError, ArgumentError => rb_error
|
|
59
|
-
unless shell_command?(input) || rb_error.is_a?(NameError) || rb_error.is_a?(SyntaxError)
|
|
60
|
-
$rber = rb_error
|
|
61
|
-
puts @cf.fg_red("Error: ArgumentError: #{rb_error.message}")
|
|
62
|
-
next
|
|
63
|
-
end
|
|
64
|
-
info("Assuming it is not a ruby expression, the error: #{rb_error.message}")
|
|
65
|
-
debug("The detail: #{rb_error.backtrace.join("\n")}")
|
|
66
|
-
begin
|
|
67
|
-
input = Yarsh.expand_aliases(input, Config.instance.aliases)
|
|
68
|
-
execute(input)
|
|
69
|
-
rescue Exception => e
|
|
70
|
-
$sher = e
|
|
71
|
-
info("#{e.class}: #{e.message}")
|
|
72
|
-
puts "#{@cf.bold.underline.fg_blue('Shell:')} '#{input}': #{e.message}"
|
|
73
|
-
puts "#{@cf.bold.underline.fg_red('Ruby:')} '#{input}': #{rb_error.message}"
|
|
74
|
-
end
|
|
75
|
-
end
|
|
45
|
+
fp execute_multiline(input)
|
|
76
46
|
rescue Interrupt
|
|
77
47
|
next
|
|
78
48
|
rescue SystemExit
|
|
@@ -91,6 +61,74 @@ module Yarsh
|
|
|
91
61
|
end
|
|
92
62
|
alias fp format_output
|
|
93
63
|
|
|
64
|
+
def read_input
|
|
65
|
+
@core.readmultiline(Config.instance.prompt, true) do |buffer|
|
|
66
|
+
!incomplete_line?(buffer) || buffer.match?(/\n[ \t]*\n\z/)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# A line is "incomplete" (multiline continues) only when it is not a
|
|
71
|
+
# shell command: `cd /tmp`, `grep foo /etc/passwd` etc. fail to compile
|
|
72
|
+
# as Ruby (unterminated regexp) but must stay single-line.
|
|
73
|
+
def incomplete_line?(line)
|
|
74
|
+
Yarsh::Multiline.incomplete?(line) && !shell_command?(line)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def execute_multiline(input)
|
|
78
|
+
source = Yarsh::Multiline.transform(input) { |line| shell_command?(line) }
|
|
79
|
+
debug("What I will evaluate as multiline:\n#{@cf.bold.fg_blue(source)}")
|
|
80
|
+
@bind.eval(source)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Evaluate a yarsh line. A yarsh line is a ruby expression or a shell syntax
|
|
84
|
+
def yarsh_eval(input)
|
|
85
|
+
debug("I want to evaluate the input: #{input}")
|
|
86
|
+
return execute_pipe(input, @bind) if input.include?('-->')
|
|
87
|
+
|
|
88
|
+
return execute(input) if shell_path?(input)
|
|
89
|
+
|
|
90
|
+
if input == 'cd' || input.start_with?('cd ')
|
|
91
|
+
cd(input.split(' ', 2)[1])
|
|
92
|
+
return nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
begin
|
|
96
|
+
@bind.eval(input)
|
|
97
|
+
rescue NameError, SyntaxError, ArgumentError => rb_error
|
|
98
|
+
unless shell_command?(input) || rb_error.is_a?(NameError) || rb_error.is_a?(SyntaxError)
|
|
99
|
+
$rber = rb_error
|
|
100
|
+
return @cf.fg_red("Error: ArgumentError: #{rb_error.message}")
|
|
101
|
+
end
|
|
102
|
+
info("Assuming it is not a ruby expression, the error: #{rb_error.message}")
|
|
103
|
+
debug("The detail: #{rb_error.backtrace.join("\n")}")
|
|
104
|
+
begin
|
|
105
|
+
input = Yarsh.expand_aliases(input, Config.instance.aliases)
|
|
106
|
+
execute(input)
|
|
107
|
+
rescue Exception => e
|
|
108
|
+
$sher = e
|
|
109
|
+
info("#{e.class}: #{e.message}")
|
|
110
|
+
puts "#{@cf.bold.underline.fg_blue('Shell:')} '#{input}': #{e.message}"
|
|
111
|
+
puts "#{@cf.bold.underline.fg_red('Ruby:')} '#{input}': #{rb_error.message}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Runs a shell line from inside a multiline Ruby buffer (via __sh__).
|
|
117
|
+
def __sh_exec(command)
|
|
118
|
+
command = Yarsh.expand_aliases(command, Config.instance.aliases)
|
|
119
|
+
if command.include?('-->')
|
|
120
|
+
puts execute_pipe(command, @bind)
|
|
121
|
+
else
|
|
122
|
+
execute(command)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def setup_reline_prompt
|
|
127
|
+
Reline.prompt_proc = proc do |lines|
|
|
128
|
+
[Config.instance.prompt] + Array.new([lines.size - 1, 0].max, ' > ')
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
94
132
|
def cd(path = '~')
|
|
95
133
|
Dir.chdir(File.expand_path(path || '~'))
|
|
96
134
|
end
|
|
@@ -177,7 +215,30 @@ module Yarsh
|
|
|
177
215
|
|
|
178
216
|
def setup_reline_history
|
|
179
217
|
Dir.mkdir(File.dirname(@history_file)) unless Dir.exist?(File.dirname(@history_file))
|
|
180
|
-
|
|
218
|
+
return unless File.exist?(@history_file)
|
|
219
|
+
|
|
220
|
+
pending = nil
|
|
221
|
+
File.readlines(@history_file, chomp: true).each do |line|
|
|
222
|
+
next if line.strip.empty?
|
|
223
|
+
|
|
224
|
+
unless pending
|
|
225
|
+
pending = line
|
|
226
|
+
next
|
|
227
|
+
end
|
|
228
|
+
if incomplete_line?(pending)
|
|
229
|
+
candidate = "#{pending}\n#{line}"
|
|
230
|
+
if Yarsh::Multiline.incomplete?(candidate)
|
|
231
|
+
pending = candidate
|
|
232
|
+
else
|
|
233
|
+
Reline::HISTORY << candidate
|
|
234
|
+
pending = nil
|
|
235
|
+
end
|
|
236
|
+
else
|
|
237
|
+
Reline::HISTORY << pending
|
|
238
|
+
pending = line
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
Reline::HISTORY << pending if pending
|
|
181
242
|
end
|
|
182
243
|
end
|
|
183
244
|
end
|
data/lib/yarsh/version.rb
CHANGED
data/lib/yarsh.rb
CHANGED
|
@@ -4,13 +4,15 @@ require 'reline'
|
|
|
4
4
|
require 'open3'
|
|
5
5
|
require 'singleton'
|
|
6
6
|
require 'logger'
|
|
7
|
+
require 'ripper'
|
|
8
|
+
require 'prism'
|
|
7
9
|
|
|
8
10
|
module Yarsh
|
|
9
11
|
class Error < StandardError; end
|
|
10
12
|
end
|
|
11
13
|
|
|
12
|
-
Dir['
|
|
13
|
-
require_relative
|
|
14
|
+
Dir[File.expand_path('**/*.rb', __dir__)].each do |f|
|
|
15
|
+
require_relative f
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
module Yarsh
|
|
@@ -34,6 +36,14 @@ module Yarsh
|
|
|
34
36
|
|
|
35
37
|
class Error < StandardError; end
|
|
36
38
|
|
|
39
|
+
def self.current_shell
|
|
40
|
+
@current_shell
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.current_shell=(shell)
|
|
44
|
+
@current_shell = shell
|
|
45
|
+
end
|
|
46
|
+
|
|
37
47
|
# in a Shell input, find all command matching the keys of aliases and replace them with
|
|
38
48
|
# the values. Leading and trailing whitespace around the match is preserved.
|
|
39
49
|
def self.expand_aliases(input, aliases)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yarsh
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- yarsh4all
|
|
@@ -44,8 +44,10 @@ files:
|
|
|
44
44
|
- exe/yarsh
|
|
45
45
|
- lib/yarsh.rb
|
|
46
46
|
- lib/yarsh/ansi_color_formatter.rb
|
|
47
|
+
- lib/yarsh/binding.rb
|
|
47
48
|
- lib/yarsh/config.rb
|
|
48
49
|
- lib/yarsh/instance_methods.rb
|
|
50
|
+
- lib/yarsh/multiline.rb
|
|
49
51
|
- lib/yarsh/prompt.rb
|
|
50
52
|
- lib/yarsh/shell.rb
|
|
51
53
|
- lib/yarsh/version.rb
|