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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5273b4e67dcb97cfa9dd5af63b83cfe7aa1d47cf27803baf27a0e35544346c19
4
- data.tar.gz: 195f587ad4ee4dda6890520fdf2f2be20c7bdb2a6ab92f6d709dc24aaac5a7ad
3
+ metadata.gz: 5c0badb7f9f245222eb3ff552fff43848240d14e3e8d8efddae83004a77d72ab
4
+ data.tar.gz: d2920f423aac65f6dec454ebf84104fb97c60923358790440463bee0ada9cd8b
5
5
  SHA512:
6
- metadata.gz: d0b448b5c4fbdec98e05c3519989e5ff74f8b8a7a5372a7d8772196ced90b15a64a5be7b97af6a7b954d9c08d046172e1c60ebb348018552f56fd09ca85aa8ee
7
- data.tar.gz: b177ff3a557c6acdf842e2e29af9dcba09a118926b7d43c7d74507e5877952bef9bdf5faeab6b5f8f210d45ad1e2ce13eb8f01bf5593e08dbbabb464d42aa767
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
- - **Main logic** lives in `lib/yarsh.rb` (module `Yarsh` with `self.shell`,
19
- `self.console`, `self.execute`, `self.shell_path?`, `self.execute_pipe`, etc.).
20
- `bin/console` calls `Yarsh.console` (IRB session); `exe/yarsh` calls `Yarsh.shell`.
21
- - `exe/yarsh` is the gem executable (packaged via `spec.bindir = "exe"` in the
22
- gemspec). `bin/console` is for development only (adds `bundler/setup`).
23
- - `Yarsh.shell_path?` recognizes absolute/relative/home paths for shell execution.
24
- - `Yarsh.execute_pipe` enables piping shell output into Ruby via the `-->` operator.
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
- ## REPL evaluation flow
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
- All steps below are wrapped in a single `begin..rescue` so no error crashes the REPL:
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
- 1. `input.include?("-->")` `Yarsh.execute_pipe(input)`, print result
47
- 2. `Yarsh.shell_path?(input)` execute directly via `bash -c`
48
- 3. `bind.eval(input)` — Ruby evaluation
49
- 4. `NameError` → if first token is `cd`, call `Dir.chdir`; else shell-execute
50
- 5. Other exceptions print `Error: ExceptionClass: message`
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
- - **Bug:** `Yarsh.execute` (in `lib/yarsh.rb`) uses `system("bash -c 'source
64
- ~/.bashrc; #{cmd}'")` the command is passed correctly, but this depends on
65
- `~/.bashrc` existing.
66
- - **Dead code:** `Yarsh.execute_system_command` (Open3 version) is defined but
67
- never called. `Yarsh.shell` calls `Yarsh.execute`, not
68
- `Yarsh.execute_system_command`.
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
- (53032be) Commands are saved to `~/.yarsh/history` across sessions. Max 1000 lines;
29
+ Commands are saved to `~/.yarsh/history` across sessions. Max 1000 lines;
30
30
  concurrent-session safe via `File.flock`.
31
- (77af3b8) Add tests for append_history: creation, append, trim, concurrent safety.
31
+ Add tests for append_history: creation, append, trim, concurrent safety.
32
32
 
33
33
  ### `bin/console` launches an IRB session
34
34
 
35
- (33f86c8) `bin/console` now starts a standard IRB session with the gem loaded
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
- (13655c4) Tests split per class: `test_yarsh.rb` (module-level: constants,
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
- (e3d1f44) `test_console_launches_irb_session` no longer uses
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
- (ab3998e) Registering the first alias via `sh_alias`/`add_alias` ran the
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
- (4f0364e) `Config#prompt=` raises the new `Yarsh::InvalidPromptError`
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
- (d867a11) Besides the existing `debug`, the REPL now exposes `info`,
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
- (3883da3) New `fg_color256`/`bg_color256` emit 8-bit indexed color
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
- (13ea85f) Shell commands like `bundle exec rake test` raise
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
- (9009bfb) The project is now called **yarsh** throughout: module `Rsh`
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. Version stays 0.1.0.
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, multi-line editing or non interactive with a file as argument. I dream of a shell syntax we can do:
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
@@ -0,0 +1,10 @@
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
+ end
@@ -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
- git_branch = `git status --porcelain -b`.split("\n")&.first&.gsub('## ', '') || '' if File.directory?('.git')
12
- git_branch = ' ' + git_branch unless git_branch.empty?
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
- SHELL_BUILTINS = %w[alias bg bind break builtin cd command echo eval exec exit
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
- @core.readline(Config.instance.prompt, true)
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
- if input.include?('-->')
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
- File.readlines(@history_file, chomp: true).each { |line| Reline::HISTORY << line } if File.exist?(@history_file)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yarsh
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
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['lib/yarsh/**/*.rb'].each do |f|
13
- require_relative File.join('..', f)
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.1.1
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