ruby-shell 3.6.15 → 3.6.16

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 (3) hide show
  1. checksums.yaml +4 -4
  2. data/bin/rsh +34 -20
  3. metadata +4 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b48490df2e369dfa33c4d92ce4c713573d7b1e54ff565a53b62a217db070ca6
4
- data.tar.gz: d79027f5999c4765f99b900d7d4bc9edd7dfd90f195799815e6715577ed2bcd8
3
+ metadata.gz: 3e133cdb364d88a5e82942a136ffd6a284105553a5e367bc5415e4623b1715ef
4
+ data.tar.gz: e712cf81e8ffccebadfb579272892d54617da2e59e53a95fab02b5e6766a5a9f
5
5
  SHA512:
6
- metadata.gz: 81b35c1a4f9b1e30b29381ad3c563b4da9efdf5d4fee1157409c87e7fb256f096fa855f352b4cb7faa1d0d9bd60c9755326f818991a52b8fe2f2eb3365c16409
7
- data.tar.gz: ee3bffbbd08096c6651dc8abf6984acb17f1d373d5d2558ea4354c56e1e8d1f6659205be9b339deb1b8e502d875975ade197153e48a787266a0082c074ded8e7
6
+ metadata.gz: f3d81facd0b334b13a4112a17c02aa43e0364844d1405d45e3d989ef2122d24ea0dad9ebf80a86bfa1840fd0d1f1e56c2cddf1fe0c01590aa9feb8df764bc88f
7
+ data.tar.gz: 7cf717175c6319230ab76586a02d065196e41a51b0e429a26d16bd5dd37ef3c037262a89f59697b4c31b7ad149664e32d8c19cbc9ff6d5581fa6d378463ed421
data/bin/rsh CHANGED
@@ -162,6 +162,7 @@ begin # Initialization
162
162
  @history_dedup = 'smart' # History dedup mode: 'off', 'full', 'smart'
163
163
  @auto_correct = false # Auto-correct typos (default: off)
164
164
  @slow_command_threshold = 0 # Threshold for slow command alerts (0 = disabled)
165
+ @config_dirty = [] # Track which config vars were changed via :config
165
166
  @plugin_dir = Dir.home + '/.rsh/plugins' # Plugins directory
166
167
  @plugins = [] # Loaded plugin instances
167
168
  @plugin_enabled = [] # List of enabled plugin names (whitelist)
@@ -429,7 +430,7 @@ def getstr # A custom Readline-like function
429
430
  when 'C-G' # Ctrl-G opens command in $EDITOR
430
431
  temp_file = "/tmp/rsh_edit_#{Process.pid}.tmp"
431
432
  File.write(temp_file, @history[0] || "")
432
- system("#{ENV['EDITOR'] || 'vi'} #{temp_file}")
433
+ system(ENV['EDITOR'] || 'vi', temp_file)
433
434
  if File.exist?(temp_file)
434
435
  edited = File.read(temp_file).strip
435
436
  # Convert multi-line to single line with proper separators
@@ -561,7 +562,7 @@ def getstr # A custom Readline-like function
561
562
  end
562
563
  lift = true
563
564
  when 'C-Y' # Copy command line to primary selection
564
- system("echo -n '#{@history[0]}' | xclip")
565
+ IO.popen('xclip', 'w') { |io| io.write(@history[0]) }
565
566
  puts "\n#{Time.now.strftime("%H:%M:%S")}: Copied to primary selection (paste with middle buttoni)".c(@c_stamp)
566
567
  when 'C-Z' # Suspend current process (background job)
567
568
  if @current_pid
@@ -968,7 +969,7 @@ def tab(type)
968
969
  begin
969
970
  count = Dir.entries(clean_item).length - 2
970
971
  print " [#{count}]".c(244)
971
- rescue
972
+ rescue Errno::EACCES, Errno::ENOENT
972
973
  end
973
974
  else
974
975
  size = File.size(clean_item)
@@ -1004,7 +1005,7 @@ def tab(type)
1004
1005
  begin
1005
1006
  count = Dir.entries(clean_item).length - 2
1006
1007
  print " [#{count}]".c(244)
1007
- rescue
1008
+ rescue Errno::EACCES, Errno::ENOENT
1008
1009
  end
1009
1010
  else
1010
1011
  size = File.size(clean_item)
@@ -1088,6 +1089,7 @@ def tabend
1088
1089
  @c.col(@c_col)
1089
1090
  end
1090
1091
  def get_command_switches(command) # Helper function to extract switches from --help
1092
+ require 'shellwords' unless defined?(Shellwords)
1091
1093
  # Check cache first (cache expires after 1 hour)
