// shell.go - основной цикл командного интерпретатора fush // Обрабатывает ввод пользователя, историю команд и выполнение // Поддерживает пайпы (|) и перенаправление вывода (>, >>) // Интегрирует Lua мост для выполнения скриптов // Включает встроенную команду mycurl для скачивания файлов package shell import ( "bufio" "fmt" "io" "os" "os/exec" "path/filepath" "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 := ansi.BrightCyan 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 { // Проверяем перенаправление вывода ДО обработки пайпов var outputFile string var appendMode bool // Проверяем >> (добавление) if idx := strings.Index(input, ">>"); idx != -1 { outputFile = strings.TrimSpace(input[idx+2:]) input = strings.TrimSpace(input[:idx]) appendMode = true } else if idx := strings.Index(input, ">"); idx != -1 { outputFile = strings.TrimSpace(input[idx+1:]) input = strings.TrimSpace(input[:idx]) appendMode = false } // Проверяем наличие пайпов (каналов) if strings.Contains(input, "|") { return s.executePipeline(input, outputFile, appendMode) } // Разбиваем на команду и аргументы parts := strings.Fields(input) if len(parts) == 0 { return nil } cmd := parts[0] args := parts[1:] var err error var output []byte // Проверяем внутренние команды switch cmd { case "exit": err = s.cmdExit(args) case "help": err = s.cmdHelp(args) case "ls": output, err = s.cmdLs(args) case "cd": err = s.cmdCd(args) case "pwd": output, err = s.cmdPwd(args) case "mkdir": err = s.cmdMkdir(args) case "rm": err = s.cmdRm(args) case "touch": err = s.cmdTouch(args) case "cat": output, err = s.cmdCat(args) case "echo": output, err = s.cmdEcho(args) case "exec": err = s.cmdExec(args) case "mycurl", "curl": // Поддержка обоих имен err = s.cmdMycurl(args) default: // Проверяем существование Lua скрипта ПО ПУТИ scriptPath := s.findLuaScript(cmd) if scriptPath != "" { // Выполняем Lua скрипт err = s.luaBridge.ExecuteScriptPath(scriptPath, args) if err == nil { return nil } } // Пытаемся выполнить как внешнюю команду err = s.executeExternal(cmd, args, nil, nil, nil) } // Обрабатываем перенаправление вывода для внутренних команд if err == nil && outputFile != "" && output != nil { var file *os.File var openErr error if appendMode { file, openErr = os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) } else { file, openErr = os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) } if openErr != nil { return fmt.Errorf("ошибка открытия файла '%s': %v", outputFile, openErr) } defer file.Close() _, writeErr := file.Write(output) if writeErr != nil { return fmt.Errorf("ошибка записи в файл '%s': %v", outputFile, writeErr) } } return err } // findLuaScript ищет Lua скрипт в директории скриптов func (s *Shell) findLuaScript(name string) string { // Проверяем с расширением .lua и без possibleNames := []string{name, name + ".lua"} for _, n := range possibleNames { fullPath := filepath.Join(s.config.LuaScriptsDir, n) if _, err := os.Stat(fullPath); err == nil { return fullPath } } return "" } // executePipeline выполняет цепочку команд с пайпами func (s *Shell) executePipeline(input string, outputFile string, appendMode bool) 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", "mycurl", "curl": return fmt.Errorf("команда '%s' не может использоваться в пайплайне", parts[0]) 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 } // Настройка вывода последней команды if outputFile != "" { var file *os.File var err error if appendMode { file, err = os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) } else { file, err = os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) } if err != nil { return fmt.Errorf("ошибка открытия файла '%s': %v", outputFile, err) } defer file.Close() commands[len(commands)-1].Stdout = file } else { commands[len(commands)-1].Stdout = os.Stdout } commands[len(commands)-1].Stderr = os.Stderr 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:] 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) }