ruby-shell 3.2.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 (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +138 -17
  3. data/bin/rsh +265 -71
  4. metadata +6 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8d8b425248a8544d33dab32c9e5322522389665593a9b1c9dfedf2f6be72996
4
- data.tar.gz: 44dcba6f5eca65d0667bc380677f26b7d52b07e8147f2d33bd18d28f676bfa99
3
+ metadata.gz: cd3cc49ebe2106b1077e32c07694a7d2cd4a302aba740baa1fd744ab4f097d7e
4
+ data.tar.gz: 86a2a648f07605d15850908faf20d92b36a07a501c94d5c88262d9b9aac07b24
5
5
  SHA512:
6
- metadata.gz: e8af85c2f352a7144ec51ed76c19144e44918c78b0d459d2aef58c0e33521cb08033a54bd2b7bd6d3da4402bc879c806c78353ba92c7fe30822aec9983d58480
7
- data.tar.gz: f85f97778ffc6efae4758fb67902361c40f1f3ad3e0b4857bf166538e0066d2074481ceef7dcf5989f6f2db90f0b50ef957f035cfba1b5b3320a700dd42f4ec5
6
+ metadata.gz: c9dc294728b34590500a3700f2b7d1c0ca4a42bbce9e4868403f6056cf1fda458ac30a43f6b83013c000ed48d51abe177e0ae9ab2855ee77a85bffc134267ae1
7
+ data.tar.gz: a4fbc3bcd9b71a81e89bb91bb4eba80bec7e75ea417cd8170a3150e34de9a7d7a6b63827b00e25a9195dc0198327fb7f25d7284dc7740d4a8b35abdb5e850937
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 v3.2.0 - Plugin System & Productivity ⭐⭐⭐
36
+ ## NEW in v3.3.0 - Quote-less Syntax, Parametrized Nicks & More ⭐⭐⭐
37
+ * **No More Quotes**: Simplified syntax - `:nick la = ls -la` instead of `:nick "la = ls -la"`
38
+ * **Parametrized Nicks**: `:nick gp = git push origin {{branch}}` then use `gp branch=main`
39
+ * **Ctrl-G Multi-line Edit**: Press Ctrl-G to edit command in $EDITOR for complex scripts
40
+ * **Custom Validation Rules**: `:validate rm -rf / = block` prevents dangerous commands
41
+ * **Shell Script Support**: for/while/if loops work with full bash syntax
42
+ * **Cleaner Commands**: `:config auto_correct on`, `:bm work /tmp #dev`, `:theme dracula`
43
+ * **Simplified Architecture**: Removed :template (merged into :nick for simplicity)
44
+ * **Backward Compatible**: Old quote syntax still works for existing .rshrc files
45
+ * **Better UX**: Less typing, more powerful, feels more natural
46
+
47
+ ## v3.2.0 - Plugin System & Productivity ⭐⭐⭐
37
48
  * **Plugin Architecture**: Extensible plugin system with lifecycle hooks and extension points
38
49
  * **Lifecycle Hooks**: on_startup, on_command_before, on_command_after, on_prompt
39
50
  * **Extension Points**: add_completions (TAB completion), add_commands (custom commands)
@@ -109,22 +120,23 @@ Special functions/integrations:
109
120
  * Use `:` followed by a Ruby expression to access the whole world of Ruby
110
121
 
111
122
  Special commands:
112
- * `:nick 'll = ls -l'` to make a command alias (ll) point to a command (ls -l)
113
- * `:gnick 'h = /home/me'` to make a general alias (h) point to something (/home/me)
114
- * `:nick` lists all command nicks, `:gnick` lists general nicks (NEW in v3.0)
115
- * `:nick '-name'` delete a command nick, `:gnick '-name'` delete a general nick (NEW in v3.0)
123
+ * `:nick ll = ls -l` to make a command alias (ll) point to a command (ls -l)
124
+ * `:gnick h = /home/me` to make a general alias (h) point to something (/home/me)
125
+ * `:nick` lists all command nicks, `:gnick` lists general nicks
126
+ * `:nick -name` delete a command nick, `:gnick -name` delete a general nick
116
127
  * `:history` will list the command history, while `:rmhistory` will delete the history
117
128
  * `:jobs` will list background jobs, `:fg [job_id]` brings jobs to foreground, `:bg [job_id]` resumes stopped jobs
118
- * `:defun 'func(args) = code'` defines Ruby functions callable as shell commands (now persistent!)
119
- * `:defun?` lists all user-defined functions, `:defun '-func'` removes functions
120
- * `:stats` shows command execution statistics and analytics (NEW in v3.0)
121
- * `:bm "name"` or `:bookmark "name"` bookmark current directory, `:bm "name path #tags"` with tags (NEW in v3.0)
122
- * `:bm` lists all bookmarks, just type bookmark name to jump (e.g., `work`) (NEW in v3.0)
123
- * `:bm "-name"` delete bookmark, `:bm "?tag"` search by tag (NEW in v3.0)
124
- * `:save_session "name"` saves named session, `:load_session "name"` loads session (NEW in v3.0)
125
- * `:list_sessions` shows all saved sessions, `:rmsession "name"` or `:rmsession "*"` deletes (NEW in v3.1)
126
- * `:theme "name"` applies color scheme, `:config` manages settings, `:env` manages environment (NEW in v3.1)
127
- * `:plugins` lists plugins, `:plugins "disable", "name"` disables, `:plugins "reload"` reloads (NEW in v3.2)
129
+ * `:defun func(args) = code` defines Ruby functions callable as shell commands (persistent!)
130
+ * `:defun?` lists all user-defined functions, `:defun -func` removes functions
131
+ * `:stats` shows command execution statistics, `:stats --graph` for visual charts, `:stats --clear` to reset
132
+ * `:bm name` or `:bookmark name` bookmark current directory, `:bm name path #tags` with tags
133
+ * `:bm` lists all bookmarks, just type bookmark name to jump (e.g., `work`)
134
+ * `:bm -name` delete bookmark, `:bm ?tag` search by tag, `:bm --stats` show statistics
135
+ * `:save_session name` saves named session, `:load_session name` loads session
136
+ * `:list_sessions` shows all saved sessions, `:rmsession name` or `:rmsession *` deletes
137
+ * `:theme name` applies color scheme, `:config` manages settings, `:env` manages environment
138
+ * `:plugins` lists plugins, `:plugins disable name` disables, `:plugins reload` reloads
139
+ * `:calc expression` inline calculator with Ruby Math library
128
140
  * `:info` shows introduction and feature overview
129
141
  * `:version` Shows the rsh version number and the last published gem file version
130
142
  * `:help` will display a compact command reference in two columns
@@ -151,10 +163,82 @@ Add to your `.rshrc`:
151
163
  ```
152
164
 
153
165
  ## Moving around
154
- While you `cd` around to different directories, you can see the last 10 directories visited via the command `:dirs` or the convenient shortcut `#`. Entering the number in the list (like `6` and ENTER) will jump you to that directory. Entering `-` will jump you back to the previous dir (equivalent of `1`. Entering `~` will get you to your home dir. If you want to bookmark a special directory, you can do that via a general nick like this: `:gnick "x = /path/to/a/dir/"` - this would bookmark the directory to the single letter `x`.
166
+ While you `cd` around to different directories, you can see the last 10 directories visited via the command `:dirs` or the convenient shortcut `#`. Entering the number in the list (like `6` and ENTER) will jump you to that directory. Entering `-` will jump you back to the previous dir (equivalent of `1`. Entering `~` will get you to your home dir. If you want to bookmark a special directory, you can do that via a general nick like this: `:gnick x = /path/to/a/dir/` - this would bookmark the directory to the single letter `x`.
155
167
 
156
168
  ## Nicks
157
- Add command nicks (aliases) with `:nick "some_nick = some_command"`, e.g. `:nick "ls = ls --color"`. Add general nicks that will substitute anything on a command line (not just commands) like this `:gnick "some_gnick = some_command"`, e.g. `:gnick "x = /home/user/somewhere"`. List nicks with `:nick`, list gnicks with `:gnick`. Remove a nick with `:nick "-some_command"`, e.g. `:nick "-ls"` to remove an `ls` nick. Same for gnicks.
169
+
170
+ Nicks are powerful aliases that can be simple command shortcuts or complex parametrized templates.
171
+
172
+ ### Simple Nicks
173
+ ```bash
174
+ :nick ls = ls --color # Simple alias
175
+ :nick la = ls -la # Another shortcut
176
+ :nick # List all nicks
177
+ :nick -la # Delete a nick
178
+ ```
179
+
180
+ ### Parametrized Nicks (NEW in v3.3!)
181
+ Create templates with `{{placeholder}}` parameters:
182
+
183
+ ```bash
184
+ # Git shortcuts with branch parameter
185
+ :nick gp = git push origin {{branch}}
186
+ gp branch=main # Executes: git push origin main
187
+ gp branch=develop # Executes: git push origin develop
188
+
189
+ # Deployment with multiple parameters
190
+ :nick deploy = ssh {{user}}@{{host}} 'cd {{path}} && git pull'
191
+ deploy user=admin host=prod path=/var/www
192
+ # Executes: ssh admin@prod 'cd /var/www && git pull'
193
+
194
+ # Backup with source and destination
195
+ :nick backup = rsync -av {{src}} {{dest}}
196
+ backup src=/data dest=/backup
197
+ # Executes: rsync -av /data /backup
198
+ ```
199
+
200
+ **How it works:**
201
+ - Define nick with `{{param}}` placeholders
202
+ - Use with `key=value` syntax
203
+ - Parameters auto-expand and get stripped from final command
204
+ - Works with any number of parameters
205
+
206
+ ### General Nicks (gnicks)
207
+ Substitute anywhere on command line (not just commands):
208
+ ```bash
209
+ :gnick h = /home/user # Directory shortcut
210
+ :gnick # List all gnicks
211
+ :gnick -h # Delete a gnick
212
+ ```
213
+
214
+ ## Multi-line Command Editing (v3.3.0+)
215
+
216
+ Press **Ctrl-G** to edit the current command in your $EDITOR:
217
+
218
+ ```bash
219
+ # Start typing a complex command
220
+ for i in {1..10}
221
+
222
+ # Press Ctrl-G
223
+ # Your editor opens with the command
224
+ # Add more lines:
225
+ for i in {1..10}
226
+ echo "Processing: $i"
227
+ sleep 1
228
+ done
229
+
230
+ # Save and quit
231
+ # Command appears on command line (converted to single-line with ;)
232
+ # Press ENTER to execute
233
+ ```
234
+
235
+ **Perfect for:**
236
+ - Complex shell scripts
237
+ - Long commands with many options
238
+ - Multi-line constructs (for, while, if)
239
+ - Commands you want to review/edit carefully
240
+
241
+ ---
158
242
 
159
243
  ## Tab completion
160
244
  You can tab complete almost anything. Hitting `TAB` will try to complete in this priority: nicks, gnicks, commands, dirs/files. Special completions:
@@ -227,6 +311,43 @@ Ruby functions have access to:
227
311
  - JSON/XML parsing
228
312
  - And everything else Ruby can do!
229
313
 
314
+ ## Custom Validation Rules (v3.3.0+)
315
+
316
+ Create safety rules to block, confirm, warn, or log specific command patterns:
317
+
318
+ ```bash
319
+ # Block dangerous commands completely
320
+ :validate rm -rf / = block
321
+
322
+ # Require confirmation for risky operations
323
+ :validate git push --force = confirm
324
+ :validate DROP TABLE = confirm
325
+
326
+ # Show warnings but allow execution
327
+ :validate sudo = warn
328
+ :validate chmod 777 = warn
329
+
330
+ # Log specific commands for audit trail
331
+ :validate npm install = log
332
+ # Logs to ~/.rsh_validation.log
333
+
334
+ # List all rules
335
+ :validate
336
+
337
+ # Delete rule by index
338
+ :validate -1
339
+ ```
340
+
341
+ **Actions:**
342
+ - `block` - Prevent command execution completely
343
+ - `confirm` - Ask for confirmation (y/N)
344
+ - `warn` - Show warning but allow
345
+ - `log` - Silently log to ~/.rsh_validation.log
346
+
347
+ **Pattern matching:** Uses regex, so you can match complex patterns.
348
+
349
+ ---
350
+
230
351
  ## Plugin System (v3.2.0+)
231
352
 
232
353
  rsh supports a powerful plugin system for extending functionality. Plugins are Ruby classes placed in `~/.rsh/plugins/` that can:
data/bin/rsh CHANGED
@@ -8,7 +8,7 @@
8
8
  # Web_site: http://isene.com/
9
9
  # Github: https://github.com/isene/rsh
10
10
  # License: Public domain
11
- @version = "3.2.0" # Plugin system: Extensible architecture with lifecycle hooks, completions, commands, and plugin management
11
+ @version = "3.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)
@@ -150,6 +150,7 @@ begin # Initialization
150
150
  @plugins = [] # Loaded plugin instances
151
151
  @plugin_disabled = [] # List of disabled plugin names
152
152
  @plugin_commands = {} # Commands added by plugins
153
+ @validation_rules = [] # Custom validation rules
153
154
  # Built-in rsh commands are called with : prefix, so no need for separate tracking
154
155
  Dir.mkdir(Dir.home + '/.rsh') unless Dir.exist?(Dir.home + '/.rsh')
155
156
  Dir.mkdir(@session_dir) unless Dir.exist?(@session_dir)
@@ -161,7 +162,7 @@ end
161
162
  # HELP TEXT
162
163
  @info = <<~INFO
163
164
 
164
- Hello #{@user}, welcome to rsh v3.2 - the Ruby SHell.
165
+ Hello #{@user}, welcome to rsh v3.3 - the Ruby SHell.
165
166
 
166
167
  rsh does not attempt to compete with the grand old shells like bash and zsh.
167
168
  It serves the specific needs and wants of its author. If you like it, then feel free
@@ -187,17 +188,22 @@ end
187
188
  * Syntax validation - Pre-execution warnings for dangerous or malformed commands
188
189
  * Unified command syntax - :nick, :gnick, :bm all work the same way (list/create/delete)
189
190
 
190
- NEW in v3.2:
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:
191
201
  * Plugin system - Extensible architecture for custom commands, completions, and hooks
192
- * Lifecycle hooks - on_startup, on_command_before, on_command_after, on_prompt
193
- * Plugin management - :plugins list/reload/enable/disable/info
194
- * Example plugins - git_prompt, command_logger, kubectl_completion included
195
- * Auto-correct typos - :config "auto_correct" "on" automatically fixes gti → closest match
196
- * Command timing alerts - :config "slow_command_threshold" "5" warns on slow commands
197
- * Inline calculator - :calc 2 + 2, :calc "Math.sqrt(16)", full Ruby Math library
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
198
205
  * Enhanced history - !!, !-2, !5:7 for repeat last, nth-to-last, and chaining
199
206
  * Stats visualization - :stats --graph for colorful ASCII bar charts
200
- * See PLUGIN_GUIDE.md for complete plugin development documentation
201
207
 
202
208
  v3.1 Features:
203
209
  * Multiple named sessions - :save_session "project" and :load_session "project"
@@ -355,8 +361,23 @@ def getstr # A custom Readline-like function
355
361
  chr = getchr
356
362
  puts "DEBUG: Got char: '#{chr}' (length: #{chr.length})" if ENV['RSH_DEBUG']
357
363
  case chr
358
- when 'C-G', 'C-C'
359
- @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] = ""
360
381
  @pos = 0
361
382
  when 'C-E' # Ctrl-C exits gracefully but without updating .rshrc
362
383
  print "\n"
@@ -620,7 +641,7 @@ def tab(type)
620
641
  :nick :gnick :bm :bookmark :stats :defun :defun?
621
642
  :history :rmhistory :jobs :fg :bg
622
643
  :save_session :load_session :list_sessions :delete_session :rmsession
623
- :config :env :theme :plugins :calc
644
+ :config :env :theme :plugins :calc :validate
624
645
  :info :version :help
625
646
  ]
