ruby-shell 3.1.0 → 3.3.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 (5) hide show
  1. checksums.yaml +4 -4
  2. data/PLUGIN_GUIDE.md +757 -0
  3. data/README.md +199 -15
  4. data/bin/rsh +672 -81
  5. metadata +7 -5
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.1.0" # Quick wins: Multiple sessions, stats export, bookmark features, themes, config, env management
11
+ @version = "3.3.0" # Quote-less syntax: Simplified colon commands without quotes for cleaner UX
12
12
 
13
13
  # MODULES, CLASSES AND EXTENSIONS
14
14
  class String # Add coloring to strings (with escaping for Readline)
@@ -144,8 +144,17 @@ begin # Initialization
144
144
  @session_autosave = 0 # Auto-save interval (0 = disabled)
145
145
  @session_last_save = Time.now.to_i # Last auto-save timestamp
146
146
  @history_dedup = 'smart' # History dedup mode: 'off', 'full', 'smart'
147
+ @auto_correct = false # Auto-correct typos (default: off)
148
+ @slow_command_threshold = 0 # Threshold for slow command alerts (0 = disabled)
149
+ @plugin_dir = Dir.home + '/.rsh/plugins' # Plugins directory
150
+ @plugins = [] # Loaded plugin instances
151
+ @plugin_disabled = [] # List of disabled plugin names
152
+ @plugin_commands = {} # Commands added by plugins
153
+ @validation_rules = [] # Custom validation rules
154
+ # Built-in rsh commands are called with : prefix, so no need for separate tracking
147
155
  Dir.mkdir(Dir.home + '/.rsh') unless Dir.exist?(Dir.home + '/.rsh')
148
156
  Dir.mkdir(@session_dir) unless Dir.exist?(@session_dir)
157
+ Dir.mkdir(@plugin_dir) unless Dir.exist?(@plugin_dir)
149
158
  def pre_cmd; end # User-defined function to be run BEFORE command execution
150
159
  def post_cmd; end # User-defined function to be run AFTER command execution
151
160
  end
@@ -153,7 +162,7 @@ end
153
162
  # HELP TEXT
154
163
  @info = <<~INFO
155
164
 
156
- Hello #{@user}, welcome to rsh v3.1 - the Ruby SHell.
165
+ Hello #{@user}, welcome to rsh v3.3 - the Ruby SHell.
157
166
 
158
167
  rsh does not attempt to compete with the grand old shells like bash and zsh.
159
168
  It serves the specific needs and wants of its author. If you like it, then feel free
@@ -179,7 +188,24 @@ end
179
188
  * Syntax validation - Pre-execution warnings for dangerous or malformed commands
180
189
  * Unified command syntax - :nick, :gnick, :bm all work the same way (list/create/delete)
181
190
 
182
- NEW in v3.1:
191
+ NEW in v3.3:
192
+ * Quote-less syntax - No more quotes! Use :nick la = ls -la instead of :nick "la = ls -la"
193
+ * Parametrized nicks - :nick gp = git push origin {{branch}}, then: gp branch=main
194
+ * Ctrl-G multi-line edit - Press Ctrl-G to edit command in $EDITOR
195
+ * Custom validation - :validate rm -rf / = block prevents dangerous commands
196
+ * Shell script support - for/while/if loops work with full bash syntax
197
+ * Simplified - Removed :template, everything now in :nick
198
+ * Backward compatible - Old quote syntax still works
199
+
200
+ v3.2 Features:
201
+ * Plugin system - Extensible architecture for custom commands, completions, and hooks
202
+ * Auto-correct typos - :config auto_correct on (with confirmation prompt)
203
+ * Command timing alerts - :config slow_command_threshold 5 warns on slow commands
204
+ * Inline calculator - :calc 2 + 2, :calc "Math::PI", full Ruby Math library
205
+ * Enhanced history - !!, !-2, !5:7 for repeat last, nth-to-last, and chaining
206
+ * Stats visualization - :stats --graph for colorful ASCII bar charts
207
+
208
+ v3.1 Features:
183
209
  * Multiple named sessions - :save_session "project" and :load_session "project"
184
210
  * Stats export - :stats --csv or :stats --json for data analysis
185
211
  * Session auto-save - Set @session_autosave = 300 in .rshrc for 5-min auto-save
@@ -188,8 +214,6 @@ end
188
214
  * Color themes - :theme solarized|dracula|gruvbox|nord|monokai
189
215
  * Config management - :config shows/sets history_dedup, session_autosave, etc.
190
216
  * Environment management - :env for listing/setting/exporting environment variables
191
- * Bookmark TAB completion - Bookmarks appear in TAB completion list
192
- * List sessions - :list_sessions shows all saved sessions with timestamps
193
217
 
194
218
  Config file (.rshrc) updates on exit (Ctrl-d) or not (Ctrl-e).
195
219
  All colors are themeable in .rshrc (see github link for possibilities).
@@ -337,8 +361,23 @@ def getstr # A custom Readline-like function
337
361
  chr = getchr
338
362
  puts "DEBUG: Got char: '#{chr}' (length: #{chr.length})" if ENV['RSH_DEBUG']
339
363
  case chr
340
- when 'C-G', 'C-C'
341
- @history[0] = ""
364
+ when 'C-G' # Ctrl-G opens command in $EDITOR
365
+ temp_file = "/tmp/rsh_edit_#{Process.pid}.tmp"
366
+ File.write(temp_file, @history[0] || "")
367
+ system("#{ENV['EDITOR'] || 'vi'} #{temp_file}")
368
+ if File.exist?(temp_file)
369
+ edited = File.read(temp_file).strip
370
+ # Convert multi-line to single line with proper separators
371
+ if edited.include?("\n")
372
+ # Join lines with semicolons, preserving quoted strings
373
+ edited = edited.split("\n").map(&:strip).reject(&:empty?).join('; ')
374
+ end
375
+ @history[0] = edited
376
+ @pos = edited.length
377
+ File.delete(temp_file)
378
+ end
379
+ when 'C-C'
380
+ @history[0] = ""
342
381
  @pos = 0
343
382
  when 'C-E' # Ctrl-C exits gracefully but without updating .rshrc
344
383
  print "\n"
@@ -602,7 +641,7 @@ def tab(type)
602
641
  :nick :gnick :bm :bookmark :stats :defun :defun?
603
642
  :history :rmhistory :jobs :fg :bg
604
643
  :save_session :load_session :list_sessions :delete_session :rmsession
605
- :config :env :theme
644
+ :config :env :theme :plugins :calc :validate
606
645
  :info :version :help
607
646
  ]
608
647
  search_str = @tabstr[1..-1] || "" # Remove leading :
@@ -887,6 +926,8 @@ def config(*args) # Configure rsh settings
887
926
  puts "\n Current Configuration:".c(@c_prompt).b
888
927
  puts " history_dedup: #{@history_dedup}"
889
928
  puts " session_autosave: #{@session_autosave}s #{@session_autosave > 0 ? '(enabled)' : '(disabled)'}"
929
+ puts " auto_correct: #{@auto_correct ? 'on' : 'off'}"
930
+ puts " slow_command_threshold: #{@slow_command_threshold}s #{@slow_command_threshold > 0 ? '(enabled)' : '(disabled)'}"
890
931
  puts " completion_limit: #{@completion_limit}"
