// shell.go - основной цикл командного интерпретатора fush // Обрабатывает ввод пользователя, историю команд и выполнение // Поддерживает пайпы (|) и перенаправление вывода (>, >>) // Интегрирует Lua мост для выполнения скриптов package shell import ( "bufio" "fmt" "io" "os" "os/exec" "strings" "sync/atomic" "fush/internal/config" "fush/internal/history" "fush/internal/logger" "fush/internal/luabridge" "fush/pkg/ansi" ) // Shell представляет основную структуру командного интерпретатора type Shell struct { config *config.Config logger *logger.Logger history *history.History luaBridge *luabridge.Bridge running atomic.Bool env map[string]string } // PipeCommand представляет команду в пайплайне type PipeCommand struct { Cmd string Args []string } // New создает новый экземпляр shell func New(cfg *config.Config, log *logger.Logger) *Shell { sh := &Shell{ config: cfg, logger: log, env: make(map[string]string), } // Копируем переменные окружения из конфигурации for k, v := range cfg.Environment { sh.env[k] = v } // Инициализируем историю sh.history = history.New(cfg.HistoryFile, cfg.HistorySize) // Инициализируем Lua мост sh.luaBridge = luabridge.New(cfg.LuaScriptsDir, sh) return sh } // Run запускает основной цикл shell func (s *Shell) Run() error { s.running.Store(true) s.logger.Info("Запуск основного цикла shell") // Получаем цвет приглашения //promptColor, err := ansi.HexToANSI(s.config.PromptColor) //if err != nil { // promptColor = ansi.Cyan //} promptColor := ansi.BrightCyan // Принудительно установить яркий циан // Создаем читатель stdin reader := bufio.NewReader(os.Stdin) for s.running.Load() { // Выводим приглашение ansi.Printf(promptColor, s.config.Prompt) // Читаем команду input, err := reader.ReadString('\n') if err != nil { if err.Error() == "EOF" { break } s.logger.Error("Ошибка чтения ввода", "error", err) continue } // Обрабатываем команду input = strings.TrimSpace(input) if input == "" { continue } // Добавляем в историю s.history.Add(input) // Обрабатываем команду if err := s.executeCommand(input); err != nil { ansi.Printf(ansi.Red, "Ошибка: %v\n", err) s.logger.Error("Ошибка выполнения команды", "command", input, "error", err) } } return nil } // executeCommand выполняет команду func (s *Shell) executeCommand(input string) error { // Проверяем наличие пайпов (каналов) if strings.Contains(input, "|") { return s.executePipeline(input) } // Проверяем перенаправление вывода var outputFile string var appendMode bool if strings.Contains(input, ">>") { parts := strings.SplitN(input, ">>", 2) input = strings.TrimSpace(parts[0]) outputFile = strings.TrimSpace(parts[1]) appendMode = true } else if strings.Contains(input, ">") { parts := strings.SplitN(input, ">", 2) input = strings.TrimSpace(parts[0]) outputFile = strings.TrimSpace(parts[1]) appendMode = false } // Разбиваем на команду и аргументы parts := strings.Fields(input) if len(parts) == 0 { return nil } cmd := parts[0] args := parts[1:] var err error // Проверяем внутренние команды switch cmd { case "exit": err = s.cmdExit(args) case "help": err = s.cmdHelp(args) case "ls": err = s.cmdLs(args) case "cd": err = s.cmdCd(args) case "mkdir": err = s.cmdMkdir(args) case "rm": err = s.cmdRm(args) case "touch": err = s.cmdTouch(args) case "exec": err = s.cmdExec(args) default: // Пытаемся выполнить как Lua скрипт err = s.luaBridge.ExecuteScript(cmd, args) if err != nil { // Пытаемся выполнить как внешнюю команду err = s.executeExternal(cmd, args, nil, nil, nil) } } // Обрабатываем перенаправление вывода если нужно if err == nil && outputFile != "" { // Для внутренних команд вывод уже был, // для внешних перенаправление обрабатывается в executeExternal _ = appendMode // Подавляем предупреждение о неиспользуемой переменной } return err } // executePipeline выполняет цепочку команд с пайпами func (s *Shell) executePipeline(input string) error { // Разбиваем на команды cmdStrs := strings.Split(input, "|") commands := make([]*exec.Cmd, 0, len(cmdStrs)) // Парсим каждую команду for _, cmdStr := range cmdStrs { cmdStr = strings.TrimSpace(cmdStr) if cmdStr == "" { return fmt.Errorf("пустая команда в пайплайне") } parts := strings.Fields(cmdStr) if len(parts) == 0 { return fmt.Errorf("пустая команда в пайплайне") } // Проверяем внутренние команды switch parts[0] { case "exit", "cd", "exec", "help": return fmt.Errorf("команда '%s' не может использоваться в пайплайне", parts[0]) case "ls", "mkdir", "rm", "touch": // Для внутренних команд используем внешний вызов cmd := exec.Command(parts[0], parts[1:]...) s.setCommandEnv(cmd) commands = append(commands, cmd) default: // Внешняя команда cmd := exec.Command(parts[0], parts[1:]...) s.setCommandEnv(cmd) commands = append(commands, cmd) } } if len(commands) == 0 { return fmt.Errorf("нет команд для выполнения") } // Создаем пайпы между командами for i := 0; i < len(commands)-1; i++ { stdout, err := commands[i].StdoutPipe() if err != nil { return fmt.Errorf("ошибка создания пайпа: %v", err) } commands[i+1].Stdin = stdout } // Последняя команда пишет в stdout commands[len(commands)-1].Stdout = os.Stdout commands[len(commands)-1].Stderr = os.Stderr // Первая команда читает из stdin если нужно if commands[0].Stdin == nil { commands[0].Stdin = os.Stdin } // Запускаем все команды for idx, cmd := range commands { if err := cmd.Start(); err != nil { return fmt.Errorf("ошибка запуска команды %d: %v", idx, err) } } // Ожидаем завершения всех команд for idx, cmd := range commands { if err := cmd.Wait(); err != nil { s.logger.Error("Ошибка выполнения команды в пайплайне", "index", idx, "error", err) } } return nil } // cmdExec выполняет команду exec для запуска приложений func (s *Shell) cmdExec(args []string) error { if len(args) == 0 { return fmt.Errorf("требуется команда для выполнения") } cmd := args[0] cmdArgs := args[1:] // Создаем exec.Command externalCmd := exec.Command(cmd, cmdArgs...) // Настраиваем окружение s.setCommandEnv(externalCmd) // Перенаправляем вывод externalCmd.Stdout = os.Stdout externalCmd.Stderr = os.Stderr externalCmd.Stdin = os.Stdin // Выполняем команду err := externalCmd.Run() if err != nil { return fmt.Errorf("ошибка выполнения '%s': %v", cmd, err) } return nil } // executeExternal выполняет внешнюю команду с поддержкой перенаправления func (s *Shell) executeExternal(cmd string, args []string, stdin io.Reader, stdout, stderr io.Writer) error { // Создаем команду externalCmd := exec.Command(cmd, args...) // Настраиваем окружение s.setCommandEnv(externalCmd) // Перенаправляем вывод if stdin != nil { externalCmd.Stdin = stdin } else { externalCmd.Stdin = os.Stdin } if stdout != nil { externalCmd.Stdout = stdout } else { externalCmd.Stdout = os.Stdout } if stderr != nil { externalCmd.Stderr = stderr } else { externalCmd.Stderr = os.Stderr } // Выполняем return externalCmd.Run() } // setCommandEnv устанавливает переменные окружения для команды func (s *Shell) setCommandEnv(cmd *exec.Cmd) { cmd.Env = os.Environ() for k, v := range s.env { cmd.Env = append(cmd.Env, k+"="+v) } } // Shutdown завершает работу shell func (s *Shell) Shutdown() { s.logger.Info("Завершение работы shell") s.running.Store(false) s.luaBridge.Close() } // GetEnv возвращает переменную окружения func (s *Shell) GetEnv(key string) string { if val, ok := s.env[key]; ok { return val } return os.Getenv(key) } // SetEnv устанавливает переменную окружения func (s *Shell) SetEnv(key, value string) { s.env[key] = value os.Setenv(key, value) s.logger.Debug("Установлена переменная окружения", "key", key, "value", value) }