Skip to content

Commit 9c8064c

Browse files
committed
Implement support for "$XZY" style environment variables
1 parent ca1c071 commit 9c8064c

File tree

3 files changed

+83
-22
lines changed

3 files changed

+83
-22
lines changed

libraries/rushell/src/Tokenizer.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,18 @@ export class Token {
4646
}
4747
}
4848

49-
const wordCharacterOrBackslashRegExp: RegExp = /[a-z0-9_\\]/i;
49+
const textCharacterRegExp: RegExp = /[a-z0-9_\\]/i;
50+
const startVariableCharacterRegExp: RegExp = /[a-z_]/i;
51+
const variableCharacterRegExp: RegExp = /[a-z0-9_]/i;
5052

5153
export class Tokenizer {
5254
public readonly input: TextRange;
5355
private _currentIndex: number;
5456

57+
private static _isSpace(c: string | undefined): boolean {
58+
return c === ' ' || c === '\t';
59+
}
60+
5561
constructor(input: TextRange | string) {
5662
if (typeof(input) === 'string') {
5763
this.input = TextRange.fromString(input);
@@ -77,7 +83,7 @@ export class Tokenizer {
7783
}
7884

7985
// Is it a sequence of whitespace?
80-
if (/[ \t]/.test(firstChar)) {
86+
if (Tokenizer._isSpace(firstChar)) {
8187
this._get();
8288

8389
while (Tokenizer._isSpace(this._peek())) {
@@ -100,30 +106,45 @@ export class Tokenizer {
100106
}
101107

102108
// Is it a text token?
103-
if (wordCharacterOrBackslashRegExp.test(firstChar)) {
109+
if (textCharacterRegExp.test(firstChar)) {
104110
let text: string = '';
105111
let c: string | undefined = firstChar;
106112
do {
107113
if (c === '\\') {
108114
this._get(); // discard the backslash
109115
if (this._peek() === undefined) {
110116
throw new ParseError('Backslash encountered at end of stream',
111-
input.getNewRange(this._currentIndex, this._currentIndex+1));
117+
input.getNewRange(this._currentIndex, this._currentIndex + 1));
112118
}
113119
text += this._get();
114120
} else {
115121
text += this._get();
116122
}
117123

118124
c = this._peek();
119-
if (c == undefined) {
120-
break;
121-
}
122-
} while (wordCharacterOrBackslashRegExp.test(c));
125+
} while (c && textCharacterRegExp.test(c));
123126

124127
return new Token(TokenKind.Text, input.getNewRange(startIndex, this._currentIndex), text);
125128
}
126129

130+
// Is it a dollar variable? The valid environment variable names are [A-Z_][A-Z0-9_]*
131+
if (firstChar === '$') {
132+
this._get();
133+
134+
let name: string = this._get() || '';
135+
if (!startVariableCharacterRegExp.test(name)) {
136+
throw new ParseError('The "$" symbol must be followed by a letter or underscore',
137+
input.getNewRange(startIndex, this._currentIndex));
138+
}
139+
140+
let c: string | undefined = this._peek();
141+
while (c && variableCharacterRegExp.test(c)) {
142+
name += this._get();
143+
c = this._peek();
144+
}
145+
return new Token(TokenKind.DollarVariable, input.getNewRange(startIndex, this._currentIndex), name);
146+
}
147+
127148
// Is it the "&&" token?
128149
if (firstChar === '&') {
129150
if (this._peek2() === '&') {
@@ -163,13 +184,9 @@ export class Tokenizer {
163184
}
164185

165186
private _peek2(): string | undefined {
166-
if (this._currentIndex+1 >= this.input.end) {
187+
if (this._currentIndex + 1 >= this.input.end) {
167188
return undefined;
168189
}
169-
return this.input.buffer[this._currentIndex+1];
170-
}
171-
172-
private static _isSpace(c: string | undefined): boolean {
173-
return c === ' ' || c === '\t';
190+
return this.input.buffer[this._currentIndex + 1];
174191
}
175192
}

libraries/rushell/src/test/Tokenizer.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4-
import { Tokenizer, TokenKind } from '../Tokenizer';
4+
import { Tokenizer, TokenKind, Token } from '../Tokenizer';
55

66
function escape(s: string): string {
77
return s.replace(/\n/g, '[n]')
@@ -10,8 +10,13 @@ function escape(s: string): string {
1010
.replace(/\\/g, '[b]');
1111
}
1212

13+
function tokenize(input: string): Token[] {
14+
const tokenizer: Tokenizer = new Tokenizer(input);
15+
return tokenizer.getTokens();
16+
}
17+
1318
function matchSnapshot(input: string): void {
14-
const tokenizer = new Tokenizer(input);
19+
const tokenizer: Tokenizer = new Tokenizer(input);
1520
expect({
1621
input: escape(tokenizer.input.toString()),
1722
tokens: tokenizer.getTokens().map(x => [TokenKind[x.kind], escape(x.toString())])
@@ -28,7 +33,7 @@ test('01: white space tokens', () => {
2833
});
2934

3035
test('02: text with escapes', () => {
31-
matchSnapshot(' ab+56\\>qrst$(abc\\))');
36+
matchSnapshot(' ab+56\\>qrst(abc\\))');
3237
});
3338

3439
test('03: The && operator', () => {
@@ -37,3 +42,10 @@ test('03: The && operator', () => {
3742
matchSnapshot('&&');
3843
matchSnapshot('&');
3944
});
45+
46+
test('04: dollar variables', () => {
47+
matchSnapshot('$abc123.456');
48+
matchSnapshot('$ab$_90');
49+
expect(() => tokenize('$')).toThrowError();
50+
expect(() => tokenize('${abc}')).toThrowError();
51+
});

libraries/rushell/src/test/__snapshots__/Tokenizer.test.ts.snap

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ Object {
7777

7878
exports[`02: text with escapes 1`] = `
7979
Object {
80-
"input": " ab+56[b]>qrst$(abc[b]))",
80+
"input": " ab+56[b]>qrst(abc[b]))",
8181
"tokens": Array [
8282
Array [
8383
"Spaces",
@@ -95,10 +95,6 @@ Object {
9595
"Text",
9696
"56>qrst",
9797
],
98-
Array [
99-
"Other",
100-
"$",
101-
],
10298
Array [
10399
"Other",
104100
"(",
@@ -186,3 +182,39 @@ Object {
186182
],
187183
}
188184
`;
185+
186+
exports[`04: dollar variables 1`] = `
187+
Object {
188+
"input": "$abc123.456",
189+
"tokens": Array [
190+
Array [
191+
"DollarVariable",
192+
"abc123",
193+
],
194+
Array [
195+
"Other",
196+
".",
197+
],
198+
Array [
199+
"Text",
200+
"456",
201+
],
202+
],
203+
}
204+
`;
205+
206+
exports[`04: dollar variables 2`] = `
207+
Object {
208+
"input": "$ab$_90",
209+
"tokens": Array [
210+
Array [
211+
"DollarVariable",
212+
"ab",
213+
],
214+
Array [
215+
"DollarVariable",
216+
"_90",
217+
],
218+
],
219+
}
220+
`;

0 commit comments

Comments
 (0)