891
932
  puts " completion_fuzzy: #{@completion_fuzzy}"
892
933
  puts " completion_case_sensitive: #{@completion_case_sensitive}"
@@ -907,13 +948,21 @@ def config(*args) # Configure rsh settings
907
948
  @session_autosave = value.to_i
908
949
  puts "Session auto-save set to #{value}s #{value.to_i > 0 ? '(enabled)' : '(disabled)'}"
909
950
  rshrc
951
+ when 'auto_correct'
952
+ @auto_correct = %w[on true yes 1].include?(value.to_s.downcase)
953
+ puts "Auto-correct #{@auto_correct ? 'enabled' : 'disabled'}"
954
+ rshrc
955
+ when 'slow_command_threshold'
956
+ @slow_command_threshold = value.to_i
957
+ puts "Slow command threshold set to #{value}s #{value.to_i > 0 ? '(enabled)' : '(disabled)'}"
958
+ rshrc
910
959
  when 'completion_limit'
911
960
  @completion_limit = value.to_i
912
961
  puts "Completion limit set to #{value}"
913
962
  rshrc
914
963
  else
915
964
  puts "Unknown setting '#{setting}'"
916
- puts "Available: history_dedup, session_autosave, completion_limit"
965
+ puts "Available: history_dedup, session_autosave, auto_correct, slow_command_threshold, completion_limit"
917
966
  end
918
967
  end
919
968
  def env(*args) # Environment variable management
@@ -1024,6 +1073,14 @@ def rshrc # Write updates to .rshrc
1024
1073
  conf += "@history_dedup = '#{@history_dedup}'\n" if @history_dedup && @history_dedup != 'smart'
1025
1074
  conf.sub!(/^@session_autosave.*(\n|$)/, "")
1026
1075
  conf += "@session_autosave = #{@session_autosave}\n" if @session_autosave && @session_autosave > 0
1076
+ conf.sub!(/^@auto_correct.*(\n|$)/, "")
1077
+ conf += "@auto_correct = #{@auto_correct}\n" if @auto_correct
1078
+ conf.sub!(/^@slow_command_threshold.*(\n|$)/, "")
1079
+ conf += "@slow_command_threshold = #{@slow_command_threshold}\n" if @slow_command_threshold && @slow_command_threshold > 0
1080
+ conf.sub!(/^@plugin_disabled.*(\n|$)/, "")
1081
+ conf += "@plugin_disabled = #{@plugin_disabled}\n" unless @plugin_disabled.empty?
1082
+ conf.sub!(/^@validation_rules.*(\n|$)/, "")
1083
+ conf += "@validation_rules = #{@validation_rules}\n" unless @validation_rules.empty?
1027
1084
  # Only write @cmd_completions if user has customized it
1028
1085
  unless conf =~ /^@cmd_completions\s*=/
1029
1086
  # Don't write default completions to avoid cluttering .rshrc
@@ -1038,7 +1095,7 @@ def rshrc # Write updates to .rshrc
1038
1095
  puts "Warning: Error saving history: #{e.message}"
1039
1096
  end
1040
1097
  File.write(Dir.home+'/.rshrc', conf)
1041
- puts "\n.rshrc updated"
1098
+ puts ".rshrc updated"
1042
1099
  end
1043
1100
 
1044
1101
  # RSH FUNCTIONS
@@ -1061,23 +1118,24 @@ def help
1061
1118
  left_col << "UP/DOWN Navigate history"
1062
1119
  left_col << "TAB Tab complete"
1063
1120
  left_col << "Shift-TAB Search history"
1121
+ left_col << "Ctrl-G Edit in $EDITOR"
1064
1122
  left_col << "Ctrl-Y Copy to clipboard"
1065
1123
  left_col << "Ctrl-D Exit + save .rshrc"
1066
1124
  left_col << "Ctrl-E Exit without save"
1067
1125
  left_col << "Ctrl-L Clear screen"
1068
1126
  left_col << "Ctrl-Z Suspend job"
1069
- left_col << "Ctrl-C/G Clear line"
1127
+ left_col << "Ctrl-C Clear line"
1070
1128
  left_col << "Ctrl-K Delete history item"
1071
1129
  left_col << "Ctrl-U Clear line"
1072
1130
  left_col << "Ctrl-W Delete previous word"
1073
1131
  left_col << ""
1074
1132
  left_col << "SPECIAL COMMANDS:".c(@c_prompt).b
1075
- left_col << ":nick 'll = ls -l' Command alias"
1076
- left_col << ":gnick 'h = /home' General alias"
1133
+ left_col << ":nick ll = ls -l Command alias"
1134
+ left_col << ":gnick h = /home General alias"
1077
1135
  left_col << ":nick List nicks"
1078
1136
  left_col << ":gnick List gnicks"
1079
- left_col << ":nick '-name' Delete nick"
1080
- left_col << ":gnick '-name' Delete gnick"
1137
+ left_col << ":nick -name Delete nick"
1138
+ left_col << ":gnick -name Delete gnick"
1081
1139
  left_col << ":history Show history"
1082
1140
  left_col << ":rmhistory Clear history"
1083
1141
  left_col << ":info About rsh"
@@ -1086,9 +1144,9 @@ def help
1086
1144
 
1087
1145
  # Right column content
1088
1146
  right_col << "RUBY FUNCTIONS:".c(@c_prompt).b
1089
- right_col << ":defun 'f(x) = x*2' Define function"
1147
+ right_col << ":defun f(x) = x*2 Define function"
1090
1148
  right_col << ":defun? List functions"
1091
- right_col << ":defun '-f' Remove function"
1149
+ right_col << ":defun -f Remove function"
1092
1150
  right_col << "Call as: f 5 (returns 10)"
1093
1151
  right_col << ""
1094
1152
  right_col << "JOB CONTROL:".c(@c_prompt).b
@@ -1097,20 +1155,30 @@ def help
1097
1155
  right_col << ":fg [id] Foreground job"
1098
1156
  right_col << ":bg [id] Resume in bg"
1099
1157
  right_col << ""
1100
- right_col << "v3.0/3.1 FEATURES:".c(@c_prompt).b
1101
- right_col << ":stats [--csv|--json] Analytics"
1102
- right_col << ":bm \"name\" Create bookmark"
1103
- right_col << "name Jump to bookmark"
1104
- right_col << ":bm --stats Bookmark stats"
1105
- right_col << ":bm --export file Export bookmarks"
1106
- right_col << ":bm --import file Import bookmarks"
1107
- right_col << ":save_session [nm] Save session"
1108
- right_col << ":load_session [nm] Load session"
1109
- right_col << ":list_sessions List all sessions"
1110
- right_col << ":rmsession name|* Delete session(s)"
1111
- right_col << ":theme [name] Color presets"
1112
- right_col << ":config [set val] Settings"
1113
- right_col << ":env [VARNAME] Env management"
1158
+ right_col << "v3.3 NEW FEATURES:".c(@c_prompt).b
1159
+ right_col << "No quotes! :nick la = ls -la"
1160
+ right_col << ":nick gp={{branch}} Parametrized"
1161
+ right_col << ":validate pat = act Safety rules"
1162
+ right_col << "Ctrl-G Edit in \$EDITOR"
1163
+ right_col << "for i in {1..5} Shell scripts"
1164
+ right_col << ""
1165
+ right_col << "v3.2 FEATURES:".c(@c_prompt).b
1166
+ right_col << ":plugins [cmd] Plugin system"
1167
+ right_col << ":stats --graph Visual charts"
1168
+ right_col << ":calc 2 + 2 Calculator"
1169
+ right_col << "!!, !-2, !5:7 History chain"
1170
+ right_col << ""
1171
+ right_col << "PLUGIN SYSTEM:".c(@c_prompt).b
1172
+ right_col << ":plugins List all"
1173
+ right_col << ":plugins reload Reload"
1174
+ right_col << ":plugins disable nm Disable"
1175
+ right_col << ""
1176
+ right_col << "MORE FEATURES:".c(@c_prompt).b
1177
+ right_col << ":config auto_correct Auto-fix"
1178
+ right_col << ":bm name path #tags Bookmarks"
1179
+ right_col << ":save_session nm Sessions"
1180
+ right_col << ":theme name Themes"
1181
+ right_col << ":env VAR Env vars"
1114
1182
  right_col << ""
