mirror of
https://github.com/samsonjs/csc360-a1-shell.git
synced 2026-04-27 14:57:43 +00:00
Ditch wordexp and modify Shellwords code to make it work
This commit is contained in:
parent
bec3f4a1f3
commit
79ba26c76b
5 changed files with 102 additions and 9 deletions
|
|
@ -7,4 +7,3 @@ gem "parser", "~> 3.3.10"
|
||||||
gem "rake", "~> 13.0"
|
gem "rake", "~> 13.0"
|
||||||
gem "reline", "~> 0.6"
|
gem "reline", "~> 0.6"
|
||||||
gem "standard", "~> 1.52.0", require: false
|
gem "standard", "~> 1.52.0", require: false
|
||||||
gem "wordexp", "~> 0.2"
|
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,6 @@ GEM
|
||||||
unicode-display_width (3.2.0)
|
unicode-display_width (3.2.0)
|
||||||
unicode-emoji (~> 4.1)
|
unicode-emoji (~> 4.1)
|
||||||
unicode-emoji (4.2.0)
|
unicode-emoji (4.2.0)
|
||||||
wordexp (0.2.2)
|
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-21
|
arm64-darwin-21
|
||||||
|
|
@ -103,13 +102,12 @@ PLATFORMS
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
guard (~> 2.19)
|
guard (~> 2.19)
|
||||||
guard-rake
|
guard-rake (~> 1.0)
|
||||||
minitest (~> 6.0)
|
minitest (~> 6.0)
|
||||||
parser (~> 3.3.10)
|
parser (~> 3.3.10)
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
reline (~> 0.6)
|
reline (~> 0.6)
|
||||||
standard (~> 1.52.0)
|
standard (~> 1.52.0)
|
||||||
wordexp (~> 0.2)
|
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
4.0.3
|
4.0.3
|
||||||
|
|
|
||||||
|
|
@ -3,30 +3,32 @@ begin
|
||||||
rescue LoadError
|
rescue LoadError
|
||||||
require "reline"
|
require "reline"
|
||||||
end
|
end
|
||||||
require "wordexp"
|
|
||||||
|
|
||||||
require "shell/builtins"
|
require "shell/builtins"
|
||||||
require "shell/colours"
|
require "shell/colours"
|
||||||
require "shell/job_control"
|
require "shell/job_control"
|
||||||
require "shell/logger"
|
require "shell/logger"
|
||||||
|
require "shell/word_expander"
|
||||||
|
|
||||||
module Shell
|
module Shell
|
||||||
class REPL
|
class REPL
|
||||||
include Colours
|
include Colours
|
||||||
|
|
||||||
attr_reader :builtins, :job_control, :logger, :options
|
attr_reader :builtins, :job_control, :logger, :options, :word_expander
|
||||||
|
|
||||||
attr_accessor :precmd_hook
|
attr_accessor :precmd_hook
|
||||||
|
|
||||||
def initialize(builtins: nil, job_control: nil, logger: nil)
|
def initialize(builtins: nil, job_control: nil, logger: nil, word_expander: nil)
|
||||||
logger ||= Logger.instance
|
logger ||= Logger.instance
|
||||||
job_control ||= JobControl.new(logger: logger)
|
job_control ||= JobControl.new(logger: logger)
|
||||||
builtins ||= Builtins.new(job_control: job_control)
|
builtins ||= Builtins.new(job_control: job_control)
|
||||||
|
word_expander ||= WordExpander.new
|
||||||
|
|
||||||
@builtins = builtins
|
@builtins = builtins
|
||||||
@job_control = job_control
|
@job_control = job_control
|
||||||
@logger = logger
|
@logger = logger
|
||||||
@options = {}
|
@options = {}
|
||||||
|
@word_expander = word_expander
|
||||||
end
|
end
|
||||||
|
|
||||||
def start(options: nil)
|
def start(options: nil)
|
||||||
|
|
@ -48,7 +50,7 @@ module Shell
|
||||||
return 0 if line.strip.empty? # no input, no-op
|
return 0 if line.strip.empty? # no input, no-op
|
||||||
|
|
||||||
logger.verbose "Processing command: #{line.inspect}"
|
logger.verbose "Processing command: #{line.inspect}"
|
||||||
args = Wordexp.expand(line)
|
args = word_expander.expand(line)
|
||||||
cmd = args.shift
|
cmd = args.shift
|
||||||
logger.verbose "Parsed command: #{cmd} #{args.inspect}"
|
logger.verbose "Parsed command: #{cmd} #{args.inspect}"
|
||||||
if builtins.builtin?(cmd)
|
if builtins.builtin?(cmd)
|
||||||
|
|
|
||||||
93
ruby/shell/word_expander.rb
Normal file
93
ruby/shell/word_expander.rb
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
require "shellwords"
|
||||||
|
|
||||||
|
module Shell
|
||||||
|
class WordExpander
|
||||||
|
ENV_VAR_REGEX = /\$(?:\{([^}]+)\}|(\w+)\b)/
|
||||||
|
|
||||||
|
# Splits the given line into multiple words, performing the following transformations:
|
||||||
|
#
|
||||||
|
# - Splits into words taking quoting and backslash escaping into account
|
||||||
|
# - Expands environment variables using $NAME and ${NAME} syntax
|
||||||
|
# - Tilde expansion, which means that ~ is expanded to $HOME
|
||||||
|
# - Glob expansion on files and directories
|
||||||
|
def expand(line)
|
||||||
|
shellsplit(line)
|
||||||
|
.map do |word|
|
||||||
|
word
|
||||||
|
.gsub(ENV_VAR_REGEX) do
|
||||||
|
name = Regexp.last_match(2) || Regexp.last_match(1)
|
||||||
|
ENV.fetch(name)
|
||||||
|
end
|
||||||
|
# TODO: expand globs
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Lifted directly from Ruby 4.0.0.
|
||||||
|
#
|
||||||
|
# Splits a string into an array of tokens in the same way the UNIX
|
||||||
|
# Bourne shell does.
|
||||||
|
#
|
||||||
|
# argv = Shellwords.split('here are "two words"')
|
||||||
|
# argv #=> ["here", "are", "two words"]
|
||||||
|
#
|
||||||
|
# +line+ must not contain NUL characters because of nature of
|
||||||
|
# +exec+ system call.
|
||||||
|
#
|
||||||
|
# Note, however, that this is not a command line parser. Shell
|
||||||
|
# metacharacters except for the single and double quotes and
|
||||||
|
# backslash are not treated as such.
|
||||||
|
#
|
||||||
|
# argv = Shellwords.split('ruby my_prog.rb | less')
|
||||||
|
# argv #=> ["ruby", "my_prog.rb", "|", "less"]
|
||||||
|
#
|
||||||
|
# String#shellsplit is a shortcut for this function.
|
||||||
|
#
|
||||||
|
# argv = 'here are "two words"'.shellsplit
|
||||||
|
# argv #=> ["here", "are", "two words"]
|
||||||
|
def shellsplit(line)
|
||||||
|
words = []
|
||||||
|
field = "".dup
|
||||||
|
at_word_start = true
|
||||||
|
found_glob_char = false
|
||||||
|
line.scan(/\G\s*(?>([^\0\s\\'"]+)|'([^\0']*)'|"((?:[^\0"\\]|\\[^\0])*)"|(\\[^\0]?)|(\S))(\s|\z)?/m) do
|
||||||
|
|word, sq, dq, esc, garbage, sep|
|
||||||
|
if garbage
|
||||||
|
b = $~.begin(0)
|
||||||
|
line = $~[0]
|
||||||
|
line = "..." + line if b > 0
|
||||||
|
raise ArgumentError, "#{(garbage == "\0") ? "Nul character" : "Unmatched quote"} at #{b}: #{line}"
|
||||||
|
end
|
||||||
|
# 2.2.3 Double-Quotes:
|
||||||
|
#
|
||||||
|
# The <backslash> shall retain its special meaning as an
|
||||||
|
# escape character only when followed by one of the following
|
||||||
|
# characters when considered special:
|
||||||
|
#
|
||||||
|
# $ ` " \ <newline>
|
||||||
|
field << (word || sq || (dq && dq.gsub(/\\([$`"\\\n])/, '\\1')) || esc.gsub(/\\(.)/, '\\1'))
|
||||||
|
found_glob_char = word && word =~ /[*?\[]/ # must be unquoted
|
||||||
|
# Expand tildes at the beginning of unquoted words.
|
||||||
|
if word && at_word_start
|
||||||
|
field.sub!(/^~/, Dir.home)
|
||||||
|
end
|
||||||
|
at_word_start = false
|
||||||
|
if sep
|
||||||
|
if found_glob_char
|
||||||
|
glob_words = expand_globs(field)
|
||||||
|
words += (glob_words.empty? ? [field] : glob_words)
|
||||||
|
else
|
||||||
|
words << field
|
||||||
|
end
|
||||||
|
field = "".dup
|
||||||
|
at_word_start = true
|
||||||
|
found_glob_char = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
words
|
||||||
|
end
|
||||||
|
|
||||||
|
def expand_globs(word)
|
||||||
|
Dir.glob(word)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -19,7 +19,8 @@ class ShellTest < Minitest::Test
|
||||||
|
|
||||||
def test_expands_environment_variables
|
def test_expands_environment_variables
|
||||||
assert_equal Dir.home, `#{A1_PATH} -c 'echo $HOME'`.chomp
|
assert_equal Dir.home, `#{A1_PATH} -c 'echo $HOME'`.chomp
|
||||||
assert system("#{A1_PATH} -c 'echo $HOME' >/dev/null")
|
assert_equal Dir.home, `#{A1_PATH} -c 'echo ${HOME}'`.chomp
|
||||||
|
assert_equal "#{Dir.home} #{Dir.home}", `#{A1_PATH} -c 'echo ${HOME} ${HOME}'`.chomp
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_fails_on_unknown_variables
|
def test_fails_on_unknown_variables
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue