Skip to content

Commit c4faeaf

Browse files
authored
Merge pull request #1680 from gagoman/feature/1323
Add "pub run --list" command to show all available executables #2
2 parents 5f6cda0 + 9c3d6e0 commit c4faeaf

File tree

2 files changed

+314
-12
lines changed

2 files changed

+314
-12
lines changed

lib/src/command/deps.dart

Lines changed: 89 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,26 @@
44

55
import 'dart:collection';
66

7+
import 'package:analyzer/analyzer.dart' as analyzer;
8+
import 'package:path/path.dart' as p;
9+
710
import '../ascii_tree.dart' as tree;
811
import '../command.dart';
12+
import '../dart.dart';
913
import '../log.dart' as log;
1014
import '../package.dart';
1115
import '../utils.dart';
1216

17+
/// Returns `true` if [path] looks like a Dart entrypoint.
18+
bool _isDartExecutable(String path) {
19+
try {
20+
var unit = analyzer.parseDartFile(path, parseFunctionBodies: false);
21+
return isEntrypoint(unit);
22+
} on analyzer.AnalyzerErrorGroup {
23+
return false;
24+
}
25+
}
26+
1327
/// Handles the `deps` pub command.
1428
class DepsCommand extends PubCommand {
1529
String get name => "deps";
@@ -36,6 +50,9 @@ class DepsCommand extends PubCommand {
3650
negatable: true,
3751
help: "Whether to include dev dependencies.",
3852
defaultsTo: true);
53+
54+
argParser.addFlag("executables",
55+
negatable: false, help: "List all available executables.");
3956
}
4057

