yarsh 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +75 -0
- data/CHANGES.md +114 -0
- data/README.md +99 -0
- data/Rakefile +8 -0
- data/exe/yarsh +7 -0
- data/lib/yarsh/ansi_color_formatter.rb +105 -0
- data/lib/yarsh/config.rb +47 -0
- data/lib/yarsh/instance_methods.rb +26 -0
- data/lib/yarsh/prompt.rb +25 -0
- data/lib/yarsh/shell.rb +183 -0
- data/lib/yarsh/version.rb +5 -0
- data/lib/yarsh.rb +80 -0
- data/sig/yarsh.rbs +4 -0
- metadata +74 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 39081c43d270c8a1e33da05ca87657956be5dd589f2949f16cee16accf873745
|
|
4
|
+
data.tar.gz: 974a89cab7ce728724ff746d4b6febad7ffa4b8b97bd39fd30b0fe98a09ef78b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f69773fa5dcb457c89fd2559cabf7c031924bc6f99324d1df37a0e3556f5bea049de18ff09abd8f273c029b10f9e567c7c4080bb1c285cc0d2dbf854cebbfe73
|
|
7
|
+
data.tar.gz: ba74d4bb610fb0462f75ec0935be39f41e1f52f0fba1d73c092ab11a3ec1878d14dccf9e537df5006aa437894dda82871ab6a1380295e89280f2566ef1b80532
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.2
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# yarsh — Ruby Shell
|
|
2
|
+
|
|
3
|
+
A hybrid Ruby/shell interactive REPL. Input is evaluated as Ruby first; on
|
|
4
|
+
`NameError` it falls through to `bash -c`.
|
|
5
|
+
|
|
6
|
+
## Commands
|
|
7
|
+
|
|
8
|
+
| Command | Purpose |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `bin/setup` or `bundle install` | Install dependencies |
|
|
11
|
+
| `bin/console` | Launch an IRB session (development, uses `bundler/setup`) |
|
|
12
|
+
| `bundle exec ruby exe/yarsh` | Launch the REPL via the gem executable |
|
|
13
|
+
| `rake test` | Run all Minitest tests (default task) |
|
|
14
|
+
| `bundle exec rake install` | Build + install the gem locally |
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
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.
|
|
25
|
+
|
|
26
|
+
## `-->` pipe operator
|
|
27
|
+
|
|
28
|
+
Pipe shell stdout into Ruby code: `ls -al --> split("\n").each {|f| puts f }`
|
|
29
|
+
|
|
30
|
+
The shell command runs, its stdout is captured (stripped of trailing newline),
|
|
31
|
+
and the Ruby code after `-->` is evaluated via `instance_eval` on the captured
|
|
32
|
+
string. The variable `_` is also set to the output for explicit references.
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
|
|
36
|
+
| Input | Result |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `echo hello --> upcase` | `"HELLO"` |
|
|
39
|
+
| `ls --> lines.count` | (file count) |
|
|
40
|
+
| `cat data.txt --> _.split("\n").grep(/error/)` | matching lines |
|
|
41
|
+
|
|
42
|
+
## REPL evaluation flow
|
|
43
|
+
|
|
44
|
+
All steps below are wrapped in a single `begin..rescue` so no error crashes the REPL:
|
|
45
|
+
|
|
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`
|
|
51
|
+
|
|
52
|
+
## Error handling
|
|
53
|
+
|
|
54
|
+
- **Ctrl+C** (`Interrupt`) during command or eval → prints newline, returns to prompt
|
|
55
|
+
- **Ctrl+D** (EOF) or `exit` → prints "Bye..." and exits
|
|
56
|
+
- `SyntaxError`, `LoadError`, `SystemStackError`, `NoMemoryError`, and all other
|
|
57
|
+
`Exception` subclasses are caught and displayed — the REPL never crashes from
|
|
58
|
+
bad input
|
|
59
|
+
- Shell execution failure (`Errno::ENOENT`, etc.) is caught and printed
|
|
60
|
+
|
|
61
|
+
## Known issues
|
|
62
|
+
|
|
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`.
|
|
69
|
+
|
|
70
|
+
## Style
|
|
71
|
+
|
|
72
|
+
- `# frozen_string_literal: true` on every `.rb` file.
|
|
73
|
+
- Minitest for testing. Run individual test files via
|
|
74
|
+
`bundle exec ruby -Ilib -Itest test/<file>.rb`.
|
|
75
|
+
- No linter (RuboCop), no CI, no formatter config present.
|
data/CHANGES.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Auto-completion for shell commands and paths
|
|
6
|
+
|
|
7
|
+
Tab-completion is now available in the REPL via Reline's `completion_proc`.
|
|
8
|
+
Pressing Tab completes commands from `PATH` when typing the first word, and
|
|
9
|
+
falls back to filesystem path completion for arguments. Path completion
|
|
10
|
+
includes dotfiles and appends `/` to directories for easy traversal.
|
|
11
|
+
|
|
12
|
+
### Ctrl+C no longer interrupts the shell
|
|
13
|
+
|
|
14
|
+
Pressing Ctrl+C during input or command execution now safely returns to the
|
|
15
|
+
prompt instead of crashing the REPL.
|
|
16
|
+
|
|
17
|
+
### Shell commands no longer evaluated as Ruby
|
|
18
|
+
|
|
19
|
+
Input that raises `SyntaxError` (e.g. `git add .`) is now caught and executed as
|
|
20
|
+
a shell command, matching the existing `NameError` fallback behavior.
|
|
21
|
+
|
|
22
|
+
### `cd` no longer prints `0` to stdout
|
|
23
|
+
|
|
24
|
+
`cd` (with or without arguments) is now intercepted before Ruby eval, preventing
|
|
25
|
+
`Dir.chdir`'s return value (`0`) from being printed.
|
|
26
|
+
|
|
27
|
+
### REPL history persisted to `~/.yarsh/history`
|
|
28
|
+
|
|
29
|
+
(53032be) Commands are saved to `~/.yarsh/history` across sessions. Max 1000 lines;
|
|
30
|
+
concurrent-session safe via `File.flock`.
|
|
31
|
+
(77af3b8) Add tests for append_history: creation, append, trim, concurrent safety.
|
|
32
|
+
|
|
33
|
+
### `bin/console` launches an IRB session
|
|
34
|
+
|
|
35
|
+
(33f86c8) `bin/console` now starts a standard IRB session with the gem loaded
|
|
36
|
+
(`Yarsh.console`). The hybrid REPL remains available via `bundle exec ruby exe/yarsh`
|
|
37
|
+
(`Yarsh.shell`).
|
|
38
|
+
|
|
39
|
+
### Test suite adapted to multi-file architecture
|
|
40
|
+
|
|
41
|
+
(13655c4) Tests split per class: `test_yarsh.rb` (module-level: constants,
|
|
42
|
+
`ExecOutput`, `console`), `test_shell.rb` (`Yarsh::Shell`: `shell_path?`, `cd`,
|
|
43
|
+
completion, history, `execute_pipe` incl. bind-var feature), `test_config.rb`
|
|
44
|
+
(`Yarsh::Config`: prompt, `log_level`), `test_prompt.rb` (`Prompt`,
|
|
45
|
+
`AnsiColorFormatter`). `test_expand_aliases` skipped (WIP). Also fixes inverted
|
|
46
|
+
validation in `Config#prompt=`.
|
|
47
|
+
|
|
48
|
+
### Console test works under minitest 6
|
|
49
|
+
|
|
50
|
+
(e3d1f44) `test_console_launches_irb_session` no longer uses
|
|
51
|
+
`IRB.stub` (`minitest/mock` was removed in minitest 6, breaking plain
|
|
52
|
+
`rake test`); it now stubs `IRB.start` via `define_singleton_method`.
|
|
53
|
+
README documents running tests with `bundle exec rake test`.
|
|
54
|
+
|
|
55
|
+
### `expand_aliases` no longer strips whitespace
|
|
56
|
+
|
|
57
|
+
(ab3998e) Registering the first alias via `sh_alias`/`add_alias` ran the
|
|
58
|
+
command through `expand_aliases` with an empty alias hash; the empty
|
|
59
|
+
alternation degraded the regex to `(^| +)() *` and every space was
|
|
60
|
+
deleted (`exa -l --icons=always` was stored as `exa-l--icons=always`).
|
|
61
|
+
Now returns the input unchanged when no aliases are registered,
|
|
62
|
+
`Regexp.escape`s alias names, uses a word-boundary lookahead (so `ll`
|
|
63
|
+
no longer matches inside `lll`), and preserves whitespace around
|
|
64
|
+
replaced terms. Debug prints removed; `test_expand_aliases` un-skipped
|
|
65
|
+
with a regression test for the empty-alias case.
|
|
66
|
+
|
|
67
|
+
### `Yarsh::InvalidPromptError` for invalid prompt configuration
|
|
68
|
+
|
|
69
|
+
(4f0364e) `Config#prompt=` raises the new `Yarsh::InvalidPromptError`
|
|
70
|
+
(subclass of `Yarsh::Error`) instead of a bare `StandardError`. The
|
|
71
|
+
message is a constructor argument with the documented default.
|
|
72
|
+
`Yarsh::Error` is now defined before sub-files are required, fixing a
|
|
73
|
+
load-order `NameError` when subclassing it from `config.rb`. Tests
|
|
74
|
+
assert the error class, the default message, and custom messages.
|
|
75
|
+
|
|
76
|
+
### Logging methods in `InstanceMethods`
|
|
77
|
+
|
|
78
|
+
(d867a11) Besides the existing `debug`, the REPL now exposes `info`,
|
|
79
|
+
`warn`, `error`, `fatal`, and `unknown` — each delegating to
|
|
80
|
+
`Config.instance.logger`, covering all `Logger` severities. Methods are
|
|
81
|
+
defined dynamically from the `SEVERITIES` constant
|
|
82
|
+
(`%w[DEBUG INFO WARN ERROR FATAL UNKNOWN]`).
|
|
83
|
+
|
|
84
|
+
### 256-color and text styles in `AnsiColorFormatter`
|
|
85
|
+
|
|
86
|
+
(3883da3) New `fg_color256`/`bg_color256` emit 8-bit indexed color
|
|
87
|
+
codes (`38;5;N` / `48;5;N`) and raise `Yarsh::Error` for indices outside
|
|
88
|
+
0..255. Text styles are defined dynamically from `STYLE_CODES`: `bold`,
|
|
89
|
+
`dim`, `italic`, `underline`, `blink`, `reverse`, `hidden`,
|
|
90
|
+
`strikethrough` — mirroring the `bg_*`/`fg_*` pattern. `print_color`
|
|
91
|
+
now tolerates calls with neither text nor a block, so bare style calls
|
|
92
|
+
(`formatter.bold.fg_red('x')`) chain correctly.
|
|
93
|
+
|
|
94
|
+
### Real `ArgumentError`s are no longer shell-executed
|
|
95
|
+
|
|
96
|
+
(13ea85f) Shell commands like `bundle exec rake test` raise
|
|
97
|
+
`ArgumentError` (the innermost call resolves to `Kernel#test`, which
|
|
98
|
+
requires 2 args). The fallback previously treated every `ArgumentError`
|
|
99
|
+
as a shell command, so genuine Ruby errors (`"x".sub`) were
|
|
100
|
+
shell-executed too. A new `shell_command?` predicate (first token in
|
|
101
|
+
`PATH`, a bash builtin, or a shell path) decides: `ArgumentError` falls
|
|
102
|
+
through to the shell only when the input looks like a command,
|
|
103
|
+
otherwise the real Ruby error is reported. `NameError` and `SyntaxError`
|
|
104
|
+
keep their unconditional shell fallback.
|
|
105
|
+
|
|
106
|
+
### Renamed from `rsh` to `yarsh`
|
|
107
|
+
|
|
108
|
+
(9009bfb) The project is now called **yarsh** throughout: module `Rsh`
|
|
109
|
+
→ `Yarsh`, gem name `yarsh` (`spec.name`, require paths), data dir
|
|
110
|
+
`~/.rsh` → `~/.yarsh`, executable `exe/ruby-shell` → `exe/yarsh`.
|
|
111
|
+
Files renamed: `rsh.gemspec`, `lib/rsh.rb`, `lib/rsh/`,
|
|
112
|
+
`sig/rsh.rbs`, `test/test_rsh.rb`. All references updated (tests,
|
|
113
|
+
`bin/console`, README, AGENTS.md, CHANGES.md); `Gemfile.lock`
|
|
114
|
+
regenerated. Version stays 0.1.0.
|
data/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Yarsh
|
|
2
|
+
|
|
3
|
+
Yet Another Ruby SHell is a mix of Ruby and standard Shell commands. Priority is given to Ruby but if the variable or method is not found or if it is a syntax error, it will be passed to the default shell environment. It enable the best of both worlds, the power of ruby and the simplicity of the standard Shell.
|
|
4
|
+
|
|
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
|
+
|
|
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:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
def install_rails
|
|
11
|
+
sudo apt update
|
|
12
|
+
sudo apt install ruby
|
|
13
|
+
gem install rails
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
install_rails
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
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
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
`yarsh` start the shell.
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
Use it as a normal shell, except that you can use Ruby commands and methods. For example:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
puts "Hello World! We can use Ruby or standard shell:"
|
|
30
|
+
Dir["**/*.rb"].each { |file| puts file }
|
|
31
|
+
puts "Or shell like:"
|
|
32
|
+
find . -name "*.rb" -print
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For the moment, it display ruby evaluation results with `pretty_print`.
|
|
36
|
+
|
|
37
|
+
### Special keyword for mixing
|
|
38
|
+
|
|
39
|
+
You can also mix both with the special keyword `-->`. It will pass to ruby the result of the shell command to use in Ruby. For example:
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
ls -al . --> lines.each { |file| puts file }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
the result of the exectued shell command is `ExecOutput` a subclass of `String` with some extra methods. For the moment, it has:
|
|
47
|
+
|
|
48
|
+
* `#lines`
|
|
49
|
+
* `#words`
|
|
50
|
+
|
|
51
|
+
You want to store the result of the ruby evaluation after a shell command? Use `-->(varible_name)`. For example:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
ls -al . -->(sorted) lines.sort {|a, b| a.length <=> b.length }
|
|
55
|
+
sorted.each { |file| puts "What ever you wanna do" }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Completion
|
|
59
|
+
|
|
60
|
+
For the moment, there is autocompletion for exectuables in PATH and for file and dir path for shell commands
|
|
61
|
+
|
|
62
|
+
### Errors
|
|
63
|
+
|
|
64
|
+
`$rberr` are set for every catched Ruby error.
|
|
65
|
+
|
|
66
|
+
`$sherr` are set for every catched Shell error.
|
|
67
|
+
|
|
68
|
+
### Configuration
|
|
69
|
+
|
|
70
|
+
You can create a file named `config` in `~/.yarsh`. It is plain ruby, for now, you can define aliases for shell commands, define methods that will be available in the REPL, configure the prompt looks like and some logging. Here is an example file:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
|
|
74
|
+
sh_alias 'ls', 'exa -l --icons=always'
|
|
75
|
+
sh_alias 'll', 'ls -a' # Will be expanded to exa -l --icons=always -a
|
|
76
|
+
|
|
77
|
+
configure do |c|
|
|
78
|
+
c.prompt = :powerline # To available prompt: :powerline or :basic
|
|
79
|
+
# Or define how your own logic for the prompt. Should return a string
|
|
80
|
+
c.prompt = Proc.new do
|
|
81
|
+
"hello> "
|
|
82
|
+
end
|
|
83
|
+
c.log_level = Logger::DEBUG # Lot of unuseful lines
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Will be available in the REPL once the related TODO is completed
|
|
87
|
+
def my_awsome_method
|
|
88
|
+
puts "Hello World"
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
## Development
|
|
92
|
+
|
|
93
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
94
|
+
|
|
95
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
96
|
+
|
|
97
|
+
## Contributing
|
|
98
|
+
|
|
99
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/facenord-sud/yarsh.
|
data/Rakefile
ADDED
data/exe/yarsh
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yarsh
|
|
4
|
+
class AnsiColorFormatter
|
|
5
|
+
BACKGROUND_CODES = {
|
|
6
|
+
black: "\e[40m",
|
|
7
|
+
red: "\e[41m",
|
|
8
|
+
green: "\e[42m",
|
|
9
|
+
yellow: "\e[43m",
|
|
10
|
+
blue: "\e[44m",
|
|
11
|
+
magenta: "\e[45m",
|
|
12
|
+
cyan: "\e[46m",
|
|
13
|
+
white: "\e[47m"
|
|
14
|
+
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
FORGROUND_CODES = {
|
|
18
|
+
black: "\e[30m",
|
|
19
|
+
red: "\e[31m",
|
|
20
|
+
green: "\e[32m",
|
|
21
|
+
yellow: "\e[33m",
|
|
22
|
+
blue: "\e[34m",
|
|
23
|
+
magenta: "\e[35m",
|
|
24
|
+
cyan: "\e[36m",
|
|
25
|
+
white: "\e[37m"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
STYLE_CODES = {
|
|
29
|
+
bold: "\e[1m",
|
|
30
|
+
dim: "\e[2m",
|
|
31
|
+
italic: "\e[3m",
|
|
32
|
+
underline: "\e[4m",
|
|
33
|
+
blink: "\e[5m",
|
|
34
|
+
reverse: "\e[7m",
|
|
35
|
+
hidden: "\e[8m",
|
|
36
|
+
strikethrough: "\e[9m"
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
RESET = "\e[0m"
|
|
40
|
+
|
|
41
|
+
BACKGROUND_CODES.each do |name, code|
|
|
42
|
+
define_method(:"bg_#{name}") do |text = nil, &block|
|
|
43
|
+
print_color(code, text, &block)
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
FORGROUND_CODES.each do |name, code|
|
|
49
|
+
define_method(:"fg_#{name}") do |text = nil, &block|
|
|
50
|
+
print_color(code, text, &block)
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
STYLE_CODES.each do |name, code|
|
|
56
|
+
define_method(name) do |text = nil, &block|
|
|
57
|
+
print_color(code, text, &block)
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def fg_color256(color, text = nil, &block)
|
|
63
|
+
validate_color256!(color)
|
|
64
|
+
print_color("\e[38;5;#{color}m", text, &block)
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def bg_color256(color, text = nil, &block)
|
|
69
|
+
validate_color256!(color)
|
|
70
|
+
print_color("\e[48;5;#{color}m", text, &block)
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def initialize
|
|
75
|
+
@formatted_text = ''
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def print_color(code, text = nil, &block)
|
|
79
|
+
content = text || (block && block.call).to_s
|
|
80
|
+
@formatted_text += code + content
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def validate_color256!(color)
|
|
86
|
+
return if (0..255).cover?(color)
|
|
87
|
+
|
|
88
|
+
raise Yarsh::Error, "Color must be between 0 and 255, got #{color}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
public
|
|
92
|
+
|
|
93
|
+
def to_s
|
|
94
|
+
return_text = @formatted_text
|
|
95
|
+
@formatted_text = ''
|
|
96
|
+
return_text + RESET
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def reset(text = nil)
|
|
100
|
+
@formatted_text += RESET
|
|
101
|
+
@formatted_text += text if text
|
|
102
|
+
self
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/yarsh/config.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yarsh
|
|
4
|
+
class InvalidPromptError < Yarsh::Error
|
|
5
|
+
DEFAULT_MESSAGE = 'Not a valid prompt type. Pass a method name of module Yarsh::Prompt or a block'
|
|
6
|
+
|
|
7
|
+
def initialize(message = DEFAULT_MESSAGE)
|
|
8
|
+
super(message)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class Config
|
|
13
|
+
include Singleton
|
|
14
|
+
|
|
15
|
+
attr_reader :aliases, :log_level
|
|
16
|
+
attr_accessor :logger
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@aliases = {}
|
|
20
|
+
@prompt = nil
|
|
21
|
+
@logger = Logger.new($stdout, level: Logger::UNKNOWN)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def log_level=(level)
|
|
25
|
+
@log_level = level
|
|
26
|
+
@logger.level = level
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add_alias(name, command)
|
|
30
|
+
expand_aliases = Yarsh.expand_aliases(command, @aliases)
|
|
31
|
+
@aliases[name.to_s] = expand_aliases
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def prompt
|
|
35
|
+
prompt_method = @prompt || :basic
|
|
36
|
+
return Prompt.method(prompt_method).call if prompt_method.is_a? Symbol
|
|
37
|
+
|
|
38
|
+
@prompt.call
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def prompt=(prompt)
|
|
42
|
+
raise InvalidPromptError unless prompt.respond_to?(:call) || Prompt.respond_to?(prompt)
|
|
43
|
+
|
|
44
|
+
@prompt = prompt
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yarsh
|
|
4
|
+
module InstanceMethods
|
|
5
|
+
SEVERITIES = %w[DEBUG INFO WARN ERROR FATAL UNKNOWN]
|
|
6
|
+
|
|
7
|
+
def sh_alias(new_name, command)
|
|
8
|
+
Config.instance.add_alias(new_name, command)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def source(filename)
|
|
12
|
+
load(filename, Yarsh::InstanceMethods)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def configure(&block)
|
|
16
|
+
block.call(Config.instance)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
SEVERITIES.each do |severity|
|
|
20
|
+
method_name = severity.downcase
|
|
21
|
+
define_method(method_name) do |message|
|
|
22
|
+
Config.instance.logger.public_send(method_name, message)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/yarsh/prompt.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yarsh
|
|
4
|
+
module Prompt
|
|
5
|
+
def self.powerline
|
|
6
|
+
c = Yarsh::AnsiColorFormatter.new
|
|
7
|
+
base_color = 240
|
|
8
|
+
seg = Dir.pwd.gsub(/^#{Dir.home}/, '~').split('/')
|
|
9
|
+
seg = (seg.size > 3 ? [' …'] + seg[-3..-1] : seg)
|
|
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?
|
|
13
|
+
c.bg_color256(base_color)
|
|
14
|
+
.fg_white("#{seg.join(' ')}#{git_branch}")
|
|
15
|
+
.reset
|
|
16
|
+
.fg_color256(base_color, '')
|
|
17
|
+
.reset(' ')
|
|
18
|
+
.to_s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.basic
|
|
22
|
+
"#{Dir.pwd}> "
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/yarsh/shell.rb
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Yarsh
|
|
3
|
+
class Shell
|
|
4
|
+
include Yarsh
|
|
5
|
+
include InstanceMethods
|
|
6
|
+
|
|
7
|
+
SHELL_BUILTINS = %w[alias bg bind break builtin cd command echo eval exec exit
|
|
8
|
+
export false fc fg getopts hash help history jobs kill let
|
|
9
|
+
local logout mapfile popd printf pushd pwd read readonly
|
|
10
|
+
return set shift shopt source suspend test times trap true
|
|
11
|
+
type typeset ulimit umask unalias unset wait].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(bind)
|
|
14
|
+
@bind = bind
|
|
15
|
+
@core = Reline.core
|
|
16
|
+
@history_file = File.expand_path(HISTORY_FILE)
|
|
17
|
+
@config_file = File.expand_path(CONFIG_FILE)
|
|
18
|
+
@cf = Yarsh::AnsiColorFormatter.new
|
|
19
|
+
setup_reline_history
|
|
20
|
+
setup_reline_completion
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def shell
|
|
24
|
+
# TODO: defined method in the config file should be availabel in the shell
|
|
25
|
+
load @config_file, InstanceMethods if File.exist? @config_file
|
|
26
|
+
loop do
|
|
27
|
+
input = begin
|
|
28
|
+
@core.readline(Config.instance.prompt, true)
|
|
29
|
+
rescue Interrupt
|
|
30
|
+
next
|
|
31
|
+
end
|
|
32
|
+
if input.nil? or input == 'exit'
|
|
33
|
+
puts 'Bye...'
|
|
34
|
+
break
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
append_history(input, @history_file)
|
|
38
|
+
|
|
39
|
+
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
|
|
76
|
+
rescue Interrupt
|
|
77
|
+
next
|
|
78
|
+
rescue SystemExit
|
|
79
|
+
raise
|
|
80
|
+
rescue Exception => e
|
|
81
|
+
$rber = e
|
|
82
|
+
puts @cf.fg_red("Error: #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def format_output(object)
|
|
90
|
+
pp object
|
|
91
|
+
end
|
|
92
|
+
alias fp format_output
|
|
93
|
+
|
|
94
|
+
def cd(path = '~')
|
|
95
|
+
Dir.chdir(File.expand_path(path || '~'))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def execute(cmd)
|
|
99
|
+
system(cmd, exception: true)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def shell_command?(input)
|
|
103
|
+
first_token = input.strip.split(/\s+/, 2).first
|
|
104
|
+
return true if shell_path?(first_token)
|
|
105
|
+
return true if SHELL_BUILTINS.include?(first_token)
|
|
106
|
+
|
|
107
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).any? do |dir|
|
|
108
|
+
File.executable?(File.join(dir, first_token))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def complete_command(word)
|
|
113
|
+
ENV['PATH'].split(File::PATH_SEPARATOR)
|
|
114
|
+
.flat_map { |dir| Dir["#{dir}/#{word}*"] }
|
|
115
|
+
.map { |f| File.basename(f) }
|
|
116
|
+
.uniq
|
|
117
|
+
.sort
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def complete_path(word)
|
|
121
|
+
word = './' if word.empty?
|
|
122
|
+
glob_word = word.start_with?('~') ? File.expand_path(word) : word
|
|
123
|
+
entries = Dir.glob("#{glob_word}*").sort
|
|
124
|
+
entries = entries.map { |e| File.directory?(e) ? "#{e}/" : e }
|
|
125
|
+
if word.start_with?('~')
|
|
126
|
+
home = ENV['HOME']
|
|
127
|
+
entries.map { |e| e.sub(/\A#{Regexp.escape(home)}/, '~') }
|
|
128
|
+
else
|
|
129
|
+
entries
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def append_history(input, history_file, max_lines = 1000)
|
|
134
|
+
File.open(history_file, File::RDWR | File::CREAT) do |f|
|
|
135
|
+
f.flock(File::LOCK_EX)
|
|
136
|
+
f.seek(0, IO::SEEK_END)
|
|
137
|
+
f.puts(input)
|
|
138
|
+
f.rewind
|
|
139
|
+
lines = f.readlines(chomp: true)
|
|
140
|
+
if lines.size > max_lines
|
|
141
|
+
trimmed = lines.last(max_lines)
|
|
142
|
+
f.rewind
|
|
143
|
+
f.truncate(0)
|
|
144
|
+
trimmed.each { |l| f.puts(l) }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def execute_pipe(input, bind)
|
|
150
|
+
cmd, ruby_code = input.split('-->', 2).map(&:strip)
|
|
151
|
+
bind_var = nil
|
|
152
|
+
ruby_code.gsub!(/\(\w+\)( )*/) do |m|
|
|
153
|
+
bind_var = m.match(/\w+/)
|
|
154
|
+
''
|
|
155
|
+
end
|
|
156
|
+
cmd = Yarsh.expand_aliases(cmd, Config.instance.aliases)
|
|
157
|
+
output = ExecOutput.new(`#{cmd} 2>&1`.chomp).instance_eval("_ = self\n#{ruby_code}", __FILE__, __LINE__)
|
|
158
|
+
bind_var = bind_var&.[](0)&.to_sym
|
|
159
|
+
bind.local_variable_set(bind_var, output) unless bind_var.nil?
|
|
160
|
+
output
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def setup_reline_completion
|
|
164
|
+
Reline.completion_proc = proc { |target, pre|
|
|
165
|
+
# puts "target='#{target}' pre='#{pre}'"
|
|
166
|
+
if target.match?(%r{\A\.\.?(/|\z)}) || target.start_with?('/', '~') || target.include?('/')
|
|
167
|
+
complete_path(target)
|
|
168
|
+
elsif target.empty? && pre.empty?
|
|
169
|
+
complete_command(target)
|
|
170
|
+
elsif pre.end_with?(' ')
|
|
171
|
+
complete_path(target)
|
|
172
|
+
else
|
|
173
|
+
complete_command(target)
|
|
174
|
+
end
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def setup_reline_history
|
|
179
|
+
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)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
data/lib/yarsh.rb
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'reline'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'singleton'
|
|
6
|
+
require 'logger'
|
|
7
|
+
|
|
8
|
+
module Yarsh
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
Dir['lib/yarsh/**/*.rb'].each do |f|
|
|
13
|
+
require_relative File.join('..', f)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module Yarsh
|
|
17
|
+
YARSH_DIR = '~/.yarsh'
|
|
18
|
+
HISTORY_FILE = File.join Yarsh::YARSH_DIR, 'history'
|
|
19
|
+
CONFIG_FILE = File.join Yarsh::YARSH_DIR, 'config'
|
|
20
|
+
|
|
21
|
+
class ExecOutput < String
|
|
22
|
+
def initialize(string)
|
|
23
|
+
super(string)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def lines
|
|
27
|
+
split("\n")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def words
|
|
31
|
+
split(' ')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class Error < StandardError; end
|
|
36
|
+
|
|
37
|
+
# in a Shell input, find all command matching the keys of aliases and replace them with
|
|
38
|
+
# the values. Leading and trailing whitespace around the match is preserved.
|
|
39
|
+
def self.expand_aliases(input, aliases)
|
|
40
|
+
return input if aliases.empty?
|
|
41
|
+
|
|
42
|
+
terms = aliases.keys.map { |k| Regexp.escape(k.to_s) }.join('|')
|
|
43
|
+
regex = Regexp.new(/(^| +)(#{terms})(?![A-Za-z0-9_])/)
|
|
44
|
+
input.gsub(regex) do |match|
|
|
45
|
+
rep = aliases[match.strip]
|
|
46
|
+
next match unless rep
|
|
47
|
+
|
|
48
|
+
match[/\A( +)/, 1].to_s + rep
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def shell_path?(input)
|
|
53
|
+
input.match?(%r{\A\.\.?/}) ||
|
|
54
|
+
input.match?(%r{\A/}) ||
|
|
55
|
+
input.match?(/\A~/)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.execute_system_command(command)
|
|
59
|
+
Open3.popen3(command) do |input, output, error, wait_thread|
|
|
60
|
+
output_printer = Thread.new do
|
|
61
|
+
output.read.tap { |data| print data }
|
|
62
|
+
end
|
|
63
|
+
error_printer = Thread.new do
|
|
64
|
+
error.read.tap { |data| $stderr.print data }
|
|
65
|
+
end
|
|
66
|
+
Thread.new do
|
|
67
|
+
input.write($stdin.read)
|
|
68
|
+
end
|
|
69
|
+
output_printer.join
|
|
70
|
+
error_printer.join
|
|
71
|
+
|
|
72
|
+
wait_thread.value
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.console
|
|
77
|
+
require 'irb'
|
|
78
|
+
IRB.start(__FILE__)
|
|
79
|
+
end
|
|
80
|
+
end
|
data/sig/yarsh.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: yarsh
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- yarsh4all
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: logger
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 1.7.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 1.7.0
|
|
26
|
+
description: |2
|
|
27
|
+
Yet Another Ruby SHell is a mix of Ruby and standard Shell commands.
|
|
28
|
+
Priority is given to Ruby but if the variable or method is not found or
|
|
29
|
+
if it is a syntax error, it will be passed to the default shell environment.
|
|
30
|
+
It enable the best of both worlds, the power of ruby and the simplicity
|
|
31
|
+
of the standard Shell.
|
|
32
|
+
email:
|
|
33
|
+
- yarsh4all@gmail.com
|
|
34
|
+
executables:
|
|
35
|
+
- yarsh
|
|
36
|
+
extensions: []
|
|
37
|
+
extra_rdoc_files: []
|
|
38
|
+
files:
|
|
39
|
+
- ".ruby-version"
|
|
40
|
+
- AGENTS.md
|
|
41
|
+
- CHANGES.md
|
|
42
|
+
- README.md
|
|
43
|
+
- Rakefile
|
|
44
|
+
- exe/yarsh
|
|
45
|
+
- lib/yarsh.rb
|
|
46
|
+
- lib/yarsh/ansi_color_formatter.rb
|
|
47
|
+
- lib/yarsh/config.rb
|
|
48
|
+
- lib/yarsh/instance_methods.rb
|
|
49
|
+
- lib/yarsh/prompt.rb
|
|
50
|
+
- lib/yarsh/shell.rb
|
|
51
|
+
- lib/yarsh/version.rb
|
|
52
|
+
- sig/yarsh.rbs
|
|
53
|
+
homepage: https://github.com/facenord-sud/yarsh.git
|
|
54
|
+
licenses: []
|
|
55
|
+
metadata:
|
|
56
|
+
homepage_uri: https://github.com/facenord-sud/yarsh.git
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: 4.0.2
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 4.0.6
|
|
72
|
+
specification_version: 4
|
|
73
|
+
summary: Yet Another Ruby SHell is a mix of Ruby and standard Shell commands.
|
|
74
|
+
test_files: []
|