ruby-shell 1.2 → 2.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 (3) hide show
  1. checksums.yaml +4 -4
  2. data/bin/rsh +153 -136
  3. metadata +4 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d3646331db7eac6ea26f07f0878354ca9b3ed9f46ebda4e2bd177d252ce67d3
4
- data.tar.gz: deffdb053c7284ed35110c482a30adfd0ec732ce5eb0d2ea4188638cf92d1b0e
3
+ metadata.gz: 0ec039977b048309a3b3762f8b009b83820693ec99857c9ce700322339dbecf5
4
+ data.tar.gz: 35498c5cdd7dd2eea545b85d994370dca0cfadb74415eb28bf0169f46673c425
5
5
  SHA512:
6
- metadata.gz: 6dec4e805bf977dd4de28784a1e1bc4c71c7df0c865c73e479e54d053de0743b9592c4c1d331e321da23d1c1e4136709fd7fb269acb372dac50d4aa3f1196737
7
- data.tar.gz: d50d00d2f0f9af08c1516941a808087936d512f43971eb2374b476c295137048c95bb0890960589dfdc35b9b402147d0c9d162ee1b1d75b0e6ae8b2bc541c7ed
6
+ metadata.gz: b262ececc24a31380445f8e88e645cccbef5bb4ce043143d4268c6a116bbe4df3293cd5affd26b036be1d58af7bf4fd653b1f1747a78419e40a3ef0e34d41380
7
+ data.tar.gz: 9de2ac401010abd356a3b1aefca9848de07ba1829eb45c958a963b40d78825449de04737b9e0cf78da5ba2f1530e5306656830d3c5a4ae61d6635ccb749abe6c
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 = "1.2"
11
+ @version = "2.0"
12
12
 
13
13
  # MODULES, CLASSES AND EXTENSIONS
14
14
  class String # Add coloring to strings (with escaping for Readline)
@@ -88,6 +88,12 @@ module Cursor # Terminal cursor movement ANSI codes (thanks to https://github.co
88
88
  print(CSI + 'J')
89
89
  end
90
90
  end
91
+ def stdin_clear
92
+ begin
93
+ $stdin.getc while $stdin.ready?
94
+ rescue
95
+ end
96
+ end
91
97
 
92
98
  # INITIALIZATION
93
99
  begin # Requires
@@ -174,7 +180,7 @@ def firstrun
174
180
  puts "Since there is no rsh configuration file (.rshrc), I will help you set it up to suit your needs.\n\n"
175
181
  puts "The prompt you see now is the very basic rsh prompt:"
176
182
  print "#{@prompt} (press ENTER)"
177
- STDIN.gets
183
+ $stdin.gets
178
184
  puts "\nI will now change the prompt into something more useful."
179
185
  puts "Feel free to amend the prompt in your .rshrc.\n\n"
180
186
  rc = <<~RSHRC
@@ -199,27 +205,28 @@ RSHRC
199
205
  File.write(Dir.home+'/.rshrc', rc)
200
206
  end
201
207
  def getchr # Process key presses
202
- c = STDIN.getch
208
+ c = $stdin.getch
203
209
  case c
204
- when "\e" # ANSI escape sequences
205
- case STDIN.getc
210
+ when "\e" # ANSI escape sequences (with only ESC, it should stop right here)
211
+ return "ESC" if $stdin.ready? == nil
212
+ case $stdin.getc
206
213
  when '[' # CSI
207
- case STDIN.getc
214
+ case $stdin.getc # Will get (or ASK) for more (remaining part of special character)
208
215
  when 'A' then chr = "UP"
209
216
  when 'B' then chr = "DOWN"
210
217
  when 'C' then chr = "RIGHT"
211
218
  when 'D' then chr = "LEFT"
212
219
  when 'Z' then chr = "S-TAB"