626
647
  search_str = @tabstr[1..-1] || "" # Remove leading :
@@ -1058,6 +1079,8 @@ def rshrc # Write updates to .rshrc
1058
1079
  conf += "@slow_command_threshold = #{@slow_command_threshold}\n" if @slow_command_threshold && @slow_command_threshold > 0
1059
1080
  conf.sub!(/^@plugin_disabled.*(\n|$)/, "")
1060
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?
1061
1084
  # Only write @cmd_completions if user has customized it
1062
1085
  unless conf =~ /^@cmd_completions\s*=/
1063
1086
  # Don't write default completions to avoid cluttering .rshrc
@@ -1072,7 +1095,7 @@ def rshrc # Write updates to .rshrc
1072
1095
  puts "Warning: Error saving history: #{e.message}"
1073
1096
  end
1074
1097
  File.write(Dir.home+'/.rshrc', conf)
1075
- puts "\n.rshrc updated"
1098
+ puts ".rshrc updated"
1076
1099
  end
1077
1100
 
1078
1101
  # RSH FUNCTIONS
@@ -1095,23 +1118,24 @@ def help
1095
1118
  left_col << "UP/DOWN Navigate history"
1096
1119
  left_col << "TAB Tab complete"
1097
1120
  left_col << "Shift-TAB Search history"