1092
1094
  cache_key = command.to_s.strip
1093
1095
  return [] if cache_key.empty?
@@ -1098,9 +1100,10 @@ def get_command_switches(command) # Helper function to extract switches from --h
1098
1100
  end
1099
1101
 
1100
1102
  # Parse --help output
1101
- hlp = `#{command} --help 2>/dev/null`
1103
+ escaped = Shellwords.escape(command)
1104
+ hlp = `#{escaped} --help 2>/dev/null`
1102
1105
  # Try -h if --help didn't work
1103
- hlp = `#{command} -h 2>/dev/null` if hlp.empty?
1106
+ hlp = `#{escaped} -h 2>/dev/null` if hlp.empty?
1104
1107
  return [] if hlp.empty?
1105
1108
 
1106
1109
  switches = []
@@ -1242,6 +1245,7 @@ def config(*args) # Configure rsh settings
1242
1245
  when 'history_dedup'
1243
1246
  if %w[off full smart].include?(value)
1244
1247
  @history_dedup = value
1248
+ @config_dirty << '@history_dedup'
1245
1249
  puts "History deduplication set to '#{value}'"
1246
1250
  rshrc
1247
1251
  else
@@ -1249,30 +1253,37 @@ def config(*args) # Configure rsh settings
1249
1253
  end
1250
1254
  when 'session_autosave'
1251
1255
  @session_autosave = value.to_i
1256
+ @config_dirty << '@session_autosave'
1252
1257
  puts "Session auto-save set to #{value}s #{value.to_i > 0 ? '(enabled)' : '(disabled)'}"
1253
1258
  rshrc
1254
1259
  when 'auto_correct'
1255
1260
  @auto_correct = %w[on true yes 1].include?(value.to_s.downcase)
1261
+ @config_dirty << '@auto_correct'
1256
1262
  puts "Auto-correct #{@auto_correct ? 'enabled' : 'disabled'}"
1257
1263
  rshrc
1258
1264
  when 'slow_command_threshold'
1259
1265
  @slow_command_threshold = value.to_i
1266
+ @config_dirty << '@slow_command_threshold'
1260
1267
  puts "Slow command threshold set to #{value}s #{value.to_i > 0 ? '(enabled)' : '(disabled)'}"
1261
1268
  rshrc
1262
1269
  when 'completion_learning'
1263
1270
  @completion_learning = %w[on true yes 1].include?(value.to_s.downcase)
1271
+ @config_dirty << '@completion_learning'
1264
1272
  puts "Completion learning #{@completion_learning ? 'enabled' : 'disabled'}"
1265
1273
  rshrc
1266
1274
  when 'completion_limit'
1267
1275
  @completion_limit = value.to_i
1276
+ @config_dirty << '@completion_limit'
1268
1277
  puts "Completion limit set to #{value}"
1269
1278
  rshrc
1270
1279
  when 'completion_show_metadata'
1271
1280
  @completion_show_metadata = %w[on true yes 1].include?(value.to_s.downcase)
1281
+ @config_dirty << '@completion_show_metadata'
1272
1282
  puts "Completion metadata display #{@completion_show_metadata ? 'enabled' : 'disabled'}"
1273
1283
  rshrc
1274
1284
  when 'show_tips'
1275
1285
  @show_tips = %w[on true yes 1].include?(value.to_s.downcase)
1286
+ @config_dirty << '@show_tips'
1276
1287
  puts "Startup tips #{@show_tips ? 'enabled' : 'disabled'}"
1277
1288
  rshrc
1278
1289
  else
@@ -1432,7 +1443,7 @@ def format_tab_item(item, show_metadata: false) # Format tab item with color and
1432
1443
  begin
1433
1444
  count = Dir.entries(clean_name).length - 2 # Exclude . and ..
1434
1445
  meta = "[dir, #{count} items]"
1435
- rescue
1446
+ rescue Errno::EACCES, Errno::ENOENT
1436
1447
  meta = "[dir]"
1437
1448
  end
1438
1449
  else
@@ -1511,7 +1522,7 @@ def rshrc # Write user configuration to .rshrc (portable between machines)
1511
1522
  begin
1512
1523
  file_nicks = eval($1)
1513
1524
  @nick = file_nicks.merge(@nick) if file_nicks.is_a?(Hash)
1514
- rescue
1525
+ rescue SyntaxError, StandardError
1515
1526
  # If eval fails, keep current @nick
1516
1527
  end
1517
1528
  end
@@ -1521,7 +1532,7 @@ def rshrc # Write user configuration to .rshrc (portable between machines)
1521
1532
  begin
1522
1533
  file_gnicks = eval($1)
1523
1534
  @gnick = file_gnicks.merge(@gnick) if file_gnicks.is_a?(Hash)
1524
- rescue
1535
+ rescue SyntaxError, StandardError
1525
1536
  # If eval fails, keep current @gnick
1526
1537
  end
1527
1538
  end
@@ -1535,21 +1546,23 @@ def rshrc # Write user configuration to .rshrc (portable between machines)
1535
1546
  conf.sub!(/^#{Regexp.escape(var)} =.*\n?/, "")
1536
1547
  end
1537
1548
 
1538
- # Persist user-editable configuration using helper
1549
+ # Persist runtime data (nicks, bookmarks, etc. change during normal use)
1539
1550
  conf = persist_var(conf, '@nick', @nick)
1540
1551
  conf = persist_var(conf, '@gnick', @gnick)
1541
1552
  conf = persist_var(conf, '@bookmarks', @bookmarks, !@bookmarks.empty?)
1542
1553
  conf = persist_var(conf, '@defuns', @defuns, !@defuns.empty?)
1543
- conf = persist_var(conf, '@history_dedup', @history_dedup)
1544
- conf = persist_var(conf, '@session_autosave', @session_autosave)
1545
- conf = persist_var(conf, '@auto_correct', @auto_correct)
1546
- conf = persist_var(conf, '@slow_command_threshold', @slow_command_threshold)
1547
- conf = persist_var(conf, '@completion_learning', @completion_learning)
1548
- conf = persist_var(conf, '@show_tips', @show_tips)
1549
- conf = persist_var(conf, '@completion_show_metadata', @completion_show_metadata)
1550
1554
  conf = persist_var(conf, '@plugin_disabled', @plugin_disabled, !@plugin_disabled.empty?)
1551
1555
  conf = persist_var(conf, '@validation_rules', @validation_rules, !@validation_rules.empty?)
1552
1556
 
1557
+ # Only persist config vars that were explicitly changed via :config in this session
1558
+ conf = persist_var(conf, '@history_dedup', @history_dedup, @config_dirty.include?('@history_dedup'))
1559
+ conf = persist_var(conf, '@session_autosave', @session_autosave, @config_dirty.include?('@session_autosave'))
1560
+ conf = persist_var(conf, '@auto_correct', @auto_correct, @config_dirty.include?('@auto_correct'))
1561
+ conf = persist_var(conf, '@slow_command_threshold', @slow_command_threshold, @config_dirty.include?('@slow_command_threshold'))
1562
+ conf = persist_var(conf, '@completion_learning', @completion_learning, @config_dirty.include?('@completion_learning'))
1563
+ conf = persist_var(conf, '@show_tips', @show_tips, @config_dirty.include?('@show_tips'))
1564
+ conf = persist_var(conf, '@completion_show_metadata', @completion_show_metadata, @config_dirty.include?('@completion_show_metadata'))
1565
+
1553
1566
  File.write(Dir.home+'/.rshrc', conf)
1554
1567
  rshstate # Also save runtime state
1555
1568
  end
@@ -3850,12 +3863,13 @@ loop do
3850
3863
  puts "#{Time.now.strftime("%H:%M:%S")}: #{@cmd}".c(@c_stamp)
3851
3864
  # Only auto-open files if it's a single filename (no spaces = no command with args)
3852
3865
  if File.read(@cmd).force_encoding("UTF-8").valid_encoding?
3853
- system("#{ENV['EDITOR']} #{@cmd}") # Try open with user's editor
3866
+ system(ENV['EDITOR'] || 'vi', @cmd) # Try open with user's editor
3854
3867
  else
3868
+ require 'shellwords' unless defined?(Shellwords)
3855
3869
  if @runmailcap
3856
- Thread.new { system("run-mailcap #{@cmd} 2>/dev/null") }
3870
+ Thread.new { system("run-mailcap #{Shellwords.escape(@cmd)} 2>/dev/null") }
3857
3871
  else
3858
- Thread.new { system("xdg-open #{@cmd} 2>/dev/null") }
3872
+ Thread.new { system("xdg-open #{Shellwords.escape(@cmd)} 2>/dev/null") }
3859
3873
  end
3860
3874
  end
3861
3875
  else
metadata CHANGED
@@ -1,19 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-shell
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.15
4
+ version: 3.6.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-27 00:00:00.000000000 Z
11
+ date: 2026-03-21 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.6.15: Fix config persistence bugs - scalar settings now always persist, runtime
16
- data stripped from .rshrc.'
15
+ v3.6.16: Security hardening, fix shell injection in xclip/editor/mailcap calls,
16
+ narrow bare rescues.'
17
17
  email: g@isene.com
18
18
  executables:
19
19
  - rsh