ruby-shell 2.6.2 → 2.8.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 +91 -2
  4. data/bin/rsh +359 -83
  5. metadata +7 -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: 55c80db8b211f00c2226a2bc38de34ca8bc448ff38545043f24931bde8441de6
4
+ data.tar.gz: 7f70a5858de2c5d64800202a35af757b9bef6d864ccf76ab23dc673b82451fc1
5
5
  SHA512:
6
- metadata.gz: 503296a0ae73254125e06f2602c04dda0425f0d91e26636152d4f7e97844e8fbacc6cb4bbcaca107456f54235cf47704630ec49f6c76d3f338e4e0207b164a38
7
- data.tar.gz: 4843dddcb14764bcdd30da94b138117db82b038658268c461d867dfd5ab8a7529f90027ee175988b681bfb2db6b2e809c4a47b4f8aeab967367484ce838efae0
6
+ metadata.gz: 6ad59802df4fc00d2d39191080b4a8df1d90ad5c567078c16562992b64d0aa8ad3ca59fdd9a0ab0bcefc35f6b02774305bec3f83872d463d27c2570887376901
7
+ data.tar.gz: 0a4cd352e6e20d25af578a689bffb6d460cc24f0b40328461b8a9ada40d3b03bda6e6c79674944170b752b7dc3787b9397869644025bc7925e90d54ce3bcff4e
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,28 @@ 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.8.0 - Enhanced Help System & Nick Management ⭐
37
+ * **Two-column help display**: Compact, organized help that fits on one screen
38
+ * **New `:info` command**: Shows introduction and feature overview
39
+ * **`:nickdel` and `:gnickdel`**: Intuitive commands to delete nicks and gnicks
40
+ * **Improved help organization**: Quick reference for keyboard shortcuts, commands, and features
41
+
42
+ ## Ruby Functions (v2.7.0)
43
+ * **Define Ruby functions as shell commands**: `:defun 'weather(*args) = system("curl -s wttr.in/#{args[0] || \"oslo\"}")'`
44
+ * **Call like any shell command**: `weather london`
45
+ * **Full Ruby power**: Access to Ruby stdlib, file operations, JSON parsing, web requests, etc.
46
+ * **Function management**: `:defun?` to list, `:defun '-name'` to remove
47
+ * **Syntax highlighting**: Ruby functions highlighted in bold
48
+
49
+ ## Advanced Shell Features
50
+ * **Job Control**: Background jobs (`command &`), job suspension (`Ctrl-Z`), process management
51
+ * **Job Management**: `:jobs`, `:fg [id]`, `:bg [id]` commands
52
+ * **Command Substitution**: `$(date)` and backtick support
53
+ * **Variable Expansion**: `$HOME`, `$USER`, `$?` (exit status)
54
+ * **Conditional Execution**: `cmd1 && cmd2 || cmd3`
55
+ * **Brace Expansion**: `{a,b,c}` expands to `a b c`
56
+ * **Login Shell Support**: Proper signal handling and profile loading
33
57
 
34
58
  Special functions/integrations:
35
59
  * Use `r` to launch rtfm (https://github.com/isene/RTFM) - if you have it installed
@@ -40,10 +64,22 @@ Special functions/integrations:
40
64
  Special commands:
41
65
  * `:nick 'll = ls -l'` to make a command alias (ll) point to a command (ls -l)
42
66
  * `:gnick 'h = /home/me'` to make a general alias (h) point to something (/home/me)
67
+ * `:nickdel 'name'` to delete a command nick (or use `:nick '-name'`)
68
+ * `:gnickdel 'name'` to delete a general nick (or use `:gnick '-name'`)
43
69
  * `:nick?` will list all command nicks and general nicks (you can edit your nicks in .rshrc)
44
70
  * `:history` will list the command history, while `:rmhistory` will delete the history
71
+ * `:jobs` will list background jobs, `:fg [job_id]` brings jobs to foreground, `:bg [job_id]` resumes stopped jobs
72
+ * `:defun 'func(args) = code'` defines Ruby functions callable as shell commands
73
+ * `:defun?` lists all user-defined functions, `:defun '-func'` removes functions
74
+ * `:info` shows introduction and feature overview
45
75
  * `:version` Shows the rsh version number and the last published gem file version
46
- * `:help` will display this help text
76
+ * `:help` will display a compact command reference in two columns
77
+
78
+ Background jobs:
79
+ * Use `command &` to run commands in background
80
+ * Use `:jobs` to list active background jobs
81
+ * Use `:fg` or `:fg job_id` to bring jobs to foreground
82
+ * Use `Ctrl-Z` to suspend running jobs, `:bg job_id` to resume them
47
83
 
48
84
  ## Moving around
49
85
  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`.
@@ -60,7 +96,60 @@ Hitting Shift-TAB will do a similar search through the command history - but wit
60
96
  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
97
 
62
98
  ## 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.
99
+ 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").
100
+
101
+ ## Ruby Functions - The Power Feature ⭐
102
+
103
+ rsh's unique Ruby functions let you define custom shell commands using the full power of Ruby:
104
+
105
+ ### Basic Examples
106
+ ```bash
107
+ # File operations
108
+ :defun 'count(*args) = puts Dir.glob(args[0] || "*").length'
109
+ count *.rb
110
+
111
+ # System monitoring
112
+ :defun 'mem = puts `free -h`.lines[1].split[2]'
113
+ mem
114
+
115
+ # JSON pretty-printing
116
+ :defun 'jsonpp(file) = require "json"; puts JSON.pretty_generate(JSON.parse(File.read(file)))'
117
+ jsonpp config.json
118
+ ```
119
+
120
+ ### Advanced Examples
121
+ ```bash
122
+ # Network tools
123
+ :defun 'ports = puts `netstat -tlnp`.lines.grep(/LISTEN/).map{|l| l.split[3]}'
124
+ ports
125
+
126
+ # Git helpers
127
+ :defun 'branches = puts `git branch`.lines.map{|l| l.strip.sub("* ", "")}'
128
+ branches
129
+
130
+ # Directory analysis
131
+ :defun 'sizes(*args) = Dir.glob(args[0] || "*").each{|f| puts "#{File.size(f).to_s.rjust(8)} #{f}" if File.file?(f)}'
132
+ sizes
133
+
134
+ # Weather (using external API)
135
+ :defun 'weather(*args) = system("curl -s wttr.in/#{args[0] || \"oslo\"}")'
136
+ weather london
137
+ ```
138
+
139
+ ### Function Management
140
+ ```bash
141
+ :defun? # List all defined functions
142
+ :defun '-myls' # Remove a function
143
+ ```
144
+
145
+ Ruby functions have access to:
146
+ - Full Ruby standard library
147
+ - Shell environment variables via `ENV`
148
+ - rsh internals like `@history`, `@dirs`
149
+ - File system operations
150
+ - Network requests
151
+ - JSON/XML parsing
152
+ - And everything else Ruby can do!
64
153
 
65
154
  ## Integrations
66
155
  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.8.0" # Feature release: nickdel functions, improved help system, info command
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,19 +107,23 @@ 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
143
116
 
144
117
  # HELP TEXT
145
- @help = <<~HELP
118
+ @info = <<~INFO
146
119
 
147
120
  Hello #{@user}, welcome to rsh - the Ruby SHell.
148
121
 
149
122
  rsh does not attempt to compete with the grand old shells like bash and zsh.
150
123
  It serves the specific needs and wants of its author. If you like it, then feel free
151
- to ask for more or different features here: https://github.com/isene/rsh. Features:
152
-
124
+ to ask for more or different features here: https://github.com/isene/rsh.
125
+
126
+ Features:
153
127
  * Aliases (called nicks in rsh) - both for commands and general nicks
154
128
  * Syntax highlighting, matching nicks, system commands and valid dirs/files
155
129
  * Tab completions for nicks, system commands, command switches and dirs/files
@@ -164,26 +138,18 @@ end
164
138
  * rsh specific commands and full set of Ruby commands available via :<command>
165
139
  * All colors are themeable in .rshrc (see github link for possibilities)
166
140
  * Copy current command line to primary selection (paste w/middle button) with `Ctrl-y`
141
+
142
+ Use `:help` for command reference.
143
+
144
+ INFO
167
145
 
168
- Special functions/integrations:
169
- * Use `r` to launch rtfm (https://github.com/isene/RTFM) - if you have it installed
170
- * Use `f` to launch fzf (https://github.com/junegunn/fzf) - if you have it installed
171
- * Use `=` followed by xrpn commands separated by commas or double-spaces (https://github.com/isene/xrpn)
172
- * Use `:` followed by a Ruby expression to access the whole world of Ruby
146
+ @help = <<~HELP
173
147
 
174
- Special commands:
175
- * `:nick 'll = ls -l'` to make a command alias (ll) point to a command (ls -l)
176
- * `:gnick 'h = /home/me'` to make a general alias (h) point to something (/home/me)
177
- * `:nick?` will list all command nicks and general nicks (you can edit your nicks in .rshrc)
178
- * `:history` will list the command history, while `:rmhistory` will delete the history
179
- * `:version` Shows the rsh version number and the last published gem file version
180
- * `:help` will display this help text
181
-
182
148
  HELP
183
149
 
184
150
  # GENERIC FUNCTIONS
185
151
  def firstrun
186
- puts @help
152
+ puts @info
187
153
  puts "Since there is no rsh configuration file (.rshrc), I will help you set it up to suit your needs.\n\n"
188
154
  puts "The prompt you see now is the very basic rsh prompt:"
189
155
  print "#{@prompt} (press ENTER)"
@@ -254,13 +220,13 @@ def getchr # Process key presses
254
220
  when "" then chr = "C-T"
255
221
  when "" then chr = "C-Y"
256
222
  when "" then chr = "WBACK"
223
+ when "\u001A" then chr = "C-Z"
257
224
  when "" then chr = "LDEL"
258
225
  when "\r" then chr = "ENTER"
259
226
  when "\t" then chr = "TAB"
260
227
  when /[[:print:]]/ then chr = c
261
228
  else chr = ""
262
229
  end
263
- #stdin_clear
264
230
  return chr
265
231
  end
266
232
  def getstr # A custom Readline-like function
@@ -398,6 +364,14 @@ def getstr # A custom Readline-like function
398
364
  when 'C-Y' # Copy command line to primary selection
399
365
  system("echo -n '#{@history[0]}' | xclip")
400
366
  puts "\n#{Time.now.strftime("%H:%M:%S")}: Copied to primary selection (paste with middle buttoni)".c(@c_stamp)
367
+ when 'C-Z' # Suspend current process (background job)
368
+ if @current_pid
369
+ puts "\n[#{@job_id}] Suspended #{@current_pid}"
370
+ Process.kill("STOP", @current_pid)
371
+ @jobs[@job_id] = {pid: @current_pid, cmd: @cmd, status: :stopped}
372
+ else
373
+ puts "\nNo active job to suspend"
374
+ end
401
375
  when 'C-K' # Kill/delete that entry in the history
402
376
  @history.delete_at(@stk)
403
377
  @stk -= 1
@@ -586,6 +560,8 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic
586
560
  el.c(@c_nick)
587
561
  elsif @gnick.include?(el)
588
562
  el.c(@c_gnick)
563
+ elsif self.respond_to?(el) && singleton_class.instance_methods(false).include?(el.to_sym)
564
+ el.c(@c_nick).b # Ruby functions in bold nick color
589
565
  elsif el[0] == "-"
590
566
  el.c(@c_switch)
591
567
  else
@@ -612,7 +588,93 @@ end
612
588
 
613
589
  # RSH FUNCTIONS
614
590
  def help
615
- puts @help
591
+ # Get terminal width
592
+ term_width = @maxcol || 80
593
+ col_width = 48 # Fixed width for left column
594
+
595
+ # Helper function to strip ANSI codes for length calculation
596
+ def strip_ansi(str)
597
+ str.gsub(/\001?\e\[[0-9;]*m\002?/, '')
598
+ end
599
+
600
+ left_col = []
601
+ right_col = []
602
+
603
+ # Left column content
604
+ left_col << "KEYBOARD SHORTCUTS:".c(@c_prompt).b
605
+ left_col << "RIGHT/Ctrl-F Accept suggestion"
606
+ left_col << "UP/DOWN Navigate history"
607
+ left_col << "TAB Tab complete"
608
+ left_col << "Ctrl-Y Copy to clipboard"
609
+ left_col << "Ctrl-D Exit + save .rshrc"
610
+ left_col << "Ctrl-E Exit without save"
611
+ left_col << "Ctrl-L Clear screen"
612
+ left_col << "Ctrl-Z Suspend job"
613
+ left_col << "Ctrl-C/G Clear line"
614
+ left_col << "Ctrl-K Delete history item"
615
+ left_col << "Ctrl-U Clear line"
616
+ left_col << "Ctrl-W Delete previous word"
617
+ left_col << ""
618
+ left_col << "SPECIAL COMMANDS:".c(@c_prompt).b
619
+ left_col << ":nick 'll = ls -l' Command alias"
620
+ left_col << ":gnick 'h = /home' General alias"
621
+ left_col << ":nickdel 'name' Delete nick"
622
+ left_col << ":gnickdel 'name' Delete gnick"
623
+ left_col << ":nick? List all nicks"
624
+ left_col << ":history Show history"
625
+ left_col << ":rmhistory Clear history"
626
+ left_col << ":info About rsh"
627
+ left_col << ":version Version info"
628
+ left_col << ":help This help"
629
+
630
+ # Right column content
631
+ right_col << "RUBY FUNCTIONS:".c(@c_prompt).b
632
+ right_col << ":defun 'f(x) = x*2' Define function"
633
+ right_col << ":defun? List functions"
634
+ right_col << ":defun '-f' Remove function"
635
+ right_col << "Call as: f 5 (returns 10)"
636
+ right_col << ""
637
+ right_col << "JOB CONTROL:".c(@c_prompt).b
638
+ right_col << "command & Background job"
639
+ right_col << ":jobs List jobs"
640
+ right_col << ":fg [id] Foreground job"
641
+ right_col << ":bg [id] Resume in bg"
642
+ right_col << ""
643
+ right_col << "INTEGRATIONS:".c(@c_prompt).b
644
+ right_col << "r Launch rtfm"
645
+ right_col << "f Launch fzf"
646
+ right_col << "= <expr> xrpn calculator"
647
+ right_col << ":<ruby code> Execute Ruby"
648
+ right_col << ""
649
+ right_col << "EXPANSIONS:".c(@c_prompt).b
650
+ right_col << "~ Home directory"
651
+ right_col << "$VAR, ${VAR} Environment var"
652
+ right_col << "$? Exit status"
653
+ right_col << "$(cmd), `cmd` Command subst"
654
+ right_col << "{a,b,c} Brace expansion"
655
+ right_col << "cmd1 && cmd2 Conditional"
656
+ right_col << "cmd1 || cmd2 Alternative"
657
+
658
+ # Pad columns to same length
659
+ max_lines = [left_col.length, right_col.length].max
660
+ left_col.fill("", left_col.length...max_lines)
661
+ right_col.fill("", right_col.length...max_lines)
662
+
663
+ # Print in two columns
664
+ puts
665
+ max_lines.times do |i|
666
+ left_text = left_col[i].to_s
667
+ right_text = right_col[i].to_s
668
+ # Calculate padding based on visible characters (without ANSI codes)
669
+ visible_length = strip_ansi(left_text).length
670
+ padding = col_width - visible_length
671
+ padding = 0 if padding < 0
672
+ puts " #{left_text}#{' ' * padding} #{right_text}"
673
+ end
674
+ puts
675
+ end
676
+ def info
677
+ puts @info
616
678
  end
617
679
  def version
618
680
  puts "rsh version = #{@version} (latest RubyGems version is #{Gem.latest_version_for("ruby-shell").version} - https://github.com/isene/rsh)"
@@ -653,18 +715,181 @@ def nick? # Show nicks
653
715
  puts " General nicks:".c(@c_gnick)
654
716
  @gnick.sort.each {|key, value| puts " #{key} = #{value}"}
655
717
  end
718
+ def nickdel(nick_name) # Delete a command nick
719
+ @nick.delete(nick_name)
720
+ rshrc
721
+ puts "Nick '#{nick_name}' deleted"
722
+ end
723
+ def gnickdel(nick_name) # Delete a general/global nick
724
+ @gnick.delete(nick_name)
725
+ rshrc
726
+ puts "General nick '#{nick_name}' deleted"
727
+ end
656
728
  def dirs
657
729
  puts "Past direactories:"
658
730
  @dirs.each_with_index do |e,i|
659
731
  puts "#{i}: #{e}"
660
732
  end
661
733
  end
734
+ def jobs
735
+ puts "Active jobs:"
736
+ @jobs.each do |id, job|
737
+ begin
738
+ Process.kill(0, job[:pid]) # Check if process exists
739
+ puts "[#{id}] #{job[:pid]} #{job[:status]} #{job[:cmd]}"
740
+ rescue Errno::ESRCH
741
+ @jobs.delete(id) # Clean up dead jobs
742
+ end
743
+ end
744
+ end
745
+ def fg(job_id = nil)
746
+ job_id ||= @jobs.keys.max
747
+ return puts "No jobs" if job_id.nil?
748
+ job = @jobs[job_id]
749
+ return puts "Job #{job_id} not found" unless job
750
+ puts "Bringing job #{job_id} to foreground: #{job[:cmd]}"
751
+ begin
752
+ if job[:status] == :stopped
753
+ Process.kill("CONT", job[:pid])
754
+ end
755
+ @current_pid = job[:pid]
756
+ Process.wait(job[:pid])
757
+ @jobs.delete(job_id)
758
+ @current_pid = nil
759
+ rescue Errno::ECHILD, Errno::ESRCH
760
+ @jobs.delete(job_id)
761
+ @current_pid = nil
762
+ end
763
+ end
764
+ def bg(job_id = nil)
765
+ job_id ||= @jobs.keys.max
766
+ return puts "No jobs" if job_id.nil?
767
+ job = @jobs[job_id]
768
+ return puts "Job #{job_id} not found" unless job
769
+ return puts "Job #{job_id} already running" if job[:status] == :running
770
+ puts "Resuming job #{job_id} in background: #{job[:cmd]}"
771
+ begin
772
+ Process.kill("CONT", job[:pid])
773
+ @jobs[job_id][:status] = :running
774
+ rescue Errno::ESRCH
775
+ @jobs.delete(job_id)
776
+ puts "Job #{job_id} no longer exists"
777
+ end
778
+ end
779
+ def defun(func_def) # Define a Ruby function like: `:defun "myls(*args) = Dir.glob('*').each {|f| puts f}"`
780
+ if func_def.match(/^\s*-/)
781
+ # Remove function
782
+ func_name = func_def.sub(/^\s*-/, '')
783
+ if self.respond_to?(func_name)
784
+ singleton_class.remove_method(func_name.to_sym)
785
+ puts "Function '#{func_name}' removed"
786
+ else
787
+ puts "Function '#{func_name}' not found"
788
+ end
789
+ else
790
+ # Define function
791
+ # Extract function name, params, and body from "name(params) = body" format
792
+ if func_def =~ /^(\w+)\s*\(([^)]*)\)\s*=\s*(.+)$/
793
+ func_name = $1
794
+ func_params = $2
795
+ func_body = $3
796
+
797
+ begin
798
+ eval_code = "def #{func_name}(#{func_params}); #{func_body}; end"
799
+ puts " DEBUG: Evaluating: #{eval_code}" if ENV['RSH_DEBUG']
800
+ singleton_class.class_eval(eval_code)
801
+ puts "Function '#{func_name}' defined"
802
+ puts " DEBUG: Method created? #{respond_to?(func_name)}" if ENV['RSH_DEBUG']
803
+ rescue SyntaxError => e
804
+ puts "Syntax error in function definition: #{e}"
805
+ rescue => e
806
+ puts "Error in function definition: #{e}"
807
+ end
808
+ else
809
+ puts "Invalid function format. Use: name(params) = body"
810
+ end
811
+ end
812
+ rshrc
813
+ end
814
+ def defun? # Show all user-defined functions
815
+ puts "User-defined Ruby functions:"
816
+ # Get only methods defined by defun, excluding built-ins and rsh internals
817
+ all_methods = singleton_class.instance_methods(false)
818
+ puts " All singleton methods: #{all_methods}"
819
+ excluded = [:defun, :defun?, :execute_conditional, :expand_braces]
820
+ methods = all_methods - excluded
821
+ if methods.empty?
822
+ puts " (none defined after filtering)"
823
+ else
824
+ methods.each do |method|
825
+ puts " #{method}"
826
+ end
827
+ end
828
+ end
829
+ def execute_conditional(cmd_line)
830
+ # Split on && and || while preserving the operators
831
+ parts = cmd_line.split(/(\s*&&\s*|\s*\|\|\s*)/)
832
+
833
+ result = true
834
+ i = 0
835
+ while i < parts.length
836
+ command = parts[i].strip
837
+ next if command.empty?
838
+
839
+ if command == '&&'
840
+ i += 1
841
+ next unless result # Skip if previous command failed
842
+ elsif command == '||'
843
+ i += 1
844
+ next if result # Skip if previous command succeeded
845
+ else
846
+ # Execute the command
847
+ success = system(command)
848
+ result = success
849
+ puts " Command failed: #{command} (exit #{$?.exitstatus})" unless success
850
+ end
851
+ i += 1
852
+ end
853
+ end
854
+ def expand_braces(str)
855
+ # Simple brace expansion: {a,b,c} -> a b c
856
+ str.gsub(/\{([^}]+)\}/) do |match|
857
+ items = $1.split(',').map(&:strip)
858
+ items.join(' ')
859
+ end
860
+ end
662
861
 
663
862
  # INITIAL SETUP
664
863
  begin # Load .rshrc and populate @history
665
864
  trap "SIGINT" do end
865
+ trap "SIGHUP" do
866
+ rshrc
867
+ exit
868
+ end
869
+ trap "SIGTERM" do
870
+ rshrc
871
+ exit
872
+ end
666
873
  firstrun unless File.exist?(Dir.home+'/.rshrc') # Initial loading - to get history
667
- load(Dir.home+'/.rshrc')
874
+ load(Dir.home+'/.rshrc')
875
+ # Load login shell files if rsh is running as login shell
876
+ if ENV['LOGIN_SHELL'] or $0 == "-rsh" or ARGV.include?('-l') or ARGV.include?('--login')
877
+ ['/etc/profile', Dir.home+'/.profile', Dir.home+'/.bash_profile', Dir.home+'/.bashrc'].each do |f|
878
+ if File.exist?(f)
879
+ puts "Loading #{f}..." if ENV['RSH_DEBUG']
880
+ begin
881
+ # Source shell files by extracting export statements
882
+ File.readlines(f).each do |line|
883
+ if line =~ /^\s*export\s+(\w+)=(.*)/
884
+ ENV[$1] = $2.gsub(/['"]/, '')
885
+ end
886
+ end
887
+ rescue => e
888
+ puts "Warning: Could not source #{f}: #{e}" if ENV['RSH_DEBUG']
889
+ end
890
+ end
891
+ end
892
+ end
668
893
  ENV["SHELL"] = __FILE__
669
894
  ENV["TERM"] = "rxvt-unicode-256color"
670
895
  ENV["PATH"] ? ENV["PATH"] += ":" : ENV["PATH"] = ""
@@ -736,41 +961,92 @@ loop do
736
961
  elsif @cmd == '#' # List previous directories
737
962
  dirs
738
963
  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")
964
+ # Check if it's a user-defined Ruby function FIRST (before any expansions)
965
+ cmd_parts = @cmd.split(/\s+/)
966
+ func_name = cmd_parts[0]
967
+ if self.respond_to?(func_name) && singleton_class.instance_methods(false).include?(func_name.to_sym)
968
+ begin
969
+ args = cmd_parts[1..]
970
+ puts "DEBUG: Calling #{func_name} with args: #{args}" if ENV['RSH_DEBUG']
971
+ result = self.send(func_name, *args)
972
+ puts "DEBUG: Result: #{result.inspect}" if ENV['RSH_DEBUG']
973
+ puts result unless result.nil?
974
+ rescue => e
975
+ puts "Error calling function '#{func_name}': #{e}"
976
+ end
752
977
  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") }
978
+ # Handle conditional execution (&& and ||)
979
+ if @cmd.include?('&&') || @cmd.include?('||')
980
+ execute_conditional(@cmd)
981
+ next
982
+ end
983
+ # Expand brace expansion {a,b,c}
984
+ @cmd = expand_braces(@cmd)
985
+ # Expand command substitution $(command) and backticks
986
+ @cmd = @cmd.gsub(/\$\(([^)]+)\)/) { `#{$1}`.chomp }
987
+ @cmd = @cmd.gsub(/`([^`]+)`/) { `#{$1}`.chomp }
988
+ # Expand environment variables and exit status
989
+ @cmd = @cmd.gsub(/\$\?/) { @last_exit.to_s }
990
+ @cmd = @cmd.gsub(/\$(\w+)|\$\{(\w+)\}/) { ENV[$1 || $2] || '' }
991
+ # Expand tilde
992
+ @cmd = @cmd.gsub(/~/, Dir.home)
993
+ ca = @nick.transform_keys {|k| /((^\K\s*\K)|(\|\K\s*\K))\b(?<!-)#{Regexp.escape k}\b/}
994
+ @cmd = @cmd.gsub(Regexp.union(ca.keys), @nick)
995
+ ga = @gnick.transform_keys {|k| /\b(?<!-)#{Regexp.escape k}\b/}
996
+ @cmd = @cmd.gsub(Regexp.union(ga.keys), @gnick)
997
+ @cmd = "~" if @cmd == "cd"
998
+ @cmd.sub!(/^cd (\S*).*/, '\1')
999
+ @cmd = Dir.home if @cmd == "~"
1000
+ @cmd = @dirs[1] if @cmd == "-"
1001
+ @cmd = @dirs[@cmd.to_i] if @cmd =~ /^\d$/
1002
+ # Check if it's a directory to change to
1003
+ dir = @cmd.strip.sub(/~/, Dir.home)
1004
+ if Dir.exist?(dir)
1005
+ Dir.chdir(dir)
1006
+ system("git status .") if Dir.exist?(".git")
1007
+ else
1008
+ puts "#{Time.now.strftime("%H:%M:%S")}: #{@cmd}".c(@c_stamp)
1009
+ if @cmd == "f" # fzf integration (https://github.com/junegunn/fzf)
1010
+ res = `fzf`.chomp
1011
+ Dir.chdir(File.dirname(res))
1012
+ elsif File.exist?(@cmd) and not File.executable?(@cmd) and not @cmd.include?(" ")
1013
+ # Only auto-open files if it's a single filename (no spaces = no command with args)
1014
+ if File.read(@cmd).force_encoding("UTF-8").valid_encoding?
1015
+ system("#{ENV['EDITOR']} #{@cmd}") # Try open with user's editor
763
1016
  else
764
- Thread.new { system("xdg-open #{@cmd} 2>/dev/null") }
1017
+ if @runmailcap
1018
+ Thread.new { system("run-mailcap #{@cmd} 2>/dev/null") }
1019
+ else
1020
+ Thread.new { system("xdg-open #{@cmd} 2>/dev/null") }
1021
+ end
1022
+ end
1023
+ else
1024
+ begin
1025
+ pre_cmd
1026
+ # Handle background jobs
1027
+ if @cmd.end_with?(' &')
1028
+ @cmd = @cmd[0..-3] # Remove the &
1029
+ @job_id += 1
1030
+ # Handle pipes and redirections in background
1031
+ if @cmd.include?('|') || @cmd.include?('>') || @cmd.include?('<')
1032
+ pid = spawn(@cmd, pgroup: true)
1033
+ else
1034
+ pid = spawn(@cmd)
1035
+ end
1036
+ @jobs[@job_id] = {pid: pid, cmd: @cmd, status: :running}
1037
+ puts "[#{@job_id}] #{pid} #{@cmd}"
1038
+ else
1039
+ # Better handling of pipes and redirections
1040
+ @current_pid = spawn(@cmd)
1041
+ Process.wait(@current_pid)
1042
+ @last_exit = $?.exitstatus
1043
+ @current_pid = nil
1044
+ puts " Command failed: #{@cmd} (exit #{@last_exit})" unless @last_exit == 0
1045
+ end
1046
+ post_cmd
1047
+ rescue StandardError => err
1048
+ puts "\nError: #{err}"
765
1049
  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
1050
  end
775
1051
  end
776
1052
  end
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: 2.6.2
4
+ version: 2.8.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-08 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. UPDATE
15
+ v2.8.0: Enhanced help system with two-column display, new :info command, :nickdel/:gnickdel
16
+ commands for easier nick management. v2.7.0: Ruby Functions - define custom shell
17
+ commands using full Ruby power! Also: job control, command substitution, variable
18
+ expansion, conditional execution, and login shell support.'
18
19
  email: g@isene.com
19
20
  executables:
20
21
  - rsh