diff --git a/assets/coverage-badge.svg b/assets/coverage-badge.svg
index a1f8f90..ce89bb7 100644
--- a/assets/coverage-badge.svg
+++ b/assets/coverage-badge.svg
@@ -11,7 +11,7 @@
coverage
coverage
- 16.9%
- 16.9%
+ 16.7%
+ 16.7%
diff --git a/internal/build/build.go b/internal/build/build.go
index 301a362..aa18dc2 100644
--- a/internal/build/build.go
+++ b/internal/build/build.go
@@ -27,14 +27,13 @@ import (
"log/slog"
"github.com/leonelquinteros/gotext"
- "mvdan.cc/sh/v3/syntax"
- "mvdan.cc/sh/v3/syntax/typedjson"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/distro"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
+ "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
)
@@ -133,47 +132,6 @@ type RepositoryProvider interface {
// ================================================
-type ScriptFile struct {
- File *syntax.File
- Path string
-}
-
-func (s *ScriptFile) GobEncode() ([]byte, error) {
- var buf bytes.Buffer
- enc := gob.NewEncoder(&buf)
- if err := enc.Encode(s.Path); err != nil {
- return nil, err
- }
- var fileBuf bytes.Buffer
- if err := typedjson.Encode(&fileBuf, s.File); err != nil {
- return nil, err
- }
- fileData := fileBuf.Bytes()
- if err := enc.Encode(fileData); err != nil {
- return nil, err
- }
- return buf.Bytes(), nil
-}
-
-func (s *ScriptFile) GobDecode(data []byte) error {
- buf := bytes.NewBuffer(data)
- dec := gob.NewDecoder(buf)
- if err := dec.Decode(&s.Path); err != nil {
- return err
- }
- var fileData []byte
- if err := dec.Decode(&fileData); err != nil {
- return err
- }
- fileReader := bytes.NewReader(fileData)
- file, err := typedjson.Decode(fileReader)
- if err != nil {
- return err
- }
- s.File = file.(*syntax.File)
- return nil
-}
-
type BuiltDep struct {
Name string
Path string
@@ -219,8 +177,8 @@ type ScriptResolverExecutor interface {
}
type ScriptExecutor interface {
- ReadScript(ctx context.Context, scriptPath string) (*ScriptFile, error)
- ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *ScriptFile) (string, []*types.BuildVars, error)
+ ReadScript(ctx context.Context, scriptPath string) (*alrsh.ALRSh, error)
+ ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ALRSh) (string, []*types.BuildVars, error)
PrepareDirs(
ctx context.Context,
input *BuildInput,
@@ -229,7 +187,7 @@ type ScriptExecutor interface {
ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
- sf *ScriptFile,
+ sf *alrsh.ALRSh,
varsOfPackages []*types.BuildVars,
repoDeps []string,
builtDeps []*BuiltDep,
@@ -242,7 +200,7 @@ type CacheExecutor interface {
}
type ScriptViewerExecutor interface {
- ViewScript(ctx context.Context, input *BuildInput, sf *ScriptFile, basePkg string) error
+ ViewScript(ctx context.Context, input *BuildInput, sf *alrsh.ALRSh, basePkg string) error
}
type CheckerExecutor interface {
diff --git a/internal/build/safe_script_executor.go b/internal/build/safe_script_executor.go
index 8c18b01..461bf81 100644
--- a/internal/build/safe_script_executor.go
+++ b/internal/build/safe_script_executor.go
@@ -28,6 +28,7 @@ import (
"github.com/hashicorp/go-plugin"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
+ "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
)
@@ -50,13 +51,13 @@ type ScriptExecutorRPCServer struct {
// ReadScript
//
-func (s *ScriptExecutorRPC) ReadScript(ctx context.Context, scriptPath string) (*ScriptFile, error) {
- var resp *ScriptFile
+func (s *ScriptExecutorRPC) ReadScript(ctx context.Context, scriptPath string) (*alrsh.ALRSh, error) {
+ var resp *alrsh.ALRSh
err := s.client.Call("Plugin.ReadScript", scriptPath, &resp)
return resp, err
}
-func (s *ScriptExecutorRPCServer) ReadScript(scriptPath string, resp *ScriptFile) error {
+func (s *ScriptExecutorRPCServer) ReadScript(scriptPath string, resp *alrsh.ALRSh) error {
file, err := s.Impl.ReadScript(context.Background(), scriptPath)
if err != nil {
return err
@@ -72,7 +73,7 @@ func (s *ScriptExecutorRPCServer) ReadScript(scriptPath string, resp *ScriptFile
type ExecuteFirstPassArgs struct {
Input *BuildInput
- Sf *ScriptFile
+ Sf *alrsh.ALRSh
}
type ExecuteFirstPassResp struct {
@@ -80,7 +81,7 @@ type ExecuteFirstPassResp struct {
VarsOfPackages []*types.BuildVars
}
-func (s *ScriptExecutorRPC) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *ScriptFile) (string, []*types.BuildVars, error) {
+func (s *ScriptExecutorRPC) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ALRSh) (string, []*types.BuildVars, error) {
var resp *ExecuteFirstPassResp
err := s.client.Call("Plugin.ExecuteFirstPass", &ExecuteFirstPassArgs{
Input: input,
@@ -148,7 +149,7 @@ func (s *ScriptExecutorRPCServer) PrepareDirs(args *PrepareDirsArgs, reply *stru
type ExecuteSecondPassArgs struct {
Input *BuildInput
- Sf *ScriptFile
+ Sf *alrsh.ALRSh
VarsOfPackages []*types.BuildVars
RepoDeps []string
BuiltDeps []*BuiltDep
@@ -158,7 +159,7 @@ type ExecuteSecondPassArgs struct {
func (s *ScriptExecutorRPC) ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
- sf *ScriptFile,
+ sf *alrsh.ALRSh,
varsOfPackages []*types.BuildVars,
repoDeps []string,
builtDeps []*BuiltDep,
diff --git a/internal/build/script_executor.go b/internal/build/script_executor.go
index eabce5c..01020a9 100644
--- a/internal/build/script_executor.go
+++ b/internal/build/script_executor.go
@@ -19,7 +19,6 @@ package build
import (
"bytes"
"context"
- "errors"
"fmt"
"log/slog"
"os"
@@ -37,10 +36,10 @@ import (
"mvdan.cc/sh/v3/syntax"
finddeps "gitea.plemya-x.ru/Plemya-x/ALR/internal/build/find_deps"
- "gitea.plemya-x.ru/Plemya-x/ALR/internal/distro"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/handlers"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/helpers"
+ "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
)
@@ -54,102 +53,12 @@ func NewLocalScriptExecutor(cfg Config) *LocalScriptExecutor {
}
}
-func (e *LocalScriptExecutor) ReadScript(ctx context.Context, scriptPath string) (*ScriptFile, error) {
- fl, err := readScript(scriptPath)
- if err != nil {
- return nil, err
- }
- return &ScriptFile{
- Path: scriptPath,
- File: fl,
- }, nil
+func (e *LocalScriptExecutor) ReadScript(ctx context.Context, scriptPath string) (*alrsh.ALRSh, error) {
+ return alrsh.ReadFromLocal(scriptPath)
}
-func (e *LocalScriptExecutor) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *ScriptFile) (string, []*types.BuildVars, error) {
- varsOfPackages := []*types.BuildVars{}
-
- scriptDir := filepath.Dir(sf.Path)
- env := createBuildEnvVars(input.info, types.Directories{ScriptDir: scriptDir})
-
- runner, err := interp.New(
- interp.Env(expand.ListEnviron(env...)), // Устанавливаем окружение
- interp.StdIO(os.Stdin, os.Stderr, os.Stderr), // Устанавливаем стандартный ввод-вывод
- interp.ExecHandler(helpers.Restricted.ExecHandler(handlers.NopExec)), // Ограничиваем выполнение
- interp.ReadDirHandler2(handlers.RestrictedReadDir(scriptDir)), // Ограничиваем чтение директорий
- interp.StatHandler(handlers.RestrictedStat(scriptDir)), // Ограничиваем доступ к статистике файлов
- interp.OpenHandler(handlers.RestrictedOpen(scriptDir)), // Ограничиваем открытие файлов
- interp.Dir(scriptDir),
- )
- if err != nil {
- return "", nil, err
- }
-
- err = runner.Run(ctx, sf.File) // Запускаем скрипт
- if err != nil {
- return "", nil, err
- }
-
- dec := decoder.New(input.info, runner) // Создаём новый декодер
-
- type packages struct {
- BasePkgName string `sh:"basepkg_name"`
- Names []string `sh:"name"`
- }
-
- var pkgs packages
- err = dec.DecodeVars(&pkgs)
- if err != nil {
- return "", nil, err
- }
-
- if len(pkgs.Names) == 0 {
- return "", nil, errors.New("package name is missing")
- }
-
- var vars types.BuildVars
-
- if len(pkgs.Names) == 1 {
- err = dec.DecodeVars(&vars) // Декодируем переменные
- if err != nil {
- return "", nil, err
- }
- varsOfPackages = append(varsOfPackages, &vars)
-
- return vars.Name, varsOfPackages, nil
- }
-
- var pkgNames []string
-
- if len(input.packages) != 0 {
- pkgNames = input.packages
- } else {
- pkgNames = pkgs.Names
- }
-
- for _, pkgName := range pkgNames {
- var preVars types.BuildVarsPre
- funcName := fmt.Sprintf("meta_%s", pkgName)
- meta, ok := dec.GetFuncWithSubshell(funcName)
- if !ok {
- return "", nil, fmt.Errorf("func %s is missing", funcName)
- }
- r, err := meta(ctx)
- if err != nil {
- return "", nil, err
- }
- d := decoder.New(&distro.OSRelease{}, r)
- err = d.DecodeVars(&preVars)
- if err != nil {
- return "", nil, err
- }
- vars := preVars.ToBuildVars()
- vars.Name = pkgName
- vars.Base = pkgs.BasePkgName
-
- varsOfPackages = append(varsOfPackages, &vars)
- }
-
- return pkgs.BasePkgName, varsOfPackages, nil
+func (e *LocalScriptExecutor) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ALRSh) (string, []*types.BuildVars, error) {
+ return sf.ParseBuildVars(ctx, input.info, input.packages)
}
func (e *LocalScriptExecutor) PrepareDirs(
@@ -177,13 +86,13 @@ func (e *LocalScriptExecutor) PrepareDirs(
func (e *LocalScriptExecutor) ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
- sf *ScriptFile,
+ sf *alrsh.ALRSh,
varsOfPackages []*types.BuildVars,
repoDeps []string,
builtDeps []*BuiltDep,
basePkg string,
) ([]*BuiltDep, error) {
- dirs, err := getDirs(e.cfg, sf.Path, basePkg)
+ dirs, err := getDirs(e.cfg, sf.Path(), basePkg)
if err != nil {
return nil, err
}
@@ -201,7 +110,7 @@ func (e *LocalScriptExecutor) ExecuteSecondPass(
return nil, err
}
- err = runner.Run(ctx, sf.File)
+ err = runner.Run(ctx, sf.File())
if err != nil {
return nil, err
}
diff --git a/internal/build/script_view.go b/internal/build/script_view.go
index 2da9250..52f47c3 100644
--- a/internal/build/script_view.go
+++ b/internal/build/script_view.go
@@ -20,6 +20,7 @@ import (
"context"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
+ "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
)
type ScriptViewerConfig interface {
@@ -33,12 +34,12 @@ type ScriptViewer struct {
func (s *ScriptViewer) ViewScript(
ctx context.Context,
input *BuildInput,
- sf *ScriptFile,
+ a *alrsh.ALRSh,
basePkg string,
) error {
return cliutils.PromptViewScript(
ctx,
- sf.Path,
+ a.Path(),
basePkg,
s.config.PagerStyle(),
input.opts.Interactive,
diff --git a/internal/build/utils.go b/internal/build/utils.go
index aa30686..c1be2b7 100644
--- a/internal/build/utils.go
+++ b/internal/build/utils.go
@@ -33,7 +33,6 @@ import (
_ "github.com/goreleaser/nfpm/v2/arch"
_ "github.com/goreleaser/nfpm/v2/deb"
_ "github.com/goreleaser/nfpm/v2/rpm"
- "mvdan.cc/sh/v3/syntax"
"github.com/goreleaser/nfpm/v2"
"github.com/goreleaser/nfpm/v2/files"
@@ -45,22 +44,6 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
)
-// Функция readScript анализирует скрипт сборки с использованием встроенной реализации bash
-func readScript(script string) (*syntax.File, error) {
- fl, err := os.Open(script) // Открываем файл скрипта
- if err != nil {
- return nil, err
- }
- defer fl.Close() // Закрываем файл после выполнения
-
- file, err := syntax.NewParser().Parse(fl, "alr.sh") // Парсим скрипт с помощью синтаксического анализатора
- if err != nil {
- return nil, err
- }
-
- return file, nil // Возвращаем синтаксическое дерево
-}
-
// Функция prepareDirs подготавливает директории для сборки.
func prepareDirs(dirs types.Directories) error {
err := os.RemoveAll(dirs.BaseDir) // Удаляем базовую директорию, если она существует
diff --git a/internal/translations/default.pot b/internal/translations/default.pot
index 92082a1..10fe091 100644
--- a/internal/translations/default.pot
+++ b/internal/translations/default.pot
@@ -178,19 +178,19 @@ msgstr ""
msgid "Error removing packages"
msgstr ""
-#: internal/build/build.go:417
+#: internal/build/build.go:375
msgid "Building package"
msgstr ""
-#: internal/build/build.go:446
+#: internal/build/build.go:404
msgid "The checksums array must be the same length as sources"
msgstr ""
-#: internal/build/build.go:488
+#: internal/build/build.go:446
msgid "Downloading sources"
msgstr ""
-#: internal/build/build.go:580
+#: internal/build/build.go:538
msgid "Installing dependencies"
msgstr ""
@@ -224,19 +224,19 @@ msgstr ""
msgid "AutoReq is not implemented for this package format, so it's skipped"
msgstr ""
-#: internal/build/script_executor.go:236
+#: internal/build/script_executor.go:145
msgid "Building package metadata"
msgstr ""
-#: internal/build/script_executor.go:366
+#: internal/build/script_executor.go:275
msgid "Executing prepare()"
msgstr ""
-#: internal/build/script_executor.go:375
+#: internal/build/script_executor.go:284
msgid "Executing build()"
msgstr ""
-#: internal/build/script_executor.go:404 internal/build/script_executor.go:424
+#: internal/build/script_executor.go:313 internal/build/script_executor.go:333
msgid "Executing %s()"
msgstr ""
diff --git a/internal/translations/po/ru/default.po b/internal/translations/po/ru/default.po
index dd35c8c..13fc296 100644
--- a/internal/translations/po/ru/default.po
+++ b/internal/translations/po/ru/default.po
@@ -185,19 +185,19 @@ msgstr "Для команды remove ожидался хотя бы 1 аргум
msgid "Error removing packages"
msgstr "Ошибка при удалении пакетов"
-#: internal/build/build.go:417
+#: internal/build/build.go:375
msgid "Building package"
msgstr "Сборка пакета"
-#: internal/build/build.go:446
+#: internal/build/build.go:404
msgid "The checksums array must be the same length as sources"
msgstr "Массив контрольных сумм должен быть той же длины, что и источники"
-#: internal/build/build.go:488
+#: internal/build/build.go:446
msgid "Downloading sources"
msgstr "Скачивание источников"
-#: internal/build/build.go:580
+#: internal/build/build.go:538
msgid "Installing dependencies"
msgstr "Установка зависимостей"
@@ -235,19 +235,19 @@ msgid "AutoReq is not implemented for this package format, so it's skipped"
msgstr ""
"AutoReq не реализовано для этого формата пакета, поэтому будет пропущено"
-#: internal/build/script_executor.go:236
+#: internal/build/script_executor.go:145
msgid "Building package metadata"
msgstr "Сборка метаданных пакета"
-#: internal/build/script_executor.go:366
+#: internal/build/script_executor.go:275
msgid "Executing prepare()"
msgstr "Выполнение prepare()"
-#: internal/build/script_executor.go:375
+#: internal/build/script_executor.go:284
msgid "Executing build()"
msgstr "Выполнение build()"
-#: internal/build/script_executor.go:404 internal/build/script_executor.go:424
+#: internal/build/script_executor.go:313 internal/build/script_executor.go:333
msgid "Executing %s()"
msgstr "Выполнение %s()"
diff --git a/pkg/alrsh/alrsh.go b/pkg/alrsh/alrsh.go
new file mode 100644
index 0000000..f4a8ff2
--- /dev/null
+++ b/pkg/alrsh/alrsh.go
@@ -0,0 +1,168 @@
+// ALR - Any Linux Repository
+// Copyright (C) 2025 The ALR Authors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package alrsh
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+
+ "mvdan.cc/sh/v3/expand"
+ "mvdan.cc/sh/v3/interp"
+ "mvdan.cc/sh/v3/syntax"
+
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/distro"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/handlers"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/helpers"
+ "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
+)
+
+type ALRSh struct {
+ file *syntax.File
+ path string
+}
+
+func createBuildEnvVars(info *distro.OSRelease, dirs types.Directories) []string {
+ env := os.Environ()
+
+ env = append(
+ env,
+ "DISTRO_NAME="+info.Name,
+ "DISTRO_PRETTY_NAME="+info.PrettyName,
+ "DISTRO_ID="+info.ID,
+ "DISTRO_VERSION_ID="+info.VersionID,
+ "DISTRO_ID_LIKE="+strings.Join(info.Like, " "),
+ "ARCH="+cpu.Arch(),
+ "NCPU="+strconv.Itoa(runtime.NumCPU()),
+ )
+
+ if dirs.ScriptDir != "" {
+ env = append(env, "scriptdir="+dirs.ScriptDir)
+ }
+
+ if dirs.PkgDir != "" {
+ env = append(env, "pkgdir="+dirs.PkgDir)
+ }
+
+ if dirs.SrcDir != "" {
+ env = append(env, "srcdir="+dirs.SrcDir)
+ }
+
+ return env
+}
+
+func (s *ALRSh) ParseBuildVars(ctx context.Context, info *distro.OSRelease, packages []string) (string, []*types.BuildVars, error) {
+ varsOfPackages := []*types.BuildVars{}
+
+ scriptDir := filepath.Dir(s.path)
+ env := createBuildEnvVars(info, types.Directories{ScriptDir: scriptDir})
+
+ runner, err := interp.New(
+ interp.Env(expand.ListEnviron(env...)), // Устанавливаем окружение
+ interp.StdIO(os.Stdin, os.Stderr, os.Stderr), // Устанавливаем стандартный ввод-вывод
+ interp.ExecHandler(helpers.Restricted.ExecHandler(handlers.NopExec)), // Ограничиваем выполнение
+ interp.ReadDirHandler2(handlers.RestrictedReadDir(scriptDir)), // Ограничиваем чтение директорий
+ interp.StatHandler(handlers.RestrictedStat(scriptDir)), // Ограничиваем доступ к статистике файлов
+ interp.OpenHandler(handlers.RestrictedOpen(scriptDir)), // Ограничиваем открытие файлов
+ interp.Dir(scriptDir),
+ )
+ if err != nil {
+ return "", nil, err
+ }
+
+ err = runner.Run(ctx, s.file) // Запускаем скрипт
+ if err != nil {
+ return "", nil, err
+ }
+
+ dec := decoder.New(info, runner) // Создаём новый декодер
+
+ type Packages struct {
+ BasePkgName string `sh:"basepkg_name"`
+ Names []string `sh:"name"`
+ }
+
+ var pkgs Packages
+ err = dec.DecodeVars(&pkgs)
+ if err != nil {
+ return "", nil, err
+ }
+
+ if len(pkgs.Names) == 0 {
+ return "", nil, errors.New("package name is missing")
+ }
+
+ var vars types.BuildVars
+
+ if len(pkgs.Names) == 1 {
+ err = dec.DecodeVars(&vars)
+ if err != nil {
+ return "", nil, err
+ }
+ varsOfPackages = append(varsOfPackages, &vars)
+
+ return vars.Name, varsOfPackages, nil
+ }
+
+ var pkgNames []string
+
+ if len(packages) != 0 {
+ pkgNames = packages
+ } else {
+ pkgNames = pkgs.Names
+ }
+
+ for _, pkgName := range pkgNames {
+ var preVars types.BuildVarsPre
+ funcName := fmt.Sprintf("meta_%s", pkgName)
+ meta, ok := dec.GetFuncWithSubshell(funcName)
+ if !ok {
+ return "", nil, fmt.Errorf("func %s is missing", funcName)
+ }
+ r, err := meta(ctx)
+ if err != nil {
+ return "", nil, err
+ }
+ d := decoder.New(&distro.OSRelease{}, r)
+ err = d.DecodeVars(&preVars)
+ if err != nil {
+ return "", nil, err
+ }
+ vars := preVars.ToBuildVars()
+ vars.Name = pkgName
+ vars.Base = pkgs.BasePkgName
+
+ varsOfPackages = append(varsOfPackages, &vars)
+ }
+
+ return pkgs.BasePkgName, varsOfPackages, nil
+}
+
+func (a *ALRSh) Path() string {
+ return a.path
+}
+
+func (a *ALRSh) File() *syntax.File {
+ return a.file
+}
diff --git a/pkg/alrsh/gob.go b/pkg/alrsh/gob.go
new file mode 100644
index 0000000..e1ad501
--- /dev/null
+++ b/pkg/alrsh/gob.go
@@ -0,0 +1,61 @@
+// ALR - Any Linux Repository
+// Copyright (C) 2025 The ALR Authors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package alrsh
+
+import (
+ "bytes"
+ "encoding/gob"
+
+ "mvdan.cc/sh/v3/syntax"
+ "mvdan.cc/sh/v3/syntax/typedjson"
+)
+
+func (s *ALRSh) GobEncode() ([]byte, error) {
+ var buf bytes.Buffer
+ enc := gob.NewEncoder(&buf)
+ if err := enc.Encode(s.path); err != nil {
+ return nil, err
+ }
+ var fileBuf bytes.Buffer
+ if err := typedjson.Encode(&fileBuf, s.file); err != nil {
+ return nil, err
+ }
+ fileData := fileBuf.Bytes()
+ if err := enc.Encode(fileData); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+func (s *ALRSh) GobDecode(data []byte) error {
+ buf := bytes.NewBuffer(data)
+ dec := gob.NewDecoder(buf)
+ if err := dec.Decode(&s.path); err != nil {
+ return err
+ }
+ var fileData []byte
+ if err := dec.Decode(&fileData); err != nil {
+ return err
+ }
+ fileReader := bytes.NewReader(fileData)
+ file, err := typedjson.Decode(fileReader)
+ if err != nil {
+ return err
+ }
+ s.file = file.(*syntax.File)
+ return nil
+}
diff --git a/pkg/alrsh/read.go b/pkg/alrsh/read.go
new file mode 100644
index 0000000..360e44d
--- /dev/null
+++ b/pkg/alrsh/read.go
@@ -0,0 +1,52 @@
+// ALR - Any Linux Repository
+// Copyright (C) 2025 The ALR Authors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package alrsh
+
+import (
+ "fmt"
+ "io/fs"
+ "os"
+
+ "mvdan.cc/sh/v3/syntax"
+)
+
+type localFs struct{}
+
+func (fs *localFs) Open(name string) (fs.File, error) {
+ return os.Open(name)
+}
+
+func ReadFromFS(fsys fs.FS, script string) (*ALRSh, error) {
+ fl, err := fsys.Open(script)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open alr.sh: %w", err)
+ }
+ defer fl.Close()
+
+ file, err := syntax.NewParser().Parse(fl, "alr.sh")
+ if err != nil {
+ return nil, err
+ }
+ return &ALRSh{
+ file: file,
+ path: script,
+ }, nil
+}
+
+func ReadFromLocal(script string) (*ALRSh, error) {
+ return ReadFromFS(&localFs{}, script)
+}
diff --git a/pkg/types/script.go b/pkg/types/script.go
new file mode 100644
index 0000000..c167a6d
--- /dev/null
+++ b/pkg/types/script.go
@@ -0,0 +1,66 @@
+// ALR - Any Linux Repository
+// Copyright (C) 2025 The ALR Authors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package types
+
+import (
+ "bytes"
+ "encoding/gob"
+
+ "mvdan.cc/sh/v3/syntax"
+ "mvdan.cc/sh/v3/syntax/typedjson"
+)
+
+type ScriptFile struct {
+ File *syntax.File
+ Path string
+}
+
+func (s *ScriptFile) GobEncode() ([]byte, error) {
+ var buf bytes.Buffer
+ enc := gob.NewEncoder(&buf)
+ if err := enc.Encode(s.Path); err != nil {
+ return nil, err
+ }
+ var fileBuf bytes.Buffer
+ if err := typedjson.Encode(&fileBuf, s.File); err != nil {
+ return nil, err
+ }
+ fileData := fileBuf.Bytes()
+ if err := enc.Encode(fileData); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+func (s *ScriptFile) GobDecode(data []byte) error {
+ buf := bytes.NewBuffer(data)
+ dec := gob.NewDecoder(buf)
+ if err := dec.Decode(&s.Path); err != nil {
+ return err
+ }
+ var fileData []byte
+ if err := dec.Decode(&fileData); err != nil {
+ return err
+ }
+ fileReader := bytes.NewReader(fileData)
+ file, err := typedjson.Decode(fileReader)
+ if err != nil {
+ return err
+ }
+ s.File = file.(*syntax.File)
+ return nil
+}