ruby-shell 2.6.2 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/.rshrc +35 -16
  3. data/README.md +81 -1
  4. data/bin/rsh +267 -65
  5. metadata +6 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abf81e738d6eec85e722c3aeb2f1994813266faa24772785ef045f22039a0b7a
4
- data.tar.gz: 7911f8b5e4fede5f8c3d137a9b50b3362afe8ea18f9cbe5b489b8434e5b3f934
3
+ metadata.gz: 35706aa5efb5ce661a102e1c6a5b22638a750b567f41930b683f239d42386964
4
+ data.tar.gz: 164ea239fc7081182c031cf002dc7e3c64696536f5795057e24ba027a899f998
5
5
  SHA512:
6
- metadata.gz: 503296a0ae73254125e06f2602c04dda0425f0d91e26636152d4f7e97844e8fbacc6cb4bbcaca107456f54235cf47704630ec49f6c76d3f338e4e0207b164a38
7
- data.tar.gz: 4843dddcb14764bcdd30da94b138117db82b038658268c461d867dfd5ab8a7529f90027ee175988b681bfb2db6b2e809c4a47b4f8aeab967367484ce838efae0
6
+ metadata.gz: 952d3ba93d41715d50a6b8a4f669f8f670f3b5c403f300f24fd7a28fe9cb432a4f66b2ba747fb9ddbab6e3ec6a33ec94113c3e499ee6014a25ec6d7861266929
7
+ data.tar.gz: 41ea80063e2fd754acd4a449ddac1f418bb3450b10bd70a9a99432159fefbcf2002bf6964638cd03079585088974d382c0978d830eb3beb8f28936cc2bc2cf65
data/.rshrc CHANGED
@@ -1,29 +1,48 @@
1
1
  # vim: set ft=ruby sw=2 sts=2 et :
2
+ # Example config file for rsh (.rshrc)
2
3
 
3
4
  # ENVIRONMENT
4
- #@lscolors = "/home/geir/.local/share/lscolors.sh"
5
- ENV["EDITOR"] = "vim"
6
- ENV["MANPAGER"] = "vim +MANPAGER -"
5
+ @lscolors = "/home/geir/.local/share/lscolors.sh"
6
+ ENV["EDITOR"] = "vim"
7
+ ENV["IRCNAME"] = "Geir Isene"
8
+ ENV["IRCNICK"] = "isene"
9
+ ENV["LESS"] = "-M-Q-r"
10
+ ENV["LESSCHARDEF"] = "8bcccbcc13b.4b95.33b."
11
+ ENV["LESSCHARSET"] = "latin1"
12
+ ENV["LESSEDIT"] = "%E ?lt+%lt. %f"
13
+ ENV["LESSOPEN"] = "| /home/geir/bin/lesspipe %s"
14
+ ENV["MANPAGER"] = "vim +MANPAGER -"
15
+ ENV["PAGER"] = "less"
16
+ ENV["TZ"] = 'Europe/Oslo'
17
+ ENV["VISUAL"] = "vim"
18
+ ENV["XDG_DATA_HOME"] = "/home/geir/.local/share"
19
+ ENV["PATH"] += ":/home/geir/bin"
7
20
 
8
21
  # PROMPT
9
22
  if @user == "root"
10
- @prompt = "#{@user}@#{@node}".c(160).b + ":".c(255) + " #{Dir.pwd}/".c(196) + " ".c(7)
23
+ @prompt = @user.c(1).b + "@#{@node}".c(1)
11
24
  else
12
- @prompt = "#{@user}@#{@node}".c(46) + ":".c(255) + " #{Dir.pwd}/".c(196) + " ".c(7)
25
+ @prompt = @user.c(2).b + "@#{@node}".c(2)
13
26
  end
27
+ @prompt += ":".c(252) + " #{Dir.pwd}/".c(3) + " ".c(255)
14
28
 
15
29
  # THEME
