diff --git a/internal/luabridge/luabridge.go b/internal/luabridge/luabridge.go new file mode 100644 index 0000000..86afd5e --- /dev/null +++ b/internal/luabridge/luabridge.go @@ -0,0 +1,344 @@ +// luabridge.go - интеграция Lua как скриптового языка в fush shell +// Предоставляет API для вызова команд shell из Lua скриптов +// Регистрирует функции exec, pipe, ls, cd, pwd в окружении Lua +// Позволяет создавать сложные скрипты для автоматизации задач + +package luabridge + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "sync/atomic" + + lua "github.com/yuin/gopher-lua" +) + +// ShellInterface определяет интерфейс для взаимодействия с shell +type ShellInterface interface { + GetEnv(key string) string + SetEnv(key, value string) +} + +// Bridge управляет выполнением Lua скриптов +type Bridge struct { + scriptsDir string + shell ShellInterface + state *lua.LState + closed atomic.Bool +} + +// New создает новый Lua мост +func New(scriptsDir string, shell ShellInterface) *Bridge { + b := &Bridge{ + scriptsDir: scriptsDir, + shell: shell, + } + + // Создаем Lua состояние + b.state = lua.NewState() + + // Регистрируем функции для Lua + b.registerFunctions() + + return b +} + +// registerFunctions регистрирует функции для Lua +func (b *Bridge) registerFunctions() { + // Регистрируем функцию print + b.state.SetGlobal("print", b.state.NewFunction(func(L *lua.LState) int { + args := make([]interface{}, L.GetTop()) + for i := 1; i <= L.GetTop(); i++ { + args[i-1] = L.Get(i) + } + fmt.Println(args...) + return 0 + })) + + // Регистрируем функцию getenv + b.state.SetGlobal("getenv", b.state.NewFunction(func(L *lua.LState) int { + key := L.CheckString(1) + value := b.shell.GetEnv(key) + L.Push(lua.LString(value)) + return 1 + })) + + // Регистрируем функцию setenv + b.state.SetGlobal("setenv", b.state.NewFunction(func(L *lua.LState) int { + key := L.CheckString(1) + value := L.CheckString(2) + b.shell.SetEnv(key, value) + return 0 + })) + + // Регистрируем функцию exec для выполнения команд из Lua + b.state.SetGlobal("exec", b.state.NewFunction(func(L *lua.LState) int { + cmd := L.CheckString(1) + args := make([]string, 0) + + // Собираем аргументы + for i := 2; i <= L.GetTop(); i++ { + args = append(args, L.CheckString(i)) + } + + // Выполняем команду + externalCmd := exec.Command(cmd, args...) + externalCmd.Stdout = os.Stdout + externalCmd.Stderr = os.Stderr + + err := externalCmd.Run() + if err != nil { + L.Push(lua.LBool(false)) + L.Push(lua.LString(err.Error())) + return 2 + } + + L.Push(lua.LBool(true)) + return 1 + })) + + // Регистрируем функцию exec_output для получения вывода команды + b.state.SetGlobal("exec_output", b.state.NewFunction(func(L *lua.LState) int { + cmd := L.CheckString(1) + args := make([]string, 0) + + for i := 2; i <= L.GetTop(); i++ { + args = append(args, L.CheckString(i)) + } + + externalCmd := exec.Command(cmd, args...) + output, err := externalCmd.Output() + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(err.Error())) + return 2 + } + + L.Push(lua.LString(string(output))) + return 1 + })) + + // Регистрируем функцию pipe для выполнения команд с пайпом + b.state.SetGlobal("pipe", b.state.NewFunction(func(L *lua.LState) int { + cmd1 := L.CheckString(1) + cmd2 := L.CheckString(2) + + // Разбиваем команды на части + parts1 := strings.Fields(cmd1) + parts2 := strings.Fields(cmd2) + + if len(parts1) == 0 || len(parts2) == 0 { + L.Push(lua.LBool(false)) + L.Push(lua.LString("пустая команда")) + return 2 + } + + // Создаем команды + cmd1Exec := exec.Command(parts1[0], parts1[1:]...) + cmd2Exec := exec.Command(parts2[0], parts2[1:]...) + + // Создаем пайп + stdout, err := cmd1Exec.StdoutPipe() + if err != nil { + L.Push(lua.LBool(false)) + L.Push(lua.LString(err.Error())) + return 2 + } + + cmd2Exec.Stdin = stdout + cmd2Exec.Stdout = os.Stdout + cmd2Exec.Stderr = os.Stderr + + // Запускаем команды + if err := cmd1Exec.Start(); err != nil { + L.Push(lua.LBool(false)) + L.Push(lua.LString(err.Error())) + return 2 + } + + if err := cmd2Exec.Start(); err != nil { + L.Push(lua.LBool(false)) + L.Push(lua.LString(err.Error())) + return 2 + } + + // Ожидаем завершения + cmd1Exec.Wait() + cmd2Exec.Wait() + + L.Push(lua.LBool(true)) + return 1 + })) + + // Регистрируем функцию ls + b.state.SetGlobal("ls", b.state.NewFunction(func(L *lua.LState) int { + path := "." + if L.GetTop() > 0 { + path = L.CheckString(1) + } + + dir, err := os.Open(path) + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(err.Error())) + return 2 + } + defer dir.Close() + + files, err := dir.Readdir(-1) + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(err.Error())) + return 2 + } + + // Создаем таблицу с результатами + tbl := L.NewTable() + for _, file := range files { + tbl.Append(lua.LString(file.Name())) + } + + L.Push(tbl) + return 1 + })) + + // Регистрируем функцию cd + b.state.SetGlobal("cd", b.state.NewFunction(func(L *lua.LState) int { + path := L.GetTop() > 0 + if !path { + path = true + // Передаем true, но нужен путь + _ = path + } + // Простая реализация cd через shell + dir := b.shell.GetEnv("HOME") + if L.GetTop() > 0 { + dir = L.CheckString(1) + } + + if err := os.Chdir(dir); err != nil { + L.Push(lua.LBool(false)) + L.Push(lua.LString(err.Error())) + return 2 + } + + L.Push(lua.LBool(true)) + return 1 + })) + + // Регистрируем функцию pwd + b.state.SetGlobal("pwd", b.state.NewFunction(func(L *lua.LState) int { + dir, err := os.Getwd() + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(err.Error())) + return 2 + } + + L.Push(lua.LString(dir)) + return 1 + })) +} + +// ExecuteScript выполняет Lua скрипт +func (b *Bridge) ExecuteScript(name string, args []string) error { + if b.closed.Load() { + return fmt.Errorf("мост закрыт") + } + + // Формируем путь к скрипту + scriptPath := filepath.Join(b.scriptsDir, name+".lua") + + // Проверяем существование файла + if _, err := os.Stat(scriptPath); os.IsNotExist(err) { + return err + } + + // Загружаем и выполняем скрипт + if err := b.state.DoFile(scriptPath); err != nil { + return err + } + + // Вызываем функцию main если она существует + if fn := b.state.GetGlobal("main"); fn.Type() == lua.LTFunction { + // Преобразуем аргументы в Lua значения + luaArgs := make([]lua.LValue, len(args)) + for i, arg := range args { + luaArgs[i] = lua.LString(arg) + } + + if err := b.state.CallByParam(lua.P{ + Fn: fn, + NRet: 0, + Protect: true, + }, luaArgs...); err != nil { + return err + } + } + + return nil +} + +// ExecuteScriptPath выполняет Lua скрипт по полному пути (НОВЫЙ МЕТОД) +func (b *Bridge) ExecuteScriptPath(scriptPath string, args []string) error { + if b.closed.Load() { + return fmt.Errorf("мост закрыт") + } + + // Проверяем существование файла + if _, err := os.Stat(scriptPath); os.IsNotExist(err) { + return err + } + + // Загружаем и выполняем скрипт + if err := b.state.DoFile(scriptPath); err != nil { + return err + } + + // Вызываем функцию main если она существует + if fn := b.state.GetGlobal("main"); fn.Type() == lua.LTFunction { + // Преобразуем аргументы в Lua значения + luaArgs := make([]lua.LValue, len(args)) + for i, arg := range args { + luaArgs[i] = lua.LString(arg) + } + + if err := b.state.CallByParam(lua.P{ + Fn: fn, + NRet: 0, + Protect: true, + }, luaArgs...); err != nil { + return err + } + } + + return nil +} + +// ExecuteString выполняет Lua код из строки +func (b *Bridge) ExecuteString(code string) error { + if b.closed.Load() { + return fmt.Errorf("мост закрыт") + } + + return b.state.DoString(code) +} + +// GetState возвращает Lua состояние +func (b *Bridge) GetState() *lua.LState { + return b.state +} + +// Close закрывает Lua состояние +func (b *Bridge) Close() { + if b.closed.Load() { + return + } + b.closed.Store(true) + if b.state != nil { + b.state.Close() + } +}