371 lines
9.7 KiB
Go
371 lines
9.7 KiB
Go
|
|
// 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)
|
|||
|
|
}
|