1121
+ left_col << "Ctrl-G Edit in $EDITOR"
1098
1122
  left_col << "Ctrl-Y Copy to clipboard"
1099
1123
  left_col << "Ctrl-D Exit + save .rshrc"
1100
1124
  left_col << "Ctrl-E Exit without save"
1101
1125
  left_col << "Ctrl-L Clear screen"
1102
1126
  left_col << "Ctrl-Z Suspend job"
1103
- left_col << "Ctrl-C/G Clear line"
1127
+ left_col << "Ctrl-C Clear line"
1104
1128
  left_col << "Ctrl-K Delete history item"
1105
1129
  left_col << "Ctrl-U Clear line"
1106
1130
  left_col << "Ctrl-W Delete previous word"
1107
1131
  left_col << ""
1108
1132
  left_col << "SPECIAL COMMANDS:".c(@c_prompt).b
1109
- left_col << ":nick 'll = ls -l' Command alias"
1110
- left_col << ":gnick 'h = /home' General alias"
1133
+ left_col << ":nick ll = ls -l Command alias"
1134
+ left_col << ":gnick h = /home General alias"
1111
1135
  left_col << ":nick List nicks"
1112
1136
  left_col << ":gnick List gnicks"
1113
- left_col << ":nick '-name' Delete nick"
1114
- left_col << ":gnick '-name' Delete gnick"
1137
+ left_col << ":nick -name Delete nick"
1138
+ left_col << ":gnick -name Delete gnick"
1115
1139
  left_col << ":history Show history"
1116
1140
  left_col << ":rmhistory Clear history"
1117
1141
  left_col << ":info About rsh"
@@ -1120,9 +1144,9 @@ def help
1120
1144
 
1121
1145
  # Right column content
1122
1146
  right_col << "RUBY FUNCTIONS:".c(@c_prompt).b
1123
- right_col << ":defun 'f(x) = x*2' Define function"
1147
+ right_col << ":defun f(x) = x*2 Define function"
1124
1148
  right_col << ":defun? List functions"
1125
- right_col << ":defun '-f' Remove function"
1149
+ right_col << ":defun -f Remove function"
1126
1150
  right_col << "Call as: f 5 (returns 10)"
1127
1151
  right_col << ""
1128
1152
  right_col << "JOB CONTROL:".c(@c_prompt).b
@@ -1131,28 +1155,30 @@ def help
1131
1155
  right_col << ":fg [id] Foreground job"
1132
1156
  right_col << ":bg [id] Resume in bg"
1133
1157
  right_col << ""
1134
- right_col << "v3.2 NEW FEATURES:".c(@c_prompt).b
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
1135
1166
  right_col << ":plugins [cmd] Plugin system"
1136
- right_col << ":stats --graph Visual bar charts"
1137
- right_col << ":calc 2 + 2 Ruby calculator"
1138
- right_col << "!!, !-2, !5:7 History repeat/chain"
1139
- right_col << ":config auto_correct Auto-fix (w/confirm)"
1140
- right_col << ":config slow_cmd... Slow alerts"
1167
+ right_col << ":stats --graph Visual charts"
1168
+ right_col << ":calc 2 + 2 Calculator"
1169
+ right_col << "!!, !-2, !5:7 History chain"
1141
1170
  right_col << ""
