Skip to content

Commit ae0290b

Browse files
committed
Add utilities for different base directories
This should be used to eventually fix dart-lang/sdk#41560. See dart-lang/sdk#49166 (comment) Test plan: ``` $ dart test 00:01 +16: All tests passed! ``` run `dart doc` and inspect docs for correctness.
1 parent 1fd87c1 commit ae0290b

File tree

5 files changed

+190
-50
lines changed

5 files changed

+190
-50
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
# Name/Organization <email address>
55

66
Google Inc.
7+
Calvin Lee <[email protected]>

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.4.2
2+
3+
- Introduce `applicationCacheHome`, `applicationDataHome`,
4+
`applicationRuntimeDir` and `applicationStateHome`.
5+
16
## 0.4.1
27

38
- Fix a broken link in the readme.

lib/cli_util.dart

Lines changed: 158 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,23 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
/// Utilities to return the Dart SDK location.
5+
/// Utilities for CLI programs written in dart.
6+
///
7+
/// This library contains information for returning the location of the dart
8+
/// SDK, and other directories that command-line applications may need to
9+
/// access. This library aims follows best practices for each platform, honoring
10+
/// the [XDG Base Directory Specification][1] on Linux and
11+
/// [File System Basics][2] on Mac OS.
12+
///
13+
/// Many functions require a `productName`, as data should be stored in a
14+
/// directory unique to your application, as to not avoid clashes with other
15+
/// programs on the same machine. For example, if you are writing a command-line
16+
/// application named 'zinger' then `productName` on Linux could be `zinger`. On
17+
/// MacOS, this should be your bundle identifier (for example,
18+
/// `com.example.Zinger`).
19+
///
20+
/// [1]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
21+
/// [2]: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW1
622
library cli_util;
723

824
import 'dart:async';
@@ -13,14 +29,34 @@ import 'package:path/path.dart' as path;
1329
/// Return the path to the current Dart SDK.
1430
String getSdkPath() => path.dirname(path.dirname(Platform.resolvedExecutable));
1531

32+
// executable are alo mentioned in the XDG spec, but these do not have as well
33+
// defined of locations on Windows, MacOS.
34+
enum _BaseDirectory { cache, config, data, runtime, state }
35+
36+
/// Get the user-specific application cache folder for the current platform.
37+
///
38+
/// This is a location appropriate for storing non-essential files that may be
39+
/// removed at any point. This method won't create the directory; It will merely
40+
/// return the recommended location.
41+
///
42+
/// The folder location depends on the platform:
43+
/// * `%LOCALAPPDATA%\<productName>` on **Windows**,
44+
/// * `$HOME/Library/Caches/<productName>` on **Mac OS**,
45+
/// * `$XDG_CACHE_HOME/<productName>` on **Linux**
46+
/// (if `$XDG_CACHE_HOME` is defined), and,
47+
/// * `$HOME/.cache/` otherwise.
48+
///
49+
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
50+
/// are undefined.
51+
String applicationCacheHome(String productName) =>
52+
path.join(_baseDirectory(_BaseDirectory.cache), productName);
53+
1654
/// Get the user-specific application configuration folder for the current
1755
/// platform.
1856
///
1957
/// This is a location appropriate for storing application specific
20-
/// configuration for the current user. The [productName] should be unique to
21-
/// avoid clashes with other applications on the same machine. This method won't
22-
/// actually create the folder, merely return the recommended location for
23-
/// storing user-specific application configuration.
58+
/// configuration for the current user. This method won't create the directory;
59+
/// It will merely return the recommended location.
2460
///
2561
/// The folder location depends on the platform:
2662
/// * `%APPDATA%\<productName>` on **Windows**,
@@ -29,54 +65,143 @@ String getSdkPath() => path.dirname(path.dirname(Platform.resolvedExecutable));
2965
/// (if `$XDG_CONFIG_HOME` is defined), and,
3066
/// * `$HOME/.config/<productName>` otherwise.
3167
///
32-
/// This aims follows best practices for each platform, honoring the
33-
/// [XDG Base Directory Specification][1] on Linux and [File System Basics][2]
34-
/// on Mac OS.
68+
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
69+
/// are undefined.
70+
String applicationConfigHome(String productName) =>
71+
path.join(_baseDirectory(_BaseDirectory.config), productName);
72+
73+
/// Get the user-specific application data folder for the current platform.
3574
///
36-
/// Throws an [EnvironmentNotFoundException] if `%APPDATA%` or `$HOME` is needed
37-
/// but undefined.
75+
/// This is a location appropriate for storing application specific
76+
/// semi-permanent data for the current user. This method won't create the
77+
/// directory; It will merely return the recommended location.
3878
///
39-
/// [1]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
40-
/// [2]: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW1
41-
String applicationConfigHome(String productName) =>
42-
path.join(_configHome, productName);
79+
/// The folder location depends on the platform:
80+
/// * `%APPDATA%\<productName>` on **Windows**,
81+
/// * `$HOME/Library/Application Support/<productName>` on **Mac OS**,
82+
/// * `$XDG_DATA_HOME/<productName>` on **Linux**
83+
/// (if `$XDG_DATA_HOME` is defined), and,
84+
/// * `$HOME/.local/share/<productName>` otherwise.
85+
///
86+
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
87+
/// are undefined.
88+
String applicationDataHome(String productName) =>
89+
path.join(_baseDirectory(_BaseDirectory.data), productName);
90+
91+
/// Get the runtime data folder for the current platform.
92+
///
93+
/// This is a location appropriate for storing runtime data for the current
94+
/// session. This method won't create the directory; It will merely return the
95+
/// recommended location.
96+
///
97+
/// The folder location depends on the platform:
98+
/// * `%LOCALAPPDATA%\<productName>` on **Windows**,
99+
/// * `$HOME/Library/Application Support/<productName>` on **Mac OS**,
100+
/// * `$XDG_DATA_HOME/<productName>` on **Linux**
101+
/// (if `$XDG_DATA_HOME` is defined), and,
102+
/// * `$HOME/.local/share/<productName>` otherwise.
103+
///
104+
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
105+
/// are undefined.
106+
String applicationRuntimeDir(String productName) =>
107+
path.join(_baseDirectory(_BaseDirectory.runtime), productName);
43108

