Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ Layout/IndentArray:
Enabled: true
EnforcedStyle: consistent

Layout/FirstParameterIndentation:
Description: Checks the indentation of the first parameter in a method call.
Enabled: true
EnforcedStyle: consistent

Style/StringLiterals:
Description: Checks if uses of quotes match the configured preference.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-string-literals
Expand Down Expand Up @@ -47,3 +52,6 @@ Style/TrailingCommaInHashLiteral:

Style/EmptyMethod:
Enabled: false

Style/SymbolArray:
EnforcedStyle: brackets
4 changes: 1 addition & 3 deletions lib/gitsh/argument_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ def length
end

def values(env)
@args.map do |arg|
arg.value(env)
end
@args.flat_map { |arg| arg.value(env) }
end

private
Expand Down
21 changes: 21 additions & 0 deletions lib/gitsh/arguments/brace_expansion.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Gitsh
module Arguments
class BraceExpansion
def initialize(options)
@options = options
end

def value(env)
options.flat_map { |option| option.value(env) }
end

def ==(other)
other.is_a?(self.class) && options == other.options
end

protected

attr_reader :options
end
end
end
5 changes: 4 additions & 1 deletion lib/gitsh/arguments/composite_argument.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ def initialize(parts)
end

def value(env)
parts.map { |part| part.value(env) }.join('')
parts.
map { |part| part.value(env) }.
inject { |first, second| first.product(second) }.
map { |values| values.join('') }
end

def ==(other)
Expand Down
2 changes: 1 addition & 1 deletion lib/gitsh/arguments/string_argument.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def initialize(value)
end

def value(_env)
raw_value
[raw_value]
end

def ==(other)
Expand Down
2 changes: 1 addition & 1 deletion lib/gitsh/arguments/subshell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def initialize(command, options = {})
def value(env)
capturing_env = CapturingEnvironment.new(env.clone)
command.execute(capturing_env)
strip_whitespace(capturing_env.captured_output)
[strip_whitespace(capturing_env.captured_output)]
end

def ==(other)
Expand Down
2 changes: 1 addition & 1 deletion lib/gitsh/arguments/variable_argument.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def initialize(variable_name)
end

def value(env)
env.fetch(variable_name)
[env.fetch(variable_name)]
end

def ==(other)
Expand Down
25 changes: 25 additions & 0 deletions lib/gitsh/lexer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Lexer < RLTK::Lexer
'\\', # Escape character
'$', # Variable or sub-shell prefix
'(', ')', # Parentheses
'{', '}', # Braces (for expansion)
]).freeze

SOFT_STRING_ESCAPABLES = CharacterClass.new([
Expand All @@ -24,6 +25,12 @@ class Lexer < RLTK::Lexer
"'", # String terminator
]).freeze

BRACE_EXPANSION_ESCAPABLES = CharacterClass.new([
'\\', # Escape character
'{', '}', # Braces (for nested expansions)
',', # Comma (for separating expansion values)
]).freeze
Comment thread
georgebrock marked this conversation as resolved.

class Environment < RLTK::Lexer::Environment
attr_reader :right_paren_stack

