Files
fush/internal/shell/shell.go

371 lines
9.7 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"
"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)
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":
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)
}