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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +146 -17
  3. data/bin/rsh +627 -173
  4. metadata +6 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8d8b425248a8544d33dab32c9e5322522389665593a9b1c9dfedf2f6be72996
4
- data.tar.gz: 44dcba6f5eca65d0667bc380677f26b7d52b07e8147f2d33bd18d28f676bfa99
3
+ metadata.gz: 5b86a585bea4b0c41a27de5679365d3ddce7b7a33481927cbf0de9bd89dcf27b
4
+ data.tar.gz: f346b777aa2863b06ef610a8e9c13325711058452f8986e5d4565aecb8ae21d4
5
5
  SHA512:
6
- metadata.gz: e8af85c2f352a7144ec51ed76c19144e44918c78b0d459d2aef58c0e33521cb08033a54bd2b7bd6d3da4402bc879c806c78353ba92c7fe30822aec9983d58480
7
- data.tar.gz: f85f97778ffc6efae4758fb67902361c40f1f3ad3e0b4857bf166538e0066d2074481ceef7dcf5989f6f2db90f0b50ef957f035cfba1b5b3320a700dd42f4ec5
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.2.0 - Plugin System & Productivity ⭐⭐⭐
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 'll = ls -l'` to make a command alias (ll) point to a command (ls -l)
113
- * `:gnick 'h = /home/me'` to make a general alias (h) point to something (/home/me)
114
- * `:nick` lists all command nicks, `:gnick` lists general nicks (NEW in v3.0)
115
- * `:nick '-name'` delete a command nick, `:gnick '-name'` delete a general nick (NEW in v3.0)
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 'func(args) = code'` defines Ruby functions callable as shell commands (now persistent!)
119
- * `:defun?` lists all user-defined functions, `:defun '-func'` removes functions
120
- * `:stats` shows command execution statistics and analytics (NEW in v3.0)
121
- * `:bm "name"` or `:bookmark "name"` bookmark current directory, `:bm "name path #tags"` with tags (NEW in v3.0)
122
- * `:bm` lists all bookmarks, just type bookmark name to jump (e.g., `work`) (NEW in v3.0)
123
- * `:bm "-name"` delete bookmark, `:bm "?tag"` search by tag (NEW in v3.0)
124
- * `:save_session "name"` saves named session, `:load_session "name"` loads session (NEW in v3.0)
125
- * `:list_sessions` shows all saved sessions, `:rmsession "name"` or `:rmsession "*"` deletes (NEW in v3.1)
126
- * `:theme "name"` applies color scheme, `:config` manages settings, `:env` manages environment (NEW in v3.1)
127
- * `:plugins` lists plugins, `:plugins "disable", "name"` disables, `:plugins "reload"` reloads (NEW in v3.2)
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 "x = /path/to/a/dir/"` - this would bookmark the directory to the single letter `x`.
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
- Add command nicks (aliases) with `:nick "some_nick = some_command"`, e.g. `:nick "ls = ls --color"`. Add general nicks that will substitute anything on a command line (not just commands) like this `:gnick "some_gnick = some_command"`, e.g. `:gnick "x = /home/user/somewhere"`. List nicks with `:nick`, list gnicks with `:gnick`. Remove a nick with `:nick "-some_command"`, e.g. `:nick "-ls"` to remove an `ls` nick. Same for gnicks.
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.2.0" # Plugin system: Extensible architecture with lifecycle hooks, completions, commands, and plugin management
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.2 - the Ruby SHell.
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.2:
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
- * Lifecycle hooks - on_startup, on_command_before, on_command_after, on_prompt
193
- * Plugin management - :plugins list/reload/enable/disable/info
194
- * Example plugins - git_prompt, command_logger, kubectl_completion included
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', 'C-C'
359
- @history[0] = ""
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
- tabbing("hist")
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
- case last_cmd
550
- when "cd", "pushd", "rmdir"
551
- type = "dirs_only"
552
- when "vim", "nano", "cat", "less", "more", "head", "tail", "file"
553
- type = "files_only"
554
- when "man", "info", "which", "whatis"
555
- type = "commands_only"
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
- # Check if command has defined completions and we're on the first argument
560
- if @cmd_completions.key?(last_cmd) && cmd_parts.length == 1
561
- type = "cmd_subcommands"
562
- @current_cmd = last_cmd
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 "\n.rshrc updated"
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 || 80
1082
- col_width = 48 # Fixed width for left column
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
- left_col = []
1090
- right_col = []
1091
-
1092
- # Left column content
1093
- left_col << "KEYBOARD SHORTCUTS:".c(@c_prompt).b
1094
- left_col << "RIGHT/Ctrl-F Accept suggestion"
1095
- left_col << "UP/DOWN Navigate history"
1096
- left_col << "TAB Tab complete"
1097
- left_col << "Shift-TAB Search history"
1098
- left_col << "Ctrl-Y Copy to clipboard"
1099
- left_col << "Ctrl-D Exit + save .rshrc"
1100
- left_col << "Ctrl-E Exit without save"
1101
- left_col << "Ctrl-L Clear screen"
1102
- left_col << "Ctrl-Z Suspend job"
1103
- left_col << "Ctrl-C/G Clear line"
1104
- left_col << "Ctrl-K Delete history item"
1105
- left_col << "Ctrl-U Clear line"
1106
- left_col << "Ctrl-W Delete previous word"
1107
- left_col << ""
1108
- left_col << "SPECIAL COMMANDS:".c(@c_prompt).b
1109
- left_col << ":nick 'll = ls -l' Command alias"
1110
- left_col << ":gnick 'h = /home' General alias"
1111
- left_col << ":nick List nicks"
1112
- left_col << ":gnick List gnicks"
1113
- left_col << ":nick '-name' Delete nick"
1114
- left_col << ":gnick '-name' Delete gnick"
1115
- left_col << ":history Show history"
1116
- left_col << ":rmhistory Clear history"
1117
- left_col << ":info About rsh"
1118
- left_col << ":version Version info"
1119
- left_col << ":help This help"
1120
-
1121
- # Right column content
1122
- right_col << "RUBY FUNCTIONS:".c(@c_prompt).b
1123
- right_col << ":defun 'f(x) = x*2' Define function"
1124
- right_col << ":defun? List functions"
1125
- right_col << ":defun '-f' Remove function"
1126
- right_col << "Call as: f 5 (returns 10)"
1127
- right_col << ""
1128
- right_col << "JOB CONTROL:".c(@c_prompt).b
1129
- right_col << "command & Background job"
1130
- right_col << ":jobs List jobs"
1131
- right_col << ":fg [id] Foreground job"
1132
- right_col << ":bg [id] Resume in bg"
1133
- right_col << ""
1134
- right_col << "v3.2 NEW FEATURES:".c(@c_prompt).b
1135
- right_col << ":plugins [cmd] Plugin system"
1136
- right_col << ":stats --graph Visual bar charts"
1137
- right_col << ":calc 2 + 2 Ruby calculator"
1138
- right_col << "!!, !-2, !5:7 History repeat/chain"
1139
- right_col << ":config auto_correct Auto-fix (w/confirm)"
1140
- right_col << ":config slow_cmd... Slow alerts"
1141
- right_col << ""
1142
- right_col << "PLUGIN SYSTEM:".c(@c_prompt).b
1143
- right_col << ":plugins List all"
1144
- right_col << ":plugins reload Reload"
1145
- right_col << ":plugins disable nm Disable"
1146
- right_col << ":plugins info nm Details"
1147
- right_col << ""
1148
- right_col << "v3.0/3.1 FEATURES:".c(@c_prompt).b
1149
- right_col << ":stats --clear Clear stats"
1150
- right_col << ":stats --csv|--json Export stats"
1151
- right_col << ":bm \"name\" Bookmarks"
1152
- right_col << ":save_session [nm] Sessions"
1153
- right_col << ":theme [name] Themes"
1154
- right_col << ":config [set val] Config"
1155
- right_col << ":env [VAR] Env vars"
1156
- right_col << ""
1157
- right_col << "INTEGRATIONS:".c(@c_prompt).b
1158
- right_col << "r Launch rtfm"
1159
- right_col << "f Launch fzf"
1160
- right_col << "= <expr> xrpn calculator"
1161
- right_col << ":<ruby code> Execute Ruby"
1162
- right_col << ""
1163
- right_col << "AI FEATURES:".c(@c_prompt).b
1164
- right_col << "@ <question> AI text response"
1165
- right_col << "@@ <request> AI command → prompt"
1166
- right_col << ""
1167
- right_col << "SMART COMPLETIONS:".c(@c_prompt).b
1168
- right_col << "git <TAB> Git subcommands"
1169
- right_col << "apt/docker <TAB> Command options"
1170
- right_col << "--format=<TAB> Option values"
1171
- right_col << "Typo suggestions Auto-correct"
1172
- right_col << ""
1173
- right_col << "EXPANSIONS:".c(@c_prompt).b
1174
- right_col << "~ Home directory"
1175
- right_col << "$VAR, ${VAR} Environment var"
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 = [left_col.length, right_col.length].max
1184
- left_col.fill("", left_col.length...max_lines)
1185
- right_col.fill("", right_col.length...max_lines)
1186
-
1187
- # Print in two columns
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
- left_text = left_col[i].to_s
1191
- right_text = right_col[i].to_s
1192
- # Calculate padding based on visible characters (without ANSI codes)
1193
- visible_length = strip_ansi(left_text).length
1194
- padding = col_width - visible_length
1195
- padding = 0 if padding < 0
1196
- puts " #{left_text}#{' ' * padding} #{right_text}"
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(*args) # Enhanced bookmark management with tags
1530
- # Handle variadic arguments
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 args[0] == '--export'
1643
+ elsif arg_str =~ /^--export\s*(.*)/
1547
1644
  # Export bookmarks to file
1548
- filename = args[1] || 'bookmarks.json'
1645
+ filename = $1.strip
1646
+ filename = 'bookmarks.json' if filename.empty?
1549
1647
  export_bookmarks(filename)
1550
- elsif args[0] == '--import'
1648
+ elsif arg_str =~ /^--import\s+(.*)/
1551
1649
  # Import bookmarks from file
1552
- filename = args[1]
1650
+ filename = $1.strip
1553
1651
  import_bookmarks(filename) if filename
1554
- elsif args[0] == '--stats'
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(*args) # Alias for bm
1607
- bm(*args)
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
- eval(@cmd[1..-1])
2691
- #rescue StandardError => err
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
- # Expand brace expansion {a,b,c}
2727
- @cmd = expand_braces(@cmd)
2728
- # Expand command substitution $(command) and backticks
2729
- @cmd = @cmd.gsub(/\$\(([^)]+)\)/) { `#{$1}`.chomp }
2730
- @cmd = @cmd.gsub(/`([^`]+)`/) { `#{$1}`.chomp }
2731
- # Expand environment variables and exit status
2732
- @cmd = @cmd.gsub(/\$\?/) { @last_exit.to_s }
2733
- @cmd = @cmd.gsub(/\$(\w+)|\$\{(\w+)\}/) { ENV[$1 || $2] || '' }
2734
- # Expand tilde
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
- ca = @nick.transform_keys {|k| /((^\K\s*\K)|(\|\K\s*\K))\b(?<!-)#{Regexp.escape k}\b/}
2737
- @cmd = @cmd.gsub(Regexp.union(ca.keys), @nick)
2738
- ga = @gnick.transform_keys {|k| /\b(?<!-)#{Regexp.escape k}\b/}
2739
- @cmd = @cmd.gsub(Regexp.union(ga.keys), @gnick)
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
- # For critical warnings, ask for confirmation
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
- pid = spawn(@cmd)
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
- @current_pid = spawn(@cmd)
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.2.0
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-22 00:00:00.000000000 Z
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.2.0: PLUGIN SYSTEM - Extensible architecture with lifecycle hooks (on_startup,
16
- on_command_before/after, on_prompt), extension points (add_completions, add_commands),
17
- plugin management, and 3 example plugins included. Plus colon command theming!'
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