Expand Down Expand Up @@ -89,6 +96,24 @@ def initialize(*args)
rule(/\\/, :soft_string) { [:WORD, '\\'] }
rule(/"/, :soft_string) { pop_state }

[:default, :brace_expansion].each do |state|
Comment thread
georgebrock marked this conversation as resolved.
rule(/\{/, state) do
push_state(:brace_expansion)
:LEFT_BRACE
end
end
rule(/#{BRACE_EXPANSION_ESCAPABLES.to_negative_regexp}+/, :brace_expansion) do |t|
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/LineLength: Line is too long. [86/80]

[:WORD, t]
end
rule(/\\#{BRACE_EXPANSION_ESCAPABLES.to_regexp}/, :brace_expansion) do |t|
[:WORD, t[1]]
end
rule(/,/, :brace_expansion) { :COMMA }
rule(/\}/, :brace_expansion) do
pop_state
:RIGHT_BRACE
end

[:default, :soft_string].each do |state|
rule(/\$/, state) { push_state(:need_var_name) }
rule(/\$\{/, state) do
Expand Down
37 changes: 33 additions & 4 deletions lib/gitsh/parser.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'rltk'
require 'gitsh/arguments/brace_expansion'
require 'gitsh/arguments/string_argument'
require 'gitsh/arguments/composite_argument'
require 'gitsh/arguments/variable_argument'
Expand Down Expand Up @@ -32,10 +33,7 @@ class Parser < RLTK::Parser
Commands::LazyCommand.new(args)
end

production(:argument_list) do
clause('.argument') { |arg| [arg] }
clause('.argument_list SPACE .argument') { |list, arg| list + [arg] }
end
nonempty_list(:argument_list, :argument, :SPACE)

production(:argument) do
clause('argument_part') { |part| part }
Expand All @@ -48,6 +46,37 @@ class Parser < RLTK::Parser
clause(:word) { |word| Arguments::StringArgument.new(word) }
clause(:VAR) { |var| Arguments::VariableArgument.new(var) }
clause(:subshell) { |program| Arguments::Subshell.new(program) }
clause(:brace_expansion) { |brace_expansion| brace_expansion }
end

production(:brace_expansion) do
# Two or more items between braces
clause('LEFT_BRACE .brace_expansion_list RIGHT_BRACE') do |options|
Arguments::BraceExpansion.new(options)
end

# Zero or one items between braces: literal that does not expand
clause('LEFT_BRACE .brace_expansion_term RIGHT_BRACE') do |term|
Arguments::CompositeArgument.new([
Arguments::StringArgument.new('{'),
term,
Arguments::StringArgument.new('}'),
])
Comment thread
georgebrock marked this conversation as resolved.
end
end

production(:brace_expansion_list) do
clause('.brace_expansion_term COMMA .brace_expansion_term') do |left, right|
Comment thread
georgebrock marked this conversation as resolved.
[left, right]
end
clause('.brace_expansion_term COMMA .brace_expansion_list') do |option, list|
Comment thread
georgebrock marked this conversation as resolved.
[option] + list
end
end

production(:brace_expansion_term) do
clause(:argument) { |arg| arg }
clause('') { Arguments::StringArgument.new('') }
end

production(:word, 'WORD+') { |words| words.inject(:+) }
Expand Down
25 changes: 24 additions & 1 deletion man/man1/gitsh.1.in
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,9 @@ argument, the string delimiters
the variable or sub-shell prefix
.Pf ( Ic $ Ns ),
parentheses
.Pf ( Ic ( ) Ns ),
.Pf ( Ic () Ns ),
braces
.Pf ( Ic {} Ns ),
and any character which would indicated the end of the argument
(spaces,
.Ic &|;# Ns )
Expand Down Expand Up @@ -353,6 +355,27 @@ but a single
.Ic \e
will also be interpreted as a literal as long as it isn't followed by
a character that can be escaped in the current context.
.
.Sh ARGUMENT EXPANSION
Like many general-purposes shells, gitsh supports argument expansions.
.Pp
.Em Brace expansion
provides a short hand for consecutive arguments that are similar to each
other. A typical example is renaming a file, where portions of the name
remain the same. For example, the following two commands are equivalent:
.Bd -literal -offset indent
mv foo_{old,new}.txt
mv foo_old.txt foo_new.txt
.Ed
.Pp
Note that empty braces
.Pf ( Ic {} Ns ),
or braces containing no comma characters (like the Git arguments
.Ic @{u}
or
.Ic stash@{1} )
are literals: no expansion occurs.
.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we mention the special cases in here?

.Sh CONFIGURATION
The following
.Xr git-config 1
Expand Down
91 changes: 91 additions & 0 deletions spec/integration/argument_globbing_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
require 'spec_helper'

describe 'Glob patterns in arguments' do
context 'brace expansion' do
it 'expands to include each option' do
GitshRunner.interactive do |gitsh|
gitsh.type(':echo h{i,o}p')

expect(gitsh).to output_no_errors
expect(gitsh).to output(/hip hop/)
end
end

it 'expands multiple expansions in one argument' do
GitshRunner.interactive do |gitsh|
gitsh.type(':echo 1{a,b}{x,y,z}')

expect(gitsh).to output_no_errors
expect(gitsh).to output(/1ax 1ay 1az 1bx 1by 1bz/)
end
end

it 'expands empty options' do
GitshRunner.interactive do |gitsh|
gitsh.type(':echo git{,,sh,,}')

expect(gitsh).to output_no_errors
expect(gitsh).to output(/git git gitsh git git/)
end
end

it 'expands nested expansions' do
GitshRunner.interactive do |gitsh|
gitsh.type(':echo f{{e,i,o}e,um}')

expect(gitsh).to output_no_errors
expect(gitsh).to output(/fee fie foe fum/)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😄

end
end

it 'supports escaped commas' do
GitshRunner.interactive do |gitsh|
gitsh.type(':echo 1{x,y\\,z}2')

expect(gitsh).to output_no_errors
expect(gitsh).to output(/1x2 1y,z2/)
end
end

it 'supports escaped braces' do
GitshRunner.interactive do |gitsh|
gitsh.type(':echo 1{x,\\{,\\}}2')

expect(gitsh).to output_no_errors
expect(gitsh).to output(/1x2 1{2 1}2/)
end
end

it 'treats empty braces as a literal argument' do
GitshRunner.interactive do |gitsh|
gitsh.type(':echo a{}')

expect(gitsh).to output_no_errors
expect(gitsh).to output(/a\{\}/)
end
end

it 'treats braces with one item and no comma as a literal argument' do
GitshRunner.interactive do |gitsh|
gitsh.type(':echo a{b}')

expect(gitsh).to output_no_errors
expect(gitsh).to output(/a\{b\}/)
end
end

it 'supports solo escaped braces' do
GitshRunner.interactive do |gitsh|
gitsh.type(':echo \\{')

expect(gitsh).to output_no_errors
expect(gitsh).to output(/\{/)

gitsh.type(':echo \\}')

expect(gitsh).to output_no_errors
expect(gitsh).to output(/\}/)
end
end
end
end
4 changes: 2 additions & 2 deletions spec/units/argument_list_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
describe '#values' do
it 'returns the values of the arguments' do
env = double('env')
hello_arg = spy('hello_arg', value: 'hello')
goodbye_arg = spy('goodbye_arg', value: 'goodbye')
hello_arg = spy('hello_arg', value: ['hello'])
goodbye_arg = spy('goodbye_arg', value: ['goodbye'])
argument_list = Gitsh::ArgumentList.new([hello_arg, goodbye_arg])

expect(argument_list.values(env)).to eq ['hello', 'goodbye']
Expand Down
43 changes: 43 additions & 0 deletions spec/units/arguments/brace_expansion_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require 'spec_helper'
require 'gitsh/arguments/brace_expansion'
require 'gitsh/arguments/string_argument'

describe Gitsh::Arguments::BraceExpansion do
describe '#value' do
it 'returns the values of its options' do
argument = described_class.new([
string_argument('foo'),
string_argument('bar'),
])
Comment thread
georgebrock marked this conversation as resolved.

expect(argument.value(double(:env))).to eq ['foo', 'bar']
end
end

describe '#==' do
it 'returns true when the options are equal' do
a1 = described_class.new(['a', 'b'])
a2 = described_class.new(['a', 'b'])

expect(a1).to eq a2
end

it 'returns false when the options are not equal' do
a = described_class.new(['a', 'b'])
b = described_class.new(['c', 'd'])

expect(a).not_to eq b
end

it 'returns false when the other object has a different class' do
arg_a = described_class.new(['a', 'b'])
double_a = double(options: ['a', 'b'])

expect(arg_a).not_to eq double_a
end
end

def string_argument(string)
instance_double(Gitsh::Arguments::StringArgument, value: string)
end
end
Loading