ruby-shell 3.2.0 → 3.4.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/README.md +146 -17
- data/bin/rsh +627 -173
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b86a585bea4b0c41a27de5679365d3ddce7b7a33481927cbf0de9bd89dcf27b
|
|
4
|
+
data.tar.gz: f346b777aa2863b06ef610a8e9c13325711058452f8986e5d4565aecb8ae21d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 181420e5e35ca971f44affd54dbf9cadf1b13d658627d5cf2aa062b1d17678ed8521911b1208a5f3072eaa06a6e8840db1317e5a15d1e1d2de672fbaf66552cb
|
|
7
|
+
data.tar.gz: '084bb4b4227110ea0a2230e8455527ed8259fdcf8285ae6fd3dac571315f662c6e0243ee66caad5dc0bacb8157f82c47cf56a1213900ed3fe4f14a15fb4bcefa'
|
data/README.md
CHANGED
|
@@ -33,7 +33,26 @@ Or simply `gem install ruby-shell`.
|
|
|
33
33
|
* All colors are themeable in .rshrc (see github link for possibilities)
|
|
34
34
|
* Copy current command line to primary selection (paste w/middle button) with `Ctrl-y`
|
|
35
35
|
|
|
36
|
-
## NEW in v3.
|
|
36
|
+
## NEW in v3.4.0 - Intelligent Completion Learning ⭐⭐
|
|
37
|
+
* **Smart TAB Completion**: Shell learns which completions you use most and ranks them higher
|
|
38
|
+
* **Context-Aware Learning**: Separate learning for git, ls, docker, and all other commands
|
|
39
|
+
* **Completion Statistics**: `:completion_stats` shows learned patterns with visual charts
|
|
40
|
+
* **Manageable**: `:config completion_learning on|off`, `:completion_reset` to clear data
|
|
41
|
+
* **Persistent**: Learning data saves to .rshrc, works across sessions
|
|
42
|
+
* **Works Everywhere**: Commands, switches, subcommands - all get smarter over time
|
|
43
|
+
|
|
44
|
+
## v3.3.0 - Quote-less Syntax, Parametrized Nicks & More ⭐⭐⭐
|
|
45
|
+
* **No More Quotes**: Simplified syntax - `:nick la = ls -la` instead of `:nick "la = ls -la"`
|
|
46
|
+
* **Parametrized Nicks**: `:nick gp = git push origin {{branch}}` then use `gp branch=main`
|
|
47
|
+
* **Ctrl-G Multi-line Edit**: Press Ctrl-G to edit command in $EDITOR for complex scripts
|
|
48
|
+
* **Custom Validation Rules**: `:validate rm -rf / = block` prevents dangerous commands
|
|
49
|
+
* **Shell Script Support**: for/while/if loops work with full bash syntax
|
|
50
|
+
* **Cleaner Commands**: `:config auto_correct on`, `:bm work /tmp #dev`, `:theme dracula`
|
|
51
|
+
* **Simplified Architecture**: Removed :template (merged into :nick for simplicity)
|
|
52
|
+
* **Backward Compatible**: Old quote syntax still works for existing .rshrc files
|
|
53
|
+
* **Better UX**: Less typing, more powerful, feels more natural
|
|
54
|
+
|
|
55
|
+
## v3.2.0 - Plugin System & Productivity ⭐⭐⭐
|
|
37
56
|
* **Plugin Architecture**: Extensible plugin system with lifecycle hooks and extension points
|
|
38
57
|
* **Lifecycle Hooks**: on_startup, on_command_before, on_command_after, on_prompt
|
|
39
58
|
* **Extension Points**: add_completions (TAB completion), add_commands (custom commands)
|
|
@@ -109,22 +128,23 @@ Special functions/integrations:
|
|
|
109
128
|
* Use `:` followed by a Ruby expression to access the whole world of Ruby
|
|
110
129
|
|
|
111
130
|
Special commands:
|
|
112
|
-
* `:nick
|
|
113
|
-
* `:gnick
|
|
114
|
-
* `:nick` lists all command nicks, `:gnick` lists general nicks
|
|
115
|
-
* `:nick
|
|
131
|
+
* `:nick ll = ls -l` to make a command alias (ll) point to a command (ls -l)
|
|
132
|
+
* `:gnick h = /home/me` to make a general alias (h) point to something (/home/me)
|
|
133
|
+
* `:nick` lists all command nicks, `:gnick` lists general nicks
|
|
134
|
+
* `:nick -name` delete a command nick, `:gnick -name` delete a general nick
|
|
116
135
|
* `:history` will list the command history, while `:rmhistory` will delete the history
|
|
117
136
|
* `:jobs` will list background jobs, `:fg [job_id]` brings jobs to foreground, `:bg [job_id]` resumes stopped jobs
|
|
118
|
-
* `:defun
|
|
119
|
-
* `:defun?` lists all user-defined functions, `:defun
|
|
120
|
-
* `:stats` shows command execution statistics
|
|
121
|
-
* `:bm
|
|
122
|
-
* `:bm` lists all bookmarks, just type bookmark name to jump (e.g., `work`)
|
|
123
|
-
* `:bm
|
|
124
|
-
* `:save_session
|
|
125
|
-
* `:list_sessions` shows all saved sessions, `:rmsession
|
|
126
|
-
* `:theme
|
|
127
|
-
* `:plugins` lists plugins, `:plugins
|
|
137
|
+
* `:defun func(args) = code` defines Ruby functions callable as shell commands (persistent!)
|
|
138
|
+
* `:defun?` lists all user-defined functions, `:defun -func` removes functions
|
|
139
|
+
* `:stats` shows command execution statistics, `:stats --graph` for visual charts, `:stats --clear` to reset
|
|
140
|
+
* `:bm name` or `:bookmark name` bookmark current directory, `:bm name path #tags` with tags
|
|
141
|
+
* `:bm` lists all bookmarks, just type bookmark name to jump (e.g., `work`)
|
|
142
|
+
* `:bm -name` delete bookmark, `:bm ?tag` search by tag, `:bm --stats` show statistics
|
|
143
|
+
* `:save_session name` saves named session, `:load_session name` loads session
|
|
144
|
+
* `:list_sessions` shows all saved sessions, `:rmsession name` or `:rmsession *` deletes
|
|
145
|
+
* `:theme name` applies color scheme, `:config` manages settings, `:env` manages environment
|
|
146
|
+
* `:plugins` lists plugins, `:plugins disable name` disables, `:plugins reload` reloads
|
|
147
|
+
* `:calc expression` inline calculator with Ruby Math library
|
|
128
148
|
* `:info` shows introduction and feature overview
|
|
129
149
|
* `:version` Shows the rsh version number and the last published gem file version
|
|
130
150
|
* `:help` will display a compact command reference in two columns
|
|
@@ -151,10 +171,82 @@ Add to your `.rshrc`:
|
|
|
151
171
|
```
|
|
152
172
|
|
|
153
173
|
## Moving around
|
|
154
|
-
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
|
|
174
|
+
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`.
|
|
155
175
|
|
|
156
176
|
## Nicks
|
|
157
|
-
|
|
177
|
+
|
|
178
|
+
Nicks are powerful aliases that can be simple command shortcuts or complex parametrized templates.
|
|
179
|
+
|
|
180
|
+
### Simple Nicks
|
|
181
|
+
```bash
|
|
182
|
+
:nick ls = ls --color # Simple alias
|
|
183
|
+
:nick la = ls -la # Another shortcut
|
|
184
|
+
:nick # List all nicks
|
|
185
|
+
:nick -la # Delete a nick
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Parametrized Nicks (NEW in v3.3!)
|
|
189
|
+
Create templates with `{{placeholder}}` parameters:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# Git shortcuts with branch parameter
|
|
193
|
+
:nick gp = git push origin {{branch}}
|
|
194
|
+
gp branch=main # Executes: git push origin main
|
|
195
|
+
gp branch=develop # Executes: git push origin develop
|
|
196
|
+
|
|
197
|
+
# Deployment with multiple parameters
|
|
198
|
+
:nick deploy = ssh {{user}}@{{host}} 'cd {{path}} && git pull'
|
|
199
|
+
deploy user=admin host=prod path=/var/www
|
|
200
|
+
# Executes: ssh admin@prod 'cd /var/www && git pull'
|
|
201
|
+
|
|
202
|
+
# Backup with source and destination
|
|
203
|
+
:nick backup = rsync -av {{src}} {{dest}}
|
|
204
|
+
backup src=/data dest=/backup
|
|
205
|
+
# Executes: rsync -av /data /backup
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**How it works:**
|
|
209
|
+
- Define nick with `{{param}}` placeholders
|
|
210
|
+
- Use with `key=value` syntax
|
|
211
|
+
- Parameters auto-expand and get stripped from final command
|
|
212
|
+
- Works with any number of parameters
|
|
213
|
+
|
|
214
|
+
### General Nicks (gnicks)
|
|
215
|
+
Substitute anywhere on command line (not just commands):
|
|
216
|
+
```bash
|
|
217
|
+
:gnick h = /home/user # Directory shortcut
|
|
218
|
+
:gnick # List all gnicks
|
|
219
|
+
:gnick -h # Delete a gnick
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Multi-line Command Editing (v3.3.0+)
|
|
223
|
+
|
|
224
|
+
Press **Ctrl-G** to edit the current command in your $EDITOR:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# Start typing a complex command
|
|
228
|
+
for i in {1..10}
|
|
229
|
+
|
|
230
|
+
# Press Ctrl-G
|
|
231
|
+
# Your editor opens with the command
|
|
232
|
+
# Add more lines:
|
|
233
|
+
for i in {1..10}
|
|
234
|
+
echo "Processing: $i"
|
|
235
|
+
sleep 1
|
|
236
|
+
done
|
|
237
|
+
|
|
238
|
+
# Save and quit
|
|
239
|
+
# Command appears on command line (converted to single-line with ;)
|
|
240
|
+
# Press ENTER to execute
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Perfect for:**
|
|
244
|
+
- Complex shell scripts
|
|
245
|
+
- Long commands with many options
|
|
246
|
+
- Multi-line constructs (for, while, if)
|
|
247
|
+
- Commands you want to review/edit carefully
|
|
248
|
+
|
|
249
|
+
---
|
|
158
250
|
|
|
159
251
|
## Tab completion
|
|
160
252
|
You can tab complete almost anything. Hitting `TAB` will try to complete in this priority: nicks, gnicks, commands, dirs/files. Special completions:
|
|
@@ -227,6 +319,43 @@ Ruby functions have access to:
|
|
|
227
319
|
- JSON/XML parsing
|
|
228
320
|
- And everything else Ruby can do!
|
|
229
321
|
|
|
322
|
+
## Custom Validation Rules (v3.3.0+)
|
|
323
|
+
|
|
324
|
+
Create safety rules to block, confirm, warn, or log specific command patterns:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
# Block dangerous commands completely
|
|
328
|
+
:validate rm -rf / = block
|
|
329
|
+
|
|
330
|
+
# Require confirmation for risky operations
|
|
331
|
+
:validate git push --force = confirm
|
|
332
|
+
:validate DROP TABLE = confirm
|
|
333
|
+
|
|
334
|
+
# Show warnings but allow execution
|
|
335
|
+
:validate sudo = warn
|
|
336
|
+
:validate chmod 777 = warn
|
|
337
|
+
|
|
338
|
+
# Log specific commands for audit trail
|
|
339
|
+
:validate npm install = log
|
|
340
|
+
# Logs to ~/.rsh_validation.log
|
|
341
|
+
|
|
342
|
+
# List all rules
|
|
343
|
+
:validate
|
|
344
|
+
|
|
345
|
+
# Delete rule by index
|
|
346
|
+
:validate -1
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Actions:**
|
|
350
|
+
- `block` - Prevent command execution completely
|
|
351
|
+
- `confirm` - Ask for confirmation (y/N)
|
|
352
|
+
- `warn` - Show warning but allow
|
|
353
|
+
- `log` - Silently log to ~/.rsh_validation.log
|
|
354
|
+
|
|
355
|
+
**Pattern matching:** Uses regex, so you can match complex patterns.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
230
359
|
## Plugin System (v3.2.0+)
|
|
231
360
|
|
|
232
361
|
rsh supports a powerful plugin system for extending functionality. Plugins are Ruby classes placed in `~/.rsh/plugins/` that can:
|
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 = "3.
|
|
11
|
+
@version = "3.4.0" # Completion learning: Shell learns your patterns and boosts frequently-used completions
|
|
12
12
|
|
|
13
13
|
# MODULES, CLASSES AND EXTENSIONS
|
|
14
14
|
class String # Add coloring to strings (with escaping for Readline)
|
|
@@ -150,6 +150,11 @@ begin # Initialization
|
|
|
150
150
|
@plugins = [] # Loaded plugin instances
|
|
151
151
|
@plugin_disabled = [] # List of disabled plugin names
|
|
152
152
|
@plugin_commands = {} # Commands added by plugins
|
|
153
|
+
@validation_rules = [] # Custom validation rules
|
|
154
|
+
@completion_weights = {} # Completion learning weights
|
|
155
|
+
@completion_learning = true # Enable completion learning (default: on)
|
|
156
|
+
@recording = {active: false, name: nil, commands: []} # Command recording state
|
|
157
|
+
@recordings = {} # Saved recordings
|
|
153
158
|
# Built-in rsh commands are called with : prefix, so no need for separate tracking
|
|
154
159
|
Dir.mkdir(Dir.home + '/.rsh') unless Dir.exist?(Dir.home + '/.rsh')
|
|
155
160
|
Dir.mkdir(@session_dir) unless Dir.exist?(@session_dir)
|
|
@@ -161,7 +166,7 @@ end
|
|
|
161
166
|
# HELP TEXT
|
|
162
167
|
@info = <<~INFO
|
|
163
168
|
|
|
164
|
-
Hello #{@user}, welcome to rsh v3.
|
|
169
|
+
Hello #{@user}, welcome to rsh v3.3 - the Ruby SHell.
|
|
165
170
|
|
|
166
171
|
rsh does not attempt to compete with the grand old shells like bash and zsh.
|
|
167
172
|
It serves the specific needs and wants of its author. If you like it, then feel free
|
|
@@ -187,17 +192,26 @@ end
|
|
|
187
192
|
* Syntax validation - Pre-execution warnings for dangerous or malformed commands
|
|
188
193
|
* Unified command syntax - :nick, :gnick, :bm all work the same way (list/create/delete)
|
|
189
194
|
|
|
190
|
-
NEW in v3.
|
|
195
|
+
NEW in v3.4:
|
|
196
|
+
* Completion learning - Shell learns which TAB completions you use and ranks them higher
|
|
197
|
+
* Context-aware - Separate learning for each command (git, ls, docker, etc.)
|
|
198
|
+
* :completion_stats - View learned patterns with visual bar charts
|
|
199
|
+
* Persistent - Learning data saves to .rshrc across sessions
|
|
200
|
+
|
|
201
|
+
v3.3 Features:
|
|
202
|
+
* Quote-less syntax - No more quotes! Use :nick la = ls -la
|
|
203
|
+
* Parametrized nicks - :nick gp = git push origin {{branch}}, then: gp branch=main
|
|
204
|
+
* Ctrl-G multi-line edit - Press Ctrl-G to edit command in $EDITOR
|
|
205
|
+
* Custom validation - :validate rm -rf / = block prevents dangerous commands
|
|
206
|
+
* Shell script support - for/while/if loops work with full bash syntax
|
|
207
|
+
|
|
208
|
+
v3.2 Features:
|
|
191
209
|
* Plugin system - Extensible architecture for custom commands, completions, and hooks
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* Auto-correct typos - :config "auto_correct" "on" automatically fixes gti → closest match
|
|
196
|
-
* Command timing alerts - :config "slow_command_threshold" "5" warns on slow commands
|
|
197
|
-
* Inline calculator - :calc 2 + 2, :calc "Math.sqrt(16)", full Ruby Math library
|
|
210
|
+
* Auto-correct typos - :config auto_correct on (with confirmation prompt)
|
|
211
|
+
* Command timing alerts - :config slow_command_threshold 5 warns on slow commands
|
|
212
|
+
* Inline calculator - :calc 2 + 2, :calc "Math::PI", full Ruby Math library
|
|
198
213
|
* Enhanced history - !!, !-2, !5:7 for repeat last, nth-to-last, and chaining
|
|
199
214
|
* Stats visualization - :stats --graph for colorful ASCII bar charts
|
|
200
|
-
* See PLUGIN_GUIDE.md for complete plugin development documentation
|
|
201
215
|
|
|
202
216
|
v3.1 Features:
|
|
203
217
|
* Multiple named sessions - :save_session "project" and :load_session "project"
|
|
@@ -355,8 +369,23 @@ def getstr # A custom Readline-like function
|
|
|
355
369
|
chr = getchr
|
|
356
370
|
puts "DEBUG: Got char: '#{chr}' (length: #{chr.length})" if ENV['RSH_DEBUG']
|
|
357
371
|
case chr
|
|
358
|
-
when 'C-G'
|
|
359
|
-
|
|
372
|
+
when 'C-G' # Ctrl-G opens command in $EDITOR
|
|
373
|
+
temp_file = "/tmp/rsh_edit_#{Process.pid}.tmp"
|
|
374
|
+
File.write(temp_file, @history[0] || "")
|
|
375
|
+
system("#{ENV['EDITOR'] || 'vi'} #{temp_file}")
|
|
376
|
+
if File.exist?(temp_file)
|
|
377
|
+
edited = File.read(temp_file).strip
|
|
378
|
+
# Convert multi-line to single line with proper separators
|
|
379
|
+
if edited.include?("\n")
|
|
380
|
+
# Join lines with semicolons, preserving quoted strings
|
|
381
|
+
edited = edited.split("\n").map(&:strip).reject(&:empty?).join('; ')
|
|
382
|
+
end
|
|
383
|
+
@history[0] = edited
|
|
384
|
+
@pos = edited.length
|
|
385
|
+
File.delete(temp_file)
|
|
386
|
+
end
|
|
387
|
+
when 'C-C'
|
|
388
|
+
@history[0] = ""
|
|
360
389
|
@pos = 0
|
|
361
390
|
when 'C-E' # Ctrl-C exits gracefully but without updating .rshrc
|
|
362
391
|
print "\n"
|
|
@@ -486,7 +515,7 @@ def getstr # A custom Readline-like function
|
|
|
486
515
|
lift = true
|
|
487
516
|
when 'S-TAB'
|
|
488
517
|
@ci = nil
|
|
489
|
-
|
|
518
|
+
tab("hist")
|
|
490
519
|
lift = true
|
|
491
520
|
when /^.$/
|
|
492
521
|
@history[0].insert(@pos,chr)
|
|
@@ -546,20 +575,29 @@ def tab(type)
|
|
|
546
575
|
last_cmd = nil
|
|
547
576
|
end
|
|
548
577
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
type = "
|
|
552
|
-
|
|
553
|
-
type = "
|
|
554
|
-
|
|
555
|
-
type = "
|
|
556
|
-
when "export", "unset"
|
|
557
|
-
type = "env_vars"
|
|
578
|
+
# Check for colon command arguments
|
|
579
|
+
if @pretab =~ /:record\s*$/
|
|
580
|
+
type = "record_args"
|
|
581
|
+
elsif @pretab =~ /:replay\s*$/
|
|
582
|
+
type = "replay_args"
|
|
583
|
+
elsif @pretab =~ /:plugins\s*$/
|
|
584
|
+
type = "plugin_args"
|
|
558
585
|
else
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
type = "
|
|
562
|
-
|
|
586
|
+
case last_cmd
|
|
587
|
+
when "cd", "pushd", "rmdir"
|
|
588
|
+
type = "dirs_only"
|
|
589
|
+
when "vim", "nano", "cat", "less", "more", "head", "tail", "file"
|
|
590
|
+
type = "files_only"
|
|
591
|
+
when "man", "info", "which", "whatis"
|
|
592
|
+
type = "commands_only"
|
|
593
|
+
when "export", "unset"
|
|
594
|
+
type = "env_vars"
|
|
595
|
+
else
|
|
596
|
+
# Check if command has defined completions and we're on the first argument
|
|
597
|
+
if @cmd_completions.key?(last_cmd) && cmd_parts.length == 1
|
|
598
|
+
type = "cmd_subcommands"
|
|
599
|
+
@current_cmd = last_cmd
|
|
600
|
+
end
|
|
563
601
|
end
|
|
564
602
|
end
|
|
565
603
|
end
|
|
@@ -570,6 +608,27 @@ def tab(type)
|
|
|
570
608
|
@tabarray = @history.select {|el| el =~ /#{@tabstr}/} # Select history items matching @tabstr
|
|
571
609
|
@tabarray.shift # Take away @history[0]
|
|
572
610
|
return if @tabarray.empty?
|
|
611
|
+
when "record_args"
|
|
612
|
+
# Completions for :record command
|
|
613
|
+
@tabarray = %w[start stop status show]
|
|
614
|
+
# Add existing recording names for show
|
|
615
|
+
@tabarray += @recordings.keys.map { |k| "show #{k}" } if @recordings.any?
|
|
616
|
+
# Add delete options
|
|
617
|
+
@tabarray += @recordings.keys.map { |k| "-#{k}" } if @recordings.any?
|
|
618
|
+
when "replay_args"
|
|
619
|
+
# Completions for :replay command - just recording names
|
|
620
|
+
@tabarray = @recordings.keys
|
|
621
|
+
when "plugin_args"
|
|
622
|
+
# Completions for :plugins command
|
|
623
|
+
@tabarray = %w[reload info]
|
|
624
|
+
# Add plugin names for enable/disable/info
|
|
625
|
+
if @plugins.any?
|
|
626
|
+
@tabarray += @plugins.map { |p| "disable #{p[:name]}" }
|
|
627
|
+
@tabarray += @plugins.map { |p| "info #{p[:name]}" }
|
|
628
|
+
end
|
|
629
|
+
if @plugin_disabled.any?
|
|
630
|
+
@tabarray += @plugin_disabled.map { |p| "enable #{p}" }
|
|
631
|
+
end
|
|
573
632
|
when "switch"
|
|
574
633
|
cmdswitch = @pretab.split(/[|, ]/).last.to_s.strip
|
|
575
634
|
@tabarray = get_command_switches(cmdswitch)
|
|
@@ -620,7 +679,9 @@ def tab(type)
|
|
|
620
679
|
:nick :gnick :bm :bookmark :stats :defun :defun?
|
|
621
680
|
:history :rmhistory :jobs :fg :bg
|
|
622
681
|
:save_session :load_session :list_sessions :delete_session :rmsession
|
|
623
|
-
:config :env :theme :plugins :calc
|
|
682
|
+
:config :env :theme :plugins :calc :validate
|
|
683
|
+
:completion_stats :completion_reset
|
|
684
|
+
:record :replay
|
|
624
685
|
:info :version :help
|
|
625
686
|
]
|
|
626
687
|
search_str = @tabstr[1..-1] || "" # Remove leading :
|
|
@@ -701,6 +762,19 @@ def tab(type)
|
|
|
701
762
|
end
|
|
702
763
|
return if @tabarray.empty?
|
|
703
764
|
@tabarray.delete("") # Don't remember why
|
|
765
|
+
|
|
766
|
+
# Apply completion learning to sort results
|
|
767
|
+
if @completion_learning && type != "hist"
|
|
768
|
+
# Determine context for learning
|
|
769
|
+
completion_context = if @pretab && !@pretab.empty?
|
|
770
|
+
# Use the command being completed
|
|
771
|
+
@pretab.strip.split(/[|;&]/).last.strip.split.first || "all"
|
|
772
|
+
else
|
|
773
|
+
"all"
|
|
774
|
+
end
|
|
775
|
+
@tabarray = sort_by_learning(completion_context, @tabarray)
|
|
776
|
+
end
|
|
777
|
+
|
|
704
778
|
@c.clear_screen_down # Here we go
|
|
705
779
|
max_items = @completion_limit || 5
|
|
706
780
|
@tabarray.length.to_i - i < max_items ? l = @tabarray.length.to_i - i : l = max_items
|
|
@@ -786,6 +860,18 @@ def tab(type)
|
|
|
786
860
|
@c.row(@c_row)
|
|
787
861
|
@c.col(@c_col)
|
|
788
862
|
@history[0] = @newhist0
|
|
863
|
+
|
|
864
|
+
# Track completion selection for learning
|
|
865
|
+
if @completion_learning && @tabarray && @tabarray[i] && type != "hist"
|
|
866
|
+
completion_context = if @pretab && !@pretab.empty?
|
|
867
|
+
@pretab.strip.split(/[|;&]/).last.strip.split.first || "all"
|
|
868
|
+
else
|
|
869
|
+
"all"
|
|
870
|
+
end
|
|
871
|
+
selected = @tabarray[i]
|
|
872
|
+
selected = selected.sub(/\s*(-.*?)[,\s].*/, '\1') if type == "switch"
|
|
873
|
+
track_completion(completion_context, selected)
|
|
874
|
+
end
|
|
789
875
|
end
|
|
790
876
|
def nextline # Handle going to the next line in the terminal
|
|
791
877
|
row, col = @c.pos
|
|
@@ -907,6 +993,7 @@ def config(*args) # Configure rsh settings
|
|
|
907
993
|
puts " session_autosave: #{@session_autosave}s #{@session_autosave > 0 ? '(enabled)' : '(disabled)'}"
|
|
908
994
|
puts " auto_correct: #{@auto_correct ? 'on' : 'off'}"
|
|
909
995
|
puts " slow_command_threshold: #{@slow_command_threshold}s #{@slow_command_threshold > 0 ? '(enabled)' : '(disabled)'}"
|
|
996
|
+
puts " completion_learning: #{@completion_learning ? 'on' : 'off'}"
|
|
910
997
|
puts " completion_limit: #{@completion_limit}"
|
|
911
998
|
puts " completion_fuzzy: #{@completion_fuzzy}"
|
|
912
999
|
puts " completion_case_sensitive: #{@completion_case_sensitive}"
|
|
@@ -935,13 +1022,17 @@ def config(*args) # Configure rsh settings
|
|
|
935
1022
|
@slow_command_threshold = value.to_i
|
|
936
1023
|
puts "Slow command threshold set to #{value}s #{value.to_i > 0 ? '(enabled)' : '(disabled)'}"
|
|
937
1024
|
rshrc
|
|
1025
|
+
when 'completion_learning'
|
|
1026
|
+
@completion_learning = %w[on true yes 1].include?(value.to_s.downcase)
|
|
1027
|
+
puts "Completion learning #{@completion_learning ? 'enabled' : 'disabled'}"
|
|
1028
|
+
rshrc
|
|
938
1029
|
when 'completion_limit'
|
|
939
1030
|
@completion_limit = value.to_i
|
|
940
1031
|
puts "Completion limit set to #{value}"
|
|
941
1032
|
rshrc
|
|
942
1033
|
else
|
|
943
1034
|
puts "Unknown setting '#{setting}'"
|
|
944
|
-
puts "Available: history_dedup, session_autosave, auto_correct, slow_command_threshold, completion_limit"
|
|
1035
|
+
puts "Available: history_dedup, session_autosave, auto_correct, slow_command_threshold, completion_learning, completion_limit"
|
|
945
1036
|
end
|
|
946
1037
|
end
|
|
947
1038
|
def env(*args) # Environment variable management
|
|
@@ -1058,6 +1149,14 @@ def rshrc # Write updates to .rshrc
|
|
|
1058
1149
|
conf += "@slow_command_threshold = #{@slow_command_threshold}\n" if @slow_command_threshold && @slow_command_threshold > 0
|
|
1059
1150
|
conf.sub!(/^@plugin_disabled.*(\n|$)/, "")
|
|
1060
1151
|
conf += "@plugin_disabled = #{@plugin_disabled}\n" unless @plugin_disabled.empty?
|
|
1152
|
+
conf.sub!(/^@validation_rules.*(\n|$)/, "")
|
|
1153
|
+
conf += "@validation_rules = #{@validation_rules}\n" unless @validation_rules.empty?
|
|
1154
|
+
conf.sub!(/^@completion_weights.*(\n|$)/, "")
|
|
1155
|
+
conf += "@completion_weights = #{@completion_weights}\n" unless @completion_weights.empty?
|
|
1156
|
+
conf.sub!(/^@completion_learning.*(\n|$)/, "")
|
|
1157
|
+
conf += "@completion_learning = #{@completion_learning}\n" unless @completion_learning
|
|
1158
|
+
conf.sub!(/^@recordings.*(\n|$)/, "")
|
|
1159
|
+
conf += "@recordings = #{@recordings}\n" unless @recordings.empty?
|
|
1061
1160
|
# Only write @cmd_completions if user has customized it
|
|
1062
1161
|
unless conf =~ /^@cmd_completions\s*=/
|
|
1063
1162
|
# Don't write default completions to avoid cluttering .rshrc
|
|
@@ -1072,128 +1171,129 @@ def rshrc # Write updates to .rshrc
|
|
|
1072
1171
|
puts "Warning: Error saving history: #{e.message}"
|
|
1073
1172
|
end
|
|
1074
1173
|
File.write(Dir.home+'/.rshrc', conf)
|
|
1075
|
-
puts "
|
|
1174
|
+
puts ".rshrc updated"
|
|
1076
1175
|
end
|
|
1077
1176
|
|
|
1078
1177
|
# RSH FUNCTIONS
|
|
1079
1178
|
def help
|
|
1080
1179
|
# Get terminal width
|
|
1081
|
-
term_width = @maxcol ||
|
|
1082
|
-
col_width =
|
|
1083
|
-
|
|
1180
|
+
term_width = @maxcol || 120
|
|
1181
|
+
col_width = 36 # Width for each of 3 columns (wider)
|
|
1182
|
+
|
|
1084
1183
|
# Helper function to strip ANSI codes for length calculation
|
|
1085
1184
|
def strip_ansi(str)
|
|
1086
1185
|
str.gsub(/\001?\e\[[0-9;]*m\002?/, '')
|
|
1087
1186
|
end
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
right_col << "$? Exit status"
|
|
1177
|
-
right_col << "$(cmd), `cmd` Command subst"
|
|
1178
|
-
right_col << "{a,b,c} Brace expansion"
|
|
1179
|
-
right_col << "cmd1 && cmd2 Conditional"
|
|
1180
|
-
right_col << "cmd1 || cmd2 Alternative"
|
|
1181
|
-
|
|
1187
|
+
|
|
1188
|
+
col1 = []
|
|
1189
|
+
col2 = []
|
|
1190
|
+
col3 = []
|
|
1191
|
+
|
|
1192
|
+
# Column 1: Keyboard + Commands + Jobs
|
|
1193
|
+
col1 << "KEYBOARD:".c(@c_prompt).b
|
|
1194
|
+
col1 << "Ctrl-G Edit in \$EDITOR"
|
|
1195
|
+
col1 << "Ctrl-Y Copy line"
|
|
1196
|
+
col1 << "Ctrl-D Exit + save"
|
|
1197
|
+
col1 << "Ctrl-C Clear line"
|
|
1198
|
+
col1 << "TAB Complete"
|
|
1199
|
+
col1 << "Shift-TAB History search"
|
|
1200
|
+
col1 << ""
|
|
1201
|
+
col1 << "CORE COMMANDS:".c(@c_prompt).b
|
|
1202
|
+
col1 << ":nick a = b Alias"
|
|
1203
|
+
col1 << ":nick gp={{br}} Parametrized"
|
|
1204
|
+
col1 << ":bm name Bookmark"
|
|
1205
|
+
col1 << ":defun f()=x Function"
|
|
1206
|
+
col1 << ":stats Analytics"
|
|
1207
|
+
col1 << ":validate p=a Safety rules"
|
|
1208
|
+
col1 << ":calc expr Calculator"
|
|
1209
|
+
col1 << ":theme name Color schemes"
|
|
1210
|
+
col1 << ":plugins Extensions"
|
|
1211
|
+
col1 << ""
|
|
1212
|
+
col1 << "JOBS:".c(@c_prompt).b
|
|
1213
|
+
col1 << "cmd & Background"
|
|
1214
|
+
col1 << ":jobs List jobs"
|
|
1215
|
+
col1 << ":fg [id] Foreground"
|
|
1216
|
+
|
|
1217
|
+
# Column 2: Sessions + Bookmarks + Recording
|
|
1218
|
+
col2 << "SESSIONS:".c(@c_prompt).b
|
|
1219
|
+
col2 << ":save_session nm Save state"
|
|
1220
|
+
col2 << ":load_session nm Load state"
|
|
1221
|
+
col2 << ":list_sessions Show all"
|
|
1222
|
+
col2 << ":rmsession nm|* Delete"
|
|
1223
|
+
col2 << ""
|
|
1224
|
+
col2 << "BOOKMARKS:".c(@c_prompt).b
|
|
1225
|
+
col2 << ":bm nm path #tag Create"
|
|
1226
|
+
col2 << "name Jump to bookmark"
|
|
1227
|
+
col2 << ":bm List all"
|
|
1228
|
+
col2 << ":bm --stats Statistics"
|
|
1229
|
+
col2 << ":bm --export f Export"
|
|
1230
|
+
col2 << ""
|
|
1231
|
+
col2 << "RECORDING:".c(@c_prompt).b
|
|
1232
|
+
col2 << ":record start nm Start recording"
|
|
1233
|
+
col2 << ":record stop Stop recording"
|
|
1234
|
+
col2 << ":record show nm Show commands"
|
|
1235
|
+
col2 << ":record -nm Delete"
|
|
1236
|
+
col2 << ":replay nm Execute"
|
|
1237
|
+
col2 << ":record List all"
|
|
1238
|
+
col2 << ""
|
|
1239
|
+
col2 << "FEATURES:".c(@c_prompt).b
|
|
1240
|
+
col2 << "gp branch=main Nick template"
|
|
1241
|
+
col2 << "!! Repeat last"
|
|
1242
|
+
col2 << "!-2 2nd to last"
|
|
1243
|
+
col2 << "!5:7 Chain commands"
|
|
1244
|
+
col2 << ":stats --graph Visual charts"
|
|
1245
|
+
col2 << ":completion_stats Learn patterns"
|
|
1246
|
+
|
|
1247
|
+
# Column 3: Config + Integrations + Expansions
|
|
1248
|
+
col3 << "CONFIG:".c(@c_prompt).b
|
|
1249
|
+
col3 << ":config auto_correct on Auto-fix"
|
|
1250
|
+
col3 << ":config completion_learning on Learn TAB"
|
|
1251
|
+
col3 << ":config slow_command_threshold 5 Slow warn"
|
|
1252
|
+
col3 << ":config session_autosave 300 Auto-save"
|
|
1253
|
+
col3 << ":config history_dedup smart Dedup"
|
|
1254
|
+
col3 << ""
|
|
1255
|
+
col3 << "INTEGRATIONS:".c(@c_prompt).b
|
|
1256
|
+
col3 << "r rtfm file manager"
|
|
1257
|
+
col3 << "f fzf fuzzy finder"
|
|
1258
|
+
col3 << "= expr xrpn calculator"
|
|
1259
|
+
col3 << "@ text AI text response"
|
|
1260
|
+
col3 << "@@ cmd AI command suggest"
|
|
1261
|
+
col3 << ""
|
|
1262
|
+
col3 << "EXPANSIONS:".c(@c_prompt).b
|
|
1263
|
+
col3 << "~ Home directory"
|
|
1264
|
+
col3 << "$VAR Environment var"
|
|
1265
|
+
col3 << "$(cmd) Command subst"
|
|
1266
|
+
col3 << "{a,b,c} Brace expansion"
|
|
1267
|
+
col3 << "cmd1 && cmd2 Conditional AND"
|
|
1268
|
+
col3 << "for i in... Bash scripts"
|
|
1269
|
+
col3 << ""
|
|
1270
|
+
col3 << "MORE:".c(@c_prompt).b
|
|
1271
|
+
col3 << ":help This help"
|
|
1272
|
+
col3 << ":info About rsh"
|
|
1273
|
+
col3 << ":version Version info"
|
|
1274
|
+
|
|
1182
1275
|
# Pad columns to same length
|
|
1183
|
-
max_lines = [
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1276
|
+
max_lines = [col1.length, col2.length, col3.length].max
|
|
1277
|
+
col1.fill("", col1.length...max_lines)
|
|
1278
|
+
col2.fill("", col2.length...max_lines)
|
|
1279
|
+
col3.fill("", col3.length...max_lines)
|
|
1280
|
+
|
|
1281
|
+
# Print in three columns
|
|
1188
1282
|
puts
|
|
1189
1283
|
max_lines.times do |i|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
padding
|
|
1195
|
-
|
|
1196
|
-
|
|
1284
|
+
text1 = col1[i].to_s
|
|
1285
|
+
text2 = col2[i].to_s
|
|
1286
|
+
text3 = col3[i].to_s
|
|
1287
|
+
|
|
1288
|
+
# Calculate padding for each column
|
|
1289
|
+
vis1 = strip_ansi(text1).length
|
|
1290
|
+
vis2 = strip_ansi(text2).length
|
|
1291
|
+
pad1 = col_width - vis1
|
|
1292
|
+
pad2 = col_width - vis2
|
|
1293
|
+
pad1 = 0 if pad1 < 0
|
|
1294
|
+
pad2 = 0 if pad2 < 0
|
|
1295
|
+
|
|
1296
|
+
puts " #{text1}#{' ' * pad1} #{text2}#{' ' * pad2} #{text3}"
|
|
1197
1297
|
end
|
|
1198
1298
|
puts
|
|
1199
1299
|
end
|
|
@@ -1526,11 +1626,8 @@ def export_stats_csv(filename = 'rsh_stats.csv') # Export stats to CSV
|
|
|
1526
1626
|
puts "Error exporting stats: #{e.message}"
|
|
1527
1627
|
end
|
|
1528
1628
|
end
|
|
1529
|
-
def bm(
|
|
1530
|
-
|
|
1531
|
-
arg_str = args.join(' ')
|
|
1532
|
-
|
|
1533
|
-
if args.empty?
|
|
1629
|
+
def bm(arg_str = nil) # Enhanced bookmark management with tags
|
|
1630
|
+
if arg_str.nil? || arg_str.empty?
|
|
1534
1631
|
# List all bookmarks
|
|
1535
1632
|
if @bookmarks.empty?
|
|
1536
1633
|
puts "No bookmarks defined. Use :bm \"name\" to bookmark current directory"
|
|
@@ -1543,15 +1640,16 @@ def bm(*args) # Enhanced bookmark management with tags
|
|
|
1543
1640
|
puts " #{name.c(@c_nick)} → #{path}#{tags.c(@c_stamp)}"
|
|
1544
1641
|
end
|
|
1545
1642
|
puts
|
|
1546
|
-
elsif
|
|
1643
|
+
elsif arg_str =~ /^--export\s*(.*)/
|
|
1547
1644
|
# Export bookmarks to file
|
|
1548
|
-
filename =
|
|
1645
|
+
filename = $1.strip
|
|
1646
|
+
filename = 'bookmarks.json' if filename.empty?
|
|
1549
1647
|
export_bookmarks(filename)
|
|
1550
|
-
elsif
|
|
1648
|
+
elsif arg_str =~ /^--import\s+(.*)/
|
|
1551
1649
|
# Import bookmarks from file
|
|
1552
|
-
filename =
|
|
1650
|
+
filename = $1.strip
|
|
1553
1651
|
import_bookmarks(filename) if filename
|
|
1554
|
-
elsif
|
|
1652
|
+
elsif arg_str == '--stats'
|
|
1555
1653
|
# Show bookmark statistics
|
|
1556
1654
|
bookmark_stats
|
|
1557
1655
|
elsif arg_str =~ /^(\w+)\s+(.+)$/
|
|
@@ -1603,8 +1701,8 @@ def bm(*args) # Enhanced bookmark management with tags
|
|
|
1603
1701
|
rshrc
|
|
1604
1702
|
end
|
|
1605
1703
|
end
|
|
1606
|
-
def bookmark(
|
|
1607
|
-
bm(
|
|
1704
|
+
def bookmark(arg_str = nil) # Alias for bm
|
|
1705
|
+
bm(arg_str)
|
|
1608
1706
|
end
|
|
1609
1707
|
def export_bookmarks(filename = 'bookmarks.json') # Export bookmarks to JSON
|
|
1610
1708
|
begin
|
|
@@ -1669,6 +1767,7 @@ def bookmark_stats # Show bookmark usage statistics
|
|
|
1669
1767
|
end
|
|
1670
1768
|
def save_session(*args) # Save current session state
|
|
1671
1769
|
session_name = args[0] || 'default'
|
|
1770
|
+
silent = args[1] == :silent # Optional silent flag
|
|
1672
1771
|
session_path = @session_dir + "/#{session_name}.json"
|
|
1673
1772
|
|
|
1674
1773
|
session = {
|
|
@@ -1682,9 +1781,9 @@ def save_session(*args) # Save current session state
|
|
|
1682
1781
|
begin
|
|
1683
1782
|
require 'json'
|
|
1684
1783
|
File.write(session_path, JSON.pretty_generate(session))
|
|
1685
|
-
puts "Session '#{session_name}' saved to #{session_path}"
|
|
1784
|
+
puts "Session '#{session_name}' saved to #{session_path}" unless silent
|
|
1686
1785
|
rescue => e
|
|
1687
|
-
puts "Error saving session: #{e.message}"
|
|
1786
|
+
puts "Error saving session: #{e.message}" unless silent
|
|
1688
1787
|
end
|
|
1689
1788
|
end
|
|
1690
1789
|
def load_session(*args) # Restore previous session
|
|
@@ -2002,13 +2101,17 @@ def validate_command(cmd) # Syntax validation before execution
|
|
|
2002
2101
|
return nil if cmd.nil? || cmd.empty?
|
|
2003
2102
|
warnings = []
|
|
2004
2103
|
|
|
2104
|
+
# Apply custom validation rules first
|
|
2105
|
+
custom_warnings = apply_validation_rules(cmd)
|
|
2106
|
+
warnings.concat(custom_warnings) if custom_warnings.any?
|
|
2107
|
+
|
|
2005
2108
|
# Check for common mistakes
|
|
2006
2109
|
warnings << "Unmatched quotes" if cmd.count("'").odd? || cmd.count('"').odd?
|
|
2007
2110
|
warnings << "Unmatched parentheses" if cmd.count("(") != cmd.count(")")
|
|
2008
2111
|
warnings << "Unmatched brackets" if cmd.count("[") != cmd.count("]")
|
|
2009
2112
|
warnings << "Unmatched braces" if cmd.count("{") != cmd.count("}")
|
|
2010
2113
|
|
|
2011
|
-
# Check for potentially dangerous patterns
|
|
2114
|
+
# Check for potentially dangerous patterns (unless user has custom rules)
|
|
2012
2115
|
warnings << "WARNING: Recursive rm detected" if cmd =~ /rm\s+.*-r.*\//
|
|
2013
2116
|
warnings << "WARNING: Force flag without path" if cmd =~ /rm\s+-[rf]+\s*$/
|
|
2014
2117
|
warnings << "WARNING: Sudo with redirection" if cmd =~ /sudo.*>/
|
|
@@ -2060,6 +2163,236 @@ def calc(*args) # Inline calculator using Ruby's Math library
|
|
|
2060
2163
|
puts "Error evaluating expression: #{e.message}"
|
|
2061
2164
|
end
|
|
2062
2165
|
end
|
|
2166
|
+
def validate(rule_str = nil) # Custom validation rule management
|
|
2167
|
+
if rule_str.nil? || rule_str.empty?
|
|
2168
|
+
# List all validation rules
|
|
2169
|
+
if @validation_rules.empty?
|
|
2170
|
+
puts "\nNo validation rules defined"
|
|
2171
|
+
puts "Usage: :validate pattern = action"
|
|
2172
|
+
puts "Actions: block, confirm, warn, log"
|
|
2173
|
+
return
|
|
2174
|
+
end
|
|
2175
|
+
puts "\n Validation Rules:".c(@c_prompt).b
|
|
2176
|
+
@validation_rules.each_with_index do |rule, i|
|
|
2177
|
+
puts " #{i+1}. #{rule[:pattern].inspect} → #{rule[:action]}"
|
|
2178
|
+
end
|
|
2179
|
+
puts
|
|
2180
|
+
elsif rule_str =~ /^-(\d+)$/
|
|
2181
|
+
# Delete rule by index
|
|
2182
|
+
index = $1.to_i - 1
|
|
2183
|
+
if index >= 0 && index < @validation_rules.length
|
|
2184
|
+
rule = @validation_rules.delete_at(index)
|
|
2185
|
+
puts "Validation rule deleted: #{rule[:pattern]}"
|
|
2186
|
+
rshrc
|
|
2187
|
+
else
|
|
2188
|
+
puts "Invalid rule index: #{$1}"
|
|
2189
|
+
end
|
|
2190
|
+
elsif rule_str =~ /^(.+?)\s*=\s*(block|confirm|warn|log)$/
|
|
2191
|
+
# Add validation rule
|
|
2192
|
+
pattern = $1.strip
|
|
2193
|
+
action = $2.strip
|
|
2194
|
+
@validation_rules << {pattern: pattern, action: action}
|
|
2195
|
+
puts "Validation rule added: #{pattern} → #{action}"
|
|
2196
|
+
rshrc
|
|
2197
|
+
else
|
|
2198
|
+
puts "Usage: :validate pattern = action"
|
|
2199
|
+
puts "Example: :validate rm -rf / = block"
|
|
2200
|
+
puts "Actions: block (prevent), confirm (ask), warn (show), log (record)"
|
|
2201
|
+
end
|
|
2202
|
+
end
|
|
2203
|
+
def apply_validation_rules(cmd) # Apply custom validation rules
|
|
2204
|
+
return [] if @validation_rules.nil? || @validation_rules.empty?
|
|
2205
|
+
|
|
2206
|
+
warnings = []
|
|
2207
|
+
|
|
2208
|
+
@validation_rules.each do |rule|
|
|
2209
|
+
if cmd =~ /#{rule[:pattern]}/
|
|
2210
|
+
case rule[:action]
|
|
2211
|
+
when 'block'
|
|
2212
|
+
warnings << "BLOCKED by rule: #{rule[:pattern]}"
|
|
2213
|
+
when 'confirm'
|
|
2214
|
+
warnings << "CONFIRM required: #{rule[:pattern]}"
|
|
2215
|
+
when 'warn'
|
|
2216
|
+
warnings << "Warning: Matches rule '#{rule[:pattern]}'"
|
|
2217
|
+
when 'log'
|
|
2218
|
+
File.write("#{ENV['HOME']}/.rsh_validation.log",
|
|
2219
|
+
"#{Time.now}: #{cmd} (matched: #{rule[:pattern]})\n",
|
|
2220
|
+
mode: 'a')
|
|
2221
|
+
end
|
|
2222
|
+
end
|
|
2223
|
+
end
|
|
2224
|
+
|
|
2225
|
+
warnings
|
|
2226
|
+
end
|
|
2227
|
+
def track_completion(context, selected) # Track completion selection for learning
|
|
2228
|
+
return unless @completion_learning
|
|
2229
|
+
return if context.nil? || selected.nil?
|
|
2230
|
+
|
|
2231
|
+
key = "#{context}:#{selected}"
|
|
2232
|
+
@completion_weights[key] ||= 0
|
|
2233
|
+
@completion_weights[key] += 1
|
|
2234
|
+
end
|
|
2235
|
+
def sort_by_learning(context, items) # Sort completions by learning weights
|
|
2236
|
+
return items unless @completion_learning
|
|
2237
|
+
return items if @completion_weights.empty?
|
|
2238
|
+
|
|
2239
|
+
# Score each item
|
|
2240
|
+
scored = items.map do |item|
|
|
2241
|
+
# Extract just the switch/command part (before space/description)
|
|
2242
|
+
item_key = item.split(/\s+/).first
|
|
2243
|
+
key = "#{context}:#{item_key}"
|
|
2244
|
+
weight = @completion_weights[key] || 0
|
|
2245
|
+
{item: item, weight: weight}
|
|
2246
|
+
end
|
|
2247
|
+
|
|
2248
|
+
# Sort: highest weight first, then alphabetically
|
|
2249
|
+
scored.sort_by { |s| [-s[:weight], s[:item]] }.map { |s| s[:item] }
|
|
2250
|
+
end
|
|
2251
|
+
def completion_stats # Show completion learning statistics
|
|
2252
|
+
if @completion_weights.empty?
|
|
2253
|
+
puts "\nNo completion learning data yet"
|
|
2254
|
+
puts "Use TAB completion and selections will be learned over time"
|
|
2255
|
+
return
|
|
2256
|
+
end
|
|
2257
|
+
|
|
2258
|
+
puts "\n Completion Learning Statistics".c(@c_prompt).b
|
|
2259
|
+
puts " " + "="*50
|
|
2260
|
+
|
|
2261
|
+
# Group by context
|
|
2262
|
+
by_context = {}
|
|
2263
|
+
@completion_weights.each do |key, weight|
|
|
2264
|
+
context, choice = key.split(':', 2)
|
|
2265
|
+
by_context[context] ||= []
|
|
2266
|
+
by_context[context] << {choice: choice, weight: weight}
|
|
2267
|
+
end
|
|
2268
|
+
|
|
2269
|
+
# Show top contexts
|
|
2270
|
+
by_context.sort_by { |ctx, items| -items.map { |i| i[:weight] }.sum }.first(10).each do |context, items|
|
|
2271
|
+
puts "\n #{context}:".c(@c_nick)
|
|
2272
|
+
items.sort_by { |i| -i[:weight] }.first(5).each do |item|
|
|
2273
|
+
bar = "■" * ([item[:weight] / 2, 20].min)
|
|
2274
|
+
puts " #{item[:choice].ljust(20)} #{item[:weight].to_s.rjust(3)}x #{bar.c(@c_path)}"
|
|
2275
|
+
end
|
|
2276
|
+
end
|
|
2277
|
+
|
|
2278
|
+
puts "\n Total learned patterns: #{@completion_weights.length}"
|
|
2279
|
+
puts
|
|
2280
|
+
end
|
|
2281
|
+
def completion_reset # Reset all completion learning
|
|
2282
|
+
@completion_weights = {}
|
|
2283
|
+
puts "Completion learning data cleared"
|
|
2284
|
+
rshrc
|
|
2285
|
+
end
|
|
2286
|
+
def record(*args) # Command recording management
|
|
2287
|
+
action = args[0]
|
|
2288
|
+
name = args[1]
|
|
2289
|
+
|
|
2290
|
+
if action.nil? || action.empty?
|
|
2291
|
+
# List recordings
|
|
2292
|
+
if @recordings.empty?
|
|
2293
|
+
puts "\nNo recordings. Use: :record start name"
|
|
2294
|
+
return
|
|
2295
|
+
end
|
|
2296
|
+
|
|
2297
|
+
puts "\n Recordings:".c(@c_prompt).b
|
|
2298
|
+
@recordings.each do |rec_name, data|
|
|
2299
|
+
created = Time.at(data[:created] || Time.now.to_i).strftime("%Y-%m-%d %H:%M")
|
|
2300
|
+
count = data[:commands]&.length || 0
|
|
2301
|
+
puts " #{rec_name.ljust(20)} #{count.to_s.rjust(3)} commands #{created}"
|
|
2302
|
+
end
|
|
2303
|
+
puts
|
|
2304
|
+
|
|
2305
|
+
# Show if currently recording
|
|
2306
|
+
if @recording[:active]
|
|
2307
|
+
puts " Currently recording: #{@recording[:name]} (#{@recording[:commands].length} commands so far)".c(214)
|
|
2308
|
+
end
|
|
2309
|
+
elsif action == 'start' && name
|
|
2310
|
+
@recording[:active] = true
|
|
2311
|
+
@recording[:name] = name
|
|
2312
|
+
@recording[:commands] = []
|
|
2313
|
+
@recording[:start_time] = Time.now.to_i
|
|
2314
|
+
puts "Recording started: #{name}".c(@c_path)
|
|
2315
|
+
elsif action == 'stop'
|
|
2316
|
+
if @recording[:active]
|
|
2317
|
+
@recordings[@recording[:name]] = {
|
|
2318
|
+
commands: @recording[:commands],
|
|
2319
|
+
created: @recording[:start_time]
|
|
2320
|
+
}
|
|
2321
|
+
puts "Recording stopped: #{@recording[:name]} (#{@recording[:commands].length} commands)".c(@c_path)
|
|
2322
|
+
@recording[:active] = false
|
|
2323
|
+
rshrc
|
|
2324
|
+
else
|
|
2325
|
+
puts "No active recording"
|
|
2326
|
+
end
|
|
2327
|
+
elsif action == 'status'
|
|
2328
|
+
if @recording[:active]
|
|
2329
|
+
puts "Recording: #{@recording[:name]} (#{@recording[:commands].length} commands)"
|
|
2330
|
+
@recording[:commands].last(5).each { |cmd| puts " #{cmd}" }
|
|
2331
|
+
else
|
|
2332
|
+
puts "No active recording"
|
|
2333
|
+
end
|
|
2334
|
+
elsif action == 'show' && name
|
|
2335
|
+
# Show recording contents
|
|
2336
|
+
unless @recordings[name]
|
|
2337
|
+
puts "Recording '#{name}' not found"
|
|
2338
|
+
return
|
|
2339
|
+
end
|
|
2340
|
+
|
|
2341
|
+
recording = @recordings[name]
|
|
2342
|
+
created = Time.at(recording[:created] || Time.now.to_i).strftime("%Y-%m-%d %H:%M:%S")
|
|
2343
|
+
|
|
2344
|
+
puts "\n Recording: #{name}".c(@c_prompt).b
|
|
2345
|
+
puts " Created: #{created}"
|
|
2346
|
+
puts " Commands: #{recording[:commands].length}"
|
|
2347
|
+
puts
|
|
2348
|
+
|
|
2349
|
+
recording[:commands].each_with_index do |cmd, i|
|
|
2350
|
+
puts " #{(i+1).to_s.rjust(3)}. #{cmd}"
|
|
2351
|
+
end
|
|
2352
|
+
puts
|
|
2353
|
+
elsif action =~ /^-(.+)$/
|
|
2354
|
+
# Delete recording
|
|
2355
|
+
rec_name = $1
|
|
2356
|
+
if @recordings.delete(rec_name)
|
|
2357
|
+
puts "Recording '#{rec_name}' deleted"
|
|
2358
|
+
rshrc
|
|
2359
|
+
else
|
|
2360
|
+
puts "Recording '#{rec_name}' not found"
|
|
2361
|
+
end
|
|
2362
|
+
else
|
|
2363
|
+
puts "Usage: :record start name|stop|status|show name|-name"
|
|
2364
|
+
end
|
|
2365
|
+
end
|
|
2366
|
+
def replay(*args) # Replay recorded commands
|
|
2367
|
+
name = args[0]
|
|
2368
|
+
|
|
2369
|
+
unless name && @recordings[name]
|
|
2370
|
+
puts "Recording '#{name}' not found"
|
|
2371
|
+
record
|
|
2372
|
+
return
|
|
2373
|
+
end
|
|
2374
|
+
|
|
2375
|
+
recording = @recordings[name]
|
|
2376
|
+
commands = recording[:commands] || []
|
|
2377
|
+
|
|
2378
|
+
puts "Replaying '#{name}' (#{commands.length} commands)...".c(@c_path)
|
|
2379
|
+
|
|
2380
|
+
commands.each_with_index do |cmd, i|
|
|
2381
|
+
puts "\n[#{i+1}/#{commands.length}] #{cmd}".c(@c_stamp)
|
|
2382
|
+
|
|
2383
|
+
result = system(cmd)
|
|
2384
|
+
exit_code = $?.exitstatus
|
|
2385
|
+
|
|
2386
|
+
unless result
|
|
2387
|
+
puts " Command failed (exit #{exit_code})".c(196)
|
|
2388
|
+
print "Continue? (Y/n): "
|
|
2389
|
+
response = $stdin.gets.chomp
|
|
2390
|
+
break if response.downcase == 'n'
|
|
2391
|
+
end
|
|
2392
|
+
end
|
|
2393
|
+
|
|
2394
|
+
puts "\nReplay complete".c(@c_path)
|
|
2395
|
+
end
|
|
2063
2396
|
def apply_auto_correct(cmd) # Apply auto-correction to command
|
|
2064
2397
|
return cmd unless @auto_correct
|
|
2065
2398
|
return cmd if cmd =~ /^:/ # Don't auto-correct colon commands
|
|
@@ -2067,6 +2400,11 @@ def apply_auto_correct(cmd) # Apply auto-correction to command
|
|
|
2067
2400
|
|
|
2068
2401
|
first_cmd = $1
|
|
2069
2402
|
|
|
2403
|
+
# Don't auto-correct shell keywords or shell scripts
|
|
2404
|
+
shell_keywords = %w[for while if then else elif fi do done case esac function select until]
|
|
2405
|
+
return cmd if shell_keywords.include?(first_cmd)
|
|
2406
|
+
return cmd if cmd =~ /\b(for|while|if|case|function|until)\b/ # Skip shell scripts
|
|
2407
|
+
|
|
2070
2408
|
# Don't auto-correct if command exists
|
|
2071
2409
|
return cmd if @exe.include?(first_cmd)
|
|
2072
2410
|
return cmd if @nick.include?(first_cmd)
|
|
@@ -2370,6 +2708,11 @@ def load_rshrc_safe
|
|
|
2370
2708
|
@plugin_disabled = [] unless @plugin_disabled.is_a?(Array)
|
|
2371
2709
|
@plugins = [] unless @plugins.is_a?(Array)
|
|
2372
2710
|
@plugin_commands = {} unless @plugin_commands.is_a?(Hash)
|
|
2711
|
+
@validation_rules = [] unless @validation_rules.is_a?(Array)
|
|
2712
|
+
@completion_weights = {} unless @completion_weights.is_a?(Hash)
|
|
2713
|
+
@completion_learning = true if @completion_learning.nil?
|
|
2714
|
+
@recording = {active: false, name: nil, commands: []} unless @recording.is_a?(Hash)
|
|
2715
|
+
@recordings = {} unless @recordings.is_a?(Hash)
|
|
2373
2716
|
|
|
2374
2717
|
# Restore defuns from .rshrc
|
|
2375
2718
|
if @defuns && !@defuns.empty?
|
|
@@ -2511,6 +2854,11 @@ def load_defaults
|
|
|
2511
2854
|
@plugin_disabled ||= []
|
|
2512
2855
|
@plugins ||= []
|
|
2513
2856
|
@plugin_commands ||= {}
|
|
2857
|
+
@validation_rules ||= []
|
|
2858
|
+
@completion_weights ||= {}
|
|
2859
|
+
@completion_learning = true if @completion_learning.nil?
|
|
2860
|
+
@recording ||= {active: false, name: nil, commands: []}
|
|
2861
|
+
@recordings ||= {}
|
|
2514
2862
|
puts "Loaded with default configuration."
|
|
2515
2863
|
end
|
|
2516
2864
|
|
|
@@ -2609,7 +2957,7 @@ loop do
|
|
|
2609
2957
|
if @session_autosave && @session_autosave > 0
|
|
2610
2958
|
current_time = Time.now.to_i
|
|
2611
2959
|
if (current_time - @session_last_save) >= @session_autosave
|
|
2612
|
-
save_session('autosave')
|
|
2960
|
+
save_session('autosave', :silent)
|
|
2613
2961
|
@session_last_save = current_time
|
|
2614
2962
|
end
|
|
2615
2963
|
end
|
|
@@ -2687,8 +3035,47 @@ loop do
|
|
|
2687
3035
|
end
|
|
2688
3036
|
if @cmd.match(/^\s*:/) # Ruby commands are prefixed with ":"
|
|
2689
3037
|
begin
|
|
2690
|
-
|
|
2691
|
-
|
|
3038
|
+
cmd_line = @cmd[1..-1].strip
|
|
3039
|
+
|
|
3040
|
+
# Extract command name and arguments
|
|
3041
|
+
if cmd_line =~ /^(\w+\??)(.*)$/
|
|
3042
|
+
cmd_name = $1
|
|
3043
|
+
cmd_args_raw = $2.strip
|
|
3044
|
+
|
|
3045
|
+
# Commands that parse their own args (need full string)
|
|
3046
|
+
# nick/gnick parse "name = value"
|
|
3047
|
+
# defun parses "name(args) = body"
|
|
3048
|
+
# bm parses "name path #tags" or "-name" or "?tag" or "--export file"
|
|
3049
|
+
# validate parses "pattern = action"
|
|
3050
|
+
single_string_cmds = %w[nick gnick defun bm bookmark validate]
|
|
3051
|
+
|
|
3052
|
+
# List of all known rsh commands (since respond_to? doesn't work for top-level methods)
|
|
3053
|
+
known_commands = %w[nick gnick defun defun? bm bookmark stats calc config env theme plugins
|
|
3054
|
+
save_session load_session list_sessions delete_session rmsession
|
|
3055
|
+
validate completion_stats completion_reset
|
|
3056
|
+
record replay
|
|
3057
|
+
history rmhistory jobs fg bg dirs help info version]
|
|
3058
|
+
|
|
3059
|
+
# Try to call as rsh method
|
|
3060
|
+
if known_commands.include?(cmd_name)
|
|
3061
|
+
if cmd_args_raw.empty?
|
|
3062
|
+
send(cmd_name.to_sym)
|
|
3063
|
+
elsif single_string_cmds.include?(cmd_name)
|
|
3064
|
+
# Pass entire args string for commands that parse it themselves
|
|
3065
|
+
send(cmd_name.to_sym, cmd_args_raw)
|
|
3066
|
+
else
|
|
3067
|
+
# Split args for variadic functions
|
|
3068
|
+
args = cmd_args_raw.split(/\s+/)
|
|
3069
|
+
send(cmd_name.to_sym, *args)
|
|
3070
|
+
end
|
|
3071
|
+
else
|
|
3072
|
+
# Fallback to eval for arbitrary Ruby (like :puts 2+2)
|
|
3073
|
+
eval(cmd_line)
|
|
3074
|
+
end
|
|
3075
|
+
else
|
|
3076
|
+
# Fallback to eval
|
|
3077
|
+
eval(cmd_line)
|
|
3078
|
+
end
|
|
2692
3079
|
rescue Exception => err
|
|
2693
3080
|
puts "\n#{err}"
|
|
2694
3081
|
end
|
|
@@ -2723,20 +3110,48 @@ loop do
|
|
|
2723
3110
|
execute_conditional(@cmd)
|
|
2724
3111
|
next
|
|
2725
3112
|
end
|
|
2726
|
-
#
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
3113
|
+
# Detect shell scripting constructs - skip expansions if found
|
|
3114
|
+
is_shell_script = !(@cmd =~ /\b(for|while|if|case|function|until)\b/).nil?
|
|
3115
|
+
|
|
3116
|
+
unless is_shell_script
|
|
3117
|
+
# Expand brace expansion {a,b,c}
|
|
3118
|
+
@cmd = expand_braces(@cmd)
|
|
3119
|
+
# Expand command substitution $(command) and backticks
|
|
3120
|
+
@cmd = @cmd.gsub(/\$\(([^)]+)\)/) { `#{$1}`.chomp }
|
|
3121
|
+
@cmd = @cmd.gsub(/`([^`]+)`/) { `#{$1}`.chomp }
|
|
3122
|
+
# Expand environment variables and exit status
|
|
3123
|
+
@cmd = @cmd.gsub(/\$\?/) { @last_exit.to_s }
|
|
3124
|
+
@cmd = @cmd.gsub(/\$(\w+)|\$\{(\w+)\}/) { ENV[$1 || $2] || '' }
|
|
3125
|
+
end
|
|
3126
|
+
# Always expand tilde
|
|
2735
3127
|
@cmd = @cmd.gsub(/~/, Dir.home)
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
3128
|
+
# Skip nick/gnick substitution for shell scripts
|
|
3129
|
+
unless is_shell_script
|
|
3130
|
+
# Check for parametrized nick BEFORE substitution
|
|
3131
|
+
cmd_parts_before = @cmd.split(/\s+/)
|
|
3132
|
+
first_cmd = cmd_parts_before[0]
|
|
3133
|
+
used_param_nick = first_cmd && @nick[first_cmd] && @nick[first_cmd].include?('{{')
|
|
3134
|
+
|
|
3135
|
+
# Do nick/gnick substitution
|
|
3136
|
+
ca = @nick.transform_keys {|k| /((^\K\s*\K)|(\|\K\s*\K))\b(?<!-)#{Regexp.escape k}\b/}
|
|
3137
|
+
@cmd = @cmd.gsub(Regexp.union(ca.keys), @nick)
|
|
3138
|
+
ga = @gnick.transform_keys {|k| /\b(?<!-)#{Regexp.escape k}\b/}
|
|
3139
|
+
@cmd = @cmd.gsub(Regexp.union(ga.keys), @gnick)
|
|
3140
|
+
|
|
3141
|
+
# Expand placeholders if parametrized nick was used
|
|
3142
|
+
if used_param_nick
|
|
3143
|
+
params = {}
|
|
3144
|
+
cmd_parts_before[1..].each do |part|
|
|
3145
|
+
if part =~ /^(\w+)=(.+)$/
|
|
3146
|
+
params[$1] = $2
|
|
3147
|
+
end
|
|
3148
|
+
end
|
|
3149
|
+
# Replace placeholders
|
|
3150
|
+
params.each { |k, v| @cmd.gsub!(/\{\{#{k}\}\}/, v) }
|
|
3151
|
+
# Remove key=value parameters from command
|
|
3152
|
+
@cmd = @cmd.split(/\s+/).reject { |p| p =~ /^\w+=/ }.join(' ')
|
|
3153
|
+
end
|
|
3154
|
+
end
|
|
2740
3155
|
@cmd = "~" if @cmd == "cd"
|
|
2741
3156
|
@cmd.sub!(/^cd (\S*).*/, '\1')
|
|
2742
3157
|
@cmd = Dir.home if @cmd == "~"
|
|
@@ -2785,8 +3200,16 @@ loop do
|
|
|
2785
3200
|
# Validate command after auto-correction
|
|
2786
3201
|
warnings = validate_command(@cmd)
|
|
2787
3202
|
if warnings && !warnings.empty?
|
|
3203
|
+
# Check for BLOCKED commands
|
|
3204
|
+
if warnings.any? { |w| w.start_with?("BLOCKED") }
|
|
3205
|
+
warnings.select { |w| w.start_with?("BLOCKED") }.each { |w| puts "#{w}".c(196) }
|
|
3206
|
+
puts "Command execution blocked by validation rule"
|
|
3207
|
+
next
|
|
3208
|
+
end
|
|
3209
|
+
|
|
2788
3210
|
# Show non-auto-correct warnings
|
|
2789
|
-
warnings.reject { |w| w.start_with?("AUTO-CORRECTING:") }.each { |w| puts "#{w}".c(196) }
|
|
3211
|
+
warnings.reject { |w| w.start_with?("AUTO-CORRECTING:") || w.start_with?("CONFIRM") }.each { |w| puts "#{w}".c(196) }
|
|
3212
|
+
|
|
2790
3213
|
# Show auto-correct and ask for confirmation
|
|
2791
3214
|
auto_correct_warnings = warnings.select { |w| w.start_with?("AUTO-CORRECTING:") }
|
|
2792
3215
|
if auto_correct_warnings.any?
|
|
@@ -2798,7 +3221,19 @@ loop do
|
|
|
2798
3221
|
next
|
|
2799
3222
|
end
|
|
2800
3223
|
end
|
|
2801
|
-
|
|
3224
|
+
|
|
3225
|
+
# For CONFIRM validation rules
|
|
3226
|
+
if warnings.any? { |w| w.start_with?("CONFIRM") }
|
|
3227
|
+
warnings.select { |w| w.start_with?("CONFIRM") }.each { |w| puts "#{w}".c(214) }
|
|
3228
|
+
print "Confirm execution? (y/N): "
|
|
3229
|
+
response = $stdin.gets.chomp
|
|
3230
|
+
unless response.downcase == 'y'
|
|
3231
|
+
puts "Command cancelled"
|
|
3232
|
+
next
|
|
3233
|
+
end
|
|
3234
|
+
end
|
|
3235
|
+
|
|
3236
|
+
# For other critical warnings
|
|
2802
3237
|
if warnings.any? { |w| w.start_with?("WARNING:") }
|
|
2803
3238
|
print "Continue anyway? (y/N): "
|
|
2804
3239
|
response = $stdin.gets.chomp
|
|
@@ -2829,6 +3264,9 @@ loop do
|
|
|
2829
3264
|
# Start timing
|
|
2830
3265
|
start_time = Time.now
|
|
2831
3266
|
|
|
3267
|
+
# Detect if command needs bash (for loops, etc.)
|
|
3268
|
+
needs_bash = !(@cmd =~ /\b(for|while|if|case|function|until)\b/).nil? || @cmd.include?(';')
|
|
3269
|
+
|
|
2832
3270
|
# Handle background jobs
|
|
2833
3271
|
if @cmd.end_with?(' &')
|
|
2834
3272
|
@cmd = @cmd[0..-3] # Remove the &
|
|
@@ -2837,13 +3275,21 @@ loop do
|
|
|
2837
3275
|
if @cmd.include?('|') || @cmd.include?('>') || @cmd.include?('<')
|
|
2838
3276
|
pid = spawn(@cmd, pgroup: true)
|
|
2839
3277
|
else
|
|
2840
|
-
|
|
3278
|
+
if needs_bash
|
|
3279
|
+
pid = spawn("bash", "-c", @cmd)
|
|
3280
|
+
else
|
|
3281
|
+
pid = spawn(@cmd)
|
|
3282
|
+
end
|
|
2841
3283
|
end
|
|
2842
3284
|
@jobs[@job_id] = {pid: pid, cmd: @cmd, status: :running}
|
|
2843
3285
|
puts "[#{@job_id}] #{pid} #{@cmd}"
|
|
2844
3286
|
else
|
|
2845
3287
|
# Better handling of pipes and redirections
|
|
2846
|
-
|
|
3288
|
+
if needs_bash
|
|
3289
|
+
@current_pid = spawn("bash", "-c", @cmd)
|
|
3290
|
+
else
|
|
3291
|
+
@current_pid = spawn(@cmd)
|
|
3292
|
+
end
|
|
2847
3293
|
Process.wait(@current_pid)
|
|
2848
3294
|
@last_exit = $?.exitstatus
|
|
2849
3295
|
@current_pid = nil
|
|
@@ -2864,6 +3310,14 @@ loop do
|
|
|
2864
3310
|
puts "⚠ Command took #{'%.1f' % elapsed}s (threshold: #{@slow_command_threshold}s)".c(214)
|
|
2865
3311
|
end
|
|
2866
3312
|
|
|
3313
|
+
# Record command if recording is active
|
|
3314
|
+
if @recording[:active] && @last_exit == 0
|
|
3315
|
+
# Don't record :record commands themselves
|
|
3316
|
+
unless @cmd =~ /^:record/
|
|
3317
|
+
@recording[:commands] << @cmd
|
|
3318
|
+
end
|
|
3319
|
+
end
|
|
3320
|
+
|
|
2867
3321
|
# Call plugin on_command_after hooks
|
|
2868
3322
|
call_plugin_hook(:on_command_after, @cmd, @last_exit)
|
|
2869
3323
|
|
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: 3.
|
|
4
|
+
version: 3.4.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: 2025-10-
|
|
11
|
+
date: 2025-10-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: 'A shell written in Ruby with extensive tab completions, aliases/nicks,
|
|
14
14
|
history, syntax highlighting, theming, auto-cd, auto-opening files and more. UPDATE
|
|
15
|
-
v3.
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
v3.4.0: COMPLETION LEARNING - Shell learns which TAB completions you use most and
|
|
16
|
+
intelligently ranks them higher. Context-aware learning per command. :completion_stats
|
|
17
|
+
shows patterns. Persistent across sessions. Plus all v3.3 features: quote-less syntax,
|
|
18
|
+
parametrized nicks, Ctrl-G editing, validation rules, shell scripts!'
|
|
18
19
|
email: g@isene.com
|
|
19
20
|
executables:
|
|
20
21
|
- rsh
|