diff --git a/README.md b/README.md index 9bfb5dd..6e09ec1 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ With no FILE, or when FILE is -, read standard input. -T, --show-tabs display TAB characters as ^I -s, --squeeze-blank suppress repeated empty output lines --version output version information and exit - -v, --show-nonprinting use ^ and U+ notation, except for LFD and TAB + -v, --show-nonprinting use ^ and M- notation, except for LFD and TAB Examples: dcat f - g Output f's contents, then standard input, then g's contents. @@ -93,8 +93,4 @@ if (result.exitCode == exitSuccess) { stderr.writeln("Error: $message"); } } -``` - -## Differences from [GNU cat](https://www.gnu.org/software/coreutils/manual/html_node/cat-invocation.html#cat-invocation) - - No binary file support. - - The [U+](https://en.wikipedia.org/wiki/Unicode) notation is used instead of `M-` for non-printing characters. \ No newline at end of file +``` \ No newline at end of file diff --git a/bin/dcat.dart b/bin/dcat.dart index 2c939db..cc3aebf 100644 --- a/bin/dcat.dart +++ b/bin/dcat.dart @@ -11,19 +11,18 @@ import 'package:indent/indent.dart'; const appName = 'dcat'; const appVersion = '1.0.0'; const helpFlag = 'help'; +const homePage = 'https://github.com/ethauvin/dcat'; const numberFlag = 'number'; -const numberNonBlank = 'number-nonblank'; +const numberNonBlankFlag = 'number-nonblank'; const showAllFlag = 'show-all'; const showEndsFlag = 'show-ends'; const showNonPrintingEndsFlag = 'show-nonprinting-ends'; const showNonPrintingFlag = 'show-nonprinting'; const showNonPrintingTabsFlag = 'show-nonprinting-tabs'; const showTabsFlag = 'show-tabs'; -const squeezeBlank = 'squeeze-blank'; +const squeezeBlankFlag = 'squeeze-blank'; const versionFlag = 'version'; -const _homePage = 'https://github.com/ethauvin/dcat'; - /// Concatenates files specified in [arguments]. /// /// Usage: `dcat [option] [file]…` @@ -47,10 +46,10 @@ Future main(List arguments) async { } else { final paths = argResults.rest; var showEnds = argResults[showEndsFlag]; - var showTabs = argResults[showTabsFlag]; var showLineNumbers = argResults[numberFlag]; - var showNonBlank = argResults[numberNonBlank]; + final showNonBlank = argResults[numberNonBlankFlag]; var showNonPrinting = argResults[showNonPrintingFlag]; + var showTabs = argResults[showTabsFlag]; if (argResults[showNonPrintingEndsFlag]) { showNonPrinting = showEnds = true; @@ -70,12 +69,12 @@ Future main(List arguments) async { final result = await cat(paths, stdout, input: stdin, + numberNonBlank: showNonBlank, showEnds: showEnds, showLineNumbers: showLineNumbers, - numberNonBlank: showNonBlank, + showNonPrinting: showNonPrinting, showTabs: showTabs, - squeezeBlank: argResults[squeezeBlank], - showNonPrinting: showNonPrinting); + squeezeBlank: argResults[squeezeBlankFlag]); for (final message in result.messages) { await printError(message); @@ -93,7 +92,7 @@ Future setupArgsParser() async { parser.addFlag(showAllFlag, negatable: false, abbr: 'A', help: 'equivalent to -vET'); - parser.addFlag(numberNonBlank, + parser.addFlag(numberNonBlankFlag, negatable: false, abbr: 'b', help: 'number nonempty output lines, overrides -n'); @@ -109,7 +108,7 @@ Future setupArgsParser() async { negatable: false, abbr: 't', help: 'equivalent to -vT'); parser.addFlag(showTabsFlag, negatable: false, abbr: 'T', help: 'display TAB characters as ^I'); - parser.addFlag(squeezeBlank, + parser.addFlag(squeezeBlankFlag, negatable: false, abbr: 's', help: 'suppress repeated empty output lines'); @@ -119,7 +118,7 @@ Future setupArgsParser() async { parser.addFlag(showNonPrintingFlag, negatable: false, abbr: 'v', - help: 'use ^ and U+ notation, except for LFD and TAB'); + help: 'use ^ and M- notation, except for LFD and TAB'); return parser; } @@ -131,18 +130,18 @@ Future printError(String message) async { /// Prints the version info. Future printVersion() async { - print('''$appName (Dart cat) $appVersion + stdout.writeln('''$appName (Dart cat) $appVersion Copyright (C) 2021 Erik C. Thauvin License 3-Clause BSD: Written by Erik C. Thauvin -Source: $_homePage'''); +Source: $homePage'''); return exitSuccess; } /// Prints usage with [options]. Future usage(String options) async { - print('''Usage: $appName [OPTION]... [FILE]... + stdout.writeln('''Usage: $appName [OPTION]... [FILE]... Concatenate FILE(s) to standard output. With no FILE, or when FILE is -, read standard input. @@ -152,6 +151,6 @@ Examples: $appName f - g Output f's contents, then standard input, then g's contents. $appName Copy standard input to standard output. -Source and documentation: <$_homePage>'''); +Source and documentation: <$homePage>'''); return exitSuccess; } diff --git a/doc/api/dcat/cat.html b/doc/api/dcat/cat.html index 163c36e..bb1ec15 100644 --- a/doc/api/dcat/cat.html +++ b/doc/api/dcat/cat.html @@ -56,12 +56,12 @@ cat(
  1. List<String> paths,
  2. IOSink output,
  3. {Stream<List<int>>? input,
  4. -
  5. bool showEnds = false,
  6. bool numberNonBlank = false,
  7. +
  8. bool showEnds = false,
  9. bool showLineNumbers = false,
  10. +
  11. bool showNonPrinting = false,
  12. bool showTabs = false,
  13. -
  14. bool squeezeBlank = false,
  15. -
  16. bool showNonPrinting = false}
  17. +
  18. bool squeezeBlank = false}
) @@ -81,20 +81,20 @@