1142
1171
  right_col << "PLUGIN SYSTEM:".c(@c_prompt).b
1143
1172
  right_col << ":plugins List all"
1144
1173
  right_col << ":plugins reload Reload"
1145
1174
  right_col << ":plugins disable nm Disable"
1146
- right_col << ":plugins info nm Details"
1147
1175
  right_col << ""
1148
- right_col << "v3.0/3.1 FEATURES:".c(@c_prompt).b
1149
- right_col << ":stats --clear Clear stats"
1150
- right_col << ":stats --csv|--json Export stats"
1151
- right_col << ":bm \"name\" Bookmarks"
1152
- right_col << ":save_session [nm] Sessions"
1153
- right_col << ":theme [name] Themes"
1154
- right_col << ":config [set val] Config"
1155
- right_col << ":env [VAR] Env vars"
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"
1156
1182
  right_col << ""
1157
1183
  right_col << "INTEGRATIONS:".c(@c_prompt).b
1158
1184
  right_col << "r Launch rtfm"
@@ -1526,11 +1552,8 @@ def export_stats_csv(filename = 'rsh_stats.csv') # Export stats to CSV
1526
1552
  puts "Error exporting stats: #{e.message}"
1527
1553
  end
1528
1554
  end
1529
- def bm(*args) # Enhanced bookmark management with tags
1530
- # Handle variadic arguments
1531
- arg_str = args.join(' ')
1532
-
1533
- if args.empty?
1555
+ def bm(arg_str = nil) # Enhanced bookmark management with tags
1556
+ if arg_str.nil? || arg_str.empty?
1534
1557
  # List all bookmarks
