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 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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
data/exe/yarsh ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'yarsh'
5
+
6
+ extend Yarsh::InstanceMethods
7
+ Yarsh::Shell.new(binding).shell
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yarsh
4
+ VERSION = "0.1.0"
5
+ 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
@@ -0,0 +1,4 @@
1
+ module Yarsh
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []