Added CatError class.

This commit is contained in:
Erik C. Thauvin 2021-10-20 22:42:04 -07:00
parent dc3e3896d7
commit 9593799e2a
4 changed files with 97 additions and 54 deletions

View file

@ -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}");
} }
} }
``` ```

View file

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

View file

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

View file

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