1535
1558
  if @bookmarks.empty?
1536
1559
  puts "No bookmarks defined. Use :bm \"name\" to bookmark current directory"
@@ -1543,15 +1566,16 @@ def bm(*args) # Enhanced bookmark management with tags
1543
1566
  puts " #{name.c(@c_nick)} → #{path}#{tags.c(@c_stamp)}"
1544
1567
  end
1545
1568
  puts
1546
- elsif args[0] == '--export'
1569
+ elsif arg_str =~ /^--export\s*(.*)/
1547
1570
  # Export bookmarks to file
1548
- filename = args[1] || 'bookmarks.json'
1571
+ filename = $1.strip
1572
+ filename = 'bookmarks.json' if filename.empty?
1549
1573
  export_bookmarks(filename)
1550
- elsif args[0] == '--import'
1574
+ elsif arg_str =~ /^--import\s+(.*)/
1551
1575
  # Import bookmarks from file
1552
- filename = args[1]
1576
+ filename = $1.strip
1553
1577
  import_bookmarks(filename) if filename
1554
- elsif args[0] == '--stats'
1578
+ elsif arg_str == '--stats'
1555
1579
  # Show bookmark statistics
1556
1580
  bookmark_stats
1557
1581
  elsif arg_str =~ /^(\w+)\s+(.+)$/
@@ -1603,8 +1627,8 @@ def bm(*args) # Enhanced bookmark management with tags
1603
1627
  rshrc
1604
1628
  end
1605
1629
  end
1606
- def bookmark(*args) # Alias for bm
1607
- bm(*args)
1630
+ def bookmark(arg_str = nil) # Alias for bm
1631
+ bm(arg_str)
1608
1632
  end
1609
1633
  def export_bookmarks(filename = 'bookmarks.json') # Export bookmarks to JSON
1610
1634
  begin
@@ -1669,6 +1693,7 @@ def bookmark_stats # Show bookmark usage statistics
1669
1693
  end
1670
1694
  def save_session(*args) # Save current session state
1671
1695
  session_name = args[0] || 'default'
1696
+ silent = args[1] == :silent # Optional silent flag
1672
1697
  session_path = @session_dir + "/#{session_name}.json"
1673
1698
 
