Initial dataxl CLI

This commit is contained in:
2026-06-26 21:12:47 +09:00
commit fab059bc0a
9 changed files with 1066 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
/dataxl
*.tmp
*.log
*.xlsx
*.tsv
*.csv
*.json
*.yaml
*.yml
*.toml
!examples/*

130
README.md Normal file
View File

@@ -0,0 +1,130 @@
# dataxl
`dataxl` は、構造化データファイルとExcelの間で情報を行き来させるための
Go製CLIです。
主な用途は、YAML/TOML/JSONなどの構造化ファイルをExcelに貼り付けやすい
TSVへ変換したり、Excelからコピーした表を再び構造化データへ戻したりする
ことです。
## 対応形式
- JSON
- YAML / YML
- TOML
- CSV
- TSV
- XLSX
## インストール
Go 1.24以上が必要です。
```sh
go install git.rumginger.org/agent/dataxl/cmd/dataxl@latest
```
リポジトリをcloneして使う場合:
```sh
git clone https://git.rumginger.org/agent/dataxl.git
cd dataxl
go build ./cmd/dataxl
```
## 基本的な使い方
YAMLをExcel貼り付け向けのTSVへ変換:
```sh
dataxl -from yaml -to tsv -i examples/people.yaml -o people.tsv
```
ExcelからコピーしたTSVをJSONへ戻す:
```sh
dataxl -from tsv -to json < people.tsv > people.json
```
JSONからExcel workbookを作成:
```sh
dataxl -from json -to xlsx -i people.json -o people.xlsx
```
Excel workbookをYAMLへ変換:
```sh
dataxl -from xlsx -to yaml -i people.xlsx -sheet Sheet1
```
入力または出力ファイル名から形式を推定できる場合、`-from` または `-to`
省略できます。stdin/stdoutを使う場合は明示してください。
```sh
dataxl -i examples/people.yaml -o people.tsv
dataxl -from tsv -to yaml < people.tsv
```
## Excelとの連携
Excelへ貼り付ける場合はTSVが便利です。
```sh
dataxl -from yaml -to tsv -i examples/people.yaml
```
出力をそのままコピーしてExcelのシートに貼り付けると、タブ区切りの列として
展開されます。
Excelから戻す場合は、シート上の範囲をコピーしてstdinへ渡します。
```sh
dataxl -from tsv -to yaml > restored.yaml
```
## 入れ子構造の表現
構造化データの入れ子は、Excelで編集しやすい列名へ展開されます。
入力例:
```yaml
- id: 1
user:
name: Alice
items:
- sku: A-001
qty: 2
```
TSV出力例:
```tsv
id items[0].qty items[0].sku user.name
1 2 A-001 Alice
```
逆方向の変換では、`user.name``items[0].sku` のような列名から入れ子の
map/arrayを復元します。
## CLIオプション
- `-i`: 入力ファイル。省略時はstdin。
- `-o`: 出力ファイル。省略時はstdout。
- `-from`: 入力形式。`json`, `yaml`, `toml`, `csv`, `tsv`, `xlsx`
- `-to`: 出力形式。`json`, `yaml`, `toml`, `csv`, `tsv`, `xlsx`
- `-sheet`: XLSXの読み書きに使うシート名。既定値は `Sheet1`
- `-pretty`: JSONなどの構造化出力を整形するか。既定値は `true`
## 現在の制約
- 表形式では1行目をヘッダーとして扱います。
- XLSXは指定した1シートのみ読み書きします。
- セル値の型推定は、空文字、真偽値、整数、小数、文字列の範囲です。
- 複雑なExcel書式や数式の保持は目的外です。
## 開発者向け情報
- 設計概要: [docs/architecture.md](docs/architecture.md)
- 開発手順: [docs/development.md](docs/development.md)

597
cmd/dataxl/main.go Normal file
View File

@@ -0,0 +1,597 @@
package main
import (
"bytes"
"encoding/csv"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"github.com/BurntSushi/toml"
"github.com/xuri/excelize/v2"
"gopkg.in/yaml.v3"
)
type options struct {
inFile string
outFile string
from string
to string
sheet string
pretty bool
}
type table struct {
Header []string
Rows [][]string
}
func main() {
if err := run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr); err != nil {
fmt.Fprintln(os.Stderr, "dataxl:", err)
os.Exit(1)
}
}
func run(args []string, stdin io.Reader, stdout, stderr io.Writer) error {
var opt options
fs := flag.NewFlagSet("dataxl", flag.ContinueOnError)
fs.SetOutput(stderr)
fs.StringVar(&opt.inFile, "i", "", "input file, defaults to stdin")
fs.StringVar(&opt.outFile, "o", "", "output file, defaults to stdout")
fs.StringVar(&opt.from, "from", "", "input format: json, yaml, toml, csv, tsv, xlsx")
fs.StringVar(&opt.to, "to", "", "output format: json, yaml, toml, csv, tsv, xlsx")
fs.StringVar(&opt.sheet, "sheet", "Sheet1", "worksheet name for xlsx input/output")
fs.BoolVar(&opt.pretty, "pretty", true, "pretty-print structured output")
fs.Usage = func() {
fmt.Fprintln(stderr, `Usage:
dataxl -from yaml -to tsv -i input.yaml -o output.tsv
dataxl -from tsv -to json < paste-from-excel.tsv
dataxl -from json -to xlsx -i data.json -o data.xlsx
Formats:
json, yaml/yml, toml, csv, tsv, xlsx
Notes:
Structured records are flattened into spreadsheet columns such as user.name
and items[0].sku. Spreadsheet columns with those names are restored when
converting back to json/yaml/toml.`)
}
if err := fs.Parse(args); err != nil {
return err
}
if fs.NArg() != 0 {
return fmt.Errorf("unexpected arguments: %s", strings.Join(fs.Args(), " "))
}
input, err := readInput(opt.inFile, stdin)
if err != nil {
return err
}
opt.from = normalizeFormat(opt.from)
opt.to = normalizeFormat(opt.to)
if opt.from == "" {
opt.from = inferFormat(opt.inFile)
}
if opt.to == "" {
opt.to = inferFormat(opt.outFile)
}
if opt.from == "" || opt.to == "" {
return errors.New("both -from and -to are required when a format cannot be inferred from file names")
}
if err := validateFormat(opt.from); err != nil {
return err
}
if err := validateFormat(opt.to); err != nil {
return err
}
var out []byte
if isTabular(opt.from) && isTabular(opt.to) {
t, err := parseTable(input, opt.from, opt.sheet)
if err != nil {
return err
}
out, err = encodeTable(t, opt.to, opt.sheet)
if err != nil {
return err
}
} else if isTabular(opt.from) {
t, err := parseTable(input, opt.from, opt.sheet)
if err != nil {
return err
}
records := tableToRecords(t)
out, err = encodeStructured(records, opt.to, opt.pretty)
if err != nil {
return err
}
} else if isTabular(opt.to) {
value, err := parseStructured(input, opt.from)
if err != nil {
return err
}
t := valueToTable(value)
out, err = encodeTable(t, opt.to, opt.sheet)
if err != nil {
return err
}
} else {
value, err := parseStructured(input, opt.from)
if err != nil {
return err
}
out, err = encodeStructured(value, opt.to, opt.pretty)
if err != nil {
return err
}
}
return writeOutput(opt.outFile, stdout, out)
}
func readInput(path string, stdin io.Reader) ([]byte, error) {
if path == "" || path == "-" {
return io.ReadAll(stdin)
}
return os.ReadFile(path)
}
func writeOutput(path string, stdout io.Writer, data []byte) error {
if path == "" || path == "-" {
_, err := stdout.Write(data)
return err
}
return os.WriteFile(path, data, 0644)
}
func normalizeFormat(format string) string {
format = strings.ToLower(strings.TrimPrefix(strings.TrimSpace(format), "."))
switch format {
case "yml":
return "yaml"
case "xlsm", "xls":
return "xlsx"
default:
return format
}
}
func inferFormat(path string) string {
if path == "" || path == "-" {
return ""
}
return normalizeFormat(strings.TrimPrefix(filepath.Ext(path), "."))
}
func validateFormat(format string) error {
switch format {
case "json", "yaml", "toml", "csv", "tsv", "xlsx":
return nil
default:
return fmt.Errorf("unsupported format %q", format)
}
}
func isTabular(format string) bool {
return format == "csv" || format == "tsv" || format == "xlsx"
}
func parseStructured(input []byte, format string) (any, error) {
var value any
switch format {
case "json":
dec := json.NewDecoder(bytes.NewReader(input))
dec.UseNumber()
if err := dec.Decode(&value); err != nil {
return nil, err
}
case "yaml":
if err := yaml.Unmarshal(input, &value); err != nil {
return nil, err
}
value = normalizeYAML(value)
case "toml":
var m map[string]any
if err := toml.Unmarshal(input, &m); err != nil {
return nil, err
}
value = m
default:
return nil, fmt.Errorf("format %q is not structured", format)
}
return value, nil
}
func encodeStructured(value any, format string, pretty bool) ([]byte, error) {
switch format {
case "json":
if pretty {
return json.MarshalIndent(value, "", " ")
}
return json.Marshal(value)
case "yaml":
return yaml.Marshal(value)
case "toml":
m, ok := value.(map[string]any)
if !ok {
m = map[string]any{"rows": value}
}
var b bytes.Buffer
err := toml.NewEncoder(&b).Encode(m)
return b.Bytes(), err
default:
return nil, fmt.Errorf("format %q is not structured", format)
}
}
func normalizeYAML(value any) any {
switch v := value.(type) {
case map[string]any:
out := make(map[string]any, len(v))
for key, child := range v {
out[key] = normalizeYAML(child)
}
return out
case map[any]any:
out := make(map[string]any, len(v))
for key, child := range v {
out[fmt.Sprint(key)] = normalizeYAML(child)
}
return out
case []any:
for i := range v {
v[i] = normalizeYAML(v[i])
}
}
return value
}
func parseTable(input []byte, format, sheet string) (table, error) {
switch format {
case "csv":
return readDelimited(input, ',')
case "tsv":
return readDelimited(input, '\t')
case "xlsx":
f, err := excelize.OpenReader(bytes.NewReader(input))
if err != nil {
return table{}, err
}
defer f.Close()
rows, err := f.GetRows(sheet)
if err != nil {
return table{}, err
}
return rowsToTable(rows), nil
default:
return table{}, fmt.Errorf("format %q is not tabular", format)
}
}
func readDelimited(input []byte, comma rune) (table, error) {
r := csv.NewReader(bytes.NewReader(input))
r.Comma = comma
r.FieldsPerRecord = -1
r.TrimLeadingSpace = comma == ','
rows, err := r.ReadAll()
if err != nil {
return table{}, err
}
return rowsToTable(rows), nil
}
func rowsToTable(rows [][]string) table {
if len(rows) == 0 {
return table{}
}
width := 0
for _, row := range rows {
if len(row) > width {
width = len(row)
}
}
header := padRow(rows[0], width)
body := make([][]string, 0, len(rows)-1)
for _, row := range rows[1:] {
body = append(body, padRow(row, width))
}
return table{Header: header, Rows: body}
}
func padRow(row []string, width int) []string {
out := make([]string, width)
copy(out, row)
return out
}
func encodeTable(t table, format, sheet string) ([]byte, error) {
switch format {
case "csv":
return writeDelimited(t, ',')
case "tsv":
return writeDelimited(t, '\t')
case "xlsx":
return writeXLSX(t, sheet)
default:
return nil, fmt.Errorf("format %q is not tabular", format)
}
}
func writeDelimited(t table, comma rune) ([]byte, error) {
var b bytes.Buffer
w := csv.NewWriter(&b)
w.Comma = comma
if err := w.Write(t.Header); err != nil {
return nil, err
}
for _, row := range t.Rows {
if err := w.Write(row); err != nil {
return nil, err
}
}
w.Flush()
return b.Bytes(), w.Error()
}
func writeXLSX(t table, sheet string) ([]byte, error) {
f := excelize.NewFile()
defaultSheet := f.GetSheetName(0)
if sheet == "" {
sheet = "Sheet1"
}
if defaultSheet != sheet {
if err := f.SetSheetName(defaultSheet, sheet); err != nil {
return nil, err
}
}
rows := append([][]string{t.Header}, t.Rows...)
for r, row := range rows {
for c, value := range row {
cell, err := excelize.CoordinatesToCellName(c+1, r+1)
if err != nil {
return nil, err
}
if err := f.SetCellValue(sheet, cell, value); err != nil {
return nil, err
}
}
}
if len(t.Header) > 0 {
end, _ := excelize.CoordinatesToCellName(len(t.Header), 1)
style, _ := f.NewStyle(&excelize.Style{Font: &excelize.Font{Bold: true}})
_ = f.SetCellStyle(sheet, "A1", end, style)
_ = f.SetPanes(sheet, &excelize.Panes{
Freeze: true,
Split: false,
XSplit: 0,
YSplit: 1,
TopLeftCell: "A2",
ActivePane: "bottomLeft",
})
}
var b bytes.Buffer
if err := f.Write(&b); err != nil {
return nil, err
}
return b.Bytes(), nil
}
func valueToTable(value any) table {
records := recordsFromValue(value)
flatRows := make([]map[string]string, 0, len(records))
seen := map[string]bool{}
var header []string
for _, record := range records {
flat := map[string]string{}
flatten("", record, flat)
for key := range flat {
if !seen[key] {
seen[key] = true
header = append(header, key)
}
}
flatRows = append(flatRows, flat)
}
sort.Strings(header)
rows := make([][]string, 0, len(flatRows))
for _, flat := range flatRows {
row := make([]string, len(header))
for i, key := range header {
row[i] = flat[key]
}
rows = append(rows, row)
}
return table{Header: header, Rows: rows}
}
func recordsFromValue(value any) []any {
switch v := value.(type) {
case []any:
return v
case map[string]any:
for _, key := range []string{"rows", "records", "items"} {
if rows, ok := v[key].([]any); ok {
return rows
}
}
return []any{v}
default:
return []any{v}
}
}
func flatten(prefix string, value any, out map[string]string) {
switch v := value.(type) {
case map[string]any:
if len(v) == 0 && prefix != "" {
out[prefix] = "{}"
return
}
keys := make([]string, 0, len(v))
for key := range v {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
childPrefix := key
if prefix != "" {
childPrefix = prefix + "." + key
}
flatten(childPrefix, v[key], out)
}
case []any:
if len(v) == 0 && prefix != "" {
out[prefix] = "[]"
return
}
for i, child := range v {
flatten(fmt.Sprintf("%s[%d]", prefix, i), child, out)
}
default:
if prefix == "" {
prefix = "value"
}
out[prefix] = scalarString(v)
}
}
func scalarString(value any) string {
switch v := value.(type) {
case nil:
return ""
case json.Number:
return v.String()
case string:
return v
case bool:
return strconv.FormatBool(v)
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
return fmt.Sprint(v)
default:
data, err := json.Marshal(v)
if err != nil {
return fmt.Sprint(v)
}
return string(data)
}
}
func tableToRecords(t table) []map[string]any {
var records []map[string]any
for _, row := range t.Rows {
record := map[string]any{}
for i, header := range t.Header {
header = strings.TrimSpace(header)
if header == "" || i >= len(row) {
continue
}
setPath(record, header, parseCell(row[i]))
}
records = append(records, record)
}
return records
}
func parseCell(s string) any {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
if s == "true" {
return true
}
if s == "false" {
return false
}
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
return i
}
if f, err := strconv.ParseFloat(s, 64); err == nil && strings.ContainsAny(s, ".eE") {
return f
}
return s
}
var pathTokenRE = regexp.MustCompile(`([^\.\[\]]+)|\[(\d+)\]`)
func setPath(root map[string]any, path string, value any) {
tokens := parsePath(path)
if len(tokens) == 0 {
return
}
var cur any = root
for i, token := range tokens {
last := i == len(tokens)-1
nextIsIndex := !last && tokens[i+1].isIndex
if token.isIndex {
continue
}
m, ok := cur.(map[string]any)
if !ok {
return
}
if last {
m[token.key] = value
return
}
if _, ok := m[token.key]; !ok {
if nextIsIndex {
m[token.key] = []any{}
} else {
m[token.key] = map[string]any{}
}
}
if nextIsIndex {
slice, _ := m[token.key].([]any)
index := tokens[i+1].index
for len(slice) <= index {
if i+2 < len(tokens) && !tokens[i+2].isIndex {
slice = append(slice, map[string]any{})
} else {
slice = append(slice, nil)
}
}
m[token.key] = slice
if i+2 == len(tokens) {
slice[index] = value
return
}
if slice[index] == nil {
slice[index] = map[string]any{}
}
cur = slice[index]
i++
continue
}
cur = m[token.key]
}
}
type pathToken struct {
key string
index int
isIndex bool
}
func parsePath(path string) []pathToken {
matches := pathTokenRE.FindAllStringSubmatch(path, -1)
tokens := make([]pathToken, 0, len(matches))
for _, m := range matches {
if m[1] != "" {
tokens = append(tokens, pathToken{key: m[1]})
continue
}
index, _ := strconv.Atoi(m[2])
tokens = append(tokens, pathToken{index: index, isIndex: true})
}
return tokens
}

93
cmd/dataxl/main_test.go Normal file
View File

@@ -0,0 +1,93 @@
package main
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func TestYAMLToTSVFlattensForExcel(t *testing.T) {
input := strings.NewReader(`
- id: 1
user:
name: Alice
items:
- sku: A-001
qty: 2
- id: 2
user:
name: Bob
items:
- sku: B-002
qty: 5
`)
var out bytes.Buffer
var errOut bytes.Buffer
err := run([]string{"-from", "yaml", "-to", "tsv"}, input, &out, &errOut)
if err != nil {
t.Fatalf("run failed: %v\nstderr: %s", err, errOut.String())
}
got := out.String()
for _, want := range []string{"id", "items[0].qty", "items[0].sku", "user.name", "Alice", "B-002"} {
if !strings.Contains(got, want) {
t.Fatalf("output missing %q:\n%s", want, got)
}
}
}
func TestTSVToJSONRestoresPaths(t *testing.T) {
input := strings.NewReader("id\titems[0].qty\titems[0].sku\tuser.name\n1\t2\tA-001\tAlice\n")
var out bytes.Buffer
var errOut bytes.Buffer
err := run([]string{"-from", "tsv", "-to", "json"}, input, &out, &errOut)
if err != nil {
t.Fatalf("run failed: %v\nstderr: %s", err, errOut.String())
}
var records []map[string]any
if err := json.Unmarshal(out.Bytes(), &records); err != nil {
t.Fatalf("invalid json: %v\n%s", err, out.String())
}
if len(records) != 1 {
t.Fatalf("record count = %d, want 1", len(records))
}
user := records[0]["user"].(map[string]any)
if user["name"] != "Alice" {
t.Fatalf("user.name = %v, want Alice", user["name"])
}
items := records[0]["items"].([]any)
first := items[0].(map[string]any)
if first["sku"] != "A-001" || first["qty"].(float64) != 2 {
t.Fatalf("unexpected item: %#v", first)
}
}
func TestJSONToXLSXAndBack(t *testing.T) {
dir := t.TempDir()
xlsxPath := filepath.Join(dir, "data.xlsx")
jsonInput := strings.NewReader(`[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]`)
var errOut bytes.Buffer
if err := run([]string{"-from", "json", "-to", "xlsx", "-o", xlsxPath}, jsonInput, ioDiscard{}, &errOut); err != nil {
t.Fatalf("json to xlsx failed: %v\nstderr: %s", err, errOut.String())
}
if stat, err := os.Stat(xlsxPath); err != nil || stat.Size() == 0 {
t.Fatalf("xlsx was not created: stat=%v err=%v", stat, err)
}
var out bytes.Buffer
errOut.Reset()
if err := run([]string{"-from", "xlsx", "-to", "json", "-i", xlsxPath}, nil, &out, &errOut); err != nil {
t.Fatalf("xlsx to json failed: %v\nstderr: %s", err, errOut.String())
}
if !strings.Contains(out.String(), `"name": "Bob"`) {
t.Fatalf("roundtrip output missing Bob:\n%s", out.String())
}
}
type ioDiscard struct{}
func (ioDiscard) Write(p []byte) (int, error) { return len(p), nil }

93
docs/architecture.md Normal file
View File

@@ -0,0 +1,93 @@
# Architecture
`dataxl` は、ファイル形式ごとの差を小さくするために、内部表現を2つに分けています。
- structured value: JSON/YAML/TOMLから読める `map[string]any`, `[]any`, scalar
- table: Excel/CSV/TSVに近い `Header []string``Rows [][]string`
変換は原則として次のどれかです。
- structured -> structured
- structured -> table
- table -> structured
- table -> table
## Format Adapters
各形式の処理は `cmd/dataxl/main.go` の以下の関数に集約しています。
- `parseStructured`
- `encodeStructured`
- `parseTable`
- `encodeTable`
現在はCLIが小さいため単一ファイルに置いています。形式やオプションが増えたら、
`internal/format``internal/table` へ分割する余地があります。
## Table Model
表形式は必ず1行目をヘッダーとして扱います。
```go
type table struct {
Header []string
Rows [][]string
}
```
CSV/TSV/XLSXの読み込みでは、短い行を空文字で埋めて列数を揃えます。
XLSXの書き出しではヘッダーを太字にし、1行目を固定します。
## Flattening
structured -> table では、入れ子のmap/arrayを列パスへ展開します。
- map: `user.name`
- array: `items[0].sku`
- top-level scalar: `value`
列順は安定性を優先してソートしています。Excel上で列の位置が変わっても、
ヘッダー名を見て復元するため、列順には依存しません。
## Unflattening
table -> structured では、ヘッダーのパス表現からmap/arrayを復元します。
例:
```text
items[0].sku
```
は次の構造になります。
```json
{
"items": [
{
"sku": "..."
}
]
}
```
セル値は `parseCell` で軽く型推定します。
- 空文字: `""`
- `true` / `false`: boolean
- 整数: int64
- 小数または指数表記: float64
- その他: string
## TOML Output
TOMLはトップレベル配列を直接表せないため、表からTOMLへ出力する場合など、
トップレベルがmapでない値は `rows` キーに包んで出力します。
## Dependencies
- `github.com/xuri/excelize/v2`: XLSX読み書き
- `gopkg.in/yaml.v3`: YAML読み書き
- `github.com/BurntSushi/toml`: TOML読み書き
Go 1.24以上を前提にしています。

74
docs/development.md Normal file
View File

@@ -0,0 +1,74 @@
# Development
## Requirements
- Go 1.24 or later
## Setup
```sh
git clone https://git.rumginger.org/agent/dataxl.git
cd dataxl
go mod download
```
## Common Commands
Format code:
```sh
gofmt -w cmd/dataxl/*.go
```
Run tests:
```sh
go test ./...
```
Build:
```sh
go build ./cmd/dataxl
```
Run locally:
```sh
go run ./cmd/dataxl -from yaml -to tsv -i examples/people.yaml
```
## Test Coverage
The current tests cover:
- YAML -> TSV flattening
- TSV -> JSON path restoration
- JSON -> XLSX -> JSON round trip
When adding a new format or path rule, add tests around both directions where
possible.
## Release Notes
There is no automated release pipeline yet. A minimal release flow is:
```sh
go test ./...
git tag v0.1.0
git push origin main --tags
```
After tags exist, users can install a specific version:
```sh
go install git.rumginger.org/agent/dataxl/cmd/dataxl@v0.1.0
```
## Design Guidelines
- Keep stdin/stdout usable for shell pipelines.
- Keep TSV behavior predictable because it is the main Excel clipboard format.
- Preserve headers as the contract between spreadsheet data and structured data.
- Prefer explicit errors over silent best-effort conversion when a format is unsupported.
- Keep dependencies small unless a format needs a mature parser/writer.

16
examples/people.yaml Normal file
View File

@@ -0,0 +1,16 @@
- id: 1
user:
name: Alice
email: alice@example.com
department: Sales
items:
- sku: A-001
qty: 2
- id: 2
user:
name: Bob
email: bob@example.com
department: Engineering
items:
- sku: B-002
qty: 5

20
go.mod Normal file
View File

@@ -0,0 +1,20 @@
module git.rumginger.org/agent/dataxl
go 1.24.0
require (
github.com/BurntSushi/toml v1.6.0
github.com/xuri/excelize/v2 v2.10.1
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/richardlehane/mscfb v1.0.6 // indirect
github.com/richardlehane/msoleps v1.0.6 // indirect
github.com/tiendc/go-deepcopy v1.7.2 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/text v0.34.0 // indirect
)

32
go.sum Normal file
View File

@@ -0,0 +1,32 @@
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0=
github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=