From 9593799e2a755d895f5fbb170ac827e013bc0cbd Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Wed, 20 Oct 2021 22:42:04 -0700 Subject: [PATCH] Added CatError class. --- README.md | 17 ++++++----- bin/dcat.dart | 27 ++++++++--------- lib/dcat.dart | 36 +++++++++++++++-------- test/dcat_test.dart | 71 +++++++++++++++++++++++++++++++-------------- 4 files changed, 97 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 6e09ec1..46a48c9 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,8 @@ import 'package:dcat/dcat.dart'; final result = await cat(['path/to/file', 'path/to/otherfile]'], File('path/to/outfile').openWrite()); if (result.isFailure) { - for (final message in result.messages) { - print("Error: $message"); + for (final error in result.errors) { + 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. -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 -final result = await cat(['path/to/file'], stdout); -if (result.exitCode == exitSuccess) { - ... +final result = + await cat(['path/to/file'], stdout, showLineNumbers: true); +if (result.exitCode == exitSuccess) { // or result.isSuccess +... } else { - for (final message in result.messages) { - stderr.writeln("Error: $message"); + for (final error in result.errors) { + stderr.writeln("Error with '${error.path}': ${error.message}"); } } ``` \ No newline at end of file diff --git a/bin/dcat.dart b/bin/dcat.dart index 5139052..a4a598e 100644 --- a/bin/dcat.dart +++ b/bin/dcat.dart @@ -23,9 +23,9 @@ const showTabsFlag = 'show-tabs'; const squeezeBlankFlag = 'squeeze-blank'; const versionFlag = 'version'; -/// Concatenates files specified in [arguments]. -/// -/// Usage: `dcat [option] [file]…` +// Concatenates file(s) to standard output. +// +// Usage: `dcat [option] [file]…` Future main(List arguments) async { exitCode = exitSuccess; @@ -34,8 +34,8 @@ Future main(List arguments) async { try { argResults = parser.parse(arguments); } on FormatException catch (e) { - await printError( - "${e.message}\nTry '$appName --$helpFlag' for more information."); + stderr.writeln('''$appName: ${e.message} +Try '$appName --$helpFlag' for more information.'''); exitCode = exitFailure; return exitCode; } @@ -77,8 +77,8 @@ Future main(List arguments) async { showTabs: showTabs, squeezeBlank: argResults[squeezeBlankFlag]); - for (final message in result.messages) { - await printError(message); + for (final error in result.errors) { + await printError(error); } exitCode = result.exitCode; @@ -87,7 +87,7 @@ Future main(List arguments) async { return exitCode; } -/// Setup the command-line arguments parser. +// Sets up the command-line arguments parser. Future setupArgsParser() async { final parser = ArgParser(); @@ -124,12 +124,13 @@ Future setupArgsParser() async { return parser; } -/// Prints an error [message] to [stderr]. -Future printError(String message) async { - stderr.writeln("$appName: $message"); +// Prints an error to stderr. +Future printError(CatError error) async { + stderr.writeln( + '$appName: ' + (error.hasPath ? '${error.path}: ' : '') + error.message); } -/// Prints the version info. +// Prints the version info. Future printVersion() async { stdout.writeln('''$appName (Dart cat) $appVersion Copyright (C) 2021 Erik C. Thauvin @@ -140,7 +141,7 @@ Source: $homePage'''); return exitSuccess; } -/// Prints usage with [options]. +// Prints the help/usage. Future usage(String options) async { stdout.writeln('''Usage: $appName [OPTION]... [FILE]... Concatenate FILE(s) to standard output. diff --git a/lib/dcat.dart b/lib/dcat.dart index 68ab9ee..737ae9d 100644 --- a/lib/dcat.dart +++ b/lib/dcat.dart @@ -7,21 +7,33 @@ library dcat; import 'dart:io'; -/// Failure exit code. +/// The exit status code for failure. const exitFailure = 1; -/// Success exit code. +/// The exit status code for success. const exitSuccess = 0; 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 { - /// The exit code. + /// The exit status code. int exitCode = exitSuccess; - /// The error messages. - final List messages = []; + /// The list of errors. + final List errors = []; CatResult(); @@ -31,13 +43,13 @@ class CatResult { /// Returns `true` if the [exitCode] is [exitSuccess]. bool get isSuccess => exitCode == exitSuccess; - /// Add a message with an optional path. - void addMessage(String message, {String? path}) { + /// Adds an error [message] and [path]. + void addError(String message, {String? path}) { exitCode = exitFailure; if (path != null && path.isNotEmpty) { - messages.add('$path: $message'); + errors.add(CatError(message, path: path)); } else { - messages.add(message); + errors.add(CatError(message)); } } } @@ -71,7 +83,7 @@ Future cat(List paths, IOSink output, await _copyStream(input, lastLine, output, numberNonBlank, showEnds, showLineNumbers, showNonPrinting, showTabs, squeezeBlank); } catch (e) { - result.addMessage(_getErrorMessage(e)); + result.addError(_getErrorMessage(e)); } } } else { @@ -86,7 +98,7 @@ Future cat(List paths, IOSink output, await _copyStream(stream, lastLine, output, numberNonBlank, showEnds, showLineNumbers, showNonPrinting, showTabs, squeezeBlank); } catch (e) { - result.addMessage(_getErrorMessage(e), path: path); + result.addError(_getErrorMessage(e), path: path); } } } diff --git a/test/dcat_test.dart b/test/dcat_test.dart index 90971b5..c4cca9e 100644 --- a/test/dcat_test.dart +++ b/test/dcat_test.dart @@ -64,21 +64,23 @@ void main() { test('CatResult defaults', () async { final result = CatResult(); expect(result.isSuccess, true, reason: 'success by default'); - expect(result.messages.isEmpty, true, reason: 'empty by default'); - result.addMessage(sampleText); + expect(result.errors.isEmpty, true, reason: 'empty by default'); + result.addError(sampleText); expect(result.isFailure, true, reason: 'is failure'); - expect(result.messages.first, equals(sampleText), + expect(result.errors.first.message, equals(sampleText), reason: 'message is sample'); final path = 'foo/bar'; - result.addMessage(sampleText, path: path); - expect(result.messages.last, equals("$path: $sampleText"), reason: 'message has path'); + result.addError(path, path: 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 { var tmp = makeTmpFile(); final result = await cat(['-'], tmp.openWrite(), input: mockStdin()); 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(); expect(await tmp.exists(), false, reason: 'tmp file does not exists'); }); @@ -195,7 +197,7 @@ void main() { final tmp = makeTmpFile(); await cat([sampleBinary, sampleFile], tmp.openWrite(), showNonPrinting: true); - var lines = await tmp.readAsLines(); + final lines = await tmp.readAsLines(); expect(lines.first, startsWith('7z')); }); @@ -219,11 +221,21 @@ void main() { final tmp = makeTmpFile(); final result = await cat([sampleFile], tmp.openWrite()); 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.length(), greaterThan(0), 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.last, endsWith('✓'), reason: 'end with checkmark'); }); @@ -273,28 +285,45 @@ void main() { expect(lines.length, 2, reason: "two lines"); }); - test('closed stdout', () 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 { + test('stdin empty', () async { final tmp = makeTmpFile(); var result = await cat([], tmp.openWrite(), input: Stream.empty()); 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()); 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 result = await cat([], tmp.openWrite(), input: null); 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'); + }); }); }