44-
String get _configHome {
109+
/// Get the user-specific application state folder for the current platform.
110+
///
111+
/// This is a location appropriate for storing application specific state
112+
/// for the current user. This differs from [applicationDataHome] insomuch as it
113+
/// should contain data which should persist restarts, but is not important
114+
/// enough to be backed up. This method won't create the directory;
115+
// It will merely return the recommended location.
116+
///
117+
/// The folder location depends on the platform:
118+
/// * `%APPDATA%\<productName>` on **Windows**,
119+
/// * `$HOME/Library/Application Support/<productName>` on **Mac OS**,
120+
/// * `$XDG_DATA_HOME/<productName>` on **Linux**
121+
/// (if `$XDG_DATA_HOME` is defined), and,
122+
/// * `$HOME/.local/share/<productName>` otherwise.
123+
///
124+
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
125+
/// are undefined.
126+
String applicationStateHome(String productName) =>
127+
path.join(_baseDirectory(_BaseDirectory.state), productName);
128+
129+
String _baseDirectory(_BaseDirectory dir) {
45130
if (Platform.isWindows) {
46-
final appdata = _env['APPDATA'];
47-
if (appdata == null) {
48-
throw EnvironmentNotFoundException(
49-
'Environment variable %APPDATA% is not defined!');
131+
switch (dir) {
132+
case _BaseDirectory.config:
133+
case _BaseDirectory.data:
134+
return _fetchEnvRequired('APPDATA');
135+
case _BaseDirectory.cache:
136+
case _BaseDirectory.runtime:
137+
case _BaseDirectory.state:
138+
return _fetchEnvRequired('LOCALAPPDATA');
50139
}
51-
return appdata;
52140
}
53141

54142
if (Platform.isMacOS) {
55-
return path.join(_home, 'Library', 'Application Support');
143+
switch (dir) {
144+
case _BaseDirectory.config:
145+
case _BaseDirectory.data:
146+
case _BaseDirectory.state:
147+
return path.join(_home, 'Library', 'Application Support');
148+
case _BaseDirectory.cache:
149+
return path.join(_home, 'Library', 'Caches');
150+
case _BaseDirectory.runtime:
151+
// https://stackoverflow.com/a/76799489
152+
return path.join(_home, 'Library', 'Caches', 'TemporaryItems');
153+
}
56154
}
57155

58156
if (Platform.isLinux) {
59-
final xdgConfigHome = _env['XDG_CONFIG_HOME'];
60-
if (xdgConfigHome != null) {
61-
return xdgConfigHome;
157+
String xdgEnv;
158+
switch (dir) {
159+
case _BaseDirectory.config:
160+
xdgEnv = 'XDG_CONFIG_HOME';
161+
break;
162+
case _BaseDirectory.data:
163+
xdgEnv = 'XDG_DATA_HOME';
164+
break;
165+
case _BaseDirectory.state:
166+
xdgEnv = 'XDG_STATE_HOME';
167+
break;
168+
case _BaseDirectory.cache:
169+
xdgEnv = 'XDG_CACHE_HOME';
170+
break;
171+
case _BaseDirectory.runtime:
172+
xdgEnv = 'XDG_RUNTIME_HOME';
173+
break;
174+
}
175+
final val = _env[xdgEnv];
176+
if (val != null) {
177+
return val;
62178
}
63-
// XDG Base Directory Specification says to use $HOME/.config/ when
64-
// $XDG_CONFIG_HOME isn't defined.
65-
return path.join(_home, '.config');
66179
}
67180

68181
// We have no guidelines, perhaps we should just do: $HOME/.config/
69182
// same as XDG specification would specify as fallback.
70-
return path.join(_home, '.config');
183+
switch (dir) {
184+
case _BaseDirectory.runtime:
185+
case _BaseDirectory.cache:
186+
return path.join(_home, '.cache');
187+
case _BaseDirectory.config:
188+
return path.join(_home, '.config');
189+
case _BaseDirectory.data:
190+
return path.join(_home, '.local', 'share');
191+
case _BaseDirectory.state:
192+
return path.join(_home, '.local', 'state');
193+
}
71194
}
72195

73-
String get _home {
74-
final home = _env['HOME'];
75-
if (home == null) {
196+
String get _home => _fetchEnvRequired('HOME');
197+
198+
String _fetchEnvRequired(String name) {
199+
final v = _env[name];
200+
if (v == null) {
76201
throw EnvironmentNotFoundException(
77-
r'Environment variable $HOME is not defined!');
202+
'Environment variable \$$name is not defined!');
78203
}
79-
return home;
204+
return v;
80205
}
81206

82207
class EnvironmentNotFoundException implements Exception {

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: cli_util
2-
version: 0.4.1
2+
version: 0.4.2
33
description: A library to help in building Dart command-line apps.
44
repository: https://github.com/dart-lang/cli_util
55

test/cli_util_test.dart

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,33 @@ void defineTests() {
1818
});
1919
});
2020

21-
group('applicationConfigHome', () {
22-
test('returns a non-empty string', () {
23-
expect(applicationConfigHome('dart'), isNotEmpty);
24-
});
21+
final functions = {
22+
'applicationCacheHome': applicationCacheHome,
23+
'applicationConfigHome': applicationConfigHome,
24+
'applicationDataHome': applicationDataHome,
25+
'applicationRuntimeDir': applicationRuntimeDir,
26+
'applicationStateHome': applicationStateHome,
27+
};
28+
functions.forEach((name, fn) {
29+
group(name, () {
30+
test('returns a non-empty string', () {
31+
expect(fn('dart'), isNotEmpty);
32+
});
2533

26-
test('has an ancestor folder that exists', () {
27-
final path = p.split(applicationConfigHome('dart'));
28-
// We expect that first two segments of the path exist. This is really
29-
// just a dummy check that some part of the path exists.
30-
expect(Directory(p.joinAll(path.take(2))).existsSync(), isTrue);
31-
});
34+
test('has an ancestor folder that exists', () {
35+
final path = p.split(fn('dart'));
36+
// We expect that first two segments of the path exist. This is really
37+
// just a dummy check that some part of the path exists.
38+
expect(Directory(p.joinAll(path.take(2))).existsSync(), isTrue);
39+
});
3240

33-
test('empty environment throws exception', () async {
34-
expect(() {
35-
runZoned(() => applicationConfigHome('dart'), zoneValues: {
36-
#environmentOverrides: <String, String>{},
37-
});
38-
}, throwsA(isA<EnvironmentNotFoundException>()));
41+
test('empty environment throws exception', () async {
42+
expect(() {
43+
runZoned(() => fn('dart'), zoneValues: {
44+
#environmentOverrides: <String, String>{},
45+
});
46+
}, throwsA(isA<EnvironmentNotFoundException>()));
47+
});
3948
});
4049
});
4150
}

0 commit comments

Comments
 (0)