fixed inner loop & added smart REPL

This commit is contained in:
2026-03-19 13:34:48 +03:00
parent f41ecfbd2f
commit 60ca2d38f9
29 changed files with 249 additions and 81 deletions
+11 -11
View File
@@ -14,19 +14,17 @@ import runner.CommandInvoker
*/
fun main(args: Array<String>) {
// Проверяем аргументы
if (args.isEmpty()) {
System.err.println("Ошибка: укажите путь к файлу коллекции.")
System.err.println("Использование: java -jar lab5.jar <file.csv>")
return
}
var filePath = "data.csv"
val filePath = args[0]
if (args.isNotEmpty()) {
filePath = args[0]
}
// Включаем Nightcall
val fileManager = FileManager(filePath)
val manager = CollectionManager()
val inputManager : InputManager = ConsoleInputManager()
val invoker = CommandInvoker()
val inputManager = ConsoleInputManager()
// Загружаем данные из файла
val loaded = fileManager.read()
@@ -50,15 +48,17 @@ fun main(args: Array<String>) {
* @param invoker инвокер команд
*/
fun runRepl(inputManager: InputManager, invoker: CommandInvoker) {
while (true) {
if (inputManager.isInteractive) print("\n ☭ ") // genius idea™
var isRunning: Boolean = true
while (isRunning) {
val line = inputManager.readLine()
// EOF — завершаем без сохранения
if (line == null) {
println("\nНу ладно.")
break
isRunning = false
continue
}
if (line.isBlank()) continue
@@ -92,7 +92,7 @@ fun registerCommands(
RemoveByIdCommand(manager),
ClearCommand(manager),
SaveCommand(manager, fileManager),
ExecuteScriptCommand(invoker, activeScripts),
ExecuteScriptCommand(invoker),
ExitCommand(),
AddIfMaxCommand(manager, inputManager),
AddIfMinCommand(manager, inputManager),
@@ -1,6 +1,6 @@
package collection
import io.github.oshai.kotlinlogging.KotlinLogging
import mu.KotlinLogging
import models.HumanBeing
import java.time.LocalDate
@@ -27,7 +27,10 @@ class CollectionManager {
* @param humanBeing элемент для добавления
* @return true - если элемент был добавлен, false - если уже существует
*/
fun add(humanBeing: HumanBeing): Boolean = collection.add(humanBeing)
fun add(humanBeing: HumanBeing): Boolean {
logger.info { "[COMMANDS] {ADD} Создан элемент ${humanBeing.id}" }
return collection.add(humanBeing)
}
/**
* Обновляет элемент с указанным [id].
@@ -1,18 +1,18 @@
package commands
import input.ScriptInputManager
import runner.CommandInvoker
import runner.CommandQueue
/**
* Читает и выполняет команды из файла-скрипта.
* Синтаксис: execute_script <file_path>
* Считывает скрипт из файла в [CommandQueue], затем выполняет все команды.
*
* Защита от рекурсии: отслеживает стек вызванных скриптов.
* Фаза 1 (чтение): рекурсивно обходит все вложенные execute_script,
* собирает команды в очередь.
* Фаза 2 (выполнение): последовательно выполняет команды из очереди.
*
* Такой подход гарантирует, что все файлы прочитаны до начала выполнения.
*/
class ExecuteScriptCommand(
private val invoker: CommandInvoker,
private val activeScripts: MutableSet<String> = mutableSetOf()
) : Command {
class ExecuteScriptCommand(private val invoker: CommandInvoker) : Command {
override val name = "execute_script"
override val description = "выполнить скрипт из файла: execute_script <file>"
@@ -21,37 +21,28 @@ class ExecuteScriptCommand(
println("[Ошибка] Укажите путь к файлу. Пример: execute_script script.txt")
return
}
val filePath = args[0]
if (filePath in activeScripts) {
println("[Ошибка] Обнаружена рекурсия: скрипт '$filePath' уже выполняется.")
// я манал это всё
println("Чтение скрипта '$filePath'...")
val queue = CommandQueue()
queue.loadFromScript(filePath)
println("Команды в очереди: ${queue.peek()}") // временно
if (queue.isEmpty()) {
println("Текст Скриптонита пустой или не найден.")
return
}
val scriptInput = try {
ScriptInputManager(filePath)
} catch (e: Exception) {
println("[Ошибка] Не удалось открыть файл '$filePath': ${e.message}")
return
println("Прочитано команд: ${queue.size()}. Начинаем выполнение...")
while (!queue.isEmpty()) {
val line = queue.dequeue() ?: break
println(" [скриптонит] > $line")
invoker.execute(line)
}
activeScripts.add(filePath)
println("Выполнение скрипта '$filePath'...")
try {
var line = scriptInput.readLine()
while (line != null) {
if (line.isNotBlank()) {
println(" [скрипт] > $line")
invoker.execute(line)
}
line = scriptInput.readLine()
}
} catch (e: IllegalStateException) {
println("[Ошибка] ${e.message}")
} finally {
activeScripts.remove(filePath)
println("Скрипт '$filePath' завершён.")
}
println("Скриптонит зачитал '$filePath'.")
}
}
+1 -1
View File
@@ -11,6 +11,6 @@ class ExitCommand : Command {
override fun execute(args: List<String>) {
println("Завершение работы.")
exitProcess(0)
exitProcess(0) //nizya
}
}
+32 -12
View File
@@ -1,21 +1,41 @@
package input
import java.io.BufferedReader
import java.io.InputStreamReader
import org.jline.reader.EndOfFileException
import org.jline.reader.LineReaderBuilder
import org.jline.reader.UserInterruptException
import org.jline.reader.impl.completer.StringsCompleter
import org.jline.terminal.TerminalBuilder
/**
* Реализация [InputManager] для чтения из стандартного потока ввода.
* Использует [BufferedReader] с кодировкой системы по умолчанию,
* что обеспечивает корректную работу на Windows и Linux.
* Без этого - сломается. Не трогать!
* Реализация [InputManager] для чтения из стандартного потока ввода на основе JLine3.
* Поддерживает историю команд (стрелки ↑↓), Tab-автодополнение,
* корректную обработку Ctrl+C и Ctrl+D.
*/
class ConsoleInputManager : InputManager {
override val isInteractive: Boolean = true
class ConsoleInputManager() : InputManager {
override val isInteractive = true
private val reader = BufferedReader(
InputStreamReader(System.`in`, Charsets.UTF_8)
)
private val terminal = TerminalBuilder.builder()
.system(true)
.build()
override fun readLine(): String? = reader.readLine()
private val reader = LineReaderBuilder.builder()
.terminal(terminal)
.variable("history-size", 100)
.build()
/**
* Читает строку с промптом ☭.
* Возвращает null при Ctrl+D (EOF) или Ctrl+C.
*/
override fun readLine(): String? {
return try {
reader.readLine("\n ☭ ")
} catch (e: EndOfFileException) {
null // Ctrl+D
} catch (e: UserInterruptException) {
null // Ctrl+C
}
}
}
+4 -2
View File
@@ -31,8 +31,8 @@ class CommandInvoker {
*/
fun execute(input: String) {
val parts = input.trim().split(" ")
val name = parts[0].lowercase()
val args = parts.drop(1)
val name = parts[0].lowercase().trim()
val args = parts.drop(1)
val command = commands[name]
if (command == null) {
@@ -62,4 +62,6 @@ class CommandInvoker {
if (history.size >= 12) history.removeFirst()
history.addLast(name)
}
fun getCommandNames(): List<String> = commands.keys.toList()
}
+96
View File
@@ -0,0 +1,96 @@
package runner
import java.io.File
import java.util.Scanner
/**
* Очередь команд, собранных из файла-скрипта.
*
* Фаза 1: [loadFromScript] — рекурсивно читает файл(ы) и заполняет очередь.
* Фаза 2: [dequeue] — последовательно отдаёт команды на выполнение.
*
* Вложенные вызовы [EXECUTE_SCRIPT_CMD] раскрываются сразу при чтении —
* команды вставляются в очередь в порядке встречи.
*/
class CommandQueue {
companion object {
private const val EXECUTE_SCRIPT_CMD = "execute_script"
}
private val queue = ArrayDeque<String>()
/** Количество команд в очереди. */
fun size(): Int = queue.size
/** Возвращает true если очередь пуста. */
fun isEmpty(): Boolean = queue.isEmpty()
/**
* Рекурсивно читает скрипт и все вложенные скрипты,
* заполняя очередь командами в порядке встречи.
*
* Защита от бесконечной рекурсии — через [visitedScripts].
*
* @param filePath путь к файлу-скрипту
* @param visitedScripts стек уже посещённых файлов (для защиты от рекурсии)
*/
fun loadFromScript(
filePath: String,
visitedScripts: MutableSet<String> = mutableSetOf()
) {
// Защита от рекурсии
if (filePath in visitedScripts) {
println("[CommandQueue] Рекурсия обнаружена: '$filePath' уже в стеке, пропускаем.")
return
}
val file = File(filePath)
if (!file.exists()) {
println("[CommandQueue] Файл не найден: '$filePath'")
return
}
if (!file.canRead()) {
println("[CommandQueue] Нет прав на чтение: '$filePath'")
return
}
// а вдруг вложил
val parentDir = file.parentFile ?: File(".")
visitedScripts.add(filePath)
Scanner(file, Charsets.UTF_8).use { scanner ->
generateSequence { if (scanner.hasNextLine()) scanner.nextLine() else null }
.map { it.trim() }
.filter { it.isNotBlank() }
.forEach { line ->
val parts = line.split("\\s+".toRegex())
if (parts[0].lowercase() == EXECUTE_SCRIPT_CMD && parts.size > 1) {
val nestedPath = File(parentDir, parts[1]).path
if (!File(nestedPath).exists()) {
println("[CommandQueue] Файл не найден: '$nestedPath'")
return
}
loadFromScript(nestedPath, visitedScripts)
} else {
queue.addLast(line)
}
}
}
visitedScripts.remove(filePath)
}
/**
* Извлекает следующую команду из начала очереди.
* @return строка команды или null если очередь пуста
*/
fun dequeue(): String? = queue.removeFirstOrNull()
/**
* Возвращает все команды в очереди (без удаления).
* Полезно для отладки.
*/
fun peek(): List<String> = queue.toList()
}
+32 -13
View File
@@ -1,25 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_DIR" value="logs"/>
<property name="LOG_FILE" value="application.log"/>
<appender name="ALL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/all.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/archived/all-%d{yyyy-MM-dd_HH-mm}.%i.log</fileNamePattern>
<maxFileSize>5MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>100MB</totalSizeCap>
</rollingPolicy>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${LOG_DIR}/${LOG_FILE}</file>
<append>true</append>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/errors.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/archived/errors-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>2MB</maxFileSize>
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
<root level="DEBUG">
<appender-ref ref="ALL_FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</configuration>
</configuration>