diff --git a/internal/shell/shell.go b/internal/shell/shell.go new file mode 100644 index 0000000..1cd0b51 --- /dev/null +++ b/internal/shell/shell.go @@ -0,0 +1,373 @@ +// 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) +}