213
- when '2' then chr = "INS" ; chr = "C-INS" if STDIN.getc == "^"
214
- when '3' then chr = "DEL" ; chr = "C-DEL" if STDIN.getc == "^"
215
- when '5' then chr = "PgUP" ; chr = "C-PgUP" if STDIN.getc == "^"
216
- when '6' then chr = "PgDOWN" ; chr = "C-PgDOWN" if STDIN.getc == "^"
217
- when '7' then chr = "HOME" ; chr = "C-HOME" if STDIN.getc == "^"
218
- when '8' then chr = "END" ; chr = "C-END" if STDIN.getc == "^"
220
+ when '2' then chr = "INS" ; chr = "C-INS" if $stdin.getc == "^"
221
+ when '3' then chr = "DEL" ; chr = "C-DEL" if $stdin.getc == "^"
222
+ when '5' then chr = "PgUP" ; chr = "C-PgUP" if $stdin.getc == "^"
223
+ when '6' then chr = "PgDOWN" ; chr = "C-PgDOWN" if $stdin.getc == "^"
224
+ when '7' then chr = "HOME" ; chr = "C-HOME" if $stdin.getc == "^"
225
+ when '8' then chr = "END" ; chr = "C-END" if $stdin.getc == "^"
219
226
  else chr = ""
220
227
  end
221
228
  when 'O' # Set Ctrl+ArrowKey equal to ArrowKey; May be used for other purposes in the future
222
- case STDIN.getc
229
+ case $stdin.getc
223
230
  when 'a' then chr = "C-UP"
224
231
  when 'b' then chr = "C-DOWN"
225
232
  when 'c' then chr = "C-RIGHT"
@@ -246,6 +253,7 @@ def getchr # Process key presses
246
253
  when /[[:print:]]/ then chr = c
247
254
  else chr = ""
248
255
  end
256
+ stdin_clear
249
257
  return chr
250
258
  end
251
259
  def getstr # A custom Readline-like function
@@ -260,11 +268,15 @@ def getstr # A custom Readline-like function
260
268
  @c.clear_line
261
269
  print @prompt
262
270
  row, @pos0 = @c.pos
271
+ #@history[0] = "" if @history[0].nil?
263
272
  print cmd_check(@history[0])
