Files
fush/internal/shell/shell.go
2026-05-22 00:26:27 +03:00

344 lines
9.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}