1674
1699
  session = {
@@ -1682,9 +1707,9 @@ def save_session(*args) # Save current session state
1682
1707
  begin
1683
1708
  require 'json'
1684
1709
  File.write(session_path, JSON.pretty_generate(session))
1685
- puts "Session '#{session_name}' saved to #{session_path}"
1710
+ puts "Session '#{session_name}' saved to #{session_path}" unless silent
1686
1711
  rescue => e
1687
- puts "Error saving session: #{e.message}"
1712
+ puts "Error saving session: #{e.message}" unless silent
1688
1713
  end
1689
1714
  end
1690
1715
  def load_session(*args) # Restore previous session
@@ -2002,13 +2027,17 @@ def validate_command(cmd) # Syntax validation before execution
2002
2027
  return nil if cmd.nil? || cmd.empty?
2003
2028
  warnings = []
2004
2029
 
2030
+ # Apply custom validation rules first
2031
+ custom_warnings = apply_validation_rules(cmd)
2032
+ warnings.concat(custom_warnings) if custom_warnings.any?
2033
+
2005
2034
  # Check for common mistakes
2006
2035
  warnings << "Unmatched quotes" if cmd.count("'").odd? || cmd.count('"').odd?
2007
2036
  warnings << "Unmatched parentheses" if cmd.count("(") != cmd.count(")")
2008
2037
  warnings << "Unmatched brackets" if cmd.count("[") != cmd.count("]")
2009
2038
  warnings << "Unmatched braces" if cmd.count("{") != cmd.count("}")
2010
2039
 
2011
- # Check for potentially dangerous patterns
2040
+ # Check for potentially dangerous patterns (unless user has custom rules)
2012
2041
  warnings << "WARNING: Recursive rm detected" if cmd =~ /rm\s+.*-r.*\//
2013
2042
  warnings << "WARNING: Force flag without path" if cmd =~ /rm\s+-[rf]+\s*$/
2014
2043
  warnings << "WARNING: Sudo with redirection" if cmd =~ /sudo.*>/
@@ -2060,6 +2089,67 @@ def calc(*args) # Inline calculator using Ruby's Math library
2060
2089
  puts "Error evaluating expression: #{e.message}"
2061
2090
  end
2062
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
2063
2153
  def apply_auto_correct(cmd) # Apply auto-correction to command
2064
2154
  return cmd unless @auto_correct
2065
2155
  return cmd if cmd =~ /^:/ # Don't auto-correct colon commands
@@ -2067,6 +2157,11 @@ def apply_auto_correct(cmd) # Apply auto-correction to command
2067
2157
 
2068
2158
  first_cmd = $1
2069
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
+
2070
2165
  # Don't auto-correct if command exists
2071
2166
  return cmd if @exe.include?(first_cmd)
2072
2167
  return cmd if @nick.include?(first_cmd)
@@ -2370,6 +2465,7 @@ def load_rshrc_safe
2370
2465
  @plugin_disabled = [] unless @plugin_disabled.is_a?(Array)
2371
2466
  @plugins = [] unless @plugins.is_a?(Array)
2372
2467
  @plugin_commands = {} unless @plugin_commands.is_a?(Hash)
2468
+ @validation_rules = [] unless @validation_rules.is_a?(Array)
2373
2469
 
2374
2470
  # Restore defuns from .rshrc
2375
2471
  if @defuns && !@defuns.empty?
@@ -2511,6 +2607,7 @@ def load_defaults
2511
2607
  @plugin_disabled ||= []
2512
2608
  @plugins ||= []
2513
2609
  @plugin_commands ||= {}
2610
+ @validation_rules ||= []
2514
2611
  puts "Loaded with default configuration."
2515
2612
  end
2516
2613
 
@@ -2609,7 +2706,7 @@ loop do
2609
2706
  if @session_autosave && @session_autosave > 0
2610
2707
  current_time = Time.now.to_i
2611
2708
  if (current_time - @session_last_save) >= @session_autosave
2612
- save_session('autosave')
2709
+ save_session('autosave', :silent)
2613
2710
  @session_last_save = current_time
2614
2711
  end
2615
2712
  end
@@ -2687,8 +2784,46 @@ loop do
2687
2784
  end
2688
2785
  if @cmd.match(/^\s*:/) # Ruby commands are prefixed with ":"
2689
2786
  begin
2690
- eval(@cmd[1..-1])
2691
- #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
2692
2827
  rescue Exception => err
2693
2828
  puts "\n#{err}"
2694
2829
  end
@@ -2723,20 +2858,48 @@ loop do
2723
2858
  execute_conditional(@cmd)
2724
2859
  next
2725
2860
  end
2726
- # Expand brace expansion {a,b,c}
2727
- @cmd = expand_braces(@cmd)
2728
- # Expand command substitution $(command) and backticks
2729
- @cmd = @cmd.gsub(/\$\(([^)]+)\)/) { `#{$1}`.chomp }
2730
- @cmd = @cmd.gsub(/`([^`]+)`/) { `#{$1}`.chomp }
2731
- # Expand environment variables and exit status
2732
- @cmd = @cmd.gsub(/\$\?/) { @last_exit.to_s }
2733
- @cmd = @cmd.gsub(/\$(\w+)|\$\{(\w+)\}/) { ENV[$1 || $2] || '' }
2734
- # Expand tilde
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
2735
2875
  @cmd = @cmd.gsub(/~/, Dir.home)
