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/
|
/spec/reports/
|
||||||
/tmp/
|
/tmp/
|
||||||
/Gemfile.lock
|
/Gemfile.lock
|
||||||
|
ext/Makefile
|
||||||
|
ext/*.o
|
||||||
|
ext/*.bundle
|
||||||
19
README.md
19
README.md
|
|
@ -2,9 +2,12 @@
|
||||||
|
|
||||||
[](https://rubygems.org/gems/wordexp)
|
[](https://rubygems.org/gems/wordexp)
|
||||||
[](https://app.circleci.com/pipelines/github/samsonjs/wordexp?branch=main)
|
[](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
|
```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
|
## 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!
|
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
|
## 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 'rake/testtask'
|
||||||
require 'rubocop/rake_task'
|
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 << 'test'
|
||||||
t.libs << 'lib'
|
t.libs << 'lib'
|
||||||
t.test_files = FileList['test/**/*_test.rb']
|
t.test_files = FileList['test/**/*_test.rb']
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,11 @@
|
||||||
#!/usr/bin/env ruby -w
|
#!/usr/bin/env ruby -w
|
||||||
|
|
||||||
require 'wordexp'
|
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
|
module Wordexp
|
||||||
autoload :CLI, 'wordexp/cli'
|
autoload :CLI, 'wordexp/cli'
|
||||||
autoload :VERSION, 'wordexp/version'
|
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
module Wordexp
|
module Wordexp
|
||||||
class CLI
|
class CLI
|
||||||
def call(argv)
|
def call(string)
|
||||||
puts argv.join(' ')
|
puts Wordexp.expand(string).inspect
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
$LOAD_PATH.unshift File.expand_path('../ext', __dir__)
|
||||||
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
||||||
require 'wordexp'
|
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'
|
require 'test_helper'
|
||||||
|
|
||||||
class WordexpTest < Minitest::Test
|
class WordexpTest < Minitest::Test
|
||||||
def test_that_it_has_a_version_number
|
def test_constants
|
||||||
refute_nil ::Wordexp::VERSION
|
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,9 @@ Gem::Specification.new do |spec|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Specify which files should be added to the gem when it is released.
|
# 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.bindir = 'exe'
|
||||||
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
||||||
spec.require_paths = ['lib']
|
spec.require_paths = %w[ext lib]
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue