Upload files to "internal/repl"

This commit is contained in:
gvsafronov committed 2026-09-19 22:31:37 +00:00
1 parent f098896469
commit d695d05fbd
3 files changed
+752

No files matched your search

+305
View File
@@ -0,0 +1,305 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/repl/completer.go
// Назначение: Автодополнение команд, имён коллекций, полей и операторов.
// Поддерживает как собственный API, так и интеграцию с chzyer/readline.
package repl
import (
"sort"
"strings"
"github.com/chzyer/readline"
)
// Completer предоставляет автодополнение.
type Completer struct {
repl *Repl
}
// NewCompleter создаёт автодополнение.
func NewCompleter(r *Repl) *Completer {
return &Completer{repl: r}
}
// Complete возвращает список подсказок для заданного префикса.
func (c *Completer) Complete(prefix string) []string {
if c == nil || c.repl == nil {
return nil
}
prefix = strings.TrimSpace(prefix)
if prefix == "" {
return nil
}
parts := strings.Fields(prefix)
trailingSpace := strings.HasSuffix(prefix, " ")
if len(parts) == 0 || (len(parts) == 1 && !trailingSpace) {
return c.completeCommand(prefix)
}
cmdName := ""
for name := range c.repl.commands {
if strings.HasPrefix(prefix, name) {
if len(name) > len(cmdName) {
cmdName = name
}
}
}
if cmdName == "" {
if len(parts) > 0 {
cmdName = parts[0]
}
}
argsPrefix := strings.TrimPrefix(prefix, cmdName)
argsPrefix = strings.TrimSpace(argsPrefix)
argParts := strings.Fields(argsPrefix)
lastArg := ""
if len(argParts) > 0 && !trailingSpace {
lastArg = argParts[len(argParts)-1]
}
return c.completeArgs(cmdName, lastArg)
}
func (c *Completer) completeCommand(prefix string) []string {
var out []string
seen := map[string]struct{}{}
for name := range c.repl.commands {
if strings.HasPrefix(name, prefix) {
if _, ok := seen[name]; !ok {
seen[name] = struct{}{}
out = append(out, name)
}
}
}
extra := []string{
"help", "clear", "quit", "exit", "pager",
}
for _, e := range extra {
if strings.HasPrefix(e, prefix) {
if _, ok := seen[e]; !ok {
seen[e] = struct{}{}
out = append(out, e)
}
}
}
sort.Strings(out)
return out
}
func (c *Completer) completeArgs(cmdName, lastArg string) []string {
switch cmdName {
case "use", "drop database", "create slice", "export", "import":
return c.completeDatabases(lastArg)
case "create collection", "drop collection", "show collections",
"show indexes", "show triggers", "show deleted", "count",
"stats timestamps", "compress collection":
return c.completeCollections(lastArg)
case "insert", "find", "update", "delete", "permanent delete", "restore",
"findbyindex", "findbytime", "create index", "drop index",
"add required", "add unique", "add min", "add max", "add enum",
"create trigger", "drop trigger", "enable trigger", "disable trigger",
"show timestamps", "doc compression":
return c.completeCollectionThenField(cmdName, lastArg)
case "migration":
return c.completeMigration(lastArg)
default:
return c.completeOperators(lastArg)
}
}
func (c *Completer) completeDatabases(prefix string) []string {
if c.repl.store == nil {
return nil
}
var out []string
for _, db := range c.repl.store.ListDatabases() {
if strings.HasPrefix(db, prefix) {
out = append(out, db)
}
}
sort.Strings(out)
return out
}
func (c *Completer) completeCollections(prefix string) []string {
if c.repl.store == nil || c.repl.currentDB == "" {
return nil
}
db, err := c.repl.store.GetDatabase(c.repl.currentDB)
if err != nil {
return nil
}
var out []string
for _, coll := range db.ListCollections() {
if strings.HasPrefix(coll, prefix) {
out = append(out, coll)
}
}
sort.Strings(out)
return out
}
func (c *Completer) completeCollectionThenField(cmdName, lastArg string) []string {
if c.repl.store == nil || c.repl.currentDB == "" {
return c.completeCollections(lastArg)
}
db, err := c.repl.store.GetDatabase(c.repl.currentDB)
if err != nil {
return nil
}
colls := db.ListCollections()
for _, coll := range colls {
if coll == lastArg {
return c.completeFields(coll, "")
}
}
if cols := c.completeCollections(lastArg); len(cols) > 0 {
return cols
}
if len(colls) > 0 {
return c.completeFields(colls[0], lastArg)
}
return nil
}
func (c *Completer) completeFields(collName, prefix string) []string {
if c.repl.store == nil || c.repl.currentDB == "" {
return nil
}
db, err := c.repl.store.GetDatabase(c.repl.currentDB)
if err != nil {
return nil
}
coll, err := db.GetCollection(collName)
if err != nil {
return nil
}
fieldSet := map[string]struct{}{}
for _, doc := range coll.GetAllDocuments() {
for k := range doc.GetFields() {
fieldSet[k] = struct{}{}
}
}
var out []string
for k := range fieldSet {
if strings.HasPrefix(k, prefix) {
out = append(out, k)
}
}
sort.Strings(out)
return out
}
func (c *Completer) completeOperators(prefix string) []string {
ops := []string{
"BEFORE_INSERT", "AFTER_INSERT",
"BEFORE_UPDATE", "AFTER_UPDATE",
"BEFORE_DELETE", "AFTER_DELETE",
"abort", "skip", "modify", "log", "notify",
"unique", "required",
"start", "status", "list", "pause", "resume", "cancel", "stats", "config", "queue", "validate",
"--description", "--set", "--inc", "--currentDate", "--condition",
}
var out []string
for _, op := range ops {
if strings.HasPrefix(op, prefix) {
out = append(out, op)
}
}
sort.Strings(out)
return out
}
func (c *Completer) completeMigration(prefix string) []string {
subs := []string{"start", "status", "list", "pause", "resume", "cancel", "stats", "config", "queue", "validate"}
var out []string
for _, s := range subs {
if strings.HasPrefix(s, prefix) {
out = append(out, s)
}
}
sort.Strings(out)
return out
}
// CommonPrefix возвращает общий префикс для набора строк.
func CommonPrefix(items []string) string {
if len(items) == 0 {
return ""
}
prefix := items[0]
for _, s := range items[1:] {
for !strings.HasPrefix(s, prefix) {
if len(prefix) == 0 {
return ""
}
prefix = prefix[:len(prefix)-1]
}
}
return prefix
}
// ReadlineCompleter строит статическое дерево команд для readline.
// Динамические имена коллекций/полей дополняются через DynamicCompleter
// (см. ReadlineDynamicFunc ниже), чтобы не пересобирать дерево на каждый ввод.
func (c *Completer) ReadlineCompleter() *readline.PrefixCompleter {
items := []readline.PrefixCompleterInterface{}
// Сортируем имена команд для стабильного вывода.
names := make([]string, 0, len(c.repl.commands))
for name := range c.repl.commands {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
items = append(items, readline.PcItem(name))
}
// Системные команды.
items = append(items, readline.PcItem("help"))
items = append(items, readline.PcItem("clear"))
items = append(items, readline.PcItem("quit"))
items = append(items, readline.PcItem("exit"))
items = append(items, readline.PcItem("pager",
readline.PcItem("on"),
readline.PcItem("off"),
))
return readline.NewPrefixCompleter(items...)
}
// ReadlineDynamicFunc возвращает функцию для readline.Config.DynamicComplete.
// Она вызывается на каждый Tab и позволяет дополнять коллекции и поля.
func (c *Completer) ReadlineDynamicFunc() func(string, int) []string {
return func(line string, pos int) []string {
prefix := ""
if pos <= len(line) {
prefix = line[:pos]
}
return c.Complete(prefix)
}
}
+195
View File
@@ -0,0 +1,195 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/repl/pager.go
// Назначение: Пейджер для постраничного вывода длинных результатов.
// Использует внешний pager (less/more) если доступен, иначе встроенный.
// Совместимо с Linux и OpenIndiana.
package repl
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"strings"
)
// Pager предоставляет постраничный вывод.
type Pager struct {
enabled bool
threshold int
external string
lines int
}
// NewPager создаёт пейджер.
func NewPager(enabled bool, threshold int) *Pager {
p := &Pager{
enabled: enabled,
threshold: threshold,
lines: 24,
}
p.external = detectPager()
return p
}
// detectPager ищет доступный внешний pager.
// Порядок: $PAGER -> less -> more -> "" (встроенный).
func detectPager() string {
if env := os.Getenv("PAGER"); env != "" {
return env
}
if path, err := exec.LookPath("less"); err == nil {
return path
}
if path, err := exec.LookPath("more"); err == nil {
return path
}
return ""
}
// Enabled возвращает, включён ли пейджер.
func (p *Pager) Enabled() bool {
return p != nil && p.enabled
}
// SetEnabled включает/выключает пейджер.
func (p *Pager) SetEnabled(v bool) {
if p != nil {
p.enabled = v
}
}
// SetThreshold устанавливает порог строк.
func (p *Pager) SetThreshold(n int) {
if p != nil && n > 0 {
p.threshold = n
}
}
// Page выводит текст через пейджер, если он длиннее порога.
func (p *Pager) Page(text string) error {
if p == nil || !p.enabled {
fmt.Print(text)
return nil
}
lineCount := strings.Count(text, "\n")
if lineCount < p.threshold {
fmt.Print(text)
return nil
}
if p.external != "" {
if err := p.pageExternal(text); err == nil {
return nil
}
// fallback на встроенный
}
return p.pageBuiltin(text)
}
// pageExternal запускает внешний pager.
func (p *Pager) pageExternal(text string) error {
args := []string{}
base := strings.ToLower(p.external)
if strings.Contains(base, "less") {
// -R: сохранять ANSI-цвета, -F: выйти если всё влезает, -X: не чистить экран
args = append(args, "-RFX")
}
cmd := exec.Command(p.external, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
if _, err := io.WriteString(stdin, text); err != nil {
stdin.Close()
_ = cmd.Wait()
return err
}
stdin.Close()
return cmd.Wait()
}
// pageBuiltin — встроенный пейджер (fallback).
// Управление: Enter — следующая строка, Space — страница, q — выход.
func (p *Pager) pageBuiltin(text string) error {
scanner := bufio.NewScanner(strings.NewReader(text))
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024)
lines := []string{}
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return err
}
pageSize := p.lines - 1
if pageSize < 5 {
pageSize = 5
}
reader := bufio.NewReader(os.Stdin)
pos := 0
for pos < len(lines) {
end := pos + pageSize
if end > len(lines) {
end = len(lines)
}
for i := pos; i < end; i++ {
fmt.Println(lines[i])
}
pos = end
if pos >= len(lines) {
break
}
fmt.Printf("\033[7m--More--\033[0m (Enter/Space/q) ")
ch, err := reader.ReadByte()
fmt.Print("\r\033[K")
if err != nil {
break
}
switch ch {
case 'q', 'Q':
return nil
case '\n', '\r':
// одна строка
if pos < len(lines) {
fmt.Println(lines[pos])
pos++
}
case ' ':
// страница
default:
// игнорируем
}
}
return nil
}
+252
View File
@@ -0,0 +1,252 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/repl/table.go
// Назначение: Табличный вывод результатов (по умолчанию для find/count/stats и т.п.).
// Работает без внешних зависимостей, совместимо с Linux и OpenIndiana.
package repl
import (
"fmt"
"sort"
"strings"
"unicode/utf8"
)
// Table — простая ASCII-таблица.
type Table struct {
headers []string
rows [][]string
}
// NewTable создаёт таблицу с заголовками.
func NewTable(headers ...string) *Table {
return &Table{
headers: append([]string(nil), headers...),
rows: make([][]string, 0),
}
}
// AddRow добавляет строку.
func (t *Table) AddRow(cells ...string) {
row := make([]string, len(cells))
copy(row, cells)
t.rows = append(t.rows, row)
}
// Len возвращает число строк.
func (t *Table) Len() int {
return len(t.rows)
}
// String рендерит таблицу.
func (t *Table) String() string {
if len(t.headers) == 0 && len(t.rows) == 0 {
return ""
}
cols := len(t.headers)
for _, r := range t.rows {
if len(r) > cols {
cols = len(r)
}
}
// Нормализуем
headers := make([]string, cols)
copy(headers, t.headers)
rows := make([][]string, len(t.rows))
for i, r := range t.rows {
rr := make([]string, cols)
copy(rr, r)
rows[i] = rr
}
// Ширины
widths := make([]int, cols)
for i, h := range headers {
widths[i] = displayWidth(h)
}
for _, r := range rows {
for i, c := range r {
if w := displayWidth(c); w > widths[i] {
widths[i] = w
}
}
}
var sb strings.Builder
// Разделитель
sep := buildSeparator(widths)
// Заголовок
sb.WriteString(sep)
sb.WriteString("\n")
sb.WriteString(renderRow(headers, widths))
sb.WriteString("\n")
sb.WriteString(sep)
sb.WriteString("\n")
// Строки
for _, r := range rows {
sb.WriteString(renderRow(r, widths))
sb.WriteString("\n")
}
sb.WriteString(sep)
sb.WriteString("\n")
return sb.String()
}
func buildSeparator(widths []int) string {
var sb strings.Builder
sb.WriteString("+")
for _, w := range widths {
sb.WriteString(strings.Repeat("-", w+2))
sb.WriteString("+")
}
return sb.String()
}
func renderRow(cells []string, widths []int) string {
var sb strings.Builder
sb.WriteString("|")
for i, w := range widths {
cell := ""
if i < len(cells) {
cell = cells[i]
}
sb.WriteString(" ")
sb.WriteString(padRight(cell, w))
sb.WriteString(" |")
}
return sb.String()
}
func padRight(s string, w int) string {
dw := displayWidth(s)
if dw >= w {
return s
}
return s + strings.Repeat(" ", w-dw)
}
// displayWidth учитывает широкие руны (грубо).
func displayWidth(s string) int {
w := 0
for _, r := range s {
if r == '\t' {
w += 4
continue
}
if r < 32 {
continue
}
if isWide(r) {
w += 2
} else {
w += 1
}
}
_ = utf8.RuneCountInString(s)
return w
}
// isWide — грубая эвристика для широких символов CJK.
func isWide(r rune) bool {
return (r >= 0x1100 && r <= 0x115F) ||
(r >= 0x2E80 && r <= 0xA4CF) ||
(r >= 0xAC00 && r <= 0xD7A3) ||
(r >= 0xF900 && r <= 0xFAFF) ||
(r >= 0xFE30 && r <= 0xFE4F) ||
(r >= 0xFF00 && r <= 0xFF60) ||
(r >= 0xFFE0 && r <= 0xFFE6)
}
// MapToTable преобразует список map в таблицу с сортировкой ключей.
func MapToTable(rows []map[string]interface{}) *Table {
if len(rows) == 0 {
return NewTable()
}
// Собираем все ключи
keySet := map[string]struct{}{}
for _, r := range rows {
for k := range r {
keySet[k] = struct{}{}
}
}
keys := make([]string, 0, len(keySet))
for k := range keySet {
keys = append(keys, k)
}
sort.Strings(keys)
// _id всегда первым, если есть
if _, ok := keySet["_id"]; ok {
sort.SliceStable(keys, func(i, j int) bool {
if keys[i] == "_id" {
return true
}
if keys[j] == "_id" {
return false
}
return keys[i] < keys[j]
})
}
t := NewTable(keys...)
for _, r := range rows {
cells := make([]string, len(keys))
for i, k := range keys {
v, ok := r[k]
if !ok {
cells[i] = ""
continue
}
cells[i] = FormatCell(v)
}
t.AddRow(cells...)
}
return t
}
// FormatCell форматирует значение для ячейки.
func FormatCell(v interface{}) string {
if v == nil {
return "NULL"
}
switch x := v.(type) {
case string:
return x
case bool:
if x {
return "true"
}
return "false"
case float64:
if x == float64(int64(x)) {
return fmt.Sprintf("%d", int64(x))
}
return fmt.Sprintf("%g", x)
case int, int8, int16, int32, int64:
return fmt.Sprintf("%d", x)
case uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", x)
default:
return fmt.Sprintf("%v", x)
}
}