2736
- ca = @nick.transform_keys {|k| /((^\K\s*\K)|(\|\K\s*\K))\b(?<!-)#{Regexp.escape k}\b/}
2737
- @cmd = @cmd.gsub(Regexp.union(ca.keys), @nick)
2738
- ga = @gnick.transform_keys {|k| /\b(?<!-)#{Regexp.escape k}\b/}
2739
- @cmd = @cmd.gsub(Regexp.union(ga.keys), @gnick)
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
2740
2903
  @cmd = "~" if @cmd == "cd"
2741
2904
  @cmd.sub!(/^cd (\S*).*/, '\1')
2742
2905
  @cmd = Dir.home if @cmd == "~"
@@ -2785,8 +2948,16 @@ loop do
2785
2948
  # Validate command after auto-correction
2786
2949
  warnings = validate_command(@cmd)
2787
2950
  if warnings && !warnings.empty?
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
+
2788
2958
  # Show non-auto-correct warnings
2789
- warnings.reject { |w| w.start_with?("AUTO-CORRECTING:") }.each { |w| puts "#{w}".c(196) }
2959
+ warnings.reject { |w| w.start_with?("AUTO-CORRECTING:") || w.start_with?("CONFIRM") }.each { |w| puts "#{w}".c(196) }
2960
+
2790
2961
  # Show auto-correct and ask for confirmation
2791
2962
  auto_correct_warnings = warnings.select { |w| w.start_with?("AUTO-CORRECTING:") }
2792
2963
  if auto_correct_warnings.any?
@@ -2798,7 +2969,19 @@ loop do
2798
2969
  next
2799
2970
  end
2800
2971
  end
2801
- # For critical warnings, ask for confirmation
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
2802
2985
  if warnings.any? { |w| w.start_with?("WARNING:") }
2803
2986
  print "Continue anyway? (y/N): "
2804
2987
  response = $stdin.gets.chomp
@@ -2829,6 +3012,9 @@ loop do
2829
3012
  # Start timing
2830
3013
  start_time = Time.now
2831
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
+
2832
3018
  # Handle background jobs
2833
3019
  if @cmd.end_with?(' &')
2834
3020
  @cmd = @cmd[0..-3] # Remove the &
@@ -2837,13 +3023,21 @@ loop do
2837
3023
  if @cmd.include?('|') || @cmd.include?('>') || @cmd.include?('<')
2838
3024
  pid = spawn(@cmd, pgroup: true)
2839
3025
  else
2840
- pid = spawn(@cmd)
3026
+ if needs_bash
3027
+ pid = spawn("bash", "-c", @cmd)
3028
+ else
3029
+ pid = spawn(@cmd)
3030
+ end
2841
3031
  end
2842
3032
  @jobs[@job_id] = {pid: pid, cmd: @cmd, status: :running}
2843
3033
  puts "[#{@job_id}] #{pid} #{@cmd}"
2844
3034
  else
2845
3035
  # Better handling of pipes and redirections
2846
- @current_pid = spawn(@cmd)
3036
+ if needs_bash
3037
+ @current_pid = spawn("bash", "-c", @cmd)
3038
+ else
3039
+ @current_pid = spawn(@cmd)
3040
+ end
2847
3041
  Process.wait(@current_pid)
2848
3042
  @last_exit = $?.exitstatus
2849
3043
  @current_pid = nil
metadata CHANGED
@@ -1,20 +1,21 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-shell
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.0
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-10-22 00:00:00.000000000 Z
11
+ date: 2025-10-23 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'A shell written in Ruby with extensive tab completions, aliases/nicks,
14
14
  history, syntax highlighting, theming, auto-cd, auto-opening files and more. UPDATE
15
- v3.2.0: PLUGIN SYSTEM - Extensible architecture with lifecycle hooks (on_startup,
16
- on_command_before/after, on_prompt), extension points (add_completions, add_commands),
17
- plugin management, and 3 example plugins included. Plus colon command theming!'
15
+ v3.3.0: QUOTE-LESS SYNTAX - No quotes needed! Parametrized nicks with {{placeholders}}
16
+ - :nick gp=git push {{branch}}, use: gp branch=main. Ctrl-G multi-line editing in
17
+ $EDITOR. Custom validation rules. Full bash shell script support. Simpler, cleaner,
18
+ more powerful!'
18
19
  email: g@isene.com
19
20
  executables:
20
21
  - rsh