ruby-shell 2.6.2 → 2.8.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 +91 -2
- data/bin/rsh +359 -83
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 55c80db8b211f00c2226a2bc38de34ca8bc448ff38545043f24931bde8441de6
|
4
|
+
data.tar.gz: 7f70a5858de2c5d64800202a35af757b9bef6d864ccf76ab23dc673b82451fc1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ad59802df4fc00d2d39191080b4a8df1d90ad5c567078c16562992b64d0aa8ad3ca59fdd9a0ab0bcefc35f6b02774305bec3f83872d463d27c2570887376901
|
7
|
+
data.tar.gz: 0a4cd352e6e20d25af578a689bffb6d460cc24f0b40328461b8a9ada40d3b03bda6e6c79674944170b752b7dc3787b9397869644025bc7925e90d54ce3bcff4e
|
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,28 @@ 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.8.0 - Enhanced Help System & Nick Management ⭐
|
37
|
+
* **Two-column help display**: Compact, organized help that fits on one screen
|
38
|
+
* **New `:info` command**: Shows introduction and feature overview
|
39
|
+
* **`:nickdel` and `:gnickdel`**: Intuitive commands to delete nicks and gnicks
|
40
|
+
* **Improved help organization**: Quick reference for keyboard shortcuts, commands, and features
|
41
|
+
|
42
|
+
## Ruby Functions (v2.7.0)
|
43
|
+
* **Define Ruby functions as shell commands**: `:defun 'weather(*args) = system("curl -s wttr.in/#{args[0] || \"oslo\"}")'`
|
44
|
+
* **Call like any shell command**: `weather london`
|
45
|
+
* **Full Ruby power**: Access to Ruby stdlib, file operations, JSON parsing, web requests, etc.
|
46
|
+
* **Function management**: `:defun?` to list, `:defun '-name'` to remove
|
47
|
+
* **Syntax highlighting**: Ruby functions highlighted in bold
|
48
|
+
|
49
|
+
## Advanced Shell Features
|
50
|
+
* **Job Control**: Background jobs (`command &`), job suspension (`Ctrl-Z`), process management
|
51
|
+
* **Job Management**: `:jobs`, `:fg [id]`, `:bg [id]` commands
|
52
|
+
* **Command Substitution**: `$(date)` and backtick support
|
53
|
+
* **Variable Expansion**: `$HOME`, `$USER`, `$?` (exit status)
|
54
|
+
* **Conditional Execution**: `cmd1 && cmd2 || cmd3`
|
55
|
+
* **Brace Expansion**: `{a,b,c}` expands to `a b c`
|
56
|
+
* **Login Shell Support**: Proper signal handling and profile loading
|
33
57
|
|
34
58
|
Special functions/integrations:
|
35
59
|
* Use `r` to launch rtfm (https://github.com/isene/RTFM) - if you have it installed
|
@@ -40,10 +64,22 @@ Special functions/integrations:
|
|
40
64
|
Special commands:
|
41
65
|
* `:nick 'll = ls -l'` to make a command alias (ll) point to a command (ls -l)
|
42
66
|
* `:gnick 'h = /home/me'` to make a general alias (h) point to something (/home/me)
|
67
|
+
* `:nickdel 'name'` to delete a command nick (or use `:nick '-name'`)
|
68
|
+
* `:gnickdel 'name'` to delete a general nick (or use `:gnick '-name'`)
|
43
69
|
* `:nick?` will list all command nicks and general nicks (you can edit your nicks in .rshrc)
|
44
70
|
* `:history` will list the command history, while `:rmhistory` will delete the history
|
71
|
+
* `:jobs` will list background jobs, `:fg [job_id]` brings jobs to foreground, `:bg [job_id]` resumes stopped jobs
|
72
|
+
* `:defun 'func(args) = code'` defines Ruby functions callable as shell commands
|
73
|
+
* `:defun?` lists all user-defined functions, `:defun '-func'` removes functions
|
74
|
+
* `:info` shows introduction and feature overview
|
45
75
|
* `:version` Shows the rsh version number and the last published gem file version
|
46
|
-
* `:help` will display
|
76
|
+
* `:help` will display a compact command reference in two columns
|
77
|
+
|
78
|
+
Background jobs:
|
79
|
+
* Use `command &` to run commands in background
|
80
|
+
* Use `:jobs` to list active background jobs
|
81
|
+
* Use `:fg` or `:fg job_id` to bring jobs to foreground
|
82
|
+
* Use `Ctrl-Z` to suspend running jobs, `:bg job_id` to resume them
|
47
83
|
|
48
84
|
## Moving around
|
49
85
|
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`.
|
@@ -60,7 +96,60 @@ Hitting Shift-TAB will do a similar search through the command history - but wit
|
|
60
96
|
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
97
|
|
62
98
|
## 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.
|
99
|
+
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").
|
100
|
+
|
101
|
+
## Ruby Functions - The Power Feature ⭐
|
102
|
+
|
103
|
+
rsh's unique Ruby functions let you define custom shell commands using the full power of Ruby:
|
104
|
+
|
105
|
+
### Basic Examples
|
106
|
+
```bash
|
107
|
+
# File operations
|
108
|
+
:defun 'count(*args) = puts Dir.glob(args[0] || "*").length'
|
109
|
+
count *.rb
|
110
|
+
|
111
|
+
# System monitoring
|
112
|
+
:defun 'mem = puts `free -h`.lines[1].split[2]'
|
113
|
+
mem
|
114
|
+
|
115
|
+
# JSON pretty-printing
|
116
|
+
:defun 'jsonpp(file) = require "json"; puts JSON.pretty_generate(JSON.parse(File.read(file)))'
|
117
|
+
jsonpp config.json
|
118
|
+
```
|
119
|
+
|
120
|
+
### Advanced Examples
|
121
|
+
```bash
|
122
|
+
# Network tools
|
123
|
+
:defun 'ports = puts `netstat -tlnp`.lines.grep(/LISTEN/).map{|l| l.split[3]}'
|
124
|
+
ports
|
125
|
+
|
126
|
+
# Git helpers
|
127
|
+
:defun 'branches = puts `git branch`.lines.map{|l| l.strip.sub("* ", "")}'
|
128
|
+
branches
|
129
|
+
|
130
|
+
# Directory analysis
|
131
|
+
:defun 'sizes(*args) = Dir.glob(args[0] || "*").each{|f| puts "#{File.size(f).to_s.rjust(8)} #{f}" if File.file?(f)}'
|
132
|
+
sizes
|
133
|
+
|
134
|
+
# Weather (using external API)
|
135
|
+
:defun 'weather(*args) = system("curl -s wttr.in/#{args[0] || \"oslo\"}")'
|
136
|
+
weather london
|
137
|
+
```
|
138
|
+
|
139
|
+
### Function Management
|
140
|
+
```bash
|
141
|
+
:defun? # List all defined functions
|
142
|
+
:defun '-myls' # Remove a function
|
143
|
+
```
|
144
|
+
|
145
|
+
Ruby functions have access to:
|
146
|
+
- Full Ruby standard library
|
147
|
+
- Shell environment variables via `ENV`
|
148
|
+
- rsh internals like `@history`, `@dirs`
|
149
|
+
- File system operations
|
150
|
+
- Network requests
|
151
|
+
- JSON/XML parsing
|
152
|
+
- And everything else Ruby can do!
|
64
153
|
|
65
154
|
## Integrations
|
66
155
|
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.8.0" # Feature release: nickdel functions, improved help system, info command
|
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,19 +107,23 @@ 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
|
143
116
|
|
144
117
|
# HELP TEXT
|
145
|
-
@
|
118
|
+
@info = <<~INFO
|
146
119
|
|
147
120
|
Hello #{@user}, welcome to rsh - the Ruby SHell.
|
148
121
|
|
149
122
|
rsh does not attempt to compete with the grand old shells like bash and zsh.
|
150
123
|
It serves the specific needs and wants of its author. If you like it, then feel free
|
151
|
-
to ask for more or different features here: https://github.com/isene/rsh.
|
152
|
-
|
124
|
+
to ask for more or different features here: https://github.com/isene/rsh.
|
125
|
+
|
126
|
+
Features:
|
153
127
|
* Aliases (called nicks in rsh) - both for commands and general nicks
|
154
128
|
* Syntax highlighting, matching nicks, system commands and valid dirs/files
|
155
129
|
* Tab completions for nicks, system commands, command switches and dirs/files
|
@@ -164,26 +138,18 @@ end
|
|
164
138
|
* rsh specific commands and full set of Ruby commands available via :<command>
|
165
139
|
* All colors are themeable in .rshrc (see github link for possibilities)
|
166
140
|
* Copy current command line to primary selection (paste w/middle button) with `Ctrl-y`
|
141
|
+
|
142
|
+
Use `:help` for command reference.
|
143
|
+
|
144
|
+
INFO
|
167
145
|
|
168
|
-
|
169
|
-
* Use `r` to launch rtfm (https://github.com/isene/RTFM) - if you have it installed
|
170
|
-
* Use `f` to launch fzf (https://github.com/junegunn/fzf) - if you have it installed
|
171
|
-
* Use `=` followed by xrpn commands separated by commas or double-spaces (https://github.com/isene/xrpn)
|
172
|
-
* Use `:` followed by a Ruby expression to access the whole world of Ruby
|
146
|
+
@help = <<~HELP
|
173
147
|
|
174
|
-
Special commands:
|
175
|
-
* `:nick 'll = ls -l'` to make a command alias (ll) point to a command (ls -l)
|
176
|
-
* `:gnick 'h = /home/me'` to make a general alias (h) point to something (/home/me)
|
177
|
-
* `:nick?` will list all command nicks and general nicks (you can edit your nicks in .rshrc)
|
178
|
-
* `:history` will list the command history, while `:rmhistory` will delete the history
|
179
|
-
* `:version` Shows the rsh version number and the last published gem file version
|
180
|
-
* `:help` will display this help text
|
181
|
-
|
182
148
|
HELP
|
183
149
|
|
184
150
|
# GENERIC FUNCTIONS
|
185
151
|
def firstrun
|
186
|
-
puts @
|
152
|
+
puts @info
|
187
153
|
puts "Since there is no rsh configuration file (.rshrc), I will help you set it up to suit your needs.\n\n"
|
188
154
|
puts "The prompt you see now is the very basic rsh prompt:"
|
189
155
|
print "#{@prompt} (press ENTER)"
|
@@ -254,13 +220,13 @@ def getchr # Process key presses
|
|
254
220
|
when "" then chr = "C-T"
|
255
221
|
when "" then chr = "C-Y"
|
256
222
|
when "" then chr = "WBACK"
|
223
|
+
when "\u001A" then chr = "C-Z"
|
257
224
|
when "" then chr = "LDEL"
|
258
225
|
when "\r" then chr = "ENTER"
|
259
226
|
when "\t" then chr = "TAB"
|
260
227
|
when /[[:print:]]/ then chr = c
|
261
228
|
else chr = ""
|
262
229
|
end
|
263
|
-
#stdin_clear
|
264
230
|
return chr
|
265
231
|
end
|
266
232
|
def getstr # A custom Readline-like function
|
@@ -398,6 +364,14 @@ def getstr # A custom Readline-like function
|
|
398
364
|
when 'C-Y' # Copy command line to primary selection
|
399
365
|
system("echo -n '#{@history[0]}' | xclip")
|
400
366
|
puts "\n#{Time.now.strftime("%H:%M:%S")}: Copied to primary selection (paste with middle buttoni)".c(@c_stamp)
|
367
|
+
when 'C-Z' # Suspend current process (background job)
|
368
|
+
if @current_pid
|
369
|
+
puts "\n[#{@job_id}] Suspended #{@current_pid}"
|
370
|
+
Process.kill("STOP", @current_pid)
|
371
|
+
@jobs[@job_id] = {pid: @current_pid, cmd: @cmd, status: :stopped}
|
372
|
+
else
|
373
|
+
puts "\nNo active job to suspend"
|
374
|
+
end
|
401
375
|
when 'C-K' # Kill/delete that entry in the history
|
402
376
|
@history.delete_at(@stk)
|
403
377
|
@stk -= 1
|
@@ -586,6 +560,8 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic
|
|
586
560
|
el.c(@c_nick)
|
587
561
|
elsif @gnick.include?(el)
|
588
562
|
el.c(@c_gnick)
|
563
|
+
elsif self.respond_to?(el) && singleton_class.instance_methods(false).include?(el.to_sym)
|
564
|
+
el.c(@c_nick).b # Ruby functions in bold nick color
|
589
565
|
elsif el[0] == "-"
|
590
566
|
el.c(@c_switch)
|
591
567
|
else
|
@@ -612,7 +588,93 @@ end
|
|
612
588
|
|
613
589
|
# RSH FUNCTIONS
|
614
590
|
def help
|
615
|
-
|
591
|
+
# Get terminal width
|
592
|
+
term_width = @maxcol || 80
|
593
|
+
col_width = 48 # Fixed width for left column
|
594
|
+
|
595
|
+
# Helper function to strip ANSI codes for length calculation
|
596
|
+
def strip_ansi(str)
|
597
|
+
str.gsub(/\001?\e\[[0-9;]*m\002?/, '')
|
598
|
+
end
|
599
|
+
|
600
|
+
left_col = []
|
601
|
+
right_col = []
|
602
|
+
|
603
|
+
# Left column content
|
604
|
+
left_col << "KEYBOARD SHORTCUTS:".c(@c_prompt).b
|
605
|
+
left_col << "RIGHT/Ctrl-F Accept suggestion"
|
606
|
+
left_col << "UP/DOWN Navigate history"
|
607
|
+
left_col << "TAB Tab complete"
|
608
|
+
left_col << "Ctrl-Y Copy to clipboard"
|
609
|
+
left_col << "Ctrl-D Exit + save .rshrc"
|
610
|
+
left_col << "Ctrl-E Exit without save"
|
611
|
+
left_col << "Ctrl-L Clear screen"
|
612
|
+
left_col << "Ctrl-Z Suspend job"
|
613
|
+
left_col << "Ctrl-C/G Clear line"
|
614
|
+
left_col << "Ctrl-K Delete history item"
|
615
|
+
left_col << "Ctrl-U Clear line"
|
616
|
+
left_col << "Ctrl-W Delete previous word"
|
617
|
+
left_col << ""
|
618
|
+
left_col << "SPECIAL COMMANDS:".c(@c_prompt).b
|
619
|
+
left_col << ":nick 'll = ls -l' Command alias"
|
620
|
+
left_col << ":gnick 'h = /home' General alias"
|
621
|
+
left_col << ":nickdel 'name' Delete nick"
|
622
|
+
left_col << ":gnickdel 'name' Delete gnick"
|
623
|
+
left_col << ":nick? List all nicks"
|
624
|
+
left_col << ":history Show history"
|
625
|
+
left_col << ":rmhistory Clear history"
|
626
|
+
left_col << ":info About rsh"
|
627
|
+
left_col << ":version Version info"
|
628
|
+
left_col << ":help This help"
|
629
|
+
|
630
|
+
# Right column content
|
631
|
+
right_col << "RUBY FUNCTIONS:".c(@c_prompt).b
|
632
|
+
right_col << ":defun 'f(x) = x*2' Define function"
|
633
|
+
right_col << ":defun? List functions"
|
634
|
+
right_col << ":defun '-f' Remove function"
|
635
|
+
right_col << "Call as: f 5 (returns 10)"
|
636
|
+
right_col << ""
|
637
|
+
right_col << "JOB CONTROL:".c(@c_prompt).b
|
638
|
+
right_col << "command & Background job"
|
639
|
+
right_col << ":jobs List jobs"
|
640
|
+
right_col << ":fg [id] Foreground job"
|
641
|
+
right_col << ":bg [id] Resume in bg"
|
642
|
+
right_col << ""
|
643
|
+
right_col << "INTEGRATIONS:".c(@c_prompt).b
|
644
|
+
right_col << "r Launch rtfm"
|
645
|
+
right_col << "f Launch fzf"
|
646
|
+
right_col << "= <expr> xrpn calculator"
|
647
|
+
right_col << ":<ruby code> Execute Ruby"
|
648
|
+
right_col << ""
|
649
|
+
right_col << "EXPANSIONS:".c(@c_prompt).b
|
650
|
+
right_col << "~ Home directory"
|
651
|
+
right_col << "$VAR, ${VAR} Environment var"
|
652
|
+
right_col << "$? Exit status"
|
653
|
+
right_col << "$(cmd), `cmd` Command subst"
|
654
|
+
right_col << "{a,b,c} Brace expansion"
|
655
|
+
right_col << "cmd1 && cmd2 Conditional"
|
656
|
+
right_col << "cmd1 || cmd2 Alternative"
|
657
|
+
|
658
|
+
# Pad columns to same length
|
659
|
+
max_lines = [left_col.length, right_col.length].max
|
660
|
+
left_col.fill("", left_col.length...max_lines)
|
661
|
+
right_col.fill("", right_col.length...max_lines)
|
662
|
+
|
663
|
+
# Print in two columns
|
664
|
+
puts
|
665
|
+
max_lines.times do |i|
|
666
|
+
left_text = left_col[i].to_s
|
667
|
+
right_text = right_col[i].to_s
|
668
|
+
# Calculate padding based on visible characters (without ANSI codes)
|
669
|
+
visible_length = strip_ansi(left_text).length
|
670
|
+
padding = col_width - visible_length
|
671
|
+
padding = 0 if padding < 0
|
672
|
+
puts " #{left_text}#{' ' * padding} #{right_text}"
|
673
|
+
end
|
674
|
+
puts
|
675
|
+
end
|
676
|
+
def info
|
677
|
+
puts @info
|
616
678
|
end
|
617
679
|
def version
|
618
680
|
puts "rsh version = #{@version} (latest RubyGems version is #{Gem.latest_version_for("ruby-shell").version} - https://github.com/isene/rsh)"
|
@@ -653,18 +715,181 @@ def nick? # Show nicks
|
|
653
715
|
puts " General nicks:".c(@c_gnick)
|
654
716
|
@gnick.sort.each {|key, value| puts " #{key} = #{value}"}
|
655
717
|
end
|
718
|
+
def nickdel(nick_name) # Delete a command nick
|
719
|
+
@nick.delete(nick_name)
|
720
|
+
rshrc
|
721
|
+
puts "Nick '#{nick_name}' deleted"
|
722
|
+
end
|
723
|
+
def gnickdel(nick_name) # Delete a general/global nick
|
724
|
+
@gnick.delete(nick_name)
|
725
|
+
rshrc
|
726
|
+
puts "General nick '#{nick_name}' deleted"
|
727
|
+
end
|
656
728
|
def dirs
|
657
729
|
puts "Past direactories:"
|
658
730
|
@dirs.each_with_index do |e,i|
|
659
731
|
puts "#{i}: #{e}"
|
660
732
|
end
|
661
733
|
end
|
734
|
+
def jobs
|
735
|
+
puts "Active jobs:"
|
736
|
+
@jobs.each do |id, job|
|
737
|
+
begin
|
738
|
+
Process.kill(0, job[:pid]) # Check if process exists
|
739
|
+
puts "[#{id}] #{job[:pid]} #{job[:status]} #{job[:cmd]}"
|
740
|
+
rescue Errno::ESRCH
|
741
|
+
@jobs.delete(id) # Clean up dead jobs
|
742
|
+
end
|
743
|
+
end
|
744
|
+
end
|
745
|
+
def fg(job_id = nil)
|
746
|
+
job_id ||= @jobs.keys.max
|
747
|
+
return puts "No jobs" if job_id.nil?
|
748
|
+
job = @jobs[job_id]
|
749
|
+
return puts "Job #{job_id} not found" unless job
|
750
|
+
puts "Bringing job #{job_id} to foreground: #{job[:cmd]}"
|
751
|
+
begin
|
752
|
+
if job[:status] == :stopped
|
753
|
+
Process.kill("CONT", job[:pid])
|
754
|
+
end
|
755
|
+
@current_pid = job[:pid]
|
756
|
+
Process.wait(job[:pid])
|
757
|
+
@jobs.delete(job_id)
|
758
|
+
@current_pid = nil
|
759
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
760
|
+
@jobs.delete(job_id)
|
761
|
+
@current_pid = nil
|
762
|
+
end
|
763
|
+
end
|
764
|
+
def bg(job_id = nil)
|
765
|
+
job_id ||= @jobs.keys.max
|
766
|
+
return puts "No jobs" if job_id.nil?
|
767
|
+
job = @jobs[job_id]
|
768
|
+
return puts "Job #{job_id} not found" unless job
|
769
|
+
return puts "Job #{job_id} already running" if job[:status] == :running
|
770
|
+
puts "Resuming job #{job_id} in background: #{job[:cmd]}"
|
771
|
+
begin
|
772
|
+
Process.kill("CONT", job[:pid])
|
773
|
+
@jobs[job_id][:status] = :running
|
774
|
+
rescue Errno::ESRCH
|
775
|
+
@jobs.delete(job_id)
|
776
|
+
puts "Job #{job_id} no longer exists"
|
777
|
+
end
|
778
|
+
end
|
779
|
+
def defun(func_def) # Define a Ruby function like: `:defun "myls(*args) = Dir.glob('*').each {|f| puts f}"`
|
780
|
+
if func_def.match(/^\s*-/)
|
781
|
+
# Remove function
|
782
|
+
func_name = func_def.sub(/^\s*-/, '')
|
783
|
+
if self.respond_to?(func_name)
|
784
|
+
singleton_class.remove_method(func_name.to_sym)
|
785
|
+
puts "Function '#{func_name}' removed"
|
786
|
+
else
|
787
|
+
puts "Function '#{func_name}' not found"
|
788
|
+
end
|
789
|
+
else
|
790
|
+
# Define function
|
791
|
+
# Extract function name, params, and body from "name(params) = body" format
|
792
|
+
if func_def =~ /^(\w+)\s*\(([^)]*)\)\s*=\s*(.+)$/
|
793
|
+
func_name = $1
|
794
|
+
func_params = $2
|
795
|
+
func_body = $3
|
796
|
+
|
797
|
+
begin
|
798
|
+
eval_code = "def #{func_name}(#{func_params}); #{func_body}; end"
|
799
|
+
puts " DEBUG: Evaluating: #{eval_code}" if ENV['RSH_DEBUG']
|
800
|
+
singleton_class.class_eval(eval_code)
|
801
|
+
puts "Function '#{func_name}' defined"
|
802
|
+
puts " DEBUG: Method created? #{respond_to?(func_name)}" if ENV['RSH_DEBUG']
|
803
|
+
rescue SyntaxError => e
|
804
|
+
puts "Syntax error in function definition: #{e}"
|
805
|
+
rescue => e
|
806
|
+
puts "Error in function definition: #{e}"
|
807
|
+
end
|
808
|
+
else
|
809
|
+
puts "Invalid function format. Use: name(params) = body"
|
810
|
+
end
|
811
|
+
end
|
812
|
+
rshrc
|
813
|
+
end
|
814
|
+
def defun? # Show all user-defined functions
|
815
|
+
puts "User-defined Ruby functions:"
|
816
|
+
# Get only methods defined by defun, excluding built-ins and rsh internals
|
817
|
+
all_methods = singleton_class.instance_methods(false)
|
818
|
+
puts " All singleton methods: #{all_methods}"
|
819
|
+
excluded = [:defun, :defun?, :execute_conditional, :expand_braces]
|
820
|
+
methods = all_methods - excluded
|
821
|
+
if methods.empty?
|
822
|
+
puts " (none defined after filtering)"
|
823
|
+
else
|
824
|
+
methods.each do |method|
|
825
|
+
puts " #{method}"
|
826
|
+
end
|
827
|
+
end
|
828
|
+
end
|
829
|
+
def execute_conditional(cmd_line)
|
830
|
+
# Split on && and || while preserving the operators
|
831
|
+
parts = cmd_line.split(/(\s*&&\s*|\s*\|\|\s*)/)
|
832
|
+
|
833
|
+
result = true
|
834
|
+
i = 0
|
835
|
+
while i < parts.length
|
836
|
+
command = parts[i].strip
|
837
|
+
next if command.empty?
|
838
|
+
|
839
|
+
if command == '&&'
|
840
|
+
i += 1
|
841
|
+
next unless result # Skip if previous command failed
|
842
|
+
elsif command == '||'
|
843
|
+
i += 1
|
844
|
+
next if result # Skip if previous command succeeded
|
845
|
+
else
|
846
|
+
# Execute the command
|
847
|
+
success = system(command)
|
848
|
+
result = success
|
849
|
+
puts " Command failed: #{command} (exit #{$?.exitstatus})" unless success
|
850
|
+
end
|
851
|
+
i += 1
|
852
|
+
end
|
853
|
+
end
|
854
|
+
def expand_braces(str)
|
855
|
+
# Simple brace expansion: {a,b,c} -> a b c
|
856
|
+
str.gsub(/\{([^}]+)\}/) do |match|
|
857
|
+
items = $1.split(',').map(&:strip)
|
858
|
+
items.join(' ')
|
859
|
+
end
|
860
|
+
end
|
662
861
|
|
663
862
|
# INITIAL SETUP
|
664
863
|
begin # Load .rshrc and populate @history
|
665
864
|
trap "SIGINT" do end
|
865
|
+
trap "SIGHUP" do
|
866
|
+
rshrc
|
867
|
+
exit
|
868
|
+
end
|
869
|
+
trap "SIGTERM" do
|
870
|
+
rshrc
|
871
|
+
exit
|
872
|
+
end
|
666
873
|
firstrun unless File.exist?(Dir.home+'/.rshrc') # Initial loading - to get history
|
667
|
-
load(Dir.home+'/.rshrc')
|
874
|
+
load(Dir.home+'/.rshrc')
|
875
|
+
# Load login shell files if rsh is running as login shell
|
876
|
+
if ENV['LOGIN_SHELL'] or $0 == "-rsh" or ARGV.include?('-l') or ARGV.include?('--login')
|
877
|
+
['/etc/profile', Dir.home+'/.profile', Dir.home+'/.bash_profile', Dir.home+'/.bashrc'].each do |f|
|
878
|
+
if File.exist?(f)
|
879
|
+
puts "Loading #{f}..." if ENV['RSH_DEBUG']
|
880
|
+
begin
|
881
|
+
# Source shell files by extracting export statements
|
882
|
+
File.readlines(f).each do |line|
|
883
|
+
if line =~ /^\s*export\s+(\w+)=(.*)/
|
884
|
+
ENV[$1] = $2.gsub(/['"]/, '')
|
885
|
+
end
|
886
|
+
end
|
887
|
+
rescue => e
|
888
|
+
puts "Warning: Could not source #{f}: #{e}" if ENV['RSH_DEBUG']
|
889
|
+
end
|
890
|
+
end
|
891
|
+
end
|
892
|
+
end
|
668
893
|
ENV["SHELL"] = __FILE__
|
669
894
|
ENV["TERM"] = "rxvt-unicode-256color"
|
670
895
|
ENV["PATH"] ? ENV["PATH"] += ":" : ENV["PATH"] = ""
|
@@ -736,41 +961,92 @@ loop do
|
|
736
961
|
elsif @cmd == '#' # List previous directories
|
737
962
|
dirs
|
738
963
|
else # Execute command
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
964
|
+
# Check if it's a user-defined Ruby function FIRST (before any expansions)
|
965
|
+
cmd_parts = @cmd.split(/\s+/)
|
966
|
+
func_name = cmd_parts[0]
|
967
|
+
if self.respond_to?(func_name) && singleton_class.instance_methods(false).include?(func_name.to_sym)
|
968
|
+
begin
|
969
|
+
args = cmd_parts[1..]
|
970
|
+
puts "DEBUG: Calling #{func_name} with args: #{args}" if ENV['RSH_DEBUG']
|
971
|
+
result = self.send(func_name, *args)
|
972
|
+
puts "DEBUG: Result: #{result.inspect}" if ENV['RSH_DEBUG']
|
973
|
+
puts result unless result.nil?
|
974
|
+
rescue => e
|
975
|
+
puts "Error calling function '#{func_name}': #{e}"
|
976
|
+
end
|
752
977
|
else
|
753
|
-
|
754
|
-
if @cmd
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
978
|
+
# Handle conditional execution (&& and ||)
|
979
|
+
if @cmd.include?('&&') || @cmd.include?('||')
|
980
|
+
execute_conditional(@cmd)
|
981
|
+
next
|
982
|
+
end
|
983
|
+
# Expand brace expansion {a,b,c}
|
984
|
+
@cmd = expand_braces(@cmd)
|
985
|
+
# Expand command substitution $(command) and backticks
|
986
|
+
@cmd = @cmd.gsub(/\$\(([^)]+)\)/) { `#{$1}`.chomp }
|
987
|
+
@cmd = @cmd.gsub(/`([^`]+)`/) { `#{$1}`.chomp }
|
988
|
+
# Expand environment variables and exit status
|
989
|
+
@cmd = @cmd.gsub(/\$\?/) { @last_exit.to_s }
|
990
|
+
@cmd = @cmd.gsub(/\$(\w+)|\$\{(\w+)\}/) { ENV[$1 || $2] || '' }
|
991
|
+
# Expand tilde
|
992
|
+
@cmd = @cmd.gsub(/~/, Dir.home)
|
993
|
+
ca = @nick.transform_keys {|k| /((^\K\s*\K)|(\|\K\s*\K))\b(?<!-)#{Regexp.escape k}\b/}
|
994
|
+
@cmd = @cmd.gsub(Regexp.union(ca.keys), @nick)
|
995
|
+
ga = @gnick.transform_keys {|k| /\b(?<!-)#{Regexp.escape k}\b/}
|
996
|
+
@cmd = @cmd.gsub(Regexp.union(ga.keys), @gnick)
|
997
|
+
@cmd = "~" if @cmd == "cd"
|
998
|
+
@cmd.sub!(/^cd (\S*).*/, '\1')
|
999
|
+
@cmd = Dir.home if @cmd == "~"
|
1000
|
+
@cmd = @dirs[1] if @cmd == "-"
|
1001
|
+
@cmd = @dirs[@cmd.to_i] if @cmd =~ /^\d$/
|
1002
|
+
# Check if it's a directory to change to
|
1003
|
+
dir = @cmd.strip.sub(/~/, Dir.home)
|
1004
|
+
if Dir.exist?(dir)
|
1005
|
+
Dir.chdir(dir)
|
1006
|
+
system("git status .") if Dir.exist?(".git")
|
1007
|
+
else
|
1008
|
+
puts "#{Time.now.strftime("%H:%M:%S")}: #{@cmd}".c(@c_stamp)
|
1009
|
+
if @cmd == "f" # fzf integration (https://github.com/junegunn/fzf)
|
1010
|
+
res = `fzf`.chomp
|
1011
|
+
Dir.chdir(File.dirname(res))
|
1012
|
+
elsif File.exist?(@cmd) and not File.executable?(@cmd) and not @cmd.include?(" ")
|
1013
|
+
# Only auto-open files if it's a single filename (no spaces = no command with args)
|
1014
|
+
if File.read(@cmd).force_encoding("UTF-8").valid_encoding?
|
1015
|
+
system("#{ENV['EDITOR']} #{@cmd}") # Try open with user's editor
|
763
1016
|
else
|
764
|
-
|
1017
|
+
if @runmailcap
|
1018
|
+
Thread.new { system("run-mailcap #{@cmd} 2>/dev/null") }
|
1019
|
+
else
|
1020
|
+
Thread.new { system("xdg-open #{@cmd} 2>/dev/null") }
|
1021
|
+
end
|
1022
|
+
end
|
1023
|
+
else
|
1024
|
+
begin
|
1025
|
+
pre_cmd
|
1026
|
+
# Handle background jobs
|
1027
|
+
if @cmd.end_with?(' &')
|
1028
|
+
@cmd = @cmd[0..-3] # Remove the &
|
1029
|
+
@job_id += 1
|
1030
|
+
# Handle pipes and redirections in background
|
1031
|
+
if @cmd.include?('|') || @cmd.include?('>') || @cmd.include?('<')
|
1032
|
+
pid = spawn(@cmd, pgroup: true)
|
1033
|
+
else
|
1034
|
+
pid = spawn(@cmd)
|
1035
|
+
end
|
1036
|
+
@jobs[@job_id] = {pid: pid, cmd: @cmd, status: :running}
|
1037
|
+
puts "[#{@job_id}] #{pid} #{@cmd}"
|
1038
|
+
else
|
1039
|
+
# Better handling of pipes and redirections
|
1040
|
+
@current_pid = spawn(@cmd)
|
1041
|
+
Process.wait(@current_pid)
|
1042
|
+
@last_exit = $?.exitstatus
|
1043
|
+
@current_pid = nil
|
1044
|
+
puts " Command failed: #{@cmd} (exit #{@last_exit})" unless @last_exit == 0
|
1045
|
+
end
|
1046
|
+
post_cmd
|
1047
|
+
rescue StandardError => err
|
1048
|
+
puts "\nError: #{err}"
|
765
1049
|
end
|
766
|
-
end
|
767
|
-
else
|
768
|
-
begin
|
769
|
-
pre_cmd
|
770
|
-
puts " Not executed: #{@cmd}" unless system (@cmd) # Try execute the command
|
771
|
-
post_cmd
|
772
|
-
rescue StandardError => err
|
773
|
-
puts "\n#{err}"
|
774
1050
|
end
|
775
1051
|
end
|
776
1052
|
end
|
metadata
CHANGED
@@ -1,20 +1,21 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-shell
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.8.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-08 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. UPDATE
|
15
|
+
v2.8.0: Enhanced help system with two-column display, new :info command, :nickdel/:gnickdel
|
16
|
+
commands for easier nick management. v2.7.0: Ruby Functions - define custom shell
|
17
|
+
commands using full Ruby power! Also: job control, command substitution, variable
|
18
|
+
expansion, conditional execution, and login shell support.'
|
18
19
|
email: g@isene.com
|
19
20
|
executables:
|
20
21
|
- rsh
|