Skip to content

Commit e88f7cf

Browse files
committed
Add "pub run --list" command to show all available executables.
See #1323
1 parent 0739705 commit e88f7cf

File tree

4 files changed

+174
-8
lines changed

4 files changed

+174
-8
lines changed

lib/src/command/run.dart

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import 'package:path/path.dart' as p;
1010
import '../command.dart';
1111
import '../executable.dart';
1212
import '../io.dart';
13+
import '../log.dart' as log;
14+
import '../package.dart';
1315
import '../utils.dart';
1416

1517
/// Handles the `run` pub command.
@@ -20,15 +22,21 @@ class RunCommand extends PubCommand {
2022
bool get allowTrailingOptions => false;
2123

2224
RunCommand() {
23-
argParser.addFlag("checked", abbr: "c",
24-
help: "Enable runtime type checks and assertions.");
25+
argParser.addFlag("checked",
26+
abbr: "c", help: "Enable runtime type checks and assertions.");
27+
argParser.addFlag('list',
28+
help: 'List all available executables.', negatable: false);
2529
argParser.addOption("mode",
2630
help: 'Mode to run transformers in.\n'
27-
'(defaults to "release" for dependencies, "debug" for '
28-
'entrypoint)');
31+
'(defaults to "release" for dependencies, "debug" for entrypoint)');
2932
}
3033

3134
Future run() async {
35+
if (argResults['list']) {
36+
_listExecutables();
37+
return;
38+
}
39+
3240
if (argResults.rest.isEmpty) {
3341
usageException("Must specify an executable to run.");
3442
}
@@ -45,10 +53,10 @@ class RunCommand extends PubCommand {
4553
executable = components[1];
4654

4755
if (p.split(executable).length > 1) {
48-
// TODO(nweiz): Use adjacent strings when the new async/await compiler
49-
// lands.
50-
usageException("Cannot run an executable in a subdirectory of a " +
51-
"dependency.");
56+
// TODO(nweiz): Use adjacent strings when the new async/await compiler
57+
// lands.
58+
usageException(
59+
"Cannot run an executable in a subdirectory of a dependency.");
5260
}
5361
} else if (onlyIdentifierRegExp.hasMatch(executable)) {
5462
// "pub run foo" means the same thing as "pub run foo:foo" as long as
@@ -69,4 +77,48 @@ class RunCommand extends PubCommand {
6977
checked: argResults['checked'], mode: mode);
7078
await flushThenExit(exitCode);
7179
}
80+
81+
/// Lists all executables reachable from [entrypoint].
82+
void _listExecutables() {
83+
var ownExecutables = _listExecutablesFor(entrypoint.root)
84+
.map((executable) => p.join('bin', executable));
85+
86+
var packageExecutables = entrypoint.root.immediateDependencies
87+
.map((dep) => entrypoint.packageGraph.packages[dep.name])
88+
.expand((Package package) => _listExecutablesFor(package).map(
89+
(executable) => _normalizeExecutable(package.name, executable)));
90+
91+
var allExecutables = []
92+
..addAll(ownExecutables)
93+
..addAll(packageExecutables)
94+
..sort();
95+
96+
log.message(log.bold(allExecutables.join('\n')));
97+
}
98+
99+
/// Lists all Dart files in the `bin` directory of the [package].
100+
///
101+
/// Returns paths without extension relative to the `bin` directory of the
102+
/// [package].
103+
List<String> _listExecutablesFor(Package package) {
104+
return package
105+
.listFiles(beneath: 'bin', recursive: true)
106+
.where((executable) => p.extension(executable) == '.dart')
107+
.map((executable) => p.relative(executable, from: package.path('bin')))
108+
.map((executable) => p.withoutExtension(executable));
109+
}
110+
111+
/// If [executable] is the same as [package] name, returns unmodified
112+
/// [executable].
113+
///
114+
/// _normalizeExecutable('foo', 'foo') // -> 'foo'
115+
///
116+
/// Otherwise prepends [executable] with [package] name and separates them
117+
/// with the colon.
118+
///
119+
/// _normalizeExecutable('foo', 'bar') // -> 'foo:bar'
120+
/// _normalizeExecutable('foo', 'bar/baz') // -> 'foo:bar/baz'
121+
String _normalizeExecutable(String package, String executable) {
122+
return package == executable ? package : '$package:$executable';
123+
}
72124
}

