ruby-shell 2.6.1 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rshrc +35 -16
- data/README.md +81 -1
- data/bin/rsh +276 -73
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 35706aa5efb5ce661a102e1c6a5b22638a750b567f41930b683f239d42386964
|
4
|
+
data.tar.gz: 164ea239fc7081182c031cf002dc7e3c64696536f5795057e24ba027a899f998
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 952d3ba93d41715d50a6b8a4f669f8f670f3b5c403f300f24fd7a28fe9cb432a4f66b2ba747fb9ddbab6e3ec6a33ec94113c3e499ee6014a25ec6d7861266929
|
7
|
+
data.tar.gz: 41ea80063e2fd754acd4a449ddac1f418bb3450b10bd70a9a99432159fefbcf2002bf6964638cd03079585088974d382c0978d830eb3beb8f28936cc2bc2cf65
|
data/.rshrc
CHANGED
@@ -1,29 +1,48 @@
|
|
1
1
|
# vim: set ft=ruby sw=2 sts=2 et :
|
2
|
+
# Example config file for rsh (.rshrc)
|
2
3
|
|
3
4
|
# ENVIRONMENT
|
4
|
-
|
5
|
-
ENV["EDITOR"]
|
6
|
-
ENV["
|
5
|
+
@lscolors = "/home/geir/.local/share/lscolors.sh"
|
6
|
+
ENV["EDITOR"] = "vim"
|
7
|
+
ENV["IRCNAME"] = "Geir Isene"
|
8
|
+
ENV["IRCNICK"] = "isene"
|
9
|
+
ENV["LESS"] = "-M-Q-r"
|
10
|
+
ENV["LESSCHARDEF"] = "8bcccbcc13b.4b95.33b."
|
11
|
+
ENV["LESSCHARSET"] = "latin1"
|
12
|
+
ENV["LESSEDIT"] = "%E ?lt+%lt. %f"
|
13
|
+
ENV["LESSOPEN"] = "| /home/geir/bin/lesspipe %s"
|
14
|
+
ENV["MANPAGER"] = "vim +MANPAGER -"
|
15
|
+
ENV["PAGER"] = "less"
|
16
|
+
ENV["TZ"] = 'Europe/Oslo'
|
17
|
+
ENV["VISUAL"] = "vim"
|
18
|
+
ENV["XDG_DATA_HOME"] = "/home/geir/.local/share"
|
19
|
+
ENV["PATH"] += ":/home/geir/bin"
|
7
20
|
|
8
21
|
# PROMPT
|
9
22
|
if @user == "root"
|
10
|
-
@prompt =
|
23
|
+
@prompt = @user.c(1).b + "@#{@node}".c(1)
|
11
24
|
else
|
12
|
-
@prompt =
|
25
|
+
@prompt = @user.c(2).b + "@#{@node}".c(2)
|
13
26
|
end
|
27
|
+
@prompt += ":".c(252) + " #{Dir.pwd}/".c(3) + " ".c(255)
|
14
28
|
|
15
29
|
# THEME
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
30
|
+
#@c_prompt = 196 # Color for basic prompt
|
31
|
+
#@c_cmd = 84 # Color for valid command
|
32
|
+
#@c_nick = 87 # Color for matching nick
|
33
|
+
#@c_gnick = 123 # Color for matching gnick
|
34
|
+
#@c_path = 208 # Color for valid path
|
35
|
+
#@c_switch = 148 # Color for switches/options
|
36
|
+
#@c_tabselect = 220 # Color for selected tabcompleted item
|
37
|
+
#@c_taboption = 94 # Color for unselected tabcompleted item
|
38
|
+
#@c_stamp = 244 # Color for time stamp/command
|
39
|
+
|
40
|
+
def pre_cmd
|
41
|
+
end
|
42
|
+
|
43
|
+
def post_cmd
|
44
|
+
end
|
25
45
|
|
26
46
|
# NICKS AND HISTORY
|
27
|
-
@nick = {"ls"=>"ls --color -F"}
|
28
|
-
@gnick = {}
|
47
|
+
@nick = {"ls"=>"ls --color -F", "l"=>"less", "v"=>"vim", "gu"=>"gitmagic -u", "gc"=>"git clone", "gs"=>"git status .", "vv"=>"vim ~/.vimrc", "vr"=>"vim ~/.rshrc"}
|
29
48
|
@history = []
|
data/README.md
CHANGED
@@ -18,6 +18,8 @@ Or simply `gem install ruby-shell`.
|
|
18
18
|
[](https://youtu.be/4P2z8oSo1u4)
|
19
19
|
|
20
20
|
# Features
|
21
|
+
|
22
|
+
## Core Shell Features
|
21
23
|
* Aliases (called nicks in rsh) - both for commands and general nicks
|
22
24
|
* Syntax highlighting, matching nicks, system commands and valid dirs/files
|
23
25
|
* Tab completions for nicks, system commands, command switches and dirs/files
|
@@ -30,6 +32,22 @@ Or simply `gem install ruby-shell`.
|
|
30
32
|
* rsh specific commands and full set of Ruby commands available via :<command>
|
31
33
|
* All colors are themeable in .rshrc (see github link for possibilities)
|
32
34
|
* Copy current command line to primary selection (paste w/middle button) with `Ctrl-y`
|
35
|
+
|
36
|
+
## NEW in v2.7.0 - Ruby Functions ⭐
|
37
|
+
* **Define Ruby functions as shell commands**: `:defun 'weather(*args) = system("curl -s wttr.in/#{args[0] || \"oslo\"}")'`
|
38
|
+
* **Call like any shell command**: `weather london`
|
39
|
+
* **Full Ruby power**: Access to Ruby stdlib, file operations, JSON parsing, web requests, etc.
|
40
|
+
* **Function management**: `:defun?` to list, `:defun '-name'` to remove
|
41
|
+
* **Syntax highlighting**: Ruby functions highlighted in bold
|
42
|
+
|
43
|
+
## Advanced Shell Features
|
44
|
+
* **Job Control**: Background jobs (`command &`), job suspension (`Ctrl-Z`), process management
|
45
|
+
* **Job Management**: `:jobs`, `:fg [id]`, `:bg [id]` commands
|
46
|
+
* **Command Substitution**: `$(date)` and backtick support
|
47
|
+
* **Variable Expansion**: `$HOME`, `$USER`, `$?` (exit status)
|
48
|
+
* **Conditional Execution**: `cmd1 && cmd2 || cmd3`
|
49
|
+
* **Brace Expansion**: `{a,b,c}` expands to `a b c`
|
50
|
+
* **Login Shell Support**: Proper signal handling and profile loading
|
33
51
|
|
34
52
|
Special functions/integrations:
|
35
53
|
* Use `r` to launch rtfm (https://github.com/isene/RTFM) - if you have it installed
|
@@ -42,9 +60,18 @@ Special commands:
|
|
42
60
|
* `:gnick 'h = /home/me'` to make a general alias (h) point to something (/home/me)
|
43
61
|
* `:nick?` will list all command nicks and general nicks (you can edit your nicks in .rshrc)
|
44
62
|
* `:history` will list the command history, while `:rmhistory` will delete the history
|
63
|
+
* `:jobs` will list background jobs, `:fg [job_id]` brings jobs to foreground, `:bg [job_id]` resumes stopped jobs
|
64
|
+
* `:defun 'func(args) = code'` defines Ruby functions callable as shell commands
|
65
|
+
* `:defun?` lists all user-defined functions, `:defun '-func'` removes functions
|
45
66
|
* `:version` Shows the rsh version number and the last published gem file version
|
46
67
|
* `:help` will display this help text
|
47
68
|
|
69
|
+
Background jobs:
|
70
|
+
* Use `command &` to run commands in background
|
71
|
+
* Use `:jobs` to list active background jobs
|
72
|
+
* Use `:fg` or `:fg job_id` to bring jobs to foreground
|
73
|
+
* Use `Ctrl-Z` to suspend running jobs, `:bg job_id` to resume them
|
74
|
+
|
48
75
|
## Moving around
|
49
76
|
While you `cd` around to different directories, you can see the last 10 directories visited via the command `:dirs` or the convenient shortcut `#`. Entering the number in the list (like `6` and ENTER) will jump you to that directory. Entering `-` will jump you back to the previous dir (equivalent of `1`. Entering `~` will get you to your home dir. If you want to bookmark a special directory, you can do that via a general nick like this: `:gnick "x = /path/to/a/dir/"` - this would bookmark the directory to the single letter `x`.
|
50
77
|
|
@@ -60,7 +87,60 @@ Hitting Shift-TAB will do a similar search through the command history - but wit
|
|
60
87
|
If you press `ENTER` after writing or tab-completing to a file, rsh will try to open the file in the user's EDITOR of choice (if it is a valid text file) or use `xdg-open` to open the file using the correct program. If you, for some reason want to use `run-mailcap` instead of `xdg-open` as the file opener, simply add `@runmailcap = true` to your `.rshrc`.
|
61
88
|
|
62
89
|
## History
|
63
|
-
Show the history with `:history`. Redo a history command with an exclamation mark and the number corresponding to the position in the history, like `!5` would do the 5th history command again.
|
90
|
+
Show the history with `:history`. Redo a history command with an exclamation mark and the number corresponding to the position in the history, like `!5` would do the 5th history command again. To delete a specific entry in history, hit `UP` and move up to that entry and hit `Ctrl-k` (for "kill").
|
91
|
+
|
92
|
+
## Ruby Functions - The Power Feature ⭐
|
93
|
+
|
94
|
+
rsh's unique Ruby functions let you define custom shell commands using the full power of Ruby:
|
95
|
+
|
96
|
+
### Basic Examples
|
97
|
+
```bash
|
98
|
+
# File operations
|
99
|
+
:defun 'count(*args) = puts Dir.glob(args[0] || "*").length'
|
100
|
+
count *.rb
|
101
|
+
|
102
|
+
# System monitoring
|
103
|
+
:defun 'mem = puts `free -h`.lines[1].split[2]'
|
104
|
+
mem
|
105
|
+
|
106
|
+
# JSON pretty-printing
|
107
|
+
:defun 'jsonpp(file) = require "json"; puts JSON.pretty_generate(JSON.parse(File.read(file)))'
|
108
|
+
jsonpp config.json
|
109
|
+
```
|
110
|
+
|
111
|
+
### Advanced Examples
|
112
|
+
```bash
|
113
|
+
# Network tools
|
114
|
+
:defun 'ports = puts `netstat -tlnp`.lines.grep(/LISTEN/).map{|l| l.split[3]}'
|
115
|
+
ports
|
116
|
+
|
117
|
+
# Git helpers
|
118
|
+
:defun 'branches = puts `git branch`.lines.map{|l| l.strip.sub("* ", "")}'
|
119
|
+
branches
|
120
|
+
|
121
|
+
# Directory analysis
|
122
|
+
:defun 'sizes(*args) = Dir.glob(args[0] || "*").each{|f| puts "#{File.size(f).to_s.rjust(8)} #{f}" if File.file?(f)}'
|
123
|
+
sizes
|
124
|
+
|
125
|
+
# Weather (using external API)
|
126
|
+
:defun 'weather(*args) = system("curl -s wttr.in/#{args[0] || \"oslo\"}")'
|
127
|
+
weather london
|
128
|
+
```
|
129
|
+
|
130
|
+
### Function Management
|
131
|
+
```bash
|
132
|
+
:defun? # List all defined functions
|
133
|
+
:defun '-myls' # Remove a function
|
134
|
+
```
|
135
|
+
|
136
|
+
Ruby functions have access to:
|
137
|
+
- Full Ruby standard library
|
138
|
+
- Shell environment variables via `ENV`
|
139
|
+
- rsh internals like `@history`, `@dirs`
|
140
|
+
- File system operations
|
141
|
+
- Network requests
|
142
|
+
- JSON/XML parsing
|
143
|
+
- And everything else Ruby can do!
|
64
144
|
|
65
145
|
## Integrations
|
66
146
|
rsh is integrated with the [rtfm file manager](https://github.com/isene/RTFM), with [fzf](https://github.com/junegunn/fzf) and with the programming language [XRPN](https://github.com/isene/xrpn).
|
data/bin/rsh
CHANGED
@@ -8,7 +8,7 @@
|
|
8
8
|
# Web_site: http://isene.com/
|
9
9
|
# Github: https://github.com/isene/rsh
|
10
10
|
# License: Public domain
|
11
|
-
@version = "2.
|
11
|
+
@version = "2.7.0" # Major release: Ruby functions, job control, command substitution, login shell support
|
12
12
|
|
13
13
|
# MODULES, CLASSES AND EXTENSIONS
|
14
14
|
class String # Add coloring to strings (with escaping for Readline)
|
@@ -50,18 +50,6 @@ module Cursor # Terminal cursor movement ANSI codes (thanks to https://github.co
|
|
50
50
|
row, col = self.pos
|
51
51
|
return col
|
52
52
|
end
|
53
|
-
def up(n = nil) # Move cursor up by n
|
54
|
-
print(CSI + "#{(n || 1)}A")
|
55
|
-
end
|
56
|
-
def down(n = nil) # Move the cursor down by n
|
57
|
-
print(CSI + "#{(n || 1)}B")
|
58
|
-
end
|
59
|
-
def left(n = nil) # Move the cursor backward by n
|
60
|
-
print(CSI + "#{n || 1}D")
|
61
|
-
end
|
62
|
-
def right(n = nil) # Move the cursor forward by n
|
63
|
-
print(CSI + "#{n || 1}C")
|
64
|
-
end
|
65
53
|
def col(n = nil) # Cursor moves to nth position horizontally in the current line
|
66
54
|
print(CSI + "#{n || 1}G")
|
67
55
|
end
|
@@ -71,24 +59,12 @@ module Cursor # Terminal cursor movement ANSI codes (thanks to https://github.co
|
|
71
59
|
def next_line # Move cursor down to beginning of next line
|
72
60
|
print(CSI + 'E' + CSI + "1G")
|
73
61
|
end
|
74
|
-
def prev_line # Move cursor up to beginning of previous line
|
75
|
-
print(CSI + 'A' + CSI + "1G")
|
76
|
-
end
|
77
|
-
def clear_char(n = nil) # Erase n characters from the current cursor position
|
78
|
-
print(CSI + "#{n}X")
|
79
|
-
end
|
80
62
|
def clear_line # Erase the entire current line and return to beginning of the line
|
81
63
|
print(CSI + '2K' + CSI + "1G")
|
82
64
|
end
|
83
|
-
def clear_line_before # Erase from the beginning of the line up to and including the current cursor position.
|
84
|
-
print(CSI + '1K')
|
85
|
-
end
|
86
65
|
def clear_line_after # Erase from the current position (inclusive) to the end of the line
|
87
66
|
print(CSI + '0K')
|
88
67
|
end
|
89
|
-
def scroll_up # Scroll display up one line
|
90
|
-
print(ESC + 'M')
|
91
|
-
end
|
92
68
|
def scroll_down # Scroll display down one line
|
93
69
|
print(ESC + 'D')
|
94
70
|
end
|
@@ -96,12 +72,6 @@ module Cursor # Terminal cursor movement ANSI codes (thanks to https://github.co
|
|
96
72
|
print(CSI + 'J')
|
97
73
|
end
|
98
74
|
end
|
99
|
-
def stdin_clear
|
100
|
-
begin
|
101
|
-
$stdin.getc while $stdin.ready?
|
102
|
-
rescue
|
103
|
-
end
|
104
|
-
end
|
105
75
|
|
106
76
|
# INITIALIZATION
|
107
77
|
begin # Requires
|
@@ -137,6 +107,9 @@ begin # Initialization
|
|
137
107
|
@runmailcap = false
|
138
108
|
# Variable initializations
|
139
109
|
@dirs = ["."]*10
|
110
|
+
@jobs = {} # Background jobs tracking
|
111
|
+
@job_id = 0 # Job counter
|
112
|
+
@last_exit = 0 # Last command exit status
|
140
113
|
def pre_cmd; end # User-defined function to be run BEFORE command execution
|
141
114
|
def post_cmd; end # User-defined function to be run AFTER command execution
|
142
115
|
end
|
@@ -176,9 +149,24 @@ end
|
|
176
149
|
* `:gnick 'h = /home/me'` to make a general alias (h) point to something (/home/me)
|
177
150
|
* `:nick?` will list all command nicks and general nicks (you can edit your nicks in .rshrc)
|
178
151
|
* `:history` will list the command history, while `:rmhistory` will delete the history
|
152
|
+
* `:jobs` will list background jobs, `:fg [job_id]` brings jobs to foreground, `:bg [job_id]` resumes stopped jobs
|
153
|
+
* `:defun 'func(args) = code'` defines Ruby functions callable as shell commands
|
154
|
+
* `:defun?` lists all user-defined functions, `:defun '-func'` removes functions
|
179
155
|
* `:version` Shows the rsh version number and the last published gem file version
|
180
156
|
* `:help` will display this help text
|
181
157
|
|
158
|
+
Background jobs:
|
159
|
+
* Use `command &` to run commands in background
|
160
|
+
* Use `:jobs` to list active background jobs
|
161
|
+
* Use `:fg` or `:fg job_id` to bring jobs to foreground
|
162
|
+
* Use `Ctrl-Z` to suspend running jobs, `:bg job_id` to resume them
|
163
|
+
|
164
|
+
Ruby Functions:
|
165
|
+
* Define with `:defun 'myls(*args) = Dir.glob("*").each {|f| puts f}'`
|
166
|
+
* Call like any shell command: `myls` or `myls arg1 arg2`
|
167
|
+
* Functions have full access to Ruby stdlib and rsh internals
|
168
|
+
* Remove with `:defun '-myls'` and list with `:defun?`
|
169
|
+
|
182
170
|
HELP
|
183
171
|
|
184
172
|
# GENERIC FUNCTIONS
|
@@ -254,13 +242,13 @@ def getchr # Process key presses
|
|
254
242
|
when "" then chr = "C-T"
|
255
243
|
when "" then chr = "C-Y"
|
256
244
|
when "" then chr = "WBACK"
|
245
|
+
when "\u001A" then chr = "C-Z"
|
257
246
|
when "" then chr = "LDEL"
|
258
247
|
when "\r" then chr = "ENTER"
|
259
248
|
when "\t" then chr = "TAB"
|
260
249
|
when /[[:print:]]/ then chr = c
|
261
250
|
else chr = ""
|
262
251
|
end
|
263
|
-
#stdin_clear
|
264
252
|
return chr
|
265
253
|
end
|
266
254
|
def getstr # A custom Readline-like function
|
@@ -398,6 +386,14 @@ def getstr # A custom Readline-like function
|
|
398
386
|
when 'C-Y' # Copy command line to primary selection
|
399
387
|
system("echo -n '#{@history[0]}' | xclip")
|
400
388
|
puts "\n#{Time.now.strftime("%H:%M:%S")}: Copied to primary selection (paste with middle buttoni)".c(@c_stamp)
|
389
|
+
when 'C-Z' # Suspend current process (background job)
|
390
|
+
if @current_pid
|
391
|
+
puts "\n[#{@job_id}] Suspended #{@current_pid}"
|
392
|
+
Process.kill("STOP", @current_pid)
|
393
|
+
@jobs[@job_id] = {pid: @current_pid, cmd: @cmd, status: :stopped}
|
394
|
+
else
|
395
|
+
puts "\nNo active job to suspend"
|
396
|
+
end
|
401
397
|
when 'C-K' # Kill/delete that entry in the history
|
402
398
|
@history.delete_at(@stk)
|
403
399
|
@stk -= 1
|
@@ -433,20 +429,20 @@ def getstr # A custom Readline-like function
|
|
433
429
|
@pos += 1
|
434
430
|
end
|
435
431
|
end
|
436
|
-
#@c.col(@pos0 + @history[0].length)
|
437
432
|
@c.clear_screen_down
|
438
433
|
end
|
439
434
|
def tab(type)
|
440
435
|
i = 0
|
441
436
|
chr = ""
|
442
437
|
@tabarray = []
|
443
|
-
@pretab
|
444
|
-
@postab
|
445
|
-
@c_row, @c_col = @c.pos
|
446
|
-
@
|
447
|
-
@tabstr
|
448
|
-
@tabstr
|
449
|
-
@
|
438
|
+
@pretab = @history[0][0...@pos].to_s # Extract the current line up to cursor
|
439
|
+
@postab = @history[0][@pos..].to_s # Extract the current line from cursor to end
|
440
|
+
@c_row, @c_col = @c.pos # Get cursor position
|
441
|
+
@row0 = @c_row # Save original row
|
442
|
+
@tabstr = @pretab.split(/[|, ]/).last.to_s # Get the sustring that is being tab completed
|
443
|
+
@tabstr = "" if @pretab[-1] =~ /[ |]/ # Tab from nothing if tabbing starts with space or pipe
|
444
|
+
@tabstr = @pretab if type == "hist" # Searching for matches with whole string in history
|
445
|
+
@pretab = @pretab.delete_suffix(@tabstr)
|
450
446
|
type = "switch" if @tabstr[0] == "-"
|
451
447
|
while chr != "ENTER"
|
452
448
|
case type
|
@@ -547,6 +543,7 @@ def tab(type)
|
|
547
543
|
end
|
548
544
|
end
|
549
545
|
@c.clear_screen_down
|
546
|
+
@row0 = @c_row
|
550
547
|
@c.row(@c_row)
|
551
548
|
@c.col(@c_col)
|
552
549
|
@history[0] = @newhist0
|
@@ -585,6 +582,8 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic
|
|
585
582
|
el.c(@c_nick)
|
586
583
|
elsif @gnick.include?(el)
|
587
584
|
el.c(@c_gnick)
|
585
|
+
elsif self.respond_to?(el) && singleton_class.instance_methods(false).include?(el.to_sym)
|
586
|
+
el.c(@c_nick).b # Ruby functions in bold nick color
|
588
587
|
elsif el[0] == "-"
|
589
588
|
el.c(@c_switch)
|
590
589
|
else
|
@@ -658,12 +657,165 @@ def dirs
|
|
658
657
|
puts "#{i}: #{e}"
|
659
658
|
end
|
660
659
|
end
|
660
|
+
def jobs
|
661
|
+
puts "Active jobs:"
|
662
|
+
@jobs.each do |id, job|
|
663
|
+
begin
|
664
|
+
Process.kill(0, job[:pid]) # Check if process exists
|
665
|
+
puts "[#{id}] #{job[:pid]} #{job[:status]} #{job[:cmd]}"
|
666
|
+
rescue Errno::ESRCH
|
667
|
+
@jobs.delete(id) # Clean up dead jobs
|
668
|
+
end
|
669
|
+
end
|
670
|
+
end
|
671
|
+
def fg(job_id = nil)
|
672
|
+
job_id ||= @jobs.keys.max
|
673
|
+
return puts "No jobs" if job_id.nil?
|
674
|
+
job = @jobs[job_id]
|
675
|
+
return puts "Job #{job_id} not found" unless job
|
676
|
+
puts "Bringing job #{job_id} to foreground: #{job[:cmd]}"
|
677
|
+
begin
|
678
|
+
if job[:status] == :stopped
|
679
|
+
Process.kill("CONT", job[:pid])
|
680
|
+
end
|
681
|
+
@current_pid = job[:pid]
|
682
|
+
Process.wait(job[:pid])
|
683
|
+
@jobs.delete(job_id)
|
684
|
+
@current_pid = nil
|
685
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
686
|
+
@jobs.delete(job_id)
|
687
|
+
@current_pid = nil
|
688
|
+
end
|
689
|
+
end
|
690
|
+
def bg(job_id = nil)
|
691
|
+
job_id ||= @jobs.keys.max
|
692
|
+
return puts "No jobs" if job_id.nil?
|
693
|
+
job = @jobs[job_id]
|
694
|
+
return puts "Job #{job_id} not found" unless job
|
695
|
+
return puts "Job #{job_id} already running" if job[:status] == :running
|
696
|
+
puts "Resuming job #{job_id} in background: #{job[:cmd]}"
|
697
|
+
begin
|
698
|
+
Process.kill("CONT", job[:pid])
|
699
|
+
@jobs[job_id][:status] = :running
|
700
|
+
rescue Errno::ESRCH
|
701
|
+
@jobs.delete(job_id)
|
702
|
+
puts "Job #{job_id} no longer exists"
|
703
|
+
end
|
704
|
+
end
|
705
|
+
def defun(func_def) # Define a Ruby function like: `:defun "myls(*args) = Dir.glob('*').each {|f| puts f}"`
|
706
|
+
if func_def.match(/^\s*-/)
|
707
|
+
# Remove function
|
708
|
+
func_name = func_def.sub(/^\s*-/, '')
|
709
|
+
if self.respond_to?(func_name)
|
710
|
+
singleton_class.remove_method(func_name.to_sym)
|
711
|
+
puts "Function '#{func_name}' removed"
|
712
|
+
else
|
713
|
+
puts "Function '#{func_name}' not found"
|
714
|
+
end
|
715
|
+
else
|
716
|
+
# Define function
|
717
|
+
# Extract function name, params, and body from "name(params) = body" format
|
718
|
+
if func_def =~ /^(\w+)\s*\(([^)]*)\)\s*=\s*(.+)$/
|
719
|
+
func_name = $1
|
720
|
+
func_params = $2
|
721
|
+
func_body = $3
|
722
|
+
|
723
|
+
begin
|
724
|
+
eval_code = "def #{func_name}(#{func_params}); #{func_body}; end"
|
725
|
+
puts " DEBUG: Evaluating: #{eval_code}" if ENV['RSH_DEBUG']
|
726
|
+
singleton_class.class_eval(eval_code)
|
727
|
+
puts "Function '#{func_name}' defined"
|
728
|
+
puts " DEBUG: Method created? #{respond_to?(func_name)}" if ENV['RSH_DEBUG']
|
729
|
+
rescue SyntaxError => e
|
730
|
+
puts "Syntax error in function definition: #{e}"
|
731
|
+
rescue => e
|
732
|
+
puts "Error in function definition: #{e}"
|
733
|
+
end
|
734
|
+
else
|
735
|
+
puts "Invalid function format. Use: name(params) = body"
|
736
|
+
end
|
737
|
+
end
|
738
|
+
rshrc
|
739
|
+
end
|
740
|
+
def defun? # Show all user-defined functions
|
741
|
+
puts "User-defined Ruby functions:"
|
742
|
+
# Get only methods defined by defun, excluding built-ins and rsh internals
|
743
|
+
all_methods = singleton_class.instance_methods(false)
|
744
|
+
puts " All singleton methods: #{all_methods}"
|
745
|
+
excluded = [:defun, :defun?, :execute_conditional, :expand_braces]
|
746
|
+
methods = all_methods - excluded
|
747
|
+
if methods.empty?
|
748
|
+
puts " (none defined after filtering)"
|
749
|
+
else
|
750
|
+
methods.each do |method|
|
751
|
+
puts " #{method}"
|
752
|
+
end
|
753
|
+
end
|
754
|
+
end
|
755
|
+
def execute_conditional(cmd_line)
|
756
|
+
# Split on && and || while preserving the operators
|
757
|
+
parts = cmd_line.split(/(\s*&&\s*|\s*\|\|\s*)/)
|
758
|
+
|
759
|
+
result = true
|
760
|
+
i = 0
|
761
|
+
while i < parts.length
|
762
|
+
command = parts[i].strip
|
763
|
+
next if command.empty?
|
764
|
+
|
765
|
+
if command == '&&'
|
766
|
+
i += 1
|
767
|
+
next unless result # Skip if previous command failed
|
768
|
+
elsif command == '||'
|
769
|
+
i += 1
|
770
|
+
next if result # Skip if previous command succeeded
|
771
|
+
else
|
772
|
+
# Execute the command
|
773
|
+
success = system(command)
|
774
|
+
result = success
|
775
|
+
puts " Command failed: #{command} (exit #{$?.exitstatus})" unless success
|
776
|
+
end
|
777
|
+
i += 1
|
778
|
+
end
|
779
|
+
end
|
780
|
+
def expand_braces(str)
|
781
|
+
# Simple brace expansion: {a,b,c} -> a b c
|
782
|
+
str.gsub(/\{([^}]+)\}/) do |match|
|
783
|
+
items = $1.split(',').map(&:strip)
|
784
|
+
items.join(' ')
|
785
|
+
end
|
786
|
+
end
|
661
787
|
|
662
788
|
# INITIAL SETUP
|
663
789
|
begin # Load .rshrc and populate @history
|
664
790
|
trap "SIGINT" do end
|
791
|
+
trap "SIGHUP" do
|
792
|
+
rshrc
|
793
|
+
exit
|
794
|
+
end
|
795
|
+
trap "SIGTERM" do
|
796
|
+
rshrc
|
797
|
+
exit
|
798
|
+
end
|
665
799
|
firstrun unless File.exist?(Dir.home+'/.rshrc') # Initial loading - to get history
|
666
|
-
load(Dir.home+'/.rshrc')
|
800
|
+
load(Dir.home+'/.rshrc')
|
801
|
+
# Load login shell files if rsh is running as login shell
|
802
|
+
if ENV['LOGIN_SHELL'] or $0 == "-rsh" or ARGV.include?('-l') or ARGV.include?('--login')
|
803
|
+
['/etc/profile', Dir.home+'/.profile', Dir.home+'/.bash_profile', Dir.home+'/.bashrc'].each do |f|
|
804
|
+
if File.exist?(f)
|
805
|
+
puts "Loading #{f}..." if ENV['RSH_DEBUG']
|
806
|
+
begin
|
807
|
+
# Source shell files by extracting export statements
|
808
|
+
File.readlines(f).each do |line|
|
809
|
+
if line =~ /^\s*export\s+(\w+)=(.*)/
|
810
|
+
ENV[$1] = $2.gsub(/['"]/, '')
|
811
|
+
end
|
812
|
+
end
|
813
|
+
rescue => e
|
814
|
+
puts "Warning: Could not source #{f}: #{e}" if ENV['RSH_DEBUG']
|
815
|
+
end
|
816
|
+
end
|
817
|
+
end
|
818
|
+
end
|
667
819
|
ENV["SHELL"] = __FILE__
|
668
820
|
ENV["TERM"] = "rxvt-unicode-256color"
|
669
821
|
ENV["PATH"] ? ENV["PATH"] += ":" : ENV["PATH"] = ""
|
@@ -735,41 +887,92 @@ loop do
|
|
735
887
|
elsif @cmd == '#' # List previous directories
|
736
888
|
dirs
|
737
889
|
else # Execute command
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
890
|
+
# Check if it's a user-defined Ruby function FIRST (before any expansions)
|
891
|
+
cmd_parts = @cmd.split(/\s+/)
|
892
|
+
func_name = cmd_parts[0]
|
893
|
+
if self.respond_to?(func_name) && singleton_class.instance_methods(false).include?(func_name.to_sym)
|
894
|
+
begin
|
895
|
+
args = cmd_parts[1..]
|
896
|
+
puts "DEBUG: Calling #{func_name} with args: #{args}" if ENV['RSH_DEBUG']
|
897
|
+
result = self.send(func_name, *args)
|
898
|
+
puts "DEBUG: Result: #{result.inspect}" if ENV['RSH_DEBUG']
|
899
|
+
puts result unless result.nil?
|
900
|
+
rescue => e
|
901
|
+
puts "Error calling function '#{func_name}': #{e}"
|
902
|
+
end
|
751
903
|
else
|
752
|
-
|
753
|
-
if @cmd
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
904
|
+
# Handle conditional execution (&& and ||)
|
905
|
+
if @cmd.include?('&&') || @cmd.include?('||')
|
906
|
+
execute_conditional(@cmd)
|
907
|
+
next
|
908
|
+
end
|
909
|
+
# Expand brace expansion {a,b,c}
|
910
|
+
@cmd = expand_braces(@cmd)
|
911
|
+
# Expand command substitution $(command) and backticks
|
912
|
+
@cmd = @cmd.gsub(/\$\(([^)]+)\)/) { `#{$1}`.chomp }
|
913
|
+
@cmd = @cmd.gsub(/`([^`]+)`/) { `#{$1}`.chomp }
|
914
|
+
# Expand environment variables and exit status
|
915
|
+
@cmd = @cmd.gsub(/\$\?/) { @last_exit.to_s }
|
916
|
+
@cmd = @cmd.gsub(/\$(\w+)|\$\{(\w+)\}/) { ENV[$1 || $2] || '' }
|
917
|
+
# Expand tilde
|
918
|
+
@cmd = @cmd.gsub(/~/, Dir.home)
|
919
|
+
ca = @nick.transform_keys {|k| /((^\K\s*\K)|(\|\K\s*\K))\b(?<!-)#{Regexp.escape k}\b/}
|
920
|
+
@cmd = @cmd.gsub(Regexp.union(ca.keys), @nick)
|
921
|
+
ga = @gnick.transform_keys {|k| /\b(?<!-)#{Regexp.escape k}\b/}
|
922
|
+
@cmd = @cmd.gsub(Regexp.union(ga.keys), @gnick)
|
923
|
+
@cmd = "~" if @cmd == "cd"
|
924
|
+
@cmd.sub!(/^cd (\S*).*/, '\1')
|
925
|
+
@cmd = Dir.home if @cmd == "~"
|
926
|
+
@cmd = @dirs[1] if @cmd == "-"
|
927
|
+
@cmd = @dirs[@cmd.to_i] if @cmd =~ /^\d$/
|
928
|
+
# Check if it's a directory to change to
|
929
|
+
dir = @cmd.strip.sub(/~/, Dir.home)
|
930
|
+
if Dir.exist?(dir)
|
931
|
+
Dir.chdir(dir)
|
932
|
+
system("git status .") if Dir.exist?(".git")
|
933
|
+
else
|
934
|
+
puts "#{Time.now.strftime("%H:%M:%S")}: #{@cmd}".c(@c_stamp)
|
935
|
+
if @cmd == "f" # fzf integration (https://github.com/junegunn/fzf)
|
936
|
+
res = `fzf`.chomp
|
937
|
+
Dir.chdir(File.dirname(res))
|
938
|
+
elsif File.exist?(@cmd) and not File.executable?(@cmd) and not @cmd.include?(" ")
|
939
|
+
# Only auto-open files if it's a single filename (no spaces = no command with args)
|
940
|
+
if File.read(@cmd).force_encoding("UTF-8").valid_encoding?
|
941
|
+
system("#{ENV['EDITOR']} #{@cmd}") # Try open with user's editor
|
762
942
|
else
|
763
|
-
|
943
|
+
if @runmailcap
|
944
|
+
Thread.new { system("run-mailcap #{@cmd} 2>/dev/null") }
|
945
|
+
else
|
946
|
+
Thread.new { system("xdg-open #{@cmd} 2>/dev/null") }
|
947
|
+
end
|
948
|
+
end
|
949
|
+
else
|
950
|
+
begin
|
951
|
+
pre_cmd
|
952
|
+
# Handle background jobs
|
953
|
+
if @cmd.end_with?(' &')
|
954
|
+
@cmd = @cmd[0..-3] # Remove the &
|
955
|
+
@job_id += 1
|
956
|
+
# Handle pipes and redirections in background
|
957
|
+
if @cmd.include?('|') || @cmd.include?('>') || @cmd.include?('<')
|
958
|
+
pid = spawn(@cmd, pgroup: true)
|
959
|
+
else
|
960
|
+
pid = spawn(@cmd)
|
961
|
+
end
|
962
|
+
@jobs[@job_id] = {pid: pid, cmd: @cmd, status: :running}
|
963
|
+
puts "[#{@job_id}] #{pid} #{@cmd}"
|
964
|
+
else
|
965
|
+
# Better handling of pipes and redirections
|
966
|
+
@current_pid = spawn(@cmd)
|
967
|
+
Process.wait(@current_pid)
|
968
|
+
@last_exit = $?.exitstatus
|
969
|
+
@current_pid = nil
|
970
|
+
puts " Command failed: #{@cmd} (exit #{@last_exit})" unless @last_exit == 0
|
971
|
+
end
|
972
|
+
post_cmd
|
973
|
+
rescue StandardError => err
|
974
|
+
puts "\nError: #{err}"
|
764
975
|
end
|
765
|
-
end
|
766
|
-
else
|
767
|
-
begin
|
768
|
-
pre_cmd
|
769
|
-
puts " Not executed: #{@cmd}" unless system (@cmd) # Try execute the command
|
770
|
-
post_cmd
|
771
|
-
rescue StandardError => err
|
772
|
-
puts "\n#{err}"
|
773
976
|
end
|
774
977
|
end
|
775
978
|
end
|
metadata
CHANGED
@@ -1,20 +1,20 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-shell
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Geir Isene
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-07-01 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: 'A shell written in Ruby with extensive tab completions, aliases/nicks,
|
14
|
-
history, syntax highlighting, theming, auto-cd, auto-opening files and more.
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
history, syntax highlighting, theming, auto-cd, auto-opening files and more. MAJOR
|
15
|
+
UPDATE v2.7.0: Ruby Functions - define custom shell commands using full Ruby power!
|
16
|
+
Also: job control (background jobs, Ctrl-Z suspension), command substitution, variable
|
17
|
+
expansion, conditional execution, login shell support, and much more.'
|
18
18
|
email: g@isene.com
|
19
19
|
executables:
|
20
20
|
- rsh
|