4158
void run() {
@@ -44,18 +61,22 @@ class DepsCommand extends PubCommand {
4461

4562
_buffer = new StringBuffer();
4663

47-
_buffer.writeln(_labelPackage(entrypoint.root));
48-
49-
switch (argResults["style"]) {
50-
case "compact":
51-
_outputCompact();
52-
break;
53-
case "list":
54-
_outputList();
55-
break;
56-
case "tree":
57-
_outputTree();
58-
break;
64+
if (argResults['executables']) {
65+
_outputExecutables();
66+
} else {
67+
_buffer.writeln(_labelPackage(entrypoint.root));
68+
69+
switch (argResults["style"]) {
70+
case "compact":
71+
_outputCompact();
72+
break;
73+
case "list":
74+
_outputList();
75+
break;
76+
case "tree":
77+
_outputTree();
78+
break;
79+
}
5980
}
6081

6182
log.message(_buffer);
@@ -231,4 +252,60 @@ class DepsCommand extends PubCommand {
231252
'was generated, please run "pub get" again.');
232253
return null;
233254
}
255+
256+
/// Outputs all executables reachable from [entrypoint].
257+
void _outputExecutables() {
258+
var packages = []
259+
..add(entrypoint.root)
260+
..addAll((_includeDev
261+
? entrypoint.root.immediateDependencies
262+
: entrypoint.root.dependencies)
263+
.map((dep) => entrypoint.packageGraph.packages[dep.name]));
264+
265+
for (var package in packages) {
266+
var executables = _getExecutablesFor(package);
267+
if (executables.isNotEmpty) {
268+
_buffer.writeln(_formatExecutables(package.name, executables.toList()));
269+
}
270+
}
271+
}
272+
273+
/// Lists all Dart files in the `bin` directory of the [package].
274+
///
275+
/// Returns file names without extensions.
276+
List<String> _getExecutablesFor(Package package) => package.executableIds
277+
.where((e) => _isDartExecutable(p.absolute(package.dir, e.path)))
278+
.map((e) => p.basenameWithoutExtension(e.path));
279+
280+
/// Returns formatted string that lists [executables] for the [packageName].
281+
/// Examples:
282+
///
283+
/// _formatExecutables('foo', ['foo']) // -> 'foo'
284+
/// _formatExecutables('foo', ['bar']) // -> 'foo:bar'
285+
/// _formatExecutables('foo', ['bar', 'foo']) // -> 'foo: foo, bar'
286+
///
287+
/// Note the leading space before first executable and sorting order in the
288+
/// last example.
289+
String _formatExecutables(String packageName, List<String> executables) {
290+
if (executables.length == 1) {
291+
// If executable matches the package name omit the name of executable in
292+
// the output.
293+
return executables.first != packageName
294+
? '${packageName}:${log.bold(executables.first)}'
295+
: log.bold(executables.first);
296+
}
297+
298+
// Sort executables to make executable that matches the package name to be
299+
// the first in the list.
300+
executables.sort((e1, e2) {
301+
if (e1 == packageName)
302+
return -1;
303+
else if (e2 == packageName)
304+
return 1;
305+
else
306+
return e1.compareTo(e2);
307+
});
308+
309+
return '${packageName}: ${executables.map(log.bold).join(', ')}';
310+
}
234311
}

test/deps/executables_test.dart

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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:test/test.dart';
6+
7+
import '../descriptor.dart' as d;
8+
import '../test_pub.dart';
9+
10+
const _validMain = 'main() {}';
11+
12+
main() {
13+
_testExecutablesOutput(output, {bool dev: true}) => () async {
14+
await pubGet();
15+
await runPub(
16+
args: ['deps', '--executables']
17+
..addAll(dev ? ['--dev'] : ['--no-dev']),
18+
output: output);
19+
};
20+
21+
_testAllDepsOutput(output) => _testExecutablesOutput(output);
22+
_testNonDevDepsOutput(output) => _testExecutablesOutput(output, dev: false);
23+
24+
group("lists nothing when no executables found", () {
25+
setUp(() async {
26+
await d.dir(appPath, [d.appPubspec()]).create();
27+
});
28+
29+
test("all dependencies", _testAllDepsOutput('\n'));
30+
test("non-dev dependencies", _testNonDevDepsOutput('\n'));
31+
});
32+
33+
group("skips non-Dart executables", () {
34+
setUp(() async {
35+
await d.dir(appPath, [
36+
d.appPubspec(),
37+
d.dir('bin', [d.file('foo.py'), d.file('bar.sh')])
38+
]).create();
39+
});
40+
41+
test("all dependencies", _testAllDepsOutput('\n'));
42+
test("non-dev dependencies", _testNonDevDepsOutput('\n'));
43+
});
44+
45+
group("skips Dart executables which are not parsable", () {
46+
setUp(() async {
47+
await d.dir(appPath, [
48+
d.appPubspec(),
49+
d.dir('bin', [d.file('foo.dart', 'main() {')])
50+
]).create();
51+
});
52+
53+
test("all dependencies", _testAllDepsOutput('\n'));
54+
test("non-dev dependencies", _testNonDevDepsOutput('\n'));
55+
});
56+
57+
group("skips Dart executables without entrypoints", () {
58+
setUp(() async {
59+
await d.dir(appPath, [
60+
d.appPubspec(),
61+
d.dir(
62+
'bin', [d.file('foo.dart'), d.file('bar.dart', 'main(x, y, z) {}')])
63+
]).create();
64+
});
65+
66+
test("all dependencies", _testAllDepsOutput('\n'));
67+
test("non-dev dependencies", _testNonDevDepsOutput('\n'));
68+
});
69+
70+
group("lists valid Dart executables with entrypoints", () {
71+
setUp(() async {
72+
await d.dir(appPath, [
73+
d.appPubspec(),
74+
d.dir('bin',
75+
[d.file('foo.dart', _validMain), d.file('bar.dart', _validMain)])
76+
]).create();
77+
});
78+
79+
test("all dependencies", _testAllDepsOutput('myapp: bar, foo'));
80+
test("non-dev dependencies", _testNonDevDepsOutput('myapp: bar, foo'));
81+
});
82+
83+
group("skips executables in sub directories", () {
84+
setUp(() async {
85+
await d.dir(appPath, [
86+
d.appPubspec(),
87+
d.dir('bin', [
88+
d.file('foo.dart', _validMain),
89+
d.dir('sub', [d.file('bar.dart', _validMain)])
90+
])
91+
]).create();
92+
});
93+
94+
test("all dependencies", _testAllDepsOutput('myapp:foo'));
95+
test("non-dev dependencies", _testNonDevDepsOutput('myapp:foo'));
96+
});
97+
98+
group("lists executables from a dependency", () {
99+
setUp(() async {
100+
await d.dir('foo', [
101+
d.libPubspec('foo', '1.0.0'),
102+
d.dir('bin', [d.file('bar.dart', _validMain)])
103+
]).create();
104+
105+
await d.dir(appPath, [
106+
d.appPubspec({
107+
'foo': {'path': '../foo'}
108+
})
109+
]).create();
110+
});
111+
112+
test("all dependencies", _testAllDepsOutput('foo:bar'));
113+
test("non-dev dependencies", _testNonDevDepsOutput('foo:bar'));
114+
});
115+
116+
group("lists executables only from immediate dependencies", () {
117+
setUp(() async {
118+
await d.dir(appPath, [
119+
d.appPubspec({
120+
'foo': {'path': '../foo'}
121+
})
122+
]).create();
123+
124+
await d.dir('foo', [
125+
d.libPubspec('foo', '1.0.0', deps: {
126+
'baz': {'path': '../baz'}
127+
}),
128+
d.dir('bin', [d.file('bar.dart', _validMain)])
129+
]).create();
130+
131+
await d.dir('baz', [
132+
d.libPubspec('baz', '1.0.0'),
133+
d.dir('bin', [d.file('qux.dart', _validMain)])
134+
]).create();
135+
});
136+
137+
test("all dependencies", _testAllDepsOutput('foo:bar'));
138+
test("non-dev dependencies", _testNonDevDepsOutput('foo:bar'));
139+
});
140+
141+
group("applies formatting before printing executables", () {
142+
setUp(() async {
143+
await d.dir(appPath, [
144+
d.appPubspec({
145+
'foo': {'path': '../foo'},
146+
'bar': {'path': '../bar'}
147+
}),
148+
d.dir('bin', [d.file('myapp.dart', _validMain)])
149+
]).create();
150+
151+
await d.dir('foo', [
152+
d.libPubspec('foo', '1.0.0'),
153+
d.dir('bin',
154+
[d.file('baz.dart', _validMain), d.file('foo.dart', _validMain)])
155+
]).create();
156+
157+
await d.dir('bar', [
158+
d.libPubspec('bar', '1.0.0'),
159+
d.dir('bin', [d.file('qux.dart', _validMain)])
160+
]).create();
161+
});
162+
163+
test("all dependencies", _testAllDepsOutput('''
164+
myapp
165+
bar:qux
166+
foo: foo, baz'''));
167+
test("non-dev dependencies", _testNonDevDepsOutput('''
168+
myapp
169+
bar:qux
170+
foo: foo, baz'''));
171+
});
172+
173+
group("dev dependencies", () {
174+
setUp(() async {
175+
await d.dir('foo', [
176+
d.libPubspec('foo', '1.0.0'),
177+
d.dir('bin', [d.file('bar.dart', _validMain)])
178+
]).create();
179+
180+
await d.dir(appPath, [
181+
d.pubspec({
182+
'name': 'myapp',
183+
'dev_dependencies': {
184+
'foo': {'path': '../foo'}
185+
}
186+
})
187+
]).create();
188+
});
189+
190+
test("are listed if --dev flag is set", _testAllDepsOutput('foo:bar'));
191+
test("are skipped if --no-dev flag is set", _testNonDevDepsOutput('\n'));
192+
});
193+
194+
group("overriden dependencies executables", () {
195+
setUp(() async {
196+
await d.dir('foo-1.0', [
197+
d.libPubspec('foo', '1.0.0'),
198+
d.dir('bin', [d.file('bar.dart', _validMain)])
199+
]).create();
200+
201+
await d.dir('foo-2.0', [
202+
d.libPubspec('foo', '2.0.0'),
203+
d.dir('bin',
204+
[d.file('bar.dart', _validMain), d.file('baz.dart', _validMain)])
205+
]).create();
206+
207+
await d.dir(appPath, [
208+
d.pubspec({
209+
'name': 'myapp',
210+
'dependencies': {
211+
'foo': {'path': '../foo-1.0'}
212+
},
213+
'dependency_overrides': {
214+
'foo': {'path': '../foo-2.0'}
215+
}
216+
})
217+
]).create();
218+
});
219+
220+
test(
221+
'are listed if --dev flag is set', _testAllDepsOutput('foo: bar, baz'));
222+
test('are listed if --no-dev flag is set',
223+
_testNonDevDepsOutput('foo: bar, baz'));
224+
});
225+
}

0 commit comments

Comments
 (0)