commit 8eb7a7ce99bc9f256c9f8c4dfdaf1515a5feb462 Author: Erik C. Thauvin Date: Sat Oct 9 17:18:56 2021 -0700 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57fd0ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,88 @@ +!.vscode/extensions.json +!.vscode/launch.json +!.vscode/settings.json +!.vscode/tasks.json +*.class +*.code-workspace +*.ctxt +*.iws +*.log +*.nar +*.rar +*.sublime-* +*.tar.gz +*.zip +.DS_Store +.classpath +.dart_tool/ +.gradle +.history +.kobalt +.mtj.tmp/ +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.nb-gradle +.packages +.project +.scannerwork +.settings +.vscode/* +/**/.idea/$CACHE_FILE$ +/**/.idea/$PRODUCT_WORKSPACE_FILE$ +/**/.idea/**/caches/build_file_checksums.ser +/**/.idea/**/contentModel.xml +/**/.idea/**/dataSources.ids +/**/.idea/**/dataSources.local.xml +/**/.idea/**/dataSources/ +/**/.idea/**/dbnavigator.xml +/**/.idea/**/dictionaries +/**/.idea/**/dynamic.xml +/**/.idea/**/gradle.xml +/**/.idea/**/httpRequests +/**/.idea/**/libraries +/**/.idea/**/mongoSettings.xml +/**/.idea/**/replstate.xml +/**/.idea/**/shelf +/**/.idea/**/shelf/ +/**/.idea/**/sqlDataSources.xml +/**/.idea/**/tasks.xml +/**/.idea/**/uiDesigner.xml +/**/.idea/**/usage.statistics.xml +/**/.idea/**/workspace.xml +/**/.idea/sonarlint* +/**/.idea_modules/ +Thumbs.db +__pycache__ +atlassian-ide-plugin.xml +bin/dcat +bin/dcat.exe +build/ +cmake-build-*/ +com_crashlytics_export_strings.xml +crashlytics-build.properties +crashlytics.properties +dependency-reduced-pom.xml +deploy/ +dist/ +ehthumbs.db +fabric.properties +gen/ +gradle.properties +hs_err_pid* +kobaltBuild +kobaltw*-test +lib/kotlin* +libs/ +local.properties +out/ +pom.xml.asc +pom.xml.next +pom.xml.releaseBackup +pom.xml.tag +pom.xml.versionsBackup +proguard-project.txt +project.properties +release.properties +target/ +test-output +venv diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..562d6ca --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,43 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..639900d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..630f41c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..797acea --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3cc8402 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2021, Erik C. Thauvin (erik@thauvin.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of this project nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..545c837 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +Concatenate file(s) to standard output. + diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/bin/dcat.dart b/bin/dcat.dart new file mode 100644 index 0000000..3072f56 --- /dev/null +++ b/bin/dcat.dart @@ -0,0 +1,200 @@ +// 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:indent/indent.dart'; + +const appName = 'dcat'; +const appVersion = '1.0.0'; +const exitFailure = 1; +const exitSuccess = 0; +const helpFlag = 'help'; +const nonBlankFlag = 'number-nonblank'; +const numberFlag = 'number'; +const showEndsFlag = 'show-ends'; +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]... +/// ``` +Future main(List arguments) async { + final parser = ArgParser(); + Future returnCode; + exitCode = exitSuccess; + parser.addFlag(nonBlankFlag, + negatable: false, + abbr: 'b', + help: 'number nonempty output lines, overrides -n'); + parser.addFlag(showEndsFlag, + negatable: false, abbr: 'E', help: 'display \$ at end of each line'); + parser.addFlag(helpFlag, + negatable: false, abbr: 'h', help: 'display this help and exit'); + parser.addFlag(numberFlag, + negatable: false, abbr: 'n', help: 'number all output lines'); + parser.addFlag(showTabsFlag, + negatable: false, abbr: 'T', help: 'display TAB characters as ^I'); + parser.addFlag(squeezeBlank, + negatable: false, + abbr: 's', + help: 'suppress repeated empty output lines'); + parser.addFlag(versionFlag, + negatable: false, help: 'output version information and exit'); + + final ArgResults argResults; + try { + argResults = parser.parse(arguments); + } on FormatException catch (e) { + return await handleError( + "${e.message}\nTry '$appName --$helpFlag' for more information."); + } + + if (argResults[helpFlag]) { + returnCode = usage(parser.usage); + } else if (argResults[versionFlag]) { + returnCode = printVersion(); + } else { + final paths = argResults.rest; + returnCode = cat(paths, + showEnds: argResults[showEndsFlag], + showLineNumbers: argResults[numberFlag], + numberNonBlank: argResults[nonBlankFlag], + showTabs: argResults[showTabsFlag], + squeezeBlank: argResults[squeezeBlank]); + } + + exitCode = await returnCode; + return exitCode; +} + +/// Prints version info. +Future printVersion() async { + print('''$appName (Dart cat) $appVersion +Copyright (C) 2021 Erik C. Thauvin +License: 3-Clause BSD + +Based on +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]... +Concatenate FILE(s) to standard output. + +With no FILE, or when FILE is -, read standard input. + +${options.indent(2)} +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: '''); + 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/dcat.iml b/dcat.iml new file mode 100644 index 0000000..0931c29 --- /dev/null +++ b/dcat.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..5156e06 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,348 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "28.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.0" + args: + dependency: "direct main" + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.4" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + coverage: + dependency: transitive + description: + name: coverage + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + indent: + dependency: "direct main" + description: + name: indent + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" + lints: + dependency: "direct dev" + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.10" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test: + dependency: "direct dev" + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.18.2" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.5" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.5" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "7.3.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" +sdks: + dart: ">=2.14.3 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..16b6383 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,14 @@ +name: dcat +description: Concatenate file(s) to standard output. +version: 1.0.0 +homepage: https://github.com/ethauvin/dcat + +environment: + sdk: '>=2.14.3 <3.0.0' + +dev_dependencies: + lints: ^1.0.0 + test: ^1.18.2 +dependencies: + args: ^2.3.0 + indent: ^2.0.0 diff --git a/test/dcat_test.dart b/test/dcat_test.dart new file mode 100644 index 0000000..b6b6559 --- /dev/null +++ b/test/dcat_test.dart @@ -0,0 +1,102 @@ +import 'package:test/test.dart'; + +import '../bin/dcat.dart' as dcat; + +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)); + }); + + test('Test --version', () async { + expect(dcat.main(['--version']), completion(equals(0))); + exitCode = await dcat.main(['--version']); + expect(exitCode, equals(dcat.exitSuccess)); + }); + + test('Test directory', () async { + exitCode = await dcat.main(['bin']); + expect(exitCode, equals(dcat.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'); + }); + + test('Test cat source', () async { + await dcat.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); + expect(exitCode, 0, reason: 'result code is 0'); + expect(log.first, startsWith('1: // Copyright (c)'), + reason: 'has copyright'); + expect(log.last, endsWith(': }'), reason: 'last line'); + for (final String line in log) { + expect(line, matches('^\\d+: .*'), reason: 'has line number'); + } + }); + + test('Test cat -E', () async { + await dcat.cat(['test/test.txt'], log: log, showEnds: true); + var hasBlank = false; + for (final String line in log) { + expect(line, endsWith('\$')); + if (line == '\$') { + hasBlank = true; + } + } + expect(hasBlank, true, reason: 'has blank line'); + }); + + test('Test cat -bE', () async { + await dcat + .cat(['test/test.txt'], log: log, numberNonBlank: true, showEnds: true); + var hasBlank = false; + for (final String line in log) { + expect(line, endsWith('\$')); + if (line.contains(RegExp(r'^\d+: .*\$$'))) { + hasBlank = true; + } + } + expect(hasBlank, true, reason: 'has blank line'); + }); + + test('Test cat -T', () async { + await dcat.cat(['test/test.txt'], log: log, showTabs: true); + var hasTab = false; + for (final String line in log) { + if (line.startsWith('^I')) { + hasTab = true; + break; + } + } + expect(hasTab, true, reason: 'has tab'); + }); + + test('Test cat -s', () async { + await dcat.cat(['test/test.txt'], log: log, squeezeBlank: true); + var hasSqueeze = true; + var prevLine = 'foo'; + for (final String line in log) { + if (line == prevLine) { + hasSqueeze = false; + } + prevLine = line; + } + expect(hasSqueeze, true, reason: 'has squeeze'); + }); +} diff --git a/test/test.txt b/test/test.txt new file mode 100644 index 0000000..a0d8abe --- /dev/null +++ b/test/test.txt @@ -0,0 +1,7 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + + +Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.