add(dcli): refactoring from dcli_core dcli_terminal dcli_input
This commit is contained in:
parent
59e30aa9be
commit
11bb99a95c
48 changed files with 7081 additions and 2 deletions
|
@ -2,6 +2,7 @@
|
||||||
* This file is part of the Protevus Platform.
|
* This file is part of the Protevus Platform.
|
||||||
*
|
*
|
||||||
* (C) Protevus <developers@protevus.com>
|
* (C) Protevus <developers@protevus.com>
|
||||||
|
* (C) S. Brett Sutton <bsutton@onepub.dev>
|
||||||
*
|
*
|
||||||
* For the full copyright and license information, please view the LICENSE
|
* For the full copyright and license information, please view the LICENSE
|
||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
|
|
55
packages/console/lib/core.dart
Normal file
55
packages/console/lib/core.dart
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
16
packages/console/lib/input.dart
Normal file
16
packages/console/lib/input.dart
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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';
|
321
packages/console/lib/src/functions/backup.dart
Normal file
321
packages/console/lib/src/functions/backup.dart
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
/*
|
||||||
|
* 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');
|
||||||
|
}
|
||||||
|
}
|
45
packages/console/lib/src/functions/cat.dart
Normal file
45
packages/console/lib/src/functions/cat.dart
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* 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]);
|
||||||
|
}
|
96
packages/console/lib/src/functions/copy.dart
Normal file
96
packages/console/lib/src/functions/copy.dart
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
161
packages/console/lib/src/functions/copy_tree.dart
Normal file
161
packages/console/lib/src/functions/copy_tree.dart
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
108
packages/console/lib/src/functions/create_dir.dart
Normal file
108
packages/console/lib/src/functions/create_dir.dart
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
21
packages/console/lib/src/functions/dcli_function.dart
Normal file
21
packages/console/lib/src/functions/dcli_function.dart
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* 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]);
|
||||||
|
}
|
55
packages/console/lib/src/functions/delete.dart
Normal file
55
packages/console/lib/src/functions/delete.dart
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
70
packages/console/lib/src/functions/delete_dir.dart
Normal file
70
packages/console/lib/src/functions/delete_dir.dart
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
135
packages/console/lib/src/functions/delete_tree.dart
Normal file
135
packages/console/lib/src/functions/delete_tree.dart
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
428
packages/console/lib/src/functions/env.dart
Normal file
428
packages/console/lib/src/functions/env.dart
Normal file
|
@ -0,0 +1,428 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
564
packages/console/lib/src/functions/find.dart
Normal file
564
packages/console/lib/src/functions/find.dart
Normal file
|
@ -0,0 +1,564 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
369
packages/console/lib/src/functions/find_async.dart
Normal file
369
packages/console/lib/src/functions/find_async.dart
Normal file
|
@ -0,0 +1,369 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
62
packages/console/lib/src/functions/head.dart
Normal file
62
packages/console/lib/src/functions/head.dart
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
169
packages/console/lib/src/functions/is.dart
Normal file
169
packages/console/lib/src/functions/is.dart
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
90
packages/console/lib/src/functions/move.dart
Normal file
90
packages/console/lib/src/functions/move.dart
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
85
packages/console/lib/src/functions/move_dir.dart
Normal file
85
packages/console/lib/src/functions/move_dir.dart
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
164
packages/console/lib/src/functions/move_tree.dart
Normal file
164
packages/console/lib/src/functions/move_tree.dart
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
28
packages/console/lib/src/functions/pwd.dart
Normal file
28
packages/console/lib/src/functions/pwd.dart
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
92
packages/console/lib/src/functions/replace.dart
Normal file
92
packages/console/lib/src/functions/replace.dart
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
83
packages/console/lib/src/functions/tail.dart
Normal file
83
packages/console/lib/src/functions/tail.dart
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
77
packages/console/lib/src/functions/touch.dart
Normal file
77
packages/console/lib/src/functions/touch.dart
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
196
packages/console/lib/src/functions/which.dart
Normal file
196
packages/console/lib/src/functions/which.dart
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
848
packages/console/lib/src/input/ask.dart
Normal file
848
packages/console/lib/src/input/ask.dart
Normal file
|
@ -0,0 +1,848 @@
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
90
packages/console/lib/src/input/confirm.dart
Normal file
90
packages/console/lib/src/input/confirm.dart
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
32
packages/console/lib/src/input/echo.dart
Normal file
32
packages/console/lib/src/input/echo.dart
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/* 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();
|
||||||
|
}
|
||||||
|
}
|
185
packages/console/lib/src/input/menu.dart
Normal file
185
packages/console/lib/src/input/menu.dart
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
/* 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;
|
||||||
|
}
|
112
packages/console/lib/src/settings.dart
Normal file
112
packages/console/lib/src/settings.dart
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* 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]);
|
||||||
|
}
|
||||||
|
}
|
75
packages/console/lib/src/terminal/ansi.dart
Normal file
75
packages/console/lib/src/terminal/ansi.dart
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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'), '');
|
||||||
|
}
|
496
packages/console/lib/src/terminal/ansi_color.dart
Normal file
496
packages/console/lib/src/terminal/ansi_color.dart
Normal file
|
@ -0,0 +1,496 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
190
packages/console/lib/src/terminal/format.dart
Normal file
190
packages/console/lib/src/terminal/format.dart
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
245
packages/console/lib/src/terminal/terminal.dart
Normal file
245
packages/console/lib/src/terminal/terminal.dart
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
327
packages/console/lib/src/utils/async_circular_buffer.dart
Normal file
327
packages/console/lib/src/utils/async_circular_buffer.dart
Normal file
|
@ -0,0 +1,327 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
74
packages/console/lib/src/utils/dcli_exception.dart
Normal file
74
packages/console/lib/src/utils/dcli_exception.dart
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* 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());
|
||||||
|
}
|
58
packages/console/lib/src/utils/dcli_platform.dart
Normal file
58
packages/console/lib/src/utils/dcli_platform.dart
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* 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 }
|
23
packages/console/lib/src/utils/dev_null.dart
Normal file
23
packages/console/lib/src/utils/dev_null.dart
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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) {}
|
232
packages/console/lib/src/utils/file.dart
Normal file
232
packages/console/lib/src/utils/file.dart
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
/*
|
||||||
|
* 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.');
|
||||||
|
}
|
130
packages/console/lib/src/utils/limited_stream_controller.dart
Normal file
130
packages/console/lib/src/utils/limited_stream_controller.dart
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
15
packages/console/lib/src/utils/line_action.dart
Normal file
15
packages/console/lib/src/utils/line_action.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
188
packages/console/lib/src/utils/line_file.dart
Normal file
188
packages/console/lib/src/utils/line_file.dart
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
/*
|
||||||
|
* 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() {}
|
||||||
|
}
|
30
packages/console/lib/src/utils/platform.dart
Normal file
30
packages/console/lib/src/utils/platform.dart
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* 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';
|
101
packages/console/lib/src/utils/run_exception.dart
Normal file
101
packages/console/lib/src/utils/run_exception.dart
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
47
packages/console/lib/src/utils/stack_list.dart
Normal file
47
packages/console/lib/src/utils/stack_list.dart
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
}
|
63
packages/console/lib/src/utils/truepath.dart
Normal file
63
packages/console/lib/src/utils/truepath.dart
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
16
packages/console/lib/terminal.dart
Normal file
16
packages/console/lib/terminal.dart
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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,8 +10,19 @@ environment:
|
||||||
|
|
||||||
# Add regular dependencies here.
|
# Add regular dependencies here.
|
||||||
dependencies:
|
dependencies:
|
||||||
path: ^1.9.0
|
async: ^2.5.0
|
||||||
scope: ^4.1.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