mirror of
https://github.com/samsonjs/wordexp.git
synced 2026-03-25 08:45:54 +00:00
Implement the most basic API possible
There are no options exposed to Ruby yet.
This commit is contained in:
parent
cee89ae8c1
commit
fb1bc1a910
12 changed files with 205 additions and 12 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -8,3 +8,6 @@
|
|||
/spec/reports/
|
||||
/tmp/
|
||||
/Gemfile.lock
|
||||
ext/Makefile
|
||||
ext/*.o
|
||||
ext/*.bundle
|
||||
19
README.md
19
README.md
|
|
@ -2,9 +2,12 @@
|
|||
|
||||
[](https://rubygems.org/gems/wordexp)
|
||||
[](https://app.circleci.com/pipelines/github/samsonjs/wordexp?branch=main)
|
||||
[](https://codeclimate.com/github/samsonjs/wordexp)
|
||||
[](https://codeclimate.com/github/samsonjs/wordexp/maintainability)
|
||||
|
||||
TODO: Description of this gem goes here.
|
||||
A Ruby gem for performing shell word expansion using [GNU wordexp][]. It's like [Shellwords][] turned up to 11. Not only does it split taking quotes into account, but it also expands environment variables and tildes, and runs subcommands in \``backticks`\` or `$(dollar parentheses)`.
|
||||
|
||||
[GNU wordexp]: https://www.gnu.org/software/libc/manual/html_node/Word-Expansion.html
|
||||
[Shellwords]: https://ruby-doc.org/stdlib-3.1.0/libdoc/shellwords/rdoc/Shellwords.html
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -21,9 +24,17 @@ $ gem install wordexp
|
|||
```
|
||||
|
||||
```ruby
|
||||
require "wordexp"
|
||||
require 'wordexp'
|
||||
|
||||
cmd = Wordexp.expand("echo 'roof cats' $HOME ~/bin $(date +%F)")
|
||||
# => ["echo", "roof cats", "/home/queso", "/home/queso/bin", "2022-01-16"]
|
||||
|
||||
fork { exec(*cmd) }
|
||||
# roof cats /home/queso /home/queso/bin 2022-01-16
|
||||
```
|
||||
|
||||
With that you're half way to a [fairly usable shell in Ruby](https://github.com/samsonjs/csc360-a1-shell).
|
||||
|
||||
## Support
|
||||
|
||||
If you want to report a bug, or have ideas, feedback or questions about the gem, [let me know via GitHub issues](https://github.com/samsonjs/wordexp/issues/new) and I will do my best to provide a helpful answer. Happy hacking!
|
||||
|
|
@ -38,4 +49,4 @@ Everyone interacting in this project’s codebases, issue trackers, chat rooms a
|
|||
|
||||
## Contribution guide
|
||||
|
||||
Pull requests are welcome!
|
||||
Pull requests are welcome! Make sure that new code is reasonably well tested and all the checks pass. I'm happy to provide a bit of direction and guidance if you're unsure how to proceed with any of these things.
|
||||
|
|
|
|||
10
Rakefile
10
Rakefile
|
|
@ -2,7 +2,15 @@ require 'bundler/gem_tasks'
|
|||
require 'rake/testtask'
|
||||
require 'rubocop/rake_task'
|
||||
|
||||
Rake::TestTask.new(:test) do |t|
|
||||
task :compile do
|
||||
puts 'Compiling C extension'
|
||||
`cd ext && make clean`
|
||||
`cd ext && ruby extconf.rb`
|
||||
`cd ext && make`
|
||||
puts 'Done'
|
||||
end
|
||||
|
||||
Rake::TestTask.new(test: :compile) do |t|
|
||||
t.libs << 'test'
|
||||
t.libs << 'lib'
|
||||
t.test_files = FileList['test/**/*_test.rb']
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
#!/usr/bin/env ruby -w
|
||||
|
||||
require 'wordexp'
|
||||
Wordexp::CLI.new.call(ARGV)
|
||||
|
||||
string = ARGV.first
|
||||
if string.nil? || string.strip.empty?
|
||||
warn 'Usage: wordexp <string>'
|
||||
exit 1
|
||||
end
|
||||
|
||||
Wordexp::CLI.new.call(string)
|
||||
|
|
|
|||
3
ext/extconf.rb
Normal file
3
ext/extconf.rb
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
require 'mkmf'
|
||||
|
||||
create_makefile 'wordexp_ext'
|
||||
61
ext/wordexp_ext.c
Normal file
61
ext/wordexp_ext.c
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* @file wordexp_ext.c
|
||||
* @author Sami Samhuri (sami@samhuri.net)
|
||||
* @brief Ruby wrapper for the standard Unix wordexp function, used to expand shell command lines in many useful ways.
|
||||
* @version 0.1
|
||||
* @date 2022-01-16
|
||||
*
|
||||
* @copyright Copyright (c) Sami Samhuri 2022
|
||||
*
|
||||
* Released under the terms of the MIT license: https://sjs.mit-license.org
|
||||
*
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <wordexp.h>
|
||||
|
||||
#include "ruby.h"
|
||||
#include "ruby/encoding.h"
|
||||
|
||||
static VALUE ext_wordexp(VALUE self, VALUE rstring) {
|
||||
Check_Type(rstring, T_STRING);
|
||||
char *string = RSTRING_PTR(rstring);
|
||||
|
||||
/* Split and expand words, showing errors and failing on undefined variables */
|
||||
wordexp_t words;
|
||||
int result = wordexp(string, &words, WRDE_SHOWERR | WRDE_UNDEF);
|
||||
|
||||
/* failure */
|
||||
if (result != 0) {
|
||||
switch (result) {
|
||||
case WRDE_BADCHAR:
|
||||
return ID2SYM(rb_intern("wrde_badchar"));
|
||||
case WRDE_BADVAL:
|
||||
return ID2SYM(rb_intern("wrde_badval"));
|
||||
case WRDE_CMDSUB:
|
||||
return ID2SYM(rb_intern("wrde_cmdsub"));
|
||||
case WRDE_NOSPACE:
|
||||
return ID2SYM(rb_intern("wrde_nospace"));
|
||||
case WRDE_SYNTAX:
|
||||
return ID2SYM(rb_intern("wrde_syntax"));
|
||||
default:
|
||||
return ID2SYM(rb_intern("unknown_error"));
|
||||
}
|
||||
}
|
||||
|
||||
/* success */
|
||||
VALUE rwords = rb_ary_new2(words.we_wordc);
|
||||
for (size_t i = 0; i < words.we_wordc; i++) {
|
||||
VALUE rword = rb_str_new2(words.we_wordv[i]);
|
||||
rb_ary_push(rwords, rword);
|
||||
}
|
||||
wordfree(&words);
|
||||
return rwords;
|
||||
}
|
||||
|
||||
void Init_wordexp_ext(void) {
|
||||
VALUE Wordexp = rb_define_module("Wordexp");
|
||||
|
||||
VALUE Ext = rb_define_class_under(Wordexp, "Ext", rb_cObject);
|
||||
rb_define_singleton_method(Ext, "wordexp", ext_wordexp, 1);
|
||||
}
|
||||
|
|
@ -1,4 +1,53 @@
|
|||
require 'wordexp_ext'
|
||||
|
||||
module Wordexp
|
||||
autoload :CLI, 'wordexp/cli'
|
||||
autoload :VERSION, 'wordexp/version'
|
||||
|
||||
# Illegal occurrence of newline or one of |, &, ;, <, >, (, ), {, }.
|
||||
class BadCharacterError < StandardError; end
|
||||
|
||||
# An undefined shell variable was referenced, and the WRDE_UNDEF flag told us to consider this an error.
|
||||
class BadValueError < StandardError; end
|
||||
|
||||
# Command substitution requested, but the WRDE_NOCMD flag told us to consider this an error.
|
||||
class CommandSubstitutionError < StandardError; end
|
||||
|
||||
# Out of memory.
|
||||
class NoSpaceError < StandardError; end
|
||||
|
||||
# Shell syntax error, such as unbalanced parentheses or unmatched quotes.
|
||||
class SyntaxError < StandardError; end
|
||||
|
||||
# An undocumented error occurred.
|
||||
class UndocumentedError < StandardError; end
|
||||
|
||||
class << self
|
||||
def expand(string)
|
||||
result = Ext.wordexp(string)
|
||||
return result if result.is_a?(Array)
|
||||
|
||||
case result
|
||||
when :wrde_badchar
|
||||
# FIXME: useful message that includes the position of the character
|
||||
raise BadCharacterError, 'Bad character'
|
||||
|
||||
when :wrde_badval
|
||||
raise BadValueError, 'Bad value'
|
||||
|
||||
when :wrde_cmdsub
|
||||
raise CommandSubstitutionError, 'Command substitution is disabled'
|
||||
|
||||
when :wrde_nospace
|
||||
raise NoSpaceError, 'Out of memory'
|
||||
|
||||
when :wrde_syntax
|
||||
raise SyntaxError, 'Bad value'
|
||||
|
||||
else
|
||||
warn "wordexp returned an unexpected result: #{result.inspect}"
|
||||
raise UndocumentedError, 'An unknown error occurred'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
module Wordexp
|
||||
class CLI
|
||||
def call(argv)
|
||||
puts argv.join(' ')
|
||||
def call(string)
|
||||
puts Wordexp.expand(string).inspect
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
$LOAD_PATH.unshift File.expand_path('../ext', __dir__)
|
||||
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
||||
require 'wordexp'
|
||||
|
||||
|
|
|
|||
7
test/wordexp/version_test.rb
Normal file
7
test/wordexp/version_test.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
require 'test_helper'
|
||||
|
||||
class WordexpVersionTest < Minitest::Test
|
||||
def test_that_it_has_a_version_number
|
||||
refute_nil ::Wordexp::VERSION
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,49 @@
|
|||
require 'test_helper'
|
||||
|
||||
class WordexpTest < Minitest::Test
|
||||
def test_that_it_has_a_version_number
|
||||
refute_nil ::Wordexp::VERSION
|
||||
def test_constants
|
||||
assert_equal %w[hello wordexp], ::Wordexp.expand('hello wordexp')
|
||||
end
|
||||
|
||||
def test_quotation
|
||||
assert_equal ['hello wordexp', 'more words'], ::Wordexp.expand("'hello wordexp' \"more words\"")
|
||||
end
|
||||
|
||||
def test_environment_variable_expansion
|
||||
assert_equal [ENV['HOME']], ::Wordexp.expand('$HOME')
|
||||
end
|
||||
|
||||
def test_tilde_expansion
|
||||
assert_equal ["#{ENV['HOME']}/bin"], ::Wordexp.expand('~/bin')
|
||||
end
|
||||
|
||||
def test_command_substitution_backticks
|
||||
assert_equal ["#{ENV['HOME']}/bin"], ::Wordexp.expand('`echo ~/bin`')
|
||||
end
|
||||
|
||||
def test_command_substitution_dollar_parentheses
|
||||
assert_equal ["#{ENV['HOME']}/bin"], ::Wordexp.expand('$(echo ~/bin)')
|
||||
end
|
||||
|
||||
def test_error_badchar
|
||||
assert_raises(Wordexp::BadCharacterError) do
|
||||
::Wordexp.expand('<nope>')
|
||||
end
|
||||
end
|
||||
|
||||
def test_error_badval
|
||||
assert_raises(Wordexp::BadValueError) do
|
||||
::Wordexp.expand('$DEFINITELY_DOES_NOT_EXIST')
|
||||
end
|
||||
end
|
||||
|
||||
def test_error_cmdsub
|
||||
# cannot test this until there's a way to disable command substitution
|
||||
end
|
||||
|
||||
def test_error_syntax
|
||||
assert_raises(Wordexp::SyntaxError) do
|
||||
::Wordexp.expand('$(this is the command that never ends')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ Gem::Specification.new do |spec|
|
|||
}
|
||||
|
||||
# Specify which files should be added to the gem when it is released.
|
||||
spec.files = Dir.glob(%w[LICENSE.txt README.md lib/**/*]).reject { |f| File.directory?(f) }
|
||||
glob = %w[LICENSE.txt README.md lib/**/* ext/extconf.rb ext/wordexp_ext.c]
|
||||
spec.files = Dir.glob(glob).reject { |f| File.directory?(f) }
|
||||
spec.bindir = 'exe'
|
||||
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
||||
spec.require_paths = ['lib']
|
||||
spec.require_paths = %w[ext lib]
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue