ruby-shell 2.12.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +24 -6
- data/bin/rsh +465 -68
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 121b91f0e19b4e13cd6cacf00094552851ea44bf376c4e1f0e6554c83ee99a34
|
|
4
|
+
data.tar.gz: e63b51b9f7742b87b4bbc96ee41da6ba12ef93df0631b4ac564548b87f6d0a33
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc4fb4e0cc354ff353532a01f334ba85e23725cc5757328118ea85c0b25f46c814f12e9e63dfcf2ca83ef48f3c015696ffd9b8c6661b94398295a67c0be285dc
|
|
7
|
+
data.tar.gz: 1790d5590bcdec77d461dd491fc34fb915324e78eea7d385bbdd35e2f71f86eaa9ab3995fcbd3855c640dad029530baa7615994318fb9a42718aa7246ea938a9
|
data/README.md
CHANGED
|
@@ -33,7 +33,18 @@ Or simply `gem install ruby-shell`.
|
|
|
33
33
|
* All colors are themeable in .rshrc (see github link for possibilities)
|
|
34
34
|
* Copy current command line to primary selection (paste w/middle button) with `Ctrl-y`
|
|
35
35
|
|
|
36
|
-
## NEW in
|
|
36
|
+
## NEW in v3.0.0 - Major Feature Release ⭐⭐⭐
|
|
37
|
+
* **Persistent Ruby Functions**: defun functions now save to .rshrc and persist across sessions
|
|
38
|
+
* **Smart Command Suggestions**: Typo detection with "Did you mean...?" suggestions using Levenshtein distance
|
|
39
|
+
* **Command Analytics**: New `:stats` command shows usage statistics, performance metrics, and most-used commands
|
|
40
|
+
* **Switch Completion Caching**: Command switches from --help are cached for instant completion
|
|
41
|
+
* **Enhanced Bookmarks**: Bookmark directories with tags - `:bookmark name path #tag1,tag2`
|
|
42
|
+
* **Session Management**: Save and restore entire shell sessions with `:save_session` and `:load_session`
|
|
43
|
+
* **Syntax Validation**: Pre-execution warnings for common mistakes, dangerous commands, and typos
|
|
44
|
+
* **Option Value Completion**: TAB completion for option values like `--format=<TAB>` → json, yaml, xml
|
|
45
|
+
* **Command Performance Tracking**: Automatically tracks execution time and shows slowest commands
|
|
46
|
+
|
|
47
|
+
## AI Integration (v2.9.0) ⭐
|
|
37
48
|
* **AI-powered command assistance**: Get help with commands using natural language
|
|
38
49
|
* **`@ <question>`**: Ask questions and get AI-generated text responses
|
|
39
50
|
* **`@@ <request>`**: Describe what you want to do, and AI suggests the command
|
|
@@ -73,13 +84,18 @@ Special functions/integrations:
|
|
|
73
84
|
Special commands:
|
|
74
85
|
* `:nick 'll = ls -l'` to make a command alias (ll) point to a command (ls -l)
|
|
75
86
|
* `:gnick 'h = /home/me'` to make a general alias (h) point to something (/home/me)
|
|
76
|
-
* `:
|
|
77
|
-
* `:
|
|
78
|
-
* `:nick?` will list all command nicks and general nicks (you can edit your nicks in .rshrc)
|
|
87
|
+
* `:nick` lists all command nicks, `:gnick` lists general nicks (NEW in v3.0)
|
|
88
|
+
* `:nick '-name'` delete a command nick, `:gnick '-name'` delete a general nick (NEW in v3.0)
|
|
79
89
|
* `:history` will list the command history, while `:rmhistory` will delete the history
|
|
80
90
|
* `:jobs` will list background jobs, `:fg [job_id]` brings jobs to foreground, `:bg [job_id]` resumes stopped jobs
|
|
81
|
-
* `:defun 'func(args) = code'` defines Ruby functions callable as shell commands
|
|
91
|
+
* `:defun 'func(args) = code'` defines Ruby functions callable as shell commands (now persistent!)
|
|
82
92
|
* `:defun?` lists all user-defined functions, `:defun '-func'` removes functions
|
|
93
|
+
* `:stats` shows command execution statistics and analytics (NEW in v3.0)
|
|
94
|
+
* `:bm "name"` or `:bookmark "name"` bookmark current directory, `:bm "name path #tags"` with tags (NEW in v3.0)
|
|
95
|
+
* `:bm` lists all bookmarks, just type bookmark name to jump (e.g., `work`) (NEW in v3.0)
|
|
96
|
+
* `:bm "-name"` delete bookmark, `:bm "?tag"` search by tag (NEW in v3.0)
|
|
97
|
+
* `:save_session` saves current shell session (pwd, history, bookmarks, defuns) (NEW in v3.0)
|
|
98
|
+
* `:load_session` restores previously saved session (NEW in v3.0)
|
|
83
99
|
* `:info` shows introduction and feature overview
|
|
84
100
|
* `:version` Shows the rsh version number and the last published gem file version
|
|
85
101
|
* `:help` will display a compact command reference in two columns
|
|
@@ -185,7 +201,7 @@ Enter the command `f` to launch the fuzzy finder - select the directory/file you
|
|
|
185
201
|
If you start a line with "=", the rest of the line will be interpreted as an XRPN program. This gives you the full power of XRPN right at your fingertips. You can do simple stuff like this: `=13,23,*,x^2` and the answer to `(13 * 23)^2` will be given (89401) in the format that you have set in your `.xrpn/conf`. Or you can do more elaborate stuff like `=fix 6,5,sto c,time,'Time now is: ',atime,aview,pse,fix 0,lbl a,rcl c,prx,dse c,gto a`. Go crazy. Use single-quotes for any Alpha entry.
|
|
186
202
|
|
|
187
203
|
## Syntax highlighting
|
|
188
|
-
rsh will highlight nicks, gnicks, commands and dirs/files as they are written on the command line.
|
|
204
|
+
rsh will highlight nicks, gnicks, bookmarks, commands, switches and dirs/files as they are written on the command line. Each element type has its own color (customizable in .rshrc).
|
|
189
205
|
|
|
190
206
|
## Theming
|
|
191
207
|
In the supplied `.rshrc`, you will find a set of colors that you can change:
|
|
@@ -197,6 +213,8 @@ Variable | Description
|
|
|
197
213
|
`@c_nick` | Color for matching nick
|
|
198
214
|
`@c_gnick` | Color for matching gnick
|
|
199
215
|
`@c_path` | Color for valid path
|
|
216
|
+
`@c_switch` | Color for command switches/options
|
|
217
|
+
`@c_bookmark` | Color for bookmarks (NEW in v3.0)
|
|
200
218
|
`@c_tabselect` | Color for selected tabcompleted item
|
|
201
219
|
`@c_taboption` | Color for unselected tabcompleted item
|
|
202
220
|
`@c_stamp` | Color for time stamp/command
|
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 = "
|
|
11
|
+
@version = "3.0.0" # Major release: Persistent defuns, switch caching, smart suggestions, analytics, tooltips, bookmarks, validation, sessions
|
|
12
12
|
|
|
13
13
|
# MODULES, CLASSES AND EXTENSIONS
|
|
14
14
|
class String # Add coloring to strings (with escaping for Readline)
|
|
@@ -92,6 +92,7 @@ begin # Initialization
|
|
|
92
92
|
@c_gnick = 14 # Color for matching gnick
|
|
93
93
|
@c_path = 3 # Color for valid path
|
|
94
94
|
@c_switch = 6 # Color for switches/options
|
|
95
|
+
@c_bookmark = 13 # Color for bookmarks
|
|
95
96
|
@c_tabselect = 5 # Color for selected tabcompleted item
|
|
96
97
|
@c_taboption = 244 # Color for unselected tabcompleted item
|
|
97
98
|
@c_stamp = 244 # Color for time stamp/command
|
|
@@ -131,6 +132,13 @@ begin # Initialization
|
|
|
131
132
|
"gem" => %w[install uninstall update list search build push],
|
|
132
133
|
"bundle" => %w[install update exec check config]
|
|
133
134
|
}
|
|
135
|
+
# New v3.0 features initialization
|
|
136
|
+
@switch_cache = {} # Cache for command switches from --help
|
|
137
|
+
@switch_cache_time = {} # Timestamp for cache expiry
|
|
138
|
+
@bookmarks = {} # Enhanced bookmarks with tags
|
|
139
|
+
@defuns = {} # Store defun definitions for persistence
|
|
140
|
+
@cmd_stats = {} # Command execution statistics
|
|
141
|
+
@session_file = Dir.home + '/.rsh_session' # Session save file
|
|
134
142
|
def pre_cmd; end # User-defined function to be run BEFORE command execution
|
|
135
143
|
def post_cmd; end # User-defined function to be run AFTER command execution
|
|
136
144
|
end
|
|
@@ -138,32 +146,37 @@ end
|
|
|
138
146
|
# HELP TEXT
|
|
139
147
|
@info = <<~INFO
|
|
140
148
|
|
|
141
|
-
Hello #{@user}, welcome to rsh - the Ruby SHell.
|
|
142
|
-
|
|
143
|
-
rsh does not attempt to compete with the grand old shells like bash and zsh.
|
|
144
|
-
It serves the specific needs and wants of its author. If you like it, then feel free
|
|
149
|
+
Hello #{@user}, welcome to rsh v3.0 - the Ruby SHell.
|
|
150
|
+
|
|
151
|
+
rsh does not attempt to compete with the grand old shells like bash and zsh.
|
|
152
|
+
It serves the specific needs and wants of its author. If you like it, then feel free
|
|
145
153
|
to ask for more or different features here: https://github.com/isene/rsh.
|
|
146
|
-
|
|
147
|
-
Features:
|
|
154
|
+
|
|
155
|
+
Core Features:
|
|
148
156
|
* Aliases (called nicks in rsh) - both for commands and general nicks
|
|
149
|
-
* Syntax highlighting
|
|
157
|
+
* Syntax highlighting for nicks, bookmarks, commands, switches and valid dirs/files
|
|
150
158
|
* Tab completions for nicks, system commands, command switches and dirs/files
|
|
151
159
|
* Smart context-aware tab completion for git, apt, docker, systemctl, cargo, npm, gem, bundle
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
|
|
155
|
-
*
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
160
|
+
* History with editing, search and repeat (use `!` or UP arrow)
|
|
161
|
+
* Auto-suggestions from history (press RIGHT arrow to accept)
|
|
162
|
+
* Ruby functions callable as shell commands (persistent across sessions)
|
|
163
|
+
* AI integration: Use @ for text responses and @@ for command suggestions
|
|
164
|
+
|
|
165
|
+
NEW in v3.0:
|
|
166
|
+
* Command analytics - :stats shows usage patterns and performance metrics
|
|
167
|
+
* Enhanced bookmarks with tags - :bm "name path #tag1,tag2" then just type name to jump
|
|
168
|
+
* Session management - :save_session and :load_session preserve your entire shell state
|
|
169
|
+
* Smart typo detection - "Did you mean...?" suggestions for misspelled commands
|
|
170
|
+
* Switch caching - Faster TAB completion for command options
|
|
171
|
+
* Option value completion - TAB complete values like --format=json
|
|
172
|
+
* Syntax validation - Pre-execution warnings for dangerous or malformed commands
|
|
173
|
+
* Unified command syntax - :nick, :gnick, :bm all work the same way (list/create/delete)
|
|
174
|
+
|
|
175
|
+
Config file (.rshrc) updates on exit (Ctrl-d) or not (Ctrl-e).
|
|
176
|
+
All colors are themeable in .rshrc (see github link for possibilities).
|
|
177
|
+
|
|
178
|
+
Use `:help` for complete command reference.
|
|
179
|
+
|
|
167
180
|
INFO
|
|
168
181
|
|
|
169
182
|
@help = <<~HELP
|
|
@@ -192,6 +205,7 @@ def firstrun
|
|
|
192
205
|
@c_gnick = 87 # Color for matching gnick
|
|
193
206
|
@c_path = 208 # Color for valid path
|
|
194
207
|
@c_switch = 148 # Color for switches/options
|
|
208
|
+
@c_bookmark = 13 # Color for bookmarks
|
|
195
209
|
@c_tabselect = 207 # Color for selected tabcompleted item
|
|
196
210
|
@c_taboption = 244 # Color for unselected tabcompleted item
|
|
197
211
|
@c_stamp = 244 # Color for time stamp/command
|
|
@@ -468,7 +482,8 @@ def tab(type)
|
|
|
468
482
|
@tabstr = @pretab if type == "hist" # Searching for matches with whole string in history
|
|
469
483
|
@pretab = @pretab.delete_suffix(@tabstr)
|
|
470
484
|
end
|
|
471
|
-
type = "switch" if @tabstr && @tabstr[0] == "-"
|
|
485
|
+
type = "switch" if @tabstr && @tabstr[0] == "-" && !@tabstr.include?("=")
|
|
486
|
+
type = "option_value" if @tabstr && @tabstr =~ /^--?[\w-]+=/
|
|
472
487
|
type = "env_vars" if @tabstr && @tabstr[0] == "$"
|
|
473
488
|
|
|
474
489
|
# Debug output when RSH_DEBUG is set
|
|
@@ -517,12 +532,29 @@ def tab(type)
|
|
|
517
532
|
@tabarray.shift # Take away @history[0]
|
|
518
533
|
return if @tabarray.empty?
|
|
519
534
|
when "switch"
|
|
520
|
-
cmdswitch = @pretab.split(/[|, ]/).last.to_s
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
535
|
+
cmdswitch = @pretab.split(/[|, ]/).last.to_s.strip
|
|
536
|
+
@tabarray = get_command_switches(cmdswitch)
|
|
537
|
+
when "option_value" # Completion for option values like --format=<value>
|
|
538
|
+
if @tabstr =~ /^--?[\w-]+=(.*)/
|
|
539
|
+
value_prefix = $1
|
|
540
|
+
option = @tabstr.sub(/=.*/, '')
|
|
541
|
+
# Define common option value completions
|
|
542
|
+
value_completions = {
|
|
543
|
+
/format/ => %w[json yaml xml csv plain],
|
|
544
|
+
/output/ => %w[json yaml xml text html],
|
|
545
|
+
/level|log-level/ => %w[debug info warn error fatal],
|
|
546
|
+
/color/ => %w[auto always never],
|
|
547
|
+
/type/ => %w[file dir link all]
|
|
548
|
+
}
|
|
549
|
+
matches = []
|
|
550
|
+
value_completions.each do |pattern, values|
|
|
551
|
+
if option =~ pattern
|
|
552
|
+
matches = values.select { |v| v.start_with?(value_prefix) }
|
|
553
|
+
break unless matches.empty?
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
@tabarray = matches.map { |v| "#{option}=#{v}" }
|
|
557
|
+
end
|
|
526
558
|
when "dirs_only" # Only show directories
|
|
527
559
|
fdir = @tabstr + "*"
|
|
528
560
|
dirs = Dir.glob(fdir).select { |d| Dir.exist?(d) }.map { |d| d + "/" }
|
|
@@ -717,6 +749,85 @@ def tabend
|
|
|
717
749
|
@c_col = @pos0 + @pos
|
|
718
750
|
@c.col(@c_col)
|
|
719
751
|
end
|
|
752
|
+
def get_command_switches(command) # Helper function to extract switches from --help
|
|
753
|
+
# Check cache first (cache expires after 1 hour)
|
|
754
|
+
cache_key = command.to_s.strip
|
|
755
|
+
return [] if cache_key.empty?
|
|
756
|
+
|
|
757
|
+
current_time = Time.now.to_i
|
|
758
|
+
if @switch_cache[cache_key] && @switch_cache_time[cache_key] && (current_time - @switch_cache_time[cache_key]) < 3600
|
|
759
|
+
return @switch_cache[cache_key]
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
# Parse --help output
|
|
763
|
+
hlp = `#{command} --help 2>/dev/null`
|
|
764
|
+
# Try -h if --help didn't work
|
|
765
|
+
hlp = `#{command} -h 2>/dev/null` if hlp.empty?
|
|
766
|
+
return [] if hlp.empty?
|
|
767
|
+
|
|
768
|
+
switches = []
|
|
769
|
+
|
|
770
|
+
# Method 1: Lines starting with switches (traditional format)
|
|
771
|
+
switches = hlp.split("\n").grep(/^\s*-{1,2}[^-]/)
|
|
772
|
+
switches.map! { |h| h.sub(/^\s*/, '').sub(/^--/, ' --') }
|
|
773
|
+
switches.reject! { |h| /-</ =~ h }
|
|
774
|
+
|
|
775
|
+
# Method 2: Extract switches from usage line (git-style format)
|
|
776
|
+
if switches.empty?
|
|
777
|
+
usage_lines = hlp.split("\n").select { |l| l =~ /usage:|Usage:/ }
|
|
778
|
+
usage_lines.each do |line|
|
|
779
|
+
# Extract all switches from the usage line
|
|
780
|
+
line.scan(/(-[a-zA-Z]|--[a-z-]+)/).each do |match|
|
|
781
|
+
switch = match[0]
|
|
782
|
+
switches << (switch.start_with?('--') ? " #{switch}" : switch)
|
|
783
|
+
end
|
|
784
|
+
end
|
|
785
|
+
switches.uniq!
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
# Cache the result (even if empty, to avoid repeated failures)
|
|
789
|
+
@switch_cache[cache_key] = switches
|
|
790
|
+
@switch_cache_time[cache_key] = current_time
|
|
791
|
+
|
|
792
|
+
switches
|
|
793
|
+
end
|
|
794
|
+
def levenshtein_distance(s, t) # Calculate edit distance for smart suggestions
|
|
795
|
+
m = s.length
|
|
796
|
+
n = t.length
|
|
797
|
+
return m if n == 0
|
|
798
|
+
return n if m == 0
|
|
799
|
+
d = Array.new(m+1) {Array.new(n+1)}
|
|
800
|
+
(0..m).each {|i| d[i][0] = i}
|
|
801
|
+
(0..n).each {|j| d[0][j] = j}
|
|
802
|
+
(1..n).each do |j|
|
|
803
|
+
(1..m).each do |i|
|
|
804
|
+
d[i][j] = if s[i-1] == t[j-1]
|
|
805
|
+
d[i-1][j-1]
|
|
806
|
+
else
|
|
807
|
+
[d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+1].min
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
end
|
|
811
|
+
d[m][n]
|
|
812
|
+
end
|
|
813
|
+
def suggest_command(cmd) # Smart command suggestions for typos
|
|
814
|
+
return nil if cmd.nil? || cmd.empty?
|
|
815
|
+
return nil if @exe.include?(cmd) || @nick.include?(cmd)
|
|
816
|
+
|
|
817
|
+
# Find commands with small edit distance
|
|
818
|
+
candidates = (@exe + @nick.keys).select do |c|
|
|
819
|
+
next false if c.length < 2
|
|
820
|
+
dist = levenshtein_distance(cmd, c)
|
|
821
|
+
max_dist = [cmd.length / 3, 2].max
|
|
822
|
+
dist <= max_dist && dist > 0
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
return nil if candidates.empty?
|
|
826
|
+
|
|
827
|
+
# Sort by distance
|
|
828
|
+
candidates.sort_by! { |c| levenshtein_distance(cmd, c) }
|
|
829
|
+
candidates.first(3)
|
|
830
|
+
end
|
|
720
831
|
def hist_clean # Clean up @history
|
|
721
832
|
@history.uniq!
|
|
722
833
|
@history.compact!
|
|
@@ -734,6 +845,7 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic
|
|
|
734
845
|
|
|
735
846
|
str.gsub(/(?:\S'[^']*'|[^ '])+/) do |el|
|
|
736
847
|
clean_el = el.gsub("'", "")
|
|
848
|
+
# Priority: commands > nicks > bookmarks > paths
|
|
737
849
|
if @exe.include?(el)
|
|
738
850
|
el.c(@c_cmd)
|
|
739
851
|
elsif el == "cd"
|
|
@@ -741,8 +853,6 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic
|
|
|
741
853
|
elsif clean_el =~ /^\.\/(.+)/ && File.exist?(clean_el) && File.executable?(clean_el)
|
|
742
854
|
# Color local executables starting with ./
|
|
743
855
|
el.c(@c_cmd)
|
|
744
|
-
elsif File.exist?(clean_el)
|
|
745
|
-
el.c(@c_path)
|
|
746
856
|
elsif @nick.include?(el)
|
|
747
857
|
el.c(@c_nick)
|
|
748
858
|
elsif el == "r" or el == "f"
|
|
@@ -751,6 +861,11 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic
|
|
|
751
861
|
el.c(@c_gnick)
|
|
752
862
|
elsif self.respond_to?(el) && singleton_class.instance_methods(false).include?(el.to_sym)
|
|
753
863
|
el.c(@c_nick).b # Ruby functions in bold nick color
|
|
864
|
+
elsif @bookmarks && @bookmarks.include?(el)
|
|
865
|
+
# Color bookmarks (after commands and nicks)
|
|
866
|
+
el.c(@c_bookmark)
|
|
867
|
+
elsif File.exist?(clean_el)
|
|
868
|
+
el.c(@c_path)
|
|
754
869
|
elsif el[0] == "-"
|
|
755
870
|
el.c(@c_switch)
|
|
756
871
|
else
|
|
@@ -771,6 +886,12 @@ def rshrc # Write updates to .rshrc
|
|
|
771
886
|
conf += "@gnick = #{@gnick}\n"
|
|
772
887
|
conf.sub!(/^@cmd_frequency.*(\n|$)/, "")
|
|
773
888
|
conf += "@cmd_frequency = #{@cmd_frequency}\n"
|
|
889
|
+
conf.sub!(/^@cmd_stats.*(\n|$)/, "")
|
|
890
|
+
conf += "@cmd_stats = #{@cmd_stats}\n" unless @cmd_stats.empty?
|
|
891
|
+
conf.sub!(/^@bookmarks.*(\n|$)/, "")
|
|
892
|
+
conf += "@bookmarks = #{@bookmarks}\n" unless @bookmarks.empty?
|
|
893
|
+
conf.sub!(/^@defuns.*(\n|$)/, "")
|
|
894
|
+
conf += "@defuns = #{@defuns}\n" unless @defuns.empty?
|
|
774
895
|
# Only write @cmd_completions if user has customized it
|
|
775
896
|
unless conf =~ /^@cmd_completions\s*=/
|
|
776
897
|
# Don't write default completions to avoid cluttering .rshrc
|
|
@@ -820,9 +941,10 @@ def help
|
|
|
820
941
|
left_col << "SPECIAL COMMANDS:".c(@c_prompt).b
|
|
821
942
|
left_col << ":nick 'll = ls -l' Command alias"
|
|
822
943
|
left_col << ":gnick 'h = /home' General alias"
|
|
823
|
-
left_col << ":
|
|
824
|
-
left_col << ":
|
|
825
|
-
left_col << ":nick
|
|
944
|
+
left_col << ":nick List nicks"
|
|
945
|
+
left_col << ":gnick List gnicks"
|
|
946
|
+
left_col << ":nick '-name' Delete nick"
|
|
947
|
+
left_col << ":gnick '-name' Delete gnick"
|
|
826
948
|
left_col << ":history Show history"
|
|
827
949
|
left_col << ":rmhistory Clear history"
|
|
828
950
|
left_col << ":info About rsh"
|
|
@@ -842,6 +964,16 @@ def help
|
|
|
842
964
|
right_col << ":fg [id] Foreground job"
|
|
843
965
|
right_col << ":bg [id] Resume in bg"
|
|
844
966
|
right_col << ""
|
|
967
|
+
right_col << "v3.0 NEW FEATURES:".c(@c_prompt).b
|
|
968
|
+
right_col << ":stats Command analytics"
|
|
969
|
+
right_col << ":bm \"name\" Create bookmark"
|
|
970
|
+
right_col << "name Jump to bookmark"
|
|
971
|
+
right_col << ":bm List bookmarks"
|
|
972
|
+
right_col << ":bm \"-name\" Delete bookmark"
|
|
973
|
+
right_col << ":bm \"?tag\" Search by tag"
|
|
974
|
+
right_col << ":save_session Save session"
|
|
975
|
+
right_col << ":load_session Restore session"
|
|
976
|
+
right_col << ""
|
|
845
977
|
right_col << "INTEGRATIONS:".c(@c_prompt).b
|
|
846
978
|
right_col << "r Launch rtfm"
|
|
847
979
|
right_col << "f Launch fzf"
|
|
@@ -855,8 +987,8 @@ def help
|
|
|
855
987
|
right_col << "SMART COMPLETIONS:".c(@c_prompt).b
|
|
856
988
|
right_col << "git <TAB> Git subcommands"
|
|
857
989
|
right_col << "apt/docker <TAB> Command options"
|
|
858
|
-
right_col << "
|
|
859
|
-
right_col << "
|
|
990
|
+
right_col << "--format=<TAB> Option values"
|
|
991
|
+
right_col << "Typo suggestions Auto-correct"
|
|
860
992
|
right_col << ""
|
|
861
993
|
right_col << "EXPANSIONS:".c(@c_prompt).b
|
|
862
994
|
right_col << "~ Home directory"
|
|
@@ -899,43 +1031,47 @@ def rmhistory # Delete history
|
|
|
899
1031
|
@history = []
|
|
900
1032
|
puts "History deleted."
|
|
901
1033
|
end
|
|
902
|
-
def nick(nick_str) # Define a new nick like this: `:nick "ls = ls --color"`
|
|
903
|
-
if nick_str.
|
|
1034
|
+
def nick(nick_str = nil) # Define a new nick like this: `:nick "ls = ls --color"`
|
|
1035
|
+
if nick_str.nil? || nick_str.empty?
|
|
1036
|
+
# List all nicks
|
|
1037
|
+
puts "\n Command nicks:".c(@c_nick).b
|
|
1038
|
+
if @nick.empty?
|
|
1039
|
+
puts " (none defined)"
|
|
1040
|
+
else
|
|
1041
|
+
@nick.sort.each {|key, value| puts " #{key.c(@c_nick)} = #{value}"}
|
|
1042
|
+
end
|
|
1043
|
+
puts
|
|
1044
|
+
elsif nick_str.match(/^\s*-/)
|
|
904
1045
|
source = nick_str.sub(/^\s*-/, '')
|
|
905
1046
|
@nick.delete(source)
|
|
1047
|
+
rshrc
|
|
906
1048
|
else
|
|
907
1049
|
source = nick_str.sub(/ =.*/, '')
|
|
908
1050
|
target = nick_str.sub(/.*= /, '')
|
|
909
1051
|
@nick[source] = target
|
|
1052
|
+
rshrc
|
|
910
1053
|
end
|
|
911
|
-
rshrc
|
|
912
1054
|
end
|
|
913
|
-
def gnick(nick_str) # Define a generic/global nick to match not only commands (format like nick)
|
|
914
|
-
if nick_str.
|
|
1055
|
+
def gnick(nick_str = nil) # Define a generic/global nick to match not only commands (format like nick)
|
|
1056
|
+
if nick_str.nil? || nick_str.empty?
|
|
1057
|
+
# List all gnicks
|
|
1058
|
+
puts "\n General nicks:".c(@c_gnick).b
|
|
1059
|
+
if @gnick.empty?
|
|
1060
|
+
puts " (none defined)"
|
|
1061
|
+
else
|
|
1062
|
+
@gnick.sort.each {|key, value| puts " #{key.c(@c_gnick)} = #{value}"}
|
|
1063
|
+
end
|
|
1064
|
+
puts
|
|
1065
|
+
elsif nick_str.match(/^\s*-/)
|
|
915
1066
|
source = nick_str.sub(/^\s*-/, '')
|
|
916
1067
|
@gnick.delete(source)
|
|
1068
|
+
rshrc
|
|
917
1069
|
else
|
|
918
1070
|
source = nick_str.sub(/ =.*/, '')
|
|
919
1071
|
target = nick_str.sub(/.*= /, '')
|
|
920
1072
|
@gnick[source] = target
|
|
1073
|
+
rshrc
|
|
921
1074
|
end
|
|
922
|
-
rshrc
|
|
923
|
-
end
|
|
924
|
-
def nick? # Show nicks
|
|
925
|
-
puts " Command nicks:".c(@c_nick)
|
|
926
|
-
@nick.sort.each {|key, value| puts " #{key} = #{value}"}
|
|
927
|
-
puts " General nicks:".c(@c_gnick)
|
|
928
|
-
@gnick.sort.each {|key, value| puts " #{key} = #{value}"}
|
|
929
|
-
end
|
|
930
|
-
def nickdel(nick_name) # Delete a command nick
|
|
931
|
-
@nick.delete(nick_name)
|
|
932
|
-
rshrc
|
|
933
|
-
puts "Nick '#{nick_name}' deleted"
|
|
934
|
-
end
|
|
935
|
-
def gnickdel(nick_name) # Delete a general/global nick
|
|
936
|
-
@gnick.delete(nick_name)
|
|
937
|
-
rshrc
|
|
938
|
-
puts "General nick '#{nick_name}' deleted"
|
|
939
1075
|
end
|
|
940
1076
|
def dirs
|
|
941
1077
|
puts "Past direactories:"
|
|
@@ -994,6 +1130,7 @@ def defun(func_def) # Define a Ruby function like: `:defun "myls(*args) = Dir.g
|
|
|
994
1130
|
func_name = func_def.sub(/^\s*-/, '')
|
|
995
1131
|
if self.respond_to?(func_name)
|
|
996
1132
|
singleton_class.remove_method(func_name.to_sym)
|
|
1133
|
+
@defuns.delete(func_name)
|
|
997
1134
|
puts "Function '#{func_name}' removed"
|
|
998
1135
|
else
|
|
999
1136
|
puts "Function '#{func_name}' not found"
|
|
@@ -1005,11 +1142,12 @@ def defun(func_def) # Define a Ruby function like: `:defun "myls(*args) = Dir.g
|
|
|
1005
1142
|
func_name = $1
|
|
1006
1143
|
func_params = $2
|
|
1007
1144
|
func_body = $3
|
|
1008
|
-
|
|
1145
|
+
|
|
1009
1146
|
begin
|
|
1010
1147
|
eval_code = "def #{func_name}(#{func_params}); #{func_body}; end"
|
|
1011
1148
|
puts " DEBUG: Evaluating: #{eval_code}" if ENV['RSH_DEBUG']
|
|
1012
1149
|
singleton_class.class_eval(eval_code)
|
|
1150
|
+
@defuns[func_name] = func_def # Store for persistence
|
|
1013
1151
|
puts "Function '#{func_name}' defined"
|
|
1014
1152
|
puts " DEBUG: Method created? #{respond_to?(func_name)}" if ENV['RSH_DEBUG']
|
|
1015
1153
|
rescue SyntaxError => e
|
|
@@ -1038,6 +1176,202 @@ def defun? # Show all user-defined functions
|
|
|
1038
1176
|
end
|
|
1039
1177
|
end
|
|
1040
1178
|
end
|
|
1179
|
+
def stats # Show command execution statistics and analytics
|
|
1180
|
+
puts "\n Command Execution Statistics".c(@c_prompt).b
|
|
1181
|
+
puts " " + "="*50
|
|
1182
|
+
|
|
1183
|
+
# Most used commands
|
|
1184
|
+
if @cmd_frequency && !@cmd_frequency.empty?
|
|
1185
|
+
puts "\n Top 10 Most Used Commands:".c(@c_nick)
|
|
1186
|
+
sorted = @cmd_frequency.sort_by { |_, count| -count }.first(10)
|
|
1187
|
+
sorted.each_with_index do |(cmd, count), i|
|
|
1188
|
+
bar = "■" * ([count / 5, 20].min)
|
|
1189
|
+
puts " #{(i+1).to_s.rjust(2)}. #{cmd.ljust(20)} #{count.to_s.rjust(5)}x #{bar.c(@c_path)}"
|
|
1190
|
+
end
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
# Command statistics from @cmd_stats
|
|
1194
|
+
if @cmd_stats && !@cmd_stats.empty?
|
|
1195
|
+
total_time = @cmd_stats.values.map { |s| s[:total_time] || 0 }.sum
|
|
1196
|
+
total_cmds = @cmd_stats.values.map { |s| s[:count] || 0 }.sum
|
|
1197
|
+
|
|
1198
|
+
puts "\n Performance Statistics:".c(@c_nick)
|
|
1199
|
+
puts " Total commands executed: #{total_cmds}"
|
|
1200
|
+
puts " Total execution time: #{'%.2f' % total_time}s"
|
|
1201
|
+
puts " Average time per command: #{'%.2f' % (total_time / total_cmds)}s" if total_cmds > 0
|
|
1202
|
+
|
|
1203
|
+
puts "\n Slowest Commands:".c(@c_nick)
|
|
1204
|
+
slowest = @cmd_stats.sort_by { |_, s| -(s[:avg_time] || 0) }.first(5)
|
|
1205
|
+
slowest.each_with_index do |(cmd, stats), i|
|
|
1206
|
+
puts " #{(i+1).to_s.rjust(2)}. #{cmd.ljust(20)} avg: #{'%.3f' % (stats[:avg_time] || 0)}s"
|
|
1207
|
+
end
|
|
1208
|
+
end
|
|
1209
|
+
|
|
1210
|
+
# History statistics
|
|
1211
|
+
puts "\n History Statistics:".c(@c_nick)
|
|
1212
|
+
puts " Total history entries: #{@history.length}"
|
|
1213
|
+
puts " Unique commands: #{@history.uniq.length}"
|
|
1214
|
+
|
|
1215
|
+
# Success/failure tracking
|
|
1216
|
+
puts "\n Last command exit status: #{@last_exit == 0 ? 'Success'.c(@c_path) : "Failed (#{@last_exit})".c(196)}"
|
|
1217
|
+
puts
|
|
1218
|
+
end
|
|
1219
|
+
def bm(args = nil) # Enhanced bookmark management with tags
|
|
1220
|
+
if args.nil? || args.empty?
|
|
1221
|
+
# List all bookmarks
|
|
1222
|
+
if @bookmarks.empty?
|
|
1223
|
+
puts "No bookmarks defined. Use :bookmark <name> to bookmark current directory"
|
|
1224
|
+
return
|
|
1225
|
+
end
|
|
1226
|
+
puts "\n Bookmarks:".c(@c_prompt).b
|
|
1227
|
+
@bookmarks.each do |name, data|
|
|
1228
|
+
path = data.is_a?(Hash) ? data[:path] : data
|
|
1229
|
+
tags = data.is_a?(Hash) && data[:tags] ? " [#{data[:tags].join(', ')}]" : ""
|
|
1230
|
+
puts " #{name.c(@c_nick)} → #{path}#{tags.c(@c_stamp)}"
|
|
1231
|
+
end
|
|
1232
|
+
puts
|
|
1233
|
+
elsif args =~ /^(\w+)\s+(.+)$/
|
|
1234
|
+
# Set bookmark with optional tags
|
|
1235
|
+
name, rest = $1, $2
|
|
1236
|
+
if rest.include?('#')
|
|
1237
|
+
path_part, tag_part = rest.split('#', 2)
|
|
1238
|
+
path = path_part.strip
|
|
1239
|
+
path = Dir.pwd if path.empty?
|
|
1240
|
+
tags = tag_part.split(',').map(&:strip)
|
|
1241
|
+
@bookmarks[name] = {path: path, tags: tags}
|
|
1242
|
+
else
|
|
1243
|
+
@bookmarks[name] = {path: rest.strip, tags: []}
|
|
1244
|
+
end
|
|
1245
|
+
puts "Bookmark '#{name}' set to #{@bookmarks[name][:path]}"
|
|
1246
|
+
rshrc
|
|
1247
|
+
elsif args =~ /^-(\w+)$/
|
|
1248
|
+
# Delete bookmark
|
|
1249
|
+
name = $1
|
|
1250
|
+
if @bookmarks.delete(name)
|
|
1251
|
+
puts "Bookmark '#{name}' deleted"
|
|
1252
|
+
rshrc
|
|
1253
|
+
else
|
|
1254
|
+
puts "Bookmark '#{name}' not found"
|
|
1255
|
+
end
|
|
1256
|
+
elsif args =~ /^\?(\w*)$/
|
|
1257
|
+
# Search bookmarks by tag
|
|
1258
|
+
tag = $1
|
|
1259
|
+
if tag.empty?
|
|
1260
|
+
puts "Available tags:"
|
|
1261
|
+
all_tags = @bookmarks.values.flat_map { |d| d.is_a?(Hash) ? d[:tags] : [] }.uniq.sort
|
|
1262
|
+
puts " " + all_tags.join(", ")
|
|
1263
|
+
else
|
|
1264
|
+
matches = @bookmarks.select do |_, data|
|
|
1265
|
+
data.is_a?(Hash) && data[:tags] && data[:tags].include?(tag)
|
|
1266
|
+
end
|
|
1267
|
+
if matches.empty?
|
|
1268
|
+
puts "No bookmarks with tag '#{tag}'"
|
|
1269
|
+
else
|
|
1270
|
+
puts "Bookmarks with tag '#{tag}':"
|
|
1271
|
+
matches.each { |name, data| puts " #{name} → #{data[:path]}" }
|
|
1272
|
+
end
|
|
1273
|
+
end
|
|
1274
|
+
else
|
|
1275
|
+
# Bookmark current directory
|
|
1276
|
+
name = args.strip
|
|
1277
|
+
@bookmarks[name] = {path: Dir.pwd, tags: []}
|
|
1278
|
+
puts "Bookmark '#{name}' set to #{Dir.pwd}"
|
|
1279
|
+
rshrc
|
|
1280
|
+
end
|
|
1281
|
+
end
|
|
1282
|
+
def bookmark(args = nil) # Alias for bm
|
|
1283
|
+
bm(args)
|
|
1284
|
+
end
|
|
1285
|
+
def save_session # Save current session state
|
|
1286
|
+
session = {
|
|
1287
|
+
pwd: Dir.pwd,
|
|
1288
|
+
history: @history.first(50),
|
|
1289
|
+
bookmarks: @bookmarks,
|
|
1290
|
+
defuns: @defuns,
|
|
1291
|
+
timestamp: Time.now.to_i
|
|
1292
|
+
}
|
|
1293
|
+
begin
|
|
1294
|
+
require 'json'
|
|
1295
|
+
File.write(@session_file, JSON.pretty_generate(session))
|
|
1296
|
+
puts "Session saved to #{@session_file}"
|
|
1297
|
+
rescue => e
|
|
1298
|
+
puts "Error saving session: #{e.message}"
|
|
1299
|
+
end
|
|
1300
|
+
end
|
|
1301
|
+
def load_session # Restore previous session
|
|
1302
|
+
unless File.exist?(@session_file)
|
|
1303
|
+
puts "No saved session found"
|
|
1304
|
+
return
|
|
1305
|
+
end
|
|
1306
|
+
begin
|
|
1307
|
+
require 'json'
|
|
1308
|
+
session = JSON.parse(File.read(@session_file), symbolize_names: true)
|
|
1309
|
+
|
|
1310
|
+
# Restore state
|
|
1311
|
+
Dir.chdir(session[:pwd]) if session[:pwd] && Dir.exist?(session[:pwd])
|
|
1312
|
+
|
|
1313
|
+
# Merge history (prepend saved history)
|
|
1314
|
+
if session[:history]
|
|
1315
|
+
@history = (session[:history] + @history).uniq.first(@histsize)
|
|
1316
|
+
end
|
|
1317
|
+
|
|
1318
|
+
# Restore bookmarks
|
|
1319
|
+
if session[:bookmarks]
|
|
1320
|
+
session[:bookmarks].each do |name, data|
|
|
1321
|
+
bookmark_data = data.is_a?(Hash) ? data.transform_keys(&:to_sym) : data
|
|
1322
|
+
@bookmarks[name.to_s] = bookmark_data
|
|
1323
|
+
end
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
# Restore defuns
|
|
1327
|
+
if session[:defuns]
|
|
1328
|
+
session[:defuns].each do |name, func_def|
|
|
1329
|
+
next unless func_def.is_a?(String)
|
|
1330
|
+
@defuns[name.to_s] = func_def
|
|
1331
|
+
# Re-evaluate the function
|
|
1332
|
+
if func_def =~ /^(\w+)\s*\(([^)]*)\)\s*=\s*(.+)$/
|
|
1333
|
+
func_name, func_params, func_body = $1, $2, $3
|
|
1334
|
+
eval_code = "def #{func_name}(#{func_params}); #{func_body}; end"
|
|
1335
|
+
singleton_class.class_eval(eval_code) rescue nil
|
|
1336
|
+
end
|
|
1337
|
+
end
|
|
1338
|
+
end
|
|
1339
|
+
|
|
1340
|
+
saved_time = Time.at(session[:timestamp] || 0).strftime("%Y-%m-%d %H:%M:%S")
|
|
1341
|
+
puts "Session restored from #{saved_time}"
|
|
1342
|
+
rshrc
|
|
1343
|
+
rescue => e
|
|
1344
|
+
puts "Error loading session: #{e.message}"
|
|
1345
|
+
end
|
|
1346
|
+
end
|
|
1347
|
+
def validate_command(cmd) # Syntax validation before execution
|
|
1348
|
+
return nil if cmd.nil? || cmd.empty?
|
|
1349
|
+
warnings = []
|
|
1350
|
+
|
|
1351
|
+
# Check for common mistakes
|
|
1352
|
+
warnings << "Unmatched quotes" if cmd.count("'").odd? || cmd.count('"').odd?
|
|
1353
|
+
warnings << "Unmatched parentheses" if cmd.count("(") != cmd.count(")")
|
|
1354
|
+
warnings << "Unmatched brackets" if cmd.count("[") != cmd.count("]")
|
|
1355
|
+
warnings << "Unmatched braces" if cmd.count("{") != cmd.count("}")
|
|
1356
|
+
|
|
1357
|
+
# Check for potentially dangerous patterns
|
|
1358
|
+
warnings << "WARNING: Recursive rm detected" if cmd =~ /rm\s+.*-r.*\//
|
|
1359
|
+
warnings << "WARNING: Force flag without path" if cmd =~ /rm\s+-[rf]+\s*$/
|
|
1360
|
+
warnings << "WARNING: Sudo with redirection" if cmd =~ /sudo.*>/
|
|
1361
|
+
|
|
1362
|
+
# Check for common typos in popular commands
|
|
1363
|
+
if cmd =~ /^(\w+)/
|
|
1364
|
+
first_cmd = $1
|
|
1365
|
+
unless @exe.include?(first_cmd) || @nick.include?(first_cmd) || first_cmd == "cd"
|
|
1366
|
+
suggestions = suggest_command(first_cmd)
|
|
1367
|
+
if suggestions && !suggestions.empty?
|
|
1368
|
+
warnings << "Command '#{first_cmd}' not found. Did you mean: #{suggestions.join(', ')}?"
|
|
1369
|
+
end
|
|
1370
|
+
end
|
|
1371
|
+
end
|
|
1372
|
+
|
|
1373
|
+
warnings.empty? ? nil : warnings
|
|
1374
|
+
end
|
|
1041
1375
|
def execute_conditional(cmd_line)
|
|
1042
1376
|
# Split on && and || while preserving the operators
|
|
1043
1377
|
parts = cmd_line.split(/(\s*&&\s*|\s*\|\|\s*)/)
|
|
@@ -1316,6 +1650,25 @@ def load_rshrc_safe
|
|
|
1316
1650
|
@nick = {} unless @nick.is_a?(Hash)
|
|
1317
1651
|
@gnick = {} unless @gnick.is_a?(Hash)
|
|
1318
1652
|
@cmd_frequency = {} unless @cmd_frequency.is_a?(Hash)
|
|
1653
|
+
@cmd_stats = {} unless @cmd_stats.is_a?(Hash)
|
|
1654
|
+
@bookmarks = {} unless @bookmarks.is_a?(Hash)
|
|
1655
|
+
@defuns = {} unless @defuns.is_a?(Hash)
|
|
1656
|
+
|
|
1657
|
+
# Restore defuns from .rshrc
|
|
1658
|
+
if @defuns && !@defuns.empty?
|
|
1659
|
+
@defuns.each do |name, func_def|
|
|
1660
|
+
next unless func_def.is_a?(String)
|
|
1661
|
+
if func_def =~ /^(\w+)\s*\(([^)]*)\)\s*=\s*(.+)$/
|
|
1662
|
+
func_name, func_params, func_body = $1, $2, $3
|
|
1663
|
+
begin
|
|
1664
|
+
eval_code = "def #{func_name}(#{func_params}); #{func_body}; end"
|
|
1665
|
+
singleton_class.class_eval(eval_code)
|
|
1666
|
+
rescue => e
|
|
1667
|
+
puts "Warning: Could not load defun '#{name}': #{e.message}" if ENV['RSH_DEBUG']
|
|
1668
|
+
end
|
|
1669
|
+
end
|
|
1670
|
+
end
|
|
1671
|
+
end
|
|
1319
1672
|
|
|
1320
1673
|
rescue SyntaxError => e
|
|
1321
1674
|
puts "\n\033[31mERROR: Syntax error in .rshrc:\033[0m"
|
|
@@ -1429,6 +1782,11 @@ def load_defaults
|
|
|
1429
1782
|
@completion_show_descriptions ||= false
|
|
1430
1783
|
@completion_fuzzy ||= true
|
|
1431
1784
|
@cmd_frequency ||= {}
|
|
1785
|
+
@cmd_stats ||= {}
|
|
1786
|
+
@bookmarks ||= {}
|
|
1787
|
+
@defuns ||= {}
|
|
1788
|
+
@switch_cache ||= {}
|
|
1789
|
+
@switch_cache_time ||= {}
|
|
1432
1790
|
puts "Loaded with default configuration."
|
|
1433
1791
|
end
|
|
1434
1792
|
|
|
@@ -1509,7 +1867,7 @@ loop do
|
|
|
1509
1867
|
begin
|
|
1510
1868
|
@user = Etc.getpwuid(Process.euid).name # For use in @prompt
|
|
1511
1869
|
@node = Etc.uname[:nodename] # For use in @prompt
|
|
1512
|
-
h = @history; load_rshrc_safe; @history = h # reload prompt but
|
|
1870
|
+
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
|
|
1513
1871
|
@prompt.gsub!(/#{Dir.home}/, '~') # Simplify path in prompt
|
|
1514
1872
|
system("printf \"\033]0;rsh: #{Dir.pwd}\007\"") # Set Window title to path
|
|
1515
1873
|
@history[0] = "" unless @history[0]
|
|
@@ -1609,11 +1967,22 @@ loop do
|
|
|
1609
1967
|
@cmd = Dir.home if @cmd == "~"
|
|
1610
1968
|
@cmd = @dirs[1] if @cmd == "-"
|
|
1611
1969
|
@cmd = @dirs[@cmd.to_i] if @cmd =~ /^\d$/
|
|
1612
|
-
# Check if it's a directory to change to
|
|
1970
|
+
# Check if it's a directory to change to first
|
|
1613
1971
|
dir = @cmd.strip.sub(/~/, Dir.home)
|
|
1614
1972
|
if Dir.exist?(dir)
|
|
1615
|
-
Dir.chdir(dir)
|
|
1973
|
+
Dir.chdir(dir)
|
|
1616
1974
|
system("git status .") if Dir.exist?(".git")
|
|
1975
|
+
# Then check if it's a bookmark (commands and nicks already handled above)
|
|
1976
|
+
elsif @bookmarks && @bookmarks[@cmd]
|
|
1977
|
+
bookmark_data = @bookmarks[@cmd]
|
|
1978
|
+
bm_dir = bookmark_data.is_a?(Hash) ? bookmark_data[:path] : bookmark_data
|
|
1979
|
+
if Dir.exist?(bm_dir)
|
|
1980
|
+
Dir.chdir(bm_dir)
|
|
1981
|
+
puts "Jumped to bookmark '#{@cmd}' → #{bm_dir}".c(@c_path)
|
|
1982
|
+
system("git status .") if Dir.exist?(".git")
|
|
1983
|
+
else
|
|
1984
|
+
puts "Bookmark '#{@cmd}' points to non-existent directory: #{bm_dir}".c(196)
|
|
1985
|
+
end
|
|
1617
1986
|
else
|
|
1618
1987
|
puts "#{Time.now.strftime("%H:%M:%S")}: #{@cmd}".c(@c_stamp)
|
|
1619
1988
|
if @cmd == "f" # fzf integration (https://github.com/junegunn/fzf)
|
|
@@ -1630,16 +1999,34 @@ loop do
|
|
|
1630
1999
|
Thread.new { system("xdg-open #{@cmd} 2>/dev/null") }
|
|
1631
2000
|
end
|
|
1632
2001
|
end
|
|
1633
|
-
else
|
|
2002
|
+
else
|
|
1634
2003
|
begin
|
|
2004
|
+
# Validate command before execution
|
|
2005
|
+
warnings = validate_command(@cmd)
|
|
2006
|
+
if warnings && !warnings.empty?
|
|
2007
|
+
warnings.each { |w| puts "#{w}".c(196) }
|
|
2008
|
+
# For critical warnings, ask for confirmation
|
|
2009
|
+
if warnings.any? { |w| w.start_with?("WARNING:") }
|
|
2010
|
+
print "Continue anyway? (y/N): "
|
|
2011
|
+
response = $stdin.gets.chomp
|
|
2012
|
+
unless response.downcase == 'y'
|
|
2013
|
+
puts "Command cancelled"
|
|
2014
|
+
next
|
|
2015
|
+
end
|
|
2016
|
+
end
|
|
2017
|
+
end
|
|
2018
|
+
|
|
1635
2019
|
pre_cmd
|
|
1636
2020
|
|
|
1637
2021
|
# Track command frequency for intelligent completion
|
|
1638
|
-
if @cmd && !@cmd.empty?
|
|
1639
|
-
|
|
1640
|
-
@cmd_frequency[cmd_base] = (@cmd_frequency[cmd_base] || 0) + 1
|
|
2022
|
+
cmd_base = @cmd.split.first if @cmd && !@cmd.empty?
|
|
2023
|
+
if cmd_base
|
|
2024
|
+
@cmd_frequency[cmd_base] = (@cmd_frequency[cmd_base] || 0) + 1
|
|
1641
2025
|
end
|
|
1642
2026
|
|
|
2027
|
+
# Start timing
|
|
2028
|
+
start_time = Time.now
|
|
2029
|
+
|
|
1643
2030
|
# Handle background jobs
|
|
1644
2031
|
if @cmd.end_with?(' &')
|
|
1645
2032
|
@cmd = @cmd[0..-3] # Remove the &
|
|
@@ -1653,13 +2040,23 @@ loop do
|
|
|
1653
2040
|
@jobs[@job_id] = {pid: pid, cmd: @cmd, status: :running}
|
|
1654
2041
|
puts "[#{@job_id}] #{pid} #{@cmd}"
|
|
1655
2042
|
else
|
|
1656
|
-
# Better handling of pipes and redirections
|
|
2043
|
+
# Better handling of pipes and redirections
|
|
1657
2044
|
@current_pid = spawn(@cmd)
|
|
1658
2045
|
Process.wait(@current_pid)
|
|
1659
2046
|
@last_exit = $?.exitstatus
|
|
1660
2047
|
@current_pid = nil
|
|
1661
2048
|
puts " Command failed: #{@cmd} (exit #{@last_exit})" unless @last_exit == 0
|
|
1662
2049
|
end
|
|
2050
|
+
|
|
2051
|
+
# Track execution time
|
|
2052
|
+
elapsed = Time.now - start_time
|
|
2053
|
+
if cmd_base && elapsed > 0.01 # Only track if > 10ms
|
|
2054
|
+
@cmd_stats[cmd_base] ||= {count: 0, total_time: 0.0, avg_time: 0.0}
|
|
2055
|
+
@cmd_stats[cmd_base][:count] += 1
|
|
2056
|
+
@cmd_stats[cmd_base][:total_time] += elapsed
|
|
2057
|
+
@cmd_stats[cmd_base][:avg_time] = @cmd_stats[cmd_base][:total_time] / @cmd_stats[cmd_base][:count]
|
|
2058
|
+
end
|
|
2059
|
+
|
|
1663
2060
|
post_cmd
|
|
1664
2061
|
rescue StandardError => err
|
|
1665
2062
|
puts "\nError: #{err}"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby-shell
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Geir Isene
|
|
@@ -12,8 +12,9 @@ date: 2025-10-22 00:00:00.000000000 Z
|
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: 'A shell written in Ruby with extensive tab completions, aliases/nicks,
|
|
14
14
|
history, syntax highlighting, theming, auto-cd, auto-opening files and more. UPDATE
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
v3.0.0: MAJOR RELEASE - Persistent defuns, smart command suggestions with typo detection,
|
|
16
|
+
command analytics with :stats, switch caching, enhanced bookmarks with tags, session
|
|
17
|
+
save/restore, syntax validation, option value completion, and performance tracking.'
|
|
17
18
|
email: g@isene.com
|
|
18
19
|
executables:
|
|
19
20
|
- rsh
|