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 -2
View File
@@ -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")
}
-2
View File
@@ -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
1 id name coordX coordY creationDate realHero hasToothpick impactSpeed soundtrackName minutesOfWaiting weaponType carCool
2 600f12bc-94da-4c83-834b-cc95d780328c gosling 45 36 2026-03-07 true true 999.0 RealHero 90.0 HAMMER true
+7
View File
@@ -0,0 +1,7 @@
создать базовый файл +
Проверять, что файл еще существует
починить логи +
проверять недоступные директории при загрузке и сохранении
+1
View File
@@ -0,0 +1 @@
2026-03-19 12:49:46.830 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
+1
View File
@@ -0,0 +1 @@
2026-03-07 15:27:16.645 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
+1
View File
@@ -0,0 +1 @@
2026-03-07 15:29:36.592 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
+2
View 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} Возвращена коллекция
+3
View File
@@ -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} Возвращена коллекция
+1
View File
@@ -0,0 +1 @@
2026-03-07 15:47:19.407 [main] INFO collection.CollectionManager - [COMMANDS] {GET_ALL} Возвращена коллекция
+1
View File
@@ -0,0 +1 @@
2026-03-07 15:49:52.504 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
+3
View 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} Возвращена коллекция
+1
View File
@@ -0,0 +1 @@
2026-03-07 15:55:20.905 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
+1
View File
@@ -0,0 +1 @@
2026-03-19 12:31:36.549 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
+2
View 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
+1
View File
@@ -0,0 +1 @@
2026-03-19 12:43:02.028 [main] INFO collection.CollectionManager - [COMMANDS] {LOAD_FROM_FILE} Загрузка коллекции из файла
+3
View 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} Проверка наличия элементов в коллекции
+2
View File
@@ -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} Проверка наличия элементов в коллекции
View File
View File
+3
View File
@@ -0,0 +1,3 @@
info
add
help
+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()}. Начинаем выполнение...")
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'.")
}
}
+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
}
}
}
+3 -1
View File
@@ -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()
}
+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()
}
+31 -12
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>
-3
View File
@@ -1,3 +0,0 @@
info
execute_script sample
help