16
- @c_prompt = 196 # Color for basic prompt
17
- @c_cmd = 48 # Color for valid command
18
- @c_nick = 51 # Color for matching nick
19
- @c_gnick = 87 # Color for matching gnick
20
- @c_path = 208 # Color for valid path
21
- @c_switch = 148 # Color for switches/options
22
- @c_tabselect = 207 # Color for selected tabcompleted item
23
- @c_taboption = 244 # Color for unselected tabcompleted item
24
- @c_stamp = 244 # Color for time stamp/command
30
+ #@c_prompt = 196 # Color for basic prompt
31
+ #@c_cmd = 84 # Color for valid command
32
+ #@c_nick = 87 # Color for matching nick
33
+ #@c_gnick = 123 # Color for matching gnick
34
+ #@c_path = 208 # Color for valid path
35
+ #@c_switch = 148 # Color for switches/options
36
+ #@c_tabselect = 220 # Color for selected tabcompleted item
37
+ #@c_taboption = 94 # Color for unselected tabcompleted item
38
+ #@c_stamp = 244 # Color for time stamp/command
39
+
40
+ def pre_cmd
41
+ end
42
+
43
+ def post_cmd
44
+ end
25
45
 
26
46
  # NICKS AND HISTORY
27
- @nick = {"ls"=>"ls --color -F"}
28
- @gnick = {}
47
+ @nick = {"ls"=>"ls --color -F", "l"=>"less", "v"=>"vim", "gu"=>"gitmagic -u", "gc"=>"git clone", "gs"=>"git status .", "vv"=>"vim ~/.vimrc", "vr"=>"vim ~/.rshrc"}
29
48
  @history = []
