├── .github ├── dependabot.yml └── workflows │ ├── build.yaml │ ├── no-response.yml │ └── publish.yaml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── main.dart ├── lib ├── cli_logging.dart └── cli_util.dart ├── pubspec.yaml └── test └── cli_util_test.dart /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | # See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | schedule: 9 | interval: monthly 10 | labels: 11 | - autosubmit 12 | groups: 13 | github-actions: 14 | patterns: 15 | - "*" 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | schedule: 5 | # “At 00:00 (UTC) on Sunday.” 6 | - cron: '0 0 * * 0' 7 | pull_request: 8 | branches: [ master ] 9 | push: 10 | branches: [ master ] 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | 15 | jobs: 16 | analyze: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | sdk: [dev] 22 | steps: 23 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 24 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 25 | with: 26 | sdk: ${{ matrix.sdk }} 27 | - id: install 28 | name: Install dependencies 29 | run: dart pub get 30 | - name: Check formatting 31 | run: dart format --output=none --set-exit-if-changed . 32 | if: always() && steps.install.outcome == 'success' 33 | - name: Analyze code 34 | run: dart analyze --fatal-infos 35 | if: always() && steps.install.outcome == 'success' 36 | 37 | test: 38 | needs: analyze 39 | runs-on: ${{ matrix.os }} 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | os: [ubuntu-latest] 44 | sdk: ['3.4', dev] 45 | steps: 46 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 47 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 48 | with: 49 | sdk: ${{ matrix.sdk }} 50 | - id: install 51 | name: Install dependencies 52 | run: dart pub get 53 | - name: Run tests 54 | run: dart test --test-randomize-ordering-seed=random 55 | if: always() && steps.install.outcome == 'success' 56 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | # A workflow to close issues where the author hasn't responded to a request for 2 | # more information; see https://github.com/actions/stale. 3 | 4 | name: No Response 5 | 6 | # Run as a daily cron. 7 | on: 8 | schedule: 9 | # Every day at 8am 10 | - cron: '0 8 * * *' 11 | 12 | # All permissions not specified are set to 'none'. 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | 17 | jobs: 18 | no-response: 19 | runs-on: ubuntu-latest 20 | if: ${{ github.repository_owner == 'dart-lang' }} 21 | steps: 22 | - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e 23 | with: 24 | # Don't automatically mark inactive issues+PRs as stale. 25 | days-before-stale: -1 26 | # Close needs-info issues and PRs after 14 days of inactivity. 27 | days-before-close: 14 28 | stale-issue-label: "needs-info" 29 | close-issue-message: > 30 | Without additional information we're not able to resolve this issue. 31 | Feel free to add more info or respond to any questions above and we 32 | can reopen the case. Thanks for your contribution! 33 | stale-pr-label: "needs-info" 34 | close-pr-message: > 35 | Without additional information we're not able to resolve this PR. 36 | Feel free to add more info or respond to any questions above. 37 | Thanks for your contribution! 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # A CI configuration to auto-publish pub packages. 2 | 3 | name: Publish 4 | 5 | on: 6 | pull_request: 7 | branches: [ master ] 8 | push: 9 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 10 | 11 | jobs: 12 | publish: 13 | if: ${{ github.repository_owner == 'dart-lang' }} 14 | uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | .packages 3 | pubspec.lock 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the cli_util project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.2-wip 2 | 3 | - Add `sdkPath` getter, deprecate `getSdkPath` function. 4 | 5 | ## 0.4.1 6 | 7 | - Fix a broken link in the readme. 8 | - Require Dart 3.0. 9 | 10 | ## 0.4.0 11 | 12 | - Remove the deprecated method `getSdkDir()` (instead, use `getSdkPath()`). 13 | - Require Dart 2.19. 14 | 15 | ## 0.3.5 16 | - Make `applicationConfigHome` throw an `Exception` when it fails to find a 17 | configuration folder. 18 | 19 | ## 0.3.4 20 | 21 | - Introduce `applicationConfigHome` for making it easy to consistently find the 22 | user-specific application configuration folder. 23 | 24 | ## 0.3.3 25 | 26 | - Reverted `meta` constraint to `^1.3.0`. 27 | 28 | ## 0.3.2 29 | 30 | - Update `meta` constraint to `>=1.3.0 <3.0.0`. 31 | 32 | ## 0.3.1 33 | 34 | - Fix a bug in `AnsiProgress` where the spinning character doesn't every update. 35 | 36 | ## 0.3.0 37 | 38 | - Stable null safety release. 39 | 40 | ## 0.3.0-nullsafety.0 41 | 42 | - Updated to support 2.12.0 and null safety. 43 | 44 | ## 0.2.1 45 | 46 | ## 0.2.0 47 | 48 | - Add `Logger.write` and `Logger.writeCharCode` methods which write without 49 | printing a trailing newline. 50 | 51 | ## 0.1.4 52 | 53 | - Add `Ansi.reversed` getter. 54 | 55 | ## 0.1.3+2 56 | 57 | - Update Dart SDK constraint to < 3.0.0. 58 | 59 | ## 0.1.3+1 60 | 61 | - Update Dart SDK to 2.0.0-dev. 62 | 63 | ## 0.1.3 64 | 65 | - In verbose mode, instead of printing the diff from the last log message, 66 | print the total time since the tool started 67 | - Change to not buffer the last log message sent in verbose logging mode 68 | - Expose more classes from the logging library 69 | 70 | ## 0.1.2+1 71 | 72 | - Remove unneeded change to Dart SDK constraint. 73 | 74 | ## 0.1.2 75 | 76 | - Fix a bug in `getSdkDir` (#21) 77 | 78 | ## 0.1.1 79 | 80 | - Updated to the output for indeterminate progress 81 | - Exposed a `Logger.isVerbose` getter 82 | 83 | ## 0.1.0 84 | 85 | - Added a new `getSdkPath()` method to get the location of the SDK (this uses the new 86 | `Platform.resolvedExecutable` API to locate the SDK) 87 | - Deprecated `getSdkDir()` in favor of `getSdkPath()` 88 | - Add the `cli_logging.dart` library - utilities to display output and progress 89 | 90 | ## 0.0.1+3 91 | 92 | - Find SDK properly when invoked from inside SDK tests. 93 | 94 | ## 0.0.1+2 95 | 96 | - Support an executable in a symlinked directory. 97 | 98 | ## 0.0.1+1 99 | 100 | - Fix for when the dart executable can't be found by `which`. 101 | 102 | ## 0.0.1 103 | 104 | - Initial version 105 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | Before we can use your code, you must sign the 6 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 7 | (CLA), which you can do online. The CLA is necessary mainly because you own the 8 | copyright to your changes, even after your contribution becomes part of our 9 | codebase, so we need your permission to use and distribute your code. We also 10 | need to be sure of various other things—for instance that you'll tell us if you 11 | know that your code infringes on other people's patents. You don't have to sign 12 | the CLA until after you've submitted your code for review and a member has 13 | approved it, but you must do it before we can put your code into our codebase. 14 | 15 | Before you start working on a larger contribution, you should get in touch with 16 | us first through the issue tracker with your idea so that we can help out and 17 | possibly guide you. Coordinating up front makes it much easier to avoid 18 | frustration later on. 19 | 20 | ### Code reviews 21 | All submissions, including submissions by project members, require review. 22 | 23 | ### File headers 24 | All files in the project must start with the following header. 25 | 26 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 27 | // for details. All rights reserved. Use of this source code is governed by a 28 | // BSD-style license that can be found in the LICENSE file. 29 | 30 | ### The small print 31 | Contributions made by corporations are covered by a different agreement than the 32 | one above, the 33 | [Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repo has moved to https://github.com/dart-lang/tools/tree/main/pkgs/cli_util 3 | 4 | [![Dart CI](https://github.com/dart-lang/cli_util/actions/workflows/build.yaml/badge.svg)](https://github.com/dart-lang/cli_util/actions/workflows/build.yaml) 5 | [![Pub](https://img.shields.io/pub/v/cli_util.svg)](https://pub.dev/packages/cli_util) 6 | [![package publisher](https://img.shields.io/pub/publisher/cli_util.svg)](https://pub.dev/packages/cli_util/publisher) 7 | 8 | A package to help in building Dart command-line apps. 9 | 10 | ## What's this? 11 | 12 | `package:cli_util` provides: 13 | - utilities to find the Dart SDK directory (`sdkPath`) 14 | - utilities to find the settings directory for a tool (`applicationConfigHome()`) 15 | - utilities to aid in showing rich CLI output and progress information (`cli_logging.dart`) 16 | 17 | ## Locating the Dart SDK 18 | 19 | ```dart 20 | import 'dart:io'; 21 | 22 | import 'package:cli_util/cli_util.dart'; 23 | import 'package:path/path.dart' as path; 24 | 25 | main(args) { 26 | // Get SDK directory from cli_util. 27 | var sdkDir = sdkPath; 28 | 29 | // Do stuff... For example, print version string 30 | var versionFile = File(path.join(sdkDir, 'version')); 31 | print(versionFile.readAsStringSync()); 32 | } 33 | ``` 34 | 35 | ## Displaying output and progress 36 | 37 | `package:cli_util` can also be used to help CLI tools display output and progress. 38 | It has a logging mechanism which can help differentiate between regular tool 39 | output and error messages, and can facilitate having a more verbose (`-v`) mode for 40 | output. 41 | 42 | In addition, it can display an indeterminate progress spinner for longer running 43 | tasks, and optionally display the elapsed time when finished: 44 | 45 | ```dart 46 | import 'package:cli_util/cli_logging.dart'; 47 | 48 | void main(List args) async { 49 | var verbose = args.contains('-v'); 50 | var logger = verbose ? Logger.verbose() : Logger.standard(); 51 | 52 | logger.stdout('Hello world!'); 53 | logger.trace('message 1'); 54 | await Future.delayed(Duration(milliseconds: 200)); 55 | logger.trace('message 2'); 56 | logger.trace('message 3'); 57 | 58 | var progress = logger.progress('doing some work'); 59 | await Future.delayed(Duration(seconds: 2)); 60 | progress.finish(showTiming: true); 61 | 62 | logger.stdout('All ${logger.ansi.emphasized('done')}.'); 63 | logger.flush(); 64 | } 65 | ``` 66 | 67 | ## Features and bugs 68 | 69 | Please file feature requests and bugs at the [issue tracker][tracker]. 70 | 71 | [tracker]: https://github.com/dart-lang/cli_util/issues 72 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/language/analysis-options 2 | include: package:dart_flutter_team_lints/analysis_options.yaml 3 | 4 | analyzer: 5 | language: 6 | strict-casts: true 7 | strict-inference: true 8 | strict-raw-types: true 9 | 10 | linter: 11 | rules: 12 | - avoid_bool_literals_in_conditional_expressions 13 | - avoid_classes_with_only_static_members 14 | - avoid_private_typedef_functions 15 | - avoid_redundant_argument_values 16 | - avoid_returning_this 17 | - avoid_unused_constructor_parameters 18 | - avoid_void_async 19 | - cancel_subscriptions 20 | - join_return_with_assignment 21 | - literal_only_boolean_expressions 22 | - missing_whitespace_between_adjacent_strings 23 | - no_adjacent_strings_in_list 24 | - no_runtimeType_toString 25 | - package_api_docs 26 | - prefer_const_declarations 27 | - prefer_expression_function_bodies 28 | - prefer_final_locals 29 | - unnecessary_await_in_return 30 | - unnecessary_raw_strings 31 | - use_if_null_to_convert_nulls_to_bools 32 | - use_raw_strings 33 | - use_string_buffers 34 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, 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 'dart:async'; 6 | 7 | import 'package:cli_util/cli_logging.dart'; 8 | 9 | Future main(List args) async { 10 | final verbose = args.contains('-v'); 11 | final logger = verbose ? Logger.verbose() : Logger.standard(); 12 | 13 | logger.stdout('Hello world!'); 14 | logger.trace('message 1'); 15 | await Future.delayed(const Duration(milliseconds: 200)); 16 | logger.trace('message 2'); 17 | logger.trace('message 3'); 18 | 19 | final progress = logger.progress('doing some work'); 20 | await Future.delayed(const Duration(seconds: 2)); 21 | progress.finish(showTiming: true); 22 | 23 | logger.stdout('All ${logger.ansi.emphasized('done')}.'); 24 | } 25 | -------------------------------------------------------------------------------- /lib/cli_logging.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, 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 | /// This library contains functionality to help command-line utilities to easily 6 | /// create aesthetic output. 7 | library; 8 | 9 | import 'dart:async'; 10 | import 'dart:io' as io; 11 | 12 | /// A small utility class to make it easier to work with common ANSI escape 13 | /// sequences. 14 | class Ansi { 15 | /// Return whether the current stdout terminal supports ANSI escape sequences. 16 | static bool get terminalSupportsAnsi => 17 | io.stdout.supportsAnsiEscapes && 18 | io.stdioType(io.stdout) == io.StdioType.terminal; 19 | 20 | final bool useAnsi; 21 | 22 | Ansi(this.useAnsi); 23 | 24 | String get cyan => _code('\u001b[36m'); 25 | 26 | String get green => _code('\u001b[32m'); 27 | 28 | String get magenta => _code('\u001b[35m'); 29 | 30 | String get red => _code('\u001b[31m'); 31 | 32 | String get yellow => _code('\u001b[33m'); 33 | 34 | String get blue => _code('\u001b[34m'); 35 | 36 | String get gray => _code('\u001b[1;30m'); 37 | 38 | String get noColor => _code('\u001b[39m'); 39 | 40 | String get none => _code('\u001b[0m'); 41 | 42 | String get bold => _code('\u001b[1m'); 43 | 44 | String get reversed => _code('\u001b[7m'); 45 | 46 | String get backspace => '\b'; 47 | 48 | String get bullet => io.stdout.supportsAnsiEscapes ? '•' : '-'; 49 | 50 | /// Display [message] in an emphasized format. 51 | String emphasized(String message) => '$bold$message$none'; 52 | 53 | /// Display [message] in an subtle (gray) format. 54 | String subtle(String message) => '$gray$message$none'; 55 | 56 | /// Display [message] in an error (red) format. 57 | String error(String message) => '$red$message$none'; 58 | 59 | String _code(String ansiCode) => useAnsi ? ansiCode : ''; 60 | } 61 | 62 | /// An abstract representation of a [Logger] - used to pretty print errors, 63 | /// standard status messages, trace level output, and indeterminate progress. 64 | abstract class Logger { 65 | /// Create a normal [Logger]; this logger will not display trace level output. 66 | factory Logger.standard({Ansi? ansi}) => StandardLogger(ansi: ansi); 67 | 68 | /// Create a [Logger] that will display trace level output. 69 | /// 70 | /// If [logTime] is `true`, this logger will display the time of the message. 71 | factory Logger.verbose({Ansi? ansi, bool logTime = true}) => 72 | VerboseLogger(ansi: ansi, logTime: logTime); 73 | 74 | Ansi get ansi; 75 | 76 | bool get isVerbose; 77 | 78 | /// Print an error message. 79 | void stderr(String message); 80 | 81 | /// Print a standard status message. 82 | void stdout(String message); 83 | 84 | /// Print trace output. 85 | void trace(String message); 86 | 87 | /// Print text to stdout, without a trailing newline. 88 | void write(String message); 89 | 90 | /// Print a character code to stdout, without a trailing newline. 91 | void writeCharCode(int charCode); 92 | 93 | /// Start an indeterminate progress display. 94 | Progress progress(String message); 95 | 96 | /// Flush any un-written output. 97 | @Deprecated('This method will be removed in the future') 98 | void flush(); 99 | } 100 | 101 | /// A handle to an indeterminate progress display. 102 | abstract class Progress { 103 | final String message; 104 | final Stopwatch _stopwatch; 105 | 106 | Progress(this.message) : _stopwatch = Stopwatch()..start(); 107 | 108 | Duration get elapsed => _stopwatch.elapsed; 109 | 110 | /// Finish the indeterminate progress display. 111 | void finish({String? message, bool showTiming = false}); 112 | 113 | /// Cancel the indeterminate progress display. 114 | void cancel(); 115 | } 116 | 117 | class StandardLogger implements Logger { 118 | @override 119 | Ansi ansi; 120 | 121 | StandardLogger({Ansi? ansi}) : ansi = ansi ?? Ansi(Ansi.terminalSupportsAnsi); 122 | 123 | @override 124 | bool get isVerbose => false; 125 | 126 | Progress? _currentProgress; 127 | 128 | @override 129 | void stderr(String message) { 130 | _cancelProgress(); 131 | 132 | io.stderr.writeln(message); 133 | } 134 | 135 | @override 136 | void stdout(String message) { 137 | _cancelProgress(); 138 | 139 | print(message); 140 | } 141 | 142 | @override 143 | void trace(String message) {} 144 | 145 | @override 146 | void write(String message) { 147 | _cancelProgress(); 148 | 149 | io.stdout.write(message); 150 | } 151 | 152 | @override 153 | void writeCharCode(int charCode) { 154 | _cancelProgress(); 155 | 156 | io.stdout.writeCharCode(charCode); 157 | } 158 | 159 | void _cancelProgress() { 160 | final progress = _currentProgress; 161 | if (progress != null) { 162 | _currentProgress = null; 163 | progress.cancel(); 164 | } 165 | } 166 | 167 | @override 168 | Progress progress(String message) { 169 | _cancelProgress(); 170 | 171 | final progress = ansi.useAnsi 172 | ? AnsiProgress(ansi, message) 173 | : SimpleProgress(this, message); 174 | _currentProgress = progress; 175 | return progress; 176 | } 177 | 178 | @override 179 | @Deprecated('This method will be removed in the future') 180 | void flush() {} 181 | } 182 | 183 | class SimpleProgress extends Progress { 184 | final Logger logger; 185 | 186 | SimpleProgress(this.logger, String message) : super(message) { 187 | logger.stdout('$message...'); 188 | } 189 | 190 | @override 191 | void cancel() {} 192 | 193 | @override 194 | void finish({String? message, bool showTiming = false}) {} 195 | } 196 | 197 | class AnsiProgress extends Progress { 198 | static const List kAnimationItems = ['/', '-', r'\', '|']; 199 | 200 | final Ansi ansi; 201 | 202 | late final Timer _timer; 203 | 204 | AnsiProgress(this.ansi, String message) : super(message) { 205 | _timer = Timer.periodic(const Duration(milliseconds: 80), (t) { 206 | _updateDisplay(); 207 | }); 208 | io.stdout.write('$message... '.padRight(40)); 209 | _updateDisplay(); 210 | } 211 | 212 | @override 213 | void cancel() { 214 | if (_timer.isActive) { 215 | _timer.cancel(); 216 | _updateDisplay(cancelled: true); 217 | } 218 | } 219 | 220 | @override 221 | void finish({String? message, bool showTiming = false}) { 222 | if (_timer.isActive) { 223 | _timer.cancel(); 224 | _updateDisplay(isFinal: true, message: message, showTiming: showTiming); 225 | } 226 | } 227 | 228 | void _updateDisplay( 229 | {bool isFinal = false, 230 | bool cancelled = false, 231 | String? message, 232 | bool showTiming = false}) { 233 | var char = kAnimationItems[_timer.tick % kAnimationItems.length]; 234 | if (isFinal || cancelled) { 235 | char = ''; 236 | } 237 | io.stdout.write('${ansi.backspace}$char'); 238 | if (isFinal || cancelled) { 239 | if (message != null) { 240 | io.stdout.write(message.isEmpty ? ' ' : message); 241 | } else if (showTiming) { 242 | final time = (elapsed.inMilliseconds / 1000.0).toStringAsFixed(1); 243 | io.stdout.write('${time}s'); 244 | } else { 245 | io.stdout.write(' '); 246 | } 247 | io.stdout.writeln(); 248 | } 249 | } 250 | } 251 | 252 | class VerboseLogger implements Logger { 253 | @override 254 | Ansi ansi; 255 | bool logTime; 256 | final _timer = Stopwatch()..start(); 257 | 258 | VerboseLogger({Ansi? ansi, this.logTime = false}) 259 | : ansi = ansi ?? Ansi(Ansi.terminalSupportsAnsi); 260 | 261 | @override 262 | bool get isVerbose => true; 263 | 264 | @override 265 | void stdout(String message) { 266 | io.stdout.writeln('${_createPrefix()}$message'); 267 | } 268 | 269 | @override 270 | void stderr(String message) { 271 | io.stderr.writeln('${_createPrefix()}${ansi.red}$message${ansi.none}'); 272 | } 273 | 274 | @override 275 | void trace(String message) { 276 | io.stdout.writeln('${_createPrefix()}${ansi.gray}$message${ansi.none}'); 277 | } 278 | 279 | @override 280 | void write(String message) { 281 | io.stdout.write(message); 282 | } 283 | 284 | @override 285 | void writeCharCode(int charCode) { 286 | io.stdout.writeCharCode(charCode); 287 | } 288 | 289 | @override 290 | Progress progress(String message) => SimpleProgress(this, message); 291 | 292 | @override 293 | @Deprecated('This method will be removed in the future') 294 | void flush() {} 295 | 296 | String _createPrefix() { 297 | if (!logTime) { 298 | return ''; 299 | } 300 | 301 | var seconds = _timer.elapsedMilliseconds / 1000.0; 302 | final minutes = seconds ~/ 60; 303 | seconds -= minutes * 60.0; 304 | 305 | final buf = StringBuffer(); 306 | if (minutes > 0) { 307 | buf.write(minutes % 60); 308 | buf.write('m '); 309 | } 310 | 311 | buf.write(seconds.toStringAsFixed(3).padLeft(minutes > 0 ? 6 : 1, '0')); 312 | buf.write('s'); 313 | 314 | return '[${buf.toString().padLeft(11)}] '; 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /lib/cli_util.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, 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 | /// Utilities to locate the Dart SDK. 6 | library; 7 | 8 | import 'dart:async'; 9 | import 'dart:io'; 10 | 11 | import 'package:path/path.dart' as path; 12 | 13 | /// The path to the current Dart SDK. 14 | String get sdkPath => path.dirname(path.dirname(Platform.resolvedExecutable)); 15 | 16 | /// Returns the path to the current Dart SDK. 17 | @Deprecated("Use 'sdkPath' instead") 18 | String getSdkPath() => sdkPath; 19 | 20 | /// The user-specific application configuration folder for the current platform. 21 | /// 22 | /// This is a location appropriate for storing application specific 23 | /// configuration for the current user. The [productName] should be unique to 24 | /// avoid clashes with other applications on the same machine. This method won't 25 | /// actually create the folder, merely return the recommended location for 26 | /// storing user-specific application configuration. 27 | /// 28 | /// The folder location depends on the platform: 29 | /// * `%APPDATA%\` on **Windows**, 30 | /// * `$HOME/Library/Application Support/` on **Mac OS**, 31 | /// * `$XDG_CONFIG_HOME/` on **Linux** 32 | /// (if `$XDG_CONFIG_HOME` is defined), and, 33 | /// * `$HOME/.config/` otherwise. 34 | /// 35 | /// The chosen location aims to follow best practices for each platform, 36 | /// honoring the [XDG Base Directory Specification][1] on Linux and 37 | /// [File System Basics][2] on Mac OS. 38 | /// 39 | /// Throws an [EnvironmentNotFoundException] if an environment entry, 40 | /// `%APPDATA%` or `$HOME`, is needed and not available. 41 | /// 42 | /// [1]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html 43 | /// [2]: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW1 44 | String applicationConfigHome(String productName) => 45 | path.join(_configHome, productName); 46 | 47 | String get _configHome { 48 | if (Platform.isWindows) { 49 | return _requireEnv('APPDATA'); 50 | } 51 | 52 | if (Platform.isMacOS) { 53 | return path.join(_requireEnv('HOME'), 'Library', 'Application Support'); 54 | } 55 | 56 | if (Platform.isLinux) { 57 | final xdgConfigHome = _env['XDG_CONFIG_HOME']; 58 | if (xdgConfigHome != null) { 59 | return xdgConfigHome; 60 | } 61 | // XDG Base Directory Specification says to use $HOME/.config/ when 62 | // $XDG_CONFIG_HOME isn't defined. 63 | return path.join(_requireEnv('HOME'), '.config'); 64 | } 65 | 66 | // We have no guidelines, perhaps we should just do: $HOME/.config/ 67 | // same as XDG specification would specify as fallback. 68 | return path.join(_requireEnv('HOME'), '.config'); 69 | } 70 | 71 | String _requireEnv(String name) => 72 | _env[name] ?? (throw EnvironmentNotFoundException(name)); 73 | 74 | /// Exception thrown if a required environment entry does not exist. 75 | /// 76 | /// Thrown by [applicationConfigHome] if an expected and required 77 | /// platform specific environment entry is not available. 78 | class EnvironmentNotFoundException implements Exception { 79 | /// Name of environment entry which was needed, but not found. 80 | final String entryName; 81 | String get message => 'Environment variable \'$entryName\' is not defined!'; 82 | EnvironmentNotFoundException(this.entryName); 83 | @override 84 | String toString() => message; 85 | } 86 | 87 | // This zone override exists solely for testing (see lib/cli_util_test.dart). 88 | Map get _env => 89 | (Zone.current[#environmentOverrides] as Map?) ?? 90 | Platform.environment; 91 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: cli_util 2 | version: 0.4.2-wip 3 | description: A library to help in building Dart command-line apps. 4 | repository: https://github.com/dart-lang/cli_util 5 | 6 | environment: 7 | sdk: ^3.4.0 8 | 9 | dependencies: 10 | meta: ^1.7.0 11 | path: ^1.8.0 12 | 13 | dev_dependencies: 14 | dart_flutter_team_lints: ^3.0.0 15 | test: ^1.20.0 16 | -------------------------------------------------------------------------------- /test/cli_util_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, 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 'dart:async'; 6 | import 'dart:io'; 7 | 8 | import 'package:cli_util/cli_util.dart'; 9 | import 'package:path/path.dart' as p; 10 | import 'package:test/test.dart'; 11 | 12 | void main() { 13 | group('sdkPath', () { 14 | test('sdkPath', () { 15 | expect(sdkPath, isNotNull); 16 | }); 17 | }); 18 | 19 | group('applicationConfigHome', () { 20 | test('returns a non-empty string', () { 21 | expect(applicationConfigHome('dart'), isNotEmpty); 22 | }); 23 | 24 | test('has an ancestor folder that exists', () { 25 | final path = p.split(applicationConfigHome('dart')); 26 | // We expect that first two segments of the path exist. This is really 27 | // just a dummy check that some part of the path exists. 28 | expect(Directory(p.joinAll(path.take(2))).existsSync(), isTrue); 29 | }); 30 | 31 | test('empty environment throws exception', () async { 32 | expect(() { 33 | runZoned(() => applicationConfigHome('dart'), zoneValues: { 34 | #environmentOverrides: {}, 35 | }); 36 | }, throwsA(isA())); 37 | }); 38 | }); 39 | } 40 | --------------------------------------------------------------------------------