test/run/errors_if_no_executable_is_given_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Must specify an executable to run.
2020
Usage: pub run <executable> [args...]
2121
-h, --help Print this usage information.
2222
-c, --[no-]checked Enable runtime type checks and assertions.
23+
--list List all available executables.
2324
--mode Mode to run transformers in.
2425
(defaults to "release" for dependencies, "debug" for entrypoint)
2526

test/run/errors_if_path_in_dependency_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Cannot run an executable in a subdirectory of a dependency.
2727
Usage: pub run <executable> [args...]
2828
-h, --help Print this usage information.
2929
-c, --[no-]checked Enable runtime type checks and assertions.
30+
--list List all available executables.
3031
--mode Mode to run transformers in.
3132
(defaults to "release" for dependencies, "debug" for entrypoint)
3233

test/run/list_test.dart

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:path/path.dart' as p;
6+
7+
import '../descriptor.dart' as d;
8+
import '../test_pub.dart';
9+
10+
main() {
11+
integration("lists executables in entrypoint's bin", () {
12+
d.dir(appPath, [
13+
d.appPubspec(),
14+
d.dir('bin', [
15+
d.file('foo.dart'),
16+
d.file('bar.dart'),
17+
d.dir('sub', [d.file('baz.dart')])
18+
])
19+
]).create();
20+
21+
schedulePub(
22+
args: ['run', '--list'],
23+
output: '''
24+
${p.join('bin', 'bar')}
25+
${p.join('bin', 'foo')}
26+
${p.join('bin', 'sub', 'baz')}
27+
''');
28+
});
29+
30+
integration('lists only Dart files', () {
31+
d.dir(appPath, [
32+
d.appPubspec(),
33+
d.dir('bin', [d.file('foo.dart'), d.file('bar.sh')])
34+
]).create();
35+
36+
schedulePub(args: ['run', '--list'], output: p.join('bin', 'foo'));
37+
});
38+
39+
integration('lists executables from a dependency', () {
40+
d.dir('foo', [
41+
d.libPubspec('foo', '1.0.0'),
42+
d.dir('bin', [
43+
d.file('bar.dart'),
44+
d.dir('sub', [d.file('baz.dart'),])
45+
])
46+
]).create();
47+
48+
d.dir(appPath, [
49+
d.appPubspec({
50+
'foo': {'path': '../foo'}
51+
})
52+
]).create();
53+
54+
pubGet();
55+
schedulePub(
56+
args: ['run', '--list'],
57+
output: '''
58+
foo:bar
59+
foo:${p.join('sub', 'baz')}
60+
''');
61+
});
62+
63+
integration('lists executables only from immediate dependencies', () {
64+
d.dir('foo', [
65+
d.libPubspec('foo', '1.0.0'),
66+
d.dir('bin', [d.file('baz.dart')])
67+
]).create();
68+
69+
d.dir('bar', [
70+
d.libPubspec('bar', '1.0.0', deps: {
71+
'foo': {'path': '../foo'}
72+
}),
73+
d.dir('bin', [d.file('baz.dart')])
74+
]).create();
75+
76+
d.dir(appPath, [
77+
d.appPubspec({
78+
'bar': {'path': '../bar'}
79+
})
80+
]).create();
81+
82+
pubGet();
83+
schedulePub(args: ['run', '--list'], output: 'bar:baz');
84+
});
85+
86+
integration('normilizes executable names', () {
87+
d.dir('foo', [
88+
d.libPubspec('foo', '1.0.0'),
89+
d.dir('bin', [d.file('foo.dart'), d.file('bar.dart')])
90+
]).create();
91+
92+
d.dir(appPath, [
93+
d.appPubspec({
94+
'foo': {'path': '../foo'}
95+
})
96+
]).create();
97+
98+
pubGet();
99+
schedulePub(
100+
args: ['run', '--list'],
101+
output: '''
102+
foo
103+
foo:bar
104+
''');
105+
});
106+
107+
integration('prints blank line when no executables found', () {
108+
d.dir(appPath, [d.appPubspec()]).create();
109+
110+
schedulePub(args: ['run', '--list'], output: '\n');
111+
});
112+
}

0 commit comments

Comments
 (0)