Added CatError class.
This commit is contained in:
parent
dc3e3896d7
commit
9593799e2a
4 changed files with 97 additions and 54 deletions
17
README.md
17
README.md
|
@ -56,8 +56,8 @@ import 'package:dcat/dcat.dart';
|
||||||
final result = await cat(['path/to/file', 'path/to/otherfile]'],
|
final result = await cat(['path/to/file', 'path/to/otherfile]'],
|
||||||
File('path/to/outfile').openWrite());
|
File('path/to/outfile').openWrite());
|
||||||
if (result.isFailure) {
|
if (result.isFailure) {
|
||||||
for (final message in result.messages) {
|
for (final error in result.errors) {
|
||||||
print("Error: $message");
|
print('Error: ${error.message}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -82,15 +82,16 @@ showNonPrinting | Same as `-v` | bool
|
||||||
|
|
||||||
The remaining optional parameters are similar to the [GNU cat](https://www.gnu.org/software/coreutils/manual/html_node/cat-invocation.html#cat-invocation) utility.
|
The remaining optional parameters are similar to the [GNU cat](https://www.gnu.org/software/coreutils/manual/html_node/cat-invocation.html#cat-invocation) utility.
|
||||||
|
|
||||||
A `CatResult` object is returned which contains the `exitCode` (`exitSuccess` or `exitFailure`) and error `messages` if any:
|
A `CatResult` object is returned which contains the `exitCode` (`exitSuccess` or `exitFailure`) and `errors`, if any:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
final result = await cat(['path/to/file'], stdout);
|
final result =
|
||||||
if (result.exitCode == exitSuccess) {
|
await cat(['path/to/file'], stdout, showLineNumbers: true);
|
||||||
...
|
if (result.exitCode == exitSuccess) { // or result.isSuccess
|
||||||
|
...
|
||||||
} else {
|
} else {
|
||||||
for (final message in result.messages) {
|
for (final error in result.errors) {
|
||||||
stderr.writeln("Error: $message");
|
stderr.writeln("Error with '${error.path}': ${error.message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
|
@ -23,9 +23,9 @@ const showTabsFlag = 'show-tabs';
|
||||||
const squeezeBlankFlag = 'squeeze-blank';
|
const squeezeBlankFlag = 'squeeze-blank';
|
||||||
const versionFlag = 'version';
|
const versionFlag = 'version';
|
||||||
|
|
||||||
/// Concatenates files specified in [arguments].
|
// Concatenates file(s) to standard output.
|
||||||
///
|
//
|
||||||
/// Usage: `dcat [option] [file]…`
|
// Usage: `dcat [option] [file]…`
|
||||||
Future<int> main(List<String> arguments) async {
|
Future<int> main(List<String> arguments) async {
|
||||||
exitCode = exitSuccess;
|
exitCode = exitSuccess;
|
||||||
|
|
||||||
|
@ -34,8 +34,8 @@ Future<int> main(List<String> arguments) async {
|
||||||
try {
|
try {
|
||||||
argResults = parser.parse(arguments);
|
argResults = parser.parse(arguments);
|
||||||
} on FormatException catch (e) {
|
} on FormatException catch (e) {
|
||||||
await printError(
|
stderr.writeln('''$appName: ${e.message}
|
||||||
"${e.message}\nTry '$appName --$helpFlag' for more information.");
|
Try '$appName --$helpFlag' for more information.''');
|
||||||
exitCode = exitFailure;
|
exitCode = exitFailure;
|
||||||
return exitCode;
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
@ -77,8 +77,8 @@ Future<int> main(List<String> arguments) async {
|
||||||
showTabs: showTabs,
|
showTabs: showTabs,
|
||||||
squeezeBlank: argResults[squeezeBlankFlag]);
|
squeezeBlank: argResults[squeezeBlankFlag]);
|
||||||
|
|
||||||
for (final message in result.messages) {
|
for (final error in result.errors) {
|
||||||
await printError(message);
|
await printError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
exitCode = result.exitCode;
|
exitCode = result.exitCode;
|
||||||
|
@ -87,7 +87,7 @@ Future<int> main(List<String> arguments) async {
|
||||||
return exitCode;
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Setup the command-line arguments parser.
|
// Sets up the command-line arguments parser.
|
||||||
Future<ArgParser> setupArgsParser() async {
|
Future<ArgParser> setupArgsParser() async {
|
||||||
final parser = ArgParser();
|
final parser = ArgParser();
|
||||||
|
|
||||||
|
@ -124,12 +124,13 @@ Future<ArgParser> setupArgsParser() async {
|
||||||
return parser;
|
return parser;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prints an error [message] to [stderr].
|
// Prints an error to stderr.
|
||||||
Future<void> printError(String message) async {
|
Future<void> printError(CatError error) async {
|
||||||
stderr.writeln("$appName: $message");
|
stderr.writeln(
|
||||||
|
'$appName: ' + (error.hasPath ? '${error.path}: ' : '') + error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prints the version info.
|
// Prints the version info.
|
||||||
Future<int> printVersion() async {
|
Future<int> printVersion() async {
|
||||||
stdout.writeln('''$appName (Dart cat) $appVersion
|
stdout.writeln('''$appName (Dart cat) $appVersion
|
||||||
Copyright (C) 2021 Erik C. Thauvin
|
Copyright (C) 2021 Erik C. Thauvin
|
||||||
|
@ -140,7 +141,7 @@ Source: $homePage''');
|
||||||
return exitSuccess;
|
return exitSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prints usage with [options].
|
// Prints the help/usage.
|
||||||
Future<int> usage(String options) async {
|
Future<int> usage(String options) async {
|
||||||
stdout.writeln('''Usage: $appName [OPTION]... [FILE]...
|
stdout.writeln('''Usage: $appName [OPTION]... [FILE]...
|
||||||
Concatenate FILE(s) to standard output.
|
Concatenate FILE(s) to standard output.
|
||||||
|
|
|
@ -7,21 +7,33 @@ library dcat;
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
/// Failure exit code.
|
/// The exit status code for failure.
|
||||||
const exitFailure = 1;
|
const exitFailure = 1;
|
||||||
|
|
||||||
/// Success exit code.
|
/// The exit status code for success.
|
||||||
const exitSuccess = 0;
|
const exitSuccess = 0;
|
||||||
|
|
||||||
const _lineFeed = 10;
|
const _lineFeed = 10;
|
||||||
|
|
||||||
/// Holds the [cat] result [exitCode] and error [messages].
|
/// Holds the error [message] and [path] of the file that caused the error.
|
||||||
|
class CatError {
|
||||||
|
/// The error message.
|
||||||
|
String message;
|
||||||
|
/// The file path, if any.
|
||||||
|
String? path;
|
||||||
|
|
||||||
|
bool get hasPath => (path != null && path!.isNotEmpty);
|
||||||
|
|
||||||
|
CatError(this.message, {this.path});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds the [cat] result [exitCode] and [errors].
|
||||||
class CatResult {
|
class CatResult {
|
||||||
/// The exit code.
|
/// The exit status code.
|
||||||
int exitCode = exitSuccess;
|
int exitCode = exitSuccess;
|
||||||
|
|
||||||
/// The error messages.
|
/// The list of errors.
|
||||||
final List<String> messages = [];
|
final List<CatError> errors = [];
|
||||||
|
|
||||||
CatResult();
|
CatResult();
|
||||||
|
|
||||||
|
@ -31,13 +43,13 @@ class CatResult {
|
||||||
/// Returns `true` if the [exitCode] is [exitSuccess].
|
/// Returns `true` if the [exitCode] is [exitSuccess].
|
||||||
bool get isSuccess => exitCode == exitSuccess;
|
bool get isSuccess => exitCode == exitSuccess;
|
||||||
|
|
||||||
/// Add a message with an optional path.
|
/// Adds an error [message] and [path].
|
||||||
void addMessage(String message, {String? path}) {
|
void addError(String message, {String? path}) {
|
||||||
exitCode = exitFailure;
|
exitCode = exitFailure;
|
||||||
if (path != null && path.isNotEmpty) {
|
if (path != null && path.isNotEmpty) {
|
||||||
messages.add('$path: $message');
|
errors.add(CatError(message, path: path));
|
||||||
} else {
|
} else {
|
||||||
messages.add(message);
|
errors.add(CatError(message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,7 +83,7 @@ Future<CatResult> cat(List<String> paths, IOSink output,
|
||||||
await _copyStream(input, lastLine, output, numberNonBlank, showEnds,
|
await _copyStream(input, lastLine, output, numberNonBlank, showEnds,
|
||||||
showLineNumbers, showNonPrinting, showTabs, squeezeBlank);
|
showLineNumbers, showNonPrinting, showTabs, squeezeBlank);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result.addMessage(_getErrorMessage(e));
|
result.addError(_getErrorMessage(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -86,7 +98,7 @@ Future<CatResult> cat(List<String> paths, IOSink output,
|
||||||
await _copyStream(stream, lastLine, output, numberNonBlank, showEnds,
|
await _copyStream(stream, lastLine, output, numberNonBlank, showEnds,
|
||||||
showLineNumbers, showNonPrinting, showTabs, squeezeBlank);
|
showLineNumbers, showNonPrinting, showTabs, squeezeBlank);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result.addMessage(_getErrorMessage(e), path: path);
|
result.addError(_getErrorMessage(e), path: path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,21 +64,23 @@ void main() {
|
||||||
test('CatResult defaults', () async {
|
test('CatResult defaults', () async {
|
||||||
final result = CatResult();
|
final result = CatResult();
|
||||||
expect(result.isSuccess, true, reason: 'success by default');
|
expect(result.isSuccess, true, reason: 'success by default');
|
||||||
expect(result.messages.isEmpty, true, reason: 'empty by default');
|
expect(result.errors.isEmpty, true, reason: 'empty by default');
|
||||||
result.addMessage(sampleText);
|
result.addError(sampleText);
|
||||||
expect(result.isFailure, true, reason: 'is failure');
|
expect(result.isFailure, true, reason: 'is failure');
|
||||||
expect(result.messages.first, equals(sampleText),
|
expect(result.errors.first.message, equals(sampleText),
|
||||||
reason: 'message is sample');
|
reason: 'message is sample');
|
||||||
final path = 'foo/bar';
|
final path = 'foo/bar';
|
||||||
result.addMessage(sampleText, path: path);
|
result.addError(path, path: path);
|
||||||
expect(result.messages.last, equals("$path: $sampleText"), reason: 'message has path');
|
expect(result.errors.last.message, equals(path),
|
||||||
|
reason: 'message is foo');
|
||||||
|
expect(result.errors.last.path, equals(path), reason: 'path is foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cat -', () async {
|
test('cat -', () async {
|
||||||
var tmp = makeTmpFile();
|
var tmp = makeTmpFile();
|
||||||
final result = await cat(['-'], tmp.openWrite(), input: mockStdin());
|
final result = await cat(['-'], tmp.openWrite(), input: mockStdin());
|
||||||
expect(result.exitCode, exitSuccess, reason: 'result code is successful');
|
expect(result.exitCode, exitSuccess, reason: 'result code is successful');
|
||||||
expect(result.messages.length, 0, reason: 'no message');
|
expect(result.errors.length, 0, reason: 'no error');
|
||||||
tmp = makeTmpFile();
|
tmp = makeTmpFile();
|
||||||
expect(await tmp.exists(), false, reason: 'tmp file does not exists');
|
expect(await tmp.exists(), false, reason: 'tmp file does not exists');
|
||||||
});
|
});
|
||||||
|
@ -195,7 +197,7 @@ void main() {
|
||||||
final tmp = makeTmpFile();
|
final tmp = makeTmpFile();
|
||||||
await cat([sampleBinary, sampleFile], tmp.openWrite(),
|
await cat([sampleBinary, sampleFile], tmp.openWrite(),
|
||||||
showNonPrinting: true);
|
showNonPrinting: true);
|
||||||
var lines = await tmp.readAsLines();
|
final lines = await tmp.readAsLines();
|
||||||
expect(lines.first, startsWith('7z'));
|
expect(lines.first, startsWith('7z'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -219,11 +221,21 @@ void main() {
|
||||||
final tmp = makeTmpFile();
|
final tmp = makeTmpFile();
|
||||||
final result = await cat([sampleFile], tmp.openWrite());
|
final result = await cat([sampleFile], tmp.openWrite());
|
||||||
expect(result.isSuccess, true, reason: 'result code is success');
|
expect(result.isSuccess, true, reason: 'result code is success');
|
||||||
expect(result.messages.length, 0, reason: 'messages is empty');
|
expect(result.errors.length, 0, reason: 'no errors');
|
||||||
expect(await tmp.exists(), true, reason: 'tmp file exists');
|
expect(await tmp.exists(), true, reason: 'tmp file exists');
|
||||||
expect(await tmp.length(), greaterThan(0),
|
expect(await tmp.length(), greaterThan(0),
|
||||||
reason: 'tmp file is not empty');
|
reason: 'tmp file is not empty');
|
||||||
var lines = await tmp.readAsLines();
|
final lines = await tmp.readAsLines();
|
||||||
|
expect(lines.first, startsWith('Lorem'), reason: 'Lorem in first line');
|
||||||
|
expect(lines.last, endsWith('✓'), reason: 'end with checkmark');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cat < file', () async {
|
||||||
|
final tmp = makeTmpFile();
|
||||||
|
final result =
|
||||||
|
await cat([], tmp.openWrite(), input: File(sampleFile).openRead());
|
||||||
|
expect(result.isSuccess, true, reason: 'result is success');
|
||||||
|
final lines = await tmp.readAsLines();
|
||||||
expect(lines.first, startsWith('Lorem'), reason: 'Lorem in first line');
|
expect(lines.first, startsWith('Lorem'), reason: 'Lorem in first line');
|
||||||
expect(lines.last, endsWith('✓'), reason: 'end with checkmark');
|
expect(lines.last, endsWith('✓'), reason: 'end with checkmark');
|
||||||
});
|
});
|
||||||
|
@ -273,28 +285,45 @@ void main() {
|
||||||
expect(lines.length, 2, reason: "two lines");
|
expect(lines.length, 2, reason: "two lines");
|
||||||
});
|
});
|
||||||
|
|
||||||
test('closed stdout', () async {
|
test('stdin empty', () async {
|
||||||
final tmp = makeTmpFile();
|
|
||||||
final stream = tmp.openWrite();
|
|
||||||
stream.close();
|
|
||||||
final result = await cat([sampleFile], stream);
|
|
||||||
expect(result.messages.first, contains("closed"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('empty stdin', () async {
|
|
||||||
final tmp = makeTmpFile();
|
final tmp = makeTmpFile();
|
||||||
var result = await cat([], tmp.openWrite(), input: Stream.empty());
|
var result = await cat([], tmp.openWrite(), input: Stream.empty());
|
||||||
expect(result.exitCode, exitSuccess, reason: 'cat() is successful');
|
expect(result.exitCode, exitSuccess, reason: 'cat() is successful');
|
||||||
expect(result.messages.length, 0, reason: 'cat() has no message');
|
expect(result.errors.length, 0, reason: 'cat() has no errors');
|
||||||
result = await cat(['-'], tmp.openWrite(), input: Stream.empty());
|
result = await cat(['-'], tmp.openWrite(), input: Stream.empty());
|
||||||
expect(result.exitCode, exitSuccess, reason: 'cat(-) is successful');
|
expect(result.exitCode, exitSuccess, reason: 'cat(-) is successful');
|
||||||
expect(result.messages.length, 0, reason: 'cat(-) no message');
|
expect(result.errors.length, 0, reason: 'cat(-) no errors');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('invalid stdin', () async {
|
test('stdin error', () async {
|
||||||
|
final result =
|
||||||
|
await cat([], stdout, input: Stream.error(Exception(sampleText)));
|
||||||
|
expect(result.isFailure, true, reason: 'cat() is failure');
|
||||||
|
expect(result.errors.first.message, contains(sampleText),
|
||||||
|
reason: 'error is sample');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stdin filesystem error', () async {
|
||||||
|
final result = await cat([], stdout,
|
||||||
|
input: Stream.error(FileSystemException(sampleText)));
|
||||||
|
expect(result.isFailure, true, reason: 'cat() is failure');
|
||||||
|
expect(result.errors.first.message, contains(sampleText),
|
||||||
|
reason: 'error is sample');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stdin invalid', () async {
|
||||||
final tmp = makeTmpFile();
|
final tmp = makeTmpFile();
|
||||||
final result = await cat([], tmp.openWrite(), input: null);
|
final result = await cat([], tmp.openWrite(), input: null);
|
||||||
expect(result.exitCode, exitSuccess);
|
expect(result.exitCode, exitSuccess);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('stdout closed', () async {
|
||||||
|
final tmp = makeTmpFile();
|
||||||
|
final stream = tmp.openWrite();
|
||||||
|
stream.close();
|
||||||
|
final result = await cat([sampleFile], stream);
|
||||||
|
expect(result.errors.first.message, contains("closed"),
|
||||||
|
reason: 'stream is closed');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue