fixed inner loop & added smart REPL
This commit is contained in:
+11
-2
@@ -15,8 +15,9 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:7.0.3")
|
||||
implementation("org.slf4j:slf4j-simple:2.0.3")
|
||||
implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
|
||||
implementation("ch.qos.logback:logback-classic:1.5.7")
|
||||
implementation("org.jline:jline:3.27.1")
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
|
||||
@@ -26,6 +27,9 @@ dependencies {
|
||||
|
||||
application {
|
||||
mainClass.set("MainKt")
|
||||
applicationDefaultJvmArgs = listOf(
|
||||
"--enable-native-access=ALL-UNNAMED"
|
||||
)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
@@ -48,3 +52,8 @@ tasks.named<ShadowJar>("shadowJar") {
|
||||
tasks.named("build") {
|
||||
dependsOn("shadowJar")
|
||||
}
|
||||
|
||||
// да, я ленивый
|
||||
tasks.withType<JavaExec> {
|
||||
jvmArgs("--enable-native-access=ALL-UNNAMED")
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
id,name,coordX,coordY,creationDate,realHero,hasToothpick,impactSpeed,soundtrackName,minutesOfWaiting,weaponType,carCool
|
||||
600f12bc-94da-4c83-834b-cc95d780328c,gosling,45,36,2026-03-07,true,true,999.0,RealHero,90.0,HAMMER,true
|
||||
|
@@ -0,0 +1,7 @@
|
||||
создать базовый файл +
|
||||
|
||||
Проверять, что файл еще существует
|
||||
|
||||
починить логи +
|
||||
|
||||
проверять недоступные директории при загрузке и сохранении
|
||||
@@ -0,0 +1 @@
|
||||
2026-03-19 12:49:46.830 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
|
||||
@@ -0,0 +1 @@
|
||||
2026-03-07 15:27:16.645 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
|
||||
@@ -0,0 +1 @@
|
||||
2026-03-07 15:29:36.592 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-03-07 15:30:11.520 [main] INFO collection.CollectionManager - [COMMANDS] {ADD} Создан элемент fa80e6c1-3c21-4338-bf8a-947aed2ef1e8
|
||||
2026-03-07 15:30:16.498 [main] INFO collection.CollectionManager - [COMMANDS] {GET_ALL} Возвращена коллекция
|
||||
@@ -0,0 +1,3 @@
|
||||
2026-03-07 15:44:08.317 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
|
||||
2026-03-07 15:44:49.883 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
|
||||
2026-03-07 15:44:51.401 [main] INFO collection.CollectionManager - [COMMANDS] {GET_ALL} Возвращена коллекция
|
||||
@@ -0,0 +1 @@
|
||||
2026-03-07 15:47:19.407 [main] INFO collection.CollectionManager - [COMMANDS] {GET_ALL} Возвращена коллекция
|
||||
@@ -0,0 +1 @@
|
||||
2026-03-07 15:49:52.504 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
|
||||
@@ -0,0 +1,3 @@
|
||||
2026-03-07 15:50:27.978 [main] INFO collection.CollectionManager - [COMMANDS] {ADD} Создан элемент 498bb6d8-ccb9-47cc-9501-796e969f3f75
|
||||
2026-03-07 15:50:30.341 [main] INFO collection.CollectionManager - [COMMANDS] {GET_ALL} Возвращена коллекция
|
||||
2026-03-07 15:50:47.498 [main] INFO collection.CollectionManager - [COMMANDS] {GET_ALL} Возвращена коллекция
|
||||
@@ -0,0 +1 @@
|
||||
2026-03-07 15:55:20.905 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
|
||||
@@ -0,0 +1 @@
|
||||
2026-03-19 12:31:36.549 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-03-19 12:32:01.504 [main] INFO collection.CollectionManager - [COMMANDS] {GET_INFO} Получение информации о коллекции
|
||||
2026-03-19 12:32:20.582 [main] INFO collection.CollectionManager - [COMMANDS] {ADD} Создан элемент a61d60d6-289c-49c5-946d-11872ab9d910
|
||||
@@ -0,0 +1 @@
|
||||
2026-03-19 12:43:02.028 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
|
||||
@@ -0,0 +1,3 @@
|
||||
2026-03-19 12:45:12.830 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
|
||||
2026-03-19 12:45:20.553 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
|
||||
2026-03-19 12:45:25.798 [main] INFO collection.CollectionManager - [COMMANDS] {IS_EMPTY} Проверка наличия элементов в коллекции
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-03-19 12:47:09.609 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
|
||||
2026-03-19 12:47:39.356 [main] INFO collection.CollectionManager - [COMMANDS] {IS_EMPTY} Проверка наличия элементов в коллекции
|
||||
@@ -0,0 +1,3 @@
|
||||
info
|
||||
add
|
||||
help
|
||||
+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()}. Начинаем выполнение...")
|
||||
|
||||
activeScripts.add(filePath)
|
||||
println("Выполнение скрипта '$filePath'...")
|
||||
|
||||
try {
|
||||
var line = scriptInput.readLine()
|
||||
while (line != null) {
|
||||
if (line.isNotBlank()) {
|
||||
println(" [скрипт] > $line")
|
||||
while (!queue.isEmpty()) {
|
||||
val line = queue.dequeue() ?: break
|
||||
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,7 +31,7 @@ class CommandInvoker {
|
||||
*/
|
||||
fun execute(input: String) {
|
||||
val parts = input.trim().split(" ")
|
||||
val name = parts[0].lowercase()
|
||||
val name = parts[0].lowercase().trim()
|
||||
val args = parts.drop(1)
|
||||
|
||||
val command = commands[name]
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user