1115
1183
  right_col << "INTEGRATIONS:".c(@c_prompt).b
1116
1184
  right_col << "r Launch rtfm"
@@ -1331,6 +1399,16 @@ def stats(*args) # Show command execution statistics and analytics
1331
1399
  fname = filename || "rsh_stats.csv"
1332
1400
  export_stats_csv(fname)
1333
1401
  return
1402
+ elsif format == "--graph"
1403
+ stats_graph
1404
+ return
1405
+ elsif format == "--clear"
1406
+ # Clear all statistics
1407
+ @cmd_frequency = {}
1408
+ @cmd_stats = {}
1409
+ puts "All statistics cleared"
1410
+ rshrc
1411
+ return
1334
1412
  end
1335
1413
 
1336
1414
  # Display stats (existing code)
@@ -1373,6 +1451,59 @@ def stats(*args) # Show command execution statistics and analytics
1373
1451
  puts "\n Last command exit status: #{@last_exit == 0 ? 'Success'.c(@c_path) : "Failed (#{@last_exit})".c(196)}"
1374
1452
  puts
1375
1453
  end
1454
+ def stats_graph # Visual graph mode for stats
1455
+ puts "\n Command Usage Graph".c(@c_prompt).b
1456
+ puts " " + "="*50
1457
+
1458
+ return puts "No data to display" if @cmd_frequency.nil? || @cmd_frequency.empty?
1459
+
1460
+ sorted = @cmd_frequency.sort_by { |_, count| -count }.first(15)
1461
+ max_count = sorted.first[1]
1462
+ max_width = 40
1463
+
1464
+ puts
1465
+ sorted.each_with_index do |(cmd, count), i|
1466
+ # Calculate bar width (scaled to max_width)
1467
+ bar_width = (count.to_f / max_count * max_width).round
1468
+ bar = "█" * bar_width
1469
+
1470
+ # Color bars by intensity
1471
+ color = case bar_width
1472
+ when 0..10 then 244 # Gray
1473
+ when 11..20 then 3 # Yellow
1474
+ when 21..30 then 214 # Orange
1475
+ else 196 # Red
1476
+ end
1477
+
1478
+ puts " #{cmd.ljust(15)} #{count.to_s.rjust(4)}x #{bar.c(color)}"
1479
+ end
1480
+
1481
+ # Performance graph if data exists
1482
+ if @cmd_stats && !@cmd_stats.empty?
1483
+ puts "\n Command Performance Graph (avg time)".c(@c_prompt).b
1484
+ puts " " + "="*50
1485
+ puts
1486
+
1487
+ slowest = @cmd_stats.sort_by { |_, s| -(s[:avg_time] || 0) }.first(10)
1488
+ max_time = slowest.first[1][:avg_time]
1489
+
1490
+ slowest.each do |cmd, stats|
1491
+ bar_width = (stats[:avg_time] / max_time * max_width).round
1492
+ bar = "█" * bar_width
1493
+
1494
+ color = case bar_width
1495
+ when 0..10 then 2 # Green (fast)
1496
+ when 11..20 then 214 # Orange
1497
+ else 196 # Red (slow)
1498
+ end
1499
+
1500
+ time_str = "#{'%.3f' % stats[:avg_time]}s"
1501
+ puts " #{cmd.ljust(15)} #{time_str.rjust(8)} #{bar.c(color)}"
1502
+ end
1503
+ end
1504
+
1505
+ puts
1506
+ end
1376
1507
  def export_stats(filename) # Export stats to file (JSON or CSV based on extension)
1377
1508
  if filename.end_with?('.csv')
1378
1509
  export_stats_csv(filename)
@@ -1421,11 +1552,8 @@ def export_stats_csv(filename = 'rsh_stats.csv') # Export stats to CSV
1421
1552
  puts "Error exporting stats: #{e.message}"
1422
1553
  end
1423
1554
  end
1424
- def bm(*args) # Enhanced bookmark management with tags
1425
- # Handle variadic arguments
1426
- arg_str = args.join(' ')
1427
-
1428
- if args.empty?
1555
+ def bm(arg_str = nil) # Enhanced bookmark management with tags
1556
+ if arg_str.nil? || arg_str.empty?
1429
1557
  # List all bookmarks
1430
1558
  if @bookmarks.empty?
1431
1559
  puts "No bookmarks defined. Use :bm \"name\" to bookmark current directory"
@@ -1438,15 +1566,16 @@ def bm(*args) # Enhanced bookmark management with tags
1438
1566
  puts " #{name.c(@c_nick)} → #{path}#{tags.c(@c_stamp)}"
1439
1567
  end
1440
1568
  puts
1441
- elsif args[0] == '--export'
1569
+ elsif arg_str =~ /^--export\s*(.*)/
1442
1570
  # Export bookmarks to file
1443
- filename = args[1] || 'bookmarks.json'
1571
+ filename = $1.strip
1572
+ filename = 'bookmarks.json' if filename.empty?
1444
1573
  export_bookmarks(filename)
1445
- elsif args[0] == '--import'
1574
+ elsif arg_str =~ /^--import\s+(.*)/
1446
1575
  # Import bookmarks from file
1447
- filename = args[1]
1576
+ filename = $1.strip
1448
1577
  import_bookmarks(filename) if filename
1449
- elsif args[0] == '--stats'
1578
+ elsif arg_str == '--stats'
1450
1579
  # Show bookmark statistics
1451
1580
  bookmark_stats
1452
1581
  elsif arg_str =~ /^(\w+)\s+(.+)$/
@@ -1498,8 +1627,8 @@ def bm(*args) # Enhanced bookmark management with tags
1498
1627
  rshrc
1499
1628
  end
1500
1629
  end
1501
- def bookmark(*args) # Alias for bm
1502
- bm(*args)
1630
+ def bookmark(arg_str = nil) # Alias for bm
1631
+ bm(arg_str)
1503
1632
  end
1504
1633
  def export_bookmarks(filename = 'bookmarks.json') # Export bookmarks to JSON
1505
1634
  begin
@@ -1564,6 +1693,7 @@ def bookmark_stats # Show bookmark usage statistics
1564
1693
  end
1565
1694
  def save_session(*args) # Save current session state
1566
1695
  session_name = args[0] || 'default'
1696
+ silent = args[1] == :silent # Optional silent flag
1567
1697
  session_path = @session_dir + "/#{session_name}.json"
1568
1698
 
1569
1699
  session = {
@@ -1577,9 +1707,9 @@ def save_session(*args) # Save current session state
1577
1707
  begin
1578
1708
  require 'json'
1579
1709
  File.write(session_path, JSON.pretty_generate(session))
1580
- puts "Session '#{session_name}' saved to #{session_path}"
1710
+ puts "Session '#{session_name}' saved to #{session_path}" unless silent
1581
1711
  rescue => e
1582
- puts "Error saving session: #{e.message}"
1712
+ puts "Error saving session: #{e.message}" unless silent
1583
1713
  end
1584
1714
  end
1585
1715
  def load_session(*args) # Restore previous session
@@ -1744,34 +1874,309 @@ def theme(*args) # Apply color scheme presets
1744
1874
  puts "Theme '#{name}' applied"
1745
1875
  puts "Add this to .rshrc to make it permanent: :theme \"#{name}\""
1746
1876
  end
1877
+ def load_plugins # Load all plugins from plugin directory
1878
+ return unless Dir.exist?(@plugin_dir)
1879
+
1880
+ plugin_files = Dir.glob(@plugin_dir + '/*.rb').sort
1881
+
1882
+ plugin_files.each do |plugin_file|
1883
+ plugin_name = File.basename(plugin_file, '.rb')
1884
+
1885
+ # Skip if disabled
1886
+ next if @plugin_disabled.include?(plugin_name)
1887
+
1888
+ begin
1889
+ # Load the plugin file
1890
+ load(plugin_file)
1891
+
1892
+ # Find the plugin class (conventionally PluginNamePlugin)
1893
+ class_name = plugin_name.split('_').map(&:capitalize).join + 'Plugin'
1894
+
1895
+ # Try to instantiate the plugin
1896
+ if Object.const_defined?(class_name)
1897
+ plugin_class = Object.const_get(class_name)
1898
+ rsh_context = {
1899
+ version: @version,
1900
+ history: @history,
1901
+ bookmarks: @bookmarks,
1902
+ nick: @nick,
1903
+ gnick: @gnick,
1904
+ pwd: Dir.pwd,
1905
+ config: method(:config),
1906
+ rsh: self
1907
+ }
1908
+ plugin_instance = plugin_class.new(rsh_context)
1909
+ @plugins << { name: plugin_name, instance: plugin_instance, class: class_name }
1910
+
1911
+ # Load plugin completions
1912
+ if plugin_instance.respond_to?(:add_completions)
1913
+ completions = plugin_instance.add_completions
1914
+ @cmd_completions.merge!(completions) if completions.is_a?(Hash)
1915
+ end
1916
+
1917
+ # Load plugin commands
1918
+ if plugin_instance.respond_to?(:add_commands)
1919
+ commands = plugin_instance.add_commands
1920
+ @plugin_commands.merge!(commands) if commands.is_a?(Hash)
1921
+ end
1922
+
1923
+ puts " Loaded plugin: #{plugin_name} (#{class_name})" if ENV['RSH_DEBUG']
1924
+ end
1925
+ rescue => e
1926
+ puts "Warning: Failed to load plugin '#{plugin_name}': #{e.message}" if ENV['RSH_DEBUG']
1927
+ puts " #{e.backtrace.first}" if ENV['RSH_DEBUG']
1928
+ end
1929
+ end
1930
+
1931
+ # Call on_startup for all plugins
1932
+ @plugins.each do |plugin|
1933
+ begin
1934
+ plugin[:instance].on_startup if plugin[:instance].respond_to?(:on_startup)
1935
+ rescue => e
1936
+ puts "Warning: Plugin '#{plugin[:name]}' on_startup failed: #{e.message}" if ENV['RSH_DEBUG']
1937
+ end
1938
+ end
1939
+ end
1940
+ def call_plugin_hook(hook_name, *args) # Call a lifecycle hook for all plugins
1941
+ results = []
1942
+ @plugins.each do |plugin|
1943
+ begin
1944
+ if plugin[:instance].respond_to?(hook_name)
1945
+ result = plugin[:instance].send(hook_name, *args)
1946
+ results << result unless result.nil?
1947
+ end
1948
+ rescue => e
1949
+ puts "Warning: Plugin '#{plugin[:name]}' hook '#{hook_name}' failed: #{e.message}" if ENV['RSH_DEBUG']
1950
+ end
1951
+ end
1952
+ results
1953
+ end
1954
+ def plugins(*args) # Plugin management command
1955
+ if args.empty?
1956
+ # List all plugins
1957
+ if @plugins.empty?
1958
+ puts "\nNo plugins loaded"
1959
+ puts "Place .rb files in #{@plugin_dir}"
1960
+ return
1961
+ end
1962
+
1963
+ puts "\n Loaded Plugins:".c(@c_prompt).b
1964
+ @plugins.each do |plugin|
1965
+ status = @plugin_disabled.include?(plugin[:name]) ? '[disabled]'.c(196) : '[enabled]'.c(@c_path)
1966
+ puts " #{plugin[:name].ljust(20)} #{status} (#{plugin[:class]})"
1967
+ end
1968
+
1969
+ unless @plugin_disabled.empty?
1970
+ puts "\n Disabled Plugins:".c(@c_stamp)
1971
+ @plugin_disabled.each { |name| puts " #{name}" }
1972
+ end
1973
+ puts
1974
+ elsif args[0] == 'reload'
1975
+ # Reload all plugins
1976
+ @plugins = []
1977
+ @plugin_commands = {}
1978
+ load_plugins
1979
+ puts "Plugins reloaded (#{@plugins.length} loaded)"
1980
+ elsif args[0] == 'enable' && args[1]
1981
+ # Enable a plugin
1982
+ plugin_name = args[1]
1983
+ @plugin_disabled.delete(plugin_name)
1984
+ puts "Plugin '#{plugin_name}' enabled. Use :plugins reload to load it"
1985
+ rshrc
1986
+ elsif args[0] == 'disable' && args[1]
1987
+ # Disable a plugin
1988
+ plugin_name = args[1]
1989
+ unless @plugin_disabled.include?(plugin_name)
1990
+ @plugin_disabled << plugin_name
1991
+ end
1992
+ @plugins.reject! { |p| p[:name] == plugin_name }
1993
+ puts "Plugin '#{plugin_name}' disabled"
1994
+ rshrc
1995
+ elsif args[0] == 'info' && args[1]
1996
+ # Show plugin info
1997
+ plugin_name = args[1]
1998
+ plugin = @plugins.find { |p| p[:name] == plugin_name }
1999
+ if plugin
2000
+ puts "\n Plugin: #{plugin_name}".c(@c_prompt).b
2001
+ puts " Class: #{plugin[:class]}"
2002
+ puts " File: #{@plugin_dir}/#{plugin_name}.rb"
2003
+
2004
+ puts "\n Hooks:".c(@c_nick)
2005
+ %i[on_startup on_command_before on_command_after on_prompt].each do |hook|
2006
+ has_hook = plugin[:instance].respond_to?(hook) ? '✓' : '✗'
2007
+ puts " #{has_hook} #{hook}"
2008
+ end
2009
+
2010
+ puts "\n Extensions:".c(@c_nick)
2011
+ puts " ✓ add_completions" if plugin[:instance].respond_to?(:add_completions)
2012
+ puts " ✓ add_commands" if plugin[:instance].respond_to?(:add_commands)
2013
+ puts
2014
+ else
2015
+ puts "Plugin '#{plugin_name}' not found"
2016
+ end
2017
+ else
2018
+ puts "Usage:"
2019
+ puts " :plugins List all plugins"
2020
+ puts " :plugins reload Reload all plugins"
2021
+ puts " :plugins enable NAME Enable a plugin"
2022
+ puts " :plugins disable NAME Disable a plugin"
2023
+ puts " :plugins info NAME Show plugin details"
2024
+ end
2025
+ end
1747
2026
  def validate_command(cmd) # Syntax validation before execution
1748
2027
  return nil if cmd.nil? || cmd.empty?
1749
2028
  warnings = []
1750
2029
 
2030
+ # Apply custom validation rules first
2031
+ custom_warnings = apply_validation_rules(cmd)
2032
+ warnings.concat(custom_warnings) if custom_warnings.any?
2033
+
1751
2034
  # Check for common mistakes
1752
2035
  warnings << "Unmatched quotes" if cmd.count("'").odd? || cmd.count('"').odd?
1753
2036
  warnings << "Unmatched parentheses" if cmd.count("(") != cmd.count(")")
1754
2037
  warnings << "Unmatched brackets" if cmd.count("[") != cmd.count("]")
1755
2038
  warnings << "Unmatched braces" if cmd.count("{") != cmd.count("}")
1756
2039
 
1757
- # Check for potentially dangerous patterns
2040
+ # Check for potentially dangerous patterns (unless user has custom rules)
1758
2041
  warnings << "WARNING: Recursive rm detected" if cmd =~ /rm\s+.*-r.*\//
1759
2042
  warnings << "WARNING: Force flag without path" if cmd =~ /rm\s+-[rf]+\s*$/
1760
2043
  warnings << "WARNING: Sudo with redirection" if cmd =~ /sudo.*>/
1761
2044
 
1762
- # Check for common typos in popular commands
1763
- if cmd =~ /^(\w+)/
2045
+ # Check for common typos in popular commands (skip for : commands)
2046
+ if cmd =~ /^(\w+)/ && cmd !~ /^:/
1764
2047
  first_cmd = $1
1765
- unless @exe.include?(first_cmd) || @nick.include?(first_cmd) || first_cmd == "cd"
2048
+ # Check if command exists (don't warn for valid commands)
2049
+ is_valid_cmd = @exe.include?(first_cmd) ||
2050
+ @nick.include?(first_cmd) ||
2051
+ first_cmd == "cd" ||
2052
+ (@plugin_commands && @plugin_commands[first_cmd]) ||
2053
+ (@defuns && @defuns.keys.include?(first_cmd))
2054
+
2055
+ unless is_valid_cmd
1766
2056
  suggestions = suggest_command(first_cmd)
1767
2057
  if suggestions && !suggestions.empty?
1768
- warnings << "Command '#{first_cmd}' not found. Did you mean: #{suggestions.join(', ')}?"
2058
+ # Auto-correct if enabled and first suggestion has distance 2
2059
+ if @auto_correct && levenshtein_distance(first_cmd, suggestions[0]) <= 2
2060
+ warnings << "AUTO-CORRECTING: '#{first_cmd}' → '#{suggestions[0]}'"
2061
+ else
2062
+ warnings << "Command '#{first_cmd}' not found. Did you mean: #{suggestions.join(', ')}?"
2063
+ end
1769
2064
  end
1770
2065
  end
1771
2066
  end
1772
2067
 
1773
2068
  warnings.empty? ? nil : warnings
1774
2069
  end
2070
+ def calc(*args) # Inline calculator using Ruby's Math library
2071
+ if args.empty?
2072
+ puts "Usage: calc <expression>"
2073
+ puts "Examples:"
2074
+ puts " calc 2 + 2"
2075
+ puts " calc \"Math.sqrt(16)\""
2076
+ puts " calc \"Math::PI * 2\""
2077
+ return
2078
+ end
2079
+
2080
+ expression = args.join(' ')
2081
+
2082
+ begin
2083
+ # Safe evaluation with Math library
2084
+ result = eval(expression, binding, __FILE__, __LINE__)
2085
+ puts result
2086
+ rescue SyntaxError => e
2087
+ puts "Syntax error in expression: #{e.message}"
2088
+ rescue => e
2089
+ puts "Error evaluating expression: #{e.message}"
2090
+ end
2091
+ end
2092
+ def validate(rule_str = nil) # Custom validation rule management
2093
+ if rule_str.nil? || rule_str.empty?
2094
+ # List all validation rules
2095
+ if @validation_rules.empty?
2096
+ puts "\nNo validation rules defined"
2097
+ puts "Usage: :validate pattern = action"
2098
+ puts "Actions: block, confirm, warn, log"
2099
+ return
2100
+ end
2101
+ puts "\n Validation Rules:".c(@c_prompt).b
2102
+ @validation_rules.each_with_index do |rule, i|
2103
+ puts " #{i+1}. #{rule[:pattern].inspect} → #{rule[:action]}"
2104
+ end
2105
+ puts
2106
+ elsif rule_str =~ /^-(\d+)$/
2107
+ # Delete rule by index
2108
+ index = $1.to_i - 1
2109
+ if index >= 0 && index < @validation_rules.length
2110
+ rule = @validation_rules.delete_at(index)
2111
+ puts "Validation rule deleted: #{rule[:pattern]}"
2112
+ rshrc
2113
+ else
2114
+ puts "Invalid rule index: #{$1}"
2115
+ end
2116
+ elsif rule_str =~ /^(.+?)\s*=\s*(block|confirm|warn|log)$/
2117
+ # Add validation rule
2118
+ pattern = $1.strip
2119
+ action = $2.strip
2120
+ @validation_rules << {pattern: pattern, action: action}
2121
+ puts "Validation rule added: #{pattern} → #{action}"
2122
+ rshrc
2123
+ else
2124
+ puts "Usage: :validate pattern = action"
2125
+ puts "Example: :validate rm -rf / = block"
2126
+ puts "Actions: block (prevent), confirm (ask), warn (show), log (record)"
2127
+ end
2128
+ end
2129
+ def apply_validation_rules(cmd) # Apply custom validation rules
2130
+ return [] if @validation_rules.nil? || @validation_rules.empty?
2131
+
2132
+ warnings = []
2133
+
2134
+ @validation_rules.each do |rule|
2135
+ if cmd =~ /#{rule[:pattern]}/
2136
+ case rule[:action]
2137
+ when 'block'
2138
+ warnings << "BLOCKED by rule: #{rule[:pattern]}"
2139
+ when 'confirm'
2140
+ warnings << "CONFIRM required: #{rule[:pattern]}"
2141
+ when 'warn'
2142
+ warnings << "Warning: Matches rule '#{rule[:pattern]}'"
2143
+ when 'log'
2144
+ File.write("#{ENV['HOME']}/.rsh_validation.log",
2145
+ "#{Time.now}: #{cmd} (matched: #{rule[:pattern]})\n",
2146
+ mode: 'a')
2147
+ end
2148
+ end
2149
+ end
2150
+
2151
+ warnings
2152
+ end
2153
+ def apply_auto_correct(cmd) # Apply auto-correction to command
2154
+ return cmd unless @auto_correct
2155
+ return cmd if cmd =~ /^:/ # Don't auto-correct colon commands
2156
+ return cmd unless cmd =~ /^(\w+)/
2157
+
2158
+ first_cmd = $1
2159
+
2160
+ # Don't auto-correct shell keywords or shell scripts
2161
+ shell_keywords = %w[for while if then else elif fi do done case esac function select until]
2162
+ return cmd if shell_keywords.include?(first_cmd)
2163
+ return cmd if cmd =~ /\b(for|while|if|case|function|until)\b/ # Skip shell scripts
2164
+
2165
+ # Don't auto-correct if command exists
2166
+ return cmd if @exe.include?(first_cmd)
2167
+ return cmd if @nick.include?(first_cmd)
2168
+ return cmd if first_cmd == "cd"
2169
+ return cmd if @plugin_commands && @plugin_commands[first_cmd]
2170
+ return cmd if @defuns && @defuns.keys.include?(first_cmd) # Check user defuns
2171
+
2172
+ suggestions = suggest_command(first_cmd)
2173
+ if suggestions && !suggestions.empty? && levenshtein_distance(first_cmd, suggestions[0]) <= 2
2174
+ # Auto-correct using first (closest) suggestion
2175
+ cmd.sub(/^#{first_cmd}/, suggestions[0])
2176
+ else
2177
+ cmd
2178
+ end
2179
+ end
1775
2180
  def execute_conditional(cmd_line)
1776
2181
  # Split on && and || while preserving the operators
1777
2182
  parts = cmd_line.split(/(\s*&&\s*|\s*\|\|\s*)/)
@@ -2055,6 +2460,12 @@ def load_rshrc_safe
2055
2460
  @defuns = {} unless @defuns.is_a?(Hash)
2056
2461
  @history_dedup = 'smart' unless @history_dedup.is_a?(String)
2057
2462
  @session_autosave = 0 unless @session_autosave.is_a?(Integer)
2463
+ @auto_correct = false unless [@auto_correct].any? { |v| v == true || v == false }
2464
+ @slow_command_threshold = 0 unless @slow_command_threshold.is_a?(Integer)
2465
+ @plugin_disabled = [] unless @plugin_disabled.is_a?(Array)
2466
+ @plugins = [] unless @plugins.is_a?(Array)
2467
+ @plugin_commands = {} unless @plugin_commands.is_a?(Hash)
2468
+ @validation_rules = [] unless @validation_rules.is_a?(Array)
2058
2469
 
2059
2470
  # Restore defuns from .rshrc
2060
2471
  if @defuns && !@defuns.empty?
@@ -2191,6 +2602,12 @@ def load_defaults
2191
2602
  @switch_cache_time ||= {}
2192
2603
  @history_dedup ||= 'smart'
2193
2604
  @session_autosave ||= 0
2605
+ @auto_correct ||= false
2606
+ @slow_command_threshold ||= 0
2607
+ @plugin_disabled ||= []
2608
+ @plugins ||= []
2609
+ @plugin_commands ||= {}
2610
+ @validation_rules ||= []
2194
2611
  puts "Loaded with default configuration."
2195
2612
  end
2196
2613
 
@@ -2264,6 +2681,7 @@ begin # Load .rshrc and populate @history
2264
2681
  @c.restore # Max row & col gotten, cursor restored
2265
2682
  hist_clean # Remove duplicates, etc
2266
2683
  @path.map! {|p| p + "/*"} # Set proper format for path search
2684
+ @plugins_loaded = false # Defer plugin loading until first command
2267
2685
  end
2268
2686
 
2269
2687
  # MAIN PART
@@ -2273,14 +2691,22 @@ loop do
2273
2691
  @node = Etc.uname[:nodename] # For use in @prompt
2274
2692
  h = @history; f = @cmd_frequency; s = @cmd_stats; b = @bookmarks; d = @defuns; load_rshrc_safe; @history = h; @cmd_frequency = f; @cmd_stats = s; @bookmarks = b; @defuns = d # reload prompt but preserve runtime data
2275
2693
  @prompt.gsub!(/#{Dir.home}/, '~') # Simplify path in prompt
2276
- system("printf \"\033]0;rsh: #{Dir.pwd}\007\"") # Set Window title to path
2694
+ system("printf \"\033]0;rsh: #{Dir.pwd}\007\"") # Set Window title to path
2277
2695
  @history[0] = "" unless @history[0]
2278
2696
  cache_executables # Use cached executable lookup
2697
+ # Load plugins on first command (lazy loading)
2698
+ unless @plugins_loaded
2699
+ load_plugins
2700
+ @plugins_loaded = true
2701
+ end
2702
+ # Append plugin prompt additions (after plugins loaded)
2703
+ plugin_prompts = call_plugin_hook(:on_prompt)
2704
+ @prompt += plugin_prompts.join if plugin_prompts.any?
2279
2705
  # Auto-save session if enabled and interval elapsed
2280
2706
  if @session_autosave && @session_autosave > 0
2281
2707
  current_time = Time.now.to_i
2282
2708
  if (current_time - @session_last_save) >= @session_autosave
2283
- save_session('autosave')
2709
+ save_session('autosave', :silent)
2284
2710
  @session_last_save = current_time
2285
2711
  end
2286
2712
  end
@@ -2290,8 +2716,33 @@ loop do
2290
2716
  @dirs.pop
2291
2717
  hist_clean # Clean up the history
2292
2718
  @cmd = "ls" if @cmd == "" # Default to ls when no command is given
2293
- if @cmd.match(/^!\d+/)
2294
- hi = @history[@cmd.sub(/^!(\d+)$/, '\1').to_i+1]
2719
+ # Enhanced history commands
2720
+ if @cmd == '!!'
2721
+ # Repeat last command
2722
+ @cmd = @history[1] if @history.length > 1
2723
+ elsif @cmd =~ /^!-(\d+)$/
2724
+ # Repeat nth to last (e.g., !-2 = 2nd to last)
2725
+ index = $1.to_i
2726
+ @cmd = @history[index] if @history.length > index
2727
+ elsif @cmd =~ /^!(\d+):(\d+)$/
2728
+ # Chain commands (e.g., !5:7 = commands 5, 6, 7)
2729
+ start_idx = $1.to_i + 1
2730
+ end_idx = $2.to_i + 1
2731
+ if start_idx < @history.length && end_idx < @history.length && start_idx <= end_idx
2732
+ commands = @history[start_idx..end_idx].compact.reverse
2733
+ # Filter out colon commands (they don't work in shell chains)
2734
+ commands.reject! { |c| c =~ /^:/ }
2735
+ if commands.empty?
2736
+ puts "Cannot chain colon commands (use shell commands only)"
2737
+ @cmd = "ls" # Default to ls
2738
+ else
2739
+ @cmd = commands.join(' && ')
2740
+ puts " Chaining: #{@cmd}".c(@c_stamp)
2741
+ end
2742
+ end
2743
+ elsif @cmd.match(/^!\d+$/)
2744
+ # Original: !5 = command 5
2745
+ hi = @history[@cmd.sub(/^!(\d+)$/, '\1').to_i+1]
2295
2746
  @cmd = hi if hi
2296
2747
  end
2297
2748
  # Move cursor to end of line and print the full command before clearing
@@ -2333,26 +2784,73 @@ loop do
2333
2784
  end
2334
2785
  if @cmd.match(/^\s*:/) # Ruby commands are prefixed with ":"
2335
2786
  begin
2336
- eval(@cmd[1..-1])
2337
- #rescue StandardError => err
2787
+ cmd_line = @cmd[1..-1].strip
2788
+
2789
+ # Extract command name and arguments
2790
+ if cmd_line =~ /^(\w+\??)(.*)$/
2791
+ cmd_name = $1
2792
+ cmd_args_raw = $2.strip
2793
+
2794
+ # Commands that parse their own args (need full string)
2795
+ # nick/gnick parse "name = value"
2796
+ # defun parses "name(args) = body"
2797
+ # bm parses "name path #tags" or "-name" or "?tag" or "--export file"
2798
+ # validate parses "pattern = action"
2799
+ single_string_cmds = %w[nick gnick defun bm bookmark validate]
2800
+
2801
+ # List of all known rsh commands (since respond_to? doesn't work for top-level methods)
2802
+ known_commands = %w[nick gnick defun defun? bm bookmark stats calc config env theme plugins
2803
+ save_session load_session list_sessions delete_session rmsession
2804
+ validate
2805
+ history rmhistory jobs fg bg dirs help info version]
2806
+
2807
+ # Try to call as rsh method
2808
+ if known_commands.include?(cmd_name)
2809
+ if cmd_args_raw.empty?
2810
+ send(cmd_name.to_sym)
2811
+ elsif single_string_cmds.include?(cmd_name)
2812
+ # Pass entire args string for commands that parse it themselves
2813
+ send(cmd_name.to_sym, cmd_args_raw)
2814
+ else
2815
+ # Split args for variadic functions
2816
+ args = cmd_args_raw.split(/\s+/)
2817
+ send(cmd_name.to_sym, *args)
2818
+ end
2819
+ else
2820
+ # Fallback to eval for arbitrary Ruby (like :puts 2+2)
2821
+ eval(cmd_line)
2822
+ end
2823
+ else
2824
+ # Fallback to eval
2825
+ eval(cmd_line)
2826
+ end
2338
2827
  rescue Exception => err
2339
2828
  puts "\n#{err}"
2340
2829
  end
2341
2830
  elsif @cmd == '#' # List previous directories
2342
2831
  dirs
2343
2832
  else # Execute command
2344
- # Check if it's a user-defined Ruby function FIRST (before any expansions)
2833
+ # Check if it's a plugin command FIRST
2345
2834
  cmd_parts = @cmd.split(/\s+/)
2346
- func_name = cmd_parts[0]
2347
- if self.respond_to?(func_name) && singleton_class.instance_methods(false).include?(func_name.to_sym)
2835
+ cmd_name = cmd_parts[0]
2836
+ if @plugin_commands && @plugin_commands[cmd_name]
2837
+ begin
2838
+ args = cmd_parts[1..]
2839
+ result = @plugin_commands[cmd_name].call(*args)
2840
+ puts result unless result.nil?
2841
+ rescue => e
2842
+ puts "Error in plugin command '#{cmd_name}': #{e}"
2843
+ end
2844
+ # Then check if it's a user-defined Ruby function (before any expansions)
2845
+ elsif self.respond_to?(cmd_name) && singleton_class.instance_methods(false).include?(cmd_name.to_sym)
2348
2846
  begin
2349
2847
  args = cmd_parts[1..]
2350
- puts "DEBUG: Calling #{func_name} with args: #{args}" if ENV['RSH_DEBUG']
2351
- result = self.send(func_name, *args)
2848
+ puts "DEBUG: Calling #{cmd_name} with args: #{args}" if ENV['RSH_DEBUG']
2849
+ result = self.send(cmd_name, *args)
2352
2850
  puts "DEBUG: Result: #{result.inspect}" if ENV['RSH_DEBUG']
2353
2851
  puts result unless result.nil?
2354
2852
  rescue => e
2355
- puts "Error calling function '#{func_name}': #{e}"
2853
+ puts "Error calling function '#{cmd_name}': #{e}"
2356
2854
  end
2357
2855
  else
2358
2856
  # Handle conditional execution (&& and ||)
@@ -2360,20 +2858,48 @@ loop do
2360
2858
  execute_conditional(@cmd)
2361
2859
  next
2362
2860
  end
2363
- # Expand brace expansion {a,b,c}
2364
- @cmd = expand_braces(@cmd)
2365
- # Expand command substitution $(command) and backticks
2366
- @cmd = @cmd.gsub(/\$\(([^)]+)\)/) { `#{$1}`.chomp }
2367
- @cmd = @cmd.gsub(/`([^`]+)`/) { `#{$1}`.chomp }
2368
- # Expand environment variables and exit status
2369
- @cmd = @cmd.gsub(/\$\?/) { @last_exit.to_s }
2370
- @cmd = @cmd.gsub(/\$(\w+)|\$\{(\w+)\}/) { ENV[$1 || $2] || '' }
2371
- # Expand tilde
2861
+ # Detect shell scripting constructs - skip expansions if found
2862
+ is_shell_script = !(@cmd =~ /\b(for|while|if|case|function|until)\b/).nil?
2863
+
2864
+ unless is_shell_script
2865
+ # Expand brace expansion {a,b,c}
2866
+ @cmd = expand_braces(@cmd)
2867
+ # Expand command substitution $(command) and backticks
2868
+ @cmd = @cmd.gsub(/\$\(([^)]+)\)/) { `#{$1}`.chomp }
2869
+ @cmd = @cmd.gsub(/`([^`]+)`/) { `#{$1}`.chomp }
2870
+ # Expand environment variables and exit status
2871
+ @cmd = @cmd.gsub(/\$\?/) { @last_exit.to_s }
2872
+ @cmd = @cmd.gsub(/\$(\w+)|\$\{(\w+)\}/) { ENV[$1 || $2] || '' }
2873
+ end
2874
+ # Always expand tilde
2372
2875
  @cmd = @cmd.gsub(/~/, Dir.home)
