Implement the most basic API possible

There are no options exposed to Ruby yet.
This commit is contained in:
Sami Samhuri 2022-01-16 16:41:44 -08:00
parent cee89ae8c1
commit fb1bc1a910
No known key found for this signature in database
GPG key ID: 4B4195422742FC16
12 changed files with 205 additions and 12 deletions

3
.gitignore vendored
View file

@ -8,3 +8,6 @@
/spec/reports/
/tmp/
/Gemfile.lock
ext/Makefile
ext/*.o
ext/*.bundle

View file

@ -2,9 +2,12 @@
[![Gem Version](https://badge.fury.io/rb/wordexp.svg)](https://rubygems.org/gems/wordexp)
[![Circle](https://circleci.com/gh/samsonjs/wordexp/tree/main.svg?style=shield)](https://app.circleci.com/pipelines/github/samsonjs/wordexp?branch=main)
[![Code Climate](https://codeclimate.com/github/samsonjs/wordexp/badges/gpa.svg)](https://codeclimate.com/github/samsonjs/wordexp)
[![Code Climate Maintainability](https://api.codeclimate.com/v1/badges/21cc24badf15d19b5cec/maintainability)](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 projects 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.

View file

@ -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']

View file

@ -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
View file

@ -0,0 +1,3 @@
require 'mkmf'
create_makefile 'wordexp_ext'

61
ext/wordexp_ext.c Normal file
View 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);
}

View file

@ -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

View file

@ -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

View file

@ -1,3 +1,4 @@
$LOAD_PATH.unshift File.expand_path('../ext', __dir__)
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
require 'wordexp'

View 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

View file

@ -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

View file

@ -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