data/README.md CHANGED
@@ -18,6 +18,8 @@ Or simply `gem install ruby-shell`.
18
18
  [![rsh screencast](/img/rsh-screencast.png)](https://youtu.be/4P2z8oSo1u4)
19
19
 
20
20
  # Features
21
+
22
+ ## Core Shell Features
21
23
  * Aliases (called nicks in rsh) - both for commands and general nicks
22
24
  * Syntax highlighting, matching nicks, system commands and valid dirs/files
23
25
  * Tab completions for nicks, system commands, command switches and dirs/files
@@ -30,6 +32,22 @@ Or simply `gem install ruby-shell`.
30
32
  * rsh specific commands and full set of Ruby commands available via :<command>
31
33
  * All colors are themeable in .rshrc (see github link for possibilities)
32
34
  * Copy current command line to primary selection (paste w/middle button) with `Ctrl-y`
35
+
36
+ ## NEW in v2.7.0 - Ruby Functions ⭐
37
+ * **Define Ruby functions as shell commands**: `:defun 'weather(*args) = system("curl -s wttr.in/#{args[0] || \"oslo\"}")'`
38
+ * **Call like any shell command**: `weather london`
39
+ * **Full Ruby power**: Access to Ruby stdlib, file operations, JSON parsing, web requests, etc.
40
+ * **Function management**: `:defun?` to list, `:defun '-name'` to remove
41
+ * **Syntax highlighting**: Ruby functions highlighted in bold
42
+
43
+ ## Advanced Shell Features
44
+ * **Job Control**: Background jobs (`command &`), job suspension (`Ctrl-Z`), process management
45
+ * **Job Management**: `:jobs`, `:fg [id]`, `:bg [id]` commands
46
+ * **Command Substitution**: `$(date)` and backtick support
47
+ * **Variable Expansion**: `$HOME`, `$USER`, `$?` (exit status)
48
+ * **Conditional Execution**: `cmd1 && cmd2 || cmd3`
49
+ * **Brace Expansion**: `{a,b,c}` expands to `a b c`
50
+ * **Login Shell Support**: Proper signal handling and profile loading
33
51
 
34
52
  Special functions/integrations:
35
53
  * Use `r` to launch rtfm (https://github.com/isene/RTFM) - if you have it installed
@@ -42,9 +60,18 @@ Special commands:
42
60
  * `:gnick 'h = /home/me'` to make a general alias (h) point to something (/home/me)
43
61
  * `:nick?` will list all command nicks and general nicks (you can edit your nicks in .rshrc)
44
62
  * `:history` will list the command history, while `:rmhistory` will delete the history
63
+ * `:jobs` will list background jobs, `:fg [job_id]` brings jobs to foreground, `:bg [job_id]` resumes stopped jobs
64
+ * `:defun 'func(args) = code'` defines Ruby functions callable as shell commands
65
+ * `:defun?` lists all user-defined functions, `:defun '-func'` removes functions
45
66
  * `:version` Shows the rsh version number and the last published gem file version
46
67
  * `:help` will display this help text
47
68
 
69
+ Background jobs:
70
+ * Use `command &` to run commands in background
71
+ * Use `:jobs` to list active background jobs
72
+ * Use `:fg` or `:fg job_id` to bring jobs to foreground
73
+ * Use `Ctrl-Z` to suspend running jobs, `:bg job_id` to resume them
74
+
48
75
  ## Moving around
49
76
  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`.
50
77
 
@@ -60,7 +87,60 @@ Hitting Shift-TAB will do a similar search through the command history - but wit
60
87
  If you press `ENTER` after writing or tab-completing to a file, rsh will try to open the file in the user's EDITOR of choice (if it is a valid text file) or use `xdg-open` to open the file using the correct program. If you, for some reason want to use `run-mailcap` instead of `xdg-open` as the file opener, simply add `@runmailcap = true` to your `.rshrc`.
61
88
 
62
89
  ## History
63
- Show the history with `:history`. Redo a history command with an exclamation mark and the number corresponding to the position in the history, like `!5` would do the 5th history command again.
90
+ Show the history with `:history`. Redo a history command with an exclamation mark and the number corresponding to the position in the history, like `!5` would do the 5th history command again. To delete a specific entry in history, hit `UP` and move up to that entry and hit `Ctrl-k` (for "kill").
91
+
92
+ ## Ruby Functions - The Power Feature ⭐
93
+
94
+ rsh's unique Ruby functions let you define custom shell commands using the full power of Ruby:
95
+
96
+ ### Basic Examples
97
+ ```bash
98
+ # File operations
99
+ :defun 'count(*args) = puts Dir.glob(args[0] || "*").length'
100
+ count *.rb
101
+
102
+ # System monitoring
103
+ :defun 'mem = puts `free -h`.lines[1].split[2]'
104
+ mem
105
+
106
+ # JSON pretty-printing
107
+ :defun 'jsonpp(file) = require "json"; puts JSON.pretty_generate(JSON.parse(File.read(file)))'
108
+ jsonpp config.json
109
+ ```
110
+
111
+ ### Advanced Examples
112
+ ```bash
113
+ # Network tools
114
+ :defun 'ports = puts `netstat -tlnp`.lines.grep(/LISTEN/).map{|l| l.split[3]}'
115
+ ports
116
+
117
+ # Git helpers
118
+ :defun 'branches = puts `git branch`.lines.map{|l| l.strip.sub("* ", "")}'
119
+ branches
120
+
121
+ # Directory analysis
122
+ :defun 'sizes(*args) = Dir.glob(args[0] || "*").each{|f| puts "#{File.size(f).to_s.rjust(8)} #{f}" if File.file?(f)}'
123
+ sizes
124
+
125
+ # Weather (using external API)
126
+ :defun 'weather(*args) = system("curl -s wttr.in/#{args[0] || \"oslo\"}")'
127
+ weather london
128
+ ```
129
+
130
+ ### Function Management
131
+ ```bash
132
+ :defun? # List all defined functions
133
+ :defun '-myls' # Remove a function
134
+ ```
135
+
136
+ Ruby functions have access to:
137
+ - Full Ruby standard library
138
+ - Shell environment variables via `ENV`
139
+ - rsh internals like `@history`, `@dirs`
140
+ - File system operations
141
+ - Network requests
142
+ - JSON/XML parsing
143
+ - And everything else Ruby can do!
64
144
 
65
145
  ## Integrations
66
146
  rsh is integrated with the [rtfm file manager](https://github.com/isene/RTFM), with [fzf](https://github.com/junegunn/fzf) and with the programming language [XRPN](https://github.com/isene/xrpn).
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 = "2.6.2"
11
+ @version = "2.7.0" # Major release: Ruby functions, job control, command substitution, login shell support
12
12
 
13
13
  # MODULES, CLASSES AND EXTENSIONS
14
14
  class String # Add coloring to strings (with escaping for Readline)
@@ -50,18 +50,6 @@ module Cursor # Terminal cursor movement ANSI codes (thanks to https://github.co
50
50
  row, col = self.pos
51
51
  return col
52
52
  end
53
- def up(n = nil) # Move cursor up by n
54
- print(CSI + "#{(n || 1)}A")
55
- end
56
- def down(n = nil) # Move the cursor down by n
57
- print(CSI + "#{(n || 1)}B")
58
- end
59
- def left(n = nil) # Move the cursor backward by n
60
- print(CSI + "#{n || 1}D")
61
- end
62
- def right(n = nil) # Move the cursor forward by n
63
- print(CSI + "#{n || 1}C")
64
- end
65
53
  def col(n = nil) # Cursor moves to nth position horizontally in the current line
66
54
  print(CSI + "#{n || 1}G")
67
55
  end
@@ -71,24 +59,12 @@ module Cursor # Terminal cursor movement ANSI codes (thanks to https://github.co
71
59
  def next_line # Move cursor down to beginning of next line
72
60
  print(CSI + 'E' + CSI + "1G")
73
61
  end
74
- def prev_line # Move cursor up to beginning of previous line
75
- print(CSI + 'A' + CSI + "1G")
76
- end
77
- def clear_char(n = nil) # Erase n characters from the current cursor position
78
- print(CSI + "#{n}X")
79
- end
80
62
  def clear_line # Erase the entire current line and return to beginning of the line
81
63
  print(CSI + '2K' + CSI + "1G")
82
64
  end
83
- def clear_line_before # Erase from the beginning of the line up to and including the current cursor position.
84
- print(CSI + '1K')
85
- end
86
65
  def clear_line_after # Erase from the current position (inclusive) to the end of the line
87
66
  print(CSI + '0K')
88
67
  end
89
- def scroll_up # Scroll display up one line
90
- print(ESC + 'M')
91
- end
92
68
  def scroll_down # Scroll display down one line
93
69
  print(ESC + 'D')
94
70
  end
@@ -96,12 +72,6 @@ module Cursor # Terminal cursor movement ANSI codes (thanks to https://github.co
96
72
  print(CSI + 'J')
97
73
  end
98
74
  end
99
- def stdin_clear
100
- begin
101
- $stdin.getc while $stdin.ready?
102
- rescue
103
- end
104
- end
105
75
 
106
76
  # INITIALIZATION
107
77
  begin # Requires
@@ -137,6 +107,9 @@ begin # Initialization
137
107
  @runmailcap = false
138
108
  # Variable initializations
139
109
  @dirs = ["."]*10
110
+ @jobs = {} # Background jobs tracking
111
+ @job_id = 0 # Job counter
112
+ @last_exit = 0 # Last command exit status
140
113
  def pre_cmd; end # User-defined function to be run BEFORE command execution
141
114
  def post_cmd; end # User-defined function to be run AFTER command execution
142
115
  end
@@ -176,9 +149,24 @@ end
176
149
  * `:gnick 'h = /home/me'` to make a general alias (h) point to something (/home/me)
177
150
  * `:nick?` will list all command nicks and general nicks (you can edit your nicks in .rshrc)
178
151
  * `:history` will list the command history, while `:rmhistory` will delete the history
152
+ * `:jobs` will list background jobs, `:fg [job_id]` brings jobs to foreground, `:bg [job_id]` resumes stopped jobs
153
+ * `:defun 'func(args) = code'` defines Ruby functions callable as shell commands
154
+ * `:defun?` lists all user-defined functions, `:defun '-func'` removes functions
179
155
  * `:version` Shows the rsh version number and the last published gem file version
180
156
  * `:help` will display this help text
181
157
 
158
+ Background jobs:
159
+ * Use `command &` to run commands in background
160
+ * Use `:jobs` to list active background jobs
161
+ * Use `:fg` or `:fg job_id` to bring jobs to foreground
162
+ * Use `Ctrl-Z` to suspend running jobs, `:bg job_id` to resume them
163
+
164
+ Ruby Functions:
165
+ * Define with `:defun 'myls(*args) = Dir.glob("*").each {|f| puts f}'`
166
+ * Call like any shell command: `myls` or `myls arg1 arg2`
167
+ * Functions have full access to Ruby stdlib and rsh internals
168
+ * Remove with `:defun '-myls'` and list with `:defun?`
169
+
182
170
  HELP
183
171
 
184
172
  # GENERIC FUNCTIONS
@@ -254,13 +242,13 @@ def getchr # Process key presses
254
242
  when "" then chr = "C-T"
255
243
  when "" then chr = "C-Y"
256
244
  when "" then chr = "WBACK"
245
+ when "\u001A" then chr = "C-Z"
257
246
  when "" then chr = "LDEL"
258
247
  when "\r" then chr = "ENTER"
259
248
  when "\t" then chr = "TAB"
260
249
  when /[[:print:]]/ then chr = c
261
250
  else chr = ""
262
251
  end
263
- #stdin_clear
264
252
  return chr
265
253
  end
266
254
  def getstr # A custom Readline-like function
@@ -398,6 +386,14 @@ def getstr # A custom Readline-like function
398
386
  when 'C-Y' # Copy command line to primary selection
399
387
  system("echo -n '#{@history[0]}' | xclip")
400
388
  puts "\n#{Time.now.strftime("%H:%M:%S")}: Copied to primary selection (paste with middle buttoni)".c(@c_stamp)
389
+ when 'C-Z' # Suspend current process (background job)
390
+ if @current_pid
391
+ puts "\n[#{@job_id}] Suspended #{@current_pid}"
392
+ Process.kill("STOP", @current_pid)
393
+ @jobs[@job_id] = {pid: @current_pid, cmd: @cmd, status: :stopped}
394
+ else
395
+ puts "\nNo active job to suspend"
396
+ end
401
397
  when 'C-K' # Kill/delete that entry in the history
402
398
  @history.delete_at(@stk)
403
399
  @stk -= 1
@@ -586,6 +582,8 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic
586
582
  el.c(@c_nick)
587
583
  elsif @gnick.include?(el)
588
584
  el.c(@c_gnick)
585
+ elsif self.respond_to?(el) && singleton_class.instance_methods(false).include?(el.to_sym)
586
+ el.c(@c_nick).b # Ruby functions in bold nick color
589
587
  elsif el[0] == "-"
590
588
  el.c(@c_switch)
591
589
  else
@@ -659,12 +657,165 @@ def dirs
659
657
  puts "#{i}: #{e}"
660
658
  end
661
659
  end
660
+ def jobs
661
+ puts "Active jobs:"
662
+ @jobs.each do |id, job|
663
+ begin
664
+ Process.kill(0, job[:pid]) # Check if process exists
665
+ puts "[#{id}] #{job[:pid]} #{job[:status]} #{job[:cmd]}"
666
+ rescue Errno::ESRCH
667
+ @jobs.delete(id) # Clean up dead jobs
668
+ end
669
+ end
670
+ end
671
+ def fg(job_id = nil)
672
+ job_id ||= @jobs.keys.max
673
+ return puts "No jobs" if job_id.nil?
674
+ job = @jobs[job_id]
675
+ return puts "Job #{job_id} not found" unless job
676
+ puts "Bringing job #{job_id} to foreground: #{job[:cmd]}"
677
+ begin
678
+ if job[:status] == :stopped
679
+ Process.kill("CONT", job[:pid])
680
+ end
681
+ @current_pid = job[:pid]
682
+ Process.wait(job[:pid])
683
+ @jobs.delete(job_id)
684
+ @current_pid = nil
685
+ rescue Errno::ECHILD, Errno::ESRCH
686
+ @jobs.delete(job_id)
687
+ @current_pid = nil
688
+ end
689
+ end
690
+ def bg(job_id = nil)
691
+ job_id ||= @jobs.keys.max
692
+ return puts "No jobs" if job_id.nil?
693
+ job = @jobs[job_id]
694
+ return puts "Job #{job_id} not found" unless job
695
+ return puts "Job #{job_id} already running" if job[:status] == :running
696
+ puts "Resuming job #{job_id} in background: #{job[:cmd]}"
697
+ begin
698
+ Process.kill("CONT", job[:pid])
699
+ @jobs[job_id][:status] = :running
700
+ rescue Errno::ESRCH
701
+ @jobs.delete(job_id)
702
+ puts "Job #{job_id} no longer exists"
703
+ end
704
+ end
705
+ def defun(func_def) # Define a Ruby function like: `:defun "myls(*args) = Dir.glob('*').each {|f| puts f}"`
706
+ if func_def.match(/^\s*-/)
707
+ # Remove function
708
+ func_name = func_def.sub(/^\s*-/, '')
709
+ if self.respond_to?(func_name)
710
+ singleton_class.remove_method(func_name.to_sym)
711
+ puts "Function '#{func_name}' removed"
712
+ else
713
+ puts "Function '#{func_name}' not found"
714
+ end
715
+ else
716
+ # Define function
717
+ # Extract function name, params, and body from "name(params) = body" format
718
+ if func_def =~ /^(\w+)\s*\(([^)]*)\)\s*=\s*(.+)$/
719
+ func_name = $1
720
+ func_params = $2
721
+ func_body = $3
722
+
723
+ begin
724
+ eval_code = "def #{func_name}(#{func_params}); #{func_body}; end"
725
+ puts " DEBUG: Evaluating: #{eval_code}" if ENV['RSH_DEBUG']
726
+ singleton_class.class_eval(eval_code)
727
+ puts "Function '#{func_name}' defined"
728
+ puts " DEBUG: Method created? #{respond_to?(func_name)}" if ENV['RSH_DEBUG']
729
+ rescue SyntaxError => e
730
+ puts "Syntax error in function definition: #{e}"
731
+ rescue => e
732
+ puts "Error in function definition: #{e}"
733
+ end
734
+ else
735
+ puts "Invalid function format. Use: name(params) = body"
736
+ end
737
+ end
738
+ rshrc
739
+ end
740
+ def defun? # Show all user-defined functions
741
+ puts "User-defined Ruby functions:"
742
+ # Get only methods defined by defun, excluding built-ins and rsh internals
743
+ all_methods = singleton_class.instance_methods(false)
744
+ puts " All singleton methods: #{all_methods}"
745
+ excluded = [:defun, :defun?, :execute_conditional, :expand_braces]
746
+ methods = all_methods - excluded
747
+ if methods.empty?
748
+ puts " (none defined after filtering)"
749
+ else
750
+ methods.each do |method|
751
+ puts " #{method}"
752
+ end
753
+ end
754
+ end
755
+ def execute_conditional(cmd_line)
756
+ # Split on && and || while preserving the operators
757
+ parts = cmd_line.split(/(\s*&&\s*|\s*\|\|\s*)/)
758
+
759
+ result = true
760
+ i = 0
761
+ while i < parts.length
762
+ command = parts[i].strip
763
+ next if command.empty?
764
+
765
+ if command == '&&'
766
+ i += 1
767
+ next unless result # Skip if previous command failed
768
+ elsif command == '||'
769
+ i += 1
770
+ next if result # Skip if previous command succeeded
771
+ else
772
+ # Execute the command
773
+ success = system(command)
774
+ result = success
775
+ puts " Command failed: #{command} (exit #{$?.exitstatus})" unless success
776
+ end
777
+ i += 1
778
+ end
779
+ end
780
+ def expand_braces(str)
781
+ # Simple brace expansion: {a,b,c} -> a b c
782
+ str.gsub(/\{([^}]+)\}/) do |match|
783
+ items = $1.split(',').map(&:strip)
784
+ items.join(' ')
785
+ end
786
+ end
662
787
 
663
788
  # INITIAL SETUP
664
789
  begin # Load .rshrc and populate @history
665
790
  trap "SIGINT" do end
791
+ trap "SIGHUP" do
792
+ rshrc
793
+ exit
794
+ end
795
+ trap "SIGTERM" do
796
+ rshrc
797
+ exit
798
+ end
666
799
  firstrun unless File.exist?(Dir.home+'/.rshrc') # Initial loading - to get history
667
- load(Dir.home+'/.rshrc')
800
+ load(Dir.home+'/.rshrc')
801
+ # Load login shell files if rsh is running as login shell
802
+ if ENV['LOGIN_SHELL'] or $0 == "-rsh" or ARGV.include?('-l') or ARGV.include?('--login')
803
+ ['/etc/profile', Dir.home+'/.profile', Dir.home+'/.bash_profile', Dir.home+'/.bashrc'].each do |f|
804
+ if File.exist?(f)
805
+ puts "Loading #{f}..." if ENV['RSH_DEBUG']
806
+ begin
807
+ # Source shell files by extracting export statements
808
+ File.readlines(f).each do |line|
809
+ if line =~ /^\s*export\s+(\w+)=(.*)/
810
+ ENV[$1] = $2.gsub(/['"]/, '')
811
+ end
812
+ end
813
+ rescue => e
814
+ puts "Warning: Could not source #{f}: #{e}" if ENV['RSH_DEBUG']
815
+ end
816
+ end
817
+ end
818
+ end
668
819
  ENV["SHELL"] = __FILE__
669
820
  ENV["TERM"] = "rxvt-unicode-256color"
670
821
  ENV["PATH"] ? ENV["PATH"] += ":" : ENV["PATH"] = ""
@@ -736,41 +887,92 @@ loop do
736
887
  elsif @cmd == '#' # List previous directories
737
888
  dirs
738
889
  else # Execute command
739
- ca = @nick.transform_keys {|k| /((^\K\s*\K)|(\|\K\s*\K))\b(?<!-)#{Regexp.escape k}\b/}
740
- @cmd = @cmd.gsub(Regexp.union(ca.keys), @nick)
741
- ga = @gnick.transform_keys {|k| /\b(?<!-)#{Regexp.escape k}\b/}
742
- @cmd = @cmd.gsub(Regexp.union(ga.keys), @gnick)
743
- @cmd = "~" if @cmd == "cd"
744
- @cmd.sub!(/^cd (\S*).*/, '\1')
745
- @cmd = Dir.home if @cmd == "~"
746
- @cmd = @dirs[1] if @cmd == "-"
747
- @cmd = @dirs[@cmd.to_i] if @cmd =~ /^\d$/
748
- dir = @cmd.strip.sub(/~/, Dir.home)
749
- if Dir.exist?(dir)
750
- Dir.chdir(dir)
751
- system("git status .") if Dir.exist?(".git")
890
+ # Check if it's a user-defined Ruby function FIRST (before any expansions)
891
+ cmd_parts = @cmd.split(/\s+/)
892
+ func_name = cmd_parts[0]
893
+ if self.respond_to?(func_name) && singleton_class.instance_methods(false).include?(func_name.to_sym)
894
+ begin
895
+ args = cmd_parts[1..]
896
+ puts "DEBUG: Calling #{func_name} with args: #{args}" if ENV['RSH_DEBUG']
897
+ result = self.send(func_name, *args)
898
+ puts "DEBUG: Result: #{result.inspect}" if ENV['RSH_DEBUG']
899
+ puts result unless result.nil?
900
+ rescue => e
901
+ puts "Error calling function '#{func_name}': #{e}"
902
+ end
752
903
  else
753
- puts "#{Time.now.strftime("%H:%M:%S")}: #{@cmd}".c(@c_stamp)
754
- if @cmd == "f" # fzf integration (https://github.com/junegunn/fzf)
755
- res = `fzf`.chomp
756
- Dir.chdir(File.dirname(res))
757
- elsif File.exist?(@cmd) and not File.executable?(@cmd)
758
- if File.read(@cmd).force_encoding("UTF-8").valid_encoding?
759
- system("#{ENV['EDITOR']} #{@cmd}") # Try open with user's editor
760
- else
761
- if @runmailcap
762
- Thread.new { system("run-mailcap #{@cmd} 2>/dev/null") }
904
+ # Handle conditional execution (&& and ||)
905
+ if @cmd.include?('&&') || @cmd.include?('||')
906
+ execute_conditional(@cmd)
907
+ next
908
+ end
909
+ # Expand brace expansion {a,b,c}
910
+ @cmd = expand_braces(@cmd)
911
+ # Expand command substitution $(command) and backticks
912
+ @cmd = @cmd.gsub(/\$\(([^)]+)\)/) { `#{$1}`.chomp }
913
+ @cmd = @cmd.gsub(/`([^`]+)`/) { `#{$1}`.chomp }
914
+ # Expand environment variables and exit status
915
+ @cmd = @cmd.gsub(/\$\?/) { @last_exit.to_s }
916
+ @cmd = @cmd.gsub(/\$(\w+)|\$\{(\w+)\}/) { ENV[$1 || $2] || '' }
917
+ # Expand tilde
918
+ @cmd = @cmd.gsub(/~/, Dir.home)
919
+ ca = @nick.transform_keys {|k| /((^\K\s*\K)|(\|\K\s*\K))\b(?<!-)#{Regexp.escape k}\b/}
920
+ @cmd = @cmd.gsub(Regexp.union(ca.keys), @nick)
921
+ ga = @gnick.transform_keys {|k| /\b(?<!-)#{Regexp.escape k}\b/}
922
+ @cmd = @cmd.gsub(Regexp.union(ga.keys), @gnick)
923
+ @cmd = "~" if @cmd == "cd"
924
+ @cmd.sub!(/^cd (\S*).*/, '\1')
925
+ @cmd = Dir.home if @cmd == "~"
926
+ @cmd = @dirs[1] if @cmd == "-"
927
+ @cmd = @dirs[@cmd.to_i] if @cmd =~ /^\d$/
928
+ # Check if it's a directory to change to
929
+ dir = @cmd.strip.sub(/~/, Dir.home)
930
+ if Dir.exist?(dir)
931
+ Dir.chdir(dir)
932
+ system("git status .") if Dir.exist?(".git")
933
+ else
934
+ puts "#{Time.now.strftime("%H:%M:%S")}: #{@cmd}".c(@c_stamp)
935
+ if @cmd == "f" # fzf integration (https://github.com/junegunn/fzf)
936
+ res = `fzf`.chomp
937
+ Dir.chdir(File.dirname(res))
938
+ elsif File.exist?(@cmd) and not File.executable?(@cmd) and not @cmd.include?(" ")
939
+ # Only auto-open files if it's a single filename (no spaces = no command with args)
940
+ if File.read(@cmd).force_encoding("UTF-8").valid_encoding?
941
+ system("#{ENV['EDITOR']} #{@cmd}") # Try open with user's editor
763
942
  else
764
- Thread.new { system("xdg-open #{@cmd} 2>/dev/null") }
943
+ if @runmailcap
944
+ Thread.new { system("run-mailcap #{@cmd} 2>/dev/null") }
945
+ else
946
+ Thread.new { system("xdg-open #{@cmd} 2>/dev/null") }
947
+ end
948
+ end
949
+ else
950
+ begin
951
+ pre_cmd
952
+ # Handle background jobs
953
+ if @cmd.end_with?(' &')
954
+ @cmd = @cmd[0..-3] # Remove the &
955
+ @job_id += 1
956
+ # Handle pipes and redirections in background
957
+ if @cmd.include?('|') || @cmd.include?('>') || @cmd.include?('<')
958
+ pid = spawn(@cmd, pgroup: true)
959
+ else
960
+ pid = spawn(@cmd)
961
+ end
962
+ @jobs[@job_id] = {pid: pid, cmd: @cmd, status: :running}
963
+ puts "[#{@job_id}] #{pid} #{@cmd}"
964
+ else
965
+ # Better handling of pipes and redirections
966
+ @current_pid = spawn(@cmd)
967
+ Process.wait(@current_pid)
968
+ @last_exit = $?.exitstatus
969
+ @current_pid = nil
970
+ puts " Command failed: #{@cmd} (exit #{@last_exit})" unless @last_exit == 0
971
+ end
972
+ post_cmd
973
+ rescue StandardError => err
974
+ puts "\nError: #{err}"
765
975
  end
766
- end
767
- else
768
- begin
769
- pre_cmd
770
- puts " Not executed: #{@cmd}" unless system (@cmd) # Try execute the command
771
- post_cmd
772
- rescue StandardError => err
773
- puts "\n#{err}"
774
976
  end
775
977
  end
776
978
  end
metadata CHANGED
@@ -1,20 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-shell
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.2
4
+ version: 2.7.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: 2024-11-01 00:00:00.000000000 Z
11
+ date: 2025-07-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'A shell written in Ruby with extensive tab completions, aliases/nicks,
14
- history, syntax highlighting, theming, auto-cd, auto-opening files and more. In
15
- continual development. New in 2.0: Full rewrite of tab completion engine. Lots of
16
- other bug fixes. 2.6: Handling line longer than terminal width. 2.6.2: Fixed issue
17
- with tabbing at bottom of screen.'
14
+ history, syntax highlighting, theming, auto-cd, auto-opening files and more. MAJOR
15
+ UPDATE v2.7.0: Ruby Functions - define custom shell commands using full Ruby power!
16
+ Also: job control (background jobs, Ctrl-Z suspension), command substitution, variable
17
+ expansion, conditional execution, login shell support, and much more.'
18
18
  email: g@isene.com
19
19
  executables:
20
20
  - rsh