2373
- ca = @nick.transform_keys {|k| /((^\K\s*\K)|(\|\K\s*\K))\b(?<!-)#{Regexp.escape k}\b/}
2374
- @cmd = @cmd.gsub(Regexp.union(ca.keys), @nick)
2375
- ga = @gnick.transform_keys {|k| /\b(?<!-)#{Regexp.escape k}\b/}
2376
- @cmd = @cmd.gsub(Regexp.union(ga.keys), @gnick)
2876
+ # Skip nick/gnick substitution for shell scripts
2877
+ unless is_shell_script
2878
+ # Check for parametrized nick BEFORE substitution
2879
+ cmd_parts_before = @cmd.split(/\s+/)
2880
+ first_cmd = cmd_parts_before[0]
2881
+ used_param_nick = first_cmd && @nick[first_cmd] && @nick[first_cmd].include?('{{')
2882
+
2883
+ # Do nick/gnick substitution
2884
+ ca = @nick.transform_keys {|k| /((^\K\s*\K)|(\|\K\s*\K))\b(?<!-)#{Regexp.escape k}\b/}
2885
+ @cmd = @cmd.gsub(Regexp.union(ca.keys), @nick)
2886
+ ga = @gnick.transform_keys {|k| /\b(?<!-)#{Regexp.escape k}\b/}
2887
+ @cmd = @cmd.gsub(Regexp.union(ga.keys), @gnick)
2888
+
2889
+ # Expand placeholders if parametrized nick was used
2890
+ if used_param_nick
2891
+ params = {}
2892
+ cmd_parts_before[1..].each do |part|
2893
+ if part =~ /^(\w+)=(.+)$/
2894
+ params[$1] = $2
2895
+ end
2896
+ end
2897
+ # Replace placeholders
2898
+ params.each { |k, v| @cmd.gsub!(/\{\{#{k}\}\}/, v) }
2899
+ # Remove key=value parameters from command
2900
+ @cmd = @cmd.split(/\s+/).reject { |p| p =~ /^\w+=/ }.join(' ')
2901
+ end
2902
+ end
2377
2903
  @cmd = "~" if @cmd == "cd"
2378
2904
  @cmd.sub!(/^cd (\S*).*/, '\1')
2379
2905
  @cmd = Dir.home if @cmd == "~"
@@ -2414,11 +2940,48 @@ loop do
2414
2940
  end
2415
2941
  else
2416
2942
  begin
2417
- # Validate command before execution
2943
+ pre_cmd
2944
+
2945
+ # Apply auto-correct if enabled (before validation)
2946
+ @cmd = apply_auto_correct(@cmd)
2947
+
2948
+ # Validate command after auto-correction
2418
2949
  warnings = validate_command(@cmd)
2419
2950
  if warnings && !warnings.empty?
2420
- warnings.each { |w| puts "#{w}".c(196) }
2421
- # For critical warnings, ask for confirmation
2951
+ # Check for BLOCKED commands
2952
+ if warnings.any? { |w| w.start_with?("BLOCKED") }
2953
+ warnings.select { |w| w.start_with?("BLOCKED") }.each { |w| puts "#{w}".c(196) }
2954
+ puts "Command execution blocked by validation rule"
2955
+ next
2956
+ end
2957
+
2958
+ # Show non-auto-correct warnings
2959
+ warnings.reject { |w| w.start_with?("AUTO-CORRECTING:") || w.start_with?("CONFIRM") }.each { |w| puts "#{w}".c(196) }
2960
+
2961
+ # Show auto-correct and ask for confirmation
2962
+ auto_correct_warnings = warnings.select { |w| w.start_with?("AUTO-CORRECTING:") }
2963
+ if auto_correct_warnings.any?
2964
+ auto_correct_warnings.each { |w| puts "#{w}".c(214) }
2965
+ print "Accept auto-correction? (Y/n): "
2966
+ response = $stdin.gets.chomp
2967
+ if response.downcase == 'n'
2968
+ puts "Auto-correction cancelled"
2969
+ next
2970
+ end
2971
+ end
2972
+
2973
+ # For CONFIRM validation rules
2974
+ if warnings.any? { |w| w.start_with?("CONFIRM") }
2975
+ warnings.select { |w| w.start_with?("CONFIRM") }.each { |w| puts "#{w}".c(214) }
2976
+ print "Confirm execution? (y/N): "
2977
+ response = $stdin.gets.chomp
2978
+ unless response.downcase == 'y'
2979
+ puts "Command cancelled"
2980
+ next
2981
+ end
2982
+ end
2983
+
2984
+ # For other critical warnings
2422
2985
  if warnings.any? { |w| w.start_with?("WARNING:") }
2423
2986
  print "Continue anyway? (y/N): "
2424
2987
  response = $stdin.gets.chomp
@@ -2429,7 +2992,16 @@ loop do
2429
2992
  end
2430
2993
  end
2431
2994
 
2432
- pre_cmd
2995
+ # Call plugin on_command_before hooks
2996
+ plugin_results = call_plugin_hook(:on_command_before, @cmd)
2997
+ # If any plugin returns false, skip command
2998
+ if plugin_results.include?(false)
2999
+ puts "Command blocked by plugin"
3000
+ next
3001
+ end
3002
+ # If any plugin returns a modified command, use it
3003
+ modified_cmd = plugin_results.find { |r| r.is_a?(String) }
3004
+ @cmd = modified_cmd if modified_cmd
2433
3005
 
2434
3006
  # Track command frequency for intelligent completion
2435
3007
  cmd_base = @cmd.split.first if @cmd && !@cmd.empty?
@@ -2440,6 +3012,9 @@ loop do
2440
3012
  # Start timing
2441
3013
  start_time = Time.now
2442
3014
 
3015
+ # Detect if command needs bash (for loops, etc.)
3016
+ needs_bash = !(@cmd =~ /\b(for|while|if|case|function|until)\b/).nil? || @cmd.include?(';')
3017
+
2443
3018
  # Handle background jobs
2444
3019
  if @cmd.end_with?(' &')
2445
3020
  @cmd = @cmd[0..-3] # Remove the &
@@ -2448,13 +3023,21 @@ loop do
2448
3023
  if @cmd.include?('|') || @cmd.include?('>') || @cmd.include?('<')
2449
3024
  pid = spawn(@cmd, pgroup: true)
2450
3025
  else
2451
- pid = spawn(@cmd)
3026
+ if needs_bash
3027
+ pid = spawn("bash", "-c", @cmd)
3028
+ else
3029
+ pid = spawn(@cmd)
3030
+ end
2452
3031
  end
2453
3032
  @jobs[@job_id] = {pid: pid, cmd: @cmd, status: :running}
2454
3033
  puts "[#{@job_id}] #{pid} #{@cmd}"
2455
3034
  else
2456
3035
  # Better handling of pipes and redirections
2457
- @current_pid = spawn(@cmd)
3036
+ if needs_bash
3037
+ @current_pid = spawn("bash", "-c", @cmd)
3038
+ else
3039
+ @current_pid = spawn(@cmd)
3040
+ end
2458
3041
  Process.wait(@current_pid)
2459
3042
  @last_exit = $?.exitstatus
2460
3043
  @current_pid = nil
@@ -2470,6 +3053,14 @@ loop do
2470
3053
  @cmd_stats[cmd_base][:avg_time] = @cmd_stats[cmd_base][:total_time] / @cmd_stats[cmd_base][:count]
2471
3054
  end
2472
3055
 
3056
+ # Slow command alert
3057
+ if @slow_command_threshold > 0 && elapsed > @slow_command_threshold
3058
+ puts "⚠ Command took #{'%.1f' % elapsed}s (threshold: #{@slow_command_threshold}s)".c(214)
3059
+ end
3060
+
3061
+ # Call plugin on_command_after hooks
3062
+ call_plugin_hook(:on_command_after, @cmd, @last_exit)
3063
+
2473
3064
  post_cmd
2474
3065
  rescue StandardError => err
2475
3066
  puts "\nError: #{err}"