Skip to content

Commit 008b5c8

Browse files
committed
feat: add experimental support for positional arguments
1 parent 532e451 commit 008b5c8

File tree

4 files changed

+109
-47
lines changed

4 files changed

+109
-47
lines changed

lib/commands/run_command.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ class RunCommand {
4040
for (final command in commands.whereType<ExecutableCommand>()) {
4141
stdout.writeln('${applyBoldGreen('>')} ${command.path}');
4242
stdout.writeln(
43-
'${applyBoldGreen(r'$')} ${applyBold(command.command!)}\n',
43+
'${applyBoldGreen(r'$')} ${applyBold(command.command)}\n',
4444
);
45-
final exitCode = await bindings.execute(command.command!);
45+
final exitCode = await bindings.execute(command.command);
4646

4747
if (exitCode > 0) {
4848
throw RpsException('Command ended with a non zero exit code.');

lib/pubspec.dart

Lines changed: 89 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import 'dart:io';
2+
import 'dart:math' as math;
3+
import 'package:equatable/equatable.dart';
4+
import 'package:meta/meta.dart';
25
import 'package:path/path.dart' as p;
36
import 'package:rps/rps_exception.dart';
47
import 'package:yaml/yaml.dart';
@@ -9,6 +12,7 @@ class ScriptContext {
912
final List<String> path;
1013
String get stringPath => path.join(' ');
1114

15+
/// List of remaining arguments
1216
List<String> get rest => arguments.sublist(position + 1);
1317

1418
final int position;
@@ -43,66 +47,104 @@ class ScriptContext {
4347
}
4448
}
4549

50+
@immutable
4651
abstract class PubspecCommand {
47-
final String path;
48-
final String? command;
52+
String get path;
53+
String? get command;
4954

5055
/// Whether this command should be executed
5156
bool get executable;
57+
}
5258

53-
PubspecCommand(this.path, this.command);
54-
55-
@override
56-
bool operator ==(Object other) {
57-
return other is PubspecCommand &&
58-
other.command == command &&
59-
other.path == path;
60-
}
59+
class PositionalArgument {
60+
final int position;
61+
final int start;
62+
final int end;
6163

62-
@override
63-
int get hashCode => Object.hash(path, command, runtimeType);
64+
PositionalArgument(this.position, this.start, this.end);
6465
}
6566

66-
class ExecutableCommand extends PubspecCommand {
67+
class ExecutableCommand extends PubspecCommand with EquatableMixin {
6768
@override
6869
bool get executable => true;
6970

70-
ExecutableCommand(super.path, super.command);
71-
7271
@override
73-
bool operator ==(Object other) {
74-
return other is ExecutableCommand &&
75-
other.command == command &&
76-
other.path == path;
77-
}
78-
72+
final String command;
7973
@override
80-
int get hashCode => Object.hash(path, command, runtimeType);
74+
final String path;
75+
76+
static final _positionalArgumentsRegexp = RegExp(r'\$\{\s{0,}[0-9]+\s{0,}\}');
77+
78+
ExecutableCommand._(this.command, this.path);
79+
80+
factory ExecutableCommand(String path, String command, List<String> rest) {
81+
final argumentsInCommand = _positionalArgumentsRegexp.allMatches(command);
82+
83+
if (argumentsInCommand.isNotEmpty) {
84+
final usedArguments = <int>{};
85+
final filledCommand = command.replaceAllMapped(
86+
_positionalArgumentsRegexp,
87+
(match) {
88+
final content = match.group(0)!;
89+
final value = content.substring(2, content.length - 1).trim();
90+
final argumentIndex = int.tryParse(value);
91+
if (argumentIndex == null) {
92+
throw RpsException(
93+
"Bad argument script ($content). "
94+
"Only positional arguments are supported.",
95+
);
96+
} else if (argumentIndex >= rest.length) {
97+
throw RpsException(
98+
'The script "$path" defines a positional argument $content, '
99+
'but ${rest.length} positional argument(s) are given.',
100+
);
101+
} else {
102+
usedArguments.add(argumentIndex);
103+
return rest[argumentIndex];
104+
}
105+
},
106+
);
107+
108+
final lastUsed = usedArguments.reduce(math.max);
109+
if (lastUsed > usedArguments.length) {
110+
final unusedArguments = List<int>.generate(lastUsed, (index) => index)
111+
.where((e) => !usedArguments.contains(e));
112+
113+
throw RpsException(
114+
'The script defines unused positional argument(s): '
115+
'${unusedArguments.map((a) => '\${$a}').join(', ')}',
116+
);
117+
}
118+
119+
return ExecutableCommand._(
120+
[filledCommand, ...rest.sublist(lastUsed + 1)].join(' '),
121+
path,
122+
);
123+
} else {
124+
return ExecutableCommand._([command, ...rest].join(' '), path);
125+
}
126+
}
81127

82128
@override
83-
String toString() => 'ExecutableCommand("$path": "$command")';
129+
List<Object?> get props => [command, path];
84130
}
85131

86132
/// Indicates that reference was made to the command
87-
class RefCommand extends PubspecCommand {
88-
RefCommand(String path) : super(path, null);
89-
133+
class RefCommand extends PubspecCommand with EquatableMixin {
90134
/// This command is only a trace mark, nothing to call here.
91135
@override
92136
bool get executable => false;
93137

94138
@override
95-
bool operator ==(Object other) {
96-
return other is RefCommand &&
97-
other.command == command &&
98-
other.path == path;
99-
}
139+
Null get command => null;
100140

101141
@override
102-
int get hashCode => Object.hash(path, runtimeType);
142+
final String path;
143+
144+
RefCommand(this.path);
103145

104146
@override
105-
String toString() => 'HookCommand("$path")';
147+
List<Object?> get props => [path];
106148
}
107149

108150
class Pubspec {
@@ -177,8 +219,9 @@ class Pubspec {
177219
);
178220
} else if (value is String) {
179221
yield* _examinateCommand(
180-
command: [value, ...context.rest].join(' '),
222+
command: value,
181223
path: context.stringPath,
224+
rest: context.rest,
182225
);
183226
} else if (value is Map) {
184227
yield* _getHookCommands(
@@ -190,7 +233,11 @@ class Pubspec {
190233
if (_hasScriptKey(value)) {
191234
final script = value[scriptKey];
192235
if (script is String) {
193-
yield* _examinateCommand(command: script, path: context.stringPath);
236+
yield* _examinateCommand(
237+
command: script,
238+
path: context.stringPath,
239+
rest: context.rest,
240+
);
194241
} else if (script is Map) {
195242
final platformKey = '\$${Platform.operatingSystem}';
196243
final command = script[platformKey] ?? script[defaultScriptKey];
@@ -205,7 +252,8 @@ class Pubspec {
205252
} else {
206253
yield* _examinateCommand(
207254
command: command,
208-
path: [context.stringPath, ...context.rest].join(' '),
255+
path: context.stringPath,
256+
rest: context.rest,
209257
);
210258
}
211259
}
@@ -234,12 +282,13 @@ class Pubspec {
234282
Iterable<PubspecCommand> _examinateCommand({
235283
required String command,
236284
required String path,
285+
required List<String> rest,
237286
}) sync* {
238287
if (command.startsWith(r'$')) {
239288
yield RefCommand(path);
240289
yield* getCommands(command.substring(1).split(RegExp(r'\s+')));
241290
} else {
242-
yield ExecutableCommand(path, command);
291+
yield ExecutableCommand(path, command, rest);
243292
}
244293
}
245294

@@ -261,6 +310,9 @@ class Pubspec {
261310
yield* _examinateCommand(
262311
path: [...context.path, key].join(' '),
263312
command: value,
313+
314+
/// TODO? ignore for hooks
315+
rest: [],
264316
);
265317
}
266318
}

pubspec.lock

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ packages:
4545
dependency: transitive
4646
description:
4747
name: collection
48-
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
48+
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
4949
url: "https://pub.dev"
5050
source: hosted
51-
version: "1.17.1"
51+
version: "1.17.2"
5252
convert:
5353
dependency: transitive
5454
description:
@@ -73,6 +73,14 @@ packages:
7373
url: "https://pub.dev"
7474
source: hosted
7575
version: "3.0.2"
76+
equatable:
77+
dependency: "direct main"
78+
description:
79+
name: equatable
80+
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
81+
url: "https://pub.dev"
82+
source: hosted
83+
version: "2.0.5"
7684
ffi:
7785
dependency: "direct main"
7886
description:
@@ -170,13 +178,13 @@ packages:
170178
source: hosted
171179
version: "0.12.14"
172180
meta:
173-
dependency: transitive
181+
dependency: "direct main"
174182
description:
175183
name: meta
176-
sha256: "12307e7f0605ce3da64cf0db90e5fcab0869f3ca03f76be6bb2991ce0a55e82b"
184+
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
177185
url: "https://pub.dev"
178186
source: hosted
179-
version: "1.9.0"
187+
version: "1.9.1"
180188
mime:
181189
dependency: transitive
182190
description:
@@ -386,4 +394,4 @@ packages:
386394
source: hosted
387395
version: "3.1.1"
388396
sdks:
389-
dart: ">=2.19.4 <3.0.0"
397+
dart: ">=2.19.4 <4.0.0"

pubspec.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: rps
22
description: rps (Run Pubspec Script) allows you to define and run scripts from pubspec.yaml.
3-
version: 0.7.0
3+
version: 0.8.0-dev.1
44
repository: https://github.com/gonuit/rps
55
homepage: https://github.com/gonuit/rps
66
issue_tracker: https://github.com/gonuit/rps/issues
@@ -9,12 +9,14 @@ executables:
99
rps:
1010

1111
environment:
12-
sdk: ">=2.19.4 <3.0.0"
12+
sdk: ">=2.19.4 <4.0.0"
1313

1414
dependencies:
1515
yaml: ^3.1.1
1616
path: ^1.8.2
1717
ffi: ^2.0.1
18+
equatable: ^2.0.5
19+
meta: ^1.9.1
1820

1921
dev_dependencies:
2022
flutter_lints: ^2.0.1

0 commit comments

Comments
 (0)