From ce05281417680c95a015843231a134cfa4acb1de Mon Sep 17 00:00:00 2001 From: Georgii Date: Mon, 28 Oct 2024 17:24:39 +0300 Subject: [PATCH] Supported Grep command. --- .../cli/ApplicationEntry.kt | 1 + core/build.gradle.kts | 1 + .../cli/command/impl/GrepCommand.kt | 96 ++++++++ .../cli/command/impl/GrepCommandTest.kt | 232 ++++++++++++++++++ docs/classes.puml | 14 +- docs/images/classes.svg | 2 +- docs/readme.md | 16 +- 7 files changed, 354 insertions(+), 8 deletions(-) create mode 100644 core/src/main/kotlin/com/github/itmosoftwaredesign/cli/command/impl/GrepCommand.kt create mode 100644 core/src/test/kotlin/com/github/itmosoftwaredesign/cli/command/impl/GrepCommandTest.kt diff --git a/app/src/main/kotlin/com/github/itmosoftwaredesign/cli/ApplicationEntry.kt b/app/src/main/kotlin/com/github/itmosoftwaredesign/cli/ApplicationEntry.kt index 3124da9..1006580 100644 --- a/app/src/main/kotlin/com/github/itmosoftwaredesign/cli/ApplicationEntry.kt +++ b/app/src/main/kotlin/com/github/itmosoftwaredesign/cli/ApplicationEntry.kt @@ -35,6 +35,7 @@ object ApplicationEntry { commandRegistry.register("echo", EchoCommand()) commandRegistry.register("wc", WcCommand()) commandRegistry.register("exit", ExitCommand()) + commandRegistry.register("grep", GrepCommand()) val interpreter = Interpreter(environment, parser, commandRegistry, System.`in`) val interpreterThread = Thread(interpreter) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index f1fce5a..f3419ee 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -28,6 +28,7 @@ repositories { dependencies { implementation(libs.jakarta.annotation) + implementation("commons-cli:commons-cli:1.9.0") testImplementation(libs.junit.jupiter) testImplementation(libs.mockk) diff --git a/core/src/main/kotlin/com/github/itmosoftwaredesign/cli/command/impl/GrepCommand.kt b/core/src/main/kotlin/com/github/itmosoftwaredesign/cli/command/impl/GrepCommand.kt new file mode 100644 index 0000000..96badf5 --- /dev/null +++ b/core/src/main/kotlin/com/github/itmosoftwaredesign/cli/command/impl/GrepCommand.kt @@ -0,0 +1,96 @@ +package com.github.itmosoftwaredesign.cli.command.impl + +import com.github.itmosoftwaredesign.cli.Environment +import com.github.itmosoftwaredesign.cli.command.Command +import com.github.itmosoftwaredesign.cli.command.CommandResult +import com.github.itmosoftwaredesign.cli.command.ErrorResult +import com.github.itmosoftwaredesign.cli.command.SuccessResult +import com.github.itmosoftwaredesign.cli.writeLineUTF8 +import jakarta.annotation.Nonnull +import org.apache.commons.cli.DefaultParser +import org.apache.commons.cli.Options +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +/** + * `Grep` command definition. + * + * Finds matches in the file according to the regex. + * + * Supported options: + * - '-A {arg}': prints the last matched line plus {arg} lines after + * - '-w': searches for the exact matches of the word + * - '-i': makes case-insensitive search + * + * @author gkashin + * @since 0.0.1 + */ +class GrepCommand : Command { + + override fun execute( + @Nonnull environment: Environment, + @Nonnull inputStream: InputStream, + @Nonnull outputStream: OutputStream, + @Nonnull errorStream: OutputStream, + @Nonnull arguments: List + ): CommandResult { + if (arguments.size < 2) { + errorStream.writeLineUTF8("usage: grep [-abcdDEFGHhIiJLlMmnOopqRSsUVvwXxZz] [-A num] [-i string] [-w string] [file ...]") + return ErrorResult(1) + } + + val options = Options() + options.addOption("i", "i", false, "Case-insensitive search.") + options.addOption("w", "w", false, "Whole word only search.") + options.addOption("A", "A", true, "Append {ARG} lines to the output.") + + val parser = DefaultParser() + val cmd = parser.parse(options, arguments.toTypedArray()) + + var pattern = cmd.argList.first() + val fileName = cmd.argList.last() + val file = File(fileName) + val optionsSet = mutableSetOf() + + if (cmd.hasOption("i")) { + optionsSet.add(RegexOption.IGNORE_CASE) + } + + if (cmd.hasOption("w")) { + pattern = "\\b$pattern\\b" + } + + var linesAfter = 0 + if (cmd.hasOption("A")) { + linesAfter = cmd.getOptionValue('A').toInt() + } + + try { + file.useLines { lines -> + val result = mutableListOf() + val linesList = lines.toList() + val excludeIndices = mutableSetOf() + for (i in linesList.indices) { + if (pattern.toRegex(optionsSet).containsMatchIn(linesList[i])) { + val end = (i + linesAfter).coerceAtMost(linesList.size - 1) + for (j in i..end) { + if (!excludeIndices.contains(j)) { + result.add(linesList[j]) + } + excludeIndices.add(j) + } + } + } + + result.forEach { outputStream.writeLineUTF8(it) } + } + } catch (e: IOException) { + errorStream.writeLineUTF8("File '$fileName' read exception, reason: ${e.message}") + return ErrorResult(1) + } + + return SuccessResult() + } +} diff --git a/core/src/test/kotlin/com/github/itmosoftwaredesign/cli/command/impl/GrepCommandTest.kt b/core/src/test/kotlin/com/github/itmosoftwaredesign/cli/command/impl/GrepCommandTest.kt new file mode 100644 index 0000000..fac40ad --- /dev/null +++ b/core/src/test/kotlin/com/github/itmosoftwaredesign/cli/command/impl/GrepCommandTest.kt @@ -0,0 +1,232 @@ +package com.github.itmosoftwaredesign.cli.command.impl + +import com.github.itmosoftwaredesign.cli.Environment +import io.mockk.mockk +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File + +class GrepCommandTest { + + private lateinit var grepCommand: GrepCommand + private lateinit var environment: Environment + private lateinit var outputStream: ByteArrayOutputStream + private lateinit var errorStream: ByteArrayOutputStream + private lateinit var inputStream: ByteArrayInputStream + + @BeforeEach + fun setUp() { + grepCommand = GrepCommand() + environment = mockk(relaxed = true) + outputStream = ByteArrayOutputStream() + errorStream = ByteArrayOutputStream() + } + + @Test + fun `should find lines in the file according to the pattern`() { + val tempFile = File.createTempFile("test", ".txt") + tempFile.writeText("" + + "this is a test file to test grep command\n" + + "THis is the second line ab\n" + + "is\n" + + "is\n" + + "is\n" + + "is\n" + + "abc\n" + + "ab\n" + ) + + val pattern = "this" + val res = grepCommand.execute( + environment, + ByteArrayInputStream(byteArrayOf()), + outputStream, + errorStream, + listOf(pattern, tempFile.absolutePath) + ) + + assertEquals("this is a test file to test grep command\n", outputStream.toString()) + assertTrue(errorStream.toString().isEmpty()) + + tempFile.deleteOnExit() + } + + @Test + fun `should find lines in the file according to the pattern with several options`() { + val tempFile = File.createTempFile("test", ".txt") + tempFile.writeText("" + + "this is a test file to test grep command\n" + + "THis is the second line ab\n" + + "is\n" + + "is\n" + + "is\n" + + "is\n" + + "abc\n" + + "ab\n" + ) + + val pattern = "this" + val res = grepCommand.execute( + environment, + ByteArrayInputStream(byteArrayOf()), + outputStream, + errorStream, + listOf(pattern, "-i", "-A", "1", tempFile.absolutePath) + ) + + assertEquals("this is a test file to test grep command\nTHis is the second line ab\nis\n", outputStream.toString()) + assertTrue(errorStream.toString().isEmpty()) + + tempFile.deleteOnExit() + } + + @Test + fun `should find lines in the file according to the pattern with -i option`() { + val tempFile = File.createTempFile("test", ".txt") + tempFile.writeText("" + + "this is a test file to test grep command\n" + + "THis is the second line ab\n" + + "is\n" + + "is\n" + + "is\n" + + "is\n" + + "abc\n" + + "ab\n" + ) + + val pattern = "this" + val res = grepCommand.execute( + environment, + ByteArrayInputStream(byteArrayOf()), + outputStream, + errorStream, + listOf(pattern, "-i", tempFile.absolutePath) + ) + + assertEquals("this is a test file to test grep command\nTHis is the second line ab\n", outputStream.toString()) + assertTrue(errorStream.toString().isEmpty()) + + tempFile.deleteOnExit() + } + + @Test + fun `should find lines in the file according to the pattern with -w option`() { + val tempFile = File.createTempFile("test", ".txt") + tempFile.writeText("" + + "this is a test file to test grep command\n" + + "THis is the second line ab\n" + + "is\n" + + "is\n" + + "is\n" + + "is\n" + + "abc\n" + + "ab\n" + ) + + val pattern = "ab" + val res = grepCommand.execute( + environment, + ByteArrayInputStream(byteArrayOf()), + outputStream, + errorStream, + listOf(pattern, "-w", tempFile.absolutePath) + ) + + assertEquals("THis is the second line ab\nab\n", outputStream.toString()) + assertTrue(errorStream.toString().isEmpty()) + + tempFile.deleteOnExit() + } + + @Test + fun `should find lines in the file with -A option`() { + val tempFile = File.createTempFile("test", ".txt") + val content = "" + + "this is a test file to test grep command\n" + + "THis is the second line ab\n" + + "is\n" + + "is\n" + + "is\n" + + "is\n" + + "abc\n" + + "ab\n" + tempFile.writeText(content) + + val pattern = "test" + val res = grepCommand.execute( + environment, + ByteArrayInputStream(byteArrayOf()), + outputStream, + errorStream, + listOf(pattern, "-A", "1", tempFile.absolutePath) + ) + + assertEquals("this is a test file to test grep command\nTHis is the second line ab\n", outputStream.toString()) + assertTrue(errorStream.toString().isEmpty()) + + tempFile.deleteOnExit() + } + + @Test + fun `should find lines in the file with -A option with lines intersection`() { + val tempFile = File.createTempFile("test", ".txt") + val content = "" + + "this is a test file to test grep command\n" + + "THis is the second line ab\n" + + "is\n" + + "is\n" + + "is\n" + + "is\n" + + "abc\n" + + "ab\n" + tempFile.writeText(content) + + val pattern = "is" + val res = grepCommand.execute( + environment, + ByteArrayInputStream(byteArrayOf()), + outputStream, + errorStream, + listOf(pattern, "-A", "2", tempFile.absolutePath) + ) + + assertEquals(content, outputStream.toString()) + assertTrue(errorStream.toString().isEmpty()) + + tempFile.deleteOnExit() + } + + @Test + fun `should write error message when file doesn't exist`() { + val nonExistentFile = "nonexistentfile.txt" + + grepCommand.execute( + environment, + ByteArrayInputStream(byteArrayOf()), + outputStream, + errorStream, + listOf("arg1", nonExistentFile) + ) + + assertTrue(errorStream.toString().contains("File '$nonExistentFile' read exception")) + } + + @Test + fun `should write error message when wrong number of arguments provided`() { + val nonExistentFile = "nonexistentfile.txt" + + grepCommand.execute( + environment, + ByteArrayInputStream(byteArrayOf()), + outputStream, + errorStream, + listOf(nonExistentFile) + ) + + assertEquals(errorStream.toString(), "usage: grep [-abcdDEFGHhIiJLlMmnOopqRSsUVvwXxZz] [-A num] [-i string] [-w string] [file ...]\n") + } +} \ No newline at end of file diff --git a/docs/classes.puml b/docs/classes.puml index 3c8aa07..5b4e6b5 100644 --- a/docs/classes.puml +++ b/docs/classes.puml @@ -1,13 +1,12 @@ @startuml class CLIInterpreter { - Environment environment - - CommandParser parser + - CommandsParser parser + run() } -class CommandParser { - + parse(String input): Command - + parsePipeline(String input): List +class CommandsParser { + + parse(String input): List } class Environment { @@ -31,13 +30,16 @@ class PwdCommand class ExitCommand -CLIInterpreter --> CommandParser +class GrepCommand + +CLIInterpreter --> CommandsParser CLIInterpreter --> Environment -CommandParser --> Command +CommandsParser --> Command Command <|-- EchoCommand Command <|-- CatCommand Command <|-- WcCommand Command <|-- PwdCommand Command <|-- ExitCommand +Command <|-- GrepCommand @enduml diff --git a/docs/images/classes.svg b/docs/images/classes.svg index b63b1c8..fb40301 100644 --- a/docs/images/classes.svg +++ b/docs/images/classes.svg @@ -1 +1 @@ -CLIInterpreterEnvironment environmentCommandParser parserrun()CommandParserparse(String input): CommandparsePipeline(String input): List<Command>EnvironmentsetVariable(String name, String value)getVariable(String name): StringgetWorkingDirectory(): StringsetWorkingDirectory(String newDirectory)Commandexecute(Environment environment, InputStream input, OutputStream output, List<String> args): voidEchoCommandCatCommandWcCommandPwdCommandExitCommand \ No newline at end of file +CLIInterpreterEnvironment environmentCommandsParser parserrun()CommandsParserparse(String input): List<Command>EnvironmentsetVariable(String name, String value)getVariable(String name): StringgetWorkingDirectory(): StringsetWorkingDirectory(String newDirectory)Commandexecute(Environment environment, InputStream input, OutputStream output, List<String> args): voidEchoCommandCatCommandWcCommandPwdCommandExitCommandGrepCommand diff --git a/docs/readme.md b/docs/readme.md index eb1bd2b..1b9fa1b 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -1,6 +1,7 @@ # Основные требования -- Поддержка команд `cat`, `echo`, `wc`, `pwd`, `exit`. +- Поддержка команд `cat`, `echo`, `wc`, `pwd`, `exit`,`grep` + - для `grep`: поддержка опций `-w`, `-i`, `-A {arg}` - Реализация оператора подстановки переменных окружения `$`. - Поддержка пайплайнов и цитирования (одинарные и двойные кавычки). - Возможность вызова внешних программ. @@ -13,6 +14,19 @@ - Поддержка пайплайнов (комбинация нескольких команд через `|`), чтобы корректно передавать вывод одной команды в качестве ввода другой. +# Зависимости + +* [Apache Commons CLI](https://commons.apache.org/proper/commons-cli/): Version: 1.9.0 + * Аналоги: + * Clikt + * kotlinx.cli - *obsolete* + * Args4j + * Picocli + * Apache Commons CLI выбрана потому, что она предоставляет более гибкий механизм парсинга аргументов, а именно, + не требует передачи аргументов при запуске программы и позволяет парсить любой список аргументов, + представленный в виде массива, что было уместно в нашей программе, так как после токенизации мы как раз получаем + готовый список аргументов. + # Определение подсистем Декомпозируем систему на несколько подсистем: