diff --git a/.github/ISSUE_TEMPLATE/cli_util.md b/.github/ISSUE_TEMPLATE/cli_util.md new file mode 100644 index 000000000..935fd4577 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/cli_util.md @@ -0,0 +1,5 @@ +--- +name: "package:cli_util" +about: "Create a bug or file a feature request against package:cli_util." +labels: "package:cli_util" +--- \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml index 8d48ebdfa..75218dbfa 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -24,6 +24,10 @@ - changed-files: - any-glob-to-any-file: 'pkgs/cli_config/**' +'package:cli_util': + - changed-files: + - any-glob-to-any-file: 'pkgs/cli_util/**' + 'package:clock': - changed-files: - any-glob-to-any-file: 'pkgs/clock/**' diff --git a/.github/workflows/cli_util.yaml b/.github/workflows/cli_util.yaml new file mode 100644 index 000000000..23d6fcaa6 --- /dev/null +++ b/.github/workflows/cli_util.yaml @@ -0,0 +1,66 @@ +name: package:cli_util + +on: + # Run on PRs and pushes to the default branch. + push: + branches: [ main ] + paths: + - '.github/workflows/cli_util.yml' + - 'pkgs/cli_util/**' + pull_request: + branches: [ main ] + paths: + - '.github/workflows/cli_util.yml' + - 'pkgs/cli_util/**' + schedule: + - cron: "0 0 * * 0" + +env: + PUB_ENVIRONMENT: bot.github + + +defaults: + run: + working-directory: pkgs/cli_util/ + +jobs: + analyze: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sdk: [dev] + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Check formatting + run: dart format --output=none --set-exit-if-changed . + if: always() && steps.install.outcome == 'success' + - name: Analyze code + run: dart analyze --fatal-infos + if: always() && steps.install.outcome == 'success' + + test: + needs: analyze + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + sdk: ['3.4', dev] + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Run tests + run: dart test --test-randomize-ordering-seed=random + if: always() && steps.install.outcome == 'success' diff --git a/README.md b/README.md index c4f28502f..83f5b16f9 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ don't naturally belong to other topic monorepos (like | [boolean_selector](pkgs/boolean_selector/) | A flexible syntax for boolean expressions, based on a simplified version of Dart's expression syntax. | [![pub package](https://img.shields.io/pub/v/boolean_selector.svg)](https://pub.dev/packages/boolean_selector) | | [browser_launcher](pkgs/browser_launcher/) | Provides a standardized way to launch web browsers for testing and tools. | [![pub package](https://img.shields.io/pub/v/browser_launcher.svg)](https://pub.dev/packages/browser_launcher) | | [cli_config](pkgs/cli_config/) | A library to take config values from configuration files, CLI arguments, and environment variables. | [![pub package](https://img.shields.io/pub/v/cli_config.svg)](https://pub.dev/packages/cli_config) | +| [cli_util](pkgs/cli_util/) | A library to help in building Dart command-line apps. | [![pub package](https://img.shields.io/pub/v/cli_util.svg)](https://pub.dev/packages/cli_util) | | [clock](pkgs/clock/) | A fakeable wrapper for dart:core clock APIs. | [![pub package](https://img.shields.io/pub/v/clock.svg)](https://pub.dev/packages/clock) | | [coverage](pkgs/coverage/) | Coverage data manipulation and formatting. | [![pub package](https://img.shields.io/pub/v/coverage.svg)](https://pub.dev/packages/coverage) | | [csslib](pkgs/csslib/) | A library for parsing and analyzing CSS (Cascading Style Sheets). | [![pub package](https://img.shields.io/pub/v/csslib.svg)](https://pub.dev/packages/csslib) | diff --git a/pkgs/cli_util/.gitignore b/pkgs/cli_util/.gitignore new file mode 100644 index 000000000..49ce72d76 --- /dev/null +++ b/pkgs/cli_util/.gitignore @@ -0,0 +1,3 @@ +.dart_tool/ +.packages +pubspec.lock diff --git a/pkgs/cli_util/AUTHORS b/pkgs/cli_util/AUTHORS new file mode 100644 index 000000000..7a6d1d960 --- /dev/null +++ b/pkgs/cli_util/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the cli_util project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/pkgs/cli_util/CHANGELOG.md b/pkgs/cli_util/CHANGELOG.md new file mode 100644 index 000000000..9e77b8fe2 --- /dev/null +++ b/pkgs/cli_util/CHANGELOG.md @@ -0,0 +1,105 @@ +## 0.4.2 + +- Add `sdkPath` getter, deprecate `getSdkPath` function. +* Move to `dart-lang/tools` monorepo. + +## 0.4.1 + +- Fix a broken link in the readme. +- Require Dart 3.0. + +## 0.4.0 + +- Remove the deprecated method `getSdkDir()` (instead, use `getSdkPath()`). +- Require Dart 2.19. + +## 0.3.5 +- Make `applicationConfigHome` throw an `Exception` when it fails to find a + configuration folder. + +## 0.3.4 + +- Introduce `applicationConfigHome` for making it easy to consistently find the + user-specific application configuration folder. + +## 0.3.3 + +- Reverted `meta` constraint to `^1.3.0`. + +## 0.3.2 + +- Update `meta` constraint to `>=1.3.0 <3.0.0`. + +## 0.3.1 + +- Fix a bug in `AnsiProgress` where the spinning character doesn't every update. + +## 0.3.0 + +- Stable null safety release. + +## 0.3.0-nullsafety.0 + +- Updated to support 2.12.0 and null safety. + +## 0.2.1 + +## 0.2.0 + +- Add `Logger.write` and `Logger.writeCharCode` methods which write without + printing a trailing newline. + +## 0.1.4 + +- Add `Ansi.reversed` getter. + +## 0.1.3+2 + +- Update Dart SDK constraint to < 3.0.0. + +## 0.1.3+1 + +- Update Dart SDK to 2.0.0-dev. + +## 0.1.3 + +- In verbose mode, instead of printing the diff from the last log message, + print the total time since the tool started +- Change to not buffer the last log message sent in verbose logging mode +- Expose more classes from the logging library + +## 0.1.2+1 + +- Remove unneeded change to Dart SDK constraint. + +## 0.1.2 + +- Fix a bug in `getSdkDir` (#21) + +## 0.1.1 + +- Updated to the output for indeterminate progress +- Exposed a `Logger.isVerbose` getter + +## 0.1.0 + +- Added a new `getSdkPath()` method to get the location of the SDK (this uses the new + `Platform.resolvedExecutable` API to locate the SDK) +- Deprecated `getSdkDir()` in favor of `getSdkPath()` +- Add the `cli_logging.dart` library - utilities to display output and progress + +## 0.0.1+3 + +- Find SDK properly when invoked from inside SDK tests. + +## 0.0.1+2 + +- Support an executable in a symlinked directory. + +## 0.0.1+1 + +- Fix for when the dart executable can't be found by `which`. + +## 0.0.1 + +- Initial version diff --git a/pkgs/cli_util/LICENSE b/pkgs/cli_util/LICENSE new file mode 100644 index 000000000..dbd2843a0 --- /dev/null +++ b/pkgs/cli_util/LICENSE @@ -0,0 +1,27 @@ +Copyright 2015, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkgs/cli_util/README.md b/pkgs/cli_util/README.md new file mode 100644 index 000000000..b86e8c084 --- /dev/null +++ b/pkgs/cli_util/README.md @@ -0,0 +1,68 @@ +[![Build Status](https://github.com/dart-lang/tools/actions/workflows/cli_util.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/cli_util.yaml) +[![Pub](https://img.shields.io/pub/v/cli_util.svg)](https://pub.dev/packages/cli_util) +[![package publisher](https://img.shields.io/pub/publisher/cli_util.svg)](https://pub.dev/packages/cli_util/publisher) + +A package to help in building Dart command-line apps. + +## What's this? + +`package:cli_util` provides: +- utilities to find the Dart SDK directory (`sdkPath`) +- utilities to find the settings directory for a tool (`applicationConfigHome()`) +- utilities to aid in showing rich CLI output and progress information (`cli_logging.dart`) + +## Locating the Dart SDK + +```dart +import 'dart:io'; + +import 'package:cli_util/cli_util.dart'; +import 'package:path/path.dart' as path; + +main(args) { + // Get SDK directory from cli_util. + var sdkDir = sdkPath; + + // Do stuff... For example, print version string + var versionFile = File(path.join(sdkDir, 'version')); + print(versionFile.readAsStringSync()); +} +``` + +## Displaying output and progress + +`package:cli_util` can also be used to help CLI tools display output and progress. +It has a logging mechanism which can help differentiate between regular tool +output and error messages, and can facilitate having a more verbose (`-v`) mode for +output. + +In addition, it can display an indeterminate progress spinner for longer running +tasks, and optionally display the elapsed time when finished: + +```dart +import 'package:cli_util/cli_logging.dart'; + +void main(List args) async { + var verbose = args.contains('-v'); + var logger = verbose ? Logger.verbose() : Logger.standard(); + + logger.stdout('Hello world!'); + logger.trace('message 1'); + await Future.delayed(Duration(milliseconds: 200)); + logger.trace('message 2'); + logger.trace('message 3'); + + var progress = logger.progress('doing some work'); + await Future.delayed(Duration(seconds: 2)); + progress.finish(showTiming: true); + + logger.stdout('All ${logger.ansi.emphasized('done')}.'); + logger.flush(); +} +``` + +## Features and bugs + +Please file feature requests and bugs at the [issue tracker][tracker]. + +[tracker]: https://github.com/dart-lang/tools/issues diff --git a/pkgs/cli_util/analysis_options.yaml b/pkgs/cli_util/analysis_options.yaml new file mode 100644 index 000000000..a30462ede --- /dev/null +++ b/pkgs/cli_util/analysis_options.yaml @@ -0,0 +1,32 @@ +# https://dart.dev/guides/language/analysis-options +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + +linter: + rules: + - avoid_bool_literals_in_conditional_expressions + - avoid_classes_with_only_static_members + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_returning_this + - avoid_unused_constructor_parameters + - avoid_void_async + - cancel_subscriptions + - join_return_with_assignment + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_runtimeType_toString + - prefer_const_declarations + - prefer_expression_function_bodies + - prefer_final_locals + - unnecessary_await_in_return + - unnecessary_raw_strings + - use_if_null_to_convert_nulls_to_bools + - use_raw_strings + - use_string_buffers diff --git a/pkgs/cli_util/example/main.dart b/pkgs/cli_util/example/main.dart new file mode 100644 index 000000000..3bae82749 --- /dev/null +++ b/pkgs/cli_util/example/main.dart @@ -0,0 +1,24 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:cli_util/cli_logging.dart'; + +Future main(List args) async { + final verbose = args.contains('-v'); + final logger = verbose ? Logger.verbose() : Logger.standard(); + + logger.stdout('Hello world!'); + logger.trace('message 1'); + await Future.delayed(const Duration(milliseconds: 200)); + logger.trace('message 2'); + logger.trace('message 3'); + + final progress = logger.progress('doing some work'); + await Future.delayed(const Duration(seconds: 2)); + progress.finish(showTiming: true); + + logger.stdout('All ${logger.ansi.emphasized('done')}.'); +} diff --git a/pkgs/cli_util/lib/cli_logging.dart b/pkgs/cli_util/lib/cli_logging.dart new file mode 100644 index 000000000..dda287bda --- /dev/null +++ b/pkgs/cli_util/lib/cli_logging.dart @@ -0,0 +1,316 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// This library contains functionality to help command-line utilities to easily +/// create aesthetic output. +library; + +import 'dart:async'; +import 'dart:io' as io; + +/// A small utility class to make it easier to work with common ANSI escape +/// sequences. +class Ansi { + /// Return whether the current stdout terminal supports ANSI escape sequences. + static bool get terminalSupportsAnsi => + io.stdout.supportsAnsiEscapes && + io.stdioType(io.stdout) == io.StdioType.terminal; + + final bool useAnsi; + + Ansi(this.useAnsi); + + String get cyan => _code('\u001b[36m'); + + String get green => _code('\u001b[32m'); + + String get magenta => _code('\u001b[35m'); + + String get red => _code('\u001b[31m'); + + String get yellow => _code('\u001b[33m'); + + String get blue => _code('\u001b[34m'); + + String get gray => _code('\u001b[1;30m'); + + String get noColor => _code('\u001b[39m'); + + String get none => _code('\u001b[0m'); + + String get bold => _code('\u001b[1m'); + + String get reversed => _code('\u001b[7m'); + + String get backspace => '\b'; + + String get bullet => io.stdout.supportsAnsiEscapes ? '•' : '-'; + + /// Display [message] in an emphasized format. + String emphasized(String message) => '$bold$message$none'; + + /// Display [message] in an subtle (gray) format. + String subtle(String message) => '$gray$message$none'; + + /// Display [message] in an error (red) format. + String error(String message) => '$red$message$none'; + + String _code(String ansiCode) => useAnsi ? ansiCode : ''; +} + +/// An abstract representation of a [Logger] - used to pretty print errors, +/// standard status messages, trace level output, and indeterminate progress. +abstract class Logger { + /// Create a normal [Logger]; this logger will not display trace level output. + factory Logger.standard({Ansi? ansi}) => StandardLogger(ansi: ansi); + + /// Create a [Logger] that will display trace level output. + /// + /// If [logTime] is `true`, this logger will display the time of the message. + factory Logger.verbose({Ansi? ansi, bool logTime = true}) => + VerboseLogger(ansi: ansi, logTime: logTime); + + Ansi get ansi; + + bool get isVerbose; + + /// Print an error message. + void stderr(String message); + + /// Print a standard status message. + void stdout(String message); + + /// Print trace output. + void trace(String message); + + /// Print text to stdout, without a trailing newline. + void write(String message); + + /// Print a character code to stdout, without a trailing newline. + void writeCharCode(int charCode); + + /// Start an indeterminate progress display. + Progress progress(String message); + + /// Flush any un-written output. + @Deprecated('This method will be removed in the future') + void flush(); +} + +/// A handle to an indeterminate progress display. +abstract class Progress { + final String message; + final Stopwatch _stopwatch; + + Progress(this.message) : _stopwatch = Stopwatch()..start(); + + Duration get elapsed => _stopwatch.elapsed; + + /// Finish the indeterminate progress display. + void finish({String? message, bool showTiming = false}); + + /// Cancel the indeterminate progress display. + void cancel(); +} + +class StandardLogger implements Logger { + @override + Ansi ansi; + + StandardLogger({Ansi? ansi}) : ansi = ansi ?? Ansi(Ansi.terminalSupportsAnsi); + + @override + bool get isVerbose => false; + + Progress? _currentProgress; + + @override + void stderr(String message) { + _cancelProgress(); + + io.stderr.writeln(message); + } + + @override + void stdout(String message) { + _cancelProgress(); + + print(message); + } + + @override + void trace(String message) {} + + @override + void write(String message) { + _cancelProgress(); + + io.stdout.write(message); + } + + @override + void writeCharCode(int charCode) { + _cancelProgress(); + + io.stdout.writeCharCode(charCode); + } + + void _cancelProgress() { + final progress = _currentProgress; + if (progress != null) { + _currentProgress = null; + progress.cancel(); + } + } + + @override + Progress progress(String message) { + _cancelProgress(); + + final progress = ansi.useAnsi + ? AnsiProgress(ansi, message) + : SimpleProgress(this, message); + _currentProgress = progress; + return progress; + } + + @override + @Deprecated('This method will be removed in the future') + void flush() {} +} + +class SimpleProgress extends Progress { + final Logger logger; + + SimpleProgress(this.logger, String message) : super(message) { + logger.stdout('$message...'); + } + + @override + void cancel() {} + + @override + void finish({String? message, bool showTiming = false}) {} +} + +class AnsiProgress extends Progress { + static const List kAnimationItems = ['/', '-', r'\', '|']; + + final Ansi ansi; + + late final Timer _timer; + + AnsiProgress(this.ansi, String message) : super(message) { + _timer = Timer.periodic(const Duration(milliseconds: 80), (t) { + _updateDisplay(); + }); + io.stdout.write('$message... '.padRight(40)); + _updateDisplay(); + } + + @override + void cancel() { + if (_timer.isActive) { + _timer.cancel(); + _updateDisplay(cancelled: true); + } + } + + @override + void finish({String? message, bool showTiming = false}) { + if (_timer.isActive) { + _timer.cancel(); + _updateDisplay(isFinal: true, message: message, showTiming: showTiming); + } + } + + void _updateDisplay( + {bool isFinal = false, + bool cancelled = false, + String? message, + bool showTiming = false}) { + var char = kAnimationItems[_timer.tick % kAnimationItems.length]; + if (isFinal || cancelled) { + char = ''; + } + io.stdout.write('${ansi.backspace}$char'); + if (isFinal || cancelled) { + if (message != null) { + io.stdout.write(message.isEmpty ? ' ' : message); + } else if (showTiming) { + final time = (elapsed.inMilliseconds / 1000.0).toStringAsFixed(1); + io.stdout.write('${time}s'); + } else { + io.stdout.write(' '); + } + io.stdout.writeln(); + } + } +} + +class VerboseLogger implements Logger { + @override + Ansi ansi; + bool logTime; + final _timer = Stopwatch()..start(); + + VerboseLogger({Ansi? ansi, this.logTime = false}) + : ansi = ansi ?? Ansi(Ansi.terminalSupportsAnsi); + + @override + bool get isVerbose => true; + + @override + void stdout(String message) { + io.stdout.writeln('${_createPrefix()}$message'); + } + + @override + void stderr(String message) { + io.stderr.writeln('${_createPrefix()}${ansi.red}$message${ansi.none}'); + } + + @override + void trace(String message) { + io.stdout.writeln('${_createPrefix()}${ansi.gray}$message${ansi.none}'); + } + + @override + void write(String message) { + io.stdout.write(message); + } + + @override + void writeCharCode(int charCode) { + io.stdout.writeCharCode(charCode); + } + + @override + Progress progress(String message) => SimpleProgress(this, message); + + @override + @Deprecated('This method will be removed in the future') + void flush() {} + + String _createPrefix() { + if (!logTime) { + return ''; + } + + var seconds = _timer.elapsedMilliseconds / 1000.0; + final minutes = seconds ~/ 60; + seconds -= minutes * 60.0; + + final buf = StringBuffer(); + if (minutes > 0) { + buf.write(minutes % 60); + buf.write('m '); + } + + buf.write(seconds.toStringAsFixed(3).padLeft(minutes > 0 ? 6 : 1, '0')); + buf.write('s'); + + return '[${buf.toString().padLeft(11)}] '; + } +} diff --git a/pkgs/cli_util/lib/cli_util.dart b/pkgs/cli_util/lib/cli_util.dart new file mode 100644 index 000000000..e497d68b5 --- /dev/null +++ b/pkgs/cli_util/lib/cli_util.dart @@ -0,0 +1,90 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Utilities to locate the Dart SDK. +library; + +import 'dart:async'; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +/// The path to the current Dart SDK. +String get sdkPath => path.dirname(path.dirname(Platform.resolvedExecutable)); + +/// Returns the path to the current Dart SDK. +@Deprecated("Use 'sdkPath' instead") +String getSdkPath() => sdkPath; + +/// The user-specific application configuration folder for the current platform. +/// +/// This is a location appropriate for storing application specific +/// configuration for the current user. The [productName] should be unique to +/// avoid clashes with other applications on the same machine. This method won't +/// actually create the folder, merely return the recommended location for +/// storing user-specific application configuration. +/// +/// The folder location depends on the platform: +/// * `%APPDATA%\` on **Windows**, +/// * `$HOME/Library/Application Support/` on **Mac OS**, +/// * `$XDG_CONFIG_HOME/` on **Linux** +/// (if `$XDG_CONFIG_HOME` is defined), and, +/// * `$HOME/.config/` otherwise. +/// +/// The chosen location aims to follow best practices for each platform, +/// honoring the [XDG Base Directory Specification][1] on Linux and +/// [File System Basics][2] on Mac OS. +/// +/// Throws an [EnvironmentNotFoundException] if an environment entry, +/// `%APPDATA%` or `$HOME`, is needed and not available. +/// +/// [1]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html +/// [2]: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW1 +String applicationConfigHome(String productName) => + path.join(_configHome, productName); + +String get _configHome { + if (Platform.isWindows) { + return _requireEnv('APPDATA'); + } + + if (Platform.isMacOS) { + return path.join(_requireEnv('HOME'), 'Library', 'Application Support'); + } + + if (Platform.isLinux) { + final xdgConfigHome = _env['XDG_CONFIG_HOME']; + if (xdgConfigHome != null) { + return xdgConfigHome; + } + // XDG Base Directory Specification says to use $HOME/.config/ when + // $XDG_CONFIG_HOME isn't defined. + return path.join(_requireEnv('HOME'), '.config'); + } + + // We have no guidelines, perhaps we should just do: $HOME/.config/ + // same as XDG specification would specify as fallback. + return path.join(_requireEnv('HOME'), '.config'); +} + +String _requireEnv(String name) => + _env[name] ?? (throw EnvironmentNotFoundException(name)); + +/// Exception thrown if a required environment entry does not exist. +/// +/// Thrown by [applicationConfigHome] if an expected and required +/// platform specific environment entry is not available. +class EnvironmentNotFoundException implements Exception { + /// Name of environment entry which was needed, but not found. + final String entryName; + String get message => 'Environment variable \'$entryName\' is not defined!'; + EnvironmentNotFoundException(this.entryName); + @override + String toString() => message; +} + +// This zone override exists solely for testing (see lib/cli_util_test.dart). +Map get _env => + (Zone.current[#environmentOverrides] as Map?) ?? + Platform.environment; diff --git a/pkgs/cli_util/pubspec.yaml b/pkgs/cli_util/pubspec.yaml new file mode 100644 index 000000000..b2253174b --- /dev/null +++ b/pkgs/cli_util/pubspec.yaml @@ -0,0 +1,15 @@ +name: cli_util +version: 0.4.2 +description: A library to help in building Dart command-line apps. +repository: https://github.com/dart-lang/tools/tree/main/pkgs/cli_util + +environment: + sdk: ^3.4.0 + +dependencies: + meta: ^1.7.0 + path: ^1.8.0 + +dev_dependencies: + dart_flutter_team_lints: ^3.0.0 + test: ^1.20.0 diff --git a/pkgs/cli_util/test/cli_util_test.dart b/pkgs/cli_util/test/cli_util_test.dart new file mode 100644 index 000000000..e16bc593f --- /dev/null +++ b/pkgs/cli_util/test/cli_util_test.dart @@ -0,0 +1,39 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'package:cli_util/cli_util.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + group('sdkPath', () { + test('sdkPath', () { + expect(sdkPath, isNotNull); + }); + }); + + group('applicationConfigHome', () { + test('returns a non-empty string', () { + expect(applicationConfigHome('dart'), isNotEmpty); + }); + + test('has an ancestor folder that exists', () { + final path = p.split(applicationConfigHome('dart')); + // We expect that first two segments of the path exist. This is really + // just a dummy check that some part of the path exists. + expect(Directory(p.joinAll(path.take(2))).existsSync(), isTrue); + }); + + test('empty environment throws exception', () async { + expect(() { + runZoned(() => applicationConfigHome('dart'), zoneValues: { + #environmentOverrides: {}, + }); + }, throwsA(isA())); + }); + }); +}