Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>
): 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<RegexOption>()

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<String>()
val linesList = lines.toList()
val excludeIndices = mutableSetOf<Int>()
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()
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
}
14 changes: 8 additions & 6 deletions docs/classes.puml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
@startuml
class CLIInterpreter {
- Environment environment
- CommandParser parser
- CommandsParser parser
+ run()
}

class CommandParser {
+ parse(String input): Command
+ parsePipeline(String input): List<Command>
class CommandsParser {
+ parse(String input): List<Command>
}

class Environment {
Expand All @@ -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
2 changes: 1 addition & 1 deletion docs/images/classes.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading