From 9ae167ac4aed6e505ac8855858767ea0c34b5f95 Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Fri, 2 Jan 2026 16:15:49 -0800 Subject: [PATCH] Start ditching wordexp in Ruby --- ruby/shell/builtins.rb | 8 +++- ruby/shell/repl.rb | 82 ++++++++++++++++++++++++++++++++++++----- ruby/test/shell_test.rb | 4 +- 3 files changed, 82 insertions(+), 12 deletions(-) diff --git a/ruby/shell/builtins.rb b/ruby/shell/builtins.rb index 4a39cd9..1259d57 100644 --- a/ruby/shell/builtins.rb +++ b/ruby/shell/builtins.rb @@ -51,7 +51,13 @@ module Shell end def builtin_cd(args) - Dir.chdir args.first + dir = args.first + if dir.nil? + Dir.chdir Dir.home + else + Dir.chdir dir + end + ENV["PWD"] = Dir.pwd 0 end diff --git a/ruby/shell/repl.rb b/ruby/shell/repl.rb index 94bf78c..2a95fe3 100644 --- a/ruby/shell/repl.rb +++ b/ruby/shell/repl.rb @@ -50,16 +50,21 @@ module Shell return 0 if line.strip.empty? # no input, no-op logger.verbose "Processing command: #{line.inspect}" - args = word_expander.expand(line) - cmd = args.shift - logger.verbose "Parsed command: #{cmd} #{args.inspect}" - if builtins.builtin?(cmd) - logger.verbose "Executing builtin #{cmd}" - builtins.exec(cmd, args) - else - logger.verbose "Shelling out for #{cmd}" - job_control.exec_command(cmd, args) + commands = parse_line(line) + result = nil + commands.each do |command| + args = word_expander.expand(command) + program = args.shift + logger.verbose "Parsed command: #{program} #{args.inspect}" + if builtins.builtin?(program) + logger.verbose "Executing builtin #{program}" + result = builtins.exec(program, args) + else + logger.verbose "Shelling out for #{program}" + result = job_control.exec_command(program, args) + end end + result rescue Errno => e warn "#{red("[ERROR]")} #{e.message}" -1 @@ -69,5 +74,64 @@ module Shell def prompt(pwd) "#{blue(pwd)}#{white("%")} #{CLEAR}" end + + def parse_line(line) + commands = [] + command = "".dup + state = :unquoted + line.each_char do |c| + case state + when :unquoted + case c + when ";" + commands << command + command = "".dup + when "'" + command << c + state = :single_quoted + when "\"" + command << c + state = :double_quoted + when "\\" + command << c + state = :escaped + else + command << c + end + + when :single_quoted + command << c + state = :unquoted if c == "'" + + when :double_quoted + case c + when "\\" + state = :double_quoted_escape + else + command << c + end + state = :unquoted if c == "\"" + + when :double_quoted_escape + case c + when "\"", "\\", "$", "`" + # no-op + else + command << "\\" # POSIX behaviour, backslash remains + end + command << c + state = :double_quoted + + when :escaped + command << c + state = :unquoted + + else + raise "Unknown state #{state}" + end + end + commands << command + commands + end end end diff --git a/ruby/test/shell_test.rb b/ruby/test/shell_test.rb index 59daea6..275d428 100644 --- a/ruby/test/shell_test.rb +++ b/ruby/test/shell_test.rb @@ -108,11 +108,11 @@ class ShellTest < Minitest::Test ######################### def test_builtin_cd_no_args - skip "cannot easily implement without sequencing with ; or &&" + assert_equal Dir.home, `#{A1_PATH} -c 'cd; echo $PWD'`.strip end def test_builtin_cd - skip "cannot easily implement without sequencing with ; or &&" + assert_equal File.join(Dir.pwd, "blah"), `#{A1_PATH} -c 'mkdir -p blah; cd blah; echo $PWD; cd ..; rm -rf blah'`.strip end def test_builtin_cd_dash