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.
- checksums.yaml +4 -4
- data/PLUGIN_GUIDE.md +757 -0
- data/README.md +199 -15
- data/bin/rsh +672 -81
- 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.
|
|
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.
|
|
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.
|
|
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'
|
|
341
|
-
|
|
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 "
|
|
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
|
|
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
|
|
1076
|
-
left_col << ":gnick
|
|
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
|
|
1080
|
-
left_col << ":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
|
|
1147
|
+
right_col << ":defun f(x) = x*2 Define function"
|
|
1090
1148
|
right_col << ":defun? List functions"
|
|
1091
|
-
right_col << ":defun
|
|
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.
|
|
1101
|
-
right_col << ":
|
|
1102
|
-
right_col << ":
|
|
1103
|
-
right_col << "
|
|
1104
|
-
right_col << "
|
|
1105
|
-
right_col << "
|
|
1106
|
-
right_col << "
|
|
1107
|
-
right_col << ":
|
|
1108
|
-
right_col << ":
|
|
1109
|
-
right_col << ":
|
|
1110
|
-
right_col << ":
|
|
1111
|
-
right_col << ":
|
|
1112
|
-
right_col << "
|
|
1113
|
-
right_col << ":
|
|
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(
|
|
1425
|
-
|
|
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
|
|
1569
|
+
elsif arg_str =~ /^--export\s*(.*)/
|
|
1442
1570
|
# Export bookmarks to file
|
|
1443
|
-
filename =
|
|
1571
|
+
filename = $1.strip
|
|
1572
|
+
filename = 'bookmarks.json' if filename.empty?
|
|
1444
1573
|
export_bookmarks(filename)
|
|
1445
|
-
elsif
|
|
1574
|
+
elsif arg_str =~ /^--import\s+(.*)/
|
|
1446
1575
|
# Import bookmarks from file
|
|
1447
|
-
filename =
|
|
1576
|
+
filename = $1.strip
|
|
1448
1577
|
import_bookmarks(filename) if filename
|
|
1449
|
-
elsif
|
|
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(
|
|
1502
|
-
bm(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2294
|
-
|
|
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
|
-
|
|
2337
|
-
|
|
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
|
|
2833
|
+
# Check if it's a plugin command FIRST
|
|
2345
2834
|
cmd_parts = @cmd.split(/\s+/)
|
|
2346
|
-
|
|
2347
|
-
if
|
|
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 #{
|
|
2351
|
-
result = self.send(
|
|
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 '#{
|
|
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
|
-
#
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
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
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2421
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}"
|