fixed inner loop & added smart REPL
This commit is contained in:
+11
-11
@@ -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'.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,6 @@ class ExitCommand : Command {
|
||||
|
||||
override fun execute(args: List<String>) {
|
||||
println("Завершение работы.")
|
||||
exitProcess(0)
|
||||
exitProcess(0) //nizya
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user