add(dcli): refactoring from dcli_core dcli_terminal dcli_input

This commit is contained in:
Patrick Stewart 2024-08-14 09:40:03 -07:00
parent 59e30aa9be
commit 11bb99a95c
48 changed files with 7081 additions and 2 deletions

View file

@ -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.

View 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;

View 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';

View 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');
}
}

View 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]);
}

View 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);
}

View 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);
}

View 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);
}

View 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]);
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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;
}

View 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;
}
}

View 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);
}

View 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;
}
}

View 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);
}

View 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);
}

View 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);
}

View 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;
}

View 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;
}
}

View 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);
}

View 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);
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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();
}
}

View 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;
}

View 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]);
}
}

View 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'), '');
}

View 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);
}

View 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
}

View 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;
}

View 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;
}

View 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());
}

View 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 }

View 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) {}

View 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.');
}

View 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;
}

View 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);

View 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() {}
}

View 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';

View 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;
}
}

View 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();
}

View 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);

View 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';

View file

@ -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