diff --git a/bin/dcat.dart b/bin/dcat.dart index 3072f56..7025cc6 100644 --- a/bin/dcat.dart +++ b/bin/dcat.dart @@ -1,16 +1,15 @@ // Copyright (c) 2021, Erik C. Thauvin. All rights reserved. // Use of this source code is governed by a BSD-style license that can be found // in the LICENSE file. -import 'dart:convert'; + import 'dart:io'; import 'package:args/args.dart'; +import 'package:dcat/dcat.dart'; import 'package:indent/indent.dart'; -const appName = 'dcat'; +const appName = libName; const appVersion = '1.0.0'; -const exitFailure = 1; -const exitSuccess = 0; const helpFlag = 'help'; const nonBlankFlag = 'number-nonblank'; const numberFlag = 'number'; @@ -19,69 +18,9 @@ const showTabsFlag = 'show-tabs'; const squeezeBlank = 'squeeze-blank'; const versionFlag = 'version'; -/// Prints [message] and [path] to stderr. -Future handleError(String message, {String path = ''}) async { - if (path.isNotEmpty) { - stderr.writeln('$appName: $path: $message'); - } else { - stderr.write('$appName: $message'); - } - return exitFailure; -} - -/// Concatenates files in [paths]. -Future cat(List paths, - {List? log, - bool showEnds = false, - bool numberNonBlank = false, - bool showLineNumbers = false, - bool showTabs = false, - bool squeezeBlank = false}) async { - var lineNumber = 1; - var returnCode = 0; - log?.clear(); - if (paths.isEmpty) { - final lines = await _readStdin(); - await _writeLines(lines, lineNumber, log, showEnds, showLineNumbers, - numberNonBlank, showTabs, squeezeBlank); - } else { - for (final path in paths) { - try { - final Stream lines; - if (path == '-') { - lines = await _readStdin(); - } else { - lines = utf8.decoder - .bind(File(path).openRead()) - .transform(const LineSplitter()); - } - lineNumber = await _writeLines(lines, lineNumber, log, showEnds, - showLineNumbers, numberNonBlank, showTabs, squeezeBlank); - } on FileSystemException catch (e) { - final String? osMessage = e.osError?.message; - final String message; - if (osMessage != null && osMessage.isNotEmpty) { - message = osMessage; - } else { - message = e.message; - } - returnCode = await handleError(message, path: path); - } on FormatException { - returnCode = - await handleError('Binary file not supported.', path: path); - } catch (e) { - returnCode = await handleError(e.toString(), path: path); - } - } - } - return returnCode; -} - /// Concatenates files specified in [arguments]. /// -/// ``` -/// dcat [OPTION]... [FILE]... -/// ``` +/// Usage: `dcat [OPTION]... [FILE]...` Future main(List arguments) async { final parser = ArgParser(); Future returnCode; @@ -109,8 +48,9 @@ Future main(List arguments) async { try { argResults = parser.parse(arguments); } on FormatException catch (e) { - return await handleError( - "${e.message}\nTry '$appName --$helpFlag' for more information."); + return await printError( + "${e.message}\nTry '$appName --$helpFlag' for more information.", + appName: appName); } if (argResults[helpFlag]) { @@ -131,21 +71,17 @@ Future main(List arguments) async { return exitCode; } -/// Prints version info. +/// Prints the version info. Future printVersion() async { print('''$appName (Dart cat) $appVersion Copyright (C) 2021 Erik C. Thauvin License: 3-Clause BSD -Based on +Inspired by Written by Erik C. Thauvin '''); return exitSuccess; } -/// Reads from stdin. -Future> _readStdin() async => - stdin.transform(utf8.decoder).transform(const LineSplitter()); - /// Prints usage with [options]. Future usage(String options) async { print('''Usage: $appName [OPTION]... [FILE]... @@ -161,40 +97,3 @@ Examples: Source and documentation: '''); return exitSuccess; } - -/// Writes lines to stdout. -Future _writeLines(Stream lines, int lineNumber, - [List? log, - bool showEnds = false, - bool showLineNumbers = false, - bool showNonBlank = false, - bool showTabs = false, - bool sqeezeBlank = false]) async { - var emptyLine = 0; - final sb = StringBuffer(); - await for (final line in lines) { - sb.clear(); - if (sqeezeBlank && line.isEmpty) { - if (++emptyLine >= 2) { - continue; - } - } else { - emptyLine = 0; - } - if (showNonBlank || showLineNumbers) { - sb.write('${lineNumber++}: '); - } - if (showTabs) { - sb.write(line.replaceAll('\t', '^I')); - } else { - sb.write(line); - } - if (showEnds) { - sb.write('\$'); - } - - log?.add(sb.toString()); - stdout.writeln(sb); - } - return lineNumber; -} diff --git a/lib/dcat.dart b/lib/dcat.dart new file mode 100644 index 0000000..0313ace --- /dev/null +++ b/lib/dcat.dart @@ -0,0 +1,114 @@ +// Copyright (c) 2021, Erik C. Thauvin. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +library dcat; + +import 'dart:convert'; +import 'dart:io'; + +const libName = 'dcat'; +const exitFailure = 1; +const exitSuccess = 0; + +/// Concatenates files in [paths] to [stdout] +/// +/// The parameters are similar to the [GNU cat utility](https://www.gnu.org/software/coreutils/manual/html_node/cat-invocation.html#cat-invocation). +/// Specify a [log] for debugging purpose. +Future cat(List paths, + {List? log, + bool showEnds = false, + bool numberNonBlank = false, + bool showLineNumbers = false, + bool showTabs = false, + bool squeezeBlank = false}) async { + var lineNumber = 1; + var returnCode = 0; + log?.clear(); + if (paths.isEmpty) { + final lines = await _readStdin(); + await _writeLines(lines, lineNumber, log, showEnds, showLineNumbers, + numberNonBlank, showTabs, squeezeBlank); + } else { + for (final path in paths) { + try { + final Stream lines; + if (path == '-') { + lines = await _readStdin(); + } else { + lines = utf8.decoder + .bind(File(path).openRead()) + .transform(const LineSplitter()); + } + lineNumber = await _writeLines(lines, lineNumber, log, showEnds, + showLineNumbers, numberNonBlank, showTabs, squeezeBlank); + } on FileSystemException catch (e) { + final String? osMessage = e.osError?.message; + final String message; + if (osMessage != null && osMessage.isNotEmpty) { + message = osMessage; + } else { + message = e.message; + } + returnCode = await printError(message, path: path); + } on FormatException { + returnCode = await printError('Binary file not supported.', path: path); + } catch (e) { + returnCode = await printError(e.toString(), path: path); + } + } + } + return returnCode; +} + +/// Prints the [appName], [path] and error [message] to [stderr]. +Future printError(String message, + {String appName = libName, String path = ''}) async { + if (path.isNotEmpty) { + stderr.writeln('$libName: $path: $message'); + } else { + stderr.write('$libName: $message'); + } + return exitFailure; +} + +/// Reads from stdin. +Future> _readStdin() async => + stdin.transform(utf8.decoder).transform(const LineSplitter()); + +/// Writes lines to stdout. +Future _writeLines(Stream lines, int lineNumber, + [List? log, + bool showEnds = false, + bool showLineNumbers = false, + bool showNonBlank = false, + bool showTabs = false, + bool squeezeBlank = false]) async { + var emptyLine = 0; + final sb = StringBuffer(); + await for (final line in lines) { + sb.clear(); + if (squeezeBlank && line.isEmpty) { + if (++emptyLine >= 2) { + continue; + } + } else { + emptyLine = 0; + } + if (showNonBlank || showLineNumbers) { + sb.write('${lineNumber++}: '); + } + if (showTabs) { + sb.write(line.replaceAll('\t', '^I')); + } else { + sb.write(line); + } + if (showEnds) { + sb.write('\$'); + } + + log?.add(sb.toString()); + stdout.writeln(sb); + } + return lineNumber; +} diff --git a/test/dcat_test.dart b/test/dcat_test.dart index b6b6559..74fb8d6 100644 --- a/test/dcat_test.dart +++ b/test/dcat_test.dart @@ -1,46 +1,46 @@ +import 'package:dcat/dcat.dart'; import 'package:test/test.dart'; -import '../bin/dcat.dart' as dcat; +import '../bin/dcat.dart' as app; void main() { final List log = []; int exitCode; test('Test Help', () async { - expect(dcat.main(['-h']), completion(equals(0))); - expect(dcat.main(['--help']), completion(equals(0))); - exitCode = await dcat.main(['-h']); - expect(exitCode, equals(dcat.exitSuccess)); + expect(app.main(['-h']), completion(equals(0))); + expect(app.main(['--help']), completion(equals(0))); + exitCode = await app.main(['-h']); + expect(exitCode, equals(exitSuccess)); }); test('Test --version', () async { - expect(dcat.main(['--version']), completion(equals(0))); - exitCode = await dcat.main(['--version']); - expect(exitCode, equals(dcat.exitSuccess)); + expect(app.main(['--version']), completion(equals(0))); + exitCode = await app.main(['--version']); + expect(exitCode, equals(exitSuccess)); }); test('Test directory', () async { - exitCode = await dcat.main(['bin']); - expect(exitCode, equals(dcat.exitFailure)); + exitCode = await app.main(['bin']); + expect(exitCode, equals(exitFailure)); }); test('Test missing file', () async { - exitCode = await dcat.main(['foo']); - expect(exitCode, equals(dcat.exitFailure), reason: 'foo not found'); - exitCode = await dcat.main(['bin/dcat.dart', 'foo']); - expect(exitCode, equals(dcat.exitFailure), reason: 'one missing file'); + exitCode = await app.main(['foo']); + expect(exitCode, equals(exitFailure), reason: 'foo not found'); + exitCode = await app.main(['bin/dcat.dart', 'foo']); + expect(exitCode, equals(exitFailure), reason: 'one missing file'); }); test('Test cat source', () async { - await dcat.cat(['bin/dcat.dart'], log: log); + await cat(['bin/dcat.dart'], log: log); expect(log.isEmpty, false, reason: 'log is empty'); expect(log.first, startsWith('// Copyright (c)'), reason: 'has copyright'); expect(log.last, equals('}')); }); test('Test cat -n source', () async { - exitCode = - await dcat.cat(['bin/dcat.dart'], log: log, showLineNumbers: true); + exitCode = await cat(['bin/dcat.dart'], log: log, showLineNumbers: true); expect(exitCode, 0, reason: 'result code is 0'); expect(log.first, startsWith('1: // Copyright (c)'), reason: 'has copyright'); @@ -51,7 +51,7 @@ void main() { }); test('Test cat -E', () async { - await dcat.cat(['test/test.txt'], log: log, showEnds: true); + await cat(['test/test.txt'], log: log, showEnds: true); var hasBlank = false; for (final String line in log) { expect(line, endsWith('\$')); @@ -63,8 +63,8 @@ void main() { }); test('Test cat -bE', () async { - await dcat - .cat(['test/test.txt'], log: log, numberNonBlank: true, showEnds: true); + await cat(['test/test.txt'], + log: log, numberNonBlank: true, showEnds: true); var hasBlank = false; for (final String line in log) { expect(line, endsWith('\$')); @@ -76,7 +76,7 @@ void main() { }); test('Test cat -T', () async { - await dcat.cat(['test/test.txt'], log: log, showTabs: true); + await cat(['test/test.txt'], log: log, showTabs: true); var hasTab = false; for (final String line in log) { if (line.startsWith('^I')) { @@ -88,7 +88,7 @@ void main() { }); test('Test cat -s', () async { - await dcat.cat(['test/test.txt'], log: log, squeezeBlank: true); + await cat(['test/test.txt'], log: log, squeezeBlank: true); var hasSqueeze = true; var prevLine = 'foo'; for (final String line in log) {