264
- @ci = @history[1..].find_index {|e| e =~ /^#{Regexp.escape(@history[0])}./}
265
- @ci += 1 unless @ci == nil
266
- if @history[0].length > 1 and @ci
267
- print @history[@ci][@history[0].length..].to_s.c(@c_stamp)
273
+ @ci = @history[1..].find_index {|e| e =~ /^#{Regexp.escape(@history[0].to_s)}./}
274
+ unless @ci == nil
275
+ @ci += 1
276
+ @ciprompt = @history[@ci][@history[0].to_s.length..].to_s
277
+ end
278
+ if @history[0].to_s.length > 1 and @ci
279
+ print @ciprompt.c(@c_stamp)
268
280
  right = true
269
281
  end
270
282
  @c.col(@pos0 + @pos)
@@ -283,10 +295,9 @@ def getstr # A custom Readline-like function
283
295
  @c.row(1)
284
296
  @c.clear_screen_down
285
297
  when 'UP' # Go up in history
286
- #@history[0] = @history[@history[(@stk+1)..].index{|h| h =~ /#{@history[0]}/}+1] #Future magick
287
298
  if @stk == 0 and @history[0].length > 0
288
299
  @tabsearch = @history[0]
289
- tabbing("hist")
300
+ tab("hist")
290
301
  else
291
302
  if lift
292
303
  @history.unshift("")
@@ -296,6 +307,7 @@ def getstr # A custom Readline-like function
296
307
  unless @stk >= @history.length - 1
297
308
  @stk += 1
298
309
  @history[0] = @history[@stk].dup
310
+ @history[0] = "" unless @history[0]
299
311
  @pos = @history[0].length
300
312
  end
301
313
  lift = false
@@ -368,6 +380,7 @@ def getstr # A custom Readline-like function
368
380
  @pos = 0
369
381
  else
370
382
  @history[0] = @history[@stk].dup
383
+ @history[0] = "" unless @history[0]
371
384
  @pos = @history[0].length
372
385
  end
373
386
  when 'LDEL' # Delete readline (Ctrl-U)
@@ -376,7 +389,8 @@ def getstr # A custom Readline-like function
376
389
  lift = true
377
390
  when 'TAB' # Tab completion of dirs and files
378
391
  @ci = nil
379
- @tabsearch =~ /^-/ ? tabbing("switch") : tabbing("all")
392
+ #@tabsearch =~ /^-/ ? tabbing("switch") : tabbing("all")
393
+ tab("all")
380
394
  lift = true
381
395
  when 'S-TAB'
382
396
  @ci = nil
@@ -387,138 +401,133 @@ def getstr # A custom Readline-like function
387
401
  @pos += 1
388
402
  lift = true
389
403
  end
390
- while STDIN.ready?
391
- chr = STDIN.getc
404
+ while $stdin.ready?
405
+ chr = $stdin.getc
392
406
  @history[0].insert(@pos,chr)
393
407
  @pos += 1
394
408
  end
395
409
  end
410
+ @c.col(@pos0 + @history[0].length)
411
+ @c.clear_screen_down
396
412
  end
397
- def tabbing(type)
398
- @tabstr = @history[@stk][0...@pos]
399
- @tabend = @history[@stk][@pos..]
400
- elements = @tabstr.split(" ")
401
- if @tabstr.match(" $")
402
- elements.append("")
403
- @tabsearch = ""
404
- else
405
- @tabsearch = elements.last.to_s
406
- @tabstr = @tabstr[0...-@tabsearch.length]
407
- end
408
- i = elements.length - 1
409
- if @tabsearch =~ /^-/
410
- until i == 0
411
- i -= 1
412
- if elements[i] !~ /^-/
413
- tab_switch(elements[i])
414
- break
415
- end
416
- end
417
- elsif type == 'all'
418
- tab_all(@tabsearch)
419
- elsif type == 'hist'
420
- tab_hist(@tabsearch)
421
- end
422
- @history[@stk] = @tabstr.to_s + @tabsearch.to_s + @tabend.to_s
423
- end
424
- def tab_all(str) # TAB completion for Dirs/files, nicks and commands
425
- exe = []
426
- @path.each do |p|
427
- Dir.glob(p).each do |c|
428
- exe.append(File.basename(c)) if File.executable?(c) and not Dir.exist?(c)
429
- end
430
- end
431
- exe.prepend(*@nick.keys)
432
- exe.prepend(*@gnick.keys)
433
- compl = exe.select {|s| s =~ Regexp.new("^" + str)}
434
- fdir = str
435
- fdir += "/" if Dir.exist?(fdir)
436
- fdir += "*"
437
- compl.prepend(*Dir.glob(fdir))
438
- match = tabselect(compl) unless compl.empty?
439
- @tabsearch = match if match
440
- @pos = @tabstr.length + @tabsearch.length if match
441
- end
442
- def tab_switch(str) # TAB completion for command switches (TAB after "-")
443
- begin
444
- hlp = `#{str} --help 2>/dev/null`
445
- hlp = hlp.split("\n").grep(/^\s*-{1,2}[^-]/)
446
- hlp = hlp.map{|h| h.sub(/^\s*/, '').sub(/^--/, ' --')}
447
- switch = tabselect(hlp)
448
- switch = switch.sub(/ .*/, '').sub(/,/, '')
449
- @tabsearch = switch if switch
450
- @pos = @tabstr.length + @tabsearch.length
451
- rescue
452
- end
453
- end
454
- def tab_hist(str)
455
- sel = @history.select {|el| el =~ /#{str}/}
456
- sel.shift
457
- sel.delete("")
458
- hist = tabselect(sel, true)
459
- if hist
460
- @tabsearch = hist
461
- @tabstr = ""
462
- @pos = @tabsearch.length
463
- end
464
- end
465
- def tabselect(ary, hist=false) # Let user select from the incoming array
466
- ary.uniq!
467
- @c_row, @c_col = @c.pos
468
- chr = ""
413
+
414
+ def tab(type)
469
415
  i = 0
416
+ chr = ""
417
+ @tabarray = []
418
+ @pretab = @history[0][0...@pos].to_s # Extract the current line up to cursor
419
+ @postab = @history[0][@pos..].to_s # Extract the current line from cursor to end
420
+ @c_row, @c_col = @c.pos # Get cursor position
421
+ @tabstr = @pretab.split(/[|, ]/).last.to_s # Get the sustring that is being tab completed
422
+ @tabstr = "" if @pretab[-1] =~ /[ |]/
423
+ @pretab = @pretab.delete_suffix(@tabstr)
424
+ type = "switch" if @tabstr[0] == "-"
470
425
  while chr != "ENTER"
471
- @c.clear_screen_down
472
- ary.length - i < 5 ? l = ary.length - i : l = 5
473
- l.times do |x|
474
- tl = @tabsearch.length
475
- if x == 0
476
- @c.clear_line
477
- tabchoice = ary[i].sub(/^ *(.*?)[ ,].*/, '\1')
478
- tabline = "#{@prompt}#{cmd_check(@tabstr)}#{tabchoice.c(@c_tabselect)}#{@tabend}"
479
- print tabline # Full command line
480
- @c_col = @pos0 + @tabstr.length + tabchoice.length
481
- nextline
482
- print " " + ary[i].sub(/(.*?)#{@tabsearch}(.*)/, '\1'.c(@c_tabselect) + @tabsearch + '\2'.c(@c_tabselect))
426
+ case type
427
+ when "hist" # Handle history completions ('UP' key)
428
+ @tabarray = @history.select {|el| el =~ /#{@tabstr}/} # Select history items matching @tabstr
429
+ @tabarray.shift # Take away @history[0]
430
+ return if @tabarray.empty?
431
+ when "switch"
432
+ cmdswitch = @pretab.split(/[|, ]/).last.to_s
433
+ hlp = `#{cmdswitch} --help 2>/dev/null`
434
+ hlp = hlp.split("\n").grep(/^\s*-{1,2}[^-]/)
435
+ hlp = hlp.map{|h| h.sub(/^\s*/, '').sub(/^--/, ' --')}
436
+ hlp = hlp.reject{|h| /-</ =~ h}
437
+ @tabarray = hlp
438
+ when "all" # Handle all other tab completions
439
+ exe = []
440
+ @path.each do |p| # Add all executables in @path
441
+ Dir.glob(p).each do |c|
442
+ exe.append(File.basename(c)) if File.executable?(c) and not Dir.exist?(c)
443
+ end
444
+ end
445
+ exe.sort!
446
+ exe.prepend(*@nick.keys) # Add nicks
447
+ exe.prepend(*@gnick.keys) # Add gnicks
448
+ compl = exe.select {|s| s =~ Regexp.new(@tabstr)} # Select only that which matches so far
449
+ fdir = @tabstr + "*"
450
+ compl.prepend(*Dir.glob(fdir)).map! do |e|
451
+ if e =~ /(?<!\\) /
452
+ e = e.sub(/(.*\/|^)(.*)/, '\1\'\2\'') unless e =~ /'/
453
+ end
454
+ Dir.exist?(e) ? e + "/" : e # Add matching dirs
455
+ end
456
+ @tabarray = compl # Finally put it into @tabarray
457
+ end
458
+ return if @tabarray.empty?
459
+ @tabarray.delete("") # Don't remember why
460
+ @c.clear_screen_down # Here we go
461
+ @tabarray.length.to_i - i < 5 ? l = @tabarray.length.to_i - i : l = 5 # Max 5 rows of completion items
462
+ l.times do |x| # Iterate through
463
+ if x == 0 # First item goes onto the commandline
464
+ @c.clear_line # Clear the line
465
+ tabchoice = @tabarray[i] # Select the item from the @tabarray
466
+ tabchoice = tabchoice.sub(/\s*(-.*?)[,\s].*/, '\1') if type == "switch"
467
+ @newhist0 = @pretab + tabchoice + @postab # Remember now the new value to be given to @history[0]
468
+ line1 = cmd_check(@pretab).to_s # Syntax highlight before @tabstr
469
+ line2 = cmd_check(@postab).to_s # Syntax highlight after @tabstr
470
+ # Color and underline the current tabchoice on the commandline:
471
+ tabline = tabchoice.sub(/(.*)#{@tabstr}(.*)/, '\1'.c(@c_tabselect) + @tabstr.u.c(@c_tabselect) + '\2'.c(@c_tabselect))
472
+ print @prompt + line1 + tabline + line2 # Print the commandline
473
+ @pos = @pretab.length.to_i + tabchoice.length.to_i # Set the position on that commandline
474
+ @c_col = @pos0 + @pos # The cursor position must include the prompt as well
475
+ @c.col(@c_col) # Set the cursor position
476
+ nextline # Then start showing the completion items
477
+ tabline = @tabarray[i] # Get the next matching tabline
478
+ # Can't nest ANSI codes, they must each complete/conclude or they will mess eachother up
479
+ tabline1 = tabline.sub(/(.*?)#{@tabstr}.*/, '\1').c(@c_tabselect) # Color the part before the @tabstr
480
+ tabline2 = tabline.sub(/.*?#{@tabstr}(.*)/, '\1').c(@c_tabselect) # Color the part after the @tabstr
481
+ print " " + tabline1 + @tabstr.c(@c_tabselect).u + tabline2 # Color & underline @tabstr
483
482
  else
484
483
  begin
485
- print " " + ary[i+x].sub(/(.*?)#{@tabsearch}(.*)/, '\1'.c(@c_taboption) + @tabsearch + '\2'.c(@c_taboption))
484
+ tabline = @tabarray[i+x] # Next tabline, and next, etc (usually 4 times here)
485
+ tabline1 = tabline.sub(/(.*?)#{@tabstr}.*/, '\1').c(@c_taboption) # Color before @tabstr
486
+ tabline2 = tabline.sub(/.*?#{@tabstr}(.*)/, '\1').c(@c_taboption) # Color after @tabstr
487
+ print " " + tabline1 + @tabstr.c(@c_taboption).u + tabline2 # Print the whole line
486
488
  rescue
487
489
  end
488
490
  end
489
- nextline
491
+ nextline # To not run off screen
490
492
  end
491
- @c.row(@c_row)
492
- @c.col(@c_col)
493
- chr = getchr
494
- case chr
495
- when 'C-G', 'C-C'
496
- @c.clear_screen_down
497
- return @tabsearch
493
+ @c.row(@c_row) # Set cursor row to commandline
494
+ @c.col(@c_col) # Set cursor col on commandline
495
+ chr = getchr # Now get user input
496
+ case chr # Treat the keypress
497
+ when 'C-G', 'C-C', 'ESC'
498
+ tabend; return
498
499
  when 'DOWN'
499
- i += 1 unless i > ary.length - 2
500
+ i += 1 unless i > @tabarray.length.to_i - 2
500
501
  when 'UP'
501
502
  i -= 1 unless i == 0
502
- when 'TAB'
503
+ when 'TAB', 'RIGHT' # Effectively the same as ENTER
503
504
  chr = "ENTER"
504
- when 'BACK'
505
- @tabsearch.chop!
506
- if @tabsearch == ''
507
- @c.clear_screen_down
508
- return ""
505
+ when 'BACK', 'LEFT' # Delete one character to the left
506
+ if @tabstr == ""
507
+ @history[0] = @pretab + @postab
508
+ tabend
509
+ return
510
+ end
511
+ @tabstr.chop!
512
+ when 'WBACK' # Delete one word to the left (Ctrl-W)
513
+ if @tabstr == ""
514
+ @history[0] = @pretab + @postab
515
+ tabend
516
+ return
509
517
  end
510
- hist ? tab_hist(@tabsearch) : tab_all(@tabsearch)
511
- return @tabsearch
518
+ @tabstr.sub!(/#{@tabstr.split(/[|, ]/).last}.*/, '')
519
+ when ' '
520
+ @tabstr += " "
521
+ chr = "ENTER"
512
522
  when /^[[:print:]]$/
513
- @tabsearch += chr
514
- hist ? tab_hist(@tabsearch) : tab_all(@tabsearch)
515
- return @tabsearch
523
+ @tabstr += chr
524
+ i = 0
516
525
  end
517
526
  end
518
527
  @c.clear_screen_down
519
528
  @c.row(@c_row)
520
529
  @c.col(@c_col)
521
- return ary[i].sub(/^ */, '')
530
+ @history[0] = @newhist0
522
531
  end
523
532
  def nextline # Handle going to the next line in the terminal
524
533
  row, col = @c.pos
@@ -528,26 +537,33 @@ def nextline # Handle going to the next line in the terminal
528
537
  end
529
538
  @c.next_line
530
539
  end
540
+ def tabend
541
+ @c.clear_screen_down
542
+ @pos = @history[0].length
543
+ @c_col = @pos0 + @pos
544
+ @c.col(@c_col)
545
+ end
531
546
  def hist_clean # Clean up @history
532
547
  @history.uniq!
533
548
  @history.compact!
534
549
  @history.delete("")
535
550
  end
536
551
  def cmd_check(str) # Check if each element on the readline matches commands, nicks, paths; color them
537
- str.gsub(/\S+/) do |el|
552
+ return if str.nil?
553
+ str.gsub(/(?:\S'[^']*'|[^ '])+/) do |el|
538
554
  if @nick.include?(el)
539
555
  el.c(@c_nick)
540
- elsif el == "r"
556
+ elsif el == "r" or el == "f"
541
557
  el.c(@c_nick)
542
558
  elsif @gnick.include?(el)
543
559
  el.c(@c_gnick)
544
- elsif File.exist?(el.sub(/^~/, "/home/#{@user}"))
560
+ elsif File.exist?(el.gsub("'", ""))
545
561
  el.c(@c_path)
546
562
  elsif system "which #{el}", %i[out err] => File::NULL
547
563
  el.c(@c_cmd)
548
564
  elsif el == "cd"
549
565
  el.c(@c_cmd)
550
- elsif el =~ /^-/
566
+ elsif el[0] == "-"
551
567
  el.c(@c_switch)
552
568
  else
553
569
  el
@@ -568,7 +584,7 @@ def rshrc # Write updates to .rshrc
568
584
  conf.sub!(/^@history.*\n/, "")
569
585
  conf += "@history = #{@history.last(@histsize)}\n"
570
586
  File.write(Dir.home+'/.rshrc', conf)
571
- puts ".rshrc updated"
587
+ puts "\n.rshrc updated"
572
588
  end
573
589
 
574
590
  # RSH FUNCTIONS
@@ -654,6 +670,7 @@ loop do
654
670
  h = @history; load(Dir.home+'/.rshrc') if File.exist?(Dir.home+'/.rshrc'); @history = h # reload prompt but not history
655
671
  @prompt.gsub!(/#{Dir.home}/, '~') # Simplify path in prompt
656
672
  system("printf \"\033]0;rsh: #{Dir.pwd}\007\"") # Set Window title to path
673
+ @history[0] = "" unless @history[0]
657
674
  getstr # Main work is here
658
675
  @cmd = @history[0]
659
676
  @dirs.unshift(Dir.pwd)
@@ -721,7 +738,7 @@ loop do
721
738
  else
722
739
  begin
723
740
  pre_cmd
724
- puts "Not executed: #{@cmd}" unless system (@cmd) # Try execute the command
741
+ puts " Not executed: #{@cmd}" unless system (@cmd) # Try execute the command
725
742
  post_cmd
726
743
  rescue StandardError => err
727
744
  puts "\n#{err}"
@@ -729,8 +746,8 @@ loop do
729
746
  end
730
747
  end
731
748
  end
732
- rescue StandardError => err # Throw error nicely
733
- puts "\n#{err}"
749
+ #rescue StandardError => err # Throw error nicely
750
+ # puts "\n#{err}"
734
751
  end
735
752
  end
736
753
 
metadata CHANGED
@@ -1,18 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-shell
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.2'
4
+ version: '2.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-10-10 00:00:00.000000000 Z
11
+ date: 2024-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. In
15
- continual development. New in 1.2: Fixed bug on tab handling'
15
+ continual development. New in 2.0: Full rewrite of tab completion engine. Lots of
16
+ other bug fixes.'
16
17
  email: g@isene.com
17
18
  executables:
18
19
  - rsh