Implementation

Future<CatResult> cat(List<String> paths, IOSink output,
     {Stream<List<int>>? input,
-    bool showEnds = false,
     bool numberNonBlank = false,
+    bool showEnds = false,
     bool showLineNumbers = false,
+    bool showNonPrinting = false,
     bool showTabs = false,
-    bool squeezeBlank = false,
-    bool showNonPrinting = false}) async {
+    bool squeezeBlank = false}) async {
   final result = CatResult();
   final lastLine = _LastLine();
 
   if (paths.isEmpty) {
     if (input != null) {
       try {
-        await _writeStream(input, lastLine, output, showEnds, showLineNumbers,
-            numberNonBlank, showTabs, squeezeBlank, showNonPrinting);
+        await _writeStream(input, lastLine, output, numberNonBlank, showEnds,
+            showLineNumbers, showNonPrinting, showTabs, squeezeBlank);
       } catch (e) {
         result.addMessage(exitFailure, _formatError(e));
       }
@@ -108,8 +108,8 @@
         } else {
           stream = File(path).openRead();
         }
-        await _writeStream(stream, lastLine, output, showEnds, showLineNumbers,
-            numberNonBlank, showTabs, squeezeBlank, showNonPrinting);
+        await _writeStream(stream, lastLine, output, numberNonBlank, showEnds,
+            showLineNumbers, showNonPrinting, showTabs, squeezeBlank);
       } catch (e) {
         result.addMessage(exitFailure, _formatError(e), path: path);
       }
diff --git a/doc/api/dcat/dcat-library.html b/doc/api/dcat/dcat-library.html
index 1aaeb3f..29b9172 100644
--- a/doc/api/dcat/dcat-library.html
+++ b/doc/api/dcat/dcat-library.html
@@ -114,7 +114,7 @@
 
       
- cat(List<String> paths, IOSink output, {Stream<List<int>>? input, bool showEnds = false, bool numberNonBlank = false, bool showLineNumbers = false, bool showTabs = false, bool squeezeBlank = false, bool showNonPrinting = false}) + cat(List<String> paths, IOSink output, {Stream<List<int>>? input, bool numberNonBlank = false, bool showEnds = false, bool showLineNumbers = false, bool showNonPrinting = false, bool showTabs = false, bool squeezeBlank = false}) Future<CatResult> diff --git a/doc/api/index.html b/doc/api/index.html index 9de1c2a..4fc17d0 100644 --- a/doc/api/index.html +++ b/doc/api/index.html @@ -69,7 +69,7 @@ With no FILE, or when FILE is -, read standard input. -T, --show-tabs display TAB characters as ^I -s, --squeeze-blank suppress repeated empty output lines --version output version information and exit - -v, --show-nonprinting use ^ and U+ notation, except for LFD and TAB + -v, --show-nonprinting use ^ and M- notation, except for LFD and TAB Examples: dcat f - g Output f's contents, then standard input, then g's contents. @@ -111,11 +111,6 @@ if (result.exitCode == exitSuccess) { } }
-

Differences from GNU cat

-
    -
  • No binary file support.
  • -
  • The U+ notation is used instead of M- for non-printing characters.
  • -
diff --git a/lib/dcat.dart b/lib/dcat.dart index 1209711..9a38cf1 100644 --- a/lib/dcat.dart +++ b/lib/dcat.dart @@ -5,7 +5,6 @@ /// A library to concatenate files to standard output or file. library dcat; -import 'dart:convert'; import 'dart:io'; /// Failure exit code. @@ -57,20 +56,20 @@ class _LastLine { /// The remaining optional parameters are similar to the [GNU cat utility](https://www.gnu.org/software/coreutils/manual/html_node/cat-invocation.html#cat-invocation). Future cat(List paths, IOSink output, {Stream>? input, - bool showEnds = false, bool numberNonBlank = false, + bool showEnds = false, bool showLineNumbers = false, + bool showNonPrinting = false, bool showTabs = false, - bool squeezeBlank = false, - bool showNonPrinting = false}) async { + bool squeezeBlank = false}) async { final result = CatResult(); final lastLine = _LastLine(); if (paths.isEmpty) { if (input != null) { try { - await _writeStream(input, lastLine, output, showEnds, showLineNumbers, - numberNonBlank, showTabs, squeezeBlank, showNonPrinting); + await _writeStream(input, lastLine, output, numberNonBlank, showEnds, + showLineNumbers, showNonPrinting, showTabs, squeezeBlank); } catch (e) { result.addMessage(exitFailure, _formatError(e)); } @@ -84,8 +83,8 @@ Future cat(List paths, IOSink output, } else { stream = File(path).openRead(); } - await _writeStream(stream, lastLine, output, showEnds, showLineNumbers, - numberNonBlank, showTabs, squeezeBlank, showNonPrinting); + await _writeStream(stream, lastLine, output, numberNonBlank, showEnds, + showLineNumbers, showNonPrinting, showTabs, squeezeBlank); } catch (e) { result.addMessage(exitFailure, _formatError(e), path: path); } @@ -104,8 +103,6 @@ String _formatError(Object e) { } else { message = e.message; } - } else if (e is FormatException) { - message = 'Binary file not supported.'; } else { message = '$e'; } @@ -114,15 +111,15 @@ String _formatError(Object e) { // Writes parsed data from a stream Future _writeStream( - Stream stream, + Stream> stream, _LastLine lastLine, IOSink out, + bool numberNonBlank, bool showEnds, bool showLineNumbers, - bool numberNonBlank, + bool showNonPrinting, bool showTabs, - bool squeezeBlank, - bool showNonPrinting) async { + bool squeezeBlank) async { // No flags if (!showEnds && !showLineNumbers && @@ -130,15 +127,17 @@ Future _writeStream( !showTabs && !squeezeBlank && !showNonPrinting) { - await stream.transform(utf8.decoder).forEach(out.write); + await stream.forEach(out.add); } else { + const caret = 94; + const questionMark = 63; const tab = 9; int squeeze = 0; - final sb = StringBuffer(); + final List buff = []; await stream.forEach((data) { - sb.clear(); - for (final ch in utf8.decode(data).runes) { + buff.clear(); + for (final ch in data) { if (lastLine.lastChar == _lineFeed) { if (squeezeBlank) { if (ch == _lineFeed) { @@ -153,52 +152,67 @@ Future _writeStream( } if (showLineNumbers || numberNonBlank) { if (!numberNonBlank || ch != _lineFeed) { - sb - ..write('${++lastLine.lineNumber}'.padLeft(6)) - ..write('\t'); + buff.addAll('${++lastLine.lineNumber}'.padLeft(6).codeUnits); + buff.add(tab); } } } lastLine.lastChar = ch; if (ch == _lineFeed) { if (showEnds) { - sb.write('\$'); + // $ at EOL + buff.add(36); } } else if (ch == tab) { if (showTabs) { - // TAB - sb.write('^I'); + // TAB (^I) + buff + ..add(caret) + ..add(73); continue; } } else if (showNonPrinting) { if (ch >= 32) { if (ch < 127) { // ASCII - sb.writeCharCode(ch); + buff.add(ch); continue; } else if (ch == 127) { - // NULL - sb.write('^?'); + // NULL (^?) + buff + ..add(caret) + ..add(questionMark); continue; } else { - // UNICODE - sb - ..write('U+') - ..write(ch.toRadixString(16).padLeft(4, '0').toUpperCase()); + // HIGH BIT (M-) + buff.add(77); + buff.add(45); + if (ch >= 128 + 32) { + if (ch < 128 + 127) { + buff.add(ch - 128); + } else { + buff + ..add(caret) + ..add(questionMark); + } + } else { + buff.add(caret); + buff.add(ch - 128 + 64); + } continue; } } else { // CTRL - sb - ..write('^') - ..writeCharCode(ch + 64); + buff + ..add(caret) + ..add(ch + 64); continue; } } - sb.writeCharCode(ch); + buff.add(ch); } - if (sb.isNotEmpty) { - out.write(sb); + if (buff.isNotEmpty) { + out.add(buff); } }); } diff --git a/pubspec.lock b/pubspec.lock index e310a66..84b7283 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -274,13 +274,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" - string_splitter: - dependency: "direct main" - description: - name: string_splitter - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0+1" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 07cde21..fa5e159 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,5 +11,4 @@ dev_dependencies: test: ^1.18.2 dependencies: args: ^2.3.0 - indent: ^2.0.0 - string_splitter: ^1.0.0+1 + indent: ^2.0.0 \ No newline at end of file diff --git a/test/dcat_test.dart b/test/dcat_test.dart index d493926..fd1ec4e 100644 --- a/test/dcat_test.dart +++ b/test/dcat_test.dart @@ -22,42 +22,37 @@ void main() { yield text.codeUnits; } - File tmpFile() => + File makeTmpFile() => File("${tempDir.path}/tmp-${DateTime.now().millisecondsSinceEpoch}.txt"); tearDownAll(() => tempDir.delete(recursive: true)); group('app', () { - test('Test Help', () async { + test('--help', () async { expect(app.main(['-h']), completion(0)); expect(app.main(['--help']), completion(0)); exitCode = await app.main(['-h']); expect(exitCode, exitSuccess); }); - test('Test --version', () async { + test('--version', () async { expect(app.main(['--version']), completion(0)); exitCode = await app.main(['--version']); expect(exitCode, exitSuccess); }); - test('Test -a', () async { + test('invalid option', () async { expect(app.main(['-a']), completion(1)); exitCode = await app.main(['-a']); expect(exitCode, exitFailure); }); - test('Test directory', () async { + test('no directories', () async { exitCode = await app.main(['bin']); expect(exitCode, exitFailure); }); - test('Test binary', () async { - exitCode = await app.main([sampleBinary]); - expect(exitCode, exitFailure); - }); - - test('Test missing file', () async { + test('missing file', () async { exitCode = await app.main(['foo']); expect(exitCode, exitFailure, reason: 'foo not found'); exitCode = await app.main([sourceFile, 'foo']); @@ -66,16 +61,17 @@ void main() { }); group('lib', () { - test('Test CatResult', () async { + test('CatResult defaults', () async { final result = CatResult(); expect(result.isSuccess, true, reason: 'success by default'); result.addMessage(exitFailure, sampleText); expect(result.isFailure, true, reason: 'is failure'); - expect(result.messages.first, equals(sampleText), reason: 'message is sample'); + expect(result.messages.first, equals(sampleText), + reason: 'message is sample'); }); - test('Test cat source', () async { - final tmp = tmpFile(); + test('cat source', () async { + final tmp = makeTmpFile(); await cat([sourceFile], tmp.openWrite()); final lines = await tmp.readAsLines(); expect(lines.isEmpty, false, reason: 'log is empty'); @@ -84,8 +80,8 @@ void main() { expect(lines.last, equals('}')); }); - test('Test cat -n source', () async { - final tmp = tmpFile(); + test('cat -n source', () async { + final tmp = makeTmpFile(); final result = await cat([sourceFile], tmp.openWrite(), showLineNumbers: true); expect(result.exitCode, 0, reason: 'result code is 0'); @@ -98,8 +94,8 @@ void main() { } }); - test('Test cat source test', () async { - final tmp = tmpFile(); + test('cat source, test', () async { + final tmp = makeTmpFile(); await cat([sourceFile, sampleFile], tmp.openWrite()); final lines = await tmp.readAsLines(); expect(lines.length, greaterThan(10), reason: 'more than 10 lines'); @@ -108,8 +104,8 @@ void main() { expect(lines.last, endsWith('✓'), reason: 'end with checkmark'); }); - test('Test cat -E', () async { - final tmp = tmpFile(); + test('cat -E', () async { + final tmp = makeTmpFile(); await cat([sampleFile], tmp.openWrite(), showEnds: true); var hasBlank = false; final lines = await tmp.readAsLines(); @@ -123,8 +119,8 @@ void main() { expect(lines.last, endsWith('✓'), reason: 'has unicode'); }); - test('Test cat -bE', () async { - final tmp = tmpFile(); + test('cat -bE', () async { + final tmp = makeTmpFile(); await cat([sampleFile], tmp.openWrite(), numberNonBlank: true, showEnds: true); final lines = await tmp.readAsLines(); @@ -137,8 +133,8 @@ void main() { } }); - test('Test cat -T', () async { - final tmp = tmpFile(); + test('cat -T', () async { + final tmp = makeTmpFile(); await cat([sampleFile], tmp.openWrite(), showTabs: true); var hasTab = false; final lines = await tmp.readAsLines(); @@ -151,8 +147,8 @@ void main() { expect(hasTab, true, reason: 'has tab'); }); - test('Test cat -s', () async { - final tmp = tmpFile(); + test('cat -s', () async { + final tmp = makeTmpFile(); await cat([sampleFile], tmp.openWrite(), squeezeBlank: true); var hasSqueeze = true; var prevLine = 'foo'; @@ -166,26 +162,26 @@ void main() { expect(hasSqueeze, true, reason: 'has squeeze'); }); - test('Test cat -A', () async { - final tmp = tmpFile(); + test('cat -A', () async { + final tmp = makeTmpFile(); await cat([sampleFile], tmp.openWrite(), showNonPrinting: true, showEnds: true, showTabs: true); final lines = await tmp.readAsLines(); expect(lines.first, endsWith('\$'), reason: '\$ at end.'); - expect(lines.last, equals('^I^A^B^C^DU+00A9^?U+0080U+2713'), + expect(lines.last, equals('^I^A^B^C^DM-BM-)^?M-BM-^@M-bM-^\\M-^S'), reason: "no last linefeed"); }); - test('Test cat -t', () async { - final tmp = tmpFile(); + test('cat -t', () async { + final tmp = makeTmpFile(); await cat([sampleFile], tmp.openWrite(), showNonPrinting: true, showTabs: true); final lines = await tmp.readAsLines(); - expect(lines.last, equals('^I^A^B^C^DU+00A9^?U+0080U+2713')); + expect(lines.last, equals('^I^A^B^C^DM-BM-)^?M-BM-^@M-bM-^\\M-^S')); }); - test('Test cat -Abs', () async { - final tmp = tmpFile(); + test('cat -Abs', () async { + final tmp = makeTmpFile(); await cat([sampleFile], tmp.openWrite(), showNonPrinting: true, showEnds: true, @@ -202,8 +198,8 @@ void main() { expect(blankLines, 2, reason: 'only 2 blank lines.'); }); - test('Test cat -v', () async { - final tmp = tmpFile(); + test('cat -v', () async { + final tmp = makeTmpFile(); await cat([sampleFile], tmp.openWrite(), showNonPrinting: true); var hasTab = false; final lines = await tmp.readAsLines(); @@ -214,12 +210,12 @@ void main() { } } expect(hasTab, true, reason: "has real tab"); - expect(lines.last, equals('\t^A^B^C^DU+00A9^?U+0080U+2713'), + expect(lines.last, equals('\t^A^B^C^DM-BM-)^?M-BM-^@M-bM-^\\M-^S'), reason: 'non-printing'); }); - test('Test cat to file', () async { - final tmp = tmpFile(); + test('cat > file', () async { + 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'); @@ -231,16 +227,22 @@ void main() { expect(lines.last, endsWith('✓'), reason: 'end with checkmark'); }); - test('Test cat with file and binary', () async { - final result = await cat([sampleFile, sampleBinary], stdout); - expect(result.isFailure, true, reason: 'result code is failure'); - expect(result.messages.length, 1, reason: 'as one message'); - expect(result.messages.first, contains('Binary'), - reason: 'message contains binary'); + test('cat -v binary, file', () async { + final tmp = makeTmpFile(); + await cat([sampleBinary, sampleFile], tmp.openWrite(), + showNonPrinting: true); + var lines = await tmp.readAsLines(); + expect(lines.first, startsWith('7z')); }); - test('Test empty stdin', () async { - final tmp = tmpFile(); + test('cat binary', () async { + final tmp = makeTmpFile(); + await cat([sampleBinary], tmp.openWrite()); + expect(tmp.readAsLines(), throwsException); + }); + + test('empty stdin', () 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'); @@ -250,32 +252,40 @@ void main() { expect(result.messages.length, 0, reason: 'cat(-) no message'); }); - test('Test cat -', () async { - var tmp = tmpFile(); + 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'); - tmp = tmpFile(); + tmp = makeTmpFile(); expect(await tmp.exists(), false, reason: 'tmp file does not exists'); }); - test('Test cat()', () async { - var tmp = tmpFile(); + test('cat()', () async { + var tmp = makeTmpFile(); await cat([], tmp.openWrite(), input: mockStdin()); var lines = await tmp.readAsLines(); expect(lines.first, equals(sampleText), reason: 'cat() is sample text'); - tmp = tmpFile(); + tmp = makeTmpFile(); await cat([], tmp.openWrite(), input: mockStdin(text: "Line 1\nLine 2")); lines = await tmp.readAsLines(); expect(lines.length, 2, reason: "two lines"); }); - test('Test cat file -', () async { - var tmp = tmpFile(); + test('cat file -', () async { + var tmp = makeTmpFile(); await cat([sampleFile, '-'], tmp.openWrite(), input: mockStdin(text: '\n$sampleText')); var lines = await tmp.readAsLines(); expect(lines.last, equals(sampleText)); }); + + 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")); + }); }); -} \ No newline at end of file +} diff --git a/test/test.7z b/test/test.7z index 98a416a..8e7e137 100644 Binary files a/test/test.7z and b/test/test.7z differ