remove:(dcli): deleting refactored code return to zero
This commit is contained in:
parent
11bb99a95c
commit
0362bb6657
51 changed files with 1 additions and 7302 deletions
|
@ -1,22 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/// This library file exports common utility functions and controllers used in the Protevus Console package.
|
|
||||||
///
|
|
||||||
/// It includes:
|
|
||||||
/// - Functionality for determining paths, exported from 'determine_paths.dart'
|
|
||||||
/// - A unit test controller, exported from 'unit_test_controller.dart'
|
|
||||||
///
|
|
||||||
/// These exports allow other parts of the application to easily access and use
|
|
||||||
/// these common utilities without needing to import them individually.
|
|
||||||
library;
|
|
||||||
|
|
||||||
export 'package:protevus_console/src/common/determine_paths.dart';
|
|
||||||
export 'package:protevus_console/src/common/unit_test_controller.dart';
|
|
|
@ -1,55 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
library;
|
|
||||||
|
|
||||||
export 'src/functions/backup.dart';
|
|
||||||
export 'src/functions/cat.dart';
|
|
||||||
export 'src/functions/copy.dart'; // show copy, CopyException;
|
|
||||||
export 'src/functions/copy_tree.dart' show CopyTreeException, copyTree;
|
|
||||||
export 'src/functions/create_dir.dart'
|
|
||||||
show CreateDirException, createDir, createTempDir, withTempDirAsync;
|
|
||||||
export 'src/functions/create_dir.dart';
|
|
||||||
export 'src/functions/dcli_function.dart';
|
|
||||||
export 'src/functions/delete.dart' show DeleteException, delete;
|
|
||||||
export 'src/functions/delete_dir.dart' show DeleteDirException, deleteDir;
|
|
||||||
export 'src/functions/env.dart'
|
|
||||||
show
|
|
||||||
Env,
|
|
||||||
HOME,
|
|
||||||
PATH,
|
|
||||||
env,
|
|
||||||
envs,
|
|
||||||
isOnPATH,
|
|
||||||
withEnvironment,
|
|
||||||
withEnvironmentAsync;
|
|
||||||
export 'src/functions/find.dart';
|
|
||||||
export 'src/functions/find_async.dart';
|
|
||||||
export 'src/functions/head.dart';
|
|
||||||
export 'src/functions/is.dart';
|
|
||||||
export 'src/functions/move.dart' show MoveException, move;
|
|
||||||
export 'src/functions/move_dir.dart' show MoveDirException, moveDir;
|
|
||||||
export 'src/functions/move_tree.dart';
|
|
||||||
export 'src/functions/pwd.dart' show pwd;
|
|
||||||
export 'src/functions/tail.dart';
|
|
||||||
export 'src/functions/touch.dart';
|
|
||||||
export 'src/functions/which.dart' show Which, WhichSearch, which;
|
|
||||||
export 'src/settings.dart';
|
|
||||||
export 'src/utils/dcli_exception.dart';
|
|
||||||
export 'src/utils/dcli_platform.dart';
|
|
||||||
export 'src/utils/dev_null.dart';
|
|
||||||
export 'src/utils/file.dart';
|
|
||||||
export 'src/utils/limited_stream_controller.dart';
|
|
||||||
export 'src/utils/line_action.dart';
|
|
||||||
export 'src/utils/line_file.dart';
|
|
||||||
export 'src/utils/platform.dart';
|
|
||||||
export 'src/utils/run_exception.dart';
|
|
||||||
export 'src/utils/stack_list.dart';
|
|
||||||
export 'src/utils/truepath.dart' show privatePath, rootPath, truepath;
|
|
|
@ -1,16 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
library;
|
|
||||||
|
|
||||||
export 'src/input/ask.dart';
|
|
||||||
export 'src/input/confirm.dart';
|
|
||||||
export 'src/input/echo.dart';
|
|
||||||
export 'src/input/menu.dart';
|
|
0
packages/console/lib/src/.gitkeep
Normal file
0
packages/console/lib/src/.gitkeep
Normal file
|
@ -1,144 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
|
|
||||||
/// Determines the paths for source and backup directories based on the given parameters.
|
|
||||||
///
|
|
||||||
/// [path] is the relative or absolute path to the
|
|
||||||
/// file that we are going to backup. If [path] is
|
|
||||||
/// relative then it is relative to [workingDirectory].
|
|
||||||
/// [backupDir] is the temporary directory that we
|
|
||||||
/// are going to backup [path] to.
|
|
||||||
///
|
|
||||||
/// We use the following directory structure for the backup
|
|
||||||
/// relative/<path to [path]>
|
|
||||||
/// absolute/<path to [path]>
|
|
||||||
///
|
|
||||||
/// On Windows to accomodate drive letters we need a slightly
|
|
||||||
/// different directory structure
|
|
||||||
/// relative/<path to [path]>
|
|
||||||
/// absolute/<XDrive>/<path to [path]>
|
|
||||||
///
|
|
||||||
/// Where 'X' is the drive letter that [path] is located on.
|
|
||||||
///
|
|
||||||
Paths determinePaths({
|
|
||||||
required String path,
|
|
||||||
required String workingDirectory,
|
|
||||||
required String backupDir,
|
|
||||||
}) {
|
|
||||||
late final String sourcePath;
|
|
||||||
late final String backupPath;
|
|
||||||
|
|
||||||
/// we use two different directories for relative and absolute
|
|
||||||
/// paths otherwise we can't differentiate when it comes time
|
|
||||||
/// to restore.
|
|
||||||
if (isRelative(path)) {
|
|
||||||
backupPath = normalize(absolute(join(backupDir, 'relative', path)));
|
|
||||||
sourcePath = join(workingDirectory, path);
|
|
||||||
} else {
|
|
||||||
sourcePath = normalize(absolute(path));
|
|
||||||
final translatedPath =
|
|
||||||
translateAbsolutePath(path, workingDirectory: workingDirectory);
|
|
||||||
backupPath = join(backupDir, 'absolute', _stripRootPrefix(translatedPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Paths(sourcePath, backupPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a pair of paths for source and backup files.
|
|
||||||
///
|
|
||||||
/// This class is used to store and manage the paths for a source file
|
|
||||||
/// and its corresponding backup file.
|
|
||||||
///
|
|
||||||
/// [sourcePath] is the path to the original source file.
|
|
||||||
/// [backupPath] is the path where the backup of the source file will be stored.
|
|
||||||
class Paths {
|
|
||||||
Paths(this.sourcePath, this.backupPath);
|
|
||||||
|
|
||||||
String sourcePath;
|
|
||||||
String backupPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes the root prefix (/ or \) from an absolute path
|
|
||||||
/// If there is no root prefix the original [absolutePath]
|
|
||||||
/// is returned untouched.
|
|
||||||
///
|
|
||||||
/// If the [absolutePath] only contains the root prefix
|
|
||||||
/// then a blank string is returned
|
|
||||||
///
|
|
||||||
/// /hellow -> hellow
|
|
||||||
/// hellow -> hellow
|
|
||||||
/// / ->
|
|
||||||
///
|
|
||||||
String? _stripRootPrefix(String absolutePath) {
|
|
||||||
if (absolutePath.startsWith(r'\') || absolutePath.startsWith('/')) {
|
|
||||||
if (absolutePath.length > 1) {
|
|
||||||
return absolutePath.substring(1);
|
|
||||||
} else {
|
|
||||||
// the path only contained the root prefix and nothing else.
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return absolutePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Translates an absolute path to a standardized format, primarily for Windows systems.
|
|
||||||
///
|
|
||||||
/// C:/abc -> /CDrive/abc
|
|
||||||
/// C:\abc -> /CDrive\abc
|
|
||||||
/// \\\abc -> \abc
|
|
||||||
/// \\abc -> abc
|
|
||||||
///
|
|
||||||
/// The [context] is only used for unit testing so
|
|
||||||
/// we can fake the platform separator.
|
|
||||||
String translateAbsolutePath(
|
|
||||||
String absolutePath, {
|
|
||||||
String? workingDirectory,
|
|
||||||
p.Context? context,
|
|
||||||
}) {
|
|
||||||
final windowsStyle = context != null && context.style == Style.windows;
|
|
||||||
if (!windowsStyle && !Platform.isWindows) {
|
|
||||||
return absolutePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
context ??= p.context;
|
|
||||||
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
workingDirectory ??= Directory.current.path;
|
|
||||||
|
|
||||||
final parts = context.split(absolutePath);
|
|
||||||
if (parts[0].contains(':')) {
|
|
||||||
final index = parts[0].indexOf(':');
|
|
||||||
|
|
||||||
final drive = parts[0][index - 1].toUpperCase();
|
|
||||||
return context.joinAll(['\\${drive}Drive', ...parts.sublist(1)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts[0].startsWith(r'\\')) {
|
|
||||||
final uncparts = parts[0].split(r'\\');
|
|
||||||
return context.joinAll([r'\UNC', ...uncparts.sublist(1)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (absolutePath.startsWith(r'\') || absolutePath.startsWith('/')) {
|
|
||||||
String drive;
|
|
||||||
if (workingDirectory.contains(':')) {
|
|
||||||
drive = workingDirectory[0].toUpperCase();
|
|
||||||
} else {
|
|
||||||
drive = Directory.current.path[0].toUpperCase();
|
|
||||||
}
|
|
||||||
return context.joinAll(['\\${drive}Drive', ...parts.sublist(1)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// probably not an absolute path
|
|
||||||
/// so just pass back what we were handed.
|
|
||||||
return absolutePath;
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'package:scope/scope.dart';
|
|
||||||
|
|
||||||
/// A utility class for managing unit test behavior in DCli.
|
|
||||||
///
|
|
||||||
/// This class provides static members to control and detect when code is running
|
|
||||||
/// within a unit test environment. It uses the `scope` package to manage a boolean
|
|
||||||
/// flag indicating whether the current execution context is a unit test.
|
|
||||||
class UnitTestController {
|
|
||||||
/// A ScopeKey used to indicate whether the current execution is within a unit test.
|
|
||||||
///
|
|
||||||
/// This key is injected when running a unit test, allowing DCli code to be
|
|
||||||
/// 'unit test' aware and modify its behavior to be unit test friendly.
|
|
||||||
/// The default value is false, indicating that by default, the code is not
|
|
||||||
/// running in a unit test environment.
|
|
||||||
///
|
|
||||||
/// Usage:
|
|
||||||
/// - When set to true, it signals that the code is running within a unit test.
|
|
||||||
/// - DCli functions can check this key to adjust their behavior accordingly.
|
|
||||||
static final unitTestingKey =
|
|
||||||
ScopeKey<bool>.withDefault(false, 'Running in a unit test');
|
|
||||||
|
|
||||||
/// Executes the provided action within a unit test context.
|
|
||||||
///
|
|
||||||
/// This method creates a new [Scope] where the [unitTestingKey] is set to true,
|
|
||||||
/// indicating that the code is running within a unit test environment. It then
|
|
||||||
/// executes the provided [action] within this scope.
|
|
||||||
///
|
|
||||||
/// Certain DCli functions modify their behavior when run within a unit test.
|
|
||||||
/// They rely on this scope to determine if they are in a unit test environment.
|
|
||||||
///
|
|
||||||
/// Usage:
|
|
||||||
/// ```dart
|
|
||||||
/// await UnitTestController.withUnitTest(() {
|
|
||||||
/// // Your unit test code here
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
/// - action: A void function that contains the code to be executed within the unit test context.
|
|
||||||
///
|
|
||||||
/// Returns:
|
|
||||||
/// A [Future] that completes when the action has finished executing.
|
|
||||||
static Future<void> withUnitTest(void Function() action) async {
|
|
||||||
final scope = Scope()..value(unitTestingKey, true);
|
|
||||||
await scope.run(() async => action());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,321 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:protevus_console/common.dart';
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
/// Provide a very simple mechanism to backup a single file.
|
|
||||||
///
|
|
||||||
/// The backup is placed in '.bak' subdirectory under the passed
|
|
||||||
/// [pathToFile]'s directory.
|
|
||||||
///
|
|
||||||
/// Be cautious that you don't nest backups of the same file
|
|
||||||
/// in your code as we always use the same backup target.
|
|
||||||
/// Instead use [withFileProtectionAsync].
|
|
||||||
///
|
|
||||||
/// We also renamed the backup to '<filename>.bak' to ensure
|
|
||||||
/// the backupfile doesn't interfere with dev tools
|
|
||||||
/// (e.g. we don't want an extra pubspec.yaml hanging about)
|
|
||||||
///
|
|
||||||
/// If a file at [pathToFile] doesn't exist then a [BackupFileException]
|
|
||||||
/// is thrown unless you pass the [ignoreMissing] flag.
|
|
||||||
///
|
|
||||||
/// See: [restoreFile]
|
|
||||||
/// [withFileProtectionAsync]
|
|
||||||
///
|
|
||||||
void backupFile(String pathToFile, {bool ignoreMissing = false}) {
|
|
||||||
if (!exists(pathToFile)) {
|
|
||||||
throw BackupFileException(
|
|
||||||
'The backup file ${truepath(pathToFile)} is missing',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final pathToBackupFile = _backupFilePath(pathToFile);
|
|
||||||
if (exists(pathToBackupFile)) {
|
|
||||||
delete(pathToBackupFile);
|
|
||||||
}
|
|
||||||
if (!exists(dirname(pathToBackupFile))) {
|
|
||||||
createDir(dirname(pathToBackupFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
verbose(() => 'Backing up ${truepath(pathToFile)}');
|
|
||||||
copy(pathToFile, pathToBackupFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Designed to work with [backupFile] to restore
|
|
||||||
/// a file from backup.
|
|
||||||
/// The existing file is deleted and restored
|
|
||||||
/// from the .bak/<filename>.bak file created when
|
|
||||||
/// you called [backupFile].
|
|
||||||
///
|
|
||||||
/// Consider using [withFileProtectionAsync] for a more robust solution.
|
|
||||||
///
|
|
||||||
/// When the last .bak file is restored, the .bak directory
|
|
||||||
/// will be deleted. If you don't restore all files (your app crashes)
|
|
||||||
/// then a .bak directory and files may be left hanging around and you may
|
|
||||||
/// need to manually restore these files.
|
|
||||||
/// If the backup file doesn't exists this function throws
|
|
||||||
/// a [RestoreFileException] unless you pass the [ignoreMissing]
|
|
||||||
/// flag.
|
|
||||||
void restoreFile(String pathToFile, {bool ignoreMissing = false}) {
|
|
||||||
final pathToBackupFile = _backupFilePath(pathToFile);
|
|
||||||
|
|
||||||
if (exists(pathToBackupFile)) {
|
|
||||||
if (exists(pathToFile)) {
|
|
||||||
delete(pathToFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
move(pathToBackupFile, pathToFile);
|
|
||||||
|
|
||||||
if (isEmpty(dirname(pathToBackupFile))) {
|
|
||||||
deleteDir(dirname(pathToBackupFile));
|
|
||||||
}
|
|
||||||
verbose(() => 'Restoring ${truepath(pathToFile)}');
|
|
||||||
} else {
|
|
||||||
if (ignoreMissing) {
|
|
||||||
verbose(
|
|
||||||
() => 'Missing restoreFile ${truepath(pathToBackupFile)} ignored.',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw RestoreFileException(
|
|
||||||
'The backup file ${truepath(pathToBackupFile)} is missing',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// EXPERIMENTAL - use with caution and the api may change.
|
|
||||||
///
|
|
||||||
/// Allows you to nominate a list of files to be backed up
|
|
||||||
/// before an operation commences
|
|
||||||
/// and then restored once the operation completes.
|
|
||||||
///
|
|
||||||
/// Currently the files a protected by making a copy of each file/directory
|
|
||||||
/// into a unique system temp directory and then moved back once the
|
|
||||||
/// [action] has completed.
|
|
||||||
///
|
|
||||||
|
|
||||||
///
|
|
||||||
/// [withFileProtectionAsync] is safe to use in a nested fashion as each call
|
|
||||||
/// to [withFileProtectionAsync] creates its own separate backup area.
|
|
||||||
///
|
|
||||||
/// If the VM aborts during execution of the [action] you will find
|
|
||||||
/// the backed up files in the system temp directory under a directory named
|
|
||||||
/// .withFileProtection'. You may need to use the time stamp to determine which
|
|
||||||
/// directory is the right one if you have had mulitple failures.
|
|
||||||
/// Under normal circumstances the temp directory is delete once the action
|
|
||||||
/// completes.
|
|
||||||
///
|
|
||||||
/// The [protected] list can contain files, directories.
|
|
||||||
///
|
|
||||||
/// If the entry is a directory then all children (files and directories)
|
|
||||||
/// are protected.
|
|
||||||
///
|
|
||||||
/// Entries in the [protected] list may be relative or absolute.
|
|
||||||
///
|
|
||||||
/// If [protected] contains a file or directory that doesn't exist
|
|
||||||
/// and the [action] subsequently creates those entities, then those files
|
|
||||||
/// and/or directories will be deleted after [action] completes.
|
|
||||||
///
|
|
||||||
/// This function can be useful for doing dry-run operations
|
|
||||||
/// where you need to ensure the filesystem is restore to its
|
|
||||||
/// prior state after the dry-run completes.
|
|
||||||
///
|
|
||||||
// ignore: flutter_style_todos
|
|
||||||
/// TODO(bsutton): make this work for other than current drive under Windows
|
|
||||||
///
|
|
||||||
Future<R> withFileProtectionAsync<R>(
|
|
||||||
List<String> protected,
|
|
||||||
Future<R> Function() action, {
|
|
||||||
String? workingDirectory,
|
|
||||||
}) async {
|
|
||||||
// removed glob support for the moment.
|
|
||||||
// This is because if one of the protected entries is missing
|
|
||||||
// then we are assuming its a glob.
|
|
||||||
// We should probably change to accepting a Pattern
|
|
||||||
// and the have the user pass an actual Glob.
|
|
||||||
// Problem with this is that find uses a subset of Glob.
|
|
||||||
// so for the moment, no glob support
|
|
||||||
// a glob pattern as supported by the [find] command.
|
|
||||||
// We only support searching for files by the glob pattern (not directories).
|
|
||||||
// If the entry is a glob pattern then it is applied recusively.
|
|
||||||
|
|
||||||
final workingDirectory0 = workingDirectory ?? pwd;
|
|
||||||
final result = await withTempDirAsync(
|
|
||||||
(backupDir) async {
|
|
||||||
verbose(() => 'withFileProtection: backing up to $backupDir');
|
|
||||||
|
|
||||||
/// backup the protected files
|
|
||||||
/// to a backupDir
|
|
||||||
for (final path in protected) {
|
|
||||||
final paths = determinePaths(
|
|
||||||
path: path,
|
|
||||||
workingDirectory: workingDirectory0,
|
|
||||||
backupDir: backupDir,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!exists(paths.sourcePath)) {
|
|
||||||
/// the file/directory doesn't exist.
|
|
||||||
/// During the restore process this path will be deleted
|
|
||||||
/// so that once again they don't exist.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFile(paths.sourcePath)) {
|
|
||||||
if (!exists(dirname(paths.backupPath))) {
|
|
||||||
createDir(dirname(paths.backupPath), recursive: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// the entity is a simple file.
|
|
||||||
copy(paths.sourcePath, paths.backupPath);
|
|
||||||
} else if (isDirectory(paths.sourcePath)) {
|
|
||||||
/// the entity is a directory so copy the whole tree
|
|
||||||
/// recursively.
|
|
||||||
if (!exists(paths.backupPath)) {
|
|
||||||
createDir(paths.backupPath, recursive: true);
|
|
||||||
}
|
|
||||||
copyTree(paths.sourcePath, paths.backupPath, includeHidden: true);
|
|
||||||
} else {
|
|
||||||
throw BackupFileException(
|
|
||||||
'Unsupported entity type for ${paths.sourcePath}. '
|
|
||||||
'Only files and directories are supported',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// else {
|
|
||||||
// /// Must be a glob.
|
|
||||||
// for (final file in find(paths.source, includeHidden: true)
|
|
||||||
// .toList()) {
|
|
||||||
// // we need to determine the paths for each [file]
|
|
||||||
// // as the can have a different relative path as we
|
|
||||||
// // do a recursive search.
|
|
||||||
// final paths = _determinePaths(
|
|
||||||
// path: file, sourceDir: sourceDir, backupDir: backupDir);
|
|
||||||
|
|
||||||
// if (!exists(dirname(paths.target))) {
|
|
||||||
// createDir(dirname(paths.target), recursive: true);
|
|
||||||
// }
|
|
||||||
// copy(paths.source, paths.target);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
final result = await action();
|
|
||||||
|
|
||||||
/// restore the protected entities
|
|
||||||
for (final path in protected) {
|
|
||||||
final paths = determinePaths(
|
|
||||||
path: path,
|
|
||||||
workingDirectory: workingDirectory0,
|
|
||||||
backupDir: backupDir,
|
|
||||||
);
|
|
||||||
{
|
|
||||||
if (!exists(paths.backupPath)) {
|
|
||||||
/// If the protected entity didn't exist before we started
|
|
||||||
/// the make certain it doesn't exist now.
|
|
||||||
_deleteEntity(paths.sourcePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFile(paths.backupPath)) {
|
|
||||||
await _restoreFile(paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDirectory(paths.backupPath)) {
|
|
||||||
_restoreDirectory(paths);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
keep: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _restoreFile(Paths paths) async {
|
|
||||||
await withTempFileAsync<void>(
|
|
||||||
(dotBak) async {
|
|
||||||
try {
|
|
||||||
if (exists(paths.sourcePath)) {
|
|
||||||
move(paths.sourcePath, dotBak);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore: flutter_style_todos
|
|
||||||
/// TODO(bsutton): consider only restoring the file if its last modified
|
|
||||||
/// time has changed.
|
|
||||||
move(paths.backupPath, paths.sourcePath);
|
|
||||||
if (exists(dotBak)) {
|
|
||||||
delete(dotBak);
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
} catch (e) {
|
|
||||||
/// The restore failed so if the dotBak file
|
|
||||||
/// exists lets at least restore that.
|
|
||||||
if (exists(dotBak)) {
|
|
||||||
/// this should never happen as if we have the dotBak
|
|
||||||
/// file then the originalFile should not exists.
|
|
||||||
/// but just in case.
|
|
||||||
if (exists(paths.sourcePath)) {
|
|
||||||
delete(paths.sourcePath);
|
|
||||||
}
|
|
||||||
move(dotBak, paths.sourcePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
create: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _backupFilePath(String pathToFile) {
|
|
||||||
final sourcePath = dirname(pathToFile);
|
|
||||||
final destPath = join(sourcePath, '.bak');
|
|
||||||
final filename = basename(pathToFile);
|
|
||||||
|
|
||||||
return '${join(destPath, filename)}.bak';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thrown by the [restoreFile] function when
|
|
||||||
/// the backup file is missing.
|
|
||||||
class RestoreFileException extends DCliException {
|
|
||||||
/// Creates a [RestoreFileException] with the given
|
|
||||||
/// message.
|
|
||||||
RestoreFileException(super.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thrown by the [backupFile] function when
|
|
||||||
/// the file to be backed up is missing.
|
|
||||||
class BackupFileException extends DCliException {
|
|
||||||
/// Creates a [BackupFileException] with the given
|
|
||||||
/// message.
|
|
||||||
BackupFileException(super.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _restoreDirectory(Paths paths) {
|
|
||||||
/// For directories we just recreate them if necessary.
|
|
||||||
/// This allows us to restore empty directories.
|
|
||||||
if (exists(paths.sourcePath)) {
|
|
||||||
deleteDir(paths.sourcePath);
|
|
||||||
}
|
|
||||||
createDir(paths.sourcePath, recursive: true);
|
|
||||||
|
|
||||||
/// The find command will return all of the nested files so
|
|
||||||
/// we don't need to restore them when we see the directory.
|
|
||||||
moveTree(paths.backupPath, paths.sourcePath, includeHidden: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deleteEntity(String path) {
|
|
||||||
if (isFile(path)) {
|
|
||||||
delete(path);
|
|
||||||
} else if (isDirectory(path)) {
|
|
||||||
deleteDir(path);
|
|
||||||
} else {
|
|
||||||
verbose(() => 'Path is of unsuported type');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
/// Prints the contents of the file located at [path] to stdout.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// cat("/var/log/syslog");
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If the file does not exists then a CatException is thrown.
|
|
||||||
///
|
|
||||||
void cat(String path, {LineAction stdout = print}) =>
|
|
||||||
Cat().cat(path, stdout: stdout);
|
|
||||||
|
|
||||||
/// Class for the [cat] function.
|
|
||||||
class Cat extends DCliFunction {
|
|
||||||
/// implementation for the [cat] function.
|
|
||||||
void cat(String path, {LineAction stdout = print}) {
|
|
||||||
verbose(() => 'cat: ${truepath(path)}');
|
|
||||||
|
|
||||||
if (!exists(path)) {
|
|
||||||
throw CatException('The file at ${truepath(path)} does not exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
LineFile(path).readAll((line) {
|
|
||||||
stdout(line);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thrown if the [cat] function encouters an error.
|
|
||||||
class CatException extends DCliFunctionException {
|
|
||||||
/// Thrown if the [cat] function encouters an error.
|
|
||||||
CatException(super.reason, [super.stacktrace]);
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Copies the file [from] to the path [to].
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// copy("/tmp/fred.text", "/tmp/fred2.text", overwrite=true);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [to] may be a directory in which case the [from] filename is
|
|
||||||
/// used to construct the [to] files full path.
|
|
||||||
///
|
|
||||||
/// If [to] is a file then the file must not exist unless [overwrite]
|
|
||||||
/// is set to true.
|
|
||||||
///
|
|
||||||
/// If [to] is a directory then the directory must exist.
|
|
||||||
///
|
|
||||||
/// If [from] is a symlink we copy the file it links to rather than
|
|
||||||
/// the symlink. This mimics the behaviour of gnu 'cp' command.
|
|
||||||
///
|
|
||||||
/// If you need to copy the actualy symlink see [symlink].
|
|
||||||
///
|
|
||||||
/// The default for [overwrite] is false.
|
|
||||||
///
|
|
||||||
/// If an error occurs a [CopyException] is thrown.
|
|
||||||
|
|
||||||
void copy(String from, String to, {bool overwrite = false}) {
|
|
||||||
var finalto = to;
|
|
||||||
if (isDirectory(finalto)) {
|
|
||||||
finalto = join(finalto, basename(from));
|
|
||||||
}
|
|
||||||
verbose(() =>
|
|
||||||
'copy ${truepath(from)} -> ${truepath(finalto)} overwrite: $overwrite');
|
|
||||||
|
|
||||||
if (overwrite == false && exists(finalto, followLinks: false)) {
|
|
||||||
throw CopyException(
|
|
||||||
'The target file ${truepath(finalto)} already exists.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
/// if we are copying a symlink then we copy the file rather than
|
|
||||||
/// the symlink as this mimicks gnu 'cp'.
|
|
||||||
if (isLink(from)) {
|
|
||||||
final resolvedFrom = resolveSymLink(from);
|
|
||||||
File(resolvedFrom).copySync(finalto);
|
|
||||||
} else {
|
|
||||||
File(from).copySync(finalto);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
catch (e) {
|
|
||||||
/// lets try and improve the message.
|
|
||||||
/// We do these checks only on failure
|
|
||||||
/// so in the most common case (everything is correct)
|
|
||||||
/// we don't waste cycles on unnecessary work.
|
|
||||||
if (isDirectory(from)) {
|
|
||||||
throw CopyException(
|
|
||||||
"The 'from' argument ${truepath(from)} is a directory. "
|
|
||||||
'Use copyTree instead.');
|
|
||||||
}
|
|
||||||
if (!exists(from)) {
|
|
||||||
throw CopyException("The 'from' file ${truepath(from)} does not exists.");
|
|
||||||
}
|
|
||||||
if (!exists(dirname(to))) {
|
|
||||||
throw CopyException(
|
|
||||||
"The 'to' directory ${truepath(dirname(to))} does not exists.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw CopyException(
|
|
||||||
'An error occured copying ${truepath(from)} to ${truepath(finalto)}. '
|
|
||||||
'Error: $e',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Throw when the [copy] function encounters an error.
|
|
||||||
class CopyException extends DCliFunctionException {
|
|
||||||
/// Throw when the [copy] function encounters an error.
|
|
||||||
CopyException(super.reason);
|
|
||||||
}
|
|
|
@ -1,161 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Copies the contents of the [from] directory to the
|
|
||||||
/// [to] path with an optional filter.
|
|
||||||
///
|
|
||||||
/// The [to] path must exist.
|
|
||||||
///
|
|
||||||
/// If any copied file already exists in the [to] path then
|
|
||||||
/// an exeption is throw and a parital copyTree may occur.
|
|
||||||
///
|
|
||||||
/// You can force the copyTree to overwrite files in the [to]
|
|
||||||
/// directory by setting [overwrite] to true (defaults to false).
|
|
||||||
///
|
|
||||||
/// The [recursive] argument controls whether subdirectories are
|
|
||||||
/// copied. If [recursive] is true (the default) it will copy
|
|
||||||
/// subdirectories.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// copyTree("/tmp/", "/tmp/new_dir", overwrite:true);
|
|
||||||
/// ```
|
|
||||||
/// By default hidden files are ignored. To allow hidden files to
|
|
||||||
/// be processed set [includeHidden] to true.
|
|
||||||
///
|
|
||||||
/// You can select which files are to be copied by passing a [filter].
|
|
||||||
/// If a [filter] isn't passed then all files are copied as per
|
|
||||||
/// the [includeHidden] state.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// copyTree("/tmp/", "/tmp/new_dir", overwrite:true, includeHidden:true
|
|
||||||
/// , filter: (file) => extension(file) == 'dart');
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// The [filter] method can also be used to report progress as it
|
|
||||||
/// is called just before we copy a file.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// copyTree("/tmp/", "/tmp/new_dir", overwrite:true
|
|
||||||
/// , filter: (file) {
|
|
||||||
/// var include = extension(file) == 'dart';
|
|
||||||
/// if (include) {
|
|
||||||
/// print('copying: $file');
|
|
||||||
/// }
|
|
||||||
/// return include;
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// The default for [overwrite] is false.
|
|
||||||
///
|
|
||||||
/// If an error occurs a [CopyTreeException] is thrown.
|
|
||||||
void copyTree(
|
|
||||||
String from,
|
|
||||||
String to, {
|
|
||||||
bool overwrite = false,
|
|
||||||
bool includeHidden = false,
|
|
||||||
bool recursive = true,
|
|
||||||
bool Function(String file) filter = _allowAll,
|
|
||||||
}) =>
|
|
||||||
_CopyTree().copyTree(
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
overwrite: overwrite,
|
|
||||||
includeHidden: includeHidden,
|
|
||||||
filter: filter,
|
|
||||||
recursive: recursive,
|
|
||||||
);
|
|
||||||
|
|
||||||
bool _allowAll(String file) => true;
|
|
||||||
|
|
||||||
class _CopyTree extends DCliFunction {
|
|
||||||
void copyTree(
|
|
||||||
String from,
|
|
||||||
String to, {
|
|
||||||
bool overwrite = false,
|
|
||||||
bool Function(String file) filter = _allowAll,
|
|
||||||
bool includeHidden = false,
|
|
||||||
bool recursive = true,
|
|
||||||
}) {
|
|
||||||
verbose(() => 'copyTree: from: $from, to: $to, overwrite: $overwrite '
|
|
||||||
'includeHidden: $includeHidden recursive: $recursive ');
|
|
||||||
if (!isDirectory(from)) {
|
|
||||||
throw CopyTreeException(
|
|
||||||
'The [from] path ${truepath(from)} must be a directory.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!exists(to)) {
|
|
||||||
throw CopyTreeException(
|
|
||||||
'The [to] path ${truepath(to)} must already exist.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDirectory(to)) {
|
|
||||||
throw CopyTreeException(
|
|
||||||
'The [to] path ${truepath(to)} must be a directory.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
find('*',
|
|
||||||
workingDirectory: from,
|
|
||||||
includeHidden: includeHidden,
|
|
||||||
recursive: recursive, progress: (item) {
|
|
||||||
_process(item.pathTo, filter, from, to,
|
|
||||||
overwrite: overwrite, recursive: recursive);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
verbose(
|
|
||||||
() => 'copyTree copied: ${truepath(from)} -> ${truepath(to)}, '
|
|
||||||
'includeHidden: $includeHidden, recursive: $recursive, '
|
|
||||||
'overwrite: $overwrite',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
catch (e) {
|
|
||||||
throw CopyTreeException(
|
|
||||||
'An error occured copying directory'
|
|
||||||
' ${truepath(from)} to ${truepath(to)}. '
|
|
||||||
'Error: $e',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _process(
|
|
||||||
String file, bool Function(String file) filter, String from, String to,
|
|
||||||
{required bool overwrite, required bool recursive}) {
|
|
||||||
if (filter(file)) {
|
|
||||||
final target = join(to, relative(file, from: from));
|
|
||||||
|
|
||||||
if (recursive && !exists(dirname(target))) {
|
|
||||||
createDir(dirname(target), recursive: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!overwrite && exists(target)) {
|
|
||||||
throw CopyTreeException(
|
|
||||||
'The target file ${truepath(target)} already exists.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
copy(file, target, overwrite: overwrite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Throw when the [copy] function encounters an error.
|
|
||||||
class CopyTreeException extends DCliFunctionException {
|
|
||||||
/// Throw when the [copy] function encounters an error.
|
|
||||||
CopyTreeException(super.reason);
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
/// Creates a directory as described by [path].
|
|
||||||
/// Path may be a single path segment (e.g. bin)
|
|
||||||
/// or a full or partial tree (e.g. /usr/bin)
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// createDir("/tmp/fred/tools", recursive: true);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If [recursive] is true then any parent
|
|
||||||
/// paths that don't exist will be created.
|
|
||||||
///
|
|
||||||
/// If [recursive] is false then any parent paths
|
|
||||||
/// don't exist then a [CreateDirException] will be thrown.
|
|
||||||
///
|
|
||||||
/// If the [path] already exists an exception is thrown.
|
|
||||||
///
|
|
||||||
/// As a convenience [createDir] returns the same path
|
|
||||||
/// that it was passed.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// var path = createDir('/tmp/new_home'));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
|
|
||||||
String createDir(String path, {bool recursive = false}) =>
|
|
||||||
_CreateDir().createDir(path, recursive: recursive);
|
|
||||||
|
|
||||||
/// Creates a temp directory and then calls [action].
|
|
||||||
/// Once action completes the temporary directory will be deleted.
|
|
||||||
///
|
|
||||||
/// The actions return value [R] is returned from the [withTempDirAsync]
|
|
||||||
/// function.
|
|
||||||
///
|
|
||||||
/// If you pass [keep] = true then the temp directory won't be deleted.
|
|
||||||
/// This can be useful when testing and you need to examine the temp directory.
|
|
||||||
///
|
|
||||||
/// You can optionally pass in your own tempDir via [pathToTempDir].
|
|
||||||
/// This can be useful when sometimes you need to control the tempDir
|
|
||||||
/// and sometimes you want it created.
|
|
||||||
/// If you pass in [pathToTempDir] it will NOT be deleted regardless
|
|
||||||
/// of the value of [keep].
|
|
||||||
Future<R> withTempDirAsync<R>(Future<R> Function(String tempDir) action,
|
|
||||||
{bool keep = false, String? pathToTempDir}) async {
|
|
||||||
final dir = pathToTempDir ?? createTempDir();
|
|
||||||
|
|
||||||
R result;
|
|
||||||
try {
|
|
||||||
result = await action(dir);
|
|
||||||
} finally {
|
|
||||||
if (!keep && pathToTempDir == null) {
|
|
||||||
deleteDir(dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a temporary directory under the system temp folder.
|
|
||||||
///
|
|
||||||
/// The temporary directory name is formed from a uuid.
|
|
||||||
/// It is your responsiblity to delete the directory once you have
|
|
||||||
/// finsihed with it.
|
|
||||||
String createTempDir() => _CreateDir().createDir(
|
|
||||||
join(Directory.systemTemp.path, '.dclitmp', const Uuid().v4()),
|
|
||||||
recursive: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
class _CreateDir extends DCliFunction {
|
|
||||||
String createDir(String path, {required bool recursive}) {
|
|
||||||
verbose(() => 'createDir: ${truepath(path)} recursive: $recursive');
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (exists(path)) {
|
|
||||||
throw CreateDirException('The path ${truepath(path)} already exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory(path).createSync(recursive: recursive);
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
catch (e) {
|
|
||||||
throw CreateDirException(
|
|
||||||
'Unable to create the directory ${truepath(path)}. Error: $e',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thrown when the function [createDir] encounters an error
|
|
||||||
class CreateDirException extends DCliFunctionException {
|
|
||||||
/// Thrown when the function [createDir] encounters an error
|
|
||||||
CreateDirException(super.reason);
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
/// Base class for the classes that implement
|
|
||||||
/// the public DCli functions.
|
|
||||||
class DCliFunction {}
|
|
||||||
|
|
||||||
/// Base class for all dcli function exceptions.
|
|
||||||
class DCliFunctionException extends DCliException {
|
|
||||||
/// Base class for all dcli function exceptions.
|
|
||||||
DCliFunctionException(super.message, [super.stackTrace]);
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Deletes the file at [path].
|
|
||||||
///
|
|
||||||
/// If the file does not exists a DeleteException is thrown.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// delete("/tmp/test.fred", ask: true);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If the [path] is a directory a DeleteException is thrown.
|
|
||||||
void delete(String path) => _Delete().delete(path);
|
|
||||||
|
|
||||||
class _Delete extends DCliFunction {
|
|
||||||
void delete(String path) {
|
|
||||||
verbose(() => 'delete: ${truepath(path)}');
|
|
||||||
|
|
||||||
if (!exists(path)) {
|
|
||||||
throw DeleteException('The path ${truepath(path)} does not exists.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDirectory(path)) {
|
|
||||||
throw DeleteException('The path ${truepath(path)} is a directory.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
File(path).deleteSync();
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
catch (e) {
|
|
||||||
throw DeleteException(
|
|
||||||
'An error occured deleting ${truepath(path)}. Error: $e',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thrown when the [delete] function encounters an error
|
|
||||||
class DeleteException extends DCliFunctionException {
|
|
||||||
/// Thrown when the [delete] function encounters an error
|
|
||||||
DeleteException(super.reason);
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Deletes the directory located at [path].
|
|
||||||
///
|
|
||||||
/// If [recursive] is true (default true) then the directory and all child files
|
|
||||||
/// and directories will be deleted.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// deleteDir("/tmp/testing", recursive=true);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If [path] is not a directory then a [DeleteDirException] is thrown.
|
|
||||||
///
|
|
||||||
/// If the directory does not exists a [DeleteDirException] is thrown.
|
|
||||||
///
|
|
||||||
/// If the directory cannot be delete (e.g. permissions) a
|
|
||||||
/// [DeleteDirException] is thrown.
|
|
||||||
///
|
|
||||||
/// If recursive is false the directory must be empty otherwise a
|
|
||||||
/// [DeleteDirException] is thrown.
|
|
||||||
///
|
|
||||||
/// See [isDirectory]
|
|
||||||
/// [exists]
|
|
||||||
///
|
|
||||||
void deleteDir(String path, {bool recursive = true}) =>
|
|
||||||
_DeleteDir().deleteDir(path, recursive: recursive);
|
|
||||||
|
|
||||||
class _DeleteDir extends DCliFunction {
|
|
||||||
void deleteDir(String path, {required bool recursive}) {
|
|
||||||
verbose(() => 'deleteDir: ${truepath(path)} recursive: $recursive');
|
|
||||||
|
|
||||||
if (!exists(path)) {
|
|
||||||
throw DeleteDirException('The path ${truepath(path)} does not exist.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDirectory(path)) {
|
|
||||||
throw DeleteDirException(
|
|
||||||
'The path ${truepath(path)} is not a directory.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Directory(path).deleteSync(recursive: recursive);
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
catch (e) {
|
|
||||||
throw DeleteDirException(
|
|
||||||
'Unable to delete the directory ${truepath(path)}. Error: $e',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Throw when [deleteDir] function encounters an error
|
|
||||||
class DeleteDirException extends DCliFunctionException {
|
|
||||||
/// Throw when [deleteDir] function encounters an error
|
|
||||||
DeleteDirException(super.reason);
|
|
||||||
}
|
|
|
@ -1,135 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
/// Recursively deletes the contents of the directory located at [path]
|
|
||||||
/// with an optional filter. The directory at [path] is not deleted.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// [path] must be a directory and must exist.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// deleteTree(join(rootPath, 'tmp')));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Pass a filter to control what is deleted. Only files/directories
|
|
||||||
/// that match the filter will be deleted.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// deleteTree(join(rootPath, 'tmp')
|
|
||||||
/// , filter: (type, path) => extension(file) == 'dart');
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// The [filter] method can also be used to report progress as it
|
|
||||||
/// is called just before we move a file or directory.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// deleteTree(join(rootPath, 'tmp')
|
|
||||||
/// , filter: (entity) {
|
|
||||||
/// var delete = extension(entity) == 'dart';
|
|
||||||
/// if (delete) {
|
|
||||||
/// print('deleting: $file');
|
|
||||||
/// }
|
|
||||||
/// return delete;
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// If an error occurs a [DeleteTreeException] is thrown.
|
|
||||||
///
|
|
||||||
/// EXPERIMENTAL
|
|
||||||
void deleteTree(
|
|
||||||
String path, {
|
|
||||||
bool Function(FileSystemEntityType type, String file) filter = _deleteAll,
|
|
||||||
}) =>
|
|
||||||
_DeleteTree().deleteTree(
|
|
||||||
path,
|
|
||||||
filter: filter,
|
|
||||||
);
|
|
||||||
|
|
||||||
bool _deleteAll(FileSystemEntityType type, String file) => true;
|
|
||||||
|
|
||||||
class _DeleteTree extends DCliFunction {
|
|
||||||
void deleteTree(
|
|
||||||
String path, {
|
|
||||||
bool Function(FileSystemEntityType type, String file) filter = _deleteAll,
|
|
||||||
}) {
|
|
||||||
if (!exists(path)) {
|
|
||||||
throw DeleteTreeException(
|
|
||||||
'The [path] ${truepath(path)} does not exist.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!isDirectory(path)) {
|
|
||||||
throw DeleteTreeException(
|
|
||||||
'The [path] ${truepath(path)} is not a directory.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
verbose(() => 'deleteTree called ${truepath(path)}');
|
|
||||||
try {
|
|
||||||
find('*',
|
|
||||||
workingDirectory: path,
|
|
||||||
includeHidden: true,
|
|
||||||
types: [Find.file, Find.directory, Find.link], progress: (item) {
|
|
||||||
_process(item.pathTo, filter, item.type);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
catch (e) {
|
|
||||||
throw DeleteTreeException(
|
|
||||||
'An error occured deleting directory ${truepath(path)}. '
|
|
||||||
'Error: $e',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _process(
|
|
||||||
String pathToFile,
|
|
||||||
bool Function(FileSystemEntityType type, String file) filter,
|
|
||||||
FileSystemEntityType type,
|
|
||||||
) {
|
|
||||||
if (filter(type, pathToFile)) {
|
|
||||||
// we create directories as we go.
|
|
||||||
// only directories that contain a file that is to be
|
|
||||||
// moved will be created.
|
|
||||||
// ignore: exhaustive_cases
|
|
||||||
switch (type) {
|
|
||||||
case FileSystemEntityType.directory:
|
|
||||||
{
|
|
||||||
deleteDir(pathToFile);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case FileSystemEntityType.file:
|
|
||||||
{
|
|
||||||
delete(pathToFile);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case FileSystemEntityType.link:
|
|
||||||
{
|
|
||||||
deleteSymlink(pathToFile);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verbose(
|
|
||||||
() => 'deleteTree delting: ${truepath(pathToFile)}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thrown when the [deleteTree] function encouters an error.
|
|
||||||
class DeleteTreeException extends DCliFunctionException {
|
|
||||||
/// Thrown when the [deleteTree] function encouters an error.
|
|
||||||
DeleteTreeException(super.reason);
|
|
||||||
}
|
|
|
@ -1,428 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:scope/scope.dart';
|
|
||||||
|
|
||||||
// ignore: unused_import
|
|
||||||
import '../settings.dart';
|
|
||||||
import '../utils/dcli_exception.dart';
|
|
||||||
import '../utils/truepath.dart';
|
|
||||||
import 'dcli_function.dart';
|
|
||||||
|
|
||||||
/// Provides access to shell environment variables.
|
|
||||||
Env get env => Env();
|
|
||||||
|
|
||||||
/// Tests if the given [path] is contained
|
|
||||||
/// in the OS's PATH environment variable.
|
|
||||||
/// An canonicalized match of [path] is made against
|
|
||||||
/// each path on the OS's path.
|
|
||||||
bool isOnPATH(String path) => Env().isOnPATH(path);
|
|
||||||
|
|
||||||
/// Returns the list of directory paths that are contained
|
|
||||||
/// in the OS's PATH environment variable.
|
|
||||||
/// They are returned in the same order that they appear within
|
|
||||||
/// the PATH environment variable (as order is important.)
|
|
||||||
//ignore: non_constant_identifier_names
|
|
||||||
List<String> get PATH => Env()._path;
|
|
||||||
|
|
||||||
/// returns the path to the OS specific HOME directory
|
|
||||||
//ignore: non_constant_identifier_names
|
|
||||||
String get HOME => Env().HOME;
|
|
||||||
|
|
||||||
/// Returns a map of all the environment variables
|
|
||||||
/// inherited from the parent as well as any changes
|
|
||||||
/// made by calls to [env[]=].
|
|
||||||
///
|
|
||||||
/// See [env[]]
|
|
||||||
/// [env[]=]
|
|
||||||
Map<String, String> get envs => Env()._envVars;
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Sets gets an environment variable for the current process.
|
|
||||||
///
|
|
||||||
/// Passing a null value will remove the key from the
|
|
||||||
/// set of environment variables.
|
|
||||||
///
|
|
||||||
/// Any child process spawned will inherit these changes.
|
|
||||||
/// e.g.
|
|
||||||
/// ```
|
|
||||||
/// /// Set the environment variable 'XXX' to 'A Value'
|
|
||||||
/// env['XXX'] = 'A Value';
|
|
||||||
///
|
|
||||||
/// // the echo command will display the value off XXX.
|
|
||||||
/// '''echo $XXX'''.run;
|
|
||||||
///
|
|
||||||
/// /// Get the current value of an environment variable
|
|
||||||
/// var xxx = env['XXX'];
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// NOTE: this does NOT affect the parent
|
|
||||||
/// processes environment.
|
|
||||||
///
|
|
||||||
|
|
||||||
/// Implementation class for the functions [env[]] and [env[]=].
|
|
||||||
class Env extends DCliFunction {
|
|
||||||
/// Implementation class for the functions [env[]] and [env[]=].
|
|
||||||
/// Returns a singleton unless we are running in a [Scope]
|
|
||||||
/// and a [scopeKey] for [Env] has been placed into the scope.
|
|
||||||
factory Env() {
|
|
||||||
if (Scope.hasScopeKey(scopeKey)) {
|
|
||||||
return Scope.use(scopeKey);
|
|
||||||
} else {
|
|
||||||
return _self ??= Env._internal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use this ctor for injecting an altered Environment
|
|
||||||
/// into a Scope.
|
|
||||||
/// The main use for this ctor is for unit testing.
|
|
||||||
factory Env.forScope(Map<String, String> map) {
|
|
||||||
final env = Env._internal();
|
|
||||||
|
|
||||||
map.forEach((key, value) {
|
|
||||||
env[key] = value;
|
|
||||||
});
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|
||||||
Env._internal() : _caseSensitive = !Settings().isWindows {
|
|
||||||
final platformVars = Platform.environment;
|
|
||||||
|
|
||||||
_envVars =
|
|
||||||
CanonicalizedMap((key) => _caseSensitive ? key : key.toUpperCase());
|
|
||||||
|
|
||||||
// build a local map with all of the OS environment vars.
|
|
||||||
for (final entry in platformVars.entries) {
|
|
||||||
_envVars.putIfAbsent(entry.key, () => entry.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static ScopeKey<Env> scopeKey = ScopeKey<Env>();
|
|
||||||
static Env? _self = Env._internal();
|
|
||||||
|
|
||||||
late Map<String, String> _envVars;
|
|
||||||
|
|
||||||
final bool _caseSensitive;
|
|
||||||
|
|
||||||
/// Returns true if environment variable keys are case sensitive.
|
|
||||||
/// All OSs are case sensitive except for Windows
|
|
||||||
bool get caseSensitive => _caseSensitive;
|
|
||||||
|
|
||||||
String? _env(String name) {
|
|
||||||
verbose(() => 'env: $name:${_envVars[name]}');
|
|
||||||
|
|
||||||
return _envVars[_caseSensitive ? name : name.toUpperCase()];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the complete set of Environment variable entries.
|
|
||||||
Iterable<MapEntry<String, String>> get entries => _envVars.entries;
|
|
||||||
|
|
||||||
/// Adds all of the entries in the [other] map as environment variables.
|
|
||||||
/// Case translation will occur if the platform is case sensitive.
|
|
||||||
void addAll(Map<String, String> other) {
|
|
||||||
for (final entry in other.entries) {
|
|
||||||
_setEnv(entry.key, entry.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if an environment variable with the name
|
|
||||||
/// [key] exists.
|
|
||||||
bool exists(String key) => _envVars.keys.contains(key);
|
|
||||||
|
|
||||||
/// returns the PATH environment var as an ordered
|
|
||||||
/// array of the paths contained in the PATH.
|
|
||||||
/// The list is ordered Left to right of the paths in
|
|
||||||
/// the PATH environment var.
|
|
||||||
List<String> get _path {
|
|
||||||
final pathEnv = this['PATH'] ?? '';
|
|
||||||
|
|
||||||
return pathEnv
|
|
||||||
.split(delimiterForPATH)
|
|
||||||
// on linux an empty path equates to the current directory.
|
|
||||||
// .where((value) => value.trim().isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the value of an environment variable.
|
|
||||||
///
|
|
||||||
/// name of the environment variable.
|
|
||||||
///
|
|
||||||
/// On posix systems name of the environment variable is case sensitive.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
///```dart
|
|
||||||
///String path = env["PATH"];
|
|
||||||
///```
|
|
||||||
///
|
|
||||||
String? operator [](String name) => _env(name);
|
|
||||||
|
|
||||||
/// Sets the value of an environment variable
|
|
||||||
/// ```dart
|
|
||||||
/// env["PASS"] = 'mypassword';
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
void operator []=(String name, String? value) => _setEnv(name, value);
|
|
||||||
|
|
||||||
/// Appends [newPath] to the list of paths in the
|
|
||||||
/// PATH environment variable.
|
|
||||||
///
|
|
||||||
/// If [newPath] is already in PATH no action is taken.
|
|
||||||
///
|
|
||||||
/// Changing the PATH has no affect on the parent
|
|
||||||
/// process (shell) that launched this script.
|
|
||||||
///
|
|
||||||
/// Changing the path affects the current script
|
|
||||||
/// and any children that it spawns.
|
|
||||||
///
|
|
||||||
/// See: [prependToPATH]
|
|
||||||
/// [removeFromPATH]
|
|
||||||
void appendToPATH(String newPath) {
|
|
||||||
if (!isOnPATH(newPath)) {
|
|
||||||
final path = PATH..add(newPath);
|
|
||||||
_setEnv('PATH', path.join(delimiterForPATH));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prepends [newPath] to the list of paths in the
|
|
||||||
/// PATH environment variable provided the
|
|
||||||
/// path isn't already on the PATH.
|
|
||||||
///
|
|
||||||
/// If [newPath] is already in PATH no action is taken.
|
|
||||||
///
|
|
||||||
/// Changing the PATH has no affect on the parent
|
|
||||||
/// process (shell) that launched this script.
|
|
||||||
///
|
|
||||||
/// Changing the path affects the current script
|
|
||||||
/// and any children that it spawns.
|
|
||||||
///
|
|
||||||
/// See: [appendToPATH]
|
|
||||||
/// [removeFromPATH]
|
|
||||||
void prependToPATH(String newPath) {
|
|
||||||
if (!isOnPATH(newPath)) {
|
|
||||||
final path = PATH..insert(0, newPath);
|
|
||||||
_setEnv('PATH', path.join(delimiterForPATH));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes the given [oldPath] from the PATH environment variable.
|
|
||||||
///
|
|
||||||
/// Changing the PATH has no affect on the parent
|
|
||||||
/// process (shell) that launched this script.
|
|
||||||
///
|
|
||||||
/// Changing the path affects the current script
|
|
||||||
/// and any children that it spawns.
|
|
||||||
///
|
|
||||||
/// See: [appendToPATH]
|
|
||||||
/// [prependToPATH]
|
|
||||||
void removeFromPATH(String oldPath) {
|
|
||||||
final path = PATH..remove(oldPath);
|
|
||||||
_setEnv('PATH', path.join(delimiterForPATH));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds [newPath] to the PATH environment variable
|
|
||||||
/// if it is not already present.
|
|
||||||
///
|
|
||||||
/// The [newPath] will be added to the end of the PATH list.
|
|
||||||
///
|
|
||||||
/// Changing the PATH has no affect on the parent
|
|
||||||
/// process (shell) that launched this script.
|
|
||||||
///
|
|
||||||
/// Changing the PATH affects the current script
|
|
||||||
/// and any children that it spawns.
|
|
||||||
@Deprecated('Use appendToPATH')
|
|
||||||
void addToPATHIfAbsent(String newPath) => appendToPATH(newPath);
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Gets the path to the user's home directory
|
|
||||||
/// using the enviornment var appropriate for the user's OS.
|
|
||||||
//ignore: non_constant_identifier_names
|
|
||||||
String get HOME {
|
|
||||||
String? home;
|
|
||||||
|
|
||||||
if (Settings().isWindows) {
|
|
||||||
home = _env('APPDATA');
|
|
||||||
} else {
|
|
||||||
home = _env('HOME');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (home == null) {
|
|
||||||
if (Settings().isWindows) {
|
|
||||||
throw DCliException(
|
|
||||||
"Unable to find the 'APPDATA' enviroment variable. "
|
|
||||||
'Ensure it is set and try again.',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw DCliException(
|
|
||||||
"Unable to find the 'HOME' enviroment variable. "
|
|
||||||
'Ensure it is set and try again.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return home;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns true if the given [checkPath] is in the list
|
|
||||||
/// of paths defined in the environment variable [PATH].
|
|
||||||
bool isOnPATH(String checkPath) {
|
|
||||||
final canon = canonicalize(truepath(checkPath));
|
|
||||||
var found = false;
|
|
||||||
for (final path in _path) {
|
|
||||||
if (canonicalize(truepath(path)) == canon) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Passing a null [value] will remove the key from the
|
|
||||||
/// set of environment variables.
|
|
||||||
void _setEnv(String name, String? value) {
|
|
||||||
verbose(() => 'env[$name] = $value');
|
|
||||||
if (value == null) {
|
|
||||||
_envVars.remove(name);
|
|
||||||
if (Settings().isWindows) {
|
|
||||||
if (name == 'HOME' || name == 'APPDATA') {
|
|
||||||
_envVars
|
|
||||||
..remove('HOME')
|
|
||||||
..remove('APPDATA');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_envVars[name] = value;
|
|
||||||
|
|
||||||
if (Settings().isWindows) {
|
|
||||||
if (name == 'HOME' || name == 'APPDATA') {
|
|
||||||
_envVars['HOME'] = value;
|
|
||||||
_envVars['APPDATA'] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the delimiter used by the PATH enviorment variable.
|
|
||||||
///
|
|
||||||
/// On linix it is ':' ond windows it is ';'
|
|
||||||
///
|
|
||||||
/// NOTE do NOT confuses this with the file system path root!!!
|
|
||||||
///
|
|
||||||
String get delimiterForPATH {
|
|
||||||
var separator = ':';
|
|
||||||
|
|
||||||
if (Settings().isWindows) {
|
|
||||||
separator = ';';
|
|
||||||
}
|
|
||||||
return separator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encodes all environment variables to a json string.
|
|
||||||
/// This method is intended to be used in conjuction with
|
|
||||||
/// [fromJson].
|
|
||||||
///
|
|
||||||
/// You will find this method useful when spawning an isolate
|
|
||||||
/// that depends on environment variables created by calls
|
|
||||||
/// to DCli [env] property.
|
|
||||||
///
|
|
||||||
/// When creating an isolate it takes its environment variables
|
|
||||||
/// from [Platform.environment]. This means that any environment
|
|
||||||
/// variables created via DCli will not be visible to the isolate.
|
|
||||||
///
|
|
||||||
/// The way to over come this problem is to call [Env().toJson()]
|
|
||||||
/// pass the resulting string to the isolate and then have the
|
|
||||||
/// isolate call [Env().fromJson()] which resets the isolates
|
|
||||||
/// environment variables.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// Future<void> startIsolate() async {
|
|
||||||
/// var iso = await IsolateRunner.spawn();
|
|
||||||
///
|
|
||||||
/// try {
|
|
||||||
/// iso.run(scheduler, Env().toJson());
|
|
||||||
/// } finally {
|
|
||||||
/// await iso.close();
|
|
||||||
/// }
|
|
||||||
///}
|
|
||||||
///
|
|
||||||
/// // This method runs in the new isolate.
|
|
||||||
/// void scheduler(String jsonEnvironment) {
|
|
||||||
/// Env().fromJson(jsonEnvironment);
|
|
||||||
/// Certbot().scheduleRenews();
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
String toJson() {
|
|
||||||
final envMap = <String, String>{}..addEntries(env.entries.toSet());
|
|
||||||
return JsonEncoder(_toEncodable).convert(envMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes a json string created by [toJson]
|
|
||||||
/// clears the current set of environment variables
|
|
||||||
/// and replaces them with the environment variables
|
|
||||||
/// encoded in the json string.
|
|
||||||
///
|
|
||||||
/// If you choose not to use [toJson] to create the json
|
|
||||||
/// then [json ] must be in form of an json encoded Map<String,String>.
|
|
||||||
void fromJson(String json) {
|
|
||||||
_envVars.clear();
|
|
||||||
env.addAll(
|
|
||||||
Map<String, String>.from(
|
|
||||||
const JsonDecoder().convert(json) as Map<dynamic, dynamic>,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _toEncodable(Object? object) => object.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Injects environment variables into the scope
|
|
||||||
/// of the [callback] method.
|
|
||||||
///
|
|
||||||
/// The passed [environment] map is merged with the current [env] and
|
|
||||||
/// injected into the [callback]'s scope.
|
|
||||||
///
|
|
||||||
/// Note: code that access [Platform.environment] directly
|
|
||||||
/// will not see the environment variables injected via
|
|
||||||
/// this method. You must use the dcli [env] variable.
|
|
||||||
///
|
|
||||||
/// Any changes to [env] within the scope of the callback
|
|
||||||
/// are only visible inside that scope and revert once [callback]
|
|
||||||
/// returns.
|
|
||||||
/// This is particularly useful for unit tests and running
|
|
||||||
/// a process that requires specific environment variables.
|
|
||||||
Future<R> withEnvironmentAsync<R>(Future<R> Function() callback,
|
|
||||||
{required Map<String, String> environment}) async {
|
|
||||||
final existing = Env()._envVars;
|
|
||||||
return (Scope()
|
|
||||||
..value(Env.scopeKey, Env.forScope(existing)..addAll(environment)))
|
|
||||||
.run(() async => callback());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Injects environment variables into the scope
|
|
||||||
/// of the [callback] method.
|
|
||||||
/// You must [withEnvironmentAsync] if the callback is async.
|
|
||||||
///
|
|
||||||
/// See [withEnvironmentAsync] for general details
|
|
||||||
R withEnvironment<R>(R Function() callback,
|
|
||||||
{required Map<String, String> environment}) {
|
|
||||||
final existing = Env()._envVars;
|
|
||||||
return (Scope()
|
|
||||||
..value(Env.scopeKey, Env.forScope(existing)..addAll(environment)))
|
|
||||||
.runSync(() => callback());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Base class for all Environment variable related exceptions.
|
|
||||||
class EnvironmentException extends DCliException {
|
|
||||||
/// Create an environment variable exception.
|
|
||||||
EnvironmentException(super.message);
|
|
||||||
}
|
|
|
@ -1,564 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
// TODO(bsutton): investigate if we need to restore the
|
|
||||||
// [LimitedStreamController]
|
|
||||||
// typedef FindController<T> = LimitedStreamController<T>;
|
|
||||||
typedef ProgressCallback = bool Function(FindItem item);
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Returns the list of files in the current and child
|
|
||||||
/// directories that match the passed glob pattern.
|
|
||||||
///
|
|
||||||
/// Each file is returned as an absolute path.
|
|
||||||
///
|
|
||||||
/// You can obtain a relative path by calling:
|
|
||||||
/// ```dart
|
|
||||||
/// var relativePath = relative(filePath, from: searchRoot);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Note: this is a limited implementation of glob.
|
|
||||||
/// See the below notes for details.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// find('*.jpg', recursive:true).forEach((file) => print(file));
|
|
||||||
///
|
|
||||||
/// List<String> results = find('[a-z]*.jpg', caseSensitive:true).toList();
|
|
||||||
///
|
|
||||||
/// find('*.jpg'
|
|
||||||
/// , types:[Find.directory, Find.file])
|
|
||||||
/// .forEach((file) => print(file));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Valid patterns are:
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [*] - matches any number of any characters including none.
|
|
||||||
///
|
|
||||||
/// [?] - matches any single character
|
|
||||||
///
|
|
||||||
/// [[abc]] - matches any one character given in the bracket
|
|
||||||
///
|
|
||||||
/// [[a-z]] - matches one character from the range given in the bracket
|
|
||||||
///
|
|
||||||
/// [[!abc]] - matches one character that is not given in the bracket
|
|
||||||
///
|
|
||||||
/// [[!a-z]] - matches one character that is not from the range given
|
|
||||||
/// in the bracket
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If [caseSensitive] is true then a case sensitive match is performed.
|
|
||||||
/// [caseSensitive] defaults to false.
|
|
||||||
///
|
|
||||||
/// If [recursive] is true then a recursive search of all subdirectories
|
|
||||||
/// (all the way down) is performed.
|
|
||||||
/// [recursive] is true by default.
|
|
||||||
///
|
|
||||||
/// [includeHidden] controls whether hidden files (.xx) are returned and
|
|
||||||
/// whether hidden directorys (.xx) are recursed into when the [recursive]
|
|
||||||
/// option is true. By default hidden files and directories are ignored.
|
|
||||||
/// If the wildcard begins with a '.' then includeHidden will be enabled
|
|
||||||
/// automatically.
|
|
||||||
///
|
|
||||||
/// [types] allows you to specify the file types you want the find to return.
|
|
||||||
/// By default [types] limits the results to files.
|
|
||||||
///
|
|
||||||
/// [workingDirectory] allows you to specify an alternate d
|
|
||||||
/// irectory to seach within
|
|
||||||
/// rather than the current work directory.
|
|
||||||
///
|
|
||||||
/// [types] the list of types to search file. Defaults to [Find.file].
|
|
||||||
/// See [Find.file], [Find.directory], [Find.link].
|
|
||||||
///
|
|
||||||
/// Passing a [progress] will allow you to process the results as the are
|
|
||||||
/// produced rather than having to wait for the call to find to complete.
|
|
||||||
/// The passed progress is also returned.
|
|
||||||
/// If the [progress] doesn't output [stdout] then you will get no results
|
|
||||||
/// back.
|
|
||||||
///
|
|
||||||
// TODO(bsutton): consider having find return a Stream and eliminate passing
|
|
||||||
/// a controller in.
|
|
||||||
void find(
|
|
||||||
String pattern, {
|
|
||||||
required ProgressCallback progress,
|
|
||||||
bool caseSensitive = false,
|
|
||||||
bool recursive = true,
|
|
||||||
bool includeHidden = false,
|
|
||||||
String workingDirectory = '.',
|
|
||||||
List<FileSystemEntityType> types = const [Find.file],
|
|
||||||
}) =>
|
|
||||||
Find()._find(
|
|
||||||
pattern,
|
|
||||||
caseSensitive: caseSensitive,
|
|
||||||
recursive: recursive,
|
|
||||||
includeHidden: includeHidden,
|
|
||||||
workingDirectory: workingDirectory,
|
|
||||||
progress: progress,
|
|
||||||
types: types,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Implementation for the [_find] function.
|
|
||||||
class Find extends DCliFunction {
|
|
||||||
final bool _closed = false;
|
|
||||||
|
|
||||||
/// Find matching files an call [progress] for each one.
|
|
||||||
void _find(
|
|
||||||
String pattern, {
|
|
||||||
required ProgressCallback progress,
|
|
||||||
bool caseSensitive = false,
|
|
||||||
bool recursive = true,
|
|
||||||
String workingDirectory = '.',
|
|
||||||
List<FileSystemEntityType> types = const [Find.file],
|
|
||||||
bool includeHidden = false,
|
|
||||||
}) {
|
|
||||||
final config = FindConfig.build(
|
|
||||||
pattern: pattern,
|
|
||||||
workingDirectory: workingDirectory,
|
|
||||||
includeHidden: includeHidden,
|
|
||||||
caseSensitive: caseSensitive);
|
|
||||||
|
|
||||||
_innerFind(
|
|
||||||
config: config,
|
|
||||||
recursive: recursive,
|
|
||||||
progress: progress,
|
|
||||||
types: types,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _innerFind({
|
|
||||||
required FindConfig config,
|
|
||||||
required ProgressCallback progress,
|
|
||||||
bool recursive = true,
|
|
||||||
List<FileSystemEntityType> types = const [Find.file],
|
|
||||||
}) {
|
|
||||||
verbose(
|
|
||||||
() => 'find: pwd: $pwd '
|
|
||||||
'workingDirectory: ${truepath(config.workingDirectory)} '
|
|
||||||
'pattern: ${config.pattern} caseSensitive: ${config.caseSensitive} '
|
|
||||||
'recursive: $recursive types: $types ',
|
|
||||||
);
|
|
||||||
final nextLevel = List<FileSystemEntity?>.filled(100, null, growable: true);
|
|
||||||
final singleDirectory =
|
|
||||||
List<FileSystemEntity?>.filled(100, null, growable: true);
|
|
||||||
final childDirectories =
|
|
||||||
List<FileSystemEntity?>.filled(100, null, growable: true);
|
|
||||||
if (!_processDirectory(
|
|
||||||
config.workingDirectory,
|
|
||||||
config.workingDirectory,
|
|
||||||
recursive,
|
|
||||||
types,
|
|
||||||
config.matcher,
|
|
||||||
config.includeHidden,
|
|
||||||
progress,
|
|
||||||
childDirectories,
|
|
||||||
)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
while (childDirectories[0] != null) {
|
|
||||||
_zeroElements(nextLevel);
|
|
||||||
for (final directory in childDirectories) {
|
|
||||||
if (directory == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// print('calling _processDirectory ${count++}');
|
|
||||||
if (!_processDirectory(
|
|
||||||
config.workingDirectory,
|
|
||||||
directory.path,
|
|
||||||
recursive,
|
|
||||||
types,
|
|
||||||
config.matcher,
|
|
||||||
config.includeHidden,
|
|
||||||
progress,
|
|
||||||
singleDirectory,
|
|
||||||
)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_appendTo(nextLevel, singleDirectory);
|
|
||||||
_zeroElements(singleDirectory);
|
|
||||||
}
|
|
||||||
_copyInto(childDirectories, nextLevel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _processDirectory(
|
|
||||||
String workingDirectory,
|
|
||||||
String currentDirectory,
|
|
||||||
bool recursive,
|
|
||||||
List<FileSystemEntityType> types,
|
|
||||||
PatternMatcher matcher,
|
|
||||||
bool includeHidden,
|
|
||||||
ProgressCallback progress,
|
|
||||||
List<FileSystemEntity?> nextLevel,
|
|
||||||
) {
|
|
||||||
// print('process Directory ${dircount++}');
|
|
||||||
final list = Directory(currentDirectory).listSync(followLinks: false);
|
|
||||||
|
|
||||||
var nextLevelIndex = 0;
|
|
||||||
|
|
||||||
for (final entity in list) {
|
|
||||||
try {
|
|
||||||
late final FileSystemEntityType type;
|
|
||||||
type = FileSystemEntity.typeSync(entity.path, followLinks: false);
|
|
||||||
|
|
||||||
if (types.contains(type) &&
|
|
||||||
matcher.match(entity.path) &&
|
|
||||||
_allowed(
|
|
||||||
workingDirectory,
|
|
||||||
entity,
|
|
||||||
includeHidden: includeHidden,
|
|
||||||
)) {
|
|
||||||
if (_closed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If the controller has been paused or hasn't yet been
|
|
||||||
/// listened to then we don't want to add files to
|
|
||||||
/// it otherwise we may run out of memory.
|
|
||||||
if (!progress(FindItem(entity.path, type))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If we are recursing then we need to add any directories
|
|
||||||
/// to the list of childDirectories that need to be recursed.
|
|
||||||
if (recursive && type == Find.directory) {
|
|
||||||
if (nextLevel.length > nextLevelIndex) {
|
|
||||||
nextLevel[nextLevelIndex] = entity;
|
|
||||||
} else {
|
|
||||||
nextLevel.add(entity);
|
|
||||||
}
|
|
||||||
nextLevelIndex++;
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
} catch (e) {
|
|
||||||
if (_isGeneralIOError(e)) {
|
|
||||||
/// can mean a corrupt disk, problems with virtualisation
|
|
||||||
/// I've seen this when gdrive.
|
|
||||||
} else if (e is FileSystemException &&
|
|
||||||
e.osError?.errorCode == _accessDenied) {
|
|
||||||
/// check for and ignore permission denied.
|
|
||||||
verbose(() => 'Permission denied: ${e.path}');
|
|
||||||
} else if (e is FileSystemException && e.osError?.errorCode == 40) {
|
|
||||||
/// ignore recursive symbolic link problems.
|
|
||||||
verbose(() => 'Too many levels of symbolic links: ${e.path}');
|
|
||||||
} else if (e is FileSystemException && e.osError?.errorCode == 22) {
|
|
||||||
/// Invalid argument - not really certain what this means but we get
|
|
||||||
/// it when processing a .steam folder that includes a windows
|
|
||||||
/// emulator.
|
|
||||||
verbose(() => 'Invalid argument: ${e.path}');
|
|
||||||
} else if (e is FileSystemException &&
|
|
||||||
e.osError?.errorCode == _directoryNotFound) {
|
|
||||||
/// The directory may have been deleted between us finding it and
|
|
||||||
/// processing it.
|
|
||||||
verbose(
|
|
||||||
() => 'File or Directory deleted whilst we were processing it:'
|
|
||||||
' ${e.path}',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// ignore: only_throw_errors
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int get _accessDenied => Settings().isWindows ? 5 : 13;
|
|
||||||
int get _directoryNotFound => Settings().isWindows ? 3 : 2;
|
|
||||||
|
|
||||||
/// Checks if a hidden file is allowed.
|
|
||||||
/// Non-hidden files are always allowed.
|
|
||||||
bool _allowed(
|
|
||||||
String workingDirectory,
|
|
||||||
FileSystemEntity entity, {
|
|
||||||
required bool includeHidden,
|
|
||||||
}) =>
|
|
||||||
includeHidden || !_isHidden(workingDirectory, entity);
|
|
||||||
|
|
||||||
// check if the entity is a hidden file (.xxx) or
|
|
||||||
// if lives in a hidden directory.
|
|
||||||
bool _isHidden(String workingDirectory, FileSystemEntity entity) {
|
|
||||||
final relativePath = relative(entity.path, from: workingDirectory);
|
|
||||||
|
|
||||||
final parts = relativePath.split(separator);
|
|
||||||
|
|
||||||
var isHidden = false;
|
|
||||||
for (final part in parts) {
|
|
||||||
if (part.startsWith('.')) {
|
|
||||||
isHidden = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isHidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// set all elements in the array to null so we can re-use the list
|
|
||||||
/// to reduce GC.
|
|
||||||
void _zeroElements(List<FileSystemEntity?> nextLevel) {
|
|
||||||
for (var i = 0; i < nextLevel.length && nextLevel[i] != null; i++) {
|
|
||||||
nextLevel[i] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _copyInto(
|
|
||||||
List<FileSystemEntity?> childDirectories,
|
|
||||||
List<FileSystemEntity?> nextLevel,
|
|
||||||
) {
|
|
||||||
_zeroElements(childDirectories);
|
|
||||||
for (var i = 0; i < nextLevel.length; i++) {
|
|
||||||
if (childDirectories.length > i) {
|
|
||||||
childDirectories[i] = nextLevel[i];
|
|
||||||
} else {
|
|
||||||
childDirectories.add(nextLevel[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _appendTo(
|
|
||||||
List<FileSystemEntity?> nextLevel,
|
|
||||||
List<FileSystemEntity?> singleDirectory,
|
|
||||||
) {
|
|
||||||
var index = _firstAvailable(nextLevel);
|
|
||||||
|
|
||||||
for (var i = 0; i < singleDirectory.length; i++) {
|
|
||||||
if (singleDirectory[i] == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (index >= nextLevel.length) {
|
|
||||||
nextLevel.add(singleDirectory[i]);
|
|
||||||
index++;
|
|
||||||
} else {
|
|
||||||
nextLevel[index++] = singleDirectory[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int _firstAvailable(List<FileSystemEntity?> nextLevel) {
|
|
||||||
var firstAvailable = 0;
|
|
||||||
while (firstAvailable < nextLevel.length &&
|
|
||||||
nextLevel[firstAvailable] != null) {
|
|
||||||
firstAvailable++;
|
|
||||||
}
|
|
||||||
return firstAvailable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// pass as a value to the find types argument
|
|
||||||
/// to select files to be found
|
|
||||||
static const file = FileSystemEntityType.file;
|
|
||||||
|
|
||||||
/// pass as a value to the final types argument
|
|
||||||
/// to select directories to be found
|
|
||||||
static const directory = FileSystemEntityType.directory;
|
|
||||||
|
|
||||||
/// pass as a value to the final types argument
|
|
||||||
/// to select links to be found
|
|
||||||
static const link = FileSystemEntityType.link;
|
|
||||||
|
|
||||||
bool _isGeneralIOError(Object e) {
|
|
||||||
var error = false;
|
|
||||||
error = e is FileSystemException &&
|
|
||||||
!Platform.isWindows &&
|
|
||||||
e.osError?.errorCode == 5;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
verbose(() => 'General IO Error(5) accessing: ${e.path}');
|
|
||||||
}
|
|
||||||
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PatternMatcher {
|
|
||||||
PatternMatcher(
|
|
||||||
this.pattern, {
|
|
||||||
required this.workingDirectory,
|
|
||||||
required this.caseSensitive,
|
|
||||||
}) {
|
|
||||||
regEx = buildRegEx();
|
|
||||||
|
|
||||||
final patternParts = split(dirname(pattern));
|
|
||||||
var count = patternParts.length;
|
|
||||||
if (patternParts.length == 1 && patternParts[0] == '.') {
|
|
||||||
count = 0;
|
|
||||||
}
|
|
||||||
directoryParts = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
String pattern;
|
|
||||||
String workingDirectory;
|
|
||||||
late RegExp regEx;
|
|
||||||
bool caseSensitive;
|
|
||||||
|
|
||||||
/// the no. of directories in the pattern
|
|
||||||
late final int directoryParts;
|
|
||||||
|
|
||||||
bool match(String path) {
|
|
||||||
final matchPart = _extractMatchPart(path);
|
|
||||||
// print('path: $path, matchPart: $matchPart pattern: $pattern');
|
|
||||||
return regEx.stringMatch(matchPart) == matchPart;
|
|
||||||
}
|
|
||||||
|
|
||||||
RegExp buildRegEx() {
|
|
||||||
var regEx = '';
|
|
||||||
|
|
||||||
for (var i = 0; i < pattern.length; i++) {
|
|
||||||
final char = pattern[i];
|
|
||||||
|
|
||||||
switch (char) {
|
|
||||||
case '[':
|
|
||||||
regEx += '[';
|
|
||||||
break;
|
|
||||||
case ']':
|
|
||||||
regEx += ']';
|
|
||||||
break;
|
|
||||||
case '*':
|
|
||||||
regEx += '.*';
|
|
||||||
break;
|
|
||||||
case '?':
|
|
||||||
regEx += '.';
|
|
||||||
break;
|
|
||||||
case '-':
|
|
||||||
regEx += '-';
|
|
||||||
break;
|
|
||||||
case '!':
|
|
||||||
regEx += '^';
|
|
||||||
break;
|
|
||||||
case '.':
|
|
||||||
regEx += r'\.';
|
|
||||||
break;
|
|
||||||
case r'\':
|
|
||||||
regEx += r'\\';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
regEx += char;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return RegExp(regEx, caseSensitive: caseSensitive);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A pattern may contain a relative path in which case
|
|
||||||
/// we need to match [path] with the same no. of directories
|
|
||||||
/// as is contained in the pattern.
|
|
||||||
///
|
|
||||||
/// This method extracts the components of a absolute [path]
|
|
||||||
/// that must be used when doing the pattern match.
|
|
||||||
String _extractMatchPart(String path) {
|
|
||||||
if (directoryParts == 0) {
|
|
||||||
return basename(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
final pathParts = split(dirname(relative(path, from: workingDirectory)));
|
|
||||||
|
|
||||||
var partsCount = pathParts.length;
|
|
||||||
if (pathParts.length == 1 && pathParts[0] == '.') {
|
|
||||||
partsCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If the path doesn't have enough parts then just
|
|
||||||
/// return the path relative to the workingDirectory.
|
|
||||||
if (partsCount < directoryParts) {
|
|
||||||
return relative(path, from: workingDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// return just the required parts.
|
|
||||||
return joinAll(
|
|
||||||
[...pathParts.sublist(partsCount - directoryParts), basename(path)],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//typedef FindProgress = Future<void> Function(String path);
|
|
||||||
//typedef FindProgress = Sink<FindItem>();
|
|
||||||
|
|
||||||
/// Holds details of a file system entity returned by the
|
|
||||||
/// [find] function.
|
|
||||||
class FindItem {
|
|
||||||
/// [pathTo] is the path to the file system entity
|
|
||||||
/// [type] is the type of file system entity.
|
|
||||||
FindItem(this.pathTo, this.type);
|
|
||||||
|
|
||||||
/// the path to the file system entity
|
|
||||||
String pathTo;
|
|
||||||
|
|
||||||
/// type of file system entity
|
|
||||||
FileSystemEntityType type;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => pathTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thrown when the [find] function encouters an error.
|
|
||||||
class FindException extends DCliFunctionException {
|
|
||||||
/// Thrown when the [move] function encouters an error.
|
|
||||||
FindException(super.reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
class FindConfig {
|
|
||||||
factory FindConfig.build(
|
|
||||||
{required String pattern,
|
|
||||||
required String workingDirectory,
|
|
||||||
required bool includeHidden,
|
|
||||||
required bool caseSensitive}) {
|
|
||||||
/// strip any path components out of the pattern
|
|
||||||
/// and add them to the working directory.
|
|
||||||
/// If there is no dirname component we get '.'
|
|
||||||
final directoryPart = dirname(pattern);
|
|
||||||
if (directoryPart != '.') {
|
|
||||||
workingDirectory = join(workingDirectory, directoryPart);
|
|
||||||
}
|
|
||||||
pattern = basename(pattern);
|
|
||||||
|
|
||||||
if (!exists(workingDirectory)) {
|
|
||||||
throw FindException(
|
|
||||||
'The path ${truepath(workingDirectory)} does not exists',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final matcher = PatternMatcher(
|
|
||||||
pattern,
|
|
||||||
caseSensitive: caseSensitive,
|
|
||||||
workingDirectory: workingDirectory,
|
|
||||||
);
|
|
||||||
if (workingDirectory == '.') {
|
|
||||||
workingDirectory = pwd;
|
|
||||||
} else {
|
|
||||||
workingDirectory = truepath(workingDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (basename(pattern).startsWith('.')) {
|
|
||||||
includeHidden = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return FindConfig._(
|
|
||||||
workingDirectory: workingDirectory,
|
|
||||||
pattern: pattern,
|
|
||||||
includeHidden: includeHidden,
|
|
||||||
caseSensitive: caseSensitive,
|
|
||||||
matcher: matcher);
|
|
||||||
}
|
|
||||||
FindConfig._(
|
|
||||||
{required this.workingDirectory,
|
|
||||||
required this.pattern,
|
|
||||||
required this.includeHidden,
|
|
||||||
required this.caseSensitive,
|
|
||||||
required this.matcher});
|
|
||||||
String workingDirectory;
|
|
||||||
String pattern;
|
|
||||||
bool includeHidden;
|
|
||||||
bool caseSensitive;
|
|
||||||
PatternMatcher matcher;
|
|
||||||
}
|
|
|
@ -1,369 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Returns the list of files in the current and child
|
|
||||||
/// directories that match the passed glob pattern as a Stream
|
|
||||||
/// of absolute paths.
|
|
||||||
///
|
|
||||||
/// You can obtain a relative path by calling:
|
|
||||||
/// ```dart
|
|
||||||
/// var relativePath = relative(filePath, from: searchRoot);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Note: this is a limited implementation of glob.
|
|
||||||
/// See the below notes for details.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// await for (final file in find('*.jpg', recursive:true))
|
|
||||||
/// print(file);
|
|
||||||
///
|
|
||||||
/// List<String> results = findAsync('[a-z]*.jpg', caseSensitive:true).toList();
|
|
||||||
///
|
|
||||||
/// await for (final file in find('*.jpg', types:[Find.directory, Find.file])
|
|
||||||
/// print(file);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Valid patterns are:
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [*] - matches any number of any characters including none.
|
|
||||||
///
|
|
||||||
/// [?] - matches any single character
|
|
||||||
///
|
|
||||||
/// [[abc]] - matches any one character given in the bracket
|
|
||||||
///
|
|
||||||
/// [[a-z]] - matches one character from the range given in the bracket
|
|
||||||
///
|
|
||||||
/// [[!abc]] - matches one character that is not given in the bracket
|
|
||||||
///
|
|
||||||
/// [[!a-z]] - matches one character that is not from the range given
|
|
||||||
/// in the bracket
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If [caseSensitive] is true then a case sensitive match is performed.
|
|
||||||
/// [caseSensitive] defaults to false.
|
|
||||||
///
|
|
||||||
/// If [recursive] is true then a recursive search of all subdirectories
|
|
||||||
/// (all the way down) is performed.
|
|
||||||
/// [recursive] is true by default.
|
|
||||||
///
|
|
||||||
/// [includeHidden] controls whether hidden files (.xx) are returned and
|
|
||||||
/// whether hidden directorys (.xx) are recursed into when the [recursive]
|
|
||||||
/// option is true. By default hidden files and directories are ignored.
|
|
||||||
/// If the wildcard begins with a '.' then includeHidden will be enabled
|
|
||||||
/// automatically.
|
|
||||||
///
|
|
||||||
/// [types] allows you to specify the file types you want the find to return.
|
|
||||||
/// By default [types] limits the results to files.
|
|
||||||
///
|
|
||||||
/// [workingDirectory] allows you to specify an alternate d
|
|
||||||
/// irectory to seach within
|
|
||||||
/// rather than the current work directory.
|
|
||||||
///
|
|
||||||
/// [types] the list of types to search file. Defaults to [Find.file].
|
|
||||||
/// See [Find.file], [Find.directory], [Find.link].
|
|
||||||
///
|
|
||||||
Stream<FindItem> findAsync(
|
|
||||||
String pattern, {
|
|
||||||
bool caseSensitive = false,
|
|
||||||
bool recursive = true,
|
|
||||||
bool includeHidden = false,
|
|
||||||
String workingDirectory = '.',
|
|
||||||
List<FileSystemEntityType> types = const [Find.file],
|
|
||||||
}) async* {
|
|
||||||
// We us a [LimitedStreamController] as a slow reader
|
|
||||||
// can cause an out of memory exception if we keep pumping
|
|
||||||
// more files into the stream.
|
|
||||||
// ignore: close_sinks
|
|
||||||
final controller = LimitedStreamController<FindItem>(100);
|
|
||||||
await FindAsync()._findAsync(
|
|
||||||
pattern,
|
|
||||||
caseSensitive: caseSensitive,
|
|
||||||
recursive: recursive,
|
|
||||||
includeHidden: includeHidden,
|
|
||||||
workingDirectory: workingDirectory,
|
|
||||||
controller: controller,
|
|
||||||
types: types,
|
|
||||||
);
|
|
||||||
|
|
||||||
yield* controller.stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implementation for the [_findAsync] function.
|
|
||||||
class FindAsync extends DCliFunction {
|
|
||||||
final bool _closed = false;
|
|
||||||
|
|
||||||
/// Find matching files and return them as a stream
|
|
||||||
Future<void> _findAsync(
|
|
||||||
String pattern, {
|
|
||||||
required LimitedStreamController<FindItem> controller,
|
|
||||||
bool caseSensitive = false,
|
|
||||||
bool recursive = true,
|
|
||||||
String workingDirectory = '.',
|
|
||||||
List<FileSystemEntityType> types = const [Find.file],
|
|
||||||
bool includeHidden = false,
|
|
||||||
}) async {
|
|
||||||
final config = FindConfig.build(
|
|
||||||
pattern: pattern,
|
|
||||||
workingDirectory: workingDirectory,
|
|
||||||
includeHidden: includeHidden,
|
|
||||||
caseSensitive: caseSensitive);
|
|
||||||
|
|
||||||
await _innerFindAsync(
|
|
||||||
config: config,
|
|
||||||
recursive: recursive,
|
|
||||||
controller: controller,
|
|
||||||
types: types,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _innerFindAsync({
|
|
||||||
required FindConfig config,
|
|
||||||
required LimitedStreamController<FindItem> controller,
|
|
||||||
bool recursive = true,
|
|
||||||
List<FileSystemEntityType> types = const [Find.file],
|
|
||||||
}) async {
|
|
||||||
verbose(
|
|
||||||
() => 'find: pwd: $pwd '
|
|
||||||
'workingDirectory: ${truepath(config.workingDirectory)} '
|
|
||||||
'pattern: ${config.pattern} caseSensitive: ${config.caseSensitive} '
|
|
||||||
'recursive: $recursive types: $types ',
|
|
||||||
);
|
|
||||||
final nextLevel = List<FileSystemEntity?>.filled(100, null, growable: true);
|
|
||||||
final singleDirectory =
|
|
||||||
List<FileSystemEntity?>.filled(100, null, growable: true);
|
|
||||||
final childDirectories =
|
|
||||||
List<FileSystemEntity?>.filled(100, null, growable: true);
|
|
||||||
|
|
||||||
if (!await _processDirectory(
|
|
||||||
config,
|
|
||||||
config.workingDirectory,
|
|
||||||
recursive,
|
|
||||||
types,
|
|
||||||
controller,
|
|
||||||
childDirectories,
|
|
||||||
)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
while (childDirectories[0] != null) {
|
|
||||||
_zeroElements(nextLevel);
|
|
||||||
for (final directory in childDirectories) {
|
|
||||||
if (directory == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// print('calling _processDirectory ${count++}');
|
|
||||||
if (!await _processDirectory(
|
|
||||||
config,
|
|
||||||
directory.path,
|
|
||||||
recursive,
|
|
||||||
types,
|
|
||||||
controller,
|
|
||||||
singleDirectory,
|
|
||||||
)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_appendTo(nextLevel, singleDirectory);
|
|
||||||
_zeroElements(singleDirectory);
|
|
||||||
}
|
|
||||||
_copyInto(childDirectories, nextLevel);
|
|
||||||
}
|
|
||||||
unawaited(controller.close());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _processDirectory(
|
|
||||||
FindConfig config,
|
|
||||||
String currentDirectory,
|
|
||||||
bool recursive,
|
|
||||||
List<FileSystemEntityType> types,
|
|
||||||
LimitedStreamController<FindItem> controller,
|
|
||||||
List<FileSystemEntity?> nextLevel,
|
|
||||||
) async {
|
|
||||||
// print('process Directory ${dircount++}');
|
|
||||||
|
|
||||||
var nextLevelIndex = 0;
|
|
||||||
|
|
||||||
await for (final entity
|
|
||||||
in Directory(currentDirectory).list(followLinks: false)) {
|
|
||||||
try {
|
|
||||||
late final FileSystemEntityType type;
|
|
||||||
type = FileSystemEntity.typeSync(entity.path, followLinks: false);
|
|
||||||
|
|
||||||
if (types.contains(type) &&
|
|
||||||
config.matcher.match(entity.path) &&
|
|
||||||
_allowed(
|
|
||||||
config.workingDirectory,
|
|
||||||
entity,
|
|
||||||
includeHidden: config.includeHidden,
|
|
||||||
)) {
|
|
||||||
if (_closed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(bsutton): do we need to wait if the controller is
|
|
||||||
/// paused?
|
|
||||||
await controller.asyncAdd(FindItem(entity.path, type));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If we are recursing then we need to add any directories
|
|
||||||
/// to the list of childDirectories that need to be recursed.
|
|
||||||
if (recursive && type == Find.directory) {
|
|
||||||
if (nextLevel.length > nextLevelIndex) {
|
|
||||||
nextLevel[nextLevelIndex] = entity;
|
|
||||||
} else {
|
|
||||||
nextLevel.add(entity);
|
|
||||||
}
|
|
||||||
nextLevelIndex++;
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
} catch (e) {
|
|
||||||
if (_isGeneralIOError(e)) {
|
|
||||||
/// can mean a corrupt disk, problems with virtualisation
|
|
||||||
/// I've seen this when gdrive.
|
|
||||||
} else if (e is FileSystemException &&
|
|
||||||
e.osError?.errorCode == _accessDenied) {
|
|
||||||
/// check for and ignore permission denied.
|
|
||||||
verbose(() => 'Permission denied: ${e.path}');
|
|
||||||
} else if (e is FileSystemException && e.osError?.errorCode == 40) {
|
|
||||||
/// ignore recursive symbolic link problems.
|
|
||||||
verbose(() => 'Too many levels of symbolic links: ${e.path}');
|
|
||||||
} else if (e is FileSystemException && e.osError?.errorCode == 22) {
|
|
||||||
/// Invalid argument - not really certain what this means but we get
|
|
||||||
/// it when processing a .steam folder that includes a windows
|
|
||||||
/// emulator.
|
|
||||||
verbose(() => 'Invalid argument: ${e.path}');
|
|
||||||
} else if (e is FileSystemException &&
|
|
||||||
e.osError?.errorCode == _directoryNotFound) {
|
|
||||||
/// The directory may have been deleted between us finding it and
|
|
||||||
/// processing it.
|
|
||||||
verbose(
|
|
||||||
() => 'File or Directory deleted whilst we were processing it:'
|
|
||||||
' ${e.path}',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// ignore: only_throw_errors
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int get _accessDenied => Settings().isWindows ? 5 : 13;
|
|
||||||
int get _directoryNotFound => Settings().isWindows ? 3 : 2;
|
|
||||||
|
|
||||||
/// Checks if a hidden file is allowed.
|
|
||||||
/// Non-hidden files are always allowed.
|
|
||||||
bool _allowed(
|
|
||||||
String workingDirectory,
|
|
||||||
FileSystemEntity entity, {
|
|
||||||
required bool includeHidden,
|
|
||||||
}) =>
|
|
||||||
includeHidden || !_isHidden(workingDirectory, entity);
|
|
||||||
|
|
||||||
// check if the entity is a hidden file (.xxx) or
|
|
||||||
// if lives in a hidden directory.
|
|
||||||
bool _isHidden(String workingDirectory, FileSystemEntity entity) {
|
|
||||||
final relativePath = relative(entity.path, from: workingDirectory);
|
|
||||||
|
|
||||||
final parts = relativePath.split(separator);
|
|
||||||
|
|
||||||
var isHidden = false;
|
|
||||||
for (final part in parts) {
|
|
||||||
if (part.startsWith('.')) {
|
|
||||||
isHidden = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isHidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// set all elements in the array to null so we can re-use the list
|
|
||||||
/// to reduce GC.
|
|
||||||
void _zeroElements(List<FileSystemEntity?> nextLevel) {
|
|
||||||
for (var i = 0; i < nextLevel.length && nextLevel[i] != null; i++) {
|
|
||||||
nextLevel[i] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _copyInto(
|
|
||||||
List<FileSystemEntity?> childDirectories,
|
|
||||||
List<FileSystemEntity?> nextLevel,
|
|
||||||
) {
|
|
||||||
_zeroElements(childDirectories);
|
|
||||||
for (var i = 0; i < nextLevel.length; i++) {
|
|
||||||
if (childDirectories.length > i) {
|
|
||||||
childDirectories[i] = nextLevel[i];
|
|
||||||
} else {
|
|
||||||
childDirectories.add(nextLevel[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _appendTo(
|
|
||||||
List<FileSystemEntity?> nextLevel,
|
|
||||||
List<FileSystemEntity?> singleDirectory,
|
|
||||||
) {
|
|
||||||
var index = _firstAvailable(nextLevel);
|
|
||||||
|
|
||||||
for (var i = 0; i < singleDirectory.length; i++) {
|
|
||||||
if (singleDirectory[i] == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (index >= nextLevel.length) {
|
|
||||||
nextLevel.add(singleDirectory[i]);
|
|
||||||
index++;
|
|
||||||
} else {
|
|
||||||
nextLevel[index++] = singleDirectory[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int _firstAvailable(List<FileSystemEntity?> nextLevel) {
|
|
||||||
var firstAvailable = 0;
|
|
||||||
while (firstAvailable < nextLevel.length &&
|
|
||||||
nextLevel[firstAvailable] != null) {
|
|
||||||
firstAvailable++;
|
|
||||||
}
|
|
||||||
return firstAvailable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// pass as a value to the find types argument
|
|
||||||
/// to select files to be found
|
|
||||||
static const file = FileSystemEntityType.file;
|
|
||||||
|
|
||||||
/// pass as a value to the final types argument
|
|
||||||
/// to select directories to be found
|
|
||||||
static const directory = FileSystemEntityType.directory;
|
|
||||||
|
|
||||||
/// pass as a value to the final types argument
|
|
||||||
/// to select links to be found
|
|
||||||
static const link = FileSystemEntityType.link;
|
|
||||||
|
|
||||||
bool _isGeneralIOError(Object e) {
|
|
||||||
var error = false;
|
|
||||||
error = e is FileSystemException &&
|
|
||||||
!Platform.isWindows &&
|
|
||||||
e.osError?.errorCode == 5;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
verbose(() => 'General IO Error(5) accessing: ${e.path}');
|
|
||||||
}
|
|
||||||
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Returns count [lines] from the file at [path].
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// head('/var/log/syslog', 10).forEach((line) => print(line));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Throws a [HeadException] exception if [path] is not a file.
|
|
||||||
///
|
|
||||||
List<String> head(String path, int lines) => _Head().head(path, lines);
|
|
||||||
|
|
||||||
class _Head extends DCliFunction {
|
|
||||||
List<String> head(
|
|
||||||
String path,
|
|
||||||
int lines,
|
|
||||||
) {
|
|
||||||
verbose(() => 'head ${truepath(path)} lines: $lines');
|
|
||||||
|
|
||||||
if (!exists(path)) {
|
|
||||||
throw HeadException('The path ${truepath(path)} does not exist.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isFile(path)) {
|
|
||||||
throw HeadException('The path ${truepath(path)} is not a file.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return withOpenLineFile(path, (file) {
|
|
||||||
final result = <String>[];
|
|
||||||
file.readAll((line) {
|
|
||||||
result.add(line);
|
|
||||||
return result.length < lines;
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
catch (e) {
|
|
||||||
throw HeadException(
|
|
||||||
'An error occured reading ${truepath(path)}. Error: $e',
|
|
||||||
);
|
|
||||||
} finally {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thrown if the [head] function encounters an error.
|
|
||||||
class HeadException extends DCliFunctionException {
|
|
||||||
/// Thrown if the [head] function encounters an error.
|
|
||||||
HeadException(super.reason);
|
|
||||||
}
|
|
|
@ -1,169 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:stack_trace/stack_trace.dart';
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
// import 'package:posix/posix.dart' as posix;
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Returns true if the given [path] points to a file.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// isFile("~/fred.jpg");
|
|
||||||
/// ```
|
|
||||||
bool isFile(String path) => _Is().isFile(path);
|
|
||||||
|
|
||||||
/// Returns true if the given [path] is a directory.
|
|
||||||
/// ```dart
|
|
||||||
/// isDirectory("/tmp");
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
bool isDirectory(String path) => _Is().isDirectory(path);
|
|
||||||
|
|
||||||
/// Returns true if the given [path] is a symlink
|
|
||||||
///
|
|
||||||
/// // ```dart
|
|
||||||
/// isLink("~/fred.jpg");
|
|
||||||
/// ```
|
|
||||||
bool isLink(String path) => _Is().isLink(path);
|
|
||||||
|
|
||||||
/// Returns true if the given path exists.
|
|
||||||
/// It may be a file, directory or link.
|
|
||||||
///
|
|
||||||
/// If [followLinks] is true (the default) then [exists]
|
|
||||||
/// will return true if the resolved path exists.
|
|
||||||
///
|
|
||||||
/// If [followLinks] is false then [exists] will return
|
|
||||||
/// true if path exist, whether its a link or not.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// if (exists("/fred.txt"))
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Throws [ArgumentError] if [path] is null or an empty string.
|
|
||||||
///
|
|
||||||
/// See [isLink]
|
|
||||||
/// [isDirectory]
|
|
||||||
/// [isFile]
|
|
||||||
bool exists(String path, {bool followLinks = true}) =>
|
|
||||||
_Is().exists(path, followLinks: followLinks);
|
|
||||||
|
|
||||||
/// Returns the datetime the path was last modified
|
|
||||||
///
|
|
||||||
/// [path[ can be either a file or a directory.
|
|
||||||
///
|
|
||||||
/// Throws a [DCliException] with a nested
|
|
||||||
/// [FileSystemException] if the file does not
|
|
||||||
/// exist or the operation fails.
|
|
||||||
DateTime lastModified(String path) {
|
|
||||||
try {
|
|
||||||
return File(path).lastModifiedSync();
|
|
||||||
} on FileSystemException catch (e) {
|
|
||||||
throw DCliException.from(e, Trace.current());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the last modified datetime on the given the path.
|
|
||||||
///
|
|
||||||
/// [path] can be either a file or a directory.
|
|
||||||
///
|
|
||||||
/// Throws a [DCliException] with a nested
|
|
||||||
/// [FileSystemException] if the file does not
|
|
||||||
/// exist or the operation fails.
|
|
||||||
|
|
||||||
void setLastModifed(String path, DateTime lastModified) {
|
|
||||||
try {
|
|
||||||
File(path).setLastModifiedSync(lastModified);
|
|
||||||
} on FileSystemException catch (e) {
|
|
||||||
throw DCliException.from(e, Trace.current());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the passed [pathToDirectory] is an
|
|
||||||
/// empty directory.
|
|
||||||
/// For large directories this operation can be expensive.
|
|
||||||
bool isEmpty(String pathToDirectory) => _Is().isEmpty(pathToDirectory);
|
|
||||||
|
|
||||||
class _Is extends DCliFunction {
|
|
||||||
bool isFile(String path) {
|
|
||||||
final fromType = FileSystemEntity.typeSync(path);
|
|
||||||
return fromType == FileSystemEntityType.file;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// true if the given path is a directory.
|
|
||||||
bool isDirectory(String path) {
|
|
||||||
final fromType = FileSystemEntity.typeSync(path);
|
|
||||||
return fromType == FileSystemEntityType.directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isLink(String path) {
|
|
||||||
final fromType = FileSystemEntity.typeSync(path, followLinks: false);
|
|
||||||
return fromType == FileSystemEntityType.link;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// checks if the given [path] exists.
|
|
||||||
///
|
|
||||||
/// Throws [ArgumentError] if [path] is an empty string.
|
|
||||||
bool exists(String path, {bool followLinks = true}) {
|
|
||||||
if (path.isEmpty) {
|
|
||||||
throw ArgumentError('path must not be empty.');
|
|
||||||
}
|
|
||||||
|
|
||||||
final exists = FileSystemEntity.typeSync(path, followLinks: followLinks) !=
|
|
||||||
FileSystemEntityType.notFound;
|
|
||||||
|
|
||||||
verbose(
|
|
||||||
() =>
|
|
||||||
'exists(${truepath(path)}) found: $exists followLinks: $followLinks',
|
|
||||||
);
|
|
||||||
|
|
||||||
return exists;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// checks if the given [path] exists.
|
|
||||||
///
|
|
||||||
/// Throws [ArgumentError] if [path] is an empty string.
|
|
||||||
bool existsSync(String path, {required bool followLinks}) {
|
|
||||||
if (path.isEmpty) {
|
|
||||||
throw ArgumentError('path must not be empty.');
|
|
||||||
}
|
|
||||||
|
|
||||||
final exists = FileSystemEntity.typeSync(path, followLinks: followLinks) !=
|
|
||||||
FileSystemEntityType.notFound;
|
|
||||||
|
|
||||||
verbose(
|
|
||||||
() =>
|
|
||||||
'exists(${truepath(path)}) found: $exists followLinks: $followLinks',
|
|
||||||
);
|
|
||||||
|
|
||||||
return exists;
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime lastModified(String path) => File(path).lastModifiedSync();
|
|
||||||
|
|
||||||
void setLastModifed(String path, DateTime lastModified) {
|
|
||||||
File(path).setLastModifiedSync(lastModified);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the passed [pathToDirectory] is an
|
|
||||||
/// empty directory.
|
|
||||||
/// For large directories this operation can be expensive.
|
|
||||||
bool isEmpty(String pathToDirectory) {
|
|
||||||
final empty =
|
|
||||||
Directory(pathToDirectory).listSync(followLinks: false).isEmpty;
|
|
||||||
verbose(() => 'isEmpty(${truepath(pathToDirectory)}) : $empty');
|
|
||||||
|
|
||||||
return empty;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Moves the file [from] to the location [to].
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// createDir('/tmp/folder');
|
|
||||||
/// move('/tmp/fred.txt', '/tmp/folder/tom.txt');
|
|
||||||
/// ```
|
|
||||||
/// [from] must be a file.
|
|
||||||
///
|
|
||||||
/// [to] may be a file or a path.
|
|
||||||
///
|
|
||||||
/// If [to] is a file then a rename occurs.
|
|
||||||
///
|
|
||||||
/// if [to] is a path then [from] is moved to the given path.
|
|
||||||
///
|
|
||||||
/// If the move fails for any reason a [MoveException] is thrown.
|
|
||||||
///
|
|
||||||
|
|
||||||
void move(String from, String to, {bool overwrite = false}) {
|
|
||||||
verbose(
|
|
||||||
() => 'move ${truepath(from)} -> ${truepath(to)} overwrite: $overwrite');
|
|
||||||
|
|
||||||
var dest = to;
|
|
||||||
|
|
||||||
if (isDirectory(to)) {
|
|
||||||
dest = p.join(to, p.basename(from));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!overwrite && exists(dest)) {
|
|
||||||
throw MoveException(
|
|
||||||
'The [to] path ${truepath(dest)} already exists.'
|
|
||||||
' Use overwrite:true ',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
File(from).renameSync(dest);
|
|
||||||
} on FileSystemException catch (_) {
|
|
||||||
/// Invalid cross-device link
|
|
||||||
/// We can't move files across a partition so
|
|
||||||
/// do a copy/delete.
|
|
||||||
copy(from, to, overwrite: overwrite);
|
|
||||||
delete(from);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ignore: avoid_catches_without_on_clauses
|
|
||||||
catch (e) {
|
|
||||||
_improveError(e, from, to);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// We try to improve the OS error message, because its crap.
|
|
||||||
void _improveError(Object e, String from, String to) {
|
|
||||||
if (!exists(from)) {
|
|
||||||
throw MoveException(
|
|
||||||
'The Move of ${truepath(from)} failed as it does not exist.',
|
|
||||||
);
|
|
||||||
} else if (!exists(dirname(truepath(to)))) {
|
|
||||||
throw MoveException(
|
|
||||||
'The Move of ${truepath(from)} failed as the target directory '
|
|
||||||
'${truepath(dirname(to))} does not exist.',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw MoveException(
|
|
||||||
'The Move of ${truepath(from)} to ${truepath(to)} failed. Error $e',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thrown when the [move] function encouters an error.
|
|
||||||
class MoveException extends DCliFunctionException {
|
|
||||||
/// Thrown when the [move] function encouters an error.
|
|
||||||
MoveException(super.reason);
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
/// Moves or renames the [from] directory to the
|
|
||||||
/// to the [to] path.
|
|
||||||
///
|
|
||||||
/// The [to] path must NOT exist.
|
|
||||||
///
|
|
||||||
/// The [from] path must be a directory.
|
|
||||||
///
|
|
||||||
/// [moveDir] first tries to rename the directory, if that
|
|
||||||
/// fails due to the [to] path being on a different device
|
|
||||||
/// we fall back to a copy/delete operation.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// moveDir("/tmp/", "/tmp/new_dir");
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Throws a [MoveDirException] if:
|
|
||||||
/// the [from] path doesn't exist
|
|
||||||
/// the [from] path isn't a directory
|
|
||||||
/// the [to] path already exists.
|
|
||||||
///
|
|
||||||
void moveDir(String from, String to) => _MoveDir().moveDir(
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
);
|
|
||||||
|
|
||||||
class _MoveDir extends DCliFunction {
|
|
||||||
void moveDir(String from, String to) {
|
|
||||||
if (!exists(from)) {
|
|
||||||
throw MoveDirException(
|
|
||||||
'The [from] path ${truepath(from)} does not exists.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!isDirectory(from)) {
|
|
||||||
throw MoveDirException(
|
|
||||||
'The [from] path ${truepath(from)} must be a directory.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (exists(to)) {
|
|
||||||
throw MoveDirException('The [to] path ${truepath(to)} must NOT exist.');
|
|
||||||
}
|
|
||||||
|
|
||||||
verbose(() => 'moveDir called ${truepath(from)} -> ${truepath(to)}');
|
|
||||||
|
|
||||||
try {
|
|
||||||
Directory(from).renameSync(to);
|
|
||||||
} on FileSystemException catch (_) {
|
|
||||||
/// Most likley an Invalid cross-device move.
|
|
||||||
/// We can't move files across a partition so
|
|
||||||
/// do a copy/delete.
|
|
||||||
verbose(
|
|
||||||
() =>
|
|
||||||
'rename failed so falling back to copy/delete: ${truepath(from)} -> ${truepath(to)}',
|
|
||||||
);
|
|
||||||
|
|
||||||
copyTree(from, to, includeHidden: true);
|
|
||||||
delete(from);
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
catch (e) {
|
|
||||||
throw MoveDirException(
|
|
||||||
'The Move of ${truepath(from)} to ${truepath(to)} failed. Error $e',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thrown when the [moveDir] function encouters an error.
|
|
||||||
class MoveDirException extends DCliFunctionException {
|
|
||||||
/// Thrown when the [moveDir] function encouters an error.
|
|
||||||
MoveDirException(super.reason);
|
|
||||||
}
|
|
|
@ -1,164 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
/// Recursively moves the contents of the [from] directory to the
|
|
||||||
/// to the [to] path with an optional filter.
|
|
||||||
///
|
|
||||||
/// When filtering any files that don't match the filter will be
|
|
||||||
/// left in the [from] directory tree.
|
|
||||||
///
|
|
||||||
/// Any [from] directories that are emptied as a result of the move will
|
|
||||||
/// be removed. This includes the [from] directory itself.
|
|
||||||
///
|
|
||||||
/// [from] must be a directory
|
|
||||||
///
|
|
||||||
/// [to] must be a directory and its parent directory must exist.
|
|
||||||
///
|
|
||||||
/// If any moved files already exists in the [to] path then
|
|
||||||
/// an exeption is throw and a parital move may occured.
|
|
||||||
///
|
|
||||||
/// You can force moveTree to overwrite files in the [to]
|
|
||||||
/// directory by setting [overwrite] to true (defaults to false).
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// moveTree("/tmp/", "/tmp/new_dir", overwrite: true);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// By default hidden files are ignored. To allow hidden files to
|
|
||||||
/// be passed set [includeHidden] to true.
|
|
||||||
///
|
|
||||||
/// You can select which files/directories are to be moved by passing a [filter].
|
|
||||||
/// If a [filter] isn't passed then all files/directories are copied as per
|
|
||||||
/// the [includeHidden] state.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// moveTree("/tmp/", "/tmp/new_dir", overwrite: true
|
|
||||||
/// , filter: (file) => extension(file) == 'dart');
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// The [filter] method can also be used to report progress as it
|
|
||||||
/// is called just before we move a file or directory.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// moveTree("/tmp/", "/tmp/new_dir", overwrite: true
|
|
||||||
/// , filter: (entity) {
|
|
||||||
/// var include = extension(entity) == 'dart';
|
|
||||||
/// if (include) {
|
|
||||||
/// print('moving: $file');
|
|
||||||
/// }
|
|
||||||
/// return include;
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// The default for [overwrite] is false.
|
|
||||||
///
|
|
||||||
/// If an error occurs a [MoveTreeException] is thrown.
|
|
||||||
///
|
|
||||||
/// EXPERIMENTAL
|
|
||||||
void moveTree(
|
|
||||||
String from,
|
|
||||||
String to, {
|
|
||||||
bool overwrite = false,
|
|
||||||
bool includeHidden = false,
|
|
||||||
bool Function(String file) filter = _allowAll,
|
|
||||||
}) =>
|
|
||||||
_MoveTree().moveTree(
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
overwrite: overwrite,
|
|
||||||
includeHidden: includeHidden,
|
|
||||||
filter: filter,
|
|
||||||
);
|
|
||||||
|
|
||||||
bool _allowAll(String file) => true;
|
|
||||||
|
|
||||||
class _MoveTree extends DCliFunction {
|
|
||||||
void moveTree(
|
|
||||||
String from,
|
|
||||||
String to, {
|
|
||||||
bool overwrite = false,
|
|
||||||
bool Function(String file) filter = _allowAll,
|
|
||||||
bool includeHidden = false,
|
|
||||||
}) {
|
|
||||||
if (!isDirectory(from)) {
|
|
||||||
throw MoveTreeException(
|
|
||||||
'The [from] path ${truepath(from)} must be a directory.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!exists(to)) {
|
|
||||||
throw MoveTreeException(
|
|
||||||
'The [to] path ${truepath(to)} must already exist.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDirectory(to)) {
|
|
||||||
throw MoveTreeException(
|
|
||||||
'The [to] path ${truepath(to)} must be a directory.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
verbose(() => 'moveTree called ${truepath(from)} -> ${truepath(to)}');
|
|
||||||
|
|
||||||
try {
|
|
||||||
find('*', workingDirectory: from, includeHidden: includeHidden,
|
|
||||||
progress: (item) {
|
|
||||||
_process(item.pathTo, filter, to, from, overwrite: overwrite);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
catch (e) {
|
|
||||||
throw MoveTreeException(
|
|
||||||
'An error occured moving directory ${truepath(from)} '
|
|
||||||
'to ${truepath(to)}. Error: $e',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _process(String pathToFile, bool Function(String file) filter, String to,
|
|
||||||
String from,
|
|
||||||
{required bool overwrite}) {
|
|
||||||
if (filter(pathToFile)) {
|
|
||||||
final target = join(to, relative(pathToFile, from: from));
|
|
||||||
|
|
||||||
// we create directories as we go.
|
|
||||||
// only directories that contain a file that is to be
|
|
||||||
// moved will be created.
|
|
||||||
if (isDirectory(dirname(pathToFile))) {
|
|
||||||
if (!exists(dirname(target))) {
|
|
||||||
createDir(dirname(target), recursive: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!overwrite && exists(target)) {
|
|
||||||
throw MoveTreeException(
|
|
||||||
'The target file ${truepath(to)} already exists',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
move(pathToFile, target, overwrite: overwrite);
|
|
||||||
verbose(
|
|
||||||
() => 'moveTree moving: ${truepath(from)} -> ${truepath(target)}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thrown when the [moveTree] function encouters an error.
|
|
||||||
class MoveTreeException extends DCliFunctionException {
|
|
||||||
/// Thrown when the [moveTree] function encouters an error.
|
|
||||||
MoveTreeException(super.reason);
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'dcli_function.dart';
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Returns the current working directory.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// print(pwd);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// See join
|
|
||||||
///
|
|
||||||
String get pwd => _PWD().pwd;
|
|
||||||
|
|
||||||
class _PWD extends DCliFunction {
|
|
||||||
String get pwd => Directory.current.path;
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Does an insitu replacement on the file located at [path].
|
|
||||||
///
|
|
||||||
/// [replace] searches the file at [path] for any occurances
|
|
||||||
/// of [existing] and replaces them with [replacement].
|
|
||||||
///
|
|
||||||
/// By default we only replace the first occurance of [existing] on each line.
|
|
||||||
/// To replace every (non-overlapping) occurance of [existing] on a
|
|
||||||
/// line then set [all] to true;
|
|
||||||
///
|
|
||||||
/// The [replace] method returns the no. of lines modified.
|
|
||||||
///
|
|
||||||
/// The [existing] argument can be a simple String which or a regex.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// replace(pathToFile, 'change me', 'changed');
|
|
||||||
/// replace(pathToFile, RegExp(r'change \w+'), 'changed');
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// During the process a temporary file called [path].tmp is created
|
|
||||||
/// in the directory of [path].
|
|
||||||
/// The modified file is written to [path].tmp.
|
|
||||||
/// Once the replacement completes successfully the file at [path]
|
|
||||||
/// is renamed to [path].bak, [path].tmp is renamed to [path] and then
|
|
||||||
/// [path].bak is deleted.
|
|
||||||
///
|
|
||||||
/// The above process essentially makes replace atomic so it should
|
|
||||||
/// be impossible to loose your file. If replace does crash you may
|
|
||||||
/// have to delete [path].tmp or [path].bak but this is highly unlikely.
|
|
||||||
///
|
|
||||||
int replace(
|
|
||||||
String path,
|
|
||||||
Pattern existing,
|
|
||||||
String replacement, {
|
|
||||||
bool all = false,
|
|
||||||
}) =>
|
|
||||||
_Replace().replace(path, existing, replacement, all: all);
|
|
||||||
|
|
||||||
class _Replace extends DCliFunction {
|
|
||||||
int replace(
|
|
||||||
String path,
|
|
||||||
Pattern existing,
|
|
||||||
String replacement, {
|
|
||||||
bool all = false,
|
|
||||||
}) {
|
|
||||||
var changes = 0;
|
|
||||||
final tmp = '$path.tmp';
|
|
||||||
if (exists(tmp)) {
|
|
||||||
delete(tmp);
|
|
||||||
}
|
|
||||||
touch(tmp, create: true);
|
|
||||||
withOpenLineFile(tmp, (tmpFile) {
|
|
||||||
withOpenLineFile(path, (file) {
|
|
||||||
file.readAll((line) {
|
|
||||||
String newline;
|
|
||||||
if (all) {
|
|
||||||
newline = line.replaceAll(existing, replacement);
|
|
||||||
} else {
|
|
||||||
newline = line.replaceFirst(existing, replacement);
|
|
||||||
}
|
|
||||||
if (newline != line) {
|
|
||||||
changes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpFile.append(newline);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (changes != 0) {
|
|
||||||
move(path, '$path.bak');
|
|
||||||
move(tmp, path);
|
|
||||||
delete('$path.bak');
|
|
||||||
} else {
|
|
||||||
delete(tmp);
|
|
||||||
}
|
|
||||||
return changes;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'package:circular_buffer/circular_buffer.dart';
|
|
||||||
|
|
||||||
import '../settings.dart';
|
|
||||||
import '../utils/line_file.dart';
|
|
||||||
import '../utils/truepath.dart';
|
|
||||||
import 'dcli_function.dart';
|
|
||||||
import 'is.dart';
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Returns count [lines] from the end of the file at [path].
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// tail('/var/log/syslog', 10).forEach((line) => print(line));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Throws a [TailException] exception if [path] is not a file.
|
|
||||||
///
|
|
||||||
List<String> tail(String path, int lines) => _Tail().tail(path, lines);
|
|
||||||
|
|
||||||
class _Tail extends DCliFunction {
|
|
||||||
List<String> tail(
|
|
||||||
String path,
|
|
||||||
int lines,
|
|
||||||
) {
|
|
||||||
verbose(() => 'tail ${truepath(path)} lines: $lines');
|
|
||||||
|
|
||||||
if (lines < 1) {
|
|
||||||
throw TailException('lines must be >= 1');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!exists(path)) {
|
|
||||||
throw TailException('The path ${truepath(path)} does not exist.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isFile(path)) {
|
|
||||||
throw TailException('The path ${truepath(path)} is not a file.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// circbuffer requires a min size of 2 so we
|
|
||||||
/// add one to make certain it is always greater than one
|
|
||||||
/// and then adjust later.
|
|
||||||
final buffer = CircularBuffer<String>(lines + 1);
|
|
||||||
try {
|
|
||||||
withOpenLineFile(path, (file) {
|
|
||||||
file.readAll((line) {
|
|
||||||
buffer.add(line);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
catch (e) {
|
|
||||||
throw TailException(
|
|
||||||
'An error occured reading ${truepath(path)}. Error: $e',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final lastLines = buffer.toList();
|
|
||||||
|
|
||||||
/// adjust the buffer by stripping extra line.
|
|
||||||
if (buffer.isFilled) {
|
|
||||||
lastLines.removeAt(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return lastLines;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// thrown when the [tail] function encounters an exception
|
|
||||||
class TailException extends DCliFunctionException {
|
|
||||||
/// thrown when the [tail] function encounters an exception
|
|
||||||
TailException(super.reason);
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
/// Updates the last modified time stamp of a file.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// touch('fred.txt');
|
|
||||||
/// touch('fred.txt, create: true');
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// If [create] is true and the file doesn't exist
|
|
||||||
/// it will be created.
|
|
||||||
///
|
|
||||||
/// If [create] is false and the file doesn't exist
|
|
||||||
/// a [TouchException] will be thrown.
|
|
||||||
///
|
|
||||||
/// [create] is false by default.
|
|
||||||
///
|
|
||||||
/// As a convenience the touch function returns the [path] variable
|
|
||||||
/// that was passed in.
|
|
||||||
|
|
||||||
String touch(String path, {bool create = false}) {
|
|
||||||
final absolutePath = truepath(path);
|
|
||||||
|
|
||||||
verbose(() => 'touch: $absolutePath create: $create');
|
|
||||||
|
|
||||||
if (!exists(p.dirname(absolutePath))) {
|
|
||||||
throw TouchException(
|
|
||||||
'The directory tree above $absolutePath does not exist. '
|
|
||||||
'Create the tree and try again.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (create == false && !exists(absolutePath)) {
|
|
||||||
throw TouchException(
|
|
||||||
'The file $absolutePath does not exist. '
|
|
||||||
'Did you mean to use touch(path, create: true) ?',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final file = File(absolutePath);
|
|
||||||
|
|
||||||
if (file.existsSync()) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
file
|
|
||||||
..setLastAccessedSync(now)
|
|
||||||
..setLastModifiedSync(now);
|
|
||||||
} else {
|
|
||||||
if (create) {
|
|
||||||
file.createSync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} on FileSystemException catch (e) {
|
|
||||||
throw TouchException('Unable to touch file $absolutePath: ${e.message}');
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// thrown when the [touch] function encounters an exception
|
|
||||||
class TouchException extends DCliFunctionException {
|
|
||||||
/// thrown when the [touch] function encounters an exception
|
|
||||||
TouchException(super.reason);
|
|
||||||
}
|
|
|
@ -1,196 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Searches the PATH for the location of the application
|
|
||||||
/// give by [appname].
|
|
||||||
///
|
|
||||||
/// The search is conducted by searching each of the
|
|
||||||
/// paths in the environment variable 'PATH' from
|
|
||||||
/// left to right (start to end) as this is the
|
|
||||||
/// same order the OS searches the path.
|
|
||||||
///
|
|
||||||
/// If the [verbose] flag is true then a line is output to
|
|
||||||
/// the [progress] for each path searched.
|
|
||||||
///
|
|
||||||
/// It is possible that more than one copy of the
|
|
||||||
/// appliation is found.
|
|
||||||
///
|
|
||||||
/// [which] returns a list of paths that contain
|
|
||||||
/// [appname] in the order they were found.
|
|
||||||
///
|
|
||||||
/// The first path in the list is the one the OS
|
|
||||||
/// will be using.
|
|
||||||
///
|
|
||||||
/// if the [first] flag is true then which will
|
|
||||||
/// stop searching as soon as it finds a match.
|
|
||||||
/// [first] is true by default.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// which('ls', first: false, verbose: true);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// To print the path to the command:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// print(which('ls').path);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// To check if an app is on the path use:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// if (which('apt').found)
|
|
||||||
/// {
|
|
||||||
/// print('found apt');
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// if [extensionSearch] is true and the passed [appname] doesn't have a file
|
|
||||||
/// extension then when running on Windows the which command will search
|
|
||||||
/// for [appname] plus [appname] with each of the extensions listed
|
|
||||||
/// in the Windows environment variable PATHEX.
|
|
||||||
/// This feature is intended to make it easier to implement cross platform
|
|
||||||
/// command search. For example the dart will be 'dart'
|
|
||||||
/// on Linux and 'dart.bat' on Windows. Using `which('dart')` will find `dart`
|
|
||||||
/// on linux and `dart.bat` on Windows.
|
|
||||||
Which which(
|
|
||||||
String appname, {
|
|
||||||
bool first = true,
|
|
||||||
bool verbose = false,
|
|
||||||
bool extensionSearch = true,
|
|
||||||
void Function(WhichSearch)? progress,
|
|
||||||
}) =>
|
|
||||||
_Which().which(
|
|
||||||
appname,
|
|
||||||
first: first,
|
|
||||||
verbose: verbose,
|
|
||||||
extensionSearch: extensionSearch,
|
|
||||||
progress: progress,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Returned from the [which] funtion to provide the details we discovered
|
|
||||||
/// about appname.
|
|
||||||
class Which {
|
|
||||||
String? _path;
|
|
||||||
final _paths = <String>[];
|
|
||||||
bool _found = false;
|
|
||||||
|
|
||||||
/// The progress used to accumualte the results
|
|
||||||
/// If verbose was passed this will contain all
|
|
||||||
/// of the verbose output. If you passed a [progress]
|
|
||||||
/// into the which call then this will be the same progress
|
|
||||||
/// otherwse a Progress.devNull will be allocated and returned.
|
|
||||||
Stream<String>? progress;
|
|
||||||
|
|
||||||
/// The first path found containing appname
|
|
||||||
///
|
|
||||||
/// See [paths] for a list of all paths that contained appname
|
|
||||||
String? get path => _path;
|
|
||||||
|
|
||||||
/// Contains the list of paths that contain appname.
|
|
||||||
///
|
|
||||||
/// If no paths are found then this list will be empty.
|
|
||||||
///
|
|
||||||
/// If first is true this will contain at most 1 path.
|
|
||||||
List<String> get paths => _paths;
|
|
||||||
|
|
||||||
/// Returns true if at least one path was found that contained appname
|
|
||||||
bool get found => _found;
|
|
||||||
|
|
||||||
/// Returns true if appname was not found in any path.
|
|
||||||
bool get notfound => !_found;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Search resutls from the [which] method.
|
|
||||||
class WhichSearch {
|
|
||||||
/// the app was found on the path.
|
|
||||||
WhichSearch.found(this.path, this.exePath) : found = true;
|
|
||||||
|
|
||||||
/// the app was not found.
|
|
||||||
WhichSearch.notfound(this.path) : found = false;
|
|
||||||
|
|
||||||
/// passed in path to search for.
|
|
||||||
String path;
|
|
||||||
|
|
||||||
/// true if the app was found
|
|
||||||
bool found;
|
|
||||||
|
|
||||||
/// If the app was found this is the fully qualified path to the app.
|
|
||||||
String? exePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Which extends DCliFunction {
|
|
||||||
///
|
|
||||||
/// Searches the path for the given appname.
|
|
||||||
Which which(
|
|
||||||
String appname, {
|
|
||||||
required bool extensionSearch,
|
|
||||||
bool first = true,
|
|
||||||
bool verbose = false,
|
|
||||||
void Function(WhichSearch)? progress,
|
|
||||||
}) {
|
|
||||||
final results = Which();
|
|
||||||
for (final path in PATH) {
|
|
||||||
final fullpath =
|
|
||||||
_appExists(path, appname, extensionSearch: extensionSearch);
|
|
||||||
if (fullpath == null) {
|
|
||||||
progress?.call(WhichSearch.notfound(path));
|
|
||||||
} else {
|
|
||||||
progress?.call(WhichSearch.found(path, fullpath));
|
|
||||||
|
|
||||||
if (!results._found) {
|
|
||||||
results._path = fullpath;
|
|
||||||
}
|
|
||||||
results.paths.add(fullpath);
|
|
||||||
results._found = true;
|
|
||||||
if (first) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if [appname] exists in [pathTo].
|
|
||||||
///
|
|
||||||
/// On Windows if [extensionSearch] is true and [appname] doesn't
|
|
||||||
/// have an extension then we check each appname.extension variant
|
|
||||||
/// to see if it exists. We first check if just an file of [appname] with
|
|
||||||
/// no extension exits.
|
|
||||||
String? _appExists(
|
|
||||||
String pathTo,
|
|
||||||
String appname, {
|
|
||||||
required bool extensionSearch,
|
|
||||||
}) {
|
|
||||||
final pathToAppname = join(pathTo, appname);
|
|
||||||
if (exists(pathToAppname)) {
|
|
||||||
return pathToAppname;
|
|
||||||
}
|
|
||||||
if (Settings().isWindows && extensionSearch && extension(appname).isEmpty) {
|
|
||||||
final pathExt = env['PATHEXT'];
|
|
||||||
|
|
||||||
if (pathExt != null) {
|
|
||||||
final extensions = pathExt.split(';');
|
|
||||||
for (final extension in extensions) {
|
|
||||||
final fullname = '$pathToAppname$extension';
|
|
||||||
if (exists(fullname)) {
|
|
||||||
return fullname;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,848 +0,0 @@
|
||||||
/* Copyright (C) S. Brett Sutton - All Rights Reserved
|
|
||||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
|
||||||
* Proprietary and confidential
|
|
||||||
* Written by Brett Sutton <bsutton@onepub.dev>, Jan 2022
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:protevus_console/core.dart' as core;
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
import 'package:protevus_console/terminal.dart';
|
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
import 'package:validators2/validators2.dart';
|
|
||||||
|
|
||||||
import 'echo.dart';
|
|
||||||
|
|
||||||
typedef CustomAskPrompt = String Function(
|
|
||||||
String prompt,
|
|
||||||
String? defaultValue,
|
|
||||||
// ignore: avoid_positional_boolean_parameters
|
|
||||||
bool hidden);
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Reads a line of text from stdin with an optional prompt.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// String response = ask("Do you like me?");
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If the script is not attached to terminal [Terminal().hasTerminal]
|
|
||||||
/// then ask returns immediatly with the [defaultValue]. If no [defaultValue]
|
|
||||||
/// is passed then an empty string is returned. No validate will be applied.
|
|
||||||
///
|
|
||||||
/// If [prompt] is set then the prompt will be printed
|
|
||||||
/// to the console and the cursor placed immediately after the prompt.
|
|
||||||
///
|
|
||||||
/// Pass an empty string to suppress the prompt.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// var secret = ask('', required: false);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// By default the ask [required] argument is true requiring the user to enter
|
|
||||||
/// a non-empty string.
|
|
||||||
/// All whitespace is trimmed from the string before the user input
|
|
||||||
/// is validated so
|
|
||||||
/// a single space is not an accepted input.
|
|
||||||
///
|
|
||||||
/// If you set the [required] argument to false then the user can just hit
|
|
||||||
/// enter to skip past the ask prompt. If you use other validators when
|
|
||||||
/// [required] = false
|
|
||||||
/// then those validators will not be called if the entered value is empty
|
|
||||||
/// (after it is trimmed).
|
|
||||||
///
|
|
||||||
/// if [toLower] is true then the returned result is converted to lower case.
|
|
||||||
/// This can be useful if you need to compare the entered value.
|
|
||||||
///
|
|
||||||
/// If [hidden] is true then the entered values will not be echoed to the
|
|
||||||
/// console, instead '*' will be displayed. This is uesful for capturing
|
|
||||||
/// passwords.
|
|
||||||
///
|
|
||||||
/// NOTE: if there is no terminal detected then this will fallback to
|
|
||||||
/// a standard ask input in which case the hidden characters WILL BE DISPLAYED
|
|
||||||
/// as they are typed.
|
|
||||||
///
|
|
||||||
/// If a [defaultValue] is passed then it is displayed and the user
|
|
||||||
/// fails to enter a value (just hits the enter key) then the
|
|
||||||
/// [defaultValue] is returned.
|
|
||||||
///
|
|
||||||
/// Passing a [defaultValue] also modifies the prompt to display the value:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// var result = ask('How many', defaultValue: '5');
|
|
||||||
/// > 'How many [5]'
|
|
||||||
/// ```
|
|
||||||
/// [ask] will throw an [AskValidatorException] if the defaultValue
|
|
||||||
/// doesn't match the given [validator].
|
|
||||||
///
|
|
||||||
/// The [validator] is called each time the user hits enter.
|
|
||||||
///
|
|
||||||
/// The [validator] allows you to normalise and validate the user's
|
|
||||||
/// input. The [validator] must return the normalised value which
|
|
||||||
/// will be the value returned by [ask].
|
|
||||||
///
|
|
||||||
/// If the [validator] detects an invalid input then you MUST
|
|
||||||
/// throw [AskValidatorException(error)]. The error will
|
|
||||||
/// be displayed on the console and the user reprompted.
|
|
||||||
/// You can color code the error using any of the dcli
|
|
||||||
/// color functions. By default all input is considered valid.
|
|
||||||
///
|
|
||||||
/// The [customErrorMessage] allows you to provide a custom error message
|
|
||||||
/// to be displayed instead of the default one when the input is invalid.
|
|
||||||
/// This can be useful if you want to provide specific guidance or instructions
|
|
||||||
/// to the user regarding the expected input format or constraints.
|
|
||||||
///
|
|
||||||
///```dart
|
|
||||||
/// var subject = ask( 'Subject');
|
|
||||||
/// subject = ask( 'Subject', required: true);
|
|
||||||
/// subject = ask( 'Subject', validator: Ask.minLength(10));
|
|
||||||
/// var name = ask( 'What is your name?', validator: Ask.alpha);
|
|
||||||
/// var age = ask( 'How old are you?', validator: Ask.integer);
|
|
||||||
/// var username = ask( 'Username?', validator: Ask.email);
|
|
||||||
/// var password = ask( 'Password?', hidden: true,
|
|
||||||
/// validator: Ask.all([Ask.alphaNumeric, AskValidatorLength(10,16)]));
|
|
||||||
/// var color = ask( 'Favourite colour?'
|
|
||||||
/// , Ask.inList(['red', 'green', 'blue']));
|
|
||||||
///
|
|
||||||
///```
|
|
||||||
Future<String> ask(
|
|
||||||
String prompt, {
|
|
||||||
bool toLower = false,
|
|
||||||
bool hidden = false,
|
|
||||||
bool required = true,
|
|
||||||
String? defaultValue,
|
|
||||||
CustomAskPrompt customPrompt = Ask.defaultPrompt,
|
|
||||||
AskValidator validator = Ask.dontCare,
|
|
||||||
String? customErrorMessage,
|
|
||||||
}) async =>
|
|
||||||
Ask()._ask(
|
|
||||||
prompt,
|
|
||||||
toLower: toLower,
|
|
||||||
hidden: hidden,
|
|
||||||
required: required,
|
|
||||||
defaultValue: defaultValue,
|
|
||||||
customPrompt: customPrompt,
|
|
||||||
validator: validator,
|
|
||||||
customErrorMessage: customErrorMessage,
|
|
||||||
);
|
|
||||||
|
|
||||||
// ignore: avoid_clas
|
|
||||||
/// Class for [ask] and related code.
|
|
||||||
class Ask extends core.DCliFunction {
|
|
||||||
static const int _backspace = 127;
|
|
||||||
static const int _space = 32;
|
|
||||||
static const int _ = 8;
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Reads user input from stdin and returns it as a string.
|
|
||||||
/// [prompt]
|
|
||||||
Future<String> _ask(
|
|
||||||
String prompt, {
|
|
||||||
required bool hidden,
|
|
||||||
required bool required,
|
|
||||||
required AskValidator validator,
|
|
||||||
required CustomAskPrompt customPrompt,
|
|
||||||
bool toLower = false,
|
|
||||||
String? defaultValue,
|
|
||||||
String? customErrorMessage,
|
|
||||||
}) async {
|
|
||||||
ArgumentError.checkNotNull(prompt);
|
|
||||||
core.verbose(
|
|
||||||
() => 'ask: $prompt toLower: $toLower hidden: $hidden '
|
|
||||||
'required: $required '
|
|
||||||
'defaultValue: ${hidden ? '******' : defaultValue}',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!Terminal().hasTerminal) {
|
|
||||||
return defaultValue ?? '';
|
|
||||||
}
|
|
||||||
final finalPrompt = customPrompt(prompt, defaultValue, hidden);
|
|
||||||
|
|
||||||
var line = '';
|
|
||||||
var valid = false;
|
|
||||||
do {
|
|
||||||
await echo('$finalPrompt ');
|
|
||||||
|
|
||||||
if (hidden == true && stdin.hasTerminal) {
|
|
||||||
line = await _readHidden();
|
|
||||||
} else {
|
|
||||||
line = stdin.readLineSync(encoding: Encoding.getByName('utf-8')!) ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.isEmpty && defaultValue != null) {
|
|
||||||
line = defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toLower == true) {
|
|
||||||
line = line.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (required) {
|
|
||||||
await const _AskRequired()
|
|
||||||
.validate(line, customErrorMessage: customErrorMessage);
|
|
||||||
}
|
|
||||||
verbose(() => 'ask: pre validation "$line"');
|
|
||||||
line = await validator.validate(line,
|
|
||||||
customErrorMessage: customErrorMessage);
|
|
||||||
verbose(() => 'ask: post validation "$line"');
|
|
||||||
valid = true;
|
|
||||||
} on AskValidatorException catch (e) {
|
|
||||||
print(e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
verbose(() => 'ask: result $line');
|
|
||||||
} while (!valid);
|
|
||||||
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> _readHidden() async {
|
|
||||||
final value = <int>[];
|
|
||||||
|
|
||||||
try {
|
|
||||||
stdin.echoMode = false;
|
|
||||||
stdin.lineMode = false;
|
|
||||||
int char;
|
|
||||||
do {
|
|
||||||
char = stdin.readByteSync();
|
|
||||||
if (char != 10 && char != 13) {
|
|
||||||
if (char == _backspace) {
|
|
||||||
if (value.isNotEmpty) {
|
|
||||||
// move back a character,
|
|
||||||
// print a space an move back again.
|
|
||||||
// required to clear the current character
|
|
||||||
// move back one space.
|
|
||||||
stdout
|
|
||||||
..writeCharCode(_)
|
|
||||||
..writeCharCode(_space)
|
|
||||||
..writeCharCode(_);
|
|
||||||
value.removeLast();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// apparently flush isn't need - despite the doc.
|
|
||||||
stdout.write('*');
|
|
||||||
value.add(char);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (char != 10 && char != 13);
|
|
||||||
} finally {
|
|
||||||
stdin.lineMode = true;
|
|
||||||
stdin.echoMode = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// output a newline as we have suppressed it.
|
|
||||||
print('');
|
|
||||||
|
|
||||||
// return the entered value as a String.
|
|
||||||
return Encoding.getByName('utf-8')!.decode(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String defaultPrompt(
|
|
||||||
String prompt,
|
|
||||||
String? defaultValue,
|
|
||||||
// ignore: avoid_positional_boolean_parameters
|
|
||||||
bool hidden) {
|
|
||||||
var result = prompt;
|
|
||||||
|
|
||||||
/// completely suppress the default value and the prompt if
|
|
||||||
/// the prompt is empty.
|
|
||||||
if (defaultValue != null && prompt.isNotEmpty) {
|
|
||||||
/// don't display the default value if hidden is true.
|
|
||||||
result = '$prompt [${hidden ? '******' : defaultValue}]';
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The default validator that considers any input as valid
|
|
||||||
static const AskValidator dontCare = _AskDontCare();
|
|
||||||
|
|
||||||
/// Takes an array of validators. The input is considered valid if any one
|
|
||||||
/// of the validators returns true.
|
|
||||||
/// The validators are processed in order from left to right.
|
|
||||||
/// If none of the validators pass then the error from the first validator
|
|
||||||
/// that failed is returned. The implications is that the user will only
|
|
||||||
/// ever see the error from the first validator.
|
|
||||||
static AskValidator any(List<AskValidator> validators) =>
|
|
||||||
_AskValidatorAny(validators);
|
|
||||||
|
|
||||||
/// Takes an array of validators. The input is considered valid only if
|
|
||||||
/// everyone of the validators pass.
|
|
||||||
///
|
|
||||||
/// The validators are processed in order from left to right.
|
|
||||||
///
|
|
||||||
/// The error from the first validator that failes is returned.
|
|
||||||
///
|
|
||||||
/// It should be noted that the user input is passed to each validator in turn
|
|
||||||
/// and each validator has the opportunity to modify the input. As a result
|
|
||||||
/// a validators will be operating on a version of the input
|
|
||||||
/// that has been processed by all validators that appear
|
|
||||||
/// earlier in the list.
|
|
||||||
static AskValidator all(List<AskValidator> validators) =>
|
|
||||||
_AskValidatorAll(validators);
|
|
||||||
|
|
||||||
/// Validates that input is a IP address
|
|
||||||
/// By default both v4 and v6 addresses are valid
|
|
||||||
/// Pass a [version] to limit the input to one or the
|
|
||||||
/// other. If passed [version] must be [AskValidatorIPAddress.ipv4]
|
|
||||||
/// or [AskValidatorIPAddress.ipv6].
|
|
||||||
static AskValidator ipAddress({int version = AskValidatorIPAddress.either}) =>
|
|
||||||
AskValidatorIPAddress(version: version);
|
|
||||||
|
|
||||||
/// Validates the input against a regular expression
|
|
||||||
/// ```dart
|
|
||||||
/// ask('Variable Name:', validator: Ask.regExp(r'^[a-zA-Z_]+$'));
|
|
||||||
/// ```
|
|
||||||
static AskValidator regExp(String regExp, {String? error}) =>
|
|
||||||
_AskRegExp(regExp, error: error);
|
|
||||||
|
|
||||||
/// Validates that the entered line is no longer
|
|
||||||
/// than [maxLength].
|
|
||||||
static AskValidator lengthMax(int maxLength) =>
|
|
||||||
_AskValidatorMaxLength(maxLength);
|
|
||||||
|
|
||||||
/// Validates that the entered line is not less
|
|
||||||
/// than [minLength].
|
|
||||||
static AskValidator lengthMin(int minLength) =>
|
|
||||||
_AskValidatorMinLength(minLength);
|
|
||||||
|
|
||||||
/// Validates that the length of the entered text
|
|
||||||
/// as at least [minLength] but no more than [maxLength].
|
|
||||||
static AskValidator lengthRange(int minLength, int maxLength) =>
|
|
||||||
_AskValidatorLength(minLength, maxLength);
|
|
||||||
|
|
||||||
/// Validates that a number is between a minimum value (inclusive)
|
|
||||||
/// and a maximum value (inclusive).
|
|
||||||
static AskValidator valueRange(num minValue, num maxValue) =>
|
|
||||||
_AskValidatorValueRange(minValue, maxValue);
|
|
||||||
|
|
||||||
/// Checks that the input matches one of the
|
|
||||||
/// provided [validItems].
|
|
||||||
/// If the validator fails it prints out the
|
|
||||||
/// list of available inputs.
|
|
||||||
/// By default [caseSensitive] matches are off.
|
|
||||||
static AskValidator inList(
|
|
||||||
List<Object> validItems, {
|
|
||||||
bool caseSensitive = false,
|
|
||||||
}) =>
|
|
||||||
_AskValidatorList(validItems, caseSensitive: caseSensitive);
|
|
||||||
|
|
||||||
/// The user must enter a non-empty string.
|
|
||||||
/// Whitespace will be trimmed before the string is tested.
|
|
||||||
static const AskValidator required = _AskRequired();
|
|
||||||
|
|
||||||
/// validates that the input is an email address
|
|
||||||
static const AskValidator email = _AskEmail();
|
|
||||||
|
|
||||||
/// validates that the input is a fully qualified domian name.
|
|
||||||
static const AskValidator fqdn = _AskFQDN();
|
|
||||||
|
|
||||||
/// Validates that the input is a valid url.
|
|
||||||
/// You may pass in a list of acceptable protocols.
|
|
||||||
/// By default only 'https' is allowed.
|
|
||||||
static AskValidator url({List<String> protocols = const ['https']}) =>
|
|
||||||
_AskURL(protocols: protocols);
|
|
||||||
|
|
||||||
/// validates that the input is a date.
|
|
||||||
static const AskValidator date = _AskDate();
|
|
||||||
|
|
||||||
/// validates that the input is an integer
|
|
||||||
static const AskValidator integer = _AskInteger();
|
|
||||||
|
|
||||||
/// validates that the input is a decimal
|
|
||||||
static const AskValidator decimal = _AskDecimal();
|
|
||||||
|
|
||||||
/// validates that the input is only alpha characters
|
|
||||||
static const AskValidator alpha = _AskAlpha();
|
|
||||||
|
|
||||||
/// validates that the input is only alphanumeric characters.
|
|
||||||
static const AskValidator alphaNumeric = _AskAlphaNumeric();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thrown when an [AskValidator] detects an invalid input.
|
|
||||||
class AskValidatorException extends DCliException {
|
|
||||||
/// validator with a [message] indicating the error.
|
|
||||||
AskValidatorException(super.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Base class for all [AskValidator]s.
|
|
||||||
/// You can add your own by extending this class.
|
|
||||||
// ignore: one_member_abstracts
|
|
||||||
abstract class AskValidator {
|
|
||||||
/// allows us to make validators consts.
|
|
||||||
const AskValidator();
|
|
||||||
|
|
||||||
/// This method is called by [ask] to valiate the
|
|
||||||
/// string entered by the user.
|
|
||||||
/// It should throw an AskValidatorException if the input
|
|
||||||
/// is invalid.
|
|
||||||
/// The validate method is called when the user hits the enter key.
|
|
||||||
/// If the validation succeeds the validated line is returned.
|
|
||||||
@visibleForTesting
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The default validator that considers any input as valid
|
|
||||||
class _AskDontCare extends AskValidator {
|
|
||||||
const _AskDontCare();
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async =>
|
|
||||||
line;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The user must enter a non-empty string.
|
|
||||||
/// Whitespace will be trimmed before the string is tested.
|
|
||||||
///
|
|
||||||
class _AskRequired extends AskValidator {
|
|
||||||
const _AskRequired();
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalLine = line.trim();
|
|
||||||
if (finalLine.isEmpty) {
|
|
||||||
throw AskValidatorException(
|
|
||||||
red(customErrorMessage ?? 'You must enter a value.'));
|
|
||||||
}
|
|
||||||
return finalLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AskEmail extends AskValidator {
|
|
||||||
const _AskEmail();
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalLine = line.trim();
|
|
||||||
|
|
||||||
if (!isEmail(finalLine)) {
|
|
||||||
throw AskValidatorException(
|
|
||||||
red(customErrorMessage ?? 'Invalid email address.'));
|
|
||||||
}
|
|
||||||
return finalLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AskFQDN extends AskValidator {
|
|
||||||
const _AskFQDN();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalLine = line.trim().toLowerCase();
|
|
||||||
|
|
||||||
if (!isFQDN(finalLine)) {
|
|
||||||
throw AskValidatorException(red(customErrorMessage ?? 'Invalid FQDN.'));
|
|
||||||
}
|
|
||||||
return finalLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AskURL extends AskValidator {
|
|
||||||
const _AskURL({this.protocols = const ['https']});
|
|
||||||
|
|
||||||
final List<String> protocols;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalLine = line.trim().toLowerCase();
|
|
||||||
|
|
||||||
if (!isURL(finalLine, protocols: protocols)) {
|
|
||||||
throw AskValidatorException(red(customErrorMessage ?? 'Invalid URL.'));
|
|
||||||
}
|
|
||||||
return finalLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validates the input against a regular expression
|
|
||||||
/// ```dart
|
|
||||||
/// ask('Variable Name:', validator: Ask.regExp(r'^[a-zA-Z_]+$'));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
class _AskRegExp extends AskValidator {
|
|
||||||
/// Creates a regular expression based validator.
|
|
||||||
/// You can customise the error message by providing a value for [error].
|
|
||||||
_AskRegExp(this.regexp, {String? error}) {
|
|
||||||
_regexp = RegExp(regexp);
|
|
||||||
_error = error ?? 'Input does not match: $regexp';
|
|
||||||
}
|
|
||||||
|
|
||||||
final String regexp;
|
|
||||||
late final RegExp _regexp;
|
|
||||||
late final String _error;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalLine = line.trim();
|
|
||||||
|
|
||||||
if (!_regexp.hasMatch(finalLine)) {
|
|
||||||
throw AskValidatorException(red(customErrorMessage ?? _error));
|
|
||||||
}
|
|
||||||
return finalLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AskDate extends AskValidator {
|
|
||||||
const _AskDate();
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalLine = line.trim();
|
|
||||||
|
|
||||||
if (!isDate(finalLine)) {
|
|
||||||
throw AskValidatorException(red(customErrorMessage ?? 'Invalid date.'));
|
|
||||||
}
|
|
||||||
return finalLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AskInteger extends AskValidator {
|
|
||||||
const _AskInteger();
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalLine = line.trim();
|
|
||||||
verbose(() => 'AskInteger: $finalLine');
|
|
||||||
|
|
||||||
if (!isInt(finalLine)) {
|
|
||||||
throw AskValidatorException(
|
|
||||||
red(customErrorMessage ?? 'Invalid integer.'));
|
|
||||||
}
|
|
||||||
return finalLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AskDecimal extends AskValidator {
|
|
||||||
const _AskDecimal();
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalline = line.trim();
|
|
||||||
|
|
||||||
if (!isFloat(finalline)) {
|
|
||||||
throw AskValidatorException(
|
|
||||||
red(customErrorMessage ?? 'Invalid decimal number.'));
|
|
||||||
}
|
|
||||||
return finalline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AskAlpha extends AskValidator {
|
|
||||||
const _AskAlpha();
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalline = line.trim();
|
|
||||||
|
|
||||||
if (!isAlpha(finalline)) {
|
|
||||||
throw AskValidatorException(
|
|
||||||
red(customErrorMessage ?? 'Alphabetical characters only.'));
|
|
||||||
}
|
|
||||||
return finalline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AskAlphaNumeric extends AskValidator {
|
|
||||||
const _AskAlphaNumeric();
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalline = line.trim();
|
|
||||||
|
|
||||||
if (!isAlphanumeric(finalline)) {
|
|
||||||
throw AskValidatorException(
|
|
||||||
red(customErrorMessage ?? 'Alphanumerical characters only.'));
|
|
||||||
}
|
|
||||||
return finalline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validates that input is a IP address
|
|
||||||
/// By default both v4 and v6 addresses are valid
|
|
||||||
/// Pass a [version] to limit the input to one or the
|
|
||||||
/// other. If passed [version] must be [ipv4] or [ipv6].
|
|
||||||
class AskValidatorIPAddress extends AskValidator {
|
|
||||||
/// Validates that input is a IP address
|
|
||||||
/// By default both v4 and v6 addresses are valid
|
|
||||||
/// Pass a [version] to limit the input to one or the
|
|
||||||
/// other. If passed [version] must be 4 or 6.
|
|
||||||
const AskValidatorIPAddress({this.version = either});
|
|
||||||
|
|
||||||
/// The ip address may be either [ipv4] or [ipv6].
|
|
||||||
static const int either = 0;
|
|
||||||
|
|
||||||
/// The ip address must be an ipv4 address.
|
|
||||||
static const int ipv4 = 4;
|
|
||||||
|
|
||||||
/// The ip address must be an ipv6 address.
|
|
||||||
static const int ipv6 = 6;
|
|
||||||
|
|
||||||
/// IP version (on 4 and 6 are valid versions.)
|
|
||||||
final int version;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
assert(
|
|
||||||
version == either || version == ipv4 || version == ipv6,
|
|
||||||
'The version must be AskValidatorIPAddress.either or '
|
|
||||||
'AskValidatorIPAddress.ipv4 or AskValidatorIPAddress.ipv6',
|
|
||||||
);
|
|
||||||
|
|
||||||
final finalline = line.trim();
|
|
||||||
|
|
||||||
var validatorsVersion = IPVersion.any;
|
|
||||||
switch (version) {
|
|
||||||
case ipv4:
|
|
||||||
validatorsVersion = IPVersion.ipV4;
|
|
||||||
break;
|
|
||||||
case ipv6:
|
|
||||||
validatorsVersion = IPVersion.ipV6;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isIP(finalline, version: validatorsVersion)) {
|
|
||||||
throw AskValidatorException(
|
|
||||||
red(customErrorMessage ?? 'Invalid IP Address.'));
|
|
||||||
}
|
|
||||||
return finalline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validates that the entered line is no longer
|
|
||||||
/// than [maxLength].
|
|
||||||
class _AskValidatorMaxLength extends AskValidator {
|
|
||||||
/// Validates that the entered line is no longer
|
|
||||||
/// than [maxLength].
|
|
||||||
const _AskValidatorMaxLength(this.maxLength);
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalline = line.trim();
|
|
||||||
|
|
||||||
if (finalline.length > maxLength) {
|
|
||||||
throw AskValidatorException(
|
|
||||||
red(
|
|
||||||
customErrorMessage ??
|
|
||||||
'You have exceeded the maximum length of $maxLength characters.',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return finalline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// the maximum allows length for the entered string.
|
|
||||||
final int maxLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validates that the entered line is not less
|
|
||||||
/// than [minLength].
|
|
||||||
class _AskValidatorMinLength extends AskValidator {
|
|
||||||
/// Validates that the entered line is not less
|
|
||||||
/// than [minLength].
|
|
||||||
const _AskValidatorMinLength(this.minLength);
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalline = line.trim();
|
|
||||||
|
|
||||||
if (finalline.length < minLength) {
|
|
||||||
throw AskValidatorException(
|
|
||||||
red(customErrorMessage ??
|
|
||||||
'You must enter at least $minLength characters.'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return finalline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// the minimum allows length of the string.
|
|
||||||
final int minLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validates that the length of the entered text
|
|
||||||
// ignore: comment_references
|
|
||||||
/// as at least [minLength] but no more than [maxLength].
|
|
||||||
class _AskValidatorLength extends AskValidator {
|
|
||||||
/// Validates that the length of the entered text
|
|
||||||
/// as at least [minLength] but no more than [maxLength].
|
|
||||||
_AskValidatorLength(int minLength, int maxLength) {
|
|
||||||
_validator = _AskValidatorAll([
|
|
||||||
_AskValidatorMinLength(minLength),
|
|
||||||
_AskValidatorMaxLength(maxLength),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
late _AskValidatorAll _validator;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalline = line.trim();
|
|
||||||
|
|
||||||
return _validator.validate(finalline,
|
|
||||||
customErrorMessage: customErrorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AskValidatorValueRange extends AskValidator {
|
|
||||||
const _AskValidatorValueRange(this.minValue, this.maxValue);
|
|
||||||
|
|
||||||
final num minValue;
|
|
||||||
final num maxValue;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalline = line.trim();
|
|
||||||
|
|
||||||
final value = num.tryParse(finalline);
|
|
||||||
if (value == null) {
|
|
||||||
throw AskValidatorException(
|
|
||||||
red(customErrorMessage ?? 'Must be a number.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value < minValue) {
|
|
||||||
throw AskValidatorException(
|
|
||||||
red(customErrorMessage ??
|
|
||||||
'The number must be greater than or equal to $minValue.'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value > maxValue) {
|
|
||||||
throw AskValidatorException(
|
|
||||||
red(customErrorMessage ??
|
|
||||||
'The number must be less than or equal to $maxValue.'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes an array of validators. The input is considered valid only if
|
|
||||||
/// everyone of the validators pass.
|
|
||||||
///
|
|
||||||
/// The validators are processed in order from left to right.
|
|
||||||
///
|
|
||||||
/// The error from the first validator that failes is returned.
|
|
||||||
///
|
|
||||||
/// It should be noted that the user input is passed to each validator in turn
|
|
||||||
/// and the validator has the opportunity to modify the input. As a result
|
|
||||||
/// a validators will be operating on a version of the input
|
|
||||||
/// that has been processed by all validators that appear earlier in the list.
|
|
||||||
class _AskValidatorAll extends AskValidator {
|
|
||||||
/// Takes an array of validators. The input is considered valid only if
|
|
||||||
/// everyone of the validators pass.
|
|
||||||
///
|
|
||||||
/// The validators are processed in order from left to right.
|
|
||||||
///
|
|
||||||
/// The error from the first validator that failes is returned.
|
|
||||||
///
|
|
||||||
/// It should be noted that the user input is passed to each validator in turn
|
|
||||||
/// and the validator has the opportunity to modify the input. As a result
|
|
||||||
/// a validators will be operating on a version of the input
|
|
||||||
/// that has been processed by all validators that appear earlier
|
|
||||||
/// in the list.
|
|
||||||
_AskValidatorAll(this._validators);
|
|
||||||
|
|
||||||
final List<AskValidator> _validators;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
var finalline = line.trim();
|
|
||||||
|
|
||||||
for (final validator in _validators) {
|
|
||||||
finalline = await validator.validate(finalline,
|
|
||||||
customErrorMessage: customErrorMessage);
|
|
||||||
}
|
|
||||||
return finalline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes an array of validators. The input is considered valid if any one
|
|
||||||
/// of the validators returns true.
|
|
||||||
/// The validators are processed in order from left to right.
|
|
||||||
/// If none of the validators pass then the error from the first validator
|
|
||||||
/// that failed is returned. The implications is that the user will only
|
|
||||||
/// ever see the error from the first validator.
|
|
||||||
///
|
|
||||||
/// It should be noted that the user input is passed to each validator in turn
|
|
||||||
/// and each validator has the opportunity to modify the input. As a result
|
|
||||||
/// a validators will be operating on a version of the input
|
|
||||||
/// that has been processed by all validators that appear earlier in the list.
|
|
||||||
class _AskValidatorAny extends AskValidator {
|
|
||||||
/// Takes an array of validators. The input is considered valid if any one
|
|
||||||
/// of the validators returns true.
|
|
||||||
/// The validators are processed in order from left to right.
|
|
||||||
/// If none of the validators pass then the error from the first validator
|
|
||||||
/// that failed is returned. The implications is that the user will only
|
|
||||||
/// ever see the error from the first validator.
|
|
||||||
///
|
|
||||||
/// It should be noted that the user input is passed to each validator in turn
|
|
||||||
/// and each validator has the opportunity to modify the input. As a result
|
|
||||||
/// a validators will be operating on a version of the input
|
|
||||||
/// that has been processed by all successful validators
|
|
||||||
/// that appear earlier in the list.
|
|
||||||
///
|
|
||||||
/// Validators that fail don't get an opportunity to modify the input.
|
|
||||||
_AskValidatorAny(this._validators);
|
|
||||||
|
|
||||||
final List<AskValidator> _validators;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
var finalline = line.trim();
|
|
||||||
|
|
||||||
AskValidatorException? firstFailure;
|
|
||||||
|
|
||||||
var onePassed = false;
|
|
||||||
|
|
||||||
for (final validator in _validators) {
|
|
||||||
try {
|
|
||||||
finalline = await validator.validate(finalline,
|
|
||||||
customErrorMessage: customErrorMessage);
|
|
||||||
onePassed = true;
|
|
||||||
} on AskValidatorException catch (e) {
|
|
||||||
firstFailure ??= e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!onePassed) {
|
|
||||||
throw firstFailure!;
|
|
||||||
}
|
|
||||||
return finalline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks that the input matches one of the
|
|
||||||
/// provided [validItems].
|
|
||||||
/// If the validator fails it prints out the
|
|
||||||
/// list of available inputs.
|
|
||||||
class _AskValidatorList extends AskValidator {
|
|
||||||
/// Checks that the input matches one of the
|
|
||||||
/// provided [validItems].
|
|
||||||
/// If the validator fails it prints out the
|
|
||||||
/// list of available inputs.
|
|
||||||
/// By default [caseSensitive] matches are off.
|
|
||||||
_AskValidatorList(this.validItems, {this.caseSensitive = false});
|
|
||||||
|
|
||||||
/// The list of allowed values.
|
|
||||||
final List<Object> validItems;
|
|
||||||
final bool caseSensitive;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
var finalline = line.trim();
|
|
||||||
|
|
||||||
if (caseSensitive) {
|
|
||||||
finalline = finalline.toLowerCase();
|
|
||||||
}
|
|
||||||
var found = false;
|
|
||||||
for (final item in validItems) {
|
|
||||||
var itemValue = item.toString();
|
|
||||||
if (caseSensitive) {
|
|
||||||
itemValue = itemValue.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finalline == itemValue) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
throw AskValidatorException(
|
|
||||||
red(customErrorMessage ??
|
|
||||||
'The valid responses are ${validItems.join(' | ')}.'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalline;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
/* Copyright (C) S. Brett Sutton - All Rights Reserved
|
|
||||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
|
||||||
* Proprietary and confidential
|
|
||||||
* Written by Brett Sutton <bsutton@onepub.dev>, Jan 2022
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'package:protevus_console/terminal.dart';
|
|
||||||
|
|
||||||
import 'ask.dart';
|
|
||||||
|
|
||||||
typedef CustomConfirmPrompt = String Function(
|
|
||||||
String prompt,
|
|
||||||
// ignore: avoid_positional_boolean_parameters
|
|
||||||
bool? defaultValue);
|
|
||||||
|
|
||||||
/// [confirm] is a specialized version of ask that returns true or
|
|
||||||
/// false based on the value entered.
|
|
||||||
///
|
|
||||||
/// The user must enter a valid value or, if a [defaultValue]
|
|
||||||
/// is passed, the enter key.
|
|
||||||
///
|
|
||||||
/// Accepted values are y|t|true|yes and n|f|false|no (case insenstiive).
|
|
||||||
///
|
|
||||||
/// If the user enters an unknown value an error is printed
|
|
||||||
/// and they are reprompted.
|
|
||||||
///
|
|
||||||
/// The [prompt] is displayed to the user with ' (y/n)' appended.
|
|
||||||
///
|
|
||||||
/// If a [defaultValue] is passed then either the y or n will be capitalised
|
|
||||||
/// and if the user hits the enter key then the [defaultValue] will be returned.
|
|
||||||
///
|
|
||||||
/// If the script is not attached to a terminal [Terminal().hasTerminal]
|
|
||||||
/// then confirm returns immediately with the [defaultValue].
|
|
||||||
/// If there is no [defaultValue] then true is returned.
|
|
||||||
Future<bool> confirm(String prompt,
|
|
||||||
{bool? defaultValue,
|
|
||||||
CustomConfirmPrompt customPrompt = Confirm.defaultPrompt}) async {
|
|
||||||
var result = false;
|
|
||||||
var matched = false;
|
|
||||||
|
|
||||||
if (!Terminal().hasTerminal) {
|
|
||||||
return defaultValue ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (!matched) {
|
|
||||||
final entered = await ask(
|
|
||||||
prompt,
|
|
||||||
toLower: true,
|
|
||||||
required: false,
|
|
||||||
customPrompt: (_, __, ___) => customPrompt(prompt, defaultValue),
|
|
||||||
);
|
|
||||||
var lower = entered.trim().toLowerCase();
|
|
||||||
|
|
||||||
if (lower.isEmpty && defaultValue != null) {
|
|
||||||
lower = defaultValue ? 'true' : 'false';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['y', 't', 'true', 'yes'].contains(lower)) {
|
|
||||||
result = true;
|
|
||||||
matched = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (['n', 'f', 'false', 'no'].contains(lower)) {
|
|
||||||
result = false;
|
|
||||||
matched = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
print('Invalid value: $entered');
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore: avoid_classes_with_only_static_members
|
|
||||||
class Confirm {
|
|
||||||
// ignore: avoid_positional_boolean_parameters
|
|
||||||
static String defaultPrompt(String prompt, bool? defaultValue) {
|
|
||||||
var finalPrompt = prompt;
|
|
||||||
|
|
||||||
if (defaultValue == null) {
|
|
||||||
finalPrompt += ' (y/n):';
|
|
||||||
} else {
|
|
||||||
if (defaultValue == true) {
|
|
||||||
finalPrompt += ' (Y/n):';
|
|
||||||
} else {
|
|
||||||
finalPrompt += ' (y/N):';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return finalPrompt;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
/* Copyright (C) S. Brett Sutton - All Rights Reserved
|
|
||||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
|
||||||
* Proprietary and confidential
|
|
||||||
* Written by Brett Sutton <bsutton@onepub.dev>, Jan 2022
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
/// Writes [text] to stdout including a newline.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// echo("Hello world", newline=false);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If [newline] is false then a newline will not be output.
|
|
||||||
///
|
|
||||||
/// [newline] defaults to false.
|
|
||||||
Future<void> echo(String text, {bool newline = false}) async =>
|
|
||||||
_Echo().echo(text, newline: newline);
|
|
||||||
|
|
||||||
class _Echo extends DCliFunction {
|
|
||||||
Future<void> echo(String text, {required bool newline}) async {
|
|
||||||
if (newline) {
|
|
||||||
stdout.writeln(text);
|
|
||||||
} else {
|
|
||||||
stdout.write(text);
|
|
||||||
}
|
|
||||||
// ignore: discarded_futures
|
|
||||||
await stdout.flush();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,185 +0,0 @@
|
||||||
/* Copyright (C) S. Brett Sutton - All Rights Reserved
|
|
||||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
|
||||||
* Proprietary and confidential
|
|
||||||
* Written by Brett Sutton <bsutton@onepub.dev>, Jan 2022
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:protevus_console/terminal.dart';
|
|
||||||
|
|
||||||
import 'ask.dart';
|
|
||||||
|
|
||||||
String _noFormat<T>(T option) => option.toString();
|
|
||||||
|
|
||||||
typedef CustomMenuPrompt = String Function(
|
|
||||||
String prompt, String? defaultOption);
|
|
||||||
|
|
||||||
/// Displays a menu with each of the provided [options], prompts
|
|
||||||
/// the user to select an option and returns the selected option.
|
|
||||||
///
|
|
||||||
/// e.g.
|
|
||||||
/// ```dart
|
|
||||||
/// var colors = [Color('Red'), Color('Green')];
|
|
||||||
/// var color = menu( 'Please select a color', options: colors);
|
|
||||||
/// ```
|
|
||||||
/// Results in:
|
|
||||||
///```
|
|
||||||
/// 1) Red
|
|
||||||
/// 2) Green
|
|
||||||
/// Please select a color:
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [menu] will display an error if the user enters a non-valid
|
|
||||||
/// response and then redisplay the prompt.
|
|
||||||
///
|
|
||||||
/// Once a user selects a valid option, that option is returned.
|
|
||||||
///
|
|
||||||
/// You may provide a [limit] which will cause the
|
|
||||||
/// menu to only display the first [limit] options passed.
|
|
||||||
///
|
|
||||||
/// If you pass a [format] lambda then the [format] function
|
|
||||||
/// will be called for for each option and the resulting format
|
|
||||||
/// used to display the option in the menu.
|
|
||||||
///
|
|
||||||
/// e.g.
|
|
||||||
/// ```dart
|
|
||||||
///
|
|
||||||
/// var colors = [Color('Red'), Color('Green')];
|
|
||||||
/// var color = menu(prompt: 'Please select a color'
|
|
||||||
/// , options: colors, format: (color) => color.name);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If [format] is not passed [option.toString()] will be used
|
|
||||||
/// as the format for the menu option.
|
|
||||||
///
|
|
||||||
/// When a [limit] is applied the menu will display the first [limit]
|
|
||||||
/// options. If you specify [fromStart: false] then the menu will display the
|
|
||||||
/// last [limit] options.
|
|
||||||
///
|
|
||||||
/// If you pass a [defaultOption] the matching option is highlighted
|
|
||||||
/// in green in the menu
|
|
||||||
/// and if the user hits enter without entering a value the [defaultOption]
|
|
||||||
/// is returned.
|
|
||||||
///
|
|
||||||
/// If the [defaultOption] does not match any the supplied [options]
|
|
||||||
/// then an ArgumentError is thrown.
|
|
||||||
///
|
|
||||||
/// If the app is not attached to a terminal then the menu will not be
|
|
||||||
/// displayed and the [defaultOption] will be returned.
|
|
||||||
/// If there is no [defaultOption] then the first [options] will be returned.
|
|
||||||
///
|
|
||||||
Future<T> menu<T>(
|
|
||||||
String prompt, {
|
|
||||||
required List<T> options,
|
|
||||||
T? defaultOption,
|
|
||||||
CustomMenuPrompt customPrompt = Menu.defaultPrompt,
|
|
||||||
int? limit,
|
|
||||||
String Function(T)? format,
|
|
||||||
bool fromStart = true,
|
|
||||||
}) async {
|
|
||||||
if (options.isEmpty) {
|
|
||||||
throw ArgumentError(
|
|
||||||
'The list of [options] passed to menu(options: ) was empty.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
limit ??= options.length;
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
limit = min(options.length, limit);
|
|
||||||
format ??= _noFormat;
|
|
||||||
|
|
||||||
if (!Terminal().hasTerminal) {
|
|
||||||
if (defaultOption == null) {
|
|
||||||
return options.first;
|
|
||||||
}
|
|
||||||
return defaultOption;
|
|
||||||
}
|
|
||||||
|
|
||||||
var displayList = options;
|
|
||||||
if (fromStart == false) {
|
|
||||||
// get the last [limit] options
|
|
||||||
displayList = options.sublist(min(options.length, options.length - limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
// on the way in we check that the default value actually exists in the list.
|
|
||||||
String? defaultAsString;
|
|
||||||
// display each option.
|
|
||||||
for (var i = 1; i <= limit; i++) {
|
|
||||||
final option = displayList[i - 1];
|
|
||||||
|
|
||||||
if (option == defaultOption) {
|
|
||||||
defaultAsString = i.toString();
|
|
||||||
}
|
|
||||||
final desc = format(option);
|
|
||||||
final no = '$i'.padLeft(3);
|
|
||||||
if (defaultOption != null && defaultOption == option) {
|
|
||||||
/// highlight the default value.
|
|
||||||
print(green('$no) $desc'));
|
|
||||||
} else {
|
|
||||||
print('$no) $desc');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defaultOption != null && defaultAsString == null) {
|
|
||||||
throw ArgumentError(
|
|
||||||
"The [defaultOption] $defaultOption doesn't match any "
|
|
||||||
'of the passed [options].'
|
|
||||||
' Check the == operator for ${options[0].runtimeType}.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var valid = false;
|
|
||||||
|
|
||||||
var index = -1;
|
|
||||||
|
|
||||||
// loop until the user enters a valid selection.
|
|
||||||
while (!valid) {
|
|
||||||
final selected = await ask(prompt,
|
|
||||||
defaultValue: defaultAsString,
|
|
||||||
validator: _MenuRange(limit),
|
|
||||||
customPrompt: (_, __, ___) => customPrompt(prompt, defaultAsString));
|
|
||||||
if (selected.isEmpty) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
valid = true;
|
|
||||||
index = int.parse(selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
return options[index - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore: avoid_classes_with_only_static_members
|
|
||||||
class Menu {
|
|
||||||
static String defaultPrompt<T>(String prompt, T? defaultValue) {
|
|
||||||
var result = prompt;
|
|
||||||
|
|
||||||
/// completely suppress the default value and the prompt if
|
|
||||||
/// the prompt is empty.
|
|
||||||
if (defaultValue != null && prompt.isNotEmpty) {
|
|
||||||
result = '$prompt [$defaultValue]';
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MenuRange extends AskValidator {
|
|
||||||
const _MenuRange(this.limit);
|
|
||||||
@override
|
|
||||||
Future<String> validate(String line, {String? customErrorMessage}) async {
|
|
||||||
final finalline = line.trim();
|
|
||||||
final value = num.tryParse(finalline);
|
|
||||||
if (value == null) {
|
|
||||||
throw AskValidatorException(
|
|
||||||
red(customErrorMessage ?? 'Value must be an integer from 1 to $limit'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value < 1 || value > limit) {
|
|
||||||
throw AskValidatorException('Invalid selection.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalline;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int limit;
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:stack_trace/stack_trace.dart';
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
class Settings {
|
|
||||||
/// Returns a singleton providing
|
|
||||||
/// access to DCli settings.
|
|
||||||
factory Settings() => _self ??= Settings._init();
|
|
||||||
|
|
||||||
Settings._init();
|
|
||||||
|
|
||||||
final logger = Logger('dcli');
|
|
||||||
static Settings? _self;
|
|
||||||
|
|
||||||
bool _verboseEnabled = false;
|
|
||||||
|
|
||||||
/// returns true if the -v (verbose) flag was set on the
|
|
||||||
/// dcli command line.
|
|
||||||
/// e.g.
|
|
||||||
/// dcli -v clean
|
|
||||||
bool get isVerbose => _verboseEnabled;
|
|
||||||
|
|
||||||
// ignore: cancel_subscriptions
|
|
||||||
static StreamSubscription<LogRecord>? listener;
|
|
||||||
|
|
||||||
/// Turns on verbose logging.
|
|
||||||
Future<void> setVerbose({required bool enabled}) async {
|
|
||||||
_verboseEnabled = enabled;
|
|
||||||
|
|
||||||
// ignore: flutter_style_todos
|
|
||||||
/// TODO(bsutton): this affects everyones logging so
|
|
||||||
/// I'm uncertain if this is a problem.
|
|
||||||
hierarchicalLoggingEnabled = true;
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
logger.level = Level.INFO;
|
|
||||||
listener ??= logger.onRecord.listen((record) {
|
|
||||||
print('${record.level.name}: ${record.time}: ${record.message}');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.level = Level.OFF;
|
|
||||||
if (listener != null) {
|
|
||||||
await listener!.cancel();
|
|
||||||
listener = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Logs a message to the console if the verbose
|
|
||||||
/// settings are on.
|
|
||||||
void verbose(String? message, {Frame? frame}) {
|
|
||||||
final Frame calledBy;
|
|
||||||
if (frame == null) {
|
|
||||||
final st = Trace.current();
|
|
||||||
calledBy = st.frames[1];
|
|
||||||
} else {
|
|
||||||
calledBy = frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// We log at info level (as that is logger's default)
|
|
||||||
/// so that verbose messages will print when verbose
|
|
||||||
/// is enabled.
|
|
||||||
Logger('dcli').info('${calledBy.library}:${calledBy.line} $message');
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<LogRecord> captureLogOutput() => logger.onRecord;
|
|
||||||
|
|
||||||
void clearLogCapture() {
|
|
||||||
logger.clearListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// True if you are running on a Mac.
|
|
||||||
bool get isMacOS => DCliPlatform().isMacOS;
|
|
||||||
|
|
||||||
/// True if you are running on a Linux system.
|
|
||||||
bool get isLinux => DCliPlatform().isLinux;
|
|
||||||
|
|
||||||
/// True if you are running on a Window system.
|
|
||||||
bool get isWindows => DCliPlatform().isWindows;
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// If Settings.isVerbose is true then
|
|
||||||
/// this method will call [callback] to
|
|
||||||
/// get a String which will be logged to the
|
|
||||||
/// console or the log file set via the verbose command line
|
|
||||||
/// option.
|
|
||||||
///
|
|
||||||
/// This method is more efficient than calling Settings.verbose
|
|
||||||
/// as it will only build the string if verbose is enabled.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// verbose(() => 'Log the users name $user');
|
|
||||||
///
|
|
||||||
void verbose(String Function() callback) {
|
|
||||||
if (Settings().isVerbose) {
|
|
||||||
Settings().verbose(callback(), frame: Trace.current().frames[1]);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'ansi_color.dart';
|
|
||||||
import 'terminal.dart';
|
|
||||||
|
|
||||||
/// Helper class to assist in printing text to the console with a color.
|
|
||||||
///
|
|
||||||
/// Use one of the color functions instead of this class.
|
|
||||||
///
|
|
||||||
/// See:
|
|
||||||
/// * [AnsiColor]
|
|
||||||
/// * [Terminal]
|
|
||||||
/// ...
|
|
||||||
class Ansi {
|
|
||||||
/// Factory ctor
|
|
||||||
factory Ansi() => _self;
|
|
||||||
|
|
||||||
const Ansi._internal();
|
|
||||||
|
|
||||||
static const _self = Ansi._internal();
|
|
||||||
static bool? _emitAnsi;
|
|
||||||
|
|
||||||
/// returns true if stdout supports ansi escape characters.
|
|
||||||
static bool get isSupported {
|
|
||||||
if (_emitAnsi == null) {
|
|
||||||
// We don't trust [stdout.supportsAnsiEscapes] except on Windows.
|
|
||||||
// [stdout] relies on the TERM environment variable
|
|
||||||
// which generates false negatives.
|
|
||||||
if (!Platform.isWindows) {
|
|
||||||
_emitAnsi = true;
|
|
||||||
} else {
|
|
||||||
_emitAnsi = stdout.supportsAnsiEscapes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _emitAnsi!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// You can set [isSupported] to
|
|
||||||
/// override the detected ansi settings.
|
|
||||||
/// Dart doesn't do a great job of correctly detecting
|
|
||||||
/// ansi support so this give a way to override it.
|
|
||||||
/// If [isSupported] is true then escape charaters are emmitted
|
|
||||||
/// If [isSupported] is false escape characters are not emmited
|
|
||||||
/// By default the detected setting is used.
|
|
||||||
/// After setting emitAnsi you can reset back to the
|
|
||||||
/// default detected by calling [resetEmitAnsi].
|
|
||||||
static set isSupported(bool emit) => _emitAnsi = emit;
|
|
||||||
|
|
||||||
/// If you have called [isSupported] then calling
|
|
||||||
/// [resetEmitAnsi] will reset the emit
|
|
||||||
/// setting to the default detected.
|
|
||||||
static void get resetEmitAnsi => _emitAnsi = null;
|
|
||||||
|
|
||||||
/// ANSI Control Sequence Introducer, signals the terminal for new settings.
|
|
||||||
static const esc = '\x1b[';
|
|
||||||
// static const esc = '\u001b[';
|
|
||||||
|
|
||||||
/// Strip all ansi escape sequences from [line].
|
|
||||||
///
|
|
||||||
/// This method is useful when logging messages
|
|
||||||
/// or if you need to calculate the number of printable
|
|
||||||
/// characters in a message.
|
|
||||||
static String strip(String line) =>
|
|
||||||
line.replaceAll(RegExp('\x1b\\[[0-9;]+m'), '');
|
|
||||||
}
|
|
|
@ -1,496 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'ansi.dart';
|
|
||||||
|
|
||||||
/// Wraps the passed text with the ANSI escape sequence for
|
|
||||||
/// the color red.
|
|
||||||
/// Use this to control the color of text when printing to the
|
|
||||||
/// console.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(red('a dark message'));
|
|
||||||
/// ```
|
|
||||||
/// The [text] to wrap.
|
|
||||||
/// By default the color is [bold] however you can turn off bold
|
|
||||||
/// by setting the [bold] argment to false:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(red('a dark message', bold: false));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [background] is the background color to use when printing the
|
|
||||||
/// text. Defaults to White.
|
|
||||||
///
|
|
||||||
String red(
|
|
||||||
String text, {
|
|
||||||
AnsiColor background = AnsiColor.none,
|
|
||||||
bool bold = true,
|
|
||||||
}) =>
|
|
||||||
AnsiColor._apply(
|
|
||||||
AnsiColor(AnsiColor.codeRed, bold: bold),
|
|
||||||
text,
|
|
||||||
background: background,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Wraps the passed text with the ANSI escape sequence for
|
|
||||||
/// the color red.
|
|
||||||
/// Use this to control the color of text when printing to the
|
|
||||||
/// console.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(black('a dark message'));
|
|
||||||
/// ```
|
|
||||||
/// The [text] to wrap.
|
|
||||||
/// By default the color is [bold] however you can turn off bold
|
|
||||||
/// by setting the [bold] argment to false:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(black('a dark message', bold: false));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [background] is the background color to use when printing the
|
|
||||||
/// text. Defaults to White.
|
|
||||||
///
|
|
||||||
String black(
|
|
||||||
String text, {
|
|
||||||
AnsiColor background = AnsiColor.white,
|
|
||||||
bool bold = true,
|
|
||||||
}) =>
|
|
||||||
AnsiColor._apply(
|
|
||||||
AnsiColor(AnsiColor.codeBlack, bold: bold),
|
|
||||||
text,
|
|
||||||
background: background,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Wraps the passed text with the ANSI escape sequence for
|
|
||||||
/// the color red.
|
|
||||||
/// Use this to control the color of text when printing to the
|
|
||||||
/// console.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(green('a dark message'));
|
|
||||||
/// ```
|
|
||||||
/// The [text] to wrap.
|
|
||||||
/// By default the color is [bold] however you can turn off bold
|
|
||||||
/// by setting the [bold] argment to false:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(green('a dark message', bold: false));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [background] is the background color to use when printing the
|
|
||||||
/// text. Defaults to White.
|
|
||||||
///
|
|
||||||
String green(
|
|
||||||
String text, {
|
|
||||||
AnsiColor background = AnsiColor.none,
|
|
||||||
bool bold = true,
|
|
||||||
}) =>
|
|
||||||
AnsiColor._apply(
|
|
||||||
AnsiColor(AnsiColor.codeGreen, bold: bold),
|
|
||||||
text,
|
|
||||||
background: background,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Wraps the passed text with the ANSI escape sequence for
|
|
||||||
/// the color red.
|
|
||||||
/// Use this to control the color of text when printing to the
|
|
||||||
/// console.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(blue('a dark message'));
|
|
||||||
/// ```
|
|
||||||
/// The [text] to wrap.
|
|
||||||
/// By default the color is [bold] however you can turn off bold
|
|
||||||
/// by setting the [bold] argment to false:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(blue('a dark message', bold: false));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [background] is the background color to use when printing the
|
|
||||||
/// text. Defaults to White.
|
|
||||||
///
|
|
||||||
String blue(
|
|
||||||
String text, {
|
|
||||||
AnsiColor background = AnsiColor.none,
|
|
||||||
bool bold = true,
|
|
||||||
}) =>
|
|
||||||
AnsiColor._apply(
|
|
||||||
AnsiColor(AnsiColor.codeBlue, bold: bold),
|
|
||||||
text,
|
|
||||||
background: background,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Wraps the passed text with the ANSI escape sequence for
|
|
||||||
/// the color red.
|
|
||||||
/// Use this to control the color of text when printing to the
|
|
||||||
/// console.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(yellow('a dark message'));
|
|
||||||
/// ```
|
|
||||||
/// The [text] to wrap.
|
|
||||||
/// By default the color is [bold] however you can turn off bold
|
|
||||||
/// by setting the [bold] argment to false:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(yellow('a dark message', bold: false));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [background] is the background color to use when printing the
|
|
||||||
/// text. Defaults to White.
|
|
||||||
///
|
|
||||||
String yellow(
|
|
||||||
String text, {
|
|
||||||
AnsiColor background = AnsiColor.none,
|
|
||||||
bool bold = true,
|
|
||||||
}) =>
|
|
||||||
AnsiColor._apply(
|
|
||||||
AnsiColor(AnsiColor.codeYellow, bold: bold),
|
|
||||||
text,
|
|
||||||
background: background,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Wraps the passed text with the ANSI escape sequence for
|
|
||||||
/// the color red.
|
|
||||||
/// Use this to control the color of text when printing to the
|
|
||||||
/// console.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(magenta('a dark message'));
|
|
||||||
/// ```
|
|
||||||
/// The [text] to wrap.
|
|
||||||
/// By default the color is [bold] however you can turn off bold
|
|
||||||
/// by setting the [bold] argment to false:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(magenta('a dark message', bold: false));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [background] is the background color to use when printing the
|
|
||||||
/// text. Defaults to White.
|
|
||||||
///xt. Defaults to none.
|
|
||||||
///
|
|
||||||
String magenta(
|
|
||||||
String text, {
|
|
||||||
AnsiColor background = AnsiColor.none,
|
|
||||||
bool bold = true,
|
|
||||||
}) =>
|
|
||||||
AnsiColor._apply(
|
|
||||||
AnsiColor(AnsiColor.codeMagenta, bold: bold),
|
|
||||||
text,
|
|
||||||
background: background,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Wraps the passed text with the ANSI escape sequence for
|
|
||||||
/// the color red.
|
|
||||||
/// Use this to control the color of text when printing to the
|
|
||||||
/// console.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(cyan('a dark message'));
|
|
||||||
/// ```
|
|
||||||
/// The [text] to wrap.
|
|
||||||
/// By default the color is [bold] however you can turn off bold
|
|
||||||
/// by setting the [bold] argment to false:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(cyan('a dark message', bold: false));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [background] is the background color to use when printing the
|
|
||||||
/// text. Defaults to White.
|
|
||||||
///xt. Defaults to none.
|
|
||||||
///
|
|
||||||
String cyan(
|
|
||||||
String text, {
|
|
||||||
AnsiColor background = AnsiColor.none,
|
|
||||||
bool bold = true,
|
|
||||||
}) =>
|
|
||||||
AnsiColor._apply(
|
|
||||||
AnsiColor(AnsiColor.codeCyan, bold: bold),
|
|
||||||
text,
|
|
||||||
background: background,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Wraps the passed text with the ANSI escape sequence for
|
|
||||||
/// the color red.
|
|
||||||
/// Use this to control the color of text when printing to the
|
|
||||||
/// console.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(white('a dark message'));
|
|
||||||
/// ```
|
|
||||||
/// The [text] to wrap.
|
|
||||||
/// By default the color is [bold] however you can turn off bold
|
|
||||||
/// by setting the [bold] argment to false:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(white('a dark message', bold: false));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [background] is the background color to use when printing the
|
|
||||||
/// text. Defaults to White.
|
|
||||||
///xt. Defaults to none.
|
|
||||||
///
|
|
||||||
String white(
|
|
||||||
String text, {
|
|
||||||
AnsiColor background = AnsiColor.none,
|
|
||||||
bool bold = true,
|
|
||||||
}) =>
|
|
||||||
AnsiColor._apply(
|
|
||||||
AnsiColor(AnsiColor.codeWhite, bold: bold),
|
|
||||||
text,
|
|
||||||
background: background,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Wraps the passed text with the ANSI escape sequence for
|
|
||||||
/// the color red.
|
|
||||||
/// Use this to control the color of text when printing to the
|
|
||||||
/// console.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(orange('a dark message'));
|
|
||||||
/// ```
|
|
||||||
/// The [text] to wrap.
|
|
||||||
/// By default the color is [bold] however you can turn off bold
|
|
||||||
/// by setting the [bold] argment to false:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(orange('a dark message', bold: false));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [background] is the background color to use when printing the
|
|
||||||
/// text. Defaults to White.
|
|
||||||
///xt. Defaults to none.
|
|
||||||
///
|
|
||||||
String orange(
|
|
||||||
String text, {
|
|
||||||
AnsiColor background = AnsiColor.none,
|
|
||||||
bool bold = true,
|
|
||||||
}) =>
|
|
||||||
AnsiColor._apply(
|
|
||||||
AnsiColor(AnsiColor.codeOrange, bold: bold),
|
|
||||||
text,
|
|
||||||
background: background,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Wraps the passed text with the ANSI escape sequence for
|
|
||||||
/// the color red.
|
|
||||||
/// Use this to control the color of text when printing to the
|
|
||||||
/// console.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(grey('a dark message'));
|
|
||||||
/// ```
|
|
||||||
/// The [text] to wrap.
|
|
||||||
/// By default the color is [bold] however you can turn off bold
|
|
||||||
/// by setting the [bold] argment to false:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// print(grey('a dark message', bold: false));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [background] is the background color to use when printing the
|
|
||||||
/// text. Defaults to White.
|
|
||||||
///xt. Defaults to none.
|
|
||||||
///
|
|
||||||
String grey(
|
|
||||||
String text, {
|
|
||||||
double level = 0.5,
|
|
||||||
AnsiColor background = AnsiColor.none,
|
|
||||||
bool bold = true,
|
|
||||||
}) =>
|
|
||||||
AnsiColor._apply(
|
|
||||||
AnsiColor._grey(level: level, bold: bold),
|
|
||||||
text,
|
|
||||||
background: background,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Helper class to assist in printing text to the console with a color.
|
|
||||||
///
|
|
||||||
/// Use one of the color functions instead of this class.
|
|
||||||
///
|
|
||||||
/// See:
|
|
||||||
/// * [black]
|
|
||||||
/// * [white]
|
|
||||||
/// * [green]
|
|
||||||
/// * [orange]
|
|
||||||
/// ...
|
|
||||||
class AnsiColor {
|
|
||||||
///
|
|
||||||
const AnsiColor(
|
|
||||||
int code, {
|
|
||||||
bool bold = true,
|
|
||||||
}) : _code = code,
|
|
||||||
_bold = bold;
|
|
||||||
|
|
||||||
AnsiColor._grey({
|
|
||||||
double level = 0.5,
|
|
||||||
bool bold = true,
|
|
||||||
}) : _code = codeGrey + (level.clamp(0.0, 1.0) * 23).round(),
|
|
||||||
_bold = bold;
|
|
||||||
|
|
||||||
/// resets the color scheme.
|
|
||||||
static String reset() => _emit(_resetCode);
|
|
||||||
|
|
||||||
/// resets the foreground color
|
|
||||||
static String fgReset() => _emit(_fgResetCode);
|
|
||||||
|
|
||||||
/// resets the background color.
|
|
||||||
static String bgReset() => _emit(_bgResetCode);
|
|
||||||
|
|
||||||
final int _code;
|
|
||||||
|
|
||||||
final bool _bold;
|
|
||||||
|
|
||||||
//
|
|
||||||
static String _emit(String ansicode) => '${Ansi.esc}${ansicode}m';
|
|
||||||
|
|
||||||
/// ansi code for this color.
|
|
||||||
int get code => _code;
|
|
||||||
|
|
||||||
/// do we bold the color
|
|
||||||
bool get bold => _bold;
|
|
||||||
|
|
||||||
/// writes the text to the terminal.
|
|
||||||
String apply(String text, {AnsiColor background = none}) =>
|
|
||||||
_apply(this, text, background: background);
|
|
||||||
|
|
||||||
static String _apply(
|
|
||||||
AnsiColor color,
|
|
||||||
String text, {
|
|
||||||
AnsiColor background = none,
|
|
||||||
}) {
|
|
||||||
String? output;
|
|
||||||
|
|
||||||
if (Ansi.isSupported) {
|
|
||||||
output = '${_fg(color.code, bold: color.bold)}'
|
|
||||||
'${_bg(background.code)}$text$_reset';
|
|
||||||
} else {
|
|
||||||
output = text;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String get _reset => '${Ansi.esc}${_resetCode}m';
|
|
||||||
|
|
||||||
static String _fg(
|
|
||||||
int code, {
|
|
||||||
bool bold = true,
|
|
||||||
}) {
|
|
||||||
String output;
|
|
||||||
|
|
||||||
if (code == none.code) {
|
|
||||||
output = '';
|
|
||||||
} else if (code > 39) {
|
|
||||||
output = '${Ansi.esc}$_fgColorCode$code${bold ? ';1' : ''}m';
|
|
||||||
} else {
|
|
||||||
output = '${Ansi.esc}$code${bold ? ';1' : ''}m';
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
// background colors are fg color + 10
|
|
||||||
static String _bg(int code) {
|
|
||||||
String output;
|
|
||||||
|
|
||||||
if (code == none.code) {
|
|
||||||
output = '';
|
|
||||||
} else if (code > 49) {
|
|
||||||
output = '${Ansi.esc}$_backgroundCode${code + 10}m';
|
|
||||||
} else {
|
|
||||||
output = '${Ansi.esc}${code + 10}m';
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets
|
|
||||||
|
|
||||||
/// Reset fg and bg colors
|
|
||||||
static const String _resetCode = '0';
|
|
||||||
|
|
||||||
/// Defaults the terminal's fg color without altering the bg.
|
|
||||||
static const String _fgResetCode = '39';
|
|
||||||
|
|
||||||
/// Defaults the terminal's bg color without altering the fg.
|
|
||||||
static const String _bgResetCode = '49';
|
|
||||||
|
|
||||||
// emit this code followed by a color code to set the fg color
|
|
||||||
static const String _fgColorCode = '38;5;';
|
|
||||||
|
|
||||||
// emit this code followed by a color code to set the fg color
|
|
||||||
static const String _backgroundCode = '48;5;';
|
|
||||||
|
|
||||||
/// code for black
|
|
||||||
static const int codeBlack = 30;
|
|
||||||
|
|
||||||
/// code for red
|
|
||||||
static const int codeRed = 31;
|
|
||||||
|
|
||||||
/// code for green
|
|
||||||
static const int codeGreen = 32;
|
|
||||||
|
|
||||||
/// code for yellow
|
|
||||||
static const int codeYellow = 33;
|
|
||||||
|
|
||||||
/// code for blue
|
|
||||||
static const int codeBlue = 34;
|
|
||||||
|
|
||||||
/// code for magenta
|
|
||||||
static const int codeMagenta = 35;
|
|
||||||
|
|
||||||
/// code for cyan
|
|
||||||
static const int codeCyan = 36;
|
|
||||||
|
|
||||||
/// code for white
|
|
||||||
static const int codeWhite = 37;
|
|
||||||
|
|
||||||
/// code for orange
|
|
||||||
static const int codeOrange = 208;
|
|
||||||
|
|
||||||
/// code for grey
|
|
||||||
static const int codeGrey = 232;
|
|
||||||
|
|
||||||
/// Colors
|
|
||||||
/// black
|
|
||||||
static const AnsiColor black = AnsiColor(codeBlack);
|
|
||||||
|
|
||||||
/// red
|
|
||||||
static const AnsiColor red = AnsiColor(codeRed);
|
|
||||||
|
|
||||||
/// green
|
|
||||||
static const AnsiColor green = AnsiColor(codeGreen);
|
|
||||||
|
|
||||||
/// yellow
|
|
||||||
static const AnsiColor yellow = AnsiColor(codeYellow);
|
|
||||||
|
|
||||||
/// blue
|
|
||||||
static const AnsiColor blue = AnsiColor(codeBlue);
|
|
||||||
|
|
||||||
/// magenta
|
|
||||||
static const AnsiColor magenta = AnsiColor(codeMagenta);
|
|
||||||
|
|
||||||
/// cyan
|
|
||||||
static const AnsiColor cyan = AnsiColor(codeCyan);
|
|
||||||
|
|
||||||
/// white
|
|
||||||
static const AnsiColor white = AnsiColor(codeWhite);
|
|
||||||
|
|
||||||
/// orange
|
|
||||||
static const AnsiColor orange = AnsiColor(codeOrange);
|
|
||||||
|
|
||||||
/// passing this as the background color will cause
|
|
||||||
/// the background code to be suppressed resulting
|
|
||||||
/// in the default background color.
|
|
||||||
static const AnsiColor none = AnsiColor(-1, bold: false);
|
|
||||||
}
|
|
|
@ -1,190 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
/// provides a random collection of formatters
|
|
||||||
/// EXPERIMENTAL
|
|
||||||
///
|
|
||||||
class Format {
|
|
||||||
/// Factory constructor.
|
|
||||||
factory Format() => _self;
|
|
||||||
Format._internal();
|
|
||||||
|
|
||||||
static final _self = Format._internal();
|
|
||||||
|
|
||||||
/// [cols] is a list of strings (the columns) that
|
|
||||||
/// are to be formatted as a set of fixed with
|
|
||||||
/// columns.
|
|
||||||
///
|
|
||||||
/// As this is a fixed width row colums that exceed the given
|
|
||||||
/// width will be clipped.
|
|
||||||
/// You can make any column variable width by passing -1 as the width.
|
|
||||||
///
|
|
||||||
/// By default there is a single space between each column you
|
|
||||||
/// can pass a [delimiter] to modify this behaviour. You can
|
|
||||||
/// suppress the [delimiter] by passing an empty string ''.
|
|
||||||
///
|
|
||||||
/// [widths] defines the width of each column in the row.
|
|
||||||
///
|
|
||||||
/// If their are more [cols] than [widths] then the last width
|
|
||||||
/// is used repeatedly.
|
|
||||||
/// If [widths] is null then a default width of 20 is used.
|
|
||||||
///
|
|
||||||
/// returns a string with each of the columns padded according to the
|
|
||||||
/// [widths].
|
|
||||||
///
|
|
||||||
///
|
|
||||||
String row(
|
|
||||||
List<String?> cols, {
|
|
||||||
List<int>? widths,
|
|
||||||
List<TableAlignment>? alignments,
|
|
||||||
String? delimiter,
|
|
||||||
}) {
|
|
||||||
var row = '';
|
|
||||||
var i = 0;
|
|
||||||
widths ??= [20];
|
|
||||||
var width = widths[0];
|
|
||||||
|
|
||||||
alignments ??= [TableAlignment.left];
|
|
||||||
var alignment = alignments[0];
|
|
||||||
|
|
||||||
delimiter ??= ' ';
|
|
||||||
|
|
||||||
for (var col in cols) {
|
|
||||||
if (row.isNotEmpty) {
|
|
||||||
row += delimiter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// make row robust if a null col is passed.
|
|
||||||
col ??= '';
|
|
||||||
var colwidth = col.length;
|
|
||||||
if (colwidth > width) {
|
|
||||||
colwidth = width;
|
|
||||||
if (width != -1) {
|
|
||||||
col = col.substring(0, width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch (alignment) {
|
|
||||||
case TableAlignment.left:
|
|
||||||
row += col.padRight(width);
|
|
||||||
break;
|
|
||||||
case TableAlignment.right:
|
|
||||||
row += col.padLeft(width);
|
|
||||||
break;
|
|
||||||
case TableAlignment.middle:
|
|
||||||
final padding = width = colwidth;
|
|
||||||
row += col.padLeft(padding);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
i++;
|
|
||||||
|
|
||||||
if (i < widths.length) {
|
|
||||||
width = widths[i];
|
|
||||||
}
|
|
||||||
if (i < alignments.length) {
|
|
||||||
alignment = alignments[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Limits the [display] string's length to [width] by removing the centre
|
|
||||||
/// components of the string and replacing them with '...'
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// var long = 'http://www.onepub.dev/some/long/url';
|
|
||||||
/// print(limitString(long, width: 20))
|
|
||||||
/// > http://...ong/url
|
|
||||||
String limitString(String display, {int width = 40}) {
|
|
||||||
if (display.length <= width) {
|
|
||||||
return display;
|
|
||||||
}
|
|
||||||
final elipses = width <= 2 ? 1 : 3;
|
|
||||||
final partLength = (width - elipses) ~/ 2;
|
|
||||||
// ignore: lines_longer_than_80_chars
|
|
||||||
return '${display.substring(0, partLength)}${'.' * elipses}${display.substring(display.length - partLength)}';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns a double as a percentage to the given [precision]
|
|
||||||
/// e.g. 0.11 becomes 11% if [precision] is 0.
|
|
||||||
String percentage(double progress, int precision) =>
|
|
||||||
'${(progress * 100).toStringAsFixed(precision)}%';
|
|
||||||
|
|
||||||
/// returns the the number of [bytes] in a human readable
|
|
||||||
/// form. e.g. 1e+5 3.000T, 10.00G, 100.0M, 20.00K, 10B
|
|
||||||
///
|
|
||||||
/// Except for absurdly large no. (> 10^20)
|
|
||||||
/// the return is guarenteed to be 6 characters long.
|
|
||||||
/// For no. < 1000K we right pad the no. with spaces.
|
|
||||||
String bytesAsReadable(int bytes, {bool pad = true}) {
|
|
||||||
String human;
|
|
||||||
|
|
||||||
if (bytes < 1000) {
|
|
||||||
human = _fiveDigits(bytes, 0, 'B', pad: pad);
|
|
||||||
} else if (bytes < 1000000) {
|
|
||||||
human = _fiveDigits(bytes, 3, 'K', pad: pad);
|
|
||||||
} else if (bytes < 1000000000) {
|
|
||||||
human = _fiveDigits(bytes, 6, 'M', pad: pad);
|
|
||||||
} else if (bytes < 1000000000000) {
|
|
||||||
human = _fiveDigits(bytes, 9, 'G', pad: pad);
|
|
||||||
} else if (bytes < 1000000000000000) {
|
|
||||||
human = _fiveDigits(bytes, 12, 'T', pad: pad);
|
|
||||||
} else {
|
|
||||||
human = bytes.toStringAsExponential(0);
|
|
||||||
}
|
|
||||||
return human;
|
|
||||||
}
|
|
||||||
|
|
||||||
String _fiveDigits(int bytes, int exponent, String letter,
|
|
||||||
{bool pad = true}) {
|
|
||||||
final num result;
|
|
||||||
String human;
|
|
||||||
if (bytes < 1000) {
|
|
||||||
// less than 1K we display integers only
|
|
||||||
result = bytes ~/ pow(10, exponent);
|
|
||||||
human = '$result'.padLeft(pad ? 5 : 0);
|
|
||||||
} else {
|
|
||||||
// greater than 1K we display decimals
|
|
||||||
result = bytes / pow(10, exponent);
|
|
||||||
human = '$result';
|
|
||||||
|
|
||||||
if (human.length > 5) {
|
|
||||||
human = human.substring(0, 5);
|
|
||||||
} else {
|
|
||||||
/// add trailing zeros to maintain a fixed width of 5 chars.
|
|
||||||
if (pad) {
|
|
||||||
human = human.padRight(5, '0');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '$human$letter';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ///
|
|
||||||
// void colprint(String label, String value, {int pad = 25}) {
|
|
||||||
// print('${label.padRight(pad)}: ${value}');
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Used by [Format.row] to control the alignment of each
|
|
||||||
/// column in the table.
|
|
||||||
enum TableAlignment {
|
|
||||||
///
|
|
||||||
left,
|
|
||||||
|
|
||||||
///
|
|
||||||
right,
|
|
||||||
|
|
||||||
///
|
|
||||||
middle
|
|
||||||
}
|
|
|
@ -1,245 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:dart_console/dart_console.dart';
|
|
||||||
|
|
||||||
import 'ansi.dart';
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Modes available when clearing a screen or line.
|
|
||||||
///
|
|
||||||
/// When used with clearScreen:
|
|
||||||
/// [all] - clears the entire screen
|
|
||||||
/// [fromCursor] - clears from the cursor until the end of the screen
|
|
||||||
/// [toCursor] - clears from the start of the screen to the cursor.
|
|
||||||
///
|
|
||||||
/// When used with clearLine:
|
|
||||||
/// [all] - clears the entire line
|
|
||||||
/// [fromCursor] - clears from the cursor until the end of the line.
|
|
||||||
/// [toCursor] - clears from the start of the line to the cursor.
|
|
||||||
///
|
|
||||||
enum TerminalClearMode {
|
|
||||||
// scrollback,
|
|
||||||
/// clear whole screen
|
|
||||||
all,
|
|
||||||
|
|
||||||
/// clear screen from the cursor to the bottom of the screen.
|
|
||||||
fromCursor,
|
|
||||||
|
|
||||||
/// clear screen from the top of the screen to the cursor
|
|
||||||
toCursor
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Provides access to the Ansi Terminal.
|
|
||||||
class Terminal {
|
|
||||||
/// Factory ctor to get a Terminal
|
|
||||||
factory Terminal() => _self;
|
|
||||||
|
|
||||||
// ignore: flutter_style_todos
|
|
||||||
/// TODO(bsutton): if we don't have a terminal or ansi isn't supported
|
|
||||||
/// we need to suppress any ansi codes being output.
|
|
||||||
|
|
||||||
Terminal._internal();
|
|
||||||
|
|
||||||
static final _self = Terminal._internal();
|
|
||||||
|
|
||||||
final _console = Console();
|
|
||||||
|
|
||||||
/// Returns true if ansi escape characters are supported.
|
|
||||||
bool get isAnsi => Ansi.isSupported;
|
|
||||||
|
|
||||||
/// Clears the screen.
|
|
||||||
/// If ansi escape sequences are not supported this is a no op.
|
|
||||||
/// This call does not update the cursor position so in most
|
|
||||||
/// cases you will want to call [home] after calling [clearScreen].
|
|
||||||
/// ```dart
|
|
||||||
/// Terminal()
|
|
||||||
/// ..clearScreen()
|
|
||||||
/// ..home();
|
|
||||||
/// ```
|
|
||||||
void clearScreen({TerminalClearMode mode = TerminalClearMode.all}) {
|
|
||||||
switch (mode) {
|
|
||||||
// case AnsiClearMode.scrollback:
|
|
||||||
// write('${esc}3J', newline: false);
|
|
||||||
// break;
|
|
||||||
|
|
||||||
case TerminalClearMode.all:
|
|
||||||
_console.clearScreen();
|
|
||||||
break;
|
|
||||||
case TerminalClearMode.fromCursor:
|
|
||||||
write('${Ansi.esc}0Jm');
|
|
||||||
break;
|
|
||||||
case TerminalClearMode.toCursor:
|
|
||||||
write('${Ansi.esc}1Jm');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clears the current line, moves the cursor to column 0
|
|
||||||
/// and then prints [text] effectively overwriting the current
|
|
||||||
/// console line.
|
|
||||||
/// If the current console doesn't support ansi escape
|
|
||||||
/// sequences ([isAnsi] == false) then this call
|
|
||||||
/// will simply revert to calling [print].
|
|
||||||
void overwriteLine(String text) {
|
|
||||||
clearLine();
|
|
||||||
column = 0;
|
|
||||||
_console.write(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes [text] to the terminal at the current
|
|
||||||
/// cursor location without appending a newline character.
|
|
||||||
void write(String text) {
|
|
||||||
_console.write(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes [text] to the console followed by a newline.
|
|
||||||
/// You can control the alignment of [text] by passing the optional
|
|
||||||
/// [alignment] argument which defaults to left alignment.
|
|
||||||
/// The alignment is based on the current terminals width with
|
|
||||||
/// spaces inserted to the left of the string to facilitate the alignment.
|
|
||||||
/// Make certain the current line is clear and the cursor is at column 0
|
|
||||||
/// before calling this method otherwise the alignment will not work
|
|
||||||
/// as expected.
|
|
||||||
void writeLine(String text, {TextAlignment alignment = TextAlignment.left}) =>
|
|
||||||
_console.writeLine(text, alignment);
|
|
||||||
|
|
||||||
/// Clears the current console line without moving the cursor.
|
|
||||||
/// If you want to write over the current line then
|
|
||||||
/// call [clearLine] followed by [startOfLine] and then
|
|
||||||
/// use [write] rather than print as it will leave
|
|
||||||
/// the cursor on the current line.
|
|
||||||
/// Alternatively use [overwriteLine];
|
|
||||||
void clearLine({TerminalClearMode mode = TerminalClearMode.all}) {
|
|
||||||
switch (mode) {
|
|
||||||
// case AnsiClearMode.scrollback:
|
|
||||||
case TerminalClearMode.all:
|
|
||||||
_console.eraseLine();
|
|
||||||
break;
|
|
||||||
case TerminalClearMode.fromCursor:
|
|
||||||
_console.eraseCursorToEnd();
|
|
||||||
break;
|
|
||||||
case TerminalClearMode.toCursor:
|
|
||||||
write('${Ansi.esc}1K');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// show/hide the cursor
|
|
||||||
void showCursor({required bool show}) {
|
|
||||||
if (show) {
|
|
||||||
_console.showCursor();
|
|
||||||
} else {
|
|
||||||
_console.hideCursor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the cursor to the start of previous line.
|
|
||||||
@Deprecated('Use [cursorUp]')
|
|
||||||
static void previousLine() {
|
|
||||||
Terminal().cursorUp();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the cursor up one row
|
|
||||||
void cursorUp() => _console.cursorUp();
|
|
||||||
|
|
||||||
/// Moves the cursor down one row
|
|
||||||
void cursorDown() => _console.cursorDown();
|
|
||||||
|
|
||||||
/// Moves the cursor to the left one column
|
|
||||||
void cursorLeft() => _console.cursorUp();
|
|
||||||
|
|
||||||
/// Moves the cursor to the right one column
|
|
||||||
void cursorRight() => _console.cursorRight();
|
|
||||||
|
|
||||||
/// Returns the column location of the cursor
|
|
||||||
int get column => _cursor?.col ?? 0;
|
|
||||||
|
|
||||||
/// moves the cursor to the given column
|
|
||||||
/// 0 is the first column
|
|
||||||
// ignore: avoid_setters_without_getters
|
|
||||||
set column(int column) {
|
|
||||||
_console.cursorPosition = Coordinate(row, column);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the cursor to the start of line.
|
|
||||||
void startOfLine() {
|
|
||||||
column = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The width of the terminal in columns.
|
|
||||||
/// Where a column is one character wide.
|
|
||||||
/// If no terminal is attached, a value of 80 is returned.
|
|
||||||
/// This value can change if the user resizes the console window.
|
|
||||||
int get columns {
|
|
||||||
if (hasTerminal) {
|
|
||||||
return _console.windowWidth;
|
|
||||||
} else {
|
|
||||||
return 80;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the row location of the cursor.
|
|
||||||
/// The first row is row 0.
|
|
||||||
int get row => _cursor?.row ?? 24;
|
|
||||||
|
|
||||||
/// moves the cursor to the given row
|
|
||||||
/// 0 is the first row
|
|
||||||
set row(int row) {
|
|
||||||
_console.cursorPosition = Coordinate(row, column);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the current co-ordinates of the cursor.
|
|
||||||
/// If no terminal is attached we return null
|
|
||||||
/// as attempting to read from from stdin to
|
|
||||||
/// obtain the cursor will hang the app.
|
|
||||||
Coordinate? get _cursor {
|
|
||||||
if (hasTerminal && Ansi.isSupported) {
|
|
||||||
try {
|
|
||||||
return _console.cursorPosition;
|
|
||||||
// ignore: avoid_catching_errors
|
|
||||||
} on RangeError catch (_) {
|
|
||||||
// if stdin is closed (seems to be within docker)
|
|
||||||
// then the call to cursorPosition will fail.
|
|
||||||
// RangeError: Invalid value: Not in inclusive range 0..1114111: -1
|
|
||||||
// new String.fromCharCode (dart:core-patch/string_patch.dart:45)
|
|
||||||
// Console.cursorPosition (package:dart_console/src/console.dart:304)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether a terminal is attached to stdin.
|
|
||||||
bool get hasTerminal => stdin.hasTerminal;
|
|
||||||
|
|
||||||
/// The height of the terminal in rows.
|
|
||||||
/// Where a row is one character high.
|
|
||||||
/// If no terminal is attached to stdout, a [StdoutException] is thrown.
|
|
||||||
/// This value can change if the users resizes the console window.
|
|
||||||
int get rows {
|
|
||||||
if (hasTerminal) {
|
|
||||||
return _console.windowHeight;
|
|
||||||
} else {
|
|
||||||
return 24;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the cursor to the top left corner
|
|
||||||
/// of the screen (0,0)
|
|
||||||
void home() => _console.resetCursorPosition();
|
|
||||||
|
|
||||||
/// Returns the current console height in rows.
|
|
||||||
@Deprecated('Use rows')
|
|
||||||
int get lines => rows;
|
|
||||||
}
|
|
|
@ -1,327 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
/// A [AsyncCircularBuffer] with a fixed capacity supporting
|
|
||||||
/// all [List] operations
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// final buffer = CircularBuffer<int>(3)..add(1)..add(2);
|
|
||||||
/// print(buffer.length); // 2
|
|
||||||
/// print(buffer.first); // 1
|
|
||||||
/// print(buffer.isFilled); // false
|
|
||||||
/// print(buffer.isUnfilled); // true
|
|
||||||
///
|
|
||||||
/// buffer.add(3);
|
|
||||||
/// print(buffer.length); // 3
|
|
||||||
/// print(buffer.isFilled); // true
|
|
||||||
/// print(buffer.isUnfilled); // false
|
|
||||||
///
|
|
||||||
/// buffer.add(4);
|
|
||||||
/// print(buffer.first); // 4
|
|
||||||
/// ```
|
|
||||||
class AsyncCircularBuffer<T>
|
|
||||||
// with IterableMixin<Future<T>>
|
|
||||||
// implements Iterable<Future<T>>
|
|
||||||
// with ListMixin<T>
|
|
||||||
{
|
|
||||||
/// Creates a [AsyncCircularBuffer] with a `capacity`
|
|
||||||
AsyncCircularBuffer(int capacity)
|
|
||||||
: assert(capacity > 1, 'capacity must be at least 1'),
|
|
||||||
_capacity = capacity,
|
|
||||||
_buf = [],
|
|
||||||
_end = -1,
|
|
||||||
_count = 0,
|
|
||||||
_threshold = max(1, (0.2 * capacity).toInt());
|
|
||||||
|
|
||||||
/// Creates a [AsyncCircularBuffer] based on another `list`
|
|
||||||
AsyncCircularBuffer.of(List<T> list, [int? capacity])
|
|
||||||
: assert(capacity == null || capacity >= list.length,
|
|
||||||
'capacity must be null or greater than list'),
|
|
||||||
_capacity = capacity ?? list.length,
|
|
||||||
_buf = [...list],
|
|
||||||
_end = list.length - 1,
|
|
||||||
_count = list.length,
|
|
||||||
_threshold = max(1, 0.2 * (capacity ?? list.length) as int);
|
|
||||||
|
|
||||||
final List<T> _buf;
|
|
||||||
final int _capacity;
|
|
||||||
|
|
||||||
final int _threshold;
|
|
||||||
|
|
||||||
/// Space is available in the buffer.
|
|
||||||
/// Each time the buffer fills we won't mark
|
|
||||||
/// space available until the buffer has
|
|
||||||
/// bee read down to the [_threshold].
|
|
||||||
var _spaceAvailable = Completer<bool>();
|
|
||||||
|
|
||||||
/// Elements are available in the buffer.
|
|
||||||
var _elementsAvailable = Completer<bool>();
|
|
||||||
|
|
||||||
/// indicates that the buffer has been closed by the provider.
|
|
||||||
/// We complete the future once the buffer closes.
|
|
||||||
final _closed = Completer<bool>();
|
|
||||||
|
|
||||||
/// indicates that the buffer has been closed and
|
|
||||||
/// all elements read.
|
|
||||||
final _done = Completer<bool>();
|
|
||||||
|
|
||||||
int _start = 0;
|
|
||||||
int _end;
|
|
||||||
int _count;
|
|
||||||
|
|
||||||
/// Completes when the buffer has been closed
|
|
||||||
/// and all elements read.
|
|
||||||
Future<bool> get isDone => _done.future;
|
|
||||||
|
|
||||||
/// The [AsyncCircularBuffer] is `reset`
|
|
||||||
void reset() {
|
|
||||||
if (_closed.isCompleted) {
|
|
||||||
throw BadStateException('Buffer has been closed');
|
|
||||||
}
|
|
||||||
_start = 0;
|
|
||||||
_end = -1;
|
|
||||||
_count = 0;
|
|
||||||
|
|
||||||
_spaceAvailable.complete(true);
|
|
||||||
_elementsAvailable = Completer<bool>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Close the circular buffer indicating no more values will be added.
|
|
||||||
/// Calls to get will throw with an underflow exception if called
|
|
||||||
/// when there are no more elements in the buffer and [close] has bee
|
|
||||||
/// called.
|
|
||||||
void close() {
|
|
||||||
if (!_closed.isCompleted) {
|
|
||||||
_closed.complete(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isEmpty && !_done.isCompleted) {
|
|
||||||
_done.complete(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds [element] to the buffer.
|
|
||||||
/// This method will wait if the buffer is full.
|
|
||||||
/// An [BadStateException] will be thrown if the buffer
|
|
||||||
/// has been closed.
|
|
||||||
Future<void> add(T element) async {
|
|
||||||
if (_closed.isCompleted) {
|
|
||||||
throw BadStateException('Buffer has been closed');
|
|
||||||
}
|
|
||||||
if (isFilled) {
|
|
||||||
/// wait until we have more space available.
|
|
||||||
/// If the buffer was closed after we start waiting
|
|
||||||
/// we still allow this last add to continue.
|
|
||||||
await _spaceAvailable.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adding the next value
|
|
||||||
_end++;
|
|
||||||
if (_end == _capacity) {
|
|
||||||
_end = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_buf.length == _capacity) {
|
|
||||||
/// [_buf] is full grown so add at [_end]
|
|
||||||
_buf[_end] = element;
|
|
||||||
} else {
|
|
||||||
/// [_buf] isn't full grown yet so grow it.
|
|
||||||
_buf.add(element);
|
|
||||||
}
|
|
||||||
_count++;
|
|
||||||
|
|
||||||
if (isFilled) {
|
|
||||||
/// we are full so block add
|
|
||||||
_spaceAvailable = Completer<bool>();
|
|
||||||
}
|
|
||||||
// _incStart();
|
|
||||||
if (!_elementsAvailable.isCompleted) {
|
|
||||||
/// we now have elements available.
|
|
||||||
_elementsAvailable.complete(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _incStart() {
|
|
||||||
_start++;
|
|
||||||
if (_start == _capacity) {
|
|
||||||
_start = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the next element in the buffer.
|
|
||||||
/// If the buffer is closed and empty then returns null.
|
|
||||||
/// If the buffer is empty [get] waits until a new
|
|
||||||
/// element arrives before returning.
|
|
||||||
Future<T> get() async {
|
|
||||||
if (_isEmpty) {
|
|
||||||
if (_closed.isCompleted) {
|
|
||||||
return throw UnderflowException();
|
|
||||||
} else {
|
|
||||||
await Future.any<bool>([_elementsAvailable.future, _closed.future]);
|
|
||||||
if (_closed.isCompleted && _isEmpty) {
|
|
||||||
return throw UnderflowException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final element = this[0];
|
|
||||||
_incStart();
|
|
||||||
_count--;
|
|
||||||
|
|
||||||
/// we have less items than threshold so allow more
|
|
||||||
/// items to be added.
|
|
||||||
if (length < _threshold && !_spaceAvailable.isCompleted) {
|
|
||||||
_spaceAvailable.complete(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isEmpty) {
|
|
||||||
/// no elements available so block futher gets.
|
|
||||||
_elementsAvailable = Completer<bool>();
|
|
||||||
|
|
||||||
/// we are closed and empty.
|
|
||||||
if (_closed.isCompleted) {
|
|
||||||
_done.complete(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// iterator over the list of elements.
|
|
||||||
Iterator<Future<T>> get iterator => _CircularBufferIterator(this);
|
|
||||||
|
|
||||||
/// Number of elements of [AsyncCircularBuffer]
|
|
||||||
int get length => _count;
|
|
||||||
|
|
||||||
/// Maximum number of elements of [AsyncCircularBuffer]
|
|
||||||
int get capacity => _capacity;
|
|
||||||
|
|
||||||
/// The [AsyncCircularBuffer] `isFilled` if the `length`
|
|
||||||
/// is equal to the `capacity`
|
|
||||||
bool get isFilled => _count == _capacity;
|
|
||||||
|
|
||||||
/// The [AsyncCircularBuffer] `isUnfilled` if the `length` is
|
|
||||||
/// is less than the `capacity`
|
|
||||||
bool get isUnfilled => _count < _capacity;
|
|
||||||
|
|
||||||
/// True if the buffer is closed and will not
|
|
||||||
/// accept any more calls to [add]
|
|
||||||
bool get isClosed => _closed.isCompleted;
|
|
||||||
|
|
||||||
bool get _isEmpty => _count == 0;
|
|
||||||
|
|
||||||
bool get _isNotEmpty => _count > 0;
|
|
||||||
|
|
||||||
/// Access element at [index]
|
|
||||||
T operator [](int index) {
|
|
||||||
if (index >= 0 && index < _count) {
|
|
||||||
return _buf[(_start + index) % _buf.length]!;
|
|
||||||
}
|
|
||||||
throw RangeError.index(index, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Assign an element at [index]
|
|
||||||
void operator []=(int index, T value) {
|
|
||||||
if (index >= 0 && index < _count) {
|
|
||||||
_buf[(_start + index) % _buf.length] = value;
|
|
||||||
} else {
|
|
||||||
throw RangeError.index(index, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a stream of the contained elements.
|
|
||||||
Stream<T> stream() async* {
|
|
||||||
try {
|
|
||||||
while (!_closed.isCompleted) {
|
|
||||||
final element = await get();
|
|
||||||
yield element;
|
|
||||||
}
|
|
||||||
} on UnderflowException catch (_) {
|
|
||||||
// if we are closed whilst waiting for get we get an [UnderFlowException]
|
|
||||||
// Nothing to do here as the stream will just end naturally.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The `length` mutation is forbidden
|
|
||||||
set length(int newLength) {
|
|
||||||
throw UnsupportedError('Cannot resize immutable CircularBuffer.');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
final sb = StringBuffer()..write('[');
|
|
||||||
for (var i = 0; i < length; i++) {
|
|
||||||
if (sb.length != 1) {
|
|
||||||
sb.write(',');
|
|
||||||
}
|
|
||||||
sb.write('${this[i]}');
|
|
||||||
}
|
|
||||||
sb.write(']');
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// empties the buffer, discarding all elements.
|
|
||||||
Future<void> drain() async {
|
|
||||||
while (_isNotEmpty) {
|
|
||||||
await get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CircularBufferIterator<T> implements Iterator<Future<T>> {
|
|
||||||
///
|
|
||||||
_CircularBufferIterator(this._buffer);
|
|
||||||
// Iterate over odd numbers
|
|
||||||
final AsyncCircularBuffer<T> _buffer;
|
|
||||||
|
|
||||||
///
|
|
||||||
@override
|
|
||||||
bool moveNext() => !(_buffer._closed.isCompleted && _buffer._isEmpty);
|
|
||||||
|
|
||||||
/// will throw an [UnderflowException] if the buffer is
|
|
||||||
/// empty and closed.
|
|
||||||
@override
|
|
||||||
Future<T> get current async {
|
|
||||||
final element = await _buffer.get();
|
|
||||||
|
|
||||||
if (element == null) {
|
|
||||||
throw UnderflowException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An attempt was made to access the buffer when it was closed.
|
|
||||||
class BadStateException implements Exception {
|
|
||||||
/// An attempt was made to access the buffer when it was closed.
|
|
||||||
BadStateException(this.message);
|
|
||||||
|
|
||||||
/// The message.
|
|
||||||
String message;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => message;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An attempt was made to access the buffer when
|
|
||||||
/// it was closed and empty.
|
|
||||||
class UnderflowException implements Exception {
|
|
||||||
/// An attempt was made to access the buffer when
|
|
||||||
/// it was closed and empty.
|
|
||||||
UnderflowException();
|
|
||||||
|
|
||||||
/// the error message.
|
|
||||||
String get message => 'The buffer is closed and empty';
|
|
||||||
@override
|
|
||||||
String toString() => message;
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:stack_trace/stack_trace.dart';
|
|
||||||
|
|
||||||
/// Base class for all DCli exceptions.
|
|
||||||
class DCliException implements Exception {
|
|
||||||
///
|
|
||||||
DCliException(this.message, [Trace? stackTrace])
|
|
||||||
: cause = null,
|
|
||||||
stackTrace = stackTrace ?? Trace.current(2);
|
|
||||||
|
|
||||||
// Factory method to create DCliException from a JSON string
|
|
||||||
factory DCliException.fromJson(String jsonStr) {
|
|
||||||
final jsonMap = jsonDecode(jsonStr) as Map<String, dynamic>;
|
|
||||||
|
|
||||||
return DCliException._(
|
|
||||||
jsonMap['message'] as String,
|
|
||||||
jsonMap['cause'] as String,
|
|
||||||
Trace.parse(jsonMap['stackTrace'] as String),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DCliException._(this.message, this.cause, [Trace? stackTrace])
|
|
||||||
: stackTrace = stackTrace ?? Trace.current(2);
|
|
||||||
|
|
||||||
///
|
|
||||||
DCliException.from(this.cause, this.stackTrace) : message = cause.toString();
|
|
||||||
|
|
||||||
///
|
|
||||||
DCliException.fromException(this.cause)
|
|
||||||
: message = cause.toString(),
|
|
||||||
stackTrace = Trace.current(2);
|
|
||||||
|
|
||||||
///
|
|
||||||
final String message;
|
|
||||||
|
|
||||||
/// If DCliException is wrapping another exception then this is the
|
|
||||||
/// exeception that is wrapped.
|
|
||||||
final Object? cause;
|
|
||||||
|
|
||||||
///
|
|
||||||
Trace stackTrace;
|
|
||||||
|
|
||||||
// {
|
|
||||||
// return DCliException(this.message, stackTrace);
|
|
||||||
// }
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => message;
|
|
||||||
|
|
||||||
///
|
|
||||||
void printStackTrace() {
|
|
||||||
print(stackTrace.terse);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
'message': message,
|
|
||||||
'cause': cause?.toString(),
|
|
||||||
'stackTrace': stackTrace.toString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Method to convert DCliException to a JSON string
|
|
||||||
String toJsonString() => jsonEncode(toJson());
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:scope/scope.dart';
|
|
||||||
|
|
||||||
/// [DCliPlatform] exists so we can scope
|
|
||||||
/// Platform in unit tests to return non-standard results
|
|
||||||
/// e.g. isWindows == true on a linux platform
|
|
||||||
class DCliPlatform {
|
|
||||||
/// Returns a singleton providing
|
|
||||||
/// access to DCli settings.
|
|
||||||
factory DCliPlatform() {
|
|
||||||
if (Scope.hasScopeKey(scopeKey)) {
|
|
||||||
return Scope.use(scopeKey);
|
|
||||||
} else {
|
|
||||||
return _self ??= DCliPlatform._internal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// To use this method create a [Scope] and inject this
|
|
||||||
/// as a value into the scope.
|
|
||||||
factory DCliPlatform.forScope({DCliPlatformOS? overriddenPlatform}) =>
|
|
||||||
DCliPlatform._internal(overriddenPlatform: overriddenPlatform);
|
|
||||||
|
|
||||||
DCliPlatform._internal({this.overriddenPlatform});
|
|
||||||
|
|
||||||
static ScopeKey<DCliPlatform> scopeKey = ScopeKey<DCliPlatform>();
|
|
||||||
|
|
||||||
static DCliPlatform? _self;
|
|
||||||
|
|
||||||
DCliPlatformOS? overriddenPlatform;
|
|
||||||
|
|
||||||
/// True if you are running on a Mac.
|
|
||||||
bool get isMacOS => overriddenPlatform == null
|
|
||||||
? Platform.isMacOS
|
|
||||||
: overriddenPlatform == DCliPlatformOS.macos;
|
|
||||||
|
|
||||||
/// True if you are running on a Linux system.
|
|
||||||
bool get isLinux => overriddenPlatform == null
|
|
||||||
? Platform.isLinux
|
|
||||||
: overriddenPlatform == DCliPlatformOS.linux;
|
|
||||||
|
|
||||||
/// True if you are running on a Window system.
|
|
||||||
bool get isWindows => overriddenPlatform == null
|
|
||||||
? Platform.isWindows
|
|
||||||
: overriddenPlatform == DCliPlatformOS.windows;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DCliPlatformOS { windows, linux, macos }
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
///
|
|
||||||
/// devNull is a convenience function which you can use
|
|
||||||
/// if you want to ignore the output of a LineAction.
|
|
||||||
/// Its typical useage is a forEach where you don't want
|
|
||||||
/// to see any stdout but you still want to see errors
|
|
||||||
/// printed to stderr.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// 'git pull'.forEach(devNull, stderr: (line) => printerr(line));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// use this to consume the output.
|
|
||||||
void devNull(String? line) {}
|
|
|
@ -1,232 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
/// Opens a File and calls [action] passing in the open file.
|
|
||||||
/// When action completes the file is closed.
|
|
||||||
/// Use this method in preference to directly callling [FileSync()]
|
|
||||||
Future<R> withOpenFile<R>(
|
|
||||||
String pathToFile,
|
|
||||||
R Function(RandomAccessFile) action, {
|
|
||||||
FileMode fileMode = FileMode.writeOnlyAppend,
|
|
||||||
}) async {
|
|
||||||
final raf = File(pathToFile).openSync(mode: fileMode);
|
|
||||||
|
|
||||||
R result;
|
|
||||||
try {
|
|
||||||
result = action(raf);
|
|
||||||
} finally {
|
|
||||||
await raf.close();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Creates a link at [linkPath] which points to an
|
|
||||||
/// existing file or directory at [existingPath]
|
|
||||||
///
|
|
||||||
/// On Windows you need to be in developer mode or running as an Administrator
|
|
||||||
/// to create a symlink.
|
|
||||||
///
|
|
||||||
/// To enable developer mode see:
|
|
||||||
/// https://dcli.onepub.dev/getting-started/installing-on-windows
|
|
||||||
///
|
|
||||||
/// To check if your script is running as an administrator use:
|
|
||||||
///
|
|
||||||
/// [Shell.current.isPrivileged]
|
|
||||||
///
|
|
||||||
void symlink(
|
|
||||||
String existingPath,
|
|
||||||
String linkPath,
|
|
||||||
) {
|
|
||||||
verbose(() => 'symlink existingPath: $existingPath linkPath $linkPath');
|
|
||||||
Link(linkPath).createSync(existingPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Deletes the symlink at [linkPath]
|
|
||||||
///
|
|
||||||
/// On Windows you need to be in developer mode or running as an Administrator
|
|
||||||
/// to delete a symlink.
|
|
||||||
///
|
|
||||||
/// To enable developer mode see:
|
|
||||||
/// https://dcli.onepub.dev/getting-started/installing-on-windows
|
|
||||||
///
|
|
||||||
/// To check if your script is running as an administrator use:
|
|
||||||
///
|
|
||||||
/// [Shell.current.isPrivileged]
|
|
||||||
///
|
|
||||||
void deleteSymlink(String linkPath) {
|
|
||||||
verbose(() => 'deleteSymlink linkPath: $linkPath');
|
|
||||||
Link(linkPath).deleteSync();
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Resolves the a symbolic link [pathToLink]
|
|
||||||
/// to the ultimate target path.
|
|
||||||
///
|
|
||||||
/// The return path will be canonicalized.
|
|
||||||
///
|
|
||||||
/// e.g.
|
|
||||||
/// ```dart
|
|
||||||
/// resolveSymLink('/usr/bin/dart) == '/usr/lib/bin/dart'
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// throws a FileSystemException if the target path does not exist.
|
|
||||||
String resolveSymLink(String pathToLink) {
|
|
||||||
final normalised = canonicalize(pathToLink);
|
|
||||||
|
|
||||||
String resolved;
|
|
||||||
if (isDirectory(normalised)) {
|
|
||||||
resolved = Directory(normalised).resolveSymbolicLinksSync();
|
|
||||||
} else {
|
|
||||||
resolved = canonicalize(File(normalised).resolveSymbolicLinksSync());
|
|
||||||
}
|
|
||||||
|
|
||||||
verbose(() => 'resolveSymLink $pathToLink resolved: $resolved');
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// Returns a FileStat instance describing the
|
|
||||||
/// file or directory located by [path].
|
|
||||||
///
|
|
||||||
FileStat stat(String path) => File(path).statSync();
|
|
||||||
|
|
||||||
/// Generates a temporary filename in [pathToTempDir]
|
|
||||||
/// or if inTempDir os not passed then in
|
|
||||||
/// the system temp directory.
|
|
||||||
/// The generated filename is is guaranteed to be globally unique.
|
|
||||||
///
|
|
||||||
/// This method does NOT create the file.
|
|
||||||
///
|
|
||||||
/// The temp file name will be <uuid>.tmp
|
|
||||||
/// unless you provide a [suffix] in which
|
|
||||||
/// case the file name will be <uuid>.<suffix>
|
|
||||||
String createTempFilename({String? suffix, String? pathToTempDir}) {
|
|
||||||
var finalsuffix = suffix ?? 'tmp';
|
|
||||||
|
|
||||||
if (!finalsuffix.startsWith('.')) {
|
|
||||||
finalsuffix = '.$finalsuffix';
|
|
||||||
}
|
|
||||||
pathToTempDir ??= Directory.systemTemp.path;
|
|
||||||
const uuid = Uuid();
|
|
||||||
return '${join(pathToTempDir, uuid.v4())}$finalsuffix';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a temporary filename in the system temp directory
|
|
||||||
/// that is guaranteed to be unique.
|
|
||||||
///
|
|
||||||
/// This method does not create the file.
|
|
||||||
///
|
|
||||||
/// The temp file name will be <uuid>.tmp
|
|
||||||
/// unless you provide a [suffix] in which
|
|
||||||
/// case the file name will be <uuid>.<suffix>
|
|
||||||
String createTempFile({String? suffix}) {
|
|
||||||
final filename = createTempFilename(suffix: suffix);
|
|
||||||
touch(filename, create: true);
|
|
||||||
return filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the length of the file at [pathToFile] in bytes.
|
|
||||||
int fileLength(String pathToFile) => File(pathToFile).lengthSync();
|
|
||||||
|
|
||||||
/// Creates a temp file and then calls [action].
|
|
||||||
///
|
|
||||||
/// Once [action] completes the temporary file will be deleted.
|
|
||||||
///
|
|
||||||
/// The [action]s return value [R] is returned from the [withTempFileAsync]
|
|
||||||
/// function.
|
|
||||||
///
|
|
||||||
/// If [create] is true (default true) then the temp file will be
|
|
||||||
/// created. If [create] is false then just the name will be
|
|
||||||
/// generated.
|
|
||||||
///
|
|
||||||
/// if [pathToTempDir] is passed then the file will be created in that
|
|
||||||
/// directory otherwise the file will be created in the system
|
|
||||||
/// temp directory.
|
|
||||||
///
|
|
||||||
/// The temp file name will be <uuid>.tmp
|
|
||||||
/// unless you provide a [suffix] in which
|
|
||||||
/// case the file name will be <uuid>.<suffix>
|
|
||||||
Future<R> withTempFileAsync<R>(
|
|
||||||
Future<R> Function(String tempFile) action, {
|
|
||||||
String? suffix,
|
|
||||||
String? pathToTempDir,
|
|
||||||
bool create = true,
|
|
||||||
bool keep = false,
|
|
||||||
}) async {
|
|
||||||
final tmp = createTempFilename(suffix: suffix, pathToTempDir: pathToTempDir);
|
|
||||||
if (create) {
|
|
||||||
touch(tmp, create: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
R result;
|
|
||||||
try {
|
|
||||||
result = await action(tmp);
|
|
||||||
} finally {
|
|
||||||
if (exists(tmp) && !keep) {
|
|
||||||
delete(tmp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Digest calculateHash(String path) {
|
|
||||||
if (!exists(path)) {
|
|
||||||
throw FileNotFoundException(path);
|
|
||||||
}
|
|
||||||
final file = File(path);
|
|
||||||
var digest = Digest([0]);
|
|
||||||
|
|
||||||
if (file.lengthSync() == 0) {
|
|
||||||
return digest;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockSize = 8192; // Set the desired block size (e.g., 8 KB)
|
|
||||||
const hasher = sha256;
|
|
||||||
|
|
||||||
final randomAccessFile = file.openSync();
|
|
||||||
final chunk = List.filled(blockSize, 0);
|
|
||||||
int bytesRead;
|
|
||||||
|
|
||||||
while ((bytesRead = randomAccessFile.readIntoSync(chunk)) > 0) {
|
|
||||||
digest = md5.convert([...digest.bytes, ...chunk.sublist(0, bytesRead)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
randomAccessFile.closeSync();
|
|
||||||
|
|
||||||
final digestAsString = hasher.toString();
|
|
||||||
verbose(() => 'calculateHash($path) = $digestAsString');
|
|
||||||
|
|
||||||
return digest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thrown when a file doesn't exist
|
|
||||||
class FileNotFoundException extends DCliException {
|
|
||||||
/// Thrown when a file doesn't exist
|
|
||||||
FileNotFoundException(String path)
|
|
||||||
: super('The file ${truepath(path)} does not exist.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thrown when a path is not a file.
|
|
||||||
class NotAFileException extends DCliException {
|
|
||||||
/// Thrown when a path is not a file.
|
|
||||||
NotAFileException(String path)
|
|
||||||
: super('The path ${truepath(path)} is not a file.');
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
/// A specialized StreamController that limits the no.
|
|
||||||
/// of elements that can be in the stream.
|
|
||||||
class LimitedStreamController<T> implements StreamController<T> {
|
|
||||||
/// Creates a new [LimitedStreamController] that limits the no.
|
|
||||||
/// of elements that can be in the queue.
|
|
||||||
LimitedStreamController(this._limit,
|
|
||||||
{void Function()? onListen, void Function()? onCancel, bool sync = false})
|
|
||||||
: _streamController = StreamController<T>(
|
|
||||||
onListen: onListen, onCancel: onCancel, sync: sync);
|
|
||||||
|
|
||||||
final StreamController<T> _streamController;
|
|
||||||
|
|
||||||
final int _limit;
|
|
||||||
|
|
||||||
/// Tracks the no. of elements in the stream.
|
|
||||||
var _count = 0;
|
|
||||||
|
|
||||||
/// Used to indicate when the stream is full
|
|
||||||
var _spaceAvailable = Completer<bool>();
|
|
||||||
|
|
||||||
/// Returns the no. of elements waiting in the stream.
|
|
||||||
int get length => _count; // _buffer.length;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get isClosed => _streamController.isClosed;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get hasListener => _streamController.hasListener;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get isPaused => _streamController.isPaused;
|
|
||||||
|
|
||||||
@Deprecated('Use asyncAdd')
|
|
||||||
@override
|
|
||||||
void add(T event) {
|
|
||||||
throw UnsupportedError('Use asyncAdd');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add an event to the stream. If the
|
|
||||||
/// stream is full then this method will
|
|
||||||
/// wait until there is room.
|
|
||||||
Future<void> asyncAdd(T event) async {
|
|
||||||
if (_count >= _limit) {
|
|
||||||
await _spaceAvailable.future;
|
|
||||||
}
|
|
||||||
_count++;
|
|
||||||
_streamController.add(event);
|
|
||||||
|
|
||||||
if (_count >= _limit) {
|
|
||||||
_spaceAvailable = Completer<bool>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<T> get stream async* {
|
|
||||||
/// return _buffer.stream();
|
|
||||||
await for (final element in _streamController.stream) {
|
|
||||||
_count--;
|
|
||||||
|
|
||||||
if (_count < _limit && !_spaceAvailable.isCompleted) {
|
|
||||||
/// notify that we have space available
|
|
||||||
_spaceAvailable.complete(true);
|
|
||||||
}
|
|
||||||
yield element;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void addError(Object error, [StackTrace? stackTrace]) {
|
|
||||||
_streamController.addError(error, stackTrace);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> addStream(Stream<T> source, {bool? cancelOnError = true}) {
|
|
||||||
throw UnsupportedError('Use asyncAdd');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<dynamic> close() => _streamController.close();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<dynamic> get done => _streamController.done;
|
|
||||||
|
|
||||||
@override
|
|
||||||
StreamSink<T> get sink => throw UnsupportedError('Use asyncAdd');
|
|
||||||
|
|
||||||
@override
|
|
||||||
set onListen(void Function()? onListenHandler) {
|
|
||||||
_streamController.onListen = onListenHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ControllerCallback? get onListen => _streamController.onListen;
|
|
||||||
|
|
||||||
@override
|
|
||||||
set onPause(void Function()? onPauseHandler) {
|
|
||||||
_streamController.onPause = onPauseHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ControllerCallback? get onPause => _streamController.onPause;
|
|
||||||
|
|
||||||
@override
|
|
||||||
set onResume(void Function()? onResumeHandler) {
|
|
||||||
_streamController.onResume = onResumeHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ControllerCallback? get onResume => _streamController.onResume;
|
|
||||||
|
|
||||||
@override
|
|
||||||
set onCancel(void Function()? onCancelHandler) {
|
|
||||||
_streamController.onCancel = onCancelHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ControllerCancelCallback? get onCancel => _streamController.onCancel;
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/// Typedef for LineActions
|
|
||||||
typedef LineAction = void Function(String line);
|
|
||||||
|
|
||||||
/// Typedef for cancellable LineActions.
|
|
||||||
typedef CancelableLineAction = bool Function(String line);
|
|
|
@ -1,188 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'platform.dart';
|
|
||||||
|
|
||||||
/// Provide s collection of methods to make it easy
|
|
||||||
/// to read/write a file line by line.
|
|
||||||
class LineFile {
|
|
||||||
/// If you instantiate FileSync you MUST call [close].
|
|
||||||
///
|
|
||||||
/// We rececommend that you use withOpenFile in prefernce to directly
|
|
||||||
/// calling this method.
|
|
||||||
LineFile(String path, {FileMode fileMode = FileMode.writeOnlyAppend})
|
|
||||||
: _fileMode = fileMode {
|
|
||||||
_file = File(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
final FileMode _fileMode;
|
|
||||||
late final File _file;
|
|
||||||
late final RandomAccessFile _raf = _open(_fileMode);
|
|
||||||
|
|
||||||
RandomAccessFile _open(FileMode fileMode) => _file.openSync(mode: fileMode);
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Flushes the contents of the file to disk.
|
|
||||||
void flush() => _raf.flushSync();
|
|
||||||
|
|
||||||
/// Returns the length of the file in bytes
|
|
||||||
/// The file does NOT have to be open
|
|
||||||
/// to determine its length.
|
|
||||||
int get length => _file.lengthSync();
|
|
||||||
|
|
||||||
/// Close and flushes the file to disk.
|
|
||||||
void close() => _raf.closeSync();
|
|
||||||
|
|
||||||
/// Read file line by line.
|
|
||||||
void readAll(bool Function(String) handleLine) {
|
|
||||||
final stream = _file.openSync();
|
|
||||||
try {
|
|
||||||
final splitter = const LineSplitter()
|
|
||||||
.startChunkedConversion(_CallbackStringSync((str) {
|
|
||||||
if (!handleLine(str)) {
|
|
||||||
throw const _StopIteration();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
final decoder = const Utf8Decoder().startChunkedConversion(splitter);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
final bytes = stream.readSync(16 * 1024);
|
|
||||||
if (bytes.isEmpty) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
decoder.add(bytes);
|
|
||||||
}
|
|
||||||
decoder.close();
|
|
||||||
} on _StopIteration catch (_) {
|
|
||||||
// Ignore.
|
|
||||||
} finally {
|
|
||||||
stream.closeSync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Truncates the file to zero bytes and
|
|
||||||
/// then writes the given text to the file.
|
|
||||||
/// If [newline] is null or isn't passed then the platform
|
|
||||||
/// end of line characters are appended as defined by
|
|
||||||
/// [Platform().eol].
|
|
||||||
/// Pass null or an '' to [newline] to not add a line terminator.
|
|
||||||
void write(String line, {String? newline}) {
|
|
||||||
final finalline = line + (newline ?? eol);
|
|
||||||
_raf
|
|
||||||
..truncateSync(0)
|
|
||||||
..setPositionSync(0)
|
|
||||||
..writeStringSync(finalline)
|
|
||||||
..flushSync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Appends the [line] to the file
|
|
||||||
/// Appends [newline] after the line.
|
|
||||||
/// If [newline] is null or isn't passed then the platform
|
|
||||||
/// end of line characters are appended as defined by
|
|
||||||
/// [Platform().eol].
|
|
||||||
/// Pass null or an '' to [newline] to not add a line terminator.
|
|
||||||
void append(String line, {String? newline}) {
|
|
||||||
final finalline = line + (newline ?? eol);
|
|
||||||
|
|
||||||
_raf
|
|
||||||
..setPositionSync(_raf.lengthSync())
|
|
||||||
..writeStringSync(finalline);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads a single line from the file.
|
|
||||||
/// [lineDelimiter] the end of line delimiter.
|
|
||||||
/// May be one or two characters long.
|
|
||||||
/// Defaults to the platform specific delimiter as
|
|
||||||
/// defined by [Platform().eol].
|
|
||||||
///
|
|
||||||
String? read({String? lineDelimiter}) {
|
|
||||||
lineDelimiter ??= eol;
|
|
||||||
final line = StringBuffer();
|
|
||||||
int byte;
|
|
||||||
var priorChar = '';
|
|
||||||
|
|
||||||
var foundDelimiter = false;
|
|
||||||
|
|
||||||
while ((byte = _raf.readByteSync()) != -1) {
|
|
||||||
final char = utf8.decode([byte]);
|
|
||||||
|
|
||||||
if (_isLineDelimiter(priorChar, char, lineDelimiter)) {
|
|
||||||
foundDelimiter = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
line.write(char);
|
|
||||||
priorChar = char;
|
|
||||||
}
|
|
||||||
final endOfFile = line.isEmpty && foundDelimiter == false;
|
|
||||||
return endOfFile ? null : line.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Truncates the file to zero bytes in length.
|
|
||||||
void truncate() => _raf.truncateSync(0);
|
|
||||||
|
|
||||||
bool _isLineDelimiter(String priorChar, String char, String lineDelimiter) {
|
|
||||||
if (lineDelimiter.length == 1) {
|
|
||||||
return char == lineDelimiter;
|
|
||||||
} else {
|
|
||||||
return priorChar + char == lineDelimiter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Opens the file for random access.
|
|
||||||
void open() {
|
|
||||||
/// accessing raf causes the file to open.
|
|
||||||
// ignore: unnecessary_statements
|
|
||||||
_raf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Opens a File and calls [action] passing in the open [LineFile].
|
|
||||||
/// When action completes the file is closed.
|
|
||||||
/// Use this method in preference to directly callling [FileSync()]
|
|
||||||
R withOpenLineFile<R>(
|
|
||||||
String pathToFile,
|
|
||||||
R Function(LineFile) action, {
|
|
||||||
FileMode fileMode = FileMode.writeOnlyAppend,
|
|
||||||
}) {
|
|
||||||
final file = LineFile(pathToFile, fileMode: fileMode)..open();
|
|
||||||
|
|
||||||
late R result;
|
|
||||||
try {
|
|
||||||
result = action(file);
|
|
||||||
} finally {
|
|
||||||
file
|
|
||||||
..flush()
|
|
||||||
..close();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StopIteration implements Exception {
|
|
||||||
const _StopIteration();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CallbackStringSync implements Sink<String> {
|
|
||||||
_CallbackStringSync(this.callback);
|
|
||||||
|
|
||||||
final void Function(String) callback;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void add(String data) {
|
|
||||||
callback(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void close() {}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:protevus_console/core.dart';
|
|
||||||
|
|
||||||
/// Extensions for the Platform class
|
|
||||||
extension PlatformEx on Platform {
|
|
||||||
/// Returns the OS specific End Of Line (eol) character.
|
|
||||||
/// On Windows this is '\r\n' on all other platforms
|
|
||||||
/// it is '\n'.
|
|
||||||
/// Usage: Platform().eol
|
|
||||||
///
|
|
||||||
/// Note: you must import both:
|
|
||||||
/// ```dart
|
|
||||||
/// import 'dart:io';
|
|
||||||
/// import 'package:dcli/dcli.dart';
|
|
||||||
/// ```
|
|
||||||
String get eol => DCliPlatform().isWindows ? '\r\n' : '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
String get eol => DCliPlatform().isWindows ? '\r\n' : '\n';
|
|
|
@ -1,101 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:stack_trace/stack_trace.dart';
|
|
||||||
|
|
||||||
import 'dcli_exception.dart';
|
|
||||||
|
|
||||||
/// Thrown when any of the process related method
|
|
||||||
/// such as .run and .start fail.
|
|
||||||
class RunException extends DCliException {
|
|
||||||
///
|
|
||||||
RunException(
|
|
||||||
this.cmdLine,
|
|
||||||
this.exitCode,
|
|
||||||
this.reason, {
|
|
||||||
Trace? stackTrace,
|
|
||||||
}) : super(reason, stackTrace);
|
|
||||||
|
|
||||||
RunException.fromJson(Map<String, dynamic> json)
|
|
||||||
: cmdLine = json['cmdLine'] as String,
|
|
||||||
exitCode = json['exitCode'] as int,
|
|
||||||
reason = json['reason'] as String,
|
|
||||||
super(json['reason'] as String, json['stackTrace'] as Trace);
|
|
||||||
|
|
||||||
factory RunException.fromJsonString(String jsonString) {
|
|
||||||
final jsonMap = jsonDecode(jsonString) as Map<String, dynamic>;
|
|
||||||
return RunException(
|
|
||||||
jsonMap['cmdLine'] as String,
|
|
||||||
jsonMap['exitCode'] as int,
|
|
||||||
jsonMap['reason'] as String,
|
|
||||||
stackTrace: jsonMap['stackTrace'] != null
|
|
||||||
? Trace.parse(jsonMap['stackTrace'] as String)
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
RunException.withArgs(
|
|
||||||
String? cmd,
|
|
||||||
List<String?> args,
|
|
||||||
this.exitCode,
|
|
||||||
this.reason, {
|
|
||||||
Trace? stackTrace,
|
|
||||||
}) : cmdLine = '$cmd ${args.join(' ')}',
|
|
||||||
super(reason, stackTrace);
|
|
||||||
|
|
||||||
///
|
|
||||||
RunException.fromException(
|
|
||||||
Object exception,
|
|
||||||
String? cmd,
|
|
||||||
List<String?> args, {
|
|
||||||
Trace? stackTrace,
|
|
||||||
}) : cmdLine = '$cmd ${args.join(' ')}',
|
|
||||||
reason = exception.toString(),
|
|
||||||
exitCode = -1,
|
|
||||||
super(exception.toString(), stackTrace);
|
|
||||||
|
|
||||||
/// The command line that was being run.
|
|
||||||
String cmdLine;
|
|
||||||
|
|
||||||
/// the exit code of the command.
|
|
||||||
int? exitCode;
|
|
||||||
|
|
||||||
/// the error.
|
|
||||||
String reason;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get message => '''
|
|
||||||
$cmdLine
|
|
||||||
exit: $exitCode
|
|
||||||
reason: $reason''';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJsonMap() => {
|
|
||||||
'cmdLine': cmdLine,
|
|
||||||
'exitCode': exitCode,
|
|
||||||
'reason': reason,
|
|
||||||
'stackTrace': stackTrace,
|
|
||||||
};
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toJsonString() {
|
|
||||||
final jsonMap = <String, dynamic>{
|
|
||||||
'cmdLine': cmdLine,
|
|
||||||
'exitCode': exitCode,
|
|
||||||
'reason': reason,
|
|
||||||
'stackTrace': stackTrace.toString(),
|
|
||||||
};
|
|
||||||
final json = jsonEncode(jsonMap);
|
|
||||||
print(json);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:collection';
|
|
||||||
|
|
||||||
///
|
|
||||||
/// A classic Stack of items with a push and pop method.
|
|
||||||
///
|
|
||||||
class StackList<T> {
|
|
||||||
///
|
|
||||||
StackList();
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Creates a stack from [initialStack]
|
|
||||||
/// by pushing each element of the list
|
|
||||||
/// onto the stack from first to last.
|
|
||||||
StackList.fromList(List<T> initialStack) {
|
|
||||||
initialStack.forEach(push);
|
|
||||||
}
|
|
||||||
|
|
||||||
final _stack = Queue<T>();
|
|
||||||
|
|
||||||
///
|
|
||||||
bool get isEmpty => _stack.isEmpty;
|
|
||||||
|
|
||||||
/// push an [item] onto th stack.
|
|
||||||
void push(T item) {
|
|
||||||
_stack.addFirst(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// return and remove an item from the stack.
|
|
||||||
T pop() => _stack.removeFirst();
|
|
||||||
|
|
||||||
/// returns the item onf the top of the stack
|
|
||||||
/// but does not remove the item.
|
|
||||||
T peek() => _stack.first;
|
|
||||||
|
|
||||||
/// The of items in the stack
|
|
||||||
List<T> asList() => _stack.toList();
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
|
|
||||||
import '../functions/env.dart';
|
|
||||||
import '../functions/pwd.dart';
|
|
||||||
|
|
||||||
/// [truepath] creates an absolute and normalized path.
|
|
||||||
///
|
|
||||||
/// True path provides a safe and consistent manner for
|
|
||||||
/// manipulating, accessing and displaying paths.
|
|
||||||
///
|
|
||||||
/// Works like [join] in that it concatenates a set of directories
|
|
||||||
/// into a path.
|
|
||||||
/// [truepath] then goes on to create an absolute path which
|
|
||||||
/// is then normalize to remove any segments (.. or .).
|
|
||||||
///
|
|
||||||
String truepath(
|
|
||||||
String part1, [
|
|
||||||
String? part2,
|
|
||||||
String? part3,
|
|
||||||
String? part4,
|
|
||||||
String? part5,
|
|
||||||
String? part6,
|
|
||||||
String? part7,
|
|
||||||
]) =>
|
|
||||||
normalize(absolute(join(part1, part2, part3, part4, part5, part6, part7)));
|
|
||||||
|
|
||||||
/// Removes the users home directory from a path replacing it with ~
|
|
||||||
String privatePath(
|
|
||||||
String part1, [
|
|
||||||
String? part2,
|
|
||||||
String? part3,
|
|
||||||
String? part4,
|
|
||||||
String? part5,
|
|
||||||
String? part6,
|
|
||||||
String? part7,
|
|
||||||
]) {
|
|
||||||
final prefix = rootPrefix(HOME);
|
|
||||||
var tp = truepath(part1, part2, part3, part4, part5, part6, part7);
|
|
||||||
if (HOME != '.') {
|
|
||||||
tp = tp.replaceAll(HOME, '$prefix<HOME>');
|
|
||||||
}
|
|
||||||
return tp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the root path of your file system.
|
|
||||||
///
|
|
||||||
/// On Linux and MacOS this will be `/`
|
|
||||||
///
|
|
||||||
/// On Windows this will be `'C:\`
|
|
||||||
///
|
|
||||||
/// The drive letter will depend on the
|
|
||||||
/// drive of your present working directory (pwd).
|
|
||||||
String get rootPath => rootPrefix(pwd);
|
|
|
@ -1,16 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the Protevus Platform.
|
|
||||||
*
|
|
||||||
* (C) Protevus <developers@protevus.com>
|
|
||||||
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
library;
|
|
||||||
|
|
||||||
export 'src/terminal/ansi.dart';
|
|
||||||
export 'src/terminal/ansi_color.dart';
|
|
||||||
export 'src/terminal/format.dart';
|
|
||||||
export 'src/terminal/terminal.dart';
|
|
|
@ -10,19 +10,7 @@ environment:
|
||||||
|
|
||||||
# Add regular dependencies here.
|
# Add regular dependencies here.
|
||||||
dependencies:
|
dependencies:
|
||||||
async: ^2.5.0
|
# path: ^1.8.0
|
||||||
circular_buffer: ^0.11.0
|
|
||||||
collection: ^1.15.0
|
|
||||||
crypto: ^3.0.1
|
|
||||||
dart_console: ^4.0.0
|
|
||||||
ffi: ^2.1.0
|
|
||||||
logging: ^1.0.2
|
|
||||||
meta: ">=1.11.0 <2.0.0"
|
|
||||||
path: ^1.8.0
|
|
||||||
scope: ^4.0.0
|
|
||||||
stack_trace: ^1.10.0
|
|
||||||
uuid: ^4.1.0
|
|
||||||
validators2: ^5.0.0
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
lints: ^3.0.0
|
lints: ^3.0.0
|
||||||
|
|
Loading…
Reference in a new issue