mirror of
https://github.com/Hilbis/Hilbish
synced 2025-04-21 04:53:24 +00:00
Compare commits
No commits in common. "master" and "v2.3.1" have entirely different histories.
13
.github/workflows/docs.yml
vendored
13
.github/workflows/docs.yml
vendored
@ -9,19 +9,10 @@ jobs:
|
||||
gen:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- name: Download Task
|
||||
run: 'sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d'
|
||||
- name: Build
|
||||
run: ./bin/task
|
||||
- name: Run docgen (go-written)
|
||||
- name: Run docgen
|
||||
run: go run cmd/docgen/docgen.go
|
||||
- name: Run docgen (lua-written)
|
||||
run: ./hilbish cmd/docgen/docgen.lua
|
||||
- name: Commit new docs
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
|
26
CHANGELOG.md
26
CHANGELOG.md
@ -1,31 +1,10 @@
|
||||
# 🎀 Changelog
|
||||
|
||||
## Unreleased
|
||||
### Added
|
||||
- Forward/Right arrow key will fill in hint text (#327)
|
||||
|
||||
## [2.3.4] - 2024-12-28
|
||||
### Fixed
|
||||
- Skip over file and prevent panic if info cannot be retrieved during file completion (due to permission error or anything else)
|
||||
- Apply environment variables properly after 2.3 shell interpreter changes
|
||||
- hilbish.sink.readAll() function now reads data that doesn't end in a newline
|
||||
|
||||
## [2.3.3] - 2024-11-04
|
||||
### Fixed
|
||||
- Heredocs having issues
|
||||
|
||||
### Added
|
||||
- Adding `\` at the end of input will add a newline and prompt for more input.
|
||||
|
||||
## [2.3.2] - 2024-07-30
|
||||
### Fixed
|
||||
- Command path searching due to 2.3 changes to the shell interpreter
|
||||
|
||||
## [2.3.1] - 2024-07-27
|
||||
[hehe when you see it release](https://youtu.be/AaAF51Gwbxo?si=rhj2iYuQRkqDa693&t=64)
|
||||
|
||||
### Added
|
||||
- `hilbish.opts.tips` was added to display random tips on start up.
|
||||
- `nature.opts.tips` was added to display random tips on start up.
|
||||
Displayed tips can be modified via the `hilbish.tips` table.
|
||||
|
||||
### Fixed
|
||||
@ -790,9 +769,6 @@ This input for example will prompt for more input to complete:
|
||||
|
||||
First "stable" release of Hilbish.
|
||||
|
||||
[2.3.4]: https://github.com/Rosettea/Hilbish/compare/v2.3.3...v2.3.4
|
||||
[2.3.3]: https://github.com/Rosettea/Hilbish/compare/v2.3.2...v2.3.3
|
||||
[2.3.2]: https://github.com/Rosettea/Hilbish/compare/v2.3.1...v2.3.2
|
||||
[2.3.1]: https://github.com/Rosettea/Hilbish/compare/v2.3.0...v2.3.1
|
||||
[2.3.0]: https://github.com/Rosettea/Hilbish/compare/v2.2.3...v2.3.0
|
||||
[2.2.3]: https://github.com/Rosettea/Hilbish/compare/v2.2.2...v2.2.3
|
||||
|
18
README.md
18
README.md
@ -13,23 +13,19 @@
|
||||
<br>
|
||||
|
||||
Hilbish is an extensible shell designed to be highly customizable.
|
||||
|
||||
It is configured in Lua, and provides a good range of features.
|
||||
It aims to be easy to use for anyone, and powerful enough for
|
||||
those who need more.
|
||||
It is configured in Lua and provides a good range of features.
|
||||
It aims to be easy to use for anyone but powerful enough for
|
||||
those who need it.
|
||||
|
||||
The motivation for choosing Lua was that its simpler and better to use
|
||||
than old shell scripts. It's fine for basic interactive shell uses,
|
||||
and supports [both Lua and Sh interactively](https://rosettea.github.io/Hilbish/docs/features/runner-mode/).
|
||||
|
||||
That's the only place Hilbish can use traditional shell syntax though;
|
||||
everything else is Lua and aims to be infinitely configurable.
|
||||
|
||||
If something isn't, open an issue!
|
||||
than old shell script. It's fine for basic interactive shell uses,
|
||||
but that's the only place Hilbish has shell script; everything else is Lua
|
||||
and aims to be infinitely configurable. If something isn't, open an issue!
|
||||
|
||||
# Screenshots
|
||||
<div align="center">
|
||||
<img src="gallery/tab.png">
|
||||
<img src="gallery/pillprompt.png">
|
||||
</div>
|
||||
|
||||
# Getting Hilbish
|
||||
|
152
api.go
152
api.go
@ -13,9 +13,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
//"bytes"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
@ -27,9 +28,9 @@ import (
|
||||
|
||||
rt "github.com/arnodel/golua/runtime"
|
||||
"github.com/arnodel/golua/lib/packagelib"
|
||||
//"github.com/arnodel/golua/lib/iolib"
|
||||
"github.com/arnodel/golua/lib/iolib"
|
||||
"github.com/maxlandon/readline"
|
||||
//"mvdan.cc/sh/v3/interp"
|
||||
"mvdan.cc/sh/v3/interp"
|
||||
)
|
||||
|
||||
var exports = map[string]util.LuaExport{
|
||||
@ -38,6 +39,7 @@ var exports = map[string]util.LuaExport{
|
||||
"complete": {hlcomplete, 2, false},
|
||||
"cwd": {hlcwd, 0, false},
|
||||
"exec": {hlexec, 1, false},
|
||||
"runnerMode": {hlrunnerMode, 1, false},
|
||||
"goro": {hlgoro, 1, true},
|
||||
"highlighter": {hlhighlighter, 1, false},
|
||||
"hinter": {hlhinter, 1, false},
|
||||
@ -47,6 +49,7 @@ var exports = map[string]util.LuaExport{
|
||||
"inputMode": {hlinputMode, 1, false},
|
||||
"interval": {hlinterval, 2, false},
|
||||
"read": {hlread, 1, false},
|
||||
"run": {hlrun, 1, true},
|
||||
"timeout": {hltimeout, 2, false},
|
||||
"which": {hlwhich, 1, false},
|
||||
}
|
||||
@ -131,9 +134,6 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) {
|
||||
pluginModule := moduleLoader(rtm)
|
||||
mod.Set(rt.StringValue("module"), rt.TableValue(pluginModule))
|
||||
|
||||
sinkModule := util.SinkLoader(l)
|
||||
mod.Set(rt.StringValue("sink"), rt.TableValue(sinkModule))
|
||||
|
||||
return rt.TableValue(mod), nil
|
||||
}
|
||||
|
||||
@ -154,7 +154,6 @@ func unsetVimMode() {
|
||||
util.SetField(l, hshMod, "vimMode", rt.NilValue)
|
||||
}
|
||||
|
||||
/*
|
||||
func handleStream(v rt.Value, strms *streams, errStream bool) error {
|
||||
ud, ok := v.TryUserData()
|
||||
if !ok {
|
||||
@ -183,7 +182,112 @@ func handleStream(v rt.Value, strms *streams, errStream bool) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)
|
||||
// Runs `cmd` in Hilbish's shell script interpreter.
|
||||
// The `streams` parameter specifies the output and input streams the command should use.
|
||||
// For example, to write command output to a sink.
|
||||
// As a table, the caller can directly specify the standard output, error, and input
|
||||
// streams of the command with the table keys `out`, `err`, and `input` respectively.
|
||||
// As a boolean, it specifies whether the command should use standard output or return its output streams.
|
||||
// #param cmd string
|
||||
// #param streams table|boolean
|
||||
// #returns number, string, string
|
||||
// #example
|
||||
/*
|
||||
// This code is the same as `ls -l | wc -l`
|
||||
local fs = require 'fs'
|
||||
local pr, pw = fs.pipe()
|
||||
hilbish.run('ls -l', {
|
||||
stdout = pw,
|
||||
stderr = pw,
|
||||
})
|
||||
|
||||
pw:close()
|
||||
|
||||
hilbish.run('wc -l', {
|
||||
stdin = pr
|
||||
})
|
||||
*/
|
||||
// #example
|
||||
func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
// TODO: ON BREAKING RELEASE, DO NOT ACCEPT `streams` AS A BOOLEAN.
|
||||
if err := c.Check1Arg(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd, err := c.StringArg(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
strms := &streams{}
|
||||
var terminalOut bool
|
||||
if len(c.Etc()) != 0 {
|
||||
tout := c.Etc()[0]
|
||||
|
||||
var ok bool
|
||||
terminalOut, ok = tout.TryBool()
|
||||
if !ok {
|
||||
luastreams, ok := tout.TryTable()
|
||||
if !ok {
|
||||
return nil, errors.New("bad argument to run (expected boolean or table, got " + tout.TypeName() + ")")
|
||||
}
|
||||
|
||||
handleStream(luastreams.Get(rt.StringValue("out")), strms, false)
|
||||
handleStream(luastreams.Get(rt.StringValue("err")), strms, true)
|
||||
|
||||
stdinstrm := luastreams.Get(rt.StringValue("input"))
|
||||
if !stdinstrm.IsNil() {
|
||||
ud, ok := stdinstrm.TryUserData()
|
||||
if !ok {
|
||||
return nil, errors.New("bad type as run stdin stream (expected userdata as either sink or file, got " + stdinstrm.TypeName() + ")")
|
||||
}
|
||||
|
||||
val := ud.Value()
|
||||
var varstrm io.Reader
|
||||
if f, ok := val.(*iolib.File); ok {
|
||||
varstrm = f.Handle()
|
||||
}
|
||||
|
||||
if f, ok := val.(*sink); ok {
|
||||
varstrm = f.reader
|
||||
}
|
||||
|
||||
if varstrm == nil {
|
||||
return nil, errors.New("bad type as run stdin stream (expected userdata as either sink or file)")
|
||||
}
|
||||
|
||||
strms.stdin = varstrm
|
||||
}
|
||||
} else {
|
||||
if !terminalOut {
|
||||
strms = &streams{
|
||||
stdout: new(bytes.Buffer),
|
||||
stderr: new(bytes.Buffer),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var exitcode uint8
|
||||
stdout, stderr, err := execCommand(cmd, strms)
|
||||
|
||||
if code, ok := interp.IsExitStatus(err); ok {
|
||||
exitcode = code
|
||||
} else if err != nil {
|
||||
exitcode = 1
|
||||
}
|
||||
|
||||
var stdoutStr, stderrStr string
|
||||
if stdoutBuf, ok := stdout.(*bytes.Buffer); ok {
|
||||
stdoutStr = stdoutBuf.String()
|
||||
}
|
||||
if stderrBuf, ok := stderr.(*bytes.Buffer); ok {
|
||||
stderrStr = stderrBuf.String()
|
||||
}
|
||||
|
||||
return c.PushingNext(t.Runtime, rt.IntValue(int64(exitcode)), rt.StringValue(stdoutStr), rt.StringValue(stderrStr)), nil
|
||||
}
|
||||
|
||||
// cwd() -> string
|
||||
// Returns the current directory of the shell.
|
||||
@ -300,7 +404,7 @@ hilbish.multiprompt '-->'
|
||||
*/
|
||||
func hlmultiprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
if err := c.Check1Arg(); err != nil {
|
||||
return c.PushingNext1(t.Runtime, rt.StringValue(multilinePrompt)), nil
|
||||
return nil, err
|
||||
}
|
||||
prompt, err := c.StringArg(0)
|
||||
if err != nil {
|
||||
@ -404,7 +508,7 @@ func hlexec(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
}
|
||||
cmdArgs, _ := splitInput(cmd)
|
||||
if runtime.GOOS != "windows" {
|
||||
cmdPath, err := util.LookPath(cmdArgs[0])
|
||||
cmdPath, err := exec.LookPath(cmdArgs[0])
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
// if we get here, cmdPath will be nothing
|
||||
@ -602,7 +706,7 @@ func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
return c.PushingNext1(t.Runtime, rt.StringValue(cmd)), nil
|
||||
}
|
||||
|
||||
path, err := util.LookPath(cmd)
|
||||
path, err := exec.LookPath(cmd)
|
||||
if err != nil {
|
||||
return c.Next(), nil
|
||||
}
|
||||
@ -638,6 +742,34 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
return c.Next(), nil
|
||||
}
|
||||
|
||||
// runnerMode(mode)
|
||||
// Sets the execution/runner mode for interactive Hilbish.
|
||||
// This determines whether Hilbish wll try to run input as Lua
|
||||
// and/or sh or only do one of either.
|
||||
// Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua),
|
||||
// sh, and lua. It also accepts a function, to which if it is passed one
|
||||
// will call it to execute user input instead.
|
||||
// Read [about runner mode](../features/runner-mode) for more information.
|
||||
// #param mode string|function
|
||||
func hlrunnerMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
if err := c.Check1Arg(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mode := c.Arg(0)
|
||||
|
||||
switch mode.Type() {
|
||||
case rt.StringType:
|
||||
switch mode.AsString() {
|
||||
case "hybrid", "hybridRev", "lua", "sh": runnerMode = mode
|
||||
default: return nil, errors.New("execMode: expected either a function or hybrid, hybridRev, lua, sh. Received " + mode.AsString())
|
||||
}
|
||||
case rt.FunctionType: runnerMode = mode
|
||||
default: return nil, errors.New("execMode: expected either a function or hybrid, hybridRev, lua, sh. Received " + mode.TypeName())
|
||||
}
|
||||
|
||||
return c.Next(), nil
|
||||
}
|
||||
|
||||
// hinter(line, pos)
|
||||
// The command line hint handler. It gets called on every key insert to
|
||||
// determine what text to use as an inline hint. It is passed the current
|
||||
|
@ -84,7 +84,6 @@ var prefix = map[string]string{
|
||||
"commander": "c",
|
||||
"bait": "b",
|
||||
"terminal": "term",
|
||||
"snail": "snail",
|
||||
}
|
||||
|
||||
func getTagsAndDocs(docs string) (map[string][]tag, []string) {
|
||||
@ -209,10 +208,6 @@ func setupDocType(mod string, typ *doc.Type) *docPiece {
|
||||
}
|
||||
|
||||
func setupDoc(mod string, fun *doc.Func) *docPiece {
|
||||
if fun.Doc == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
docs := strings.TrimSpace(fun.Doc)
|
||||
tags, parts := getTagsAndDocs(docs)
|
||||
|
||||
@ -304,28 +299,10 @@ start:
|
||||
func main() {
|
||||
fset := token.NewFileSet()
|
||||
os.Mkdir("docs", 0777)
|
||||
os.RemoveAll("docs/api")
|
||||
os.Mkdir("docs/api", 0777)
|
||||
|
||||
f, err := os.Create("docs/api/_index.md")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.WriteString(`---
|
||||
title: API
|
||||
layout: doc
|
||||
weight: -100
|
||||
menu: docs
|
||||
---
|
||||
|
||||
Welcome to the API documentation for Hilbish. This documents Lua functions
|
||||
provided by Hilbish.
|
||||
`)
|
||||
f.Close()
|
||||
|
||||
os.Mkdir("emmyLuaDocs", 0777)
|
||||
|
||||
dirs := []string{"./", "./util"}
|
||||
dirs := []string{"./"}
|
||||
filepath.Walk("golibs/", func (path string, info os.FileInfo, err error) error {
|
||||
if !info.IsDir() {
|
||||
return nil
|
||||
@ -352,7 +329,7 @@ provided by Hilbish.
|
||||
pieces := []docPiece{}
|
||||
typePieces := []docPiece{}
|
||||
mod := l
|
||||
if mod == "main" || mod == "util" {
|
||||
if mod == "main" {
|
||||
mod = "hilbish"
|
||||
}
|
||||
var hasInterfaces bool
|
||||
@ -436,23 +413,14 @@ provided by Hilbish.
|
||||
interfaceModules[modname].Types = append(interfaceModules[modname].Types, piece)
|
||||
}
|
||||
|
||||
fmt.Println(filteredTypePieces)
|
||||
if newDoc, ok := docs[mod]; ok {
|
||||
oldMod := docs[mod]
|
||||
newDoc.Types = append(filteredTypePieces, oldMod.Types...)
|
||||
newDoc.Docs = append(filteredPieces, oldMod.Docs...)
|
||||
|
||||
docs[mod] = newDoc
|
||||
} else {
|
||||
docs[mod] = module{
|
||||
Types: filteredTypePieces,
|
||||
Docs: filteredPieces,
|
||||
ShortDescription: shortDesc,
|
||||
Description: strings.Join(desc, "\n"),
|
||||
HasInterfaces: hasInterfaces,
|
||||
Properties: docPieceTag("property", tags),
|
||||
Fields: docPieceTag("field", tags),
|
||||
}
|
||||
docs[mod] = module{
|
||||
Types: filteredTypePieces,
|
||||
Docs: filteredPieces,
|
||||
ShortDescription: shortDesc,
|
||||
Description: strings.Join(desc, "\n"),
|
||||
HasInterfaces: hasInterfaces,
|
||||
Properties: docPieceTag("property", tags),
|
||||
Fields: docPieceTag("field", tags),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
local fs = require 'fs'
|
||||
local emmyPattern = '^%-%-%- (.+)'
|
||||
local emmyPattern2 = '^%-%- (.+)'
|
||||
local modpattern = '^%-+ @module (.+)'
|
||||
local modpattern = '^%-+ @module (%w+)'
|
||||
local pieces = {}
|
||||
local descriptions = {}
|
||||
|
||||
local files = fs.readdir 'nature'
|
||||
for _, fname in ipairs(files) do
|
||||
@ -15,25 +13,18 @@ for _, fname in ipairs(files) do
|
||||
local mod = header:match(modpattern)
|
||||
if not mod then goto continue end
|
||||
|
||||
print(fname, mod)
|
||||
pieces[mod] = {}
|
||||
descriptions[mod] = {}
|
||||
|
||||
local docPiece = {}
|
||||
local lines = {}
|
||||
local lineno = 0
|
||||
local doingDescription = true
|
||||
|
||||
for line in f:lines() do
|
||||
lineno = lineno + 1
|
||||
lines[lineno] = line
|
||||
|
||||
if line == header then goto continue2 end
|
||||
if line:match(emmyPattern) or line:match(emmyPattern2) then
|
||||
if doingDescription then
|
||||
table.insert(descriptions[mod], line:match(emmyPattern) or line:match(emmyPattern2))
|
||||
end
|
||||
else
|
||||
doingDescription = false
|
||||
if not line:match(emmyPattern) then
|
||||
if line:match '^function' then
|
||||
local pattern = (string.format('^function %s%%.', mod) .. '(%w+)')
|
||||
local funcName = line:match(pattern)
|
||||
@ -41,12 +32,10 @@ for _, fname in ipairs(files) do
|
||||
|
||||
local dps = {
|
||||
description = {},
|
||||
example = {},
|
||||
params = {}
|
||||
}
|
||||
|
||||
local offset = 1
|
||||
local doingExample = false
|
||||
while true do
|
||||
local prev = lines[lineno - offset]
|
||||
|
||||
@ -62,23 +51,11 @@ for _, fname in ipairs(files) do
|
||||
if emmy == 'param' then
|
||||
table.insert(dps.params, 1, {
|
||||
name = emmythings[1],
|
||||
type = emmythings[2],
|
||||
-- the +1 accounts for space.
|
||||
description = table.concat(emmythings, ' '):sub(emmythings[1]:len() + 1 + emmythings[2]:len() + 1)
|
||||
type = emmythings[2]
|
||||
})
|
||||
end
|
||||
else
|
||||
if docline:match '#example' then
|
||||
doingExample = not doingExample
|
||||
end
|
||||
|
||||
if not docline:match '#example' then
|
||||
if doingExample then
|
||||
table.insert(dps.example, 1, docline)
|
||||
else
|
||||
table.insert(dps.description, 1, docline)
|
||||
end
|
||||
end
|
||||
table.insert(dps.description, 1, docline)
|
||||
end
|
||||
offset = offset + 1
|
||||
else
|
||||
@ -86,7 +63,7 @@ for _, fname in ipairs(files) do
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(pieces[mod], {funcName, dps})
|
||||
pieces[mod][funcName] = dps
|
||||
end
|
||||
docPiece = {}
|
||||
goto continue2
|
||||
@ -104,82 +81,29 @@ description: %s
|
||||
layout: doc
|
||||
menu:
|
||||
docs:
|
||||
parent: "%s"
|
||||
parent: "Nature"
|
||||
---
|
||||
|
||||
]]
|
||||
|
||||
for iface, dps in pairs(pieces) do
|
||||
local mod = iface:match '(%w+)%.' or 'nature'
|
||||
local docParent = 'Nature'
|
||||
|
||||
path = string.format('docs/%s/%s.md', mod, iface)
|
||||
if mod ~= 'nature' then
|
||||
docParent = "API"
|
||||
path = string.format('docs/api/%s/%s.md', mod, iface)
|
||||
end
|
||||
if iface == 'hilbish' then
|
||||
docParent = "API"
|
||||
path = string.format('docs/api/hilbish/_index.md', mod, iface)
|
||||
end
|
||||
|
||||
local path = string.format('docs/%s/%s.md', mod, iface)
|
||||
fs.mkdir(fs.dir(path), true)
|
||||
local f <close> = io.open(path, 'w')
|
||||
f:write(string.format(header, 'Module', iface, 'No description.'))
|
||||
print(f)
|
||||
|
||||
local exists = pcall(fs.stat, path)
|
||||
local newOrNotNature = (exists and mod ~= 'nature') or iface == 'hilbish'
|
||||
print(mod, path)
|
||||
|
||||
local f <close> = io.open(path, newOrNotNature and 'r+' or 'w+')
|
||||
local tocPos
|
||||
if not newOrNotNature then
|
||||
f:write(string.format(header, 'Module', iface, (descriptions[iface] and #descriptions[iface] > 0) and descriptions[iface][1] or 'No description.', docParent))
|
||||
if descriptions[iface] and #descriptions[iface] > 0 then
|
||||
table.remove(descriptions[iface], 1)
|
||||
f:write(string.format('\n## Introduction\n%s\n\n', table.concat(descriptions[iface], '\n')))
|
||||
f:write('## Functions\n')
|
||||
f:write([[|||
|
||||
|----|----|
|
||||
]])
|
||||
tocPos = f:seek()
|
||||
end
|
||||
end
|
||||
|
||||
local tocSearch = false
|
||||
for line in f:lines() do
|
||||
if line:match '^## Functions' then
|
||||
tocSearch = true
|
||||
end
|
||||
if tocSearch and line == '' then
|
||||
tocSearch = false
|
||||
tocPos = f:seek() - 1
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(dps, function(a, b) return a[1] < b[1] end)
|
||||
for _, piece in pairs(dps) do
|
||||
local func = piece[1]
|
||||
local docs = piece[2]
|
||||
for func, docs in pairs(dps) do
|
||||
f:write(string.format('<hr>\n<div id=\'%s\'>', func))
|
||||
local sig = string.format('%s.%s(', iface, func)
|
||||
local params = ''
|
||||
for idx, param in ipairs(docs.params) do
|
||||
sig = sig .. param.name:gsub('%?$', '')
|
||||
params = params .. param.name:gsub('%?$', '')
|
||||
if idx ~= #docs.params then
|
||||
sig = sig .. ', '
|
||||
params = params .. ', '
|
||||
end
|
||||
sig = sig .. ((param.name:gsub('%?$', '')))
|
||||
if idx ~= #docs.params then sig = sig .. ', ' end
|
||||
end
|
||||
sig = sig .. ')'
|
||||
|
||||
if tocPos then
|
||||
f:seek('set', tocPos)
|
||||
local contents = f:read '*a'
|
||||
f:seek('set', tocPos)
|
||||
local tocLine = string.format('|<a href="#%s">%s</a>|%s|\n', func, string.format('%s(%s)', func, params), docs.description[1])
|
||||
f:write(tocLine .. contents)
|
||||
f:seek 'end'
|
||||
end
|
||||
|
||||
f:write(string.format('<hr>\n<div id=\'%s\'>\n', func))
|
||||
f:write(string.format([[
|
||||
<h4 class='heading'>
|
||||
%s
|
||||
@ -196,12 +120,7 @@ for iface, dps in pairs(pieces) do
|
||||
f:write 'This function has no parameters. \n'
|
||||
end
|
||||
for _, param in ipairs(docs.params) do
|
||||
f:write(string.format('`%s` **`%s`** \n', param.name:gsub('%?$', ''), param.type))
|
||||
f:write(string.format('%s\n\n', param.description))
|
||||
end
|
||||
if #docs.example ~= 0 then
|
||||
f:write '#### Example\n'
|
||||
f:write(string.format('```lua\n%s\n```\n', table.concat(docs.example, '\n')))
|
||||
f:write(string.format('`%s` **`%s`**\n', param.name:gsub('%?$', ''), param.type))
|
||||
end
|
||||
--[[
|
||||
local params = table.filter(docs, function(t)
|
||||
|
11
complete.go
11
complete.go
@ -98,7 +98,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
|
||||
if len(fileCompletions) != 0 {
|
||||
for _, f := range fileCompletions {
|
||||
fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref)))
|
||||
if err := util.FindExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil {
|
||||
if err := findExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil {
|
||||
continue
|
||||
}
|
||||
completions = append(completions, f)
|
||||
@ -115,7 +115,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
|
||||
// get basename from matches
|
||||
for _, match := range matches {
|
||||
// check if we have execute permissions for our match
|
||||
err := util.FindExecutable(match, true, false)
|
||||
err := findExecutable(match, true, false)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@ -157,12 +157,9 @@ func matchPath(query string) ([]string, string) {
|
||||
|
||||
files, _ := os.ReadDir(path)
|
||||
for _, entry := range files {
|
||||
// should we handle errors here?
|
||||
file, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if file.Mode() & os.ModeSymlink != 0 {
|
||||
if err == nil && file.Mode() & os.ModeSymlink != 0 {
|
||||
path, err := filepath.EvalSymlinks(filepath.Join(path, file.Name()))
|
||||
if err == nil {
|
||||
file, err = os.Lstat(path)
|
||||
|
@ -28,10 +28,10 @@ interfaces and functions which directly relate to shell functionality.
|
||||
|<a href="#prependPath">prependPath(dir)</a>|Prepends `dir` to $PATH.|
|
||||
|<a href="#prompt">prompt(str, typ)</a>|Changes the shell prompt to the provided string.|
|
||||
|<a href="#read">read(prompt) -> input (string)</a>|Read input from the user, using Hilbish's line editor/input reader.|
|
||||
|<a href="#run">run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)</a>|Runs `cmd` in Hilbish's shell script interpreter.|
|
||||
|<a href="#runnerMode">runnerMode(mode)</a>|Sets the execution/runner mode for interactive Hilbish.|
|
||||
|<a href="#timeout">timeout(cb, time) -> @Timer</a>|Executed the `cb` function after a period of `time`.|
|
||||
|<a href="#which">which(name) -> string</a>|Checks if `name` is a valid command.|
|
||||
|<a href="#runnerMode">runnerMode(mode)</a>|Sets the execution/runner mode for interactive Hilbish.|
|
||||
|<a href="#run">run(cmd, streams)</a>|Runs `cmd` in Hilbish's shell script interpreter.|
|
||||
|
||||
## Static module fields
|
||||
|||
|
||||
@ -408,6 +408,72 @@ Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs.
|
||||
`string` **`prompt?`**
|
||||
Text to print before input, can be empty.
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='run'>
|
||||
<h4 class='heading'>
|
||||
hilbish.run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)
|
||||
<a href="#run" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Runs `cmd` in Hilbish's shell script interpreter.
|
||||
The `streams` parameter specifies the output and input streams the command should use.
|
||||
For example, to write command output to a sink.
|
||||
As a table, the caller can directly specify the standard output, error, and input
|
||||
streams of the command with the table keys `out`, `err`, and `input` respectively.
|
||||
As a boolean, it specifies whether the command should use standard output or return its output streams.
|
||||
|
||||
#### Parameters
|
||||
`string` **`cmd`**
|
||||
|
||||
|
||||
`table|boolean` **`streams`**
|
||||
|
||||
|
||||
#### Example
|
||||
```lua
|
||||
|
||||
// This code is the same as `ls -l | wc -l`
|
||||
local fs = require 'fs'
|
||||
local pr, pw = fs.pipe()
|
||||
hilbish.run('ls -l', {
|
||||
stdout = pw,
|
||||
stderr = pw,
|
||||
})
|
||||
|
||||
pw:close()
|
||||
|
||||
hilbish.run('wc -l', {
|
||||
stdin = pr
|
||||
})
|
||||
|
||||
```
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='runnerMode'>
|
||||
<h4 class='heading'>
|
||||
hilbish.runnerMode(mode)
|
||||
<a href="#runnerMode" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Sets the execution/runner mode for interactive Hilbish.
|
||||
This determines whether Hilbish wll try to run input as Lua
|
||||
and/or sh or only do one of either.
|
||||
Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua),
|
||||
sh, and lua. It also accepts a function, to which if it is passed one
|
||||
will call it to execute user input instead.
|
||||
Read [about runner mode](../features/runner-mode) for more information.
|
||||
|
||||
#### Parameters
|
||||
`string|function` **`mode`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
@ -453,7 +519,8 @@ Will return the path of the binary, or a basename if it's a commander.
|
||||
<hr>
|
||||
|
||||
## Sink
|
||||
A sink is a structure that has input and/or output to/from a desination.
|
||||
A sink is a structure that has input and/or output to/from
|
||||
a desination.
|
||||
|
||||
### Methods
|
||||
#### autoFlush(auto)
|
||||
@ -475,65 +542,3 @@ Writes data to a sink.
|
||||
#### writeln(str)
|
||||
Writes data to a sink with a newline at the end.
|
||||
|
||||
<hr>
|
||||
<div id='run'>
|
||||
<h4 class='heading'>
|
||||
hilbish.run(cmd, streams)
|
||||
<a href="#run" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Runs `cmd` in Hilbish's shell script interpreter.
|
||||
The `streams` parameter specifies the output and input streams the command should use.
|
||||
For example, to write command output to a sink.
|
||||
As a table, the caller can directly specify the standard output, error, and input
|
||||
streams of the command with the table keys `out`, `err`, and `input` respectively.
|
||||
As a boolean, it specifies whether the command should use standard output or return its output streams.
|
||||
#### Parameters
|
||||
`cmd` **`string`**
|
||||
|
||||
|
||||
`streams` **`table|boolean`**
|
||||
|
||||
|
||||
#### Example
|
||||
```lua
|
||||
-- This code is the same as `ls -l | wc -l`
|
||||
local fs = require 'fs'
|
||||
local pr, pw = fs.pipe()
|
||||
hilbish.run('ls -l', {
|
||||
stdout = pw,
|
||||
stderr = pw,
|
||||
})
|
||||
pw:close()
|
||||
hilbish.run('wc -l', {
|
||||
stdin = pr
|
||||
})
|
||||
```
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='runnerMode'>
|
||||
<h4 class='heading'>
|
||||
hilbish.runnerMode(mode)
|
||||
<a href="#runnerMode" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Sets the execution/runner mode for interactive Hilbish.
|
||||
**NOTE: This function is deprecated and will be removed in 3.0**
|
||||
Use `hilbish.runner.setCurrent` instead.
|
||||
This determines whether Hilbish wll try to run input as Lua
|
||||
and/or sh or only do one of either.
|
||||
Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua),
|
||||
sh, and lua. It also accepts a function, to which if it is passed one
|
||||
will call it to execute user input instead.
|
||||
Read [about runner mode](../features/runner-mode) for more information.
|
||||
#### Parameters
|
||||
`mode` **`string|function`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -1,67 +0,0 @@
|
||||
---
|
||||
title: Module hilbish.abbr
|
||||
description: command line abbreviations
|
||||
layout: doc
|
||||
menu:
|
||||
docs:
|
||||
parent: "API"
|
||||
---
|
||||
|
||||
|
||||
## Introduction
|
||||
The abbr module manages Hilbish abbreviations. These are words that can be replaced
|
||||
with longer command line strings when entered.
|
||||
As an example, `git push` can be abbreviated to `gp`. When the user types
|
||||
`gp` into the command line, after hitting space or enter, it will expand to `git push`.
|
||||
Abbreviations can be used as an alternative to aliases. They are saved entirely in the history
|
||||
Instead of the aliased form of the same command.
|
||||
|
||||
## Functions
|
||||
|||
|
||||
|----|----|
|
||||
|<a href="#remove">remove(abbr)</a>|Removes the named `abbr`.|
|
||||
|<a href="#add">add(abbr, expanded|function, opts)</a>|Adds an abbreviation. The `abbr` is the abbreviation itself,|
|
||||
<hr>
|
||||
<div id='add'>
|
||||
<h4 class='heading'>
|
||||
hilbish.abbr.add(abbr, expanded|function, opts)
|
||||
<a href="#add" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Adds an abbreviation. The `abbr` is the abbreviation itself,
|
||||
while `expanded` is what the abbreviation should expand to.
|
||||
It can be either a function or a string. If it is a function, it will expand to what
|
||||
the function returns.
|
||||
`opts` is a table that accepts 1 key: `anywhere`.
|
||||
`opts.anywhere` defines whether the abbr expands anywhere in the command line or not,
|
||||
whereas the default behavior is only at the beginning of the line
|
||||
#### Parameters
|
||||
`abbr` **`string`**
|
||||
|
||||
|
||||
`expanded|function` **`string`**
|
||||
|
||||
|
||||
`opts` **`table`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='remove'>
|
||||
<h4 class='heading'>
|
||||
hilbish.abbr.remove(abbr)
|
||||
<a href="#remove" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Removes the named `abbr`.
|
||||
#### Parameters
|
||||
`abbr` **`string`**
|
||||
|
||||
|
||||
</div>
|
||||
|
@ -14,30 +14,12 @@ directly interact with the line editor in use.
|
||||
## Functions
|
||||
|||
|
||||
|----|----|
|
||||
|<a href="#editor.deleteByAmount">deleteByAmount(amount)</a>|Deletes characters in the line by the given amount.|
|
||||
|<a href="#editor.getLine">getLine() -> string</a>|Returns the current input line.|
|
||||
|<a href="#editor.getVimRegister">getVimRegister(register) -> string</a>|Returns the text that is at the register.|
|
||||
|<a href="#editor.insert">insert(text)</a>|Inserts text into the Hilbish command line.|
|
||||
|<a href="#editor.getChar">getChar() -> string</a>|Reads a keystroke from the user. This is in a format of something like Ctrl-L.|
|
||||
|<a href="#editor.setVimRegister">setVimRegister(register, text)</a>|Sets the vim register at `register` to hold the passed text.|
|
||||
|
||||
<hr>
|
||||
<div id='editor.deleteByAmount'>
|
||||
<h4 class='heading'>
|
||||
hilbish.editor.deleteByAmount(amount)
|
||||
<a href="#editor.deleteByAmount" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Deletes characters in the line by the given amount.
|
||||
|
||||
#### Parameters
|
||||
`number` **`amount`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='editor.getLine'>
|
||||
<h4 class='heading'>
|
||||
@ -114,9 +96,6 @@ hilbish.editor.setVimRegister(register, text)
|
||||
Sets the vim register at `register` to hold the passed text.
|
||||
|
||||
#### Parameters
|
||||
`string` **`register`**
|
||||
|
||||
|
||||
`string` **`text`**
|
||||
|
||||
|
||||
|
@ -1,135 +0,0 @@
|
||||
---
|
||||
title: Module hilbish.messages
|
||||
description: simplistic message passing
|
||||
layout: doc
|
||||
menu:
|
||||
docs:
|
||||
parent: "API"
|
||||
---
|
||||
|
||||
|
||||
## Introduction
|
||||
The messages interface defines a way for Hilbish-integrated commands,
|
||||
user config and other tasks to send notifications to alert the user.z
|
||||
The `hilbish.message` type is a table with the following keys:
|
||||
`title` (string): A title for the message notification.
|
||||
`text` (string): The contents of the message.
|
||||
`channel` (string): States the origin of the message, `hilbish.*` is reserved for Hilbish tasks.
|
||||
`summary` (string): A short summary of the `text`.
|
||||
`icon` (string): Unicode (preferably standard emoji) icon for the message notification
|
||||
`read` (boolean): Whether the full message has been read or not.
|
||||
|
||||
## Functions
|
||||
|||
|
||||
|----|----|
|
||||
|<a href="#unreadCount">unreadCount()</a>|Returns the amount of unread messages.|
|
||||
|<a href="#send">send(message)</a>|Sends a message.|
|
||||
|<a href="#readAll">readAll()</a>|Marks all messages as read.|
|
||||
|<a href="#read">read(idx)</a>|Marks a message at `idx` as read.|
|
||||
|<a href="#delete">delete(idx)</a>|Deletes the message at `idx`.|
|
||||
|<a href="#clear">clear()</a>|Deletes all messages.|
|
||||
|<a href="#all">all()</a>|Returns all messages.|
|
||||
<hr>
|
||||
<div id='all'>
|
||||
<h4 class='heading'>
|
||||
hilbish.messages.all()
|
||||
<a href="#all" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Returns all messages.
|
||||
#### Parameters
|
||||
This function has no parameters.
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='clear'>
|
||||
<h4 class='heading'>
|
||||
hilbish.messages.clear()
|
||||
<a href="#clear" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Deletes all messages.
|
||||
#### Parameters
|
||||
This function has no parameters.
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='delete'>
|
||||
<h4 class='heading'>
|
||||
hilbish.messages.delete(idx)
|
||||
<a href="#delete" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Deletes the message at `idx`.
|
||||
#### Parameters
|
||||
`idx` **`number`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='read'>
|
||||
<h4 class='heading'>
|
||||
hilbish.messages.read(idx)
|
||||
<a href="#read" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Marks a message at `idx` as read.
|
||||
#### Parameters
|
||||
`idx` **`number`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='readAll'>
|
||||
<h4 class='heading'>
|
||||
hilbish.messages.readAll()
|
||||
<a href="#readAll" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Marks all messages as read.
|
||||
#### Parameters
|
||||
This function has no parameters.
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='send'>
|
||||
<h4 class='heading'>
|
||||
hilbish.messages.send(message)
|
||||
<a href="#send" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Sends a message.
|
||||
#### Parameters
|
||||
`message` **`hilbish.message`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='unreadCount'>
|
||||
<h4 class='heading'>
|
||||
hilbish.messages.unreadCount()
|
||||
<a href="#unreadCount" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Returns the amount of unread messages.
|
||||
#### Parameters
|
||||
This function has no parameters.
|
||||
</div>
|
||||
|
@ -21,18 +21,16 @@ A runner is passed the input and has to return a table with these values.
|
||||
All are not required, only the useful ones the runner needs to return.
|
||||
(So if there isn't an error, just omit `err`.)
|
||||
|
||||
- `exitCode` (number): Exit code of the command
|
||||
- `input` (string): The text input of the user. This is used by Hilbish to append extra input, in case
|
||||
more is requested.
|
||||
- `err` (string): A string that represents an error from the runner.
|
||||
This should only be set when, for example, there is a syntax error.
|
||||
It can be set to a few special values for Hilbish to throw the right
|
||||
hooks and have a better looking message.
|
||||
- `\<command>: not-found` will throw a `command.not-found` hook
|
||||
based on what `\<command>` is.
|
||||
- `\<command>: not-executable` will throw a `command.not-executable` hook.
|
||||
- `continue` (boolean): Whether Hilbish should prompt the user for no input
|
||||
- `newline` (boolean): Whether a newline should be added at the end of `input`.
|
||||
- `exitCode` (number): A numerical code to indicate the exit result.
|
||||
- `input` (string): The user input. This will be used to add
|
||||
to the history.
|
||||
- `err` (string): A string to indicate an interal error for the runner.
|
||||
It can be set to a few special values for Hilbish to throw the right hooks and have a better looking message:
|
||||
|
||||
`[command]: not-found` will throw a command.not-found hook based on what `[command]` is.
|
||||
|
||||
`[command]: not-executable` will throw a command.not-executable hook.
|
||||
- `continue` (boolean): Whether to prompt the user for more input.
|
||||
|
||||
Here is a simple example of a fennel runner. It falls back to
|
||||
shell script if fennel eval has an error.
|
||||
@ -54,16 +52,29 @@ end)
|
||||
## Functions
|
||||
|||
|
||||
|----|----|
|
||||
|<a href="#runner.setMode">setMode(cb)</a>|This is the same as the `hilbish.runnerMode` function.|
|
||||
|<a href="#runner.lua">lua(cmd)</a>|Evaluates `cmd` as Lua input. This is the same as using `dofile`|
|
||||
|<a href="#sh">sh()</a>|nil|
|
||||
|<a href="#setMode">setMode(mode)</a>|**NOTE: This function is deprecated and will be removed in 3.0**|
|
||||
|<a href="#setCurrent">setCurrent(name)</a>|Sets Hilbish's runner mode by name.|
|
||||
|<a href="#set">set(name, runner)</a>|*Sets* a runner by name. The difference between this function and|
|
||||
|<a href="#run">run(input, priv)</a>|Runs `input` with the currently set Hilbish runner.|
|
||||
|<a href="#getCurrent">getCurrent()</a>|Returns the current runner by name.|
|
||||
|<a href="#get">get(name)</a>|Get a runner by name.|
|
||||
|<a href="#exec">exec(cmd, runnerName)</a>|Executes `cmd` with a runner.|
|
||||
|<a href="#add">add(name, runner)</a>|Adds a runner to the table of available runners.|
|
||||
|<a href="#runner.sh">sh(cmd)</a>|Runs a command in Hilbish's shell script interpreter.|
|
||||
|
||||
<hr>
|
||||
<div id='runner.setMode'>
|
||||
<h4 class='heading'>
|
||||
hilbish.runner.setMode(cb)
|
||||
<a href="#runner.setMode" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
This is the same as the `hilbish.runnerMode` function.
|
||||
It takes a callback, which will be used to execute all interactive input.
|
||||
In normal cases, neither callbacks should be overrided by the user,
|
||||
as the higher level functions listed below this will handle it.
|
||||
|
||||
#### Parameters
|
||||
`function` **`cb`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='runner.lua'>
|
||||
@ -84,164 +95,20 @@ or `load`, but is appropriated for the runner interface.
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='add'>
|
||||
<div id='runner.sh'>
|
||||
<h4 class='heading'>
|
||||
hilbish.runner.add(name, runner)
|
||||
<a href="#add" class='heading-link'>
|
||||
hilbish.runner.sh(cmd)
|
||||
<a href="#runner.sh" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Adds a runner to the table of available runners.
|
||||
If runner is a table, it must have the run function in it.
|
||||
#### Parameters
|
||||
`name` **`string`**
|
||||
Name of the runner
|
||||
Runs a command in Hilbish's shell script interpreter.
|
||||
This is the equivalent of using `source`.
|
||||
|
||||
`runner` **`function|table`**
|
||||
#### Parameters
|
||||
`string` **`cmd`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='exec'>
|
||||
<h4 class='heading'>
|
||||
hilbish.runner.exec(cmd, runnerName)
|
||||
<a href="#exec" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Executes `cmd` with a runner.
|
||||
If `runnerName` is not specified, it uses the default Hilbish runner.
|
||||
#### Parameters
|
||||
`cmd` **`string`**
|
||||
|
||||
|
||||
`runnerName` **`string?`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='get'>
|
||||
<h4 class='heading'>
|
||||
hilbish.runner.get(name)
|
||||
<a href="#get" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Get a runner by name.
|
||||
#### Parameters
|
||||
`name` **`string`**
|
||||
Name of the runner to retrieve.
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='getCurrent'>
|
||||
<h4 class='heading'>
|
||||
hilbish.runner.getCurrent()
|
||||
<a href="#getCurrent" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Returns the current runner by name.
|
||||
#### Parameters
|
||||
This function has no parameters.
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='run'>
|
||||
<h4 class='heading'>
|
||||
hilbish.runner.run(input, priv)
|
||||
<a href="#run" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Runs `input` with the currently set Hilbish runner.
|
||||
This method is how Hilbish executes commands.
|
||||
`priv` is an optional boolean used to state if the input should be saved to history.
|
||||
#### Parameters
|
||||
`input` **`string`**
|
||||
|
||||
|
||||
`priv` **`bool`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='set'>
|
||||
<h4 class='heading'>
|
||||
hilbish.runner.set(name, runner)
|
||||
<a href="#set" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
*Sets* a runner by name. The difference between this function and
|
||||
add, is set will *not* check if the named runner exists.
|
||||
The runner table must have the run function in it.
|
||||
#### Parameters
|
||||
`name` **`string`**
|
||||
|
||||
|
||||
`runner` **`table`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='setCurrent'>
|
||||
<h4 class='heading'>
|
||||
hilbish.runner.setCurrent(name)
|
||||
<a href="#setCurrent" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Sets Hilbish's runner mode by name.
|
||||
#### Parameters
|
||||
`name` **`string`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='setMode'>
|
||||
<h4 class='heading'>
|
||||
hilbish.runner.setMode(mode)
|
||||
<a href="#setMode" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
**NOTE: This function is deprecated and will be removed in 3.0**
|
||||
Use `hilbish.runner.setCurrent` instead.
|
||||
This is the same as the `hilbish.runnerMode` function.
|
||||
It takes a callback, which will be used to execute all interactive input.
|
||||
Or a string which names the runner mode to use.
|
||||
#### Parameters
|
||||
`mode` **`string|function`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='sh'>
|
||||
<h4 class='heading'>
|
||||
hilbish.runner.sh()
|
||||
<a href="#sh" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
|
||||
#### Parameters
|
||||
This function has no parameters.
|
||||
</div>
|
||||
|
||||
|
@ -1,50 +0,0 @@
|
||||
---
|
||||
title: Module snail
|
||||
description: shell script interpreter library
|
||||
layout: doc
|
||||
menu:
|
||||
docs:
|
||||
parent: "API"
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
The snail library houses Hilbish's Lua wrapper of its shell script interpreter.
|
||||
It's not very useful other than running shell scripts, which can be done with other
|
||||
Hilbish functions.
|
||||
|
||||
## Functions
|
||||
|||
|
||||
|----|----|
|
||||
|<a href="#new">new() -> @Snail</a>|Creates a new Snail instance.|
|
||||
|
||||
<hr>
|
||||
<div id='new'>
|
||||
<h4 class='heading'>
|
||||
snail.new() -> <a href="/Hilbish/docs/api/snail/#snail" style="text-decoration: none;" id="lol">Snail</a>
|
||||
<a href="#new" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Creates a new Snail instance.
|
||||
|
||||
#### Parameters
|
||||
This function has no parameters.
|
||||
</div>
|
||||
|
||||
## Types
|
||||
<hr>
|
||||
|
||||
## Snail
|
||||
A Snail is a shell script interpreter instance.
|
||||
|
||||
### Methods
|
||||
#### dir(path)
|
||||
Changes the directory of the snail instance.
|
||||
The interpreter keeps its set directory even when the Hilbish process changes
|
||||
directory, so this should be called on the `hilbish.cd` hook.
|
||||
|
||||
#### run(command, streams)
|
||||
Runs a shell command. Works the same as `hilbish.run`, but only accepts a table of streams.
|
||||
|
@ -33,6 +33,19 @@ needs to run interactive input. For more detail, see the [API documentation](../
|
||||
The `hilbish.runner` interface is an alternative to using `hilbish.runnerMode`
|
||||
and also provides the shell script and Lua runner functions that Hilbish itself uses.
|
||||
|
||||
A runner function is expected to return a table with the following values:
|
||||
- `exitCode` (number): Exit code of the command
|
||||
- `input` (string): The text input of the user. This is used by Hilbish to append extra input, in case
|
||||
more is requested.
|
||||
- `err` (string): A string that represents an error from the runner.
|
||||
This should only be set when, for example, there is a syntax error.
|
||||
It can be set to a few special values for Hilbish to throw the right
|
||||
hooks and have a better looking message.
|
||||
- `<command>: not-found` will throw a `command.not-found` hook
|
||||
based on what `<command>` is.
|
||||
- `<command>: not-executable` will throw a `command.not-executable` hook.
|
||||
- `continue` (boolean): Whether Hilbish should prompt the user for no input
|
||||
|
||||
## Functions
|
||||
These are the "low level" functions for the `hilbish.runner` interface.
|
||||
|
||||
|
@ -53,39 +53,8 @@ which follows XDG on Linux and MacOS, and is located in %APPDATA% on Windows.
|
||||
As the directory is usually `~/.config` on Linux, you can run this command to copy it:
|
||||
`cp /usr/share/hilbish/.hilbishrc.lua ~/.config/hilbish/init.lua`
|
||||
|
||||
Now we can get to customization!
|
||||
|
||||
If we closely examine a small snippet of the default config:
|
||||
```lua
|
||||
-- Default Hilbish config
|
||||
-- .. with some omitted code .. --
|
||||
|
||||
local function doPrompt(fail)
|
||||
hilbish.prompt(lunacolors.format(
|
||||
'{blue}%u {cyan}%d ' .. (fail and '{red}' or '{green}') .. '∆ '
|
||||
))
|
||||
end
|
||||
|
||||
doPrompt()
|
||||
|
||||
bait.catch('command.exit', function(code)
|
||||
doPrompt(code ~= 0)
|
||||
end)
|
||||
```
|
||||
|
||||
We see a whopping **three** Hilbish libraries being used in this part of code.
|
||||
First is of course, named after the shell itself, [`hilbish`](../api/hilbish). This is kind of a
|
||||
"catch-all" namespace for functions that directly related to shell functionality/settings.
|
||||
|
||||
And as we can see, the [hilbish.prompt](../api/hilbish/#prompt) function is used
|
||||
to change our prompt. Change our prompt to what, exactly?
|
||||
|
||||
The doc for the function states that the verbs `%u` and `%d`are used for username and current directory
|
||||
of the shell, respectively.
|
||||
|
||||
We wrap this in the [`lunacolors.format`](../lunacolors) function, to give
|
||||
our prompt some nice color.
|
||||
|
||||
But you might have also noticed that this is in the `doPrompt` function, which is called once,
|
||||
and then used again in a [bait](../api/bait) hook. Specifically, the `command.exit` hook,
|
||||
which is called after a command exits, so when it finishes running.
|
||||
Now you can get to editing it. Since it's just a Lua file, having basic
|
||||
knowledge of Lua would help. All of Lua's standard libraries and functions
|
||||
from Lua 5.4 are available. Hilbish has some custom and modules that are
|
||||
available. To see them, you can run the `doc` command. This also works as
|
||||
general documentation for other things.
|
||||
|
@ -43,29 +43,5 @@ The notification. The properties are defined in the link above.
|
||||
|
||||
<hr>
|
||||
|
||||
## hilbish.cd
|
||||
Sent when the current directory of the shell is changed (via interactive means.)
|
||||
If you are implementing a custom command that changes the directory of the shell,
|
||||
you must throw this hook manually for correctness.
|
||||
|
||||
#### Variables
|
||||
`string` **`path`**
|
||||
Absolute path of the directory that was changed to.
|
||||
|
||||
`string` **`oldPath`**
|
||||
Absolute path of the directory Hilbish *was* in.
|
||||
|
||||
<hr>
|
||||
|
||||
## hilbish.vimAction
|
||||
Sent when the user does a "vim action," being something like yanking or pasting text.
|
||||
See `doc vim-mode actions` for more info.
|
||||
|
||||
#### Variables
|
||||
`string` **`actionName`**
|
||||
Absolute path of the directory that was changed to.
|
||||
|
||||
`table` **`args`**
|
||||
Table of args relating to the Vim action.
|
||||
|
||||
<hr>
|
||||
+ `hilbish.vimAction` -> actionName, args > Sent when the user does a "vim action," being something
|
||||
like yanking or pasting text. See `doc vim-mode actions` for more info.
|
||||
|
@ -1,25 +1,40 @@
|
||||
---
|
||||
title: Module dirs
|
||||
description: internal directory management
|
||||
description: No description.
|
||||
layout: doc
|
||||
menu:
|
||||
docs:
|
||||
parent: "Nature"
|
||||
---
|
||||
|
||||
<hr>
|
||||
<div id='setOld'>
|
||||
<h4 class='heading'>
|
||||
dirs.setOld(d)
|
||||
<a href="#setOld" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
## Introduction
|
||||
The dirs module defines a small set of functions to store and manage
|
||||
directories.
|
||||
Sets the old directory string.
|
||||
#### Parameters
|
||||
`d` **`string`**
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='push'>
|
||||
<h4 class='heading'>
|
||||
dirs.push()
|
||||
<a href="#push" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Add `d` to the recent directories list.
|
||||
#### Parameters
|
||||
This function has no parameters.
|
||||
</div>
|
||||
|
||||
## Functions
|
||||
|||
|
||||
|----|----|
|
||||
|<a href="#setOld">setOld(d)</a>|Sets the old directory string.|
|
||||
|<a href="#recent">recent(idx)</a>|Get entry from recent directories list based on index.|
|
||||
|<a href="#push">push(dir)</a>|Add `dir` to the recent directories list.|
|
||||
|<a href="#pop">pop(num)</a>|Remove the specified amount of dirs from the recent directories list.|
|
||||
|<a href="#peak">peak(num)</a>|Look at `num` amount of recent directories, starting from the latest.|
|
||||
<hr>
|
||||
<div id='peak'>
|
||||
<h4 class='heading'>
|
||||
@ -30,11 +45,8 @@ dirs.peak(num)
|
||||
</h4>
|
||||
|
||||
Look at `num` amount of recent directories, starting from the latest.
|
||||
This returns a table of recent directories, up to the `num` amount.
|
||||
#### Parameters
|
||||
`num` **`number`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
@ -49,24 +61,6 @@ dirs.pop(num)
|
||||
Remove the specified amount of dirs from the recent directories list.
|
||||
#### Parameters
|
||||
`num` **`number`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='push'>
|
||||
<h4 class='heading'>
|
||||
dirs.push(dir)
|
||||
<a href="#push" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Add `dir` to the recent directories list.
|
||||
#### Parameters
|
||||
`dir` **`string`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
@ -81,23 +75,5 @@ dirs.recent(idx)
|
||||
Get entry from recent directories list based on index.
|
||||
#### Parameters
|
||||
`idx` **`number`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='setOld'>
|
||||
<h4 class='heading'>
|
||||
dirs.setOld(d)
|
||||
<a href="#setOld" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Sets the old directory string.
|
||||
#### Parameters
|
||||
`d` **`string`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -1,76 +0,0 @@
|
||||
---
|
||||
title: Module doc
|
||||
description: command-line doc rendering
|
||||
layout: doc
|
||||
menu:
|
||||
docs:
|
||||
parent: "Nature"
|
||||
---
|
||||
|
||||
|
||||
## Introduction
|
||||
The doc module contains a small set of functions
|
||||
used by the Greenhouse pager to render parts of the documentation pages.
|
||||
This is only documented for the sake of it. It's only intended use
|
||||
is by the Greenhouse pager.
|
||||
|
||||
## Functions
|
||||
|||
|
||||
|----|----|
|
||||
|<a href="#renderInfoBlock">renderInfoBlock(type, text)</a>|Renders an info block. An info block is a block of text with|
|
||||
|<a href="#renderCodeBlock">renderCodeBlock(text)</a>|Assembles and renders a code block. This returns|
|
||||
|<a href="#highlight">highlight(text)</a>|Performs basic Lua code highlighting.|
|
||||
<hr>
|
||||
<div id='highlight'>
|
||||
<h4 class='heading'>
|
||||
doc.highlight(text)
|
||||
<a href="#highlight" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Performs basic Lua code highlighting.
|
||||
#### Parameters
|
||||
`text` **`string`**
|
||||
Code/text to do highlighting on.
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='renderCodeBlock'>
|
||||
<h4 class='heading'>
|
||||
doc.renderCodeBlock(text)
|
||||
<a href="#renderCodeBlock" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Assembles and renders a code block. This returns
|
||||
the supplied text based on the number of command line columns,
|
||||
and styles it to resemble a code block.
|
||||
#### Parameters
|
||||
`text` **`string`**
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id='renderInfoBlock'>
|
||||
<h4 class='heading'>
|
||||
doc.renderInfoBlock(type, text)
|
||||
<a href="#renderInfoBlock" class='heading-link'>
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
Renders an info block. An info block is a block of text with
|
||||
an icon and styled text block.
|
||||
#### Parameters
|
||||
`type` **`string`**
|
||||
Type of info block. The only one specially styled is the `warning`.
|
||||
|
||||
`text` **`string`**
|
||||
|
||||
|
||||
</div>
|
||||
|
22
editor.go
22
editor.go
@ -17,7 +17,6 @@ func editorLoader(rtm *rt.Runtime) *rt.Table {
|
||||
"getVimRegister": {editorGetRegister, 2, false},
|
||||
"getLine": {editorGetLine, 0, false},
|
||||
"readChar": {editorReadChar, 0, false},
|
||||
"deleteByAmount": {editorDeleteByAmount, 1, false},
|
||||
}
|
||||
|
||||
mod := rt.NewTable()
|
||||
@ -48,7 +47,7 @@ func editorInsert(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
// #interface editor
|
||||
// setVimRegister(register, text)
|
||||
// Sets the vim register at `register` to hold the passed text.
|
||||
// #param register string
|
||||
// #aram register string
|
||||
// #param text string
|
||||
func editorSetRegister(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
if err := c.Check1Arg(); err != nil {
|
||||
@ -107,22 +106,3 @@ func editorReadChar(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
|
||||
return c.PushingNext1(t.Runtime, rt.StringValue(string(buf))), nil
|
||||
}
|
||||
|
||||
// #interface editor
|
||||
// deleteByAmount(amount)
|
||||
// Deletes characters in the line by the given amount.
|
||||
// #param amount number
|
||||
func editorDeleteByAmount(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
if err := c.Check1Arg(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
amount, err := c.IntArg(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lr.rl.DeleteByAmount(int(amount))
|
||||
|
||||
return c.Next(), nil
|
||||
}
|
||||
|
@ -7,8 +7,11 @@ local hilbish = {}
|
||||
--- @param cmd string
|
||||
function hilbish.aliases.add(alias, cmd) end
|
||||
|
||||
--- Deletes characters in the line by the given amount.
|
||||
function hilbish.editor.deleteByAmount(amount) end
|
||||
--- This is the same as the `hilbish.runnerMode` function.
|
||||
--- It takes a callback, which will be used to execute all interactive input.
|
||||
--- In normal cases, neither callbacks should be overrided by the user,
|
||||
--- as the higher level functions listed below this will handle it.
|
||||
function hilbish.runner.setMode(cb) end
|
||||
|
||||
--- Returns the current input line.
|
||||
function hilbish.editor.getLine() end
|
||||
@ -128,6 +131,24 @@ function hilbish.prompt(str, typ) end
|
||||
--- Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs.
|
||||
function hilbish.read(prompt) end
|
||||
|
||||
--- Runs `cmd` in Hilbish's shell script interpreter.
|
||||
--- The `streams` parameter specifies the output and input streams the command should use.
|
||||
--- For example, to write command output to a sink.
|
||||
--- As a table, the caller can directly specify the standard output, error, and input
|
||||
--- streams of the command with the table keys `out`, `err`, and `input` respectively.
|
||||
--- As a boolean, it specifies whether the command should use standard output or return its output streams.
|
||||
---
|
||||
function hilbish.run(cmd, streams) end
|
||||
|
||||
--- Sets the execution/runner mode for interactive Hilbish.
|
||||
--- This determines whether Hilbish wll try to run input as Lua
|
||||
--- and/or sh or only do one of either.
|
||||
--- Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua),
|
||||
--- sh, and lua. It also accepts a function, to which if it is passed one
|
||||
--- will call it to execute user input instead.
|
||||
--- Read [about runner mode](../features/runner-mode) for more information.
|
||||
function hilbish.runnerMode(mode) end
|
||||
|
||||
--- Executed the `cb` function after a period of `time`.
|
||||
--- This creates a Timer that starts ticking immediately.
|
||||
function hilbish.timeout(cb, time) end
|
||||
@ -147,6 +168,28 @@ function hilbish.jobs:foreground() end
|
||||
--- or `load`, but is appropriated for the runner interface.
|
||||
function hilbish.runner.lua(cmd) end
|
||||
|
||||
--- Sets/toggles the option of automatically flushing output.
|
||||
--- A call with no argument will toggle the value.
|
||||
--- @param auto boolean|nil
|
||||
function hilbish:autoFlush(auto) end
|
||||
|
||||
--- Flush writes all buffered input to the sink.
|
||||
function hilbish:flush() end
|
||||
|
||||
--- Reads a liine of input from the sink.
|
||||
--- @returns string
|
||||
function hilbish:read() end
|
||||
|
||||
--- Reads all input from the sink.
|
||||
--- @returns string
|
||||
function hilbish:readAll() end
|
||||
|
||||
--- Writes data to a sink.
|
||||
function hilbish:write(str) end
|
||||
|
||||
--- Writes data to a sink with a newline at the end.
|
||||
function hilbish:writeln(str) end
|
||||
|
||||
--- Starts running the job.
|
||||
function hilbish.jobs:start() end
|
||||
|
||||
@ -157,6 +200,10 @@ function hilbish.jobs:stop() end
|
||||
--- It will throw if any error occurs.
|
||||
function hilbish.module.load(path) end
|
||||
|
||||
--- Runs a command in Hilbish's shell script interpreter.
|
||||
--- This is the equivalent of using `source`.
|
||||
function hilbish.runner.sh(cmd) end
|
||||
|
||||
--- Starts a timer.
|
||||
function hilbish.timers:start() end
|
||||
|
||||
@ -215,26 +262,4 @@ function hilbish.timers.create(type, time, callback) end
|
||||
--- Retrieves a timer via its ID.
|
||||
function hilbish.timers.get(id) end
|
||||
|
||||
--- Sets/toggles the option of automatically flushing output.
|
||||
--- A call with no argument will toggle the value.
|
||||
--- @param auto boolean|nil
|
||||
function hilbish:autoFlush(auto) end
|
||||
|
||||
--- Flush writes all buffered input to the sink.
|
||||
function hilbish:flush() end
|
||||
|
||||
--- Reads a liine of input from the sink.
|
||||
--- @returns string
|
||||
function hilbish:read() end
|
||||
|
||||
--- Reads all input from the sink.
|
||||
--- @returns string
|
||||
function hilbish:readAll() end
|
||||
|
||||
--- Writes data to a sink.
|
||||
function hilbish:write(str) end
|
||||
|
||||
--- Writes data to a sink with a newline at the end.
|
||||
function hilbish:writeln(str) end
|
||||
|
||||
return hilbish
|
||||
|
@ -1,16 +0,0 @@
|
||||
--- @meta
|
||||
|
||||
local snail = {}
|
||||
|
||||
--- Changes the directory of the snail instance.
|
||||
--- The interpreter keeps its set directory even when the Hilbish process changes
|
||||
--- directory, so this should be called on the `hilbish.cd` hook.
|
||||
function snail:dir(path) end
|
||||
|
||||
--- Creates a new Snail instance.
|
||||
function snail.new() end
|
||||
|
||||
--- Runs a shell command. Works the same as `hilbish.run`, but only accepts a table of streams.
|
||||
function snail:run(command, streams) end
|
||||
|
||||
return snail
|
@ -1,83 +0,0 @@
|
||||
--- @meta
|
||||
|
||||
local util = {}
|
||||
|
||||
---
|
||||
function util.AbbrevHome changes the user's home directory in the path string to ~ (tilde) end
|
||||
|
||||
---
|
||||
function util. end
|
||||
|
||||
---
|
||||
function util.DoFile runs the contents of the file in the Lua runtime. end
|
||||
|
||||
---
|
||||
function util.DoString runs the code string in the Lua runtime. end
|
||||
|
||||
--- directory.
|
||||
function util.ExpandHome expands ~ (tilde) in the path, changing it to the user home end
|
||||
|
||||
---
|
||||
function util. end
|
||||
|
||||
---
|
||||
function util.ForEach loops through a Lua table. end
|
||||
|
||||
---
|
||||
function util. end
|
||||
|
||||
--- a string and a closure.
|
||||
function util.HandleStrCallback handles function parameters for Go functions which take end
|
||||
|
||||
---
|
||||
function util. end
|
||||
|
||||
---
|
||||
function util. end
|
||||
|
||||
---
|
||||
function util.SetExports puts the Lua function exports in the table. end
|
||||
|
||||
--- It is accessible via the __docProp metatable. It is a table of the names of the fields.
|
||||
function util.SetField sets a field in a table, adding docs for it. end
|
||||
|
||||
--- is one which has a metatable proxy to ensure no overrides happen to it.
|
||||
--- It sets the field in the table and sets the __docProp metatable on the
|
||||
--- user facing table.
|
||||
function util.SetFieldProtected sets a field in a protected table. A protected table end
|
||||
|
||||
--- Sets/toggles the option of automatically flushing output.
|
||||
--- A call with no argument will toggle the value.
|
||||
--- @param auto boolean|nil
|
||||
function util:autoFlush(auto) end
|
||||
|
||||
--- Flush writes all buffered input to the sink.
|
||||
function util:flush() end
|
||||
|
||||
---
|
||||
function util. end
|
||||
|
||||
--- Reads a liine of input from the sink.
|
||||
--- @returns string
|
||||
function util:read() end
|
||||
|
||||
--- Reads all input from the sink.
|
||||
--- @returns string
|
||||
function util:readAll() end
|
||||
|
||||
--- Writes data to a sink.
|
||||
function util:write(str) end
|
||||
|
||||
--- Writes data to a sink with a newline at the end.
|
||||
function util:writeln(str) end
|
||||
|
||||
---
|
||||
function util. end
|
||||
|
||||
---
|
||||
function util. end
|
||||
|
||||
---
|
||||
function util. end
|
||||
|
||||
return util
|
539
exec.go
539
exec.go
@ -1,26 +1,208 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"os/exec"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hilbish/util"
|
||||
|
||||
rt "github.com/arnodel/golua/runtime"
|
||||
"mvdan.cc/sh/v3/shell"
|
||||
//"github.com/yuin/gopher-lua/parse"
|
||||
"mvdan.cc/sh/v3/interp"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
"mvdan.cc/sh/v3/expand"
|
||||
)
|
||||
|
||||
var errNotExec = errors.New("not executable")
|
||||
var errNotFound = errors.New("not found")
|
||||
var runnerMode rt.Value = rt.NilValue
|
||||
var runnerMode rt.Value = rt.StringValue("hybrid")
|
||||
|
||||
type streams struct {
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
stdin io.Reader
|
||||
}
|
||||
|
||||
type execError struct{
|
||||
typ string
|
||||
cmd string
|
||||
code int
|
||||
colon bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (e execError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.cmd, e.typ)
|
||||
}
|
||||
|
||||
func (e execError) sprint() error {
|
||||
sep := " "
|
||||
if e.colon {
|
||||
sep = ": "
|
||||
}
|
||||
|
||||
return fmt.Errorf("hilbish: %s%s%s", e.cmd, sep, e.err.Error())
|
||||
}
|
||||
|
||||
func isExecError(err error) (execError, bool) {
|
||||
if exErr, ok := err.(execError); ok {
|
||||
return exErr, true
|
||||
}
|
||||
|
||||
fields := strings.Split(err.Error(), ": ")
|
||||
knownTypes := []string{
|
||||
"not-found",
|
||||
"not-executable",
|
||||
}
|
||||
|
||||
if len(fields) > 1 && contains(knownTypes, fields[1]) {
|
||||
var colon bool
|
||||
var e error
|
||||
switch fields[1] {
|
||||
case "not-found":
|
||||
e = errNotFound
|
||||
case "not-executable":
|
||||
colon = true
|
||||
e = errNotExec
|
||||
}
|
||||
|
||||
return execError{
|
||||
cmd: fields[0],
|
||||
typ: fields[1],
|
||||
colon: colon,
|
||||
err: e,
|
||||
}, true
|
||||
}
|
||||
|
||||
return execError{}, false
|
||||
}
|
||||
|
||||
func runInput(input string, priv bool) {
|
||||
running = true
|
||||
runnerRun := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("run"))
|
||||
_, err := rt.Call1(l.MainThread(), runnerRun, rt.StringValue(input), rt.BoolValue(priv))
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
cmdString := aliases.Resolve(input)
|
||||
hooks.Emit("command.preexec", input, cmdString)
|
||||
|
||||
rerun:
|
||||
var exitCode uint8
|
||||
var err error
|
||||
var cont bool
|
||||
// save incase it changes while prompting (For some reason)
|
||||
currentRunner := runnerMode
|
||||
if currentRunner.Type() == rt.StringType {
|
||||
switch currentRunner.AsString() {
|
||||
case "hybrid":
|
||||
_, _, err = handleLua(input)
|
||||
if err == nil {
|
||||
cmdFinish(0, input, priv)
|
||||
return
|
||||
}
|
||||
input, exitCode, cont, err = handleSh(input)
|
||||
case "hybridRev":
|
||||
_, _, _, err = handleSh(input)
|
||||
if err == nil {
|
||||
cmdFinish(0, input, priv)
|
||||
return
|
||||
}
|
||||
input, exitCode, err = handleLua(input)
|
||||
case "lua":
|
||||
input, exitCode, err = handleLua(input)
|
||||
case "sh":
|
||||
input, exitCode, cont, err = handleSh(input)
|
||||
}
|
||||
} else {
|
||||
// can only be a string or function so
|
||||
var runnerErr error
|
||||
input, exitCode, cont, runnerErr, err = runLuaRunner(currentRunner, input)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
cmdFinish(124, input, priv)
|
||||
return
|
||||
}
|
||||
// yep, we only use `err` to check for lua eval error
|
||||
// our actual error should only be a runner provided error at this point
|
||||
// command not found type, etc
|
||||
err = runnerErr
|
||||
}
|
||||
|
||||
if cont {
|
||||
input, err = reprompt(input)
|
||||
if err == nil {
|
||||
goto rerun
|
||||
} else if err == io.EOF {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if exErr, ok := isExecError(err); ok {
|
||||
hooks.Emit("command." + exErr.typ, exErr.cmd)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
}
|
||||
cmdFinish(exitCode, input, priv)
|
||||
}
|
||||
|
||||
func reprompt(input string) (string, error) {
|
||||
for {
|
||||
in, err := continuePrompt(strings.TrimSuffix(input, "\\"))
|
||||
if err != nil {
|
||||
lr.SetPrompt(fmtPrompt(prompt))
|
||||
return input, err
|
||||
}
|
||||
|
||||
if strings.HasSuffix(in, "\\") {
|
||||
continue
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
}
|
||||
|
||||
func runLuaRunner(runr rt.Value, userInput string) (input string, exitCode uint8, continued bool, runnerErr, err error) {
|
||||
term := rt.NewTerminationWith(l.MainThread().CurrentCont(), 3, false)
|
||||
err = rt.Call(l.MainThread(), runr, []rt.Value{rt.StringValue(userInput)}, term)
|
||||
if err != nil {
|
||||
return "", 124, false, nil, err
|
||||
}
|
||||
|
||||
var runner *rt.Table
|
||||
var ok bool
|
||||
runnerRet := term.Get(0)
|
||||
if runner, ok = runnerRet.TryTable(); !ok {
|
||||
fmt.Fprintln(os.Stderr, "runner did not return a table")
|
||||
exitCode = 125
|
||||
input = userInput
|
||||
return
|
||||
}
|
||||
|
||||
if code, ok := runner.Get(rt.StringValue("exitCode")).TryInt(); ok {
|
||||
exitCode = uint8(code)
|
||||
}
|
||||
|
||||
if inp, ok := runner.Get(rt.StringValue("input")).TryString(); ok {
|
||||
input = inp
|
||||
}
|
||||
|
||||
if errStr, ok := runner.Get(rt.StringValue("err")).TryString(); ok {
|
||||
runnerErr = fmt.Errorf("%s", errStr)
|
||||
}
|
||||
|
||||
if c, ok := runner.Get(rt.StringValue("continue")).TryBool(); ok {
|
||||
continued = c
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func handleLua(input string) (string, uint8, error) {
|
||||
@ -50,13 +232,336 @@ func handleLua(input string) (string, uint8, error) {
|
||||
return cmdString, 125, err
|
||||
}
|
||||
|
||||
func handleSh(cmdString string) (input string, exitCode uint8, cont bool, runErr error) {
|
||||
shRunner := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("sh"))
|
||||
var err error
|
||||
input, exitCode, cont, runErr, err = runLuaRunner(shRunner, cmdString)
|
||||
if err != nil {
|
||||
runErr = err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func execSh(cmdString string) (string, uint8, bool, error) {
|
||||
_, _, err := execCommand(cmdString, nil)
|
||||
if err != nil {
|
||||
// If input is incomplete, start multiline prompting
|
||||
if syntax.IsIncomplete(err) {
|
||||
if !interactive {
|
||||
return cmdString, 126, false, err
|
||||
}
|
||||
return cmdString, 126, true, err
|
||||
} else {
|
||||
if code, ok := interp.IsExitStatus(err); ok {
|
||||
return cmdString, code, false, nil
|
||||
} else {
|
||||
return cmdString, 126, false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cmdString, 0, false, nil
|
||||
}
|
||||
|
||||
// Run command in sh interpreter
|
||||
func execCommand(cmd string, strms *streams) (io.Writer, io.Writer, error) {
|
||||
file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if strms == nil {
|
||||
strms = &streams{}
|
||||
}
|
||||
|
||||
if strms.stdout == nil {
|
||||
strms.stdout = os.Stdout
|
||||
}
|
||||
|
||||
if strms.stderr == nil {
|
||||
strms.stderr = os.Stderr
|
||||
}
|
||||
|
||||
if strms.stdin == nil {
|
||||
strms.stdin = os.Stdin
|
||||
}
|
||||
|
||||
interp.StdIO(strms.stdin, strms.stdout, strms.stderr)(runner)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
printer := syntax.NewPrinter()
|
||||
|
||||
var bg bool
|
||||
for _, stmt := range file.Stmts {
|
||||
bg = false
|
||||
if stmt.Background {
|
||||
bg = true
|
||||
printer.Print(buf, stmt.Cmd)
|
||||
|
||||
stmtStr := buf.String()
|
||||
buf.Reset()
|
||||
jobs.add(stmtStr, []string{}, "")
|
||||
}
|
||||
|
||||
interp.ExecHandler(execHandle(bg))(runner)
|
||||
err = runner.Run(context.TODO(), stmt)
|
||||
if err != nil {
|
||||
return strms.stdout, strms.stderr, err
|
||||
}
|
||||
}
|
||||
|
||||
return strms.stdout, strms.stderr, nil
|
||||
}
|
||||
|
||||
func execHandle(bg bool) interp.ExecHandlerFunc {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
_, argstring := splitInput(strings.Join(args, " "))
|
||||
// i dont really like this but it works
|
||||
if aliases.All()[args[0]] != "" {
|
||||
for i, arg := range args {
|
||||
if strings.Contains(arg, " ") {
|
||||
args[i] = fmt.Sprintf("\"%s\"", arg)
|
||||
}
|
||||
}
|
||||
_, argstring = splitInput(strings.Join(args, " "))
|
||||
|
||||
// If alias was found, use command alias
|
||||
argstring = aliases.Resolve(argstring)
|
||||
var err error
|
||||
args, err = shell.Fields(argstring, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If command is defined in Lua then run it
|
||||
luacmdArgs := rt.NewTable()
|
||||
for i, str := range args[1:] {
|
||||
luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str))
|
||||
}
|
||||
|
||||
hc := interp.HandlerCtx(ctx)
|
||||
if cmd := cmds.Commands[args[0]]; cmd != nil {
|
||||
stdin := newSinkInput(hc.Stdin)
|
||||
stdout := newSinkOutput(hc.Stdout)
|
||||
stderr := newSinkOutput(hc.Stderr)
|
||||
|
||||
sinks := rt.NewTable()
|
||||
sinks.Set(rt.StringValue("in"), rt.UserDataValue(stdin.ud))
|
||||
sinks.Set(rt.StringValue("input"), rt.UserDataValue(stdin.ud))
|
||||
sinks.Set(rt.StringValue("out"), rt.UserDataValue(stdout.ud))
|
||||
sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.ud))
|
||||
|
||||
t := rt.NewThread(l)
|
||||
sig := make(chan os.Signal)
|
||||
exit := make(chan bool)
|
||||
|
||||
luaexitcode := rt.IntValue(63)
|
||||
var err error
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
exit <- true
|
||||
}
|
||||
}()
|
||||
|
||||
signal.Notify(sig, os.Interrupt)
|
||||
select {
|
||||
case <-sig:
|
||||
t.KillContext()
|
||||
return
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
go func() {
|
||||
luaexitcode, err = rt.Call1(t, rt.FunctionValue(cmd), rt.TableValue(luacmdArgs), rt.TableValue(sinks))
|
||||
exit <- true
|
||||
}()
|
||||
|
||||
<-exit
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error())
|
||||
return interp.NewExitStatus(1)
|
||||
}
|
||||
|
||||
var exitcode uint8
|
||||
|
||||
if code, ok := luaexitcode.TryInt(); ok {
|
||||
exitcode = uint8(code)
|
||||
} else if luaexitcode != rt.NilValue {
|
||||
// deregister commander
|
||||
delete(cmds.Commands, args[0])
|
||||
fmt.Fprintf(os.Stderr, "Commander did not return number for exit code. %s, you're fired.\n", args[0])
|
||||
}
|
||||
|
||||
return interp.NewExitStatus(exitcode)
|
||||
}
|
||||
|
||||
err := lookpath(args[0])
|
||||
if err == errNotExec {
|
||||
return execError{
|
||||
typ: "not-executable",
|
||||
cmd: args[0],
|
||||
code: 126,
|
||||
colon: true,
|
||||
err: errNotExec,
|
||||
}
|
||||
} else if err != nil {
|
||||
return execError{
|
||||
typ: "not-found",
|
||||
cmd: args[0],
|
||||
code: 127,
|
||||
err: errNotFound,
|
||||
}
|
||||
}
|
||||
|
||||
killTimeout := 2 * time.Second
|
||||
// from here is basically copy-paste of the default exec handler from
|
||||
// sh/interp but with our job handling
|
||||
path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0])
|
||||
if err != nil {
|
||||
fmt.Fprintln(hc.Stderr, err)
|
||||
return interp.NewExitStatus(127)
|
||||
}
|
||||
|
||||
env := hc.Env
|
||||
envList := make([]string, 0, 64)
|
||||
env.Each(func(name string, vr expand.Variable) bool {
|
||||
if !vr.IsSet() {
|
||||
// If a variable is set globally but unset in the
|
||||
// runner, we need to ensure it's not part of the final
|
||||
// list. Seems like zeroing the element is enough.
|
||||
// This is a linear search, but this scenario should be
|
||||
// rare, and the number of variables shouldn't be large.
|
||||
for i, kv := range envList {
|
||||
if strings.HasPrefix(kv, name+"=") {
|
||||
envList[i] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
if vr.Exported && vr.Kind == expand.String {
|
||||
envList = append(envList, name+"="+vr.String())
|
||||
}
|
||||
return true
|
||||
})
|
||||
cmd := exec.Cmd{
|
||||
Path: path,
|
||||
Args: args,
|
||||
Env: envList,
|
||||
Dir: hc.Dir,
|
||||
Stdin: hc.Stdin,
|
||||
Stdout: hc.Stdout,
|
||||
Stderr: hc.Stderr,
|
||||
}
|
||||
|
||||
var j *job
|
||||
if bg {
|
||||
j = jobs.getLatest()
|
||||
j.setHandle(&cmd)
|
||||
err = j.start()
|
||||
} else {
|
||||
err = cmd.Start()
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if done := ctx.Done(); done != nil {
|
||||
go func() {
|
||||
<-done
|
||||
|
||||
if killTimeout <= 0 || runtime.GOOS == "windows" {
|
||||
cmd.Process.Signal(os.Kill)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: don't temporarily leak this goroutine
|
||||
// if the program stops itself with the
|
||||
// interrupt.
|
||||
go func() {
|
||||
time.Sleep(killTimeout)
|
||||
cmd.Process.Signal(os.Kill)
|
||||
}()
|
||||
cmd.Process.Signal(os.Interrupt)
|
||||
}()
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
}
|
||||
|
||||
exit := handleExecErr(err)
|
||||
|
||||
if bg {
|
||||
j.exitCode = int(exit)
|
||||
j.finish()
|
||||
}
|
||||
return interp.NewExitStatus(exit)
|
||||
}
|
||||
}
|
||||
|
||||
func handleExecErr(err error) (exit uint8) {
|
||||
ctx := context.TODO()
|
||||
|
||||
switch x := err.(type) {
|
||||
case *exec.ExitError:
|
||||
// started, but errored - default to 1 if OS
|
||||
// doesn't have exit statuses
|
||||
if status, ok := x.Sys().(syscall.WaitStatus); ok {
|
||||
if status.Signaled() {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
exit = uint8(128 + status.Signal())
|
||||
return
|
||||
}
|
||||
exit = uint8(status.ExitStatus())
|
||||
return
|
||||
}
|
||||
exit = 1
|
||||
return
|
||||
case *exec.Error:
|
||||
// did not start
|
||||
//fmt.Fprintf(hc.Stderr, "%v\n", err)
|
||||
exit = 127
|
||||
default: return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
func lookpath(file string) error { // custom lookpath function so we know if a command is found *and* is executable
|
||||
var skip []string
|
||||
if runtime.GOOS == "windows" {
|
||||
skip = []string{"./", "../", "~/", "C:"}
|
||||
} else {
|
||||
skip = []string{"./", "/", "../", "~/"}
|
||||
}
|
||||
for _, s := range skip {
|
||||
if strings.HasPrefix(file, s) {
|
||||
return findExecutable(file, false, false)
|
||||
}
|
||||
}
|
||||
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
|
||||
path := filepath.Join(dir, file)
|
||||
err := findExecutable(path, true, false)
|
||||
if err == errNotExec {
|
||||
return err
|
||||
} else if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
func splitInput(input string) ([]string, string) {
|
||||
// end my suffering
|
||||
// TODO: refactor this garbage
|
||||
quoted := false
|
||||
startlastcmd := false
|
||||
lastcmddone := false
|
||||
cmdArgs := []string{}
|
||||
sb := &strings.Builder{}
|
||||
cmdstr := &strings.Builder{}
|
||||
lastcmd := "" //readline.GetHistory(readline.HistorySize() - 1)
|
||||
|
||||
for _, r := range input {
|
||||
if r == '"' {
|
||||
@ -72,6 +577,22 @@ func splitInput(input string) ([]string, string) {
|
||||
// if not quoted and there's a space then add to cmdargs
|
||||
cmdArgs = append(cmdArgs, sb.String())
|
||||
sb.Reset()
|
||||
} else if !quoted && r == '^' && startlastcmd && !lastcmddone {
|
||||
// if ^ is found, isnt in quotes and is
|
||||
// the second occurence of the character and is
|
||||
// the first time "^^" has been used
|
||||
cmdstr.WriteString(lastcmd)
|
||||
sb.WriteString(lastcmd)
|
||||
|
||||
startlastcmd = !startlastcmd
|
||||
lastcmddone = !lastcmddone
|
||||
|
||||
continue
|
||||
} else if !quoted && r == '^' && !lastcmddone {
|
||||
// if ^ is found, isnt in quotes and is the
|
||||
// first time of starting "^^"
|
||||
startlastcmd = !startlastcmd
|
||||
continue
|
||||
} else {
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
@ -83,3 +604,11 @@ func splitInput(input string) ([]string, string) {
|
||||
|
||||
return cmdArgs, cmdstr.String()
|
||||
}
|
||||
|
||||
func cmdFinish(code uint8, cmdstr string, private bool) {
|
||||
util.SetField(l, hshMod, "exitCode", rt.IntValue(int64(code)))
|
||||
// using AsValue (to convert to lua type) on an interface which is an int
|
||||
// results in it being unknown in lua .... ????
|
||||
// so we allow the hook handler to take lua runtime Values
|
||||
hooks.Emit("command.exit", rt.IntValue(int64(code)), cmdstr, private)
|
||||
}
|
||||
|
@ -1,12 +1,17 @@
|
||||
//go:build unix
|
||||
|
||||
package util
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func FindExecutable(path string, inPath, dirs bool) error {
|
||||
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
|
||||
func findExecutable(path string, inPath, dirs bool) error {
|
||||
f, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -20,5 +25,5 @@ func FindExecutable(path string, inPath, dirs bool) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ErrNotExec
|
||||
return errNotExec
|
||||
}
|
@ -1,13 +1,18 @@
|
||||
//go:build windows
|
||||
|
||||
package util
|
||||
package main
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func FindExecutable(path string, inPath, dirs bool) error {
|
||||
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
||||
}
|
||||
|
||||
func findExecutable(path string, inPath, dirs bool) error {
|
||||
nameExt := filepath.Ext(path)
|
||||
pathExts := filepath.SplitList(os.Getenv("PATHEXT"))
|
||||
if inPath {
|
||||
@ -21,15 +26,15 @@ func FindExecutable(path string, inPath, dirs bool) error {
|
||||
} else {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if Contains(pathExts, nameExt) { return nil }
|
||||
return ErrNotExec
|
||||
if contains(pathExts, nameExt) { return nil }
|
||||
return errNotExec
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if Contains(pathExts, nameExt) { return nil }
|
||||
return ErrNotExec
|
||||
if contains(pathExts, nameExt) { return nil }
|
||||
return errNotExec
|
||||
}
|
||||
}
|
||||
|
4
go.mod
4
go.mod
@ -28,10 +28,10 @@ require (
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
|
||||
replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240815163633-562273e09b73
|
||||
replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240720131751-805c301321fd
|
||||
|
||||
replace github.com/maxlandon/readline => ./readline
|
||||
|
||||
replace layeh.com/gopher-luar => github.com/layeh/gopher-luar v1.0.10
|
||||
|
||||
replace github.com/arnodel/golua => github.com/Rosettea/golua v0.0.0-20241104031959-5551ea280f23
|
||||
replace github.com/arnodel/golua => github.com/Rosettea/golua v0.0.0-20240427174124-d239074c1749
|
||||
|
8
go.sum
8
go.sum
@ -1,7 +1,7 @@
|
||||
github.com/Rosettea/golua v0.0.0-20241104031959-5551ea280f23 h1:mUZnT0gmDEmTkqXsbnDbuJ3CNil7DCOMiCQYgjbKIdI=
|
||||
github.com/Rosettea/golua v0.0.0-20241104031959-5551ea280f23/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE=
|
||||
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240815163633-562273e09b73 h1:zTTUJqNnrF2qf4LgygN8Oae5Uxn6ewH0hA8jyTCHfXw=
|
||||
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240815163633-562273e09b73/go.mod h1:YZalN5H7WNQw3DGij6IvHsEhn5YMW7M2FCwG6gnfKy4=
|
||||
github.com/Rosettea/golua v0.0.0-20240427174124-d239074c1749 h1:jIFnWBTsYw8s7RX7H2AOXjDVhWP3ol7OzUVaPN2KnGI=
|
||||
github.com/Rosettea/golua v0.0.0-20240427174124-d239074c1749/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE=
|
||||
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240720131751-805c301321fd h1:THNle0FR2g7DMO1y3Bx1Zr7rYeiLXt3st3UkxEsMzL4=
|
||||
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240720131751-805c301321fd/go.mod h1:YZalN5H7WNQw3DGij6IvHsEhn5YMW7M2FCwG6gnfKy4=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
|
||||
github.com/arnodel/strftime v0.1.6 h1:0hc0pUvk8KhEMXE+htyaOUV42zNcf/csIbjzEFCJqsw=
|
||||
|
@ -19,25 +19,38 @@ import (
|
||||
rt "github.com/arnodel/golua/runtime"
|
||||
"github.com/arnodel/golua/lib/packagelib"
|
||||
"github.com/arnodel/golua/lib/iolib"
|
||||
"mvdan.cc/sh/v3/interp"
|
||||
)
|
||||
|
||||
var Loader = packagelib.Loader{
|
||||
Load: loaderFunc,
|
||||
Name: "fs",
|
||||
type fs struct{
|
||||
runner *interp.Runner
|
||||
Loader packagelib.Loader
|
||||
}
|
||||
|
||||
func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
|
||||
func New(runner *interp.Runner) *fs {
|
||||
f := &fs{
|
||||
runner: runner,
|
||||
}
|
||||
f.Loader = packagelib.Loader{
|
||||
Load: f.loaderFunc,
|
||||
Name: "fs",
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *fs) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
|
||||
exports := map[string]util.LuaExport{
|
||||
"cd": util.LuaExport{fcd, 1, false},
|
||||
"mkdir": util.LuaExport{fmkdir, 2, false},
|
||||
"stat": util.LuaExport{fstat, 1, false},
|
||||
"readdir": util.LuaExport{freaddir, 1, false},
|
||||
"abs": util.LuaExport{fabs, 1, false},
|
||||
"basename": util.LuaExport{fbasename, 1, false},
|
||||
"dir": util.LuaExport{fdir, 1, false},
|
||||
"glob": util.LuaExport{fglob, 1, false},
|
||||
"join": util.LuaExport{fjoin, 0, true},
|
||||
"pipe": util.LuaExport{fpipe, 0, false},
|
||||
"cd": util.LuaExport{f.fcd, 1, false},
|
||||
"mkdir": util.LuaExport{f.fmkdir, 2, false},
|
||||
"stat": util.LuaExport{f.fstat, 1, false},
|
||||
"readdir": util.LuaExport{f.freaddir, 1, false},
|
||||
"abs": util.LuaExport{f.fabs, 1, false},
|
||||
"basename": util.LuaExport{f.fbasename, 1, false},
|
||||
"dir": util.LuaExport{f.fdir, 1, false},
|
||||
"glob": util.LuaExport{f.fglob, 1, false},
|
||||
"join": util.LuaExport{f.fjoin, 0, true},
|
||||
"pipe": util.LuaExport{f.fpipe, 0, false},
|
||||
}
|
||||
mod := rt.NewTable()
|
||||
util.SetExports(rtm, mod, exports)
|
||||
@ -52,7 +65,7 @@ func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
|
||||
// This can be used to resolve short paths like `..` to `/home/user`.
|
||||
// #param path string
|
||||
// #returns string
|
||||
func fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
func (f *fs) fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
path, err := c.StringArg(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -72,7 +85,7 @@ func fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
// `.` will be returned.
|
||||
// #param path string Path to get the base name of.
|
||||
// #returns string
|
||||
func fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
func (f *fs) fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
if err := c.Check1Arg(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -87,7 +100,7 @@ func fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
// cd(dir)
|
||||
// Changes Hilbish's directory to `dir`.
|
||||
// #param dir string Path to change directory to.
|
||||
func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
func (f *fs) fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
if err := c.Check1Arg(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -97,10 +110,12 @@ func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
}
|
||||
path = util.ExpandHome(strings.TrimSpace(path))
|
||||
|
||||
abspath, _ := filepath.Abs(path)
|
||||
err = os.Chdir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
interp.Dir(abspath)(f.runner)
|
||||
|
||||
return c.Next(), err
|
||||
}
|
||||
@ -110,7 +125,7 @@ func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
// `~/Documents/doc.txt` then this function will return `~/Documents`.
|
||||
// #param path string Path to get the directory for.
|
||||
// #returns string
|
||||
func fdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
func (f *fs) fdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
if err := c.Check1Arg(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -141,7 +156,7 @@ print(matches)
|
||||
-- -> {'init.lua', 'code.lua'}
|
||||
#example
|
||||
*/
|
||||
func fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
func (f *fs) fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
if err := c.Check1Arg(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -175,7 +190,7 @@ print(fs.join(hilbish.userDir.config, 'hilbish'))
|
||||
-- -> '/home/user/.config/hilbish' on Linux
|
||||
#example
|
||||
*/
|
||||
func fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
func (f *fs) fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
strs := make([]string, len(c.Etc()))
|
||||
for i, v := range c.Etc() {
|
||||
if v.Type() != rt.StringType {
|
||||
@ -202,7 +217,7 @@ func fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
fs.mkdir('./foo/bar', true)
|
||||
#example
|
||||
*/
|
||||
func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
func (f *fs) fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
if err := c.CheckNArgs(2); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -233,7 +248,7 @@ func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
// The type returned is a Lua file, same as returned from `io` functions.
|
||||
// #returns File
|
||||
// #returns File
|
||||
func fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
func (f *fs) fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
rf, wf, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -248,7 +263,7 @@ func fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
// Returns a list of all files and directories in the provided path.
|
||||
// #param dir string
|
||||
// #returns table
|
||||
func freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
func (f *fs) freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
if err := c.Check1Arg(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -296,7 +311,7 @@ Would print the following:
|
||||
]]--
|
||||
#example
|
||||
*/
|
||||
func fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
func (f *fs) fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
if err := c.Check1Arg(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1,221 +0,0 @@
|
||||
// shell script interpreter library
|
||||
/*
|
||||
The snail library houses Hilbish's Lua wrapper of its shell script interpreter.
|
||||
It's not very useful other than running shell scripts, which can be done with other
|
||||
Hilbish functions.
|
||||
*/
|
||||
package snail
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"hilbish/util"
|
||||
|
||||
rt "github.com/arnodel/golua/runtime"
|
||||
"github.com/arnodel/golua/lib/packagelib"
|
||||
"github.com/arnodel/golua/lib/iolib"
|
||||
"mvdan.cc/sh/v3/interp"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
)
|
||||
|
||||
var snailMetaKey = rt.StringValue("hshsnail")
|
||||
var Loader = packagelib.Loader{
|
||||
Load: loaderFunc,
|
||||
Name: "snail",
|
||||
}
|
||||
|
||||
func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
|
||||
snailMeta := rt.NewTable()
|
||||
snailMethods := rt.NewTable()
|
||||
snailFuncs := map[string]util.LuaExport{
|
||||
"run": {snailrun, 3, false},
|
||||
"dir": {snaildir, 2, false},
|
||||
}
|
||||
util.SetExports(rtm, snailMethods, snailFuncs)
|
||||
|
||||
snailIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
arg := c.Arg(1)
|
||||
val := snailMethods.Get(arg)
|
||||
|
||||
return c.PushingNext1(t.Runtime, val), nil
|
||||
}
|
||||
snailMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(snailIndex, "__index", 2, false)))
|
||||
rtm.SetRegistry(snailMetaKey, rt.TableValue(snailMeta))
|
||||
|
||||
exports := map[string]util.LuaExport{
|
||||
"new": util.LuaExport{snailnew, 0, false},
|
||||
}
|
||||
|
||||
mod := rt.NewTable()
|
||||
util.SetExports(rtm, mod, exports)
|
||||
|
||||
return rt.TableValue(mod), nil
|
||||
}
|
||||
|
||||
// new() -> @Snail
|
||||
// Creates a new Snail instance.
|
||||
func snailnew(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
s := New(t.Runtime)
|
||||
return c.PushingNext1(t.Runtime, rt.UserDataValue(snailUserData(s))), nil
|
||||
}
|
||||
|
||||
// #member
|
||||
// run(command, streams)
|
||||
// Runs a shell command. Works the same as `hilbish.run`, but only accepts a table of streams.
|
||||
// #param command string
|
||||
// #param streams table
|
||||
func snailrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
if err := c.CheckNArgs(2); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, err := snailArg(c, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd, err := c.StringArg(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
streams := &util.Streams{}
|
||||
thirdArg := c.Arg(2)
|
||||
switch thirdArg.Type() {
|
||||
case rt.TableType:
|
||||
args := thirdArg.AsTable()
|
||||
|
||||
if luastreams, ok := args.Get(rt.StringValue("sinks")).TryTable(); ok {
|
||||
handleStream(luastreams.Get(rt.StringValue("out")), streams, false, false)
|
||||
handleStream(luastreams.Get(rt.StringValue("err")), streams, true, false)
|
||||
handleStream(luastreams.Get(rt.StringValue("input")), streams, false, true)
|
||||
}
|
||||
case rt.NilType: // noop
|
||||
default:
|
||||
return nil, errors.New("expected 3rd arg to be a table")
|
||||
}
|
||||
|
||||
var newline bool
|
||||
var cont bool
|
||||
var luaErr rt.Value = rt.NilValue
|
||||
exitCode := 0
|
||||
bg, _, _, err := s.Run(cmd, streams)
|
||||
if err != nil {
|
||||
if syntax.IsIncomplete(err) {
|
||||
/*
|
||||
if !interactive {
|
||||
return cmdString, 126, false, false, err
|
||||
}
|
||||
*/
|
||||
if strings.Contains(err.Error(), "unclosed here-document") {
|
||||
newline = true
|
||||
}
|
||||
cont = true
|
||||
} else {
|
||||
if code, ok := interp.IsExitStatus(err); ok {
|
||||
exitCode = int(code)
|
||||
} else {
|
||||
if exErr, ok := util.IsExecError(err); ok {
|
||||
exitCode = exErr.Code
|
||||
}
|
||||
luaErr = rt.StringValue(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
runnerRet := rt.NewTable()
|
||||
runnerRet.Set(rt.StringValue("input"), rt.StringValue(cmd))
|
||||
runnerRet.Set(rt.StringValue("exitCode"), rt.IntValue(int64(exitCode)))
|
||||
runnerRet.Set(rt.StringValue("continue"), rt.BoolValue(cont))
|
||||
runnerRet.Set(rt.StringValue("newline"), rt.BoolValue(newline))
|
||||
runnerRet.Set(rt.StringValue("err"), luaErr)
|
||||
|
||||
runnerRet.Set(rt.StringValue("bg"), rt.BoolValue(bg))
|
||||
return c.PushingNext1(t.Runtime, rt.TableValue(runnerRet)), nil
|
||||
}
|
||||
|
||||
// #member
|
||||
// dir(path)
|
||||
// Changes the directory of the snail instance.
|
||||
// The interpreter keeps its set directory even when the Hilbish process changes
|
||||
// directory, so this should be called on the `hilbish.cd` hook.
|
||||
// #param path string Has to be an absolute path.
|
||||
func snaildir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
if err := c.CheckNArgs(2); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, err := snailArg(c, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dir, err := c.StringArg(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
interp.Dir(dir)(s.runner)
|
||||
return c.Next(), nil
|
||||
}
|
||||
|
||||
func handleStream(v rt.Value, strms *util.Streams, errStream, inStream bool) error {
|
||||
if v == rt.NilValue {
|
||||
return nil
|
||||
}
|
||||
|
||||
ud, ok := v.TryUserData()
|
||||
if !ok {
|
||||
return errors.New("expected metatable argument")
|
||||
}
|
||||
|
||||
val := ud.Value()
|
||||
var varstrm io.ReadWriter
|
||||
if f, ok := val.(*iolib.File); ok {
|
||||
varstrm = f.Handle()
|
||||
}
|
||||
|
||||
if f, ok := val.(*util.Sink); ok {
|
||||
varstrm = f.Rw
|
||||
}
|
||||
|
||||
if varstrm == nil {
|
||||
return errors.New("expected either a sink or file")
|
||||
}
|
||||
|
||||
if errStream {
|
||||
strms.Stderr = varstrm
|
||||
} else if inStream {
|
||||
strms.Stdin = varstrm
|
||||
} else {
|
||||
strms.Stdout = varstrm
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func snailArg(c *rt.GoCont, arg int) (*Snail, error) {
|
||||
s, ok := valueToSnail(c.Arg(arg))
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("#%d must be a snail", arg + 1)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func valueToSnail(val rt.Value) (*Snail, bool) {
|
||||
u, ok := val.TryUserData()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
s, ok := u.Value().(*Snail)
|
||||
return s, ok
|
||||
}
|
||||
|
||||
func snailUserData(s *Snail) *rt.UserData {
|
||||
snailMeta := s.runtime.Registry(snailMetaKey)
|
||||
return rt.NewUserData(s, snailMeta.AsTable())
|
||||
}
|
@ -1,302 +0,0 @@
|
||||
package snail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hilbish/util"
|
||||
|
||||
rt "github.com/arnodel/golua/runtime"
|
||||
"mvdan.cc/sh/v3/shell"
|
||||
//"github.com/yuin/gopher-lua/parse"
|
||||
"mvdan.cc/sh/v3/interp"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
"mvdan.cc/sh/v3/expand"
|
||||
)
|
||||
|
||||
// #type
|
||||
// A Snail is a shell script interpreter instance.
|
||||
type Snail struct{
|
||||
runner *interp.Runner
|
||||
runtime *rt.Runtime
|
||||
}
|
||||
|
||||
func New(rtm *rt.Runtime) *Snail {
|
||||
runner, _ := interp.New()
|
||||
|
||||
return &Snail{
|
||||
runner: runner,
|
||||
runtime: rtm,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Snail) Run(cmd string, strms *util.Streams) (bool, io.Writer, io.Writer, error){
|
||||
file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
if strms == nil {
|
||||
strms = &util.Streams{}
|
||||
}
|
||||
|
||||
if strms.Stdout == nil {
|
||||
strms.Stdout = os.Stdout
|
||||
}
|
||||
|
||||
if strms.Stderr == nil {
|
||||
strms.Stderr = os.Stderr
|
||||
}
|
||||
|
||||
if strms.Stdin == nil {
|
||||
strms.Stdin = os.Stdin
|
||||
}
|
||||
|
||||
interp.StdIO(strms.Stdin, strms.Stdout, strms.Stderr)(s.runner)
|
||||
interp.Env(nil)(s.runner)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
//printer := syntax.NewPrinter()
|
||||
|
||||
var bg bool
|
||||
for _, stmt := range file.Stmts {
|
||||
bg = false
|
||||
if stmt.Background {
|
||||
bg = true
|
||||
//printer.Print(buf, stmt.Cmd)
|
||||
|
||||
//stmtStr := buf.String()
|
||||
buf.Reset()
|
||||
//jobs.add(stmtStr, []string{}, "")
|
||||
}
|
||||
|
||||
interp.ExecHandler(func(ctx context.Context, args []string) error {
|
||||
_, argstring := splitInput(strings.Join(args, " "))
|
||||
// i dont really like this but it works
|
||||
aliases := make(map[string]string)
|
||||
aliasesLua, _ := util.DoString(s.runtime, "return hilbish.aliases.list()")
|
||||
util.ForEach(aliasesLua.AsTable(), func(k, v rt.Value) {
|
||||
aliases[k.AsString()] = v.AsString()
|
||||
})
|
||||
if aliases[args[0]] != "" {
|
||||
for i, arg := range args {
|
||||
if strings.Contains(arg, " ") {
|
||||
args[i] = fmt.Sprintf("\"%s\"", arg)
|
||||
}
|
||||
}
|
||||
_, argstring = splitInput(strings.Join(args, " "))
|
||||
|
||||
// If alias was found, use command alias
|
||||
argstring = util.MustDoString(s.runtime, fmt.Sprintf(`return hilbish.aliases.resolve("%s")`, argstring)).AsString()
|
||||
|
||||
var err error
|
||||
args, err = shell.Fields(argstring, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If command is defined in Lua then run it
|
||||
luacmdArgs := rt.NewTable()
|
||||
for i, str := range args[1:] {
|
||||
luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str))
|
||||
}
|
||||
|
||||
hc := interp.HandlerCtx(ctx)
|
||||
|
||||
cmds := make(map[string]*rt.Closure)
|
||||
luaCmds := util.MustDoString(s.runtime, "local commander = require 'commander'; return commander.registry()").AsTable()
|
||||
util.ForEach(luaCmds, func(k, v rt.Value) {
|
||||
cmds[k.AsString()] = v.AsTable().Get(rt.StringValue("exec")).AsClosure()
|
||||
})
|
||||
if cmd := cmds[args[0]]; cmd != nil {
|
||||
stdin := util.NewSinkInput(s.runtime, hc.Stdin)
|
||||
stdout := util.NewSinkOutput(s.runtime, hc.Stdout)
|
||||
stderr := util.NewSinkOutput(s.runtime, hc.Stderr)
|
||||
|
||||
sinks := rt.NewTable()
|
||||
sinks.Set(rt.StringValue("in"), rt.UserDataValue(stdin.UserData))
|
||||
sinks.Set(rt.StringValue("input"), rt.UserDataValue(stdin.UserData))
|
||||
sinks.Set(rt.StringValue("out"), rt.UserDataValue(stdout.UserData))
|
||||
sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.UserData))
|
||||
|
||||
t := rt.NewThread(s.runtime)
|
||||
sig := make(chan os.Signal)
|
||||
exit := make(chan bool)
|
||||
|
||||
luaexitcode := rt.IntValue(63)
|
||||
var err error
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
exit <- true
|
||||
}
|
||||
}()
|
||||
|
||||
signal.Notify(sig, os.Interrupt)
|
||||
select {
|
||||
case <-sig:
|
||||
t.KillContext()
|
||||
return
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
go func() {
|
||||
luaexitcode, err = rt.Call1(t, rt.FunctionValue(cmd), rt.TableValue(luacmdArgs), rt.TableValue(sinks))
|
||||
exit <- true
|
||||
}()
|
||||
|
||||
<-exit
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error())
|
||||
return interp.NewExitStatus(1)
|
||||
}
|
||||
|
||||
var exitcode uint8
|
||||
|
||||
if code, ok := luaexitcode.TryInt(); ok {
|
||||
exitcode = uint8(code)
|
||||
} else if luaexitcode != rt.NilValue {
|
||||
// deregister commander
|
||||
delete(cmds, args[0])
|
||||
fmt.Fprintf(os.Stderr, "Commander did not return number for exit code. %s, you're fired.\n", args[0])
|
||||
}
|
||||
|
||||
return interp.NewExitStatus(exitcode)
|
||||
}
|
||||
|
||||
path, err := util.LookPath(args[0])
|
||||
if err == util.ErrNotExec {
|
||||
return util.ExecError{
|
||||
Typ: "not-executable",
|
||||
Cmd: args[0],
|
||||
Code: 126,
|
||||
Colon: true,
|
||||
Err: util.ErrNotExec,
|
||||
}
|
||||
} else if err != nil {
|
||||
return util.ExecError{
|
||||
Typ: "not-found",
|
||||
Cmd: args[0],
|
||||
Code: 127,
|
||||
Err: util.ErrNotFound,
|
||||
}
|
||||
}
|
||||
|
||||
killTimeout := 2 * time.Second
|
||||
// from here is basically copy-paste of the default exec handler from
|
||||
// sh/interp but with our job handling
|
||||
|
||||
env := hc.Env
|
||||
envList := os.Environ()
|
||||
env.Each(func(name string, vr expand.Variable) bool {
|
||||
if vr.Exported && vr.Kind == expand.String {
|
||||
envList = append(envList, name+"="+vr.String())
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
cmd := exec.Cmd{
|
||||
Path: path,
|
||||
Args: args,
|
||||
Env: envList,
|
||||
Dir: hc.Dir,
|
||||
Stdin: hc.Stdin,
|
||||
Stdout: hc.Stdout,
|
||||
Stderr: hc.Stderr,
|
||||
}
|
||||
|
||||
//var j *job
|
||||
if bg {
|
||||
/*
|
||||
j = jobs.getLatest()
|
||||
j.setHandle(&cmd)
|
||||
err = j.start()
|
||||
*/
|
||||
} else {
|
||||
err = cmd.Start()
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if done := ctx.Done(); done != nil {
|
||||
go func() {
|
||||
<-done
|
||||
|
||||
if killTimeout <= 0 || runtime.GOOS == "windows" {
|
||||
cmd.Process.Signal(os.Kill)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: don't temporarily leak this goroutine
|
||||
// if the program stops itself with the
|
||||
// interrupt.
|
||||
go func() {
|
||||
time.Sleep(killTimeout)
|
||||
cmd.Process.Signal(os.Kill)
|
||||
}()
|
||||
cmd.Process.Signal(os.Interrupt)
|
||||
}()
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
}
|
||||
|
||||
exit := util.HandleExecErr(err)
|
||||
|
||||
if bg {
|
||||
//j.exitCode = int(exit)
|
||||
//j.finish()
|
||||
}
|
||||
return interp.NewExitStatus(exit)
|
||||
})(s.runner)
|
||||
err = s.runner.Run(context.TODO(), stmt)
|
||||
if err != nil {
|
||||
return bg, strms.Stdout, strms.Stderr, err
|
||||
}
|
||||
}
|
||||
|
||||
return bg, strms.Stdout, strms.Stderr, nil
|
||||
}
|
||||
|
||||
func splitInput(input string) ([]string, string) {
|
||||
// end my suffering
|
||||
// TODO: refactor this garbage
|
||||
quoted := false
|
||||
cmdArgs := []string{}
|
||||
sb := &strings.Builder{}
|
||||
cmdstr := &strings.Builder{}
|
||||
|
||||
for _, r := range input {
|
||||
if r == '"' {
|
||||
// start quoted input
|
||||
// this determines if other runes are replaced
|
||||
quoted = !quoted
|
||||
// dont add back quotes
|
||||
//sb.WriteRune(r)
|
||||
} else if !quoted && r == '~' {
|
||||
// if not in quotes and ~ is found then make it $HOME
|
||||
sb.WriteString(os.Getenv("HOME"))
|
||||
} else if !quoted && r == ' ' {
|
||||
// if not quoted and there's a space then add to cmdargs
|
||||
cmdArgs = append(cmdArgs, sb.String())
|
||||
sb.Reset()
|
||||
} else {
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
cmdstr.WriteRune(r)
|
||||
}
|
||||
if sb.Len() > 0 {
|
||||
cmdArgs = append(cmdArgs, sb.String())
|
||||
}
|
||||
|
||||
return cmdArgs, cmdstr.String()
|
||||
}
|
6
job.go
6
job.go
@ -56,8 +56,8 @@ func (j *job) start() error {
|
||||
}
|
||||
j.setHandle(&cmd)
|
||||
}
|
||||
// bgProcAttr is defined in job_<os>.go, it holds a procattr struct
|
||||
// in a simple explanation, it makes signals from hilbish (like sigint)
|
||||
// bgProcAttr is defined in execfile_<os>.go, it holds a procattr struct
|
||||
// in a simple explanation, it makes signals from hilbish (sigint)
|
||||
// not go to it (child process)
|
||||
j.handle.SysProcAttr = bgProcAttr
|
||||
// reset output buffers
|
||||
@ -136,7 +136,7 @@ func luaStartJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
|
||||
if !j.running {
|
||||
err := j.start()
|
||||
exit := util.HandleExecErr(err)
|
||||
exit := handleExecErr(err)
|
||||
j.exitCode = int(exit)
|
||||
j.finish()
|
||||
}
|
||||
|
@ -10,10 +10,6 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
|
||||
func (j *job) foreground() error {
|
||||
if jobs.foreground {
|
||||
return errors.New("(another) job already foregrounded")
|
||||
|
@ -4,13 +4,8 @@ package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
||||
}
|
||||
|
||||
func (j *job) foreground() error {
|
||||
return errors.New("not supported on windows")
|
||||
}
|
||||
|
10
lua.go
10
lua.go
@ -3,13 +3,11 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"hilbish/util"
|
||||
"hilbish/golibs/bait"
|
||||
"hilbish/golibs/commander"
|
||||
"hilbish/golibs/fs"
|
||||
"hilbish/golibs/snail"
|
||||
"hilbish/golibs/terminal"
|
||||
|
||||
rt "github.com/arnodel/golua/runtime"
|
||||
@ -25,14 +23,16 @@ func luaInit() {
|
||||
MessageHandler: debuglib.Traceback,
|
||||
})
|
||||
lib.LoadAll(l)
|
||||
setupSinkType(l)
|
||||
|
||||
lib.LoadLibs(l, hilbishLoader)
|
||||
// yes this is stupid, i know
|
||||
util.DoString(l, "hilbish = require 'hilbish'")
|
||||
|
||||
lib.LoadLibs(l, fs.Loader)
|
||||
// Add fs and terminal module module to Lua
|
||||
f := fs.New(runner)
|
||||
lib.LoadLibs(l, f.Loader)
|
||||
lib.LoadLibs(l, terminal.Loader)
|
||||
lib.LoadLibs(l, snail.Loader)
|
||||
|
||||
cmds = commander.New(l)
|
||||
lib.LoadLibs(l, cmds.Loader)
|
||||
@ -64,7 +64,7 @@ func luaInit() {
|
||||
|
||||
err1 := util.DoFile(l, "nature/init.lua")
|
||||
if err1 != nil {
|
||||
err2 := util.DoFile(l, filepath.Join(dataDir, "nature", "init.lua"))
|
||||
err2 := util.DoFile(l, preloadPath)
|
||||
if err2 != nil {
|
||||
fmt.Fprintln(os.Stderr, "Missing nature module, some functionality and builtins will be missing.")
|
||||
fmt.Fprintln(os.Stderr, "local error:", err1)
|
||||
|
55
main.go
55
main.go
@ -21,6 +21,7 @@ import (
|
||||
"github.com/pborman/getopt"
|
||||
"github.com/maxlandon/readline"
|
||||
"golang.org/x/term"
|
||||
"mvdan.cc/sh/v3/interp"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -37,27 +38,16 @@ var (
|
||||
cmds *commander.Commander
|
||||
defaultConfPath string
|
||||
defaultHistPath string
|
||||
runner *interp.Runner
|
||||
)
|
||||
|
||||
func main() {
|
||||
if runtime.GOOS == "linux" {
|
||||
// dataDir should only be empty on linux to allow XDG_DATA_DIRS searching.
|
||||
// but since it might be set on some distros (nixos) we should still check if its really is empty.
|
||||
if dataDir == "" {
|
||||
searchableDirs := getenv("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/")
|
||||
dataDir = "."
|
||||
for _, path := range strings.Split(searchableDirs, ":") {
|
||||
_, err := os.Stat(filepath.Join(path, "hilbish", ".hilbishrc.lua"))
|
||||
if err == nil {
|
||||
dataDir = filepath.Join(path, "hilbish")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runner, _ = interp.New()
|
||||
curuser, _ = user.Current()
|
||||
homedir := curuser.HomeDir
|
||||
confDir, _ = os.UserConfigDir()
|
||||
preloadPath = strings.Replace(preloadPath, "~", homedir, 1)
|
||||
sampleConfPath = strings.Replace(sampleConfPath, "~", homedir, 1)
|
||||
|
||||
// i honestly dont know what directories to use for this
|
||||
switch runtime.GOOS {
|
||||
@ -151,11 +141,10 @@ func main() {
|
||||
confpath := ".hilbishrc.lua"
|
||||
if err != nil {
|
||||
// If it wasnt found, go to the real sample conf
|
||||
sampleConfigPath := filepath.Join(dataDir, ".hilbishrc.lua")
|
||||
_, err = os.ReadFile(sampleConfigPath)
|
||||
confpath = sampleConfigPath
|
||||
_, err = os.ReadFile(sampleConfPath)
|
||||
confpath = sampleConfPath
|
||||
if err != nil {
|
||||
fmt.Println("could not find .hilbishrc.lua or", sampleConfigPath)
|
||||
fmt.Println("could not find .hilbishrc.lua or", sampleConfPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -234,9 +223,8 @@ input:
|
||||
}
|
||||
|
||||
if strings.HasSuffix(input, "\\") {
|
||||
print("\n")
|
||||
for {
|
||||
input, err = continuePrompt(strings.TrimSuffix(input, "\\") + "\n", false)
|
||||
input, err = continuePrompt(input)
|
||||
if err != nil {
|
||||
running = true
|
||||
lr.SetPrompt(fmtPrompt(prompt))
|
||||
@ -260,24 +248,16 @@ input:
|
||||
exit(0)
|
||||
}
|
||||
|
||||
func continuePrompt(prev string, newline bool) (string, error) {
|
||||
func continuePrompt(prev string) (string, error) {
|
||||
hooks.Emit("multiline", nil)
|
||||
lr.SetPrompt(multilinePrompt)
|
||||
|
||||
cont, err := lr.Read()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cont = strings.TrimSpace(cont)
|
||||
|
||||
if newline {
|
||||
cont = "\n" + cont
|
||||
}
|
||||
|
||||
if strings.HasSuffix(cont, "\\") {
|
||||
cont = strings.TrimSuffix(cont, "\\") + "\n"
|
||||
}
|
||||
|
||||
return prev + cont, nil
|
||||
return prev + strings.TrimSuffix(cont, "\n"), nil
|
||||
}
|
||||
|
||||
// This semi cursed function formats our prompt (obviously)
|
||||
@ -324,6 +304,15 @@ func removeDupes(slice []string) []string {
|
||||
return newSlice
|
||||
}
|
||||
|
||||
func contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if strings.ToLower(a) == strings.ToLower(e) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func exit(code int) {
|
||||
jobs.stopAll()
|
||||
|
||||
|
@ -1,61 +0,0 @@
|
||||
-- @module hilbish.abbr
|
||||
-- command line abbreviations
|
||||
-- The abbr module manages Hilbish abbreviations. These are words that can be replaced
|
||||
-- with longer command line strings when entered.
|
||||
-- As an example, `git push` can be abbreviated to `gp`. When the user types
|
||||
-- `gp` into the command line, after hitting space or enter, it will expand to `git push`.
|
||||
-- Abbreviations can be used as an alternative to aliases. They are saved entirely in the history
|
||||
-- Instead of the aliased form of the same command.
|
||||
local bait = require 'bait'
|
||||
local hilbish = require 'hilbish'
|
||||
hilbish.abbr = {
|
||||
all = {}
|
||||
}
|
||||
|
||||
--- Adds an abbreviation. The `abbr` is the abbreviation itself,
|
||||
--- while `expanded` is what the abbreviation should expand to.
|
||||
--- It can be either a function or a string. If it is a function, it will expand to what
|
||||
--- the function returns.
|
||||
--- `opts` is a table that accepts 1 key: `anywhere`.
|
||||
--- `opts.anywhere` defines whether the abbr expands anywhere in the command line or not,
|
||||
--- whereas the default behavior is only at the beginning of the line
|
||||
-- @param abbr string
|
||||
-- @param expanded|function string
|
||||
-- @param opts table
|
||||
function hilbish.abbr.add(abbr, expanded, opts)
|
||||
print(abbr, expanded, opts)
|
||||
opts = opts or {}
|
||||
opts.abbr = abbr
|
||||
opts.expand = expanded
|
||||
hilbish.abbr.all[abbr] = opts
|
||||
end
|
||||
|
||||
--- Removes the named `abbr`.
|
||||
-- @param abbr string
|
||||
function hilbish.abbr.remove(abbr)
|
||||
hilbish.abbr.all[abbr] = nil
|
||||
end
|
||||
|
||||
bait.catch('hilbish.rawInput', function(c)
|
||||
-- 0x0d == enter
|
||||
if c == ' ' or c == string.char(0x0d) then
|
||||
-- check if the last "word" was a valid abbreviation
|
||||
local line = hilbish.editor.getLine()
|
||||
local lineSplits = string.split(line, ' ')
|
||||
local thisAbbr = hilbish.abbr.all[lineSplits[#lineSplits]]
|
||||
|
||||
if thisAbbr and (#lineSplits == 1 or thisAbbr.anywhere == true) then
|
||||
hilbish.editor.deleteByAmount(-lineSplits[#lineSplits]:len())
|
||||
if type(thisAbbr.expand) == 'string' then
|
||||
hilbish.editor.insert(thisAbbr.expand)
|
||||
elseif type(thisAbbr.expand) == 'function' then
|
||||
local expandRet = thisAbbr.expand()
|
||||
if type(expandRet) ~= 'string' then
|
||||
print(string.format('abbr %s has an expand function that did not return a string. instead it returned: %s', thisAbbr.abbr, expandRet))
|
||||
return
|
||||
end
|
||||
hilbish.editor.insert(expandRet)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
@ -3,9 +3,8 @@ local commander = require 'commander'
|
||||
local fs = require 'fs'
|
||||
local dirs = require 'nature.dirs'
|
||||
|
||||
dirs.old = hilbish.cwd()
|
||||
commander.register('cd', function (args, sinks)
|
||||
local oldPath = hilbish.cwd()
|
||||
|
||||
if #args > 1 then
|
||||
sinks.out:writeln("cd: too many arguments")
|
||||
return 1
|
||||
@ -17,13 +16,13 @@ commander.register('cd', function (args, sinks)
|
||||
sinks.out:writeln(path)
|
||||
end
|
||||
|
||||
local absPath = fs.abs(path)
|
||||
dirs.setOld(hilbish.cwd())
|
||||
dirs.push(path)
|
||||
|
||||
local ok, err = pcall(function() fs.cd(path) end)
|
||||
if not ok then
|
||||
sinks.out:writeln(err)
|
||||
return 1
|
||||
end
|
||||
|
||||
bait.throw('cd', path, oldPath)
|
||||
bait.throw('hilbish.cd', absPath, oldPath)
|
||||
bait.throw('cd', path)
|
||||
end)
|
||||
|
@ -1,13 +1,10 @@
|
||||
-- @module dirs
|
||||
-- internal directory management
|
||||
-- The dirs module defines a small set of functions to store and manage
|
||||
-- directories.
|
||||
local bait = require 'bait'
|
||||
local fs = require 'fs'
|
||||
|
||||
local dirs = {}
|
||||
|
||||
--- Last (current working) directory. Separate from recentDirs mainly for easier use.
|
||||
--- Last (current working) directory. Separate from recentDirs mainly for
|
||||
--- easier use.
|
||||
dirs.old = ''
|
||||
--- Table of recent directories. For use, look at public functions.
|
||||
dirs.recentDirs = {}
|
||||
@ -38,21 +35,19 @@ function dirRecents(num, remove)
|
||||
end
|
||||
|
||||
--- Look at `num` amount of recent directories, starting from the latest.
|
||||
--- This returns a table of recent directories, up to the `num` amount.
|
||||
-- @param num? number
|
||||
function dirs.peak(num)
|
||||
return dirRecents(num)
|
||||
end
|
||||
|
||||
--- Add `dir` to the recent directories list.
|
||||
--- @param dir string
|
||||
function dirs.push(dir)
|
||||
--- Add `d` to the recent directories list.
|
||||
function dirs.push(d)
|
||||
dirs.recentDirs[dirs.recentSize + 1] = nil
|
||||
if dirs.recentDirs[#dirs.recentDirs - 1] ~= dir then
|
||||
local ok, dir = pcall(fs.abs, dir)
|
||||
assert(ok, 'could not turn "' .. dir .. '"into an absolute path')
|
||||
if dirs.recentDirs[#dirs.recentDirs - 1] ~= d then
|
||||
ok, d = pcall(fs.abs, d)
|
||||
assert(ok, 'could not turn "' .. d .. '"into an absolute path')
|
||||
|
||||
table.insert(dirs.recentDirs, 1, dir)
|
||||
table.insert(dirs.recentDirs, 1, d)
|
||||
end
|
||||
end
|
||||
|
||||
@ -78,9 +73,4 @@ function dirs.setOld(d)
|
||||
dirs.old = d
|
||||
end
|
||||
|
||||
bait.catch('hilbish.cd', function(path, oldPath)
|
||||
dirs.setOld(oldPath)
|
||||
dirs.push(path)
|
||||
end)
|
||||
|
||||
return dirs
|
||||
|
@ -1,25 +1,13 @@
|
||||
-- @module doc
|
||||
-- command-line doc rendering
|
||||
-- The doc module contains a small set of functions
|
||||
-- used by the Greenhouse pager to render parts of the documentation pages.
|
||||
-- This is only documented for the sake of it. It's only intended use
|
||||
-- is by the Greenhouse pager.
|
||||
local lunacolors = require 'lunacolors'
|
||||
|
||||
local doc = {}
|
||||
local M = {}
|
||||
|
||||
--- Performs basic Lua code highlighting.
|
||||
--- @param text string Code/text to do highlighting on.
|
||||
function doc.highlight(text)
|
||||
function M.highlight(text)
|
||||
return text:gsub('\'.-\'', lunacolors.yellow)
|
||||
--:gsub('%-%- .-', lunacolors.black)
|
||||
end
|
||||
|
||||
--- Assembles and renders a code block. This returns
|
||||
--- the supplied text based on the number of command line columns,
|
||||
--- and styles it to resemble a code block.
|
||||
--- @param text string
|
||||
function doc.renderCodeBlock(text)
|
||||
function M.renderCodeBlock(text)
|
||||
local longest = 0
|
||||
local lines = string.split(text:gsub('\t', ' '), '\n')
|
||||
|
||||
@ -29,18 +17,14 @@ function doc.renderCodeBlock(text)
|
||||
end
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
lines[i] = lunacolors.format('{greyBg}' .. ' ' .. doc.highlight(line:sub(0, longest))
|
||||
lines[i] = lunacolors.format('{greyBg}' .. ' ' .. M.highlight(line:sub(0, longest))
|
||||
.. string.rep(' ', longest - line:len()) .. ' ')
|
||||
end
|
||||
|
||||
return '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n'
|
||||
end
|
||||
|
||||
--- Renders an info block. An info block is a block of text with
|
||||
--- an icon and styled text block.
|
||||
--- @param type string Type of info block. The only one specially styled is the `warning`.
|
||||
--- @param text string
|
||||
function doc.renderInfoBlock(type, text)
|
||||
function M.renderInfoBlock(type, text)
|
||||
local longest = 0
|
||||
local lines = string.split(text:gsub('\t', ' '), '\n')
|
||||
|
||||
@ -50,7 +34,7 @@ function doc.renderInfoBlock(type, text)
|
||||
end
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
lines[i] = ' ' .. doc.highlight(line:sub(0, longest))
|
||||
lines[i] = ' ' .. M.highlight(line:sub(0, longest))
|
||||
.. string.rep(' ', longest - line:len()) .. ' '
|
||||
end
|
||||
|
||||
@ -60,4 +44,4 @@ function doc.renderInfoBlock(type, text)
|
||||
end
|
||||
return '\n' .. heading .. '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n'
|
||||
end
|
||||
return doc
|
||||
return M
|
||||
|
@ -1,5 +1,4 @@
|
||||
-- @module greenhouse
|
||||
-- Greenhouse is a simple text scrolling handler (pager) for terminal programs.
|
||||
-- Greenhouse is a simple text scrolling handler for terminal programs.
|
||||
-- The idea is that it can be set a region to do its scrolling and paging
|
||||
-- job and then the user can draw whatever outside it.
|
||||
-- This reduces code duplication for the message viewer
|
||||
|
@ -1,4 +1,3 @@
|
||||
-- @module greenhouse.page
|
||||
local Object = require 'nature.object'
|
||||
|
||||
local Page = Object:extend()
|
||||
@ -11,7 +10,6 @@ function Page:new(title, text)
|
||||
self.children = {}
|
||||
end
|
||||
|
||||
|
||||
function Page:setText(text)
|
||||
self.lines = string.split(text, '\n')
|
||||
end
|
||||
|
@ -1,78 +0,0 @@
|
||||
-- @module hilbish
|
||||
local bait = require 'bait'
|
||||
local snail = require 'snail'
|
||||
|
||||
hilbish.snail = snail.new()
|
||||
bait.catch('hilbish.cd', function(path)
|
||||
hilbish.snail:dir(path)
|
||||
end)
|
||||
--- Runs `cmd` in Hilbish's shell script interpreter.
|
||||
--- The `streams` parameter specifies the output and input streams the command should use.
|
||||
--- For example, to write command output to a sink.
|
||||
--- As a table, the caller can directly specify the standard output, error, and input
|
||||
--- streams of the command with the table keys `out`, `err`, and `input` respectively.
|
||||
--- As a boolean, it specifies whether the command should use standard output or return its output streams.
|
||||
--- #example
|
||||
--- -- This code is the same as `ls -l | wc -l`
|
||||
--- local fs = require 'fs'
|
||||
--- local pr, pw = fs.pipe()
|
||||
--- hilbish.run('ls -l', {
|
||||
--- stdout = pw,
|
||||
--- stderr = pw,
|
||||
--- })
|
||||
--- pw:close()
|
||||
--- hilbish.run('wc -l', {
|
||||
--- stdin = pr
|
||||
--- })
|
||||
--- #example
|
||||
-- @param cmd string
|
||||
-- @param streams table|boolean
|
||||
-- @returns number, string, string
|
||||
function hilbish.run(cmd, streams)
|
||||
local sinks = {}
|
||||
|
||||
if type(streams) == 'boolean' then
|
||||
if not streams then
|
||||
sinks = {
|
||||
out = hilbish.sink.new(),
|
||||
err = hilbish.sink.new(),
|
||||
input = io.stdin
|
||||
}
|
||||
end
|
||||
elseif type(streams) == 'table' then
|
||||
sinks = streams
|
||||
end
|
||||
|
||||
local out = hilbish.snail:run(cmd, {sinks = sinks})
|
||||
local returns = {out.exitCode}
|
||||
|
||||
if type(streams) == 'boolean' and not streams then
|
||||
table.insert(returns, sinks.out:readAll())
|
||||
table.insert(returns, sinks.err:readAll())
|
||||
end
|
||||
|
||||
return table.unpack(returns)
|
||||
end
|
||||
|
||||
--- Sets the execution/runner mode for interactive Hilbish.
|
||||
--- **NOTE: This function is deprecated and will be removed in 3.0**
|
||||
--- Use `hilbish.runner.setCurrent` instead.
|
||||
--- This determines whether Hilbish wll try to run input as Lua
|
||||
--- and/or sh or only do one of either.
|
||||
--- Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua),
|
||||
--- sh, and lua. It also accepts a function, to which if it is passed one
|
||||
--- will call it to execute user input instead.
|
||||
--- Read [about runner mode](../features/runner-mode) for more information.
|
||||
-- @param mode string|function
|
||||
function hilbish.runnerMode(mode)
|
||||
if type(mode) == 'string' then
|
||||
hilbish.runner.setCurrent(mode)
|
||||
elseif type(mode) == 'function' then
|
||||
hilbish.runner.set('_', {
|
||||
run = mode
|
||||
})
|
||||
hilbish.runner.setCurrent '_'
|
||||
else
|
||||
error('expected runner mode type to be either string or function, got', type(mode))
|
||||
end
|
||||
end
|
@ -1,14 +1,3 @@
|
||||
-- @module hilbish.messages
|
||||
-- simplistic message passing
|
||||
-- The messages interface defines a way for Hilbish-integrated commands,
|
||||
-- user config and other tasks to send notifications to alert the user.z
|
||||
-- The `hilbish.message` type is a table with the following keys:
|
||||
-- `title` (string): A title for the message notification.
|
||||
-- `text` (string): The contents of the message.
|
||||
-- `channel` (string): States the origin of the message, `hilbish.*` is reserved for Hilbish tasks.
|
||||
-- `summary` (string): A short summary of the `text`.
|
||||
-- `icon` (string): Unicode (preferably standard emoji) icon for the message notification
|
||||
-- `read` (boolean): Whether the full message has been read or not.
|
||||
local bait = require 'bait'
|
||||
local commander = require 'commander'
|
||||
local lunacolors = require 'lunacolors'
|
||||
@ -55,8 +44,6 @@ function hilbish.messages.send(message)
|
||||
bait.throw('hilbish.notification', message)
|
||||
end
|
||||
|
||||
--- Marks a message at `idx` as read.
|
||||
--- @param idx number
|
||||
function hilbish.messages.read(idx)
|
||||
local msg = M._messages[idx]
|
||||
if msg then
|
||||
@ -65,20 +52,16 @@ function hilbish.messages.read(idx)
|
||||
end
|
||||
end
|
||||
|
||||
--- Marks all messages as read.
|
||||
function hilbish.messages.readAll()
|
||||
function hilbish.messages.readAll(idx)
|
||||
for _, msg in ipairs(hilbish.messages.all()) do
|
||||
hilbish.messages.read(msg.index)
|
||||
end
|
||||
end
|
||||
|
||||
--- Returns the amount of unread messages.
|
||||
function hilbish.messages.unreadCount()
|
||||
return unread
|
||||
end
|
||||
|
||||
--- Deletes the message at `idx`.
|
||||
--- @param idx number
|
||||
function hilbish.messages.delete(idx)
|
||||
local msg = M._messages[idx]
|
||||
if not msg then
|
||||
@ -88,14 +71,12 @@ function hilbish.messages.delete(idx)
|
||||
M._messages[idx] = nil
|
||||
end
|
||||
|
||||
--- Deletes all messages.
|
||||
function hilbish.messages.clear()
|
||||
for _, msg in ipairs(hilbish.messages.all()) do
|
||||
hilbish.messages.delete(msg.index)
|
||||
end
|
||||
end
|
||||
|
||||
--- Returns all messages.
|
||||
function hilbish.messages.all()
|
||||
return M._messages
|
||||
end
|
||||
|
@ -18,15 +18,12 @@ table.insert(package.searchers, function(module)
|
||||
return function() return hilbish.module.load(path) end, path
|
||||
end)
|
||||
|
||||
require 'nature.hilbish'
|
||||
|
||||
require 'nature.commands'
|
||||
require 'nature.completions'
|
||||
require 'nature.opts'
|
||||
require 'nature.vim'
|
||||
require 'nature.runner'
|
||||
require 'nature.hummingbird'
|
||||
require 'nature.abbr'
|
||||
|
||||
local shlvl = tonumber(os.getenv 'SHLVL')
|
||||
if shlvl ~= nil then
|
||||
|
@ -2,7 +2,9 @@ local bait = require 'bait'
|
||||
local lunacolors = require 'lunacolors'
|
||||
|
||||
hilbish.motd = [[
|
||||
{magenta}Hilbish{reset} blooms in the {blue}midnight.{reset}
|
||||
Wait ... {magenta}2.3{reset} is basically the same as {red}2.2?{reset}
|
||||
Erm.. {blue}Ctrl-C works for Commanders,{reset} {cyan}and the sh runner has some fixes.{reset}
|
||||
Just trust me bro, this is an imporant bug fix release. {red}- 🌺 sammyette{reset}
|
||||
]]
|
||||
|
||||
bait.catch('hilbish.init', function()
|
||||
|
@ -1,5 +1,4 @@
|
||||
-- @module hilbish.runner
|
||||
local snail = require 'snail'
|
||||
--- hilbish.runner
|
||||
local currentRunner = 'hybrid'
|
||||
local runners = {}
|
||||
|
||||
@ -7,7 +6,7 @@ local runners = {}
|
||||
hilbish = hilbish
|
||||
|
||||
--- Get a runner by name.
|
||||
--- @param name string Name of the runner to retrieve.
|
||||
--- @param name string
|
||||
--- @return table
|
||||
function hilbish.runner.get(name)
|
||||
local r = runners[name]
|
||||
@ -19,10 +18,10 @@ function hilbish.runner.get(name)
|
||||
return r
|
||||
end
|
||||
|
||||
--- Adds a runner to the table of available runners.
|
||||
--- If runner is a table, it must have the run function in it.
|
||||
--- @param name string Name of the runner
|
||||
--- @param runner function|table
|
||||
--- Adds a runner to the table of available runners. If runner is a table,
|
||||
--- it must have the run function in it.
|
||||
--- @param name string
|
||||
--- @param runner function | table
|
||||
function hilbish.runner.add(name, runner)
|
||||
if type(name) ~= 'string' then
|
||||
error 'expected runner name to be a table'
|
||||
@ -43,9 +42,7 @@ function hilbish.runner.add(name, runner)
|
||||
hilbish.runner.set(name, runner)
|
||||
end
|
||||
|
||||
--- *Sets* a runner by name. The difference between this function and
|
||||
--- add, is set will *not* check if the named runner exists.
|
||||
--- The runner table must have the run function in it.
|
||||
--- Sets a runner by name. The runner table must have the run function in it.
|
||||
--- @param name string
|
||||
--- @param runner table
|
||||
function hilbish.runner.set(name, runner)
|
||||
@ -56,11 +53,11 @@ function hilbish.runner.set(name, runner)
|
||||
runners[name] = runner
|
||||
end
|
||||
|
||||
--- Executes `cmd` with a runner.
|
||||
--- If `runnerName` is not specified, it uses the default Hilbish runner.
|
||||
--- Executes cmd with a runner. If runnerName isn't passed, it uses
|
||||
--- the user's current runner.
|
||||
--- @param cmd string
|
||||
--- @param runnerName string?
|
||||
--- @return table
|
||||
--- @return string, number, string
|
||||
function hilbish.runner.exec(cmd, runnerName)
|
||||
if not runnerName then runnerName = currentRunner end
|
||||
|
||||
@ -69,11 +66,13 @@ function hilbish.runner.exec(cmd, runnerName)
|
||||
return r.run(cmd)
|
||||
end
|
||||
|
||||
--- Sets Hilbish's runner mode by name.
|
||||
--- Sets the current interactive/command line runner mode.
|
||||
--- @param name string
|
||||
function hilbish.runner.setCurrent(name)
|
||||
hilbish.runner.get(name) -- throws if it doesnt exist.
|
||||
local r = hilbish.runner.get(name)
|
||||
currentRunner = name
|
||||
|
||||
hilbish.runner.setMode(r.run)
|
||||
end
|
||||
|
||||
--- Returns the current runner by name.
|
||||
@ -82,81 +81,6 @@ function hilbish.runner.getCurrent()
|
||||
return currentRunner
|
||||
end
|
||||
|
||||
--- **NOTE: This function is deprecated and will be removed in 3.0**
|
||||
--- Use `hilbish.runner.setCurrent` instead.
|
||||
--- This is the same as the `hilbish.runnerMode` function.
|
||||
--- It takes a callback, which will be used to execute all interactive input.
|
||||
--- Or a string which names the runner mode to use.
|
||||
-- @param mode string|function
|
||||
function hilbish.runner.setMode(mode)
|
||||
hilbish.runnerMode(mode)
|
||||
end
|
||||
|
||||
local function finishExec(exitCode, input, priv)
|
||||
hilbish.exitCode = exitCode
|
||||
bait.throw('command.exit', exitCode, input, priv)
|
||||
end
|
||||
|
||||
local function continuePrompt(prev, newline)
|
||||
local multilinePrompt = hilbish.multiprompt()
|
||||
-- the return of hilbish.read is nil when error or ctrl-d
|
||||
local cont = hilbish.read(multilinePrompt)
|
||||
if not cont then
|
||||
return
|
||||
end
|
||||
|
||||
if newline then
|
||||
cont = '\n' .. cont
|
||||
end
|
||||
|
||||
if cont:match '\\$' then
|
||||
cont = cont:gsub('\\$', '') .. '\n'
|
||||
end
|
||||
|
||||
return prev .. cont
|
||||
end
|
||||
|
||||
--- Runs `input` with the currently set Hilbish runner.
|
||||
--- This method is how Hilbish executes commands.
|
||||
--- `priv` is an optional boolean used to state if the input should be saved to history.
|
||||
-- @param input string
|
||||
-- @param priv bool
|
||||
function hilbish.runner.run(input, priv)
|
||||
local command = hilbish.aliases.resolve(input)
|
||||
bait.throw('command.preexec', input, command)
|
||||
|
||||
::rerun::
|
||||
local runner = hilbish.runner.get(currentRunner)
|
||||
local ok, out = pcall(runner.run, input)
|
||||
if not ok then
|
||||
io.stderr:write(out .. '\n')
|
||||
finishExec(124, out.input, priv)
|
||||
return
|
||||
end
|
||||
|
||||
if out.continue then
|
||||
local contInput = continuePrompt(input, out.newline)
|
||||
if contInput then
|
||||
input = contInput
|
||||
goto rerun
|
||||
end
|
||||
end
|
||||
|
||||
if out.err then
|
||||
local fields = string.split(out.err, ': ')
|
||||
if fields[2] == 'not-found' or fields[2] == 'not-executable' then
|
||||
bait.throw('command.' .. fields[2], fields[1])
|
||||
else
|
||||
io.stderr:write(out.err .. '\n')
|
||||
end
|
||||
end
|
||||
finishExec(out.exitCode, out.input, priv)
|
||||
end
|
||||
|
||||
function hilbish.runner.sh(input)
|
||||
return hilbish.snail:run(input)
|
||||
end
|
||||
|
||||
hilbish.runner.add('hybrid', function(input)
|
||||
local cmdStr = hilbish.aliases.resolve(input)
|
||||
|
||||
@ -183,5 +107,7 @@ hilbish.runner.add('lua', function(input)
|
||||
return hilbish.runner.lua(cmdStr)
|
||||
end)
|
||||
|
||||
hilbish.runner.add('sh', hilbish.runner.sh)
|
||||
hilbish.runner.setCurrent 'hybrid'
|
||||
hilbish.runner.add('sh', function(input)
|
||||
return hilbish.runner.sh(input)
|
||||
end)
|
||||
|
||||
|
23
readline/completers/command-arguments.go
Normal file
23
readline/completers/command-arguments.go
Normal file
@ -0,0 +1,23 @@
|
||||
package completers
|
||||
|
||||
import (
|
||||
"github.com/jessevdk/go-flags"
|
||||
|
||||
"github.com/maxlandon/readline"
|
||||
)
|
||||
|
||||
// CompleteCommandArguments - Completes all values for arguments to a command.
|
||||
// Arguments here are different from command options (--option).
|
||||
// Many categories, from multiple sources in multiple contexts
|
||||
func completeCommandArguments(cmd *flags.Command, arg string, lastWord string) (prefix string, completions []*readline.CompletionGroup) {
|
||||
|
||||
// the prefix is the last word, by default
|
||||
prefix = lastWord
|
||||
|
||||
// SEE completeOptionArguments FOR A WAY TO ADD COMPLETIONS TO SPECIFIC ARGUMENTS ------------------------------
|
||||
|
||||
// found := argumentByName(cmd, arg)
|
||||
// var comp *readline.CompletionGroup // This group is used as a buffer, to add groups to final completions
|
||||
|
||||
return
|
||||
}
|
124
readline/completers/env.go
Normal file
124
readline/completers/env.go
Normal file
@ -0,0 +1,124 @@
|
||||
package completers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/maxlandon/readline"
|
||||
)
|
||||
|
||||
// completeEnvironmentVariables - Returns all environment variables as suggestions
|
||||
func completeEnvironmentVariables(lastWord string) (last string, completions []*readline.CompletionGroup) {
|
||||
|
||||
// Check if last input is made of several different variables
|
||||
allVars := strings.Split(lastWord, "/")
|
||||
lastVar := allVars[len(allVars)-1]
|
||||
|
||||
var evaluated = map[string]string{}
|
||||
|
||||
grp := &readline.CompletionGroup{
|
||||
Name: "console OS environment",
|
||||
MaxLength: 5, // Should be plenty enough
|
||||
DisplayType: readline.TabDisplayGrid,
|
||||
TrimSlash: true, // Some variables can be paths
|
||||
}
|
||||
|
||||
for k, v := range clientEnv {
|
||||
if strings.HasPrefix("$"+k, lastVar) {
|
||||
grp.Suggestions = append(grp.Suggestions, "$"+k+"/")
|
||||
evaluated[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
completions = append(completions, grp)
|
||||
|
||||
return lastVar, completions
|
||||
}
|
||||
|
||||
// clientEnv - Contains all OS environment variables, client-side.
|
||||
// This is used for things like downloading/uploading files from localhost, etc.,
|
||||
// therefore we need completion and parsing stuff, sometimes.
|
||||
var clientEnv = map[string]string{}
|
||||
|
||||
// ParseEnvironmentVariables - Parses a line of input and replace detected environment variables with their values.
|
||||
func ParseEnvironmentVariables(args []string) (processed []string, err error) {
|
||||
|
||||
for _, arg := range args {
|
||||
|
||||
// Anywhere a $ is assigned means there is an env variable
|
||||
if strings.Contains(arg, "$") || strings.Contains(arg, "~") {
|
||||
|
||||
//Split in case env is embedded in path
|
||||
envArgs := strings.Split(arg, "/")
|
||||
|
||||
// If its not a path
|
||||
if len(envArgs) == 1 {
|
||||
processed = append(processed, handleCuratedVar(arg))
|
||||
}
|
||||
|
||||
// If len of the env var split is > 1, its a path
|
||||
if len(envArgs) > 1 {
|
||||
processed = append(processed, handleEmbeddedVar(arg))
|
||||
}
|
||||
} else if arg != "" && arg != " " {
|
||||
// Else, if arg is not an environment variable, return it as is
|
||||
processed = append(processed, arg)
|
||||
}
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// handleCuratedVar - Replace an environment variable alone and without any undesired characters attached
|
||||
func handleCuratedVar(arg string) (value string) {
|
||||
if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" {
|
||||
envVar := strings.TrimPrefix(arg, "$")
|
||||
val, ok := clientEnv[envVar]
|
||||
if !ok {
|
||||
return envVar
|
||||
}
|
||||
return val
|
||||
}
|
||||
if arg != "" && arg == "~" {
|
||||
return clientEnv["HOME"]
|
||||
}
|
||||
|
||||
return arg
|
||||
}
|
||||
|
||||
// handleEmbeddedVar - Replace an environment variable that is in the middle of a path, or other one-string combination
|
||||
func handleEmbeddedVar(arg string) (value string) {
|
||||
|
||||
envArgs := strings.Split(arg, "/")
|
||||
var path []string
|
||||
|
||||
for _, arg := range envArgs {
|
||||
if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" {
|
||||
envVar := strings.TrimPrefix(arg, "$")
|
||||
val, ok := clientEnv[envVar]
|
||||
if !ok {
|
||||
// Err will be caught when command is ran anyway, or completion will stop...
|
||||
path = append(path, arg)
|
||||
}
|
||||
path = append(path, val)
|
||||
} else if arg != "" && arg == "~" {
|
||||
path = append(path, clientEnv["HOME"])
|
||||
} else if arg != " " && arg != "" {
|
||||
path = append(path, arg)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(path, "/")
|
||||
}
|
||||
|
||||
// loadClientEnv - Loads all user environment variables
|
||||
func loadClientEnv() error {
|
||||
env := os.Environ()
|
||||
|
||||
for _, kv := range env {
|
||||
key := strings.Split(kv, "=")[0]
|
||||
value := strings.Split(kv, "=")[1]
|
||||
clientEnv[key] = value
|
||||
}
|
||||
return nil
|
||||
}
|
180
readline/completers/hint-completer.go
Normal file
180
readline/completers/hint-completer.go
Normal file
@ -0,0 +1,180 @@
|
||||
package completers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
|
||||
"github.com/maxlandon/readline"
|
||||
)
|
||||
|
||||
// HintCompleter - Entrypoint to all hints in the Wiregost console
|
||||
func (c *CommandCompleter) HintCompleter(line []rune, pos int) (hint []rune) {
|
||||
|
||||
// Format and sanitize input
|
||||
// @args => All items of the input line
|
||||
// @last => The last word detected in input line as []rune
|
||||
// @lastWord => The last word detected in input as string
|
||||
args, last, lastWord := formatInput(line)
|
||||
|
||||
// Detect base command automatically
|
||||
var command = c.detectedCommand(args)
|
||||
|
||||
// Menu hints (command line is empty, or nothing recognized)
|
||||
if noCommandOrEmpty(args, last, command) {
|
||||
hint = MenuHint(args, last)
|
||||
}
|
||||
|
||||
// Check environment variables
|
||||
if envVarAsked(args, lastWord) {
|
||||
return envVarHint(args, last)
|
||||
}
|
||||
|
||||
// Command Hint
|
||||
if commandFound(command) {
|
||||
|
||||
// Command hint by default (no space between cursor and last command character)
|
||||
hint = CommandHint(command)
|
||||
|
||||
// Check environment variables
|
||||
if envVarAsked(args, lastWord) {
|
||||
return envVarHint(args, last)
|
||||
}
|
||||
|
||||
// If options are asked for root command, return commpletions.
|
||||
if len(command.Groups()) > 0 {
|
||||
for _, grp := range command.Groups() {
|
||||
if opt, yes := optionArgRequired(args, last, grp); yes {
|
||||
hint = OptionArgumentHint(args, last, opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If command has args, hint for args
|
||||
if arg, yes := commandArgumentRequired(lastWord, args, command); yes {
|
||||
hint = []rune(CommandArgumentHints(args, last, command, arg))
|
||||
}
|
||||
|
||||
// Brief subcommand hint
|
||||
if lastIsSubCommand(lastWord, command) {
|
||||
hint = []rune(commandHint + command.Find(string(last)).ShortDescription)
|
||||
}
|
||||
|
||||
// Handle subcommand if found
|
||||
if sub, ok := subCommandFound(lastWord, args, command); ok {
|
||||
return HandleSubcommandHints(args, last, sub)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Handle system binaries, shell commands, etc...
|
||||
if commandFoundInPath(args[0]) {
|
||||
// hint = []rune(exeHint + util.ParseSummary(util.GetManPages(args[0])))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CommandHint - Yields the hint of a Wiregost command
|
||||
func CommandHint(command *flags.Command) (hint []rune) {
|
||||
return []rune(commandHint + command.ShortDescription)
|
||||
}
|
||||
|
||||
// HandleSubcommandHints - Handles hints for a subcommand and its arguments, options, etc.
|
||||
func HandleSubcommandHints(args []string, last []rune, command *flags.Command) (hint []rune) {
|
||||
|
||||
// If command has args, hint for args
|
||||
if arg, yes := commandArgumentRequired(string(last), args, command); yes {
|
||||
hint = []rune(CommandArgumentHints(args, last, command, arg))
|
||||
return
|
||||
}
|
||||
|
||||
// Environment variables
|
||||
if envVarAsked(args, string(last)) {
|
||||
hint = envVarHint(args, last)
|
||||
}
|
||||
|
||||
// If the last word in input is an option --name, yield argument hint if needed
|
||||
if len(command.Groups()) > 0 {
|
||||
for _, grp := range command.Groups() {
|
||||
if opt, yes := optionArgRequired(args, last, grp); yes {
|
||||
hint = OptionArgumentHint(args, last, opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If user asks for completions with "-" or "--".
|
||||
// (Note: This takes precedence on any argument hints, as it is evaluated after them)
|
||||
if commandOptionsAsked(args, string(last), command) {
|
||||
return OptionHints(args, last, command)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CommandArgumentHints - Yields hints for arguments to commands if they have some
|
||||
func CommandArgumentHints(args []string, last []rune, command *flags.Command, arg string) (hint []rune) {
|
||||
|
||||
found := argumentByName(command, arg)
|
||||
// Base Hint is just a description of the command argument
|
||||
hint = []rune(argHint + found.Description)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ModuleOptionHints - If the option being set has a description, show it
|
||||
func ModuleOptionHints(opt string) (hint []rune) {
|
||||
return
|
||||
}
|
||||
|
||||
// OptionHints - Yields hints for proposed options lists/groups
|
||||
func OptionHints(args []string, last []rune, command *flags.Command) (hint []rune) {
|
||||
return
|
||||
}
|
||||
|
||||
// OptionArgumentHint - Yields hints for arguments to an option (generally the last word in input)
|
||||
func OptionArgumentHint(args []string, last []rune, opt *flags.Option) (hint []rune) {
|
||||
return []rune(valueHint + opt.Description)
|
||||
}
|
||||
|
||||
// MenuHint - Returns the Hint for a given menu context
|
||||
func MenuHint(args []string, current []rune) (hint []rune) {
|
||||
return
|
||||
}
|
||||
|
||||
// SpecialCommandHint - Shows hints for Wiregost special commands
|
||||
func SpecialCommandHint(args []string, current []rune) (hint []rune) {
|
||||
return current
|
||||
}
|
||||
|
||||
// envVarHint - Yields hints for environment variables
|
||||
func envVarHint(args []string, last []rune) (hint []rune) {
|
||||
// Trim last in case its a path with multiple vars
|
||||
allVars := strings.Split(string(last), "/")
|
||||
lastVar := allVars[len(allVars)-1]
|
||||
|
||||
// Base hint
|
||||
hint = []rune(envHint + lastVar)
|
||||
|
||||
envVar := strings.TrimPrefix(lastVar, "$")
|
||||
|
||||
if v, ok := clientEnv[envVar]; ok {
|
||||
if v != "" {
|
||||
hintStr := string(hint) + " => " + clientEnv[envVar]
|
||||
hint = []rune(hintStr)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
// Hint signs
|
||||
menuHint = readline.RESET + readline.DIM + readline.BOLD + " menu " + readline.RESET // Dim
|
||||
envHint = readline.RESET + readline.GREEN + readline.BOLD + " env " + readline.RESET + readline.DIM + readline.GREEN // Green
|
||||
commandHint = readline.RESET + readline.DIM + readline.BOLD + " command " + readline.RESET + readline.DIM + "\033[38;5;244m" // Cream
|
||||
exeHint = readline.RESET + readline.DIM + readline.BOLD + " shell " + readline.RESET + readline.DIM // Dim
|
||||
optionHint = "\033[38;5;222m" + readline.BOLD + " options " + readline.RESET + readline.DIM + "\033[38;5;222m" // Cream-Yellow
|
||||
valueHint = readline.RESET + readline.DIM + readline.BOLD + " value " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream
|
||||
// valueHint = "\033[38;5;217m" + readline.BOLD + " Value " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream
|
||||
argHint = readline.DIM + "\033[38;5;217m" + readline.BOLD + " arg " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream
|
||||
)
|
205
readline/completers/local-filesystem.go
Normal file
205
readline/completers/local-filesystem.go
Normal file
@ -0,0 +1,205 @@
|
||||
package completers
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/maxlandon/readline"
|
||||
)
|
||||
|
||||
func completeLocalPath(last string) (string, *readline.CompletionGroup) {
|
||||
|
||||
// Completions
|
||||
completion := &readline.CompletionGroup{
|
||||
Name: "(console) local path",
|
||||
MaxLength: 10, // The grid system is not yet able to roll on comps if > MaxLength
|
||||
DisplayType: readline.TabDisplayGrid,
|
||||
TrimSlash: true,
|
||||
}
|
||||
var suggestions []string
|
||||
|
||||
// Any parsing error is silently ignored, for not messing the prompt
|
||||
processedPath, _ := ParseEnvironmentVariables([]string{last})
|
||||
|
||||
// Check if processed input is empty
|
||||
var inputPath string
|
||||
if len(processedPath) == 1 {
|
||||
inputPath = processedPath[0]
|
||||
}
|
||||
|
||||
// Add a slash if the raw input has one but not the processed input
|
||||
if len(last) > 0 && last[len(last)-1] == '/' {
|
||||
inputPath += "/"
|
||||
}
|
||||
|
||||
var linePath string // curated version of the inputPath
|
||||
var absPath string // absolute path (excluding suffix) of the inputPath
|
||||
var lastPath string // last directory in the input path
|
||||
|
||||
if strings.HasSuffix(string(inputPath), "/") {
|
||||
linePath = filepath.Dir(string(inputPath))
|
||||
absPath, _ = expand(string(linePath)) // Get absolute path
|
||||
|
||||
} else if string(inputPath) == "" {
|
||||
linePath = "."
|
||||
absPath, _ = expand(string(linePath))
|
||||
} else {
|
||||
linePath = filepath.Dir(string(inputPath))
|
||||
absPath, _ = expand(string(linePath)) // Get absolute path
|
||||
lastPath = filepath.Base(string(inputPath)) // Save filter
|
||||
}
|
||||
|
||||
// 2) We take the absolute path we found, and get all dirs in it.
|
||||
var dirs []string
|
||||
files, _ := ioutil.ReadDir(absPath)
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
dirs = append(dirs, file.Name())
|
||||
}
|
||||
}
|
||||
|
||||
switch lastPath {
|
||||
case "":
|
||||
for _, dir := range dirs {
|
||||
if strings.HasPrefix(dir, lastPath) || lastPath == dir {
|
||||
tokenized := addSpaceTokens(dir)
|
||||
suggestions = append(suggestions, tokenized+"/")
|
||||
}
|
||||
}
|
||||
default:
|
||||
filtered := []string{}
|
||||
for _, dir := range dirs {
|
||||
if strings.HasPrefix(dir, lastPath) {
|
||||
filtered = append(filtered, dir)
|
||||
}
|
||||
}
|
||||
|
||||
for _, dir := range filtered {
|
||||
if !hasPrefix([]rune(lastPath), []rune(dir)) || lastPath == dir {
|
||||
tokenized := addSpaceTokens(dir)
|
||||
suggestions = append(suggestions, tokenized+"/")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
completion.Suggestions = suggestions
|
||||
return string(lastPath), completion
|
||||
}
|
||||
|
||||
func addSpaceTokens(in string) (path string) {
|
||||
items := strings.Split(in, " ")
|
||||
for i := range items {
|
||||
if len(items) == i+1 { // If last one, no char, add and return
|
||||
path += items[i]
|
||||
return
|
||||
}
|
||||
path += items[i] + "\\ " // By default add space char and roll
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func completeLocalPathAndFiles(last string) (string, *readline.CompletionGroup) {
|
||||
|
||||
// Completions
|
||||
completion := &readline.CompletionGroup{
|
||||
Name: "(console) local directory/files",
|
||||
MaxLength: 10, // The grid system is not yet able to roll on comps if > MaxLength
|
||||
DisplayType: readline.TabDisplayGrid,
|
||||
TrimSlash: true,
|
||||
}
|
||||
var suggestions []string
|
||||
|
||||
// Any parsing error is silently ignored, for not messing the prompt
|
||||
processedPath, _ := ParseEnvironmentVariables([]string{last})
|
||||
|
||||
// Check if processed input is empty
|
||||
var inputPath string
|
||||
if len(processedPath) == 1 {
|
||||
inputPath = processedPath[0]
|
||||
}
|
||||
|
||||
// Add a slash if the raw input has one but not the processed input
|
||||
if len(last) > 0 && last[len(last)-1] == '/' {
|
||||
inputPath += "/"
|
||||
}
|
||||
|
||||
var linePath string // curated version of the inputPath
|
||||
var absPath string // absolute path (excluding suffix) of the inputPath
|
||||
var lastPath string // last directory in the input path
|
||||
|
||||
if strings.HasSuffix(string(inputPath), "/") {
|
||||
linePath = filepath.Dir(string(inputPath)) // Trim the non needed slash
|
||||
absPath, _ = expand(string(linePath)) // Get absolute path
|
||||
|
||||
} else if string(inputPath) == "" {
|
||||
linePath = "."
|
||||
absPath, _ = expand(string(linePath))
|
||||
} else {
|
||||
linePath = filepath.Dir(string(inputPath))
|
||||
absPath, _ = expand(string(linePath)) // Get absolute path
|
||||
lastPath = filepath.Base(string(inputPath)) // Save filter
|
||||
}
|
||||
|
||||
// 2) We take the absolute path we found, and get all dirs in it.
|
||||
var dirs []string
|
||||
files, _ := ioutil.ReadDir(absPath)
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
dirs = append(dirs, file.Name())
|
||||
}
|
||||
}
|
||||
|
||||
switch lastPath {
|
||||
case "":
|
||||
for _, file := range files {
|
||||
if strings.HasPrefix(file.Name(), lastPath) || lastPath == file.Name() {
|
||||
if file.IsDir() {
|
||||
suggestions = append(suggestions, file.Name()+"/")
|
||||
} else {
|
||||
suggestions = append(suggestions, file.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
filtered := []os.FileInfo{}
|
||||
for _, file := range files {
|
||||
if strings.HasPrefix(file.Name(), lastPath) {
|
||||
filtered = append(filtered, file)
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range filtered {
|
||||
if !hasPrefix([]rune(lastPath), []rune(file.Name())) || lastPath == file.Name() {
|
||||
if file.IsDir() {
|
||||
suggestions = append(suggestions, file.Name()+"/")
|
||||
} else {
|
||||
suggestions = append(suggestions, file.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
completion.Suggestions = suggestions
|
||||
return string(lastPath), completion
|
||||
}
|
||||
|
||||
// expand will expand a path with ~ to the $HOME of the current user.
|
||||
func expand(path string) (string, error) {
|
||||
if path == "" {
|
||||
return path, nil
|
||||
}
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
home = usr.HomeDir
|
||||
}
|
||||
return filepath.Abs(strings.Replace(path, "~", home, 1))
|
||||
}
|
77
readline/completers/option-arguments.go
Normal file
77
readline/completers/option-arguments.go
Normal file
@ -0,0 +1,77 @@
|
||||
package completers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
|
||||
"github.com/maxlandon/readline"
|
||||
)
|
||||
|
||||
// completeOptionArguments - Completes all values for arguments to a command. Arguments here are different from command options (--option).
|
||||
// Many categories, from multiple sources in multiple contexts
|
||||
func completeOptionArguments(cmd *flags.Command, opt *flags.Option, lastWord string) (prefix string, completions []*readline.CompletionGroup) {
|
||||
|
||||
// By default the last word is the prefix
|
||||
prefix = lastWord
|
||||
|
||||
var comp *readline.CompletionGroup // This group is used as a buffer, to add groups to final completions
|
||||
|
||||
// First of all: some options, no matter their contexts and subject, have default values.
|
||||
// When we have such an option, we don't bother analyzing context, we just build completions and return.
|
||||
if len(opt.Choices) > 0 {
|
||||
comp = &readline.CompletionGroup{
|
||||
Name: opt.ValueName, // Value names are specified in struct metadata fields
|
||||
DisplayType: readline.TabDisplayGrid,
|
||||
}
|
||||
for _, choice := range opt.Choices {
|
||||
if strings.HasPrefix(choice, lastWord) {
|
||||
comp.Suggestions = append(comp.Suggestions, choice)
|
||||
}
|
||||
}
|
||||
completions = append(completions, comp)
|
||||
return
|
||||
}
|
||||
|
||||
// EXAMPLE OF COMPLETING ARGUMENTS BASED ON THEIR NAMES -----------------------------------------------------------------------
|
||||
// We have 3 words, potentially different, with which we can filter:
|
||||
//
|
||||
// 1) '--option-name' is the string typed as input.
|
||||
// 2) 'OptionName' is the name of the struct/type for this option.
|
||||
// 3) 'ValueName' is the name of the value we expect.
|
||||
// var match = func(name string) bool {
|
||||
// if strings.Contains(opt.Field().Name, name) {
|
||||
// return true
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// // Sessions
|
||||
// if match("ImplantID") || match("SessionID") {
|
||||
// completions = append(completions, sessionIDs(lastWord))
|
||||
// }
|
||||
//
|
||||
// // Any arguments with a path name. Often we "save" files that need paths, certificates, etc
|
||||
// if match("Path") || match("Save") || match("Certificate") || match("PrivateKey") {
|
||||
// switch cmd.Name {
|
||||
// case constants.WebContentTypeStr, constants.WebUpdateStr, constants.AddWebContentStr, constants.RmWebContentStr:
|
||||
// // Make an exception for WebPath option in websites commands.
|
||||
// default:
|
||||
// switch opt.ValueName {
|
||||
// case "local-path", "path":
|
||||
// prefix, comp = completeLocalPath(lastWord)
|
||||
// completions = append(completions, comp)
|
||||
// case "local-file", "file":
|
||||
// prefix, comp = completeLocalPathAndFiles(lastWord)
|
||||
// completions = append(completions, comp)
|
||||
// default:
|
||||
// // We always have a default searching for files, locally
|
||||
// prefix, comp = completeLocalPathAndFiles(lastWord)
|
||||
// completions = append(completions, comp)
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
return
|
||||
}
|
548
readline/completers/patterns.go
Normal file
548
readline/completers/patterns.go
Normal file
@ -0,0 +1,548 @@
|
||||
package completers
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
)
|
||||
|
||||
// These functions are just shorthands for checking various conditions on the input line.
|
||||
// They make the main function more readable, which might be useful, should a logic error pop somewhere.
|
||||
|
||||
// [ Parser Commands & Options ] --------------------------------------------------------------------------
|
||||
// ArgumentByName Get the name of a detected command's argument
|
||||
func argumentByName(command *flags.Command, name string) *flags.Arg {
|
||||
args := command.Args()
|
||||
for _, arg := range args {
|
||||
if arg.Name == name {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// optionByName - Returns an option for a command or a subcommand, identified by name
|
||||
func optionByName(cmd *flags.Command, option string) *flags.Option {
|
||||
|
||||
if cmd == nil {
|
||||
return nil
|
||||
}
|
||||
// Get all (root) option groups.
|
||||
groups := cmd.Groups()
|
||||
|
||||
// For each group, build completions
|
||||
for _, grp := range groups {
|
||||
// Add each option to completion group
|
||||
for _, opt := range grp.Options() {
|
||||
if opt.LongName == option {
|
||||
return opt
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// [ Menus ] --------------------------------------------------------------------------------------------
|
||||
// Is the input line is either empty, or without any detected command ?
|
||||
func noCommandOrEmpty(args []string, last []rune, command *flags.Command) bool {
|
||||
if len(args) == 0 || len(args) == 1 && command == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// [ Commands ] -------------------------------------------------------------------------------------
|
||||
// detectedCommand - Returns the base command from parser if detected, depending on context
|
||||
func (c *CommandCompleter) detectedCommand(args []string) (command *flags.Command) {
|
||||
arg := strings.TrimSpace(args[0])
|
||||
command = c.parser.Find(arg)
|
||||
return
|
||||
}
|
||||
|
||||
// is the command a special command, usually not handled by parser ?
|
||||
func isSpecialCommand(args []string, command *flags.Command) bool {
|
||||
|
||||
// If command is not nil, return
|
||||
if command == nil {
|
||||
// Shell
|
||||
if args[0] == "!" {
|
||||
return true
|
||||
}
|
||||
// Exit
|
||||
if args[0] == "exit" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// The commmand has been found
|
||||
func commandFound(command *flags.Command) bool {
|
||||
if command != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Search for input in $PATH
|
||||
func commandFoundInPath(input string) bool {
|
||||
_, err := exec.LookPath(input)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// [ SubCommands ]-------------------------------------------------------------------------------------
|
||||
// Does the command have subcommands ?
|
||||
func hasSubCommands(command *flags.Command, args []string) bool {
|
||||
if len(args) < 2 || command == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(command.Commands()) != 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Does the input has a subcommand in it ?
|
||||
func subCommandFound(lastWord string, raw []string, command *flags.Command) (sub *flags.Command, ok bool) {
|
||||
// First, filter redundant spaces. This does not modify the actual line
|
||||
args := ignoreRedundantSpaces(raw)
|
||||
|
||||
if len(args) <= 1 || command == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
sub = command.Find(args[1])
|
||||
if sub != nil {
|
||||
return sub, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Is the last input PRECISELY a subcommand. This is used as a brief hint for the subcommand
|
||||
func lastIsSubCommand(lastWord string, command *flags.Command) bool {
|
||||
if sub := command.Find(lastWord); sub != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// [ Arguments ]-------------------------------------------------------------------------------------
|
||||
// Does the command have arguments ?
|
||||
func hasArgs(command *flags.Command) bool {
|
||||
if len(command.Args()) != 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// commandArgumentRequired - Analyses input and sends back the next argument name to provide completion for
|
||||
func commandArgumentRequired(lastWord string, raw []string, command *flags.Command) (name string, yes bool) {
|
||||
|
||||
// First, filter redundant spaces. This does not modify the actual line
|
||||
args := ignoreRedundantSpaces(raw)
|
||||
|
||||
// Trim command and subcommand args
|
||||
var remain []string
|
||||
if args[0] == command.Name {
|
||||
remain = args[1:]
|
||||
}
|
||||
if len(args) > 1 && args[1] == command.Name {
|
||||
remain = args[2:]
|
||||
}
|
||||
|
||||
// The remain may include a "" as a last element,
|
||||
// which we don't consider as a real remain, so we move it away
|
||||
switch lastWord {
|
||||
case "":
|
||||
case command.Name:
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Trim all --option flags and their arguments if they have
|
||||
remain = filterOptions(remain, command)
|
||||
|
||||
// For each argument, check if needs completion. If not continue, if yes return.
|
||||
// The arguments remainder is popped according to the number of values expected.
|
||||
for i, arg := range command.Args() {
|
||||
|
||||
// If it's required and has one argument, check filled.
|
||||
if arg.Required == 1 && arg.RequiredMaximum == 1 {
|
||||
|
||||
// If last word is the argument, and we are
|
||||
// last arg in: line keep completing.
|
||||
if len(remain) < 1 {
|
||||
return arg.Name, true
|
||||
}
|
||||
|
||||
// If the we are still writing the argument
|
||||
if len(remain) == 1 {
|
||||
if lastWord != "" {
|
||||
return arg.Name, true
|
||||
}
|
||||
}
|
||||
|
||||
// If filed and we are not last arg, continue
|
||||
if len(remain) > 1 && i < (len(command.Args())-1) {
|
||||
remain = remain[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// If we need more than one value and we knwo the maximum,
|
||||
// either return or pop the remain.
|
||||
if arg.Required > 0 && arg.RequiredMaximum > 1 {
|
||||
// Pop the corresponding amount of arguments.
|
||||
var found int
|
||||
for i := 0; i < len(remain) && i < arg.RequiredMaximum; i++ {
|
||||
remain = remain[1:]
|
||||
found++
|
||||
}
|
||||
|
||||
// If we still need values:
|
||||
if len(remain) == 0 && found <= arg.RequiredMaximum {
|
||||
if lastWord == "" { // We are done, no more completions.
|
||||
break
|
||||
} else {
|
||||
return arg.Name, true
|
||||
}
|
||||
}
|
||||
// Else go on with the next argument
|
||||
continue
|
||||
}
|
||||
|
||||
// If has required arguments, with no limit of needs, return true
|
||||
if arg.Required > 0 && arg.RequiredMaximum == -1 {
|
||||
return arg.Name, true
|
||||
}
|
||||
|
||||
// Else, if no requirements and the command has subcommands,
|
||||
// return so that we complete subcommands
|
||||
if arg.Required == -1 && len(command.Commands()) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Else, return this argument
|
||||
// NOTE: This block is after because we always use []type arguments
|
||||
// AFTER individual argument fields. Thus blocks any args that have
|
||||
// not been processed.
|
||||
if arg.Required == -1 {
|
||||
return arg.Name, true
|
||||
}
|
||||
}
|
||||
|
||||
// Once we exited the loop, it means that none of the arguments require completion:
|
||||
// They are all either optional, or fullfiled according to their required numbers.
|
||||
// Thus we return none
|
||||
return "", false
|
||||
}
|
||||
|
||||
// getRemainingArgs - Filters the input slice from commands and detected option:value pairs, and returns args
|
||||
func getRemainingArgs(args []string, last []rune, command *flags.Command) (remain []string) {
|
||||
|
||||
var input []string
|
||||
// Clean subcommand name
|
||||
if args[0] == command.Name && len(args) >= 2 {
|
||||
input = args[1:]
|
||||
} else if len(args) == 1 {
|
||||
input = args
|
||||
}
|
||||
|
||||
// For each each argument
|
||||
for i := 0; i < len(input); i++ {
|
||||
// Check option prefix
|
||||
if strings.HasPrefix(input[i], "-") || strings.HasPrefix(input[i], "--") {
|
||||
// Clean it
|
||||
cur := strings.TrimPrefix(input[i], "--")
|
||||
cur = strings.TrimPrefix(cur, "-")
|
||||
|
||||
// Check if option matches any command option
|
||||
if opt := command.FindOptionByLongName(cur); opt != nil {
|
||||
boolean := true
|
||||
if opt.Field().Type == reflect.TypeOf(boolean) {
|
||||
continue // If option is boolean, don't skip an argument
|
||||
}
|
||||
i++ // Else skip next arg in input
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Safety check
|
||||
if input[i] == "" || input[i] == " " {
|
||||
continue
|
||||
}
|
||||
|
||||
remain = append(remain, input[i])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// [ Options ]-------------------------------------------------------------------------------------
|
||||
// commandOptionsAsked - Does the user asks for options in a root command ?
|
||||
func commandOptionsAsked(args []string, lastWord string, command *flags.Command) bool {
|
||||
if len(args) >= 2 && (strings.HasPrefix(lastWord, "-") || strings.HasPrefix(lastWord, "--")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// commandOptionsAsked - Does the user asks for options in a subcommand ?
|
||||
func subCommandOptionsAsked(args []string, lastWord string, command *flags.Command) bool {
|
||||
if len(args) > 2 && (strings.HasPrefix(lastWord, "-") || strings.HasPrefix(lastWord, "--")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Is the last input argument is a dash ?
|
||||
func isOptionDash(args []string, last []rune) bool {
|
||||
if len(args) > 2 && (strings.HasPrefix(string(last), "-") || strings.HasPrefix(string(last), "--")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// optionIsAlreadySet - Detects in input if an option is already set
|
||||
func optionIsAlreadySet(args []string, lastWord string, opt *flags.Option) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if option type allows for repetition
|
||||
func optionNotRepeatable(opt *flags.Option) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// [ Option Values ]-------------------------------------------------------------------------------------
|
||||
// Is the last input word an option name (--option) ?
|
||||
func optionArgRequired(args []string, last []rune, group *flags.Group) (opt *flags.Option, yes bool) {
|
||||
|
||||
var lastItem string
|
||||
var lastOption string
|
||||
var option *flags.Option
|
||||
|
||||
// If there is argument required we must have 1) command 2) --option inputs at least.
|
||||
if len(args) <= 2 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Check for last two arguments in input
|
||||
if strings.HasPrefix(args[len(args)-2], "-") || strings.HasPrefix(args[len(args)-2], "--") {
|
||||
|
||||
// Long opts
|
||||
if strings.HasPrefix(args[len(args)-2], "--") {
|
||||
lastOption = strings.TrimPrefix(args[len(args)-2], "--")
|
||||
if opt := group.FindOptionByLongName(lastOption); opt != nil {
|
||||
option = opt
|
||||
}
|
||||
|
||||
// Short opts
|
||||
} else if strings.HasPrefix(args[len(args)-2], "-") {
|
||||
lastOption = strings.TrimPrefix(args[len(args)-2], "-")
|
||||
if len(lastOption) > 0 {
|
||||
if opt := group.FindOptionByShortName(rune(lastOption[0])); opt != nil {
|
||||
option = opt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If option is found, and we still are in writing the argument
|
||||
if (lastItem == "" && option != nil) || option != nil {
|
||||
// Check if option is a boolean, if yes return false
|
||||
boolean := true
|
||||
if option.Field().Type == reflect.TypeOf(boolean) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return option, true
|
||||
}
|
||||
|
||||
// Check for previous argument
|
||||
if lastItem != "" && option == nil {
|
||||
if strings.HasPrefix(args[len(args)-2], "-") || strings.HasPrefix(args[len(args)-2], "--") {
|
||||
|
||||
// Long opts
|
||||
if strings.HasPrefix(args[len(args)-2], "--") {
|
||||
lastOption = strings.TrimPrefix(args[len(args)-2], "--")
|
||||
if opt := group.FindOptionByLongName(lastOption); opt != nil {
|
||||
option = opt
|
||||
return option, true
|
||||
}
|
||||
|
||||
// Short opts
|
||||
} else if strings.HasPrefix(args[len(args)-2], "-") {
|
||||
lastOption = strings.TrimPrefix(args[len(args)-2], "-")
|
||||
if opt := group.FindOptionByShortName(rune(lastOption[0])); opt != nil {
|
||||
option = opt
|
||||
return option, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// [ Other ]-------------------------------------------------------------------------------------
|
||||
// Does the user asks for Environment variables ?
|
||||
func envVarAsked(args []string, lastWord string) bool {
|
||||
|
||||
// Check if the current word is an environment variable, or if the last part of it is a variable
|
||||
if len(lastWord) > 1 && strings.HasPrefix(lastWord, "$") {
|
||||
if strings.LastIndex(lastWord, "/") < strings.LastIndex(lastWord, "$") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if env var is asked in a path or something
|
||||
if len(lastWord) > 1 {
|
||||
// If last is a path, it cannot be an env var anymore
|
||||
if lastWord[len(lastWord)-1] == '/' {
|
||||
return false
|
||||
}
|
||||
|
||||
if lastWord[len(lastWord)-1] == '$' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// If we are at the beginning of an env var
|
||||
if len(lastWord) > 0 && lastWord[len(lastWord)-1] == '$' {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// filterOptions - Check various elements of an option and return a list
|
||||
func filterOptions(args []string, command *flags.Command) (processed []string) {
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
// --long-name options
|
||||
if strings.HasPrefix(arg, "--") {
|
||||
name := strings.TrimPrefix(arg, "--")
|
||||
if opt := optionByName(command, name); opt != nil {
|
||||
var boolean = true
|
||||
if opt.Field().Type == reflect.TypeOf(boolean) {
|
||||
continue
|
||||
}
|
||||
// Else skip the option argument (next item)
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
// -s short options
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
name := strings.TrimPrefix(arg, "-")
|
||||
if opt := optionByName(command, name); opt != nil {
|
||||
var boolean = true
|
||||
if opt.Field().Type == reflect.TypeOf(boolean) {
|
||||
continue
|
||||
}
|
||||
// Else skip the option argument (next item)
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
processed = append(processed, arg)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Other Functions -------------------------------------------------------------------------------------------------------------//
|
||||
|
||||
// formatInput - Formats & sanitize the command line input
|
||||
func formatInput(line []rune) (args []string, last []rune, lastWord string) {
|
||||
args = strings.Split(string(line), " ") // The readline input as a []string
|
||||
last = trimSpaceLeft([]rune(args[len(args)-1])) // The last char in input
|
||||
lastWord = string(last)
|
||||
return
|
||||
}
|
||||
|
||||
// FormatInput - Formats & sanitize the command line input
|
||||
func formatInputHighlighter(line []rune) (args []string, last []rune, lastWord string) {
|
||||
args = strings.SplitN(string(line), " ", -1)
|
||||
last = trimSpaceLeft([]rune(args[len(args)-1])) // The last char in input
|
||||
lastWord = string(last)
|
||||
return
|
||||
}
|
||||
|
||||
// ignoreRedundantSpaces - We might have several spaces between each real arguments.
|
||||
// However these indivual spaces are counted as args themselves.
|
||||
// For each space arg found, verify that no space args follow,
|
||||
// and if some are found, delete them.
|
||||
func ignoreRedundantSpaces(raw []string) (args []string) {
|
||||
|
||||
for i := 0; i < len(raw); i++ {
|
||||
// Catch a space argument.
|
||||
if raw[i] == "" {
|
||||
// The arg evaulated is always kept, because we just adjusted
|
||||
// the indexing to avoid the ones we don't need
|
||||
// args = append(args, raw[i])
|
||||
|
||||
for y, next := range raw[i:] {
|
||||
if next != "" {
|
||||
i += y - 1
|
||||
break
|
||||
}
|
||||
// If we come to the end while not breaking
|
||||
// we push the outer loop straight to the end.
|
||||
if y == len(raw[i:])-1 {
|
||||
i += y
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The arg evaulated is always kept, because we just adjusted
|
||||
// the indexing to avoid the ones we don't need
|
||||
args = append(args, raw[i])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func trimSpaceLeft(in []rune) []rune {
|
||||
firstIndex := len(in)
|
||||
for i, r := range in {
|
||||
if unicode.IsSpace(r) == false {
|
||||
firstIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
return in[firstIndex:]
|
||||
}
|
||||
|
||||
func equal(a, b []rune) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(a); i++ {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func hasPrefix(r, prefix []rune) bool {
|
||||
if len(r) < len(prefix) {
|
||||
return false
|
||||
}
|
||||
return equal(r[:len(prefix)], prefix)
|
||||
}
|
151
readline/completers/syntax-highlighter.go
Normal file
151
readline/completers/syntax-highlighter.go
Normal file
@ -0,0 +1,151 @@
|
||||
package completers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
|
||||
"github.com/maxlandon/readline"
|
||||
)
|
||||
|
||||
// SyntaxHighlighter - Entrypoint to all input syntax highlighting in the Wiregost console
|
||||
func (c *CommandCompleter) SyntaxHighlighter(input []rune) (line string) {
|
||||
|
||||
// Format and sanitize input
|
||||
args, last, lastWord := formatInputHighlighter(input)
|
||||
|
||||
// Remain is all arguments that have not been highlighted, we need it for completing long commands
|
||||
var remain = args
|
||||
|
||||
// Detect base command automatically
|
||||
var command = c.detectedCommand(args)
|
||||
|
||||
// Return input as is
|
||||
if noCommandOrEmpty(remain, last, command) {
|
||||
return string(input)
|
||||
}
|
||||
|
||||
// Base command
|
||||
if commandFound(command) {
|
||||
line, remain = highlightCommand(remain, command)
|
||||
|
||||
// SubCommand
|
||||
if sub, ok := subCommandFound(lastWord, args, command); ok {
|
||||
line, remain = highlightSubCommand(line, remain, sub)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
line = processRemain(line, remain)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func highlightCommand(args []string, command *flags.Command) (line string, remain []string) {
|
||||
line = readline.BOLD + args[0] + readline.RESET + " "
|
||||
remain = args[1:]
|
||||
return
|
||||
}
|
||||
|
||||
func highlightSubCommand(input string, args []string, command *flags.Command) (line string, remain []string) {
|
||||
line = input
|
||||
line += readline.BOLD + args[0] + readline.RESET + " "
|
||||
remain = args[1:]
|
||||
return
|
||||
}
|
||||
|
||||
func processRemain(input string, remain []string) (line string) {
|
||||
|
||||
// Check the last is not the last space in input
|
||||
if len(remain) == 1 && remain[0] == " " {
|
||||
return input
|
||||
}
|
||||
|
||||
line = input + strings.Join(remain, " ")
|
||||
// line = processEnvVars(input, remain)
|
||||
return
|
||||
}
|
||||
|
||||
// processEnvVars - Highlights environment variables. NOTE: Rewrite with logic from console/env.go
|
||||
func processEnvVars(input string, remain []string) (line string) {
|
||||
|
||||
var processed []string
|
||||
|
||||
inputSlice := strings.Split(input, " ")
|
||||
|
||||
// Check already processed input
|
||||
for _, arg := range inputSlice {
|
||||
if arg == "" || arg == " " {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(arg, "$") { // It is an env var.
|
||||
if args := strings.Split(arg, "/"); len(args) > 1 {
|
||||
for _, a := range args {
|
||||
fmt.Println(a)
|
||||
if strings.HasPrefix(a, "$") && a != " " { // It is an env var.
|
||||
processed = append(processed, "\033[38;5;108m"+readline.DIM+a+readline.RESET)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
processed = append(processed, "\033[38;5;108m"+readline.DIM+arg+readline.RESET)
|
||||
continue
|
||||
}
|
||||
processed = append(processed, arg)
|
||||
}
|
||||
|
||||
// Check remaining args (non-processed)
|
||||
for _, arg := range remain {
|
||||
if arg == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(arg, "$") && arg != "$" { // It is an env var.
|
||||
var full string
|
||||
args := strings.Split(arg, "/")
|
||||
if len(args) == 1 {
|
||||
if strings.HasPrefix(args[0], "$") && args[0] != "" && args[0] != "$" { // It is an env var.
|
||||
full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(args) > 1 {
|
||||
var counter int
|
||||
for _, arg := range args {
|
||||
// If var is an env var
|
||||
if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" {
|
||||
if counter < len(args)-1 {
|
||||
full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET + "/"
|
||||
counter++
|
||||
continue
|
||||
}
|
||||
if counter == len(args)-1 {
|
||||
full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET
|
||||
counter++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Else, if we are not at the end of array
|
||||
if counter < len(args)-1 && arg != "" {
|
||||
full += arg + "/"
|
||||
counter++
|
||||
}
|
||||
if counter == len(args)-1 {
|
||||
full += arg
|
||||
counter++
|
||||
}
|
||||
}
|
||||
}
|
||||
// Else add first var
|
||||
processed = append(processed, full)
|
||||
}
|
||||
}
|
||||
|
||||
line = strings.Join(processed, " ")
|
||||
|
||||
// Very important, keeps the line clear when erasing
|
||||
// line += " "
|
||||
|
||||
return
|
||||
}
|
289
readline/completers/tab-completer.go
Normal file
289
readline/completers/tab-completer.go
Normal file
@ -0,0 +1,289 @@
|
||||
package completers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
|
||||
"github.com/maxlandon/readline"
|
||||
)
|
||||
|
||||
// CommandCompleter - A completer using a github.com/jessevdk/go-flags Command Parser, in order
|
||||
// to build completions for commands, arguments, options and their arguments as well.
|
||||
// This completer needs to be instantiated with its constructor, in order to ensure the parser is not nil.
|
||||
type CommandCompleter struct {
|
||||
parser *flags.Parser
|
||||
}
|
||||
|
||||
// NewCommandCompleter - Instantiate a new tab completer using a github.com/jessevdk/go-flags Command Parser.
|
||||
func NewCommandCompleter(parser *flags.Parser) (completer *CommandCompleter, err error) {
|
||||
if parser == nil {
|
||||
return nil, errors.New("command completer was instantiated with a nil parser")
|
||||
}
|
||||
return &CommandCompleter{parser: parser}, nil
|
||||
}
|
||||
|
||||
// TabCompleter - A default tab completer working with a github.com/jessevdk/go-flags parser.
|
||||
func (c *CommandCompleter) TabCompleter(line []rune, pos int, dtc readline.DelayedTabContext) (lastWord string, completions []*readline.CompletionGroup) {
|
||||
|
||||
// Format and sanitize input
|
||||
// @args => All items of the input line
|
||||
// @last => The last word detected in input line as []rune
|
||||
// @lastWord => The last word detected in input as string
|
||||
args, last, lastWord := formatInput(line)
|
||||
|
||||
// Detect base command automatically
|
||||
var command = c.detectedCommand(args)
|
||||
|
||||
// Propose commands
|
||||
if noCommandOrEmpty(args, last, command) {
|
||||
return c.completeMenuCommands(lastWord, pos)
|
||||
}
|
||||
|
||||
// Check environment variables
|
||||
if envVarAsked(args, lastWord) {
|
||||
completeEnvironmentVariables(lastWord)
|
||||
}
|
||||
|
||||
// Base command has been identified
|
||||
if commandFound(command) {
|
||||
// Check environment variables again
|
||||
if envVarAsked(args, lastWord) {
|
||||
return completeEnvironmentVariables(lastWord)
|
||||
}
|
||||
|
||||
// If options are asked for root command, return commpletions.
|
||||
if len(command.Groups()) > 0 {
|
||||
for _, grp := range command.Groups() {
|
||||
if opt, yes := optionArgRequired(args, last, grp); yes {
|
||||
return completeOptionArguments(command, opt, lastWord)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then propose subcommands. We don't return from here, otherwise it always skips the next steps.
|
||||
if hasSubCommands(command, args) {
|
||||
completions = completeSubCommands(args, lastWord, command)
|
||||
}
|
||||
|
||||
// Handle subcommand if found (maybe we should rewrite this function and use it also for base command)
|
||||
if sub, ok := subCommandFound(lastWord, args, command); ok {
|
||||
return handleSubCommand(line, pos, sub)
|
||||
}
|
||||
|
||||
// If user asks for completions with "-" / "--", show command options.
|
||||
// We ask this here, after having ensured there is no subcommand invoked.
|
||||
// This prevails over command arguments, even if they are required.
|
||||
if commandOptionsAsked(args, lastWord, command) {
|
||||
return completeCommandOptions(args, lastWord, command)
|
||||
}
|
||||
|
||||
// Propose argument completion before anything, and if needed
|
||||
if arg, yes := commandArgumentRequired(lastWord, args, command); yes {
|
||||
return completeCommandArguments(command, arg, lastWord)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// [ Main Completion Functions ] -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// completeMenuCommands - Selects all commands available in a given context and returns them as suggestions
|
||||
// Many categories, all from command parsers.
|
||||
func (c *CommandCompleter) completeMenuCommands(lastWord string, pos int) (prefix string, completions []*readline.CompletionGroup) {
|
||||
|
||||
prefix = lastWord // We only return the PREFIX for readline to correctly show suggestions.
|
||||
|
||||
// Check their namespace (which should be their "group" (like utils, core, Jobs, etc))
|
||||
for _, cmd := range c.parser.Commands() {
|
||||
// If command matches readline input
|
||||
if strings.HasPrefix(cmd.Name, lastWord) {
|
||||
// Check command group: add to existing group if found
|
||||
var found bool
|
||||
for _, grp := range completions {
|
||||
if grp.Name == cmd.Aliases[0] {
|
||||
found = true
|
||||
grp.Suggestions = append(grp.Suggestions, cmd.Name)
|
||||
grp.Descriptions[cmd.Name] = readline.Dim(cmd.ShortDescription)
|
||||
}
|
||||
}
|
||||
// Add a new group if not found
|
||||
if !found {
|
||||
grp := &readline.CompletionGroup{
|
||||
Name: cmd.Aliases[0],
|
||||
Suggestions: []string{cmd.Name},
|
||||
Descriptions: map[string]string{
|
||||
cmd.Name: readline.Dim(cmd.ShortDescription),
|
||||
},
|
||||
}
|
||||
completions = append(completions, grp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make adjustments to the CompletionGroup list: set maxlength depending on items, check descriptions, etc.
|
||||
for _, grp := range completions {
|
||||
// If the length of suggestions is too long and we have
|
||||
// many groups, use grid display.
|
||||
if len(completions) >= 10 && len(grp.Suggestions) >= 7 {
|
||||
grp.DisplayType = readline.TabDisplayGrid
|
||||
} else {
|
||||
// By default, we use a map of command to descriptions
|
||||
grp.DisplayType = readline.TabDisplayList
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// completeSubCommands - Takes subcommands and gives them as suggestions
|
||||
// One category, from one source (a parent command).
|
||||
func completeSubCommands(args []string, lastWord string, command *flags.Command) (completions []*readline.CompletionGroup) {
|
||||
|
||||
group := &readline.CompletionGroup{
|
||||
Name: command.Name,
|
||||
Suggestions: []string{},
|
||||
Descriptions: map[string]string{},
|
||||
DisplayType: readline.TabDisplayList,
|
||||
}
|
||||
|
||||
for _, sub := range command.Commands() {
|
||||
if strings.HasPrefix(sub.Name, lastWord) {
|
||||
group.Suggestions = append(group.Suggestions, sub.Name)
|
||||
group.Descriptions[sub.Name] = readline.DIM + sub.ShortDescription + readline.RESET
|
||||
}
|
||||
}
|
||||
|
||||
completions = append(completions, group)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// handleSubCommand - Handles completion for subcommand options and arguments, + any option value related completion
|
||||
// Many categories, from many sources: this function calls the same functions as the ones previously called for completing its parent command.
|
||||
func handleSubCommand(line []rune, pos int, command *flags.Command) (lastWord string, completions []*readline.CompletionGroup) {
|
||||
|
||||
args, last, lastWord := formatInput(line)
|
||||
|
||||
// Check environment variables
|
||||
if envVarAsked(args, lastWord) {
|
||||
completeEnvironmentVariables(lastWord)
|
||||
}
|
||||
|
||||
// Check argument options
|
||||
if len(command.Groups()) > 0 {
|
||||
for _, grp := range command.Groups() {
|
||||
if opt, yes := optionArgRequired(args, last, grp); yes {
|
||||
return completeOptionArguments(command, opt, lastWord)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If user asks for completions with "-" or "--". This must take precedence on arguments.
|
||||
if subCommandOptionsAsked(args, lastWord, command) {
|
||||
return completeCommandOptions(args, lastWord, command)
|
||||
}
|
||||
|
||||
// If command has non-filled arguments, propose them first
|
||||
if arg, yes := commandArgumentRequired(lastWord, args, command); yes {
|
||||
return completeCommandArguments(command, arg, lastWord)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// completeCommandOptions - Yields completion for options of a command, with various decorators
|
||||
// Many categories, from one source (a command)
|
||||
func completeCommandOptions(args []string, lastWord string, cmd *flags.Command) (prefix string, completions []*readline.CompletionGroup) {
|
||||
|
||||
prefix = lastWord // We only return the PREFIX for readline to correctly show suggestions.
|
||||
|
||||
// Get all (root) option groups.
|
||||
groups := cmd.Groups()
|
||||
|
||||
// Append command options not gathered in groups
|
||||
groups = append(groups, cmd.Group)
|
||||
|
||||
// For each group, build completions
|
||||
for _, grp := range groups {
|
||||
|
||||
_, comp := completeOptionGroup(lastWord, grp, "")
|
||||
|
||||
// No need to add empty groups, will screw the completion system.
|
||||
if len(comp.Suggestions) > 0 {
|
||||
completions = append(completions, comp)
|
||||
}
|
||||
}
|
||||
|
||||
// Do the same for global options, which are not part of any group "per-se"
|
||||
_, gcomp := completeOptionGroup(lastWord, cmd.Group, "global options")
|
||||
if len(gcomp.Suggestions) > 0 {
|
||||
completions = append(completions, gcomp)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// completeOptionGroup - make completions for a single group of options. Title is optional, not used if empty.
|
||||
func completeOptionGroup(lastWord string, grp *flags.Group, title string) (prefix string, compGrp *readline.CompletionGroup) {
|
||||
|
||||
compGrp = &readline.CompletionGroup{
|
||||
Name: grp.ShortDescription,
|
||||
Descriptions: map[string]string{},
|
||||
DisplayType: readline.TabDisplayList,
|
||||
Aliases: map[string]string{},
|
||||
}
|
||||
|
||||
// An optional title for this comp group.
|
||||
// Used by global flag options, added to all commands.
|
||||
if title != "" {
|
||||
compGrp.Name = title
|
||||
}
|
||||
|
||||
// Add each option to completion group
|
||||
for _, opt := range grp.Options() {
|
||||
|
||||
// Check if option is already set, next option if yes
|
||||
// if optionNotRepeatable(opt) && optionIsAlreadySet(args, lastWord, opt) {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// Depending on the current last word, either build a group with option longs only, or with shorts
|
||||
if strings.HasPrefix("--"+opt.LongName, lastWord) {
|
||||
optName := "--" + opt.LongName
|
||||
compGrp.Suggestions = append(compGrp.Suggestions, optName)
|
||||
|
||||
// Add short if there is, and that the prefix is only one dash
|
||||
if strings.HasPrefix("-", lastWord) {
|
||||
if opt.ShortName != 0 {
|
||||
compGrp.Aliases[optName] = "-" + string(opt.ShortName)
|
||||
}
|
||||
}
|
||||
|
||||
// Option default value if any
|
||||
var def string
|
||||
if len(opt.Default) > 0 {
|
||||
def = " (default:"
|
||||
for _, d := range opt.Default {
|
||||
def += " " + d + ","
|
||||
}
|
||||
def = strings.TrimSuffix(def, ",")
|
||||
def += ")"
|
||||
}
|
||||
|
||||
desc := fmt.Sprintf(" -- %s%s%s", opt.Description, def, readline.RESET)
|
||||
compGrp.Descriptions[optName] = desc
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RecursiveGroupCompletion - Handles recursive completion for nested option groups
|
||||
// Many categories, one source (a command's root option group). Called by the function just above.
|
||||
func RecursiveGroupCompletion(args []string, last []rune, group *flags.Group) (lastWord string, completions []*readline.CompletionGroup) {
|
||||
return
|
||||
}
|
109
readline/examples/arguments.go
Normal file
109
readline/examples/arguments.go
Normal file
@ -0,0 +1,109 @@
|
||||
package main
|
||||
|
||||
// This file defines a few argument choices for commands
|
||||
|
||||
import (
|
||||
"github.com/jessevdk/go-flags"
|
||||
)
|
||||
|
||||
// Command/option argument choices
|
||||
var (
|
||||
// Logs & components
|
||||
logLevels = []string{"trace", "debug", "info", "warning", "error"}
|
||||
loggers = []string{"client", "comm"}
|
||||
|
||||
// Stages / Stagers
|
||||
implantOS = []string{"windows", "linux", "darwin"}
|
||||
implantArch = []string{"amd64", "x86"}
|
||||
implantFmt = []string{"exe", "shared", "service", "shellcode"}
|
||||
|
||||
stageListenerProtocols = []string{"tcp", "http", "https"}
|
||||
|
||||
// MSF
|
||||
msfStagerProtocols = []string{"tcp", "http", "https"}
|
||||
msfTransformFormats = []string{
|
||||
"bash",
|
||||
"c",
|
||||
"csharp",
|
||||
"dw",
|
||||
"dword",
|
||||
"hex",
|
||||
"java",
|
||||
"js_be",
|
||||
"js_le",
|
||||
"num",
|
||||
"perl",
|
||||
"pl",
|
||||
"powershell",
|
||||
"ps1",
|
||||
"py",
|
||||
"python",
|
||||
"raw",
|
||||
"rb",
|
||||
"ruby",
|
||||
"sh",
|
||||
"vbapplication",
|
||||
"vbscript",
|
||||
}
|
||||
|
||||
msfEncoders = []string{
|
||||
"x86/shikata_ga_nai",
|
||||
"x64/xor_dynamic",
|
||||
}
|
||||
|
||||
msfPayloads = map[string][]string{
|
||||
"windows": windowsMsfPayloads,
|
||||
"linux": linuxMsfPayloads,
|
||||
"osx": osxMsfPayloads,
|
||||
}
|
||||
|
||||
// ValidPayloads - Valid payloads and OS combos
|
||||
windowsMsfPayloads = []string{
|
||||
"meterpreter_reverse_http",
|
||||
"meterpreter_reverse_https",
|
||||
"meterpreter_reverse_tcp",
|
||||
"meterpreter/reverse_tcp",
|
||||
"meterpreter/reverse_http",
|
||||
"meterpreter/reverse_https",
|
||||
}
|
||||
linuxMsfPayloads = []string{
|
||||
"meterpreter_reverse_http",
|
||||
"meterpreter_reverse_https",
|
||||
"meterpreter_reverse_tcp",
|
||||
}
|
||||
osxMsfPayloads = []string{
|
||||
"meterpreter_reverse_http",
|
||||
"meterpreter_reverse_https",
|
||||
"meterpreter_reverse_tcp",
|
||||
}
|
||||
|
||||
// Comm network protocols
|
||||
portfwdProtocols = []string{"tcp", "udp"}
|
||||
transportProtocols = []string{"tcp", "udp", "ip"}
|
||||
applicationProtocols = []string{"http", "https", "mtls", "quic", "http3", "dns", "named_pipe"}
|
||||
)
|
||||
|
||||
// loadArgumentCompletions - Adds a bunch of choices for command arguments (and their completions.)
|
||||
func loadArgumentCompletions(parser *flags.Parser) {
|
||||
if parser == nil {
|
||||
return
|
||||
}
|
||||
serverCompsAddtional(parser)
|
||||
}
|
||||
|
||||
// Additional completion mappings for command in the server context
|
||||
func serverCompsAddtional(parser *flags.Parser) {
|
||||
|
||||
// Stage options
|
||||
g := parser.Find("generate")
|
||||
g.FindOptionByLongName("os").Choices = implantOS
|
||||
g.FindOptionByLongName("arch").Choices = implantArch
|
||||
g.FindOptionByLongName("format").Choices = implantFmt
|
||||
|
||||
// Stager options (mostly MSF)
|
||||
gs := g.Find("stager")
|
||||
gs.FindOptionByLongName("os").Choices = implantOS
|
||||
gs.FindOptionByLongName("arch").Choices = implantArch
|
||||
gs.FindOptionByLongName("protocol").Choices = msfStagerProtocols
|
||||
gs.FindOptionByLongName("msf-format").Choices = msfTransformFormats
|
||||
}
|
315
readline/examples/commands.go
Normal file
315
readline/examples/commands.go
Normal file
@ -0,0 +1,315 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
|
||||
"github.com/maxlandon/readline"
|
||||
)
|
||||
|
||||
// This file declares a go-flags parser and a few commands.
|
||||
|
||||
var (
|
||||
// commandParser - The command parser used by the example console.
|
||||
commandParser = flags.NewNamedParser("example", flags.IgnoreUnknown)
|
||||
)
|
||||
|
||||
func bindCommands() (err error) {
|
||||
|
||||
// core console
|
||||
// ----------------------------------------------------------------------------------------
|
||||
ex, err := commandParser.AddCommand("exit", // Command string
|
||||
"Exit from the client/server console", // Description (completions, help usage)
|
||||
"", // Long description
|
||||
&Exit{}) // Command implementation
|
||||
ex.Aliases = []string{"core"}
|
||||
|
||||
cd, err := commandParser.AddCommand("cd",
|
||||
"Change client working directory",
|
||||
"",
|
||||
&ChangeClientDirectory{})
|
||||
cd.Aliases = []string{"core"}
|
||||
|
||||
ls, err := commandParser.AddCommand("ls",
|
||||
"List directory contents",
|
||||
"",
|
||||
&ListClientDirectories{})
|
||||
ls.Aliases = []string{"core"}
|
||||
|
||||
// Log
|
||||
log, err := commandParser.AddCommand("log",
|
||||
"Manage log levels of one or more components",
|
||||
"",
|
||||
&Log{})
|
||||
log.Aliases = []string{"core"}
|
||||
|
||||
// Implant generation
|
||||
// ----------------------------------------------------------------------------------------
|
||||
g, err := commandParser.AddCommand("generate",
|
||||
"Configure and compile an implant (staged or stager)",
|
||||
"",
|
||||
&Generate{})
|
||||
g.Aliases = []string{"builds"}
|
||||
g.SubcommandsOptional = true
|
||||
|
||||
_, err = g.AddCommand("stager",
|
||||
"Generate a stager shellcode payload using MSFVenom, (to file: --save, to stdout: --format",
|
||||
"",
|
||||
&GenerateStager{})
|
||||
|
||||
r, err := commandParser.AddCommand("regenerate",
|
||||
"Recompile an implant by name, passed as argument (completed)",
|
||||
"",
|
||||
&Regenerate{})
|
||||
r.Aliases = []string{"builds"}
|
||||
|
||||
// Add choices completions (and therefore completions) to some of these commands.
|
||||
loadArgumentCompletions(commandParser)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Exit - Kill the current client console
|
||||
type Exit struct{}
|
||||
|
||||
// Execute - Run
|
||||
func (e *Exit) Execute(args []string) (err error) {
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print("Confirm exit (Y/y): ")
|
||||
text, _ := reader.ReadString('\n')
|
||||
answer := strings.TrimSpace(text)
|
||||
|
||||
if (answer == "Y") || (answer == "y") {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
return
|
||||
}
|
||||
|
||||
// ChangeClientDirectory - Change the working directory of the client console
|
||||
type ChangeClientDirectory struct {
|
||||
Positional struct {
|
||||
Path string `description:"local path" required:"1-1"`
|
||||
} `positional-args:"yes" required:"yes"`
|
||||
}
|
||||
|
||||
// Execute - Handler for ChangeDirectory
|
||||
func (cd *ChangeClientDirectory) Execute(args []string) (err error) {
|
||||
|
||||
dir, err := expand(cd.Positional.Path)
|
||||
|
||||
err = os.Chdir(dir)
|
||||
if err != nil {
|
||||
fmt.Printf(CommandError+"%s \n", err)
|
||||
} else {
|
||||
fmt.Printf(Info+"Changed directory to %s \n", dir)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ListClientDirectories - List directory contents
|
||||
type ListClientDirectories struct {
|
||||
Positional struct {
|
||||
Path []string `description:"local directory/file"`
|
||||
} `positional-args:"yes"`
|
||||
}
|
||||
|
||||
// Execute - Command
|
||||
func (ls *ListClientDirectories) Execute(args []string) error {
|
||||
|
||||
base := []string{"ls", "--color", "-l"}
|
||||
|
||||
if len(ls.Positional.Path) == 0 {
|
||||
ls.Positional.Path = []string{"."}
|
||||
}
|
||||
|
||||
fullPaths := []string{}
|
||||
for _, path := range ls.Positional.Path {
|
||||
full, _ := expand(path)
|
||||
fullPaths = append(fullPaths, full)
|
||||
}
|
||||
base = append(base, fullPaths...)
|
||||
|
||||
// Print output
|
||||
out, err := shellExec(base[0], base[1:])
|
||||
if err != nil {
|
||||
fmt.Printf(CommandError+"%s \n", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Print output
|
||||
fmt.Println(out)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// shellExec - Execute a program
|
||||
func shellExec(executable string, args []string) (string, error) {
|
||||
path, err := exec.LookPath(executable)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cmd := exec.Command(path, args...)
|
||||
|
||||
// Load OS environment
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.Trim(string(out), "/"), nil
|
||||
}
|
||||
|
||||
// Generate - Configure and compile an implant
|
||||
type Generate struct {
|
||||
StageOptions // Command makes use of full stage options
|
||||
}
|
||||
|
||||
// StageOptions - All these options, regrouped by area, are used by any command that needs full
|
||||
// configuration information for a stage Sliver implant.
|
||||
type StageOptions struct {
|
||||
// CoreOptions - All options about OS/arch, files to save, debugs, etc.
|
||||
CoreOptions struct {
|
||||
OS string `long:"os" short:"o" description:"target host operating system" default:"windows" value-name:"stage OS"`
|
||||
Arch string `long:"arch" short:"a" description:"target host CPU architecture" default:"amd64" value-name:"stage architectures"`
|
||||
Format string `long:"format" short:"f" description:"output formats (exe, shared (DLL), service (see 'psexec' for info), shellcode (Windows only)" default:"exe" value-name:"stage formats"`
|
||||
Profile string `long:"profile-name" description:"implant profile name to use (use with generate-profile)"`
|
||||
Name string `long:"name" short:"N" description:"implant name to use (overrides random name generation)"`
|
||||
Save string `long:"save" short:"s" description:"directory/file where to save binary"`
|
||||
Debug bool `long:"debug" short:"d" description:"enable debug features (incompatible with obfuscation, and prevailing)"`
|
||||
} `group:"core options"`
|
||||
|
||||
// TransportOptions - All options pertaining to transport/RPC matters
|
||||
TransportOptions struct {
|
||||
MTLS []string `long:"mtls" short:"m" description:"mTLS C2 domain(s), comma-separated (ex: mtls://host:port)" env-delim:","`
|
||||
DNS []string `long:"dns" short:"n" description:"DNS C2 domain(s), comma-separated (ex: dns://mydomain.com)" env-delim:","`
|
||||
HTTP []string `long:"http" short:"h" description:"HTTP(S) C2 domain(s)" env-delim:","`
|
||||
NamedPipe []string `long:"named-pipe" short:"p" description:"Named pipe transport strings, comma-separated" env-delim:","`
|
||||
TCPPivot []string `long:"tcp-pivot" short:"i" description:"TCP pivot transport strings, comma-separated" env-delim:","`
|
||||
Reconnect int `long:"reconnect" short:"j" description:"attempt to reconnect every n second(s)" default:"60"`
|
||||
MaxErrors int `long:"max-errors" short:"k" description:"max number of transport errors" default:"10"`
|
||||
} `group:"transport options"`
|
||||
|
||||
// SecurityOptions - All security-oriented options like restrictions.
|
||||
SecurityOptions struct {
|
||||
LimitDatetime string `long:"limit-datetime" short:"w" description:"limit execution to before datetime"`
|
||||
LimitDomain bool `long:"limit-domain-joined" short:"D" description:"limit execution to domain joined machines"`
|
||||
LimitUsername string `long:"limit-username" short:"U" description:"limit execution to specified username"`
|
||||
LimitHosname string `long:"limit-hostname" short:"H" description:"limit execution to specified hostname"`
|
||||
LimitFileExits string `long:"limit-file-exists" short:"F" description:"limit execution to hosts with this file in the filesystem"`
|
||||
} `group:"security options"`
|
||||
|
||||
// EvasionOptions - All proactive security options (obfuscation, evasion, etc)
|
||||
EvasionOptions struct {
|
||||
Canary []string `long:"canary" short:"c" description:"DNS canary domain strings, comma-separated" env-delim:","`
|
||||
SkipSymbols bool `long:"skip-obfuscation" short:"b" description:"skip binary/symbol obfuscation"`
|
||||
Evasion bool `long:"evasion" short:"e" description:"enable evasion features"`
|
||||
} `group:"evasion options"`
|
||||
}
|
||||
|
||||
// Execute - Configure and compile an implant
|
||||
func (g *Generate) Execute(args []string) (err error) {
|
||||
save := g.CoreOptions.Save
|
||||
if save == "" {
|
||||
save, _ = os.Getwd()
|
||||
}
|
||||
|
||||
fmt.Println("Executed 'generate' command. ")
|
||||
return
|
||||
}
|
||||
|
||||
// Regenerate - Recompile an implant by name, passed as argument (completed)
|
||||
type Regenerate struct {
|
||||
Positional struct {
|
||||
ImplantName string `description:"Name of Sliver implant to recompile" required:"1-1"`
|
||||
} `positional-args:"yes" required:"yes"`
|
||||
Save string `long:"save" short:"s" description:"Directory/file where to save binary"`
|
||||
}
|
||||
|
||||
// Execute - Recompile an implant with a given profile
|
||||
func (r *Regenerate) Execute(args []string) (err error) {
|
||||
fmt.Println("Executed 'regenerate' command. ")
|
||||
return
|
||||
}
|
||||
|
||||
// GenerateStager - Generate a stager payload using MSFVenom
|
||||
type GenerateStager struct {
|
||||
PayloadOptions struct {
|
||||
OS string `long:"os" short:"o" description:"target host operating system" default:"windows" value-name:"stage OS"`
|
||||
Arch string `long:"arch" short:"a" description:"target host CPU architecture" default:"amd64" value-name:"stage architectures"`
|
||||
Format string `long:"msf-format" short:"f" description:"output format (MSF Venom formats). List is auto-completed" default:"raw" value-name:"MSF Venom transform formats"`
|
||||
BadChars string `long:"badchars" short:"b" description:"bytes to exclude from stage shellcode"`
|
||||
Save string `long:"save" short:"s" description:"directory to save the generated stager to"`
|
||||
} `group:"payload options"`
|
||||
TransportOptions struct {
|
||||
LHost string `long:"lhost" short:"l" description:"listening host address" required:"true"`
|
||||
LPort int `long:"lport" short:"p" description:"listening host port" default:"8443"`
|
||||
Protocol string `long:"protocol" short:"P" description:"staging protocol (tcp/http/https)" default:"tcp" value-name:"stager protocol"`
|
||||
} `group:"transport options"`
|
||||
}
|
||||
|
||||
// Execute - Generate a stager payload using MSFVenom
|
||||
func (g *GenerateStager) Execute(args []string) (err error) {
|
||||
fmt.Println("Executed 'generate stager' subcommand. ")
|
||||
return
|
||||
}
|
||||
|
||||
// Log - Log management commands. Sets log level by default.
|
||||
type Log struct {
|
||||
Positional struct {
|
||||
Level string `description:"log level to filter by" required:"1-1"`
|
||||
Components []string `description:"components on which to apply log filter" required:"1"`
|
||||
} `positional-args:"yes" required:"true"`
|
||||
}
|
||||
|
||||
// Execute - Set the log level of one or more components
|
||||
func (l *Log) Execute(args []string) (err error) {
|
||||
fmt.Println("Executed 'log' command. ")
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
Info = fmt.Sprintf("%s[-]%s ", readline.BLUE, readline.RESET)
|
||||
Warn = fmt.Sprintf("%s[!]%s ", readline.YELLOW, readline.RESET)
|
||||
Error = fmt.Sprintf("%s[!]%s ", readline.RED, readline.RESET)
|
||||
Success = fmt.Sprintf("%s[*]%s ", readline.GREEN, readline.RESET)
|
||||
|
||||
Infof = fmt.Sprintf("%s[-] ", readline.BLUE) // Infof - formatted
|
||||
Warnf = fmt.Sprintf("%s[!] ", readline.YELLOW) // Warnf - formatted
|
||||
Errorf = fmt.Sprintf("%s[!] ", readline.RED) // Errorf - formatted
|
||||
Sucessf = fmt.Sprintf("%s[*] ", readline.GREEN) // Sucessf - formatted
|
||||
|
||||
RPCError = fmt.Sprintf("%s[RPC Error]%s ", readline.RED, readline.RESET)
|
||||
CommandError = fmt.Sprintf("%s[Command Error]%s ", readline.RED, readline.RESET)
|
||||
ParserError = fmt.Sprintf("%s[Parser Error]%s ", readline.RED, readline.RESET)
|
||||
DBError = fmt.Sprintf("%s[DB Error]%s ", readline.RED, readline.RESET)
|
||||
)
|
||||
|
||||
// expand will expand a path with ~ to the $HOME of the current user.
|
||||
func expand(path string) (string, error) {
|
||||
if path == "" {
|
||||
return path, nil
|
||||
}
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
home = usr.HomeDir
|
||||
}
|
||||
return filepath.Abs(strings.Replace(path, "~", home, 1))
|
||||
}
|
171
readline/examples/main.go
Normal file
171
readline/examples/main.go
Normal file
@ -0,0 +1,171 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
|
||||
"github.com/maxlandon/readline"
|
||||
"github.com/maxlandon/readline/completers"
|
||||
)
|
||||
|
||||
// This file shows a typical way of using readline in a loop.
|
||||
|
||||
func main() {
|
||||
// Instantiate a console object
|
||||
console := newConsole()
|
||||
|
||||
// Bind commands to the console
|
||||
bindCommands()
|
||||
|
||||
// Setup the console completers, prompts, and input modes
|
||||
console.setup()
|
||||
|
||||
// Start the readline loop (blocking)
|
||||
console.Start()
|
||||
}
|
||||
|
||||
// newConsole - Instantiates a new console with some default behavior.
|
||||
// We modify/add elements of behavior later in setup.
|
||||
func newConsole() *console {
|
||||
console := &console{
|
||||
shell: readline.NewInstance(),
|
||||
parser: commandParser,
|
||||
}
|
||||
return console
|
||||
}
|
||||
|
||||
// console - A simple console example.
|
||||
type console struct {
|
||||
shell *readline.Instance
|
||||
parser *flags.Parser
|
||||
}
|
||||
|
||||
// setup - The console sets up various elements such as the completion system, hints,
|
||||
// syntax highlighting, prompt system, commands binding, and client environment loading.
|
||||
func (c *console) setup() (err error) {
|
||||
|
||||
// Input mode & defails
|
||||
c.shell.InputMode = readline.Vim // Could be readline.Emacs for emacs input mode.
|
||||
c.shell.ShowVimMode = true
|
||||
c.shell.VimModeColorize = true
|
||||
|
||||
// Prompt: we want a two-line prompt, with a custom indicator after the Vim status
|
||||
c.shell.SetPrompt("readline ")
|
||||
c.shell.Multiline = true
|
||||
c.shell.MultilinePrompt = " > "
|
||||
|
||||
// Instantiate a default completer associated with the parser
|
||||
// declared in commands.go, and embedded into the console struct.
|
||||
// The error is muted, because we don't pass an nil parser, therefore no problems.
|
||||
defaultCompleter, _ := completers.NewCommandCompleter(c.parser)
|
||||
|
||||
// Register the completer for command/option completions, hints and syntax highlighting.
|
||||
// The completer can handle all of them.
|
||||
c.shell.TabCompleter = defaultCompleter.TabCompleter
|
||||
c.shell.HintText = defaultCompleter.HintCompleter
|
||||
c.shell.SyntaxHighlighter = defaultCompleter.SyntaxHighlighter
|
||||
|
||||
// History: by default the history is in-memory, use it with Ctrl-R
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Start - The console has a working RPC connection: we setup all
|
||||
// things pertaining to the console itself, and start the input loop.
|
||||
func (c *console) Start() (err error) {
|
||||
|
||||
// Setup console elements
|
||||
err = c.setup()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Console setup failed: %s", err)
|
||||
}
|
||||
|
||||
// Start input loop
|
||||
for {
|
||||
// Read input line
|
||||
line, _ := c.Readline()
|
||||
|
||||
// Split and sanitize input
|
||||
sanitized, empty := sanitizeInput(line)
|
||||
if empty {
|
||||
continue
|
||||
}
|
||||
|
||||
// Process various tokens on input (environment variables, paths, etc.)
|
||||
// These tokens will be expaneded by completers anyway, so this is not absolutely required.
|
||||
envParsed, _ := completers.ParseEnvironmentVariables(sanitized)
|
||||
|
||||
// Other types of tokens, needed by commands who expect a certain type
|
||||
// of arguments, such as paths with spaces.
|
||||
tokenParsed := c.parseTokens(envParsed)
|
||||
|
||||
// Execute the command and print any errors
|
||||
if _, parserErr := c.parser.ParseArgs(tokenParsed); parserErr != nil {
|
||||
fmt.Println(readline.RED + "[Error] " + readline.RESET + parserErr.Error() + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Readline - Add an empty line between input line and command output.
|
||||
func (c *console) Readline() (line string, err error) {
|
||||
line, err = c.shell.Readline()
|
||||
fmt.Println()
|
||||
return
|
||||
}
|
||||
|
||||
// sanitizeInput - Trims spaces and other unwished elements from the input line.
|
||||
func sanitizeInput(line string) (sanitized []string, empty bool) {
|
||||
|
||||
// Assume the input is not empty
|
||||
empty = false
|
||||
|
||||
// Trim border spaces
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if len(line) < 1 {
|
||||
empty = true
|
||||
return
|
||||
}
|
||||
unfiltered := strings.Split(trimmed, " ")
|
||||
|
||||
// Catch any eventual empty items
|
||||
for _, arg := range unfiltered {
|
||||
if arg != "" {
|
||||
sanitized = append(sanitized, arg)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// parseTokens - Parse and process any special tokens that are not treated by environment-like parsers.
|
||||
func (c *console) parseTokens(sanitized []string) (parsed []string) {
|
||||
|
||||
// PATH SPACE TOKENS
|
||||
// Catch \ tokens, which have been introduced in paths where some directories have spaces in name.
|
||||
// For each of these splits, we concatenate them with the next string.
|
||||
// This will also inspect commands/options/arguments, but there is no reason why a backlash should be present in them.
|
||||
var pathAdjusted []string
|
||||
var roll bool
|
||||
var arg string
|
||||
for i := range sanitized {
|
||||
if strings.HasSuffix(sanitized[i], "\\") {
|
||||
// If we find a suffix, replace with a space. Go on with next input
|
||||
arg += strings.TrimSuffix(sanitized[i], "\\") + " "
|
||||
roll = true
|
||||
} else if roll {
|
||||
// No suffix but part of previous input. Add it and go on.
|
||||
arg += sanitized[i]
|
||||
pathAdjusted = append(pathAdjusted, arg)
|
||||
arg = ""
|
||||
roll = false
|
||||
} else {
|
||||
// Default, we add our path and go on.
|
||||
pathAdjusted = append(pathAdjusted, sanitized[i])
|
||||
}
|
||||
}
|
||||
parsed = pathAdjusted
|
||||
|
||||
// Add new function here, act on parsed []string from now on, not sanitized
|
||||
return
|
||||
}
|
@ -56,10 +56,3 @@ func (rl *Instance) resetHintText() {
|
||||
//rl.hintY = 0
|
||||
rl.hintText = []rune{}
|
||||
}
|
||||
|
||||
func (rl *Instance) insertHintText() {
|
||||
if len(rl.hintText) != 0 {
|
||||
// fill in hint text
|
||||
rl.insert(rl.hintText)
|
||||
}
|
||||
}
|
||||
|
@ -707,9 +707,6 @@ func (rl *Instance) escapeSeq(r []rune) {
|
||||
rl.renderHelpers()
|
||||
return
|
||||
}
|
||||
|
||||
rl.insertHintText()
|
||||
|
||||
if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) ||
|
||||
(rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) {
|
||||
rl.moveCursorByAdjust(1)
|
||||
|
@ -142,10 +142,6 @@ func (rl *Instance) viDeleteByAdjust(adjust int) {
|
||||
rl.updateHelpers()
|
||||
}
|
||||
|
||||
func (rl *Instance) DeleteByAmount(adjust int) {
|
||||
rl.viDeleteByAdjust(adjust)
|
||||
}
|
||||
|
||||
func (rl *Instance) vimDeleteToken(r rune) bool {
|
||||
tokens, _, _ := tokeniseSplitSpaces(rl.line, 0)
|
||||
pos := int(r) - 48 // convert ASCII to integer
|
||||
|
@ -21,18 +21,16 @@ A runner is passed the input and has to return a table with these values.
|
||||
All are not required, only the useful ones the runner needs to return.
|
||||
(So if there isn't an error, just omit `err`.)
|
||||
|
||||
- `exitCode` (number): Exit code of the command
|
||||
- `input` (string): The text input of the user. This is used by Hilbish to append extra input, in case
|
||||
more is requested.
|
||||
- `err` (string): A string that represents an error from the runner.
|
||||
This should only be set when, for example, there is a syntax error.
|
||||
It can be set to a few special values for Hilbish to throw the right
|
||||
hooks and have a better looking message.
|
||||
- `<command>: not-found` will throw a `command.not-found` hook
|
||||
based on what `<command>` is.
|
||||
- `<command>: not-executable` will throw a `command.not-executable` hook.
|
||||
- `continue` (boolean): Whether Hilbish should prompt the user for no input
|
||||
- `newline` (boolean): Whether a newline should be added at the end of `input`.
|
||||
- `exitCode` (number): A numerical code to indicate the exit result.
|
||||
- `input` (string): The user input. This will be used to add
|
||||
to the history.
|
||||
- `err` (string): A string to indicate an interal error for the runner.
|
||||
It can be set to a few special values for Hilbish to throw the right hooks and have a better looking message:
|
||||
|
||||
`[command]: not-found` will throw a command.not-found hook based on what `[command]` is.
|
||||
|
||||
`[command]: not-executable` will throw a command.not-executable hook.
|
||||
- `continue` (boolean): Whether to prompt the user for more input.
|
||||
|
||||
Here is a simple example of a fennel runner. It falls back to
|
||||
shell script if fennel eval has an error.
|
||||
@ -53,7 +51,9 @@ end)
|
||||
*/
|
||||
func runnerModeLoader(rtm *rt.Runtime) *rt.Table {
|
||||
exports := map[string]util.LuaExport{
|
||||
"sh": {shRunner, 1, false},
|
||||
"lua": {luaRunner, 1, false},
|
||||
"setMode": {hlrunnerMode, 1, false},
|
||||
}
|
||||
|
||||
mod := rt.NewTable()
|
||||
@ -62,6 +62,43 @@ func runnerModeLoader(rtm *rt.Runtime) *rt.Table {
|
||||
return mod
|
||||
}
|
||||
|
||||
// #interface runner
|
||||
// setMode(cb)
|
||||
// This is the same as the `hilbish.runnerMode` function.
|
||||
// It takes a callback, which will be used to execute all interactive input.
|
||||
// In normal cases, neither callbacks should be overrided by the user,
|
||||
// as the higher level functions listed below this will handle it.
|
||||
// #param cb function
|
||||
func _runnerMode() {}
|
||||
|
||||
// #interface runner
|
||||
// sh(cmd)
|
||||
// Runs a command in Hilbish's shell script interpreter.
|
||||
// This is the equivalent of using `source`.
|
||||
// #param cmd string
|
||||
func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
if err := c.Check1Arg(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd, err := c.StringArg(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, exitCode, cont, err := execSh(aliases.Resolve(cmd))
|
||||
var luaErr rt.Value = rt.NilValue
|
||||
if err != nil {
|
||||
luaErr = rt.StringValue(err.Error())
|
||||
}
|
||||
runnerRet := rt.NewTable()
|
||||
runnerRet.Set(rt.StringValue("input"), rt.StringValue(cmd))
|
||||
runnerRet.Set(rt.StringValue("exitCode"), rt.IntValue(int64(exitCode)))
|
||||
runnerRet.Set(rt.StringValue("continue"), rt.BoolValue(cont))
|
||||
runnerRet.Set(rt.StringValue("err"), luaErr)
|
||||
|
||||
return c.PushingNext(t.Runtime, rt.TableValue(runnerRet)), nil
|
||||
}
|
||||
|
||||
// #interface runner
|
||||
// lua(cmd)
|
||||
// Evaluates `cmd` as Lua input. This is the same as using `dofile`
|
||||
|
@ -1,32 +1,35 @@
|
||||
package util
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"hilbish/util"
|
||||
|
||||
rt "github.com/arnodel/golua/runtime"
|
||||
)
|
||||
|
||||
var sinkMetaKey = rt.StringValue("hshsink")
|
||||
|
||||
// #type
|
||||
// A sink is a structure that has input and/or output to/from a desination.
|
||||
type Sink struct{
|
||||
Rw *bufio.ReadWriter
|
||||
// A sink is a structure that has input and/or output to/from
|
||||
// a desination.
|
||||
type sink struct{
|
||||
writer *bufio.Writer
|
||||
reader *bufio.Reader
|
||||
file *os.File
|
||||
UserData *rt.UserData
|
||||
ud *rt.UserData
|
||||
autoFlush bool
|
||||
}
|
||||
|
||||
func SinkLoader(rtm *rt.Runtime) *rt.Table {
|
||||
func setupSinkType(rtm *rt.Runtime) {
|
||||
sinkMeta := rt.NewTable()
|
||||
|
||||
sinkMethods := rt.NewTable()
|
||||
sinkFuncs := map[string]LuaExport{
|
||||
sinkFuncs := map[string]util.LuaExport{
|
||||
"flush": {luaSinkFlush, 1, false},
|
||||
"read": {luaSinkRead, 1, false},
|
||||
"readAll": {luaSinkReadAll, 1, false},
|
||||
@ -34,7 +37,7 @@ func SinkLoader(rtm *rt.Runtime) *rt.Table {
|
||||
"write": {luaSinkWrite, 2, false},
|
||||
"writeln": {luaSinkWriteln, 2, false},
|
||||
}
|
||||
SetExports(rtm, sinkMethods, sinkFuncs)
|
||||
util.SetExports(l, sinkMethods, sinkFuncs)
|
||||
|
||||
sinkIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
s, _ := sinkArg(c, 0)
|
||||
@ -61,25 +64,10 @@ func SinkLoader(rtm *rt.Runtime) *rt.Table {
|
||||
}
|
||||
|
||||
sinkMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(sinkIndex, "__index", 2, false)))
|
||||
rtm.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta))
|
||||
|
||||
exports := map[string]LuaExport{
|
||||
"new": {luaSinkNew, 0, false},
|
||||
}
|
||||
|
||||
mod := rt.NewTable()
|
||||
SetExports(rtm, mod, exports)
|
||||
|
||||
return mod
|
||||
l.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta))
|
||||
}
|
||||
|
||||
|
||||
func luaSinkNew(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
snk := NewSink(t.Runtime, new(bytes.Buffer))
|
||||
|
||||
return c.PushingNext1(t.Runtime, rt.UserDataValue(snk.UserData)), nil
|
||||
}
|
||||
|
||||
// #member
|
||||
// readAll() -> string
|
||||
// --- @returns string
|
||||
@ -94,17 +82,11 @@ func luaSinkReadAll(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.autoFlush {
|
||||
s.Rw.Flush()
|
||||
}
|
||||
|
||||
lines := []string{}
|
||||
for {
|
||||
line, err := s.Rw.ReadString('\n')
|
||||
line, err := s.reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// We still want to add the data we read
|
||||
lines = append(lines, line)
|
||||
break
|
||||
}
|
||||
|
||||
@ -131,7 +113,7 @@ func luaSinkRead(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
str, _ := s.Rw.ReadString('\n')
|
||||
str, _ := s.reader.ReadString('\n')
|
||||
|
||||
return c.PushingNext1(t.Runtime, rt.StringValue(str)), nil
|
||||
}
|
||||
@ -153,9 +135,9 @@ func luaSinkWrite(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.Rw.Write([]byte(data))
|
||||
s.writer.Write([]byte(data))
|
||||
if s.autoFlush {
|
||||
s.Rw.Flush()
|
||||
s.writer.Flush()
|
||||
}
|
||||
|
||||
return c.Next(), nil
|
||||
@ -178,9 +160,9 @@ func luaSinkWriteln(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.Rw.Write([]byte(data + "\n"))
|
||||
s.writer.Write([]byte(data + "\n"))
|
||||
if s.autoFlush {
|
||||
s.Rw.Flush()
|
||||
s.writer.Flush()
|
||||
}
|
||||
|
||||
return c.Next(), nil
|
||||
@ -199,7 +181,7 @@ func luaSinkFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.Rw.Flush()
|
||||
s.writer.Flush()
|
||||
|
||||
return c.Next(), nil
|
||||
}
|
||||
@ -230,25 +212,11 @@ func luaSinkAutoFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||
return c.Next(), nil
|
||||
}
|
||||
|
||||
func NewSink(rtm *rt.Runtime, Rw io.ReadWriter) *Sink {
|
||||
s := &Sink{
|
||||
Rw: bufio.NewReadWriter(bufio.NewReader(Rw), bufio.NewWriter(Rw)),
|
||||
autoFlush: true,
|
||||
func newSinkInput(r io.Reader) *sink {
|
||||
s := &sink{
|
||||
reader: bufio.NewReader(r),
|
||||
}
|
||||
s.UserData = sinkUserData(rtm, s)
|
||||
|
||||
if f, ok := Rw.(*os.File); ok {
|
||||
s.file = f
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func NewSinkInput(rtm *rt.Runtime, r io.Reader) *Sink {
|
||||
s := &Sink{
|
||||
Rw: bufio.NewReadWriter(bufio.NewReader(r), nil),
|
||||
}
|
||||
s.UserData = sinkUserData(rtm, s)
|
||||
s.ud = sinkUserData(s)
|
||||
|
||||
if f, ok := r.(*os.File); ok {
|
||||
s.file = f
|
||||
@ -257,17 +225,17 @@ func NewSinkInput(rtm *rt.Runtime, r io.Reader) *Sink {
|
||||
return s
|
||||
}
|
||||
|
||||
func NewSinkOutput(rtm *rt.Runtime, w io.Writer) *Sink {
|
||||
s := &Sink{
|
||||
Rw: bufio.NewReadWriter(nil, bufio.NewWriter(w)),
|
||||
func newSinkOutput(w io.Writer) *sink {
|
||||
s := &sink{
|
||||
writer: bufio.NewWriter(w),
|
||||
autoFlush: true,
|
||||
}
|
||||
s.UserData = sinkUserData(rtm, s)
|
||||
s.ud = sinkUserData(s)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func sinkArg(c *rt.GoCont, arg int) (*Sink, error) {
|
||||
func sinkArg(c *rt.GoCont, arg int) (*sink, error) {
|
||||
s, ok := valueToSink(c.Arg(arg))
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("#%d must be a sink", arg + 1)
|
||||
@ -276,17 +244,17 @@ func sinkArg(c *rt.GoCont, arg int) (*Sink, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func valueToSink(val rt.Value) (*Sink, bool) {
|
||||
func valueToSink(val rt.Value) (*sink, bool) {
|
||||
u, ok := val.TryUserData()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
s, ok := u.Value().(*Sink)
|
||||
s, ok := u.Value().(*sink)
|
||||
return s, ok
|
||||
}
|
||||
|
||||
func sinkUserData(rtm *rt.Runtime, s *Sink) *rt.UserData {
|
||||
sinkMeta := rtm.Registry(sinkMetaKey)
|
||||
func sinkUserData(s *sink) *rt.UserData {
|
||||
sinkMeta := l.Registry(sinkMetaKey)
|
||||
return rt.NewUserData(s, sinkMeta.AsTable())
|
||||
}
|
9
testplugin/testplugin.go
Normal file
9
testplugin/testplugin.go
Normal file
@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
rt "github.com/arnodel/golua/runtime"
|
||||
)
|
||||
|
||||
func Loader(rtm *rt.Runtime) rt.Value {
|
||||
return rt.StringValue("hello world!")
|
||||
}
|
BIN
testplugin/testplugin.so
Normal file
BIN
testplugin/testplugin.so
Normal file
Binary file not shown.
@ -1,11 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type Streams struct {
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
Stdin io.Reader
|
||||
}
|
137
util/util.go
137
util/util.go
@ -2,78 +2,14 @@ package util
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
rt "github.com/arnodel/golua/runtime"
|
||||
)
|
||||
|
||||
var ErrNotExec = errors.New("not executable")
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
type ExecError struct{
|
||||
Typ string
|
||||
Cmd string
|
||||
Code int
|
||||
Colon bool
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ExecError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Cmd, e.Typ)
|
||||
}
|
||||
|
||||
func (e ExecError) sprint() error {
|
||||
sep := " "
|
||||
if e.Colon {
|
||||
sep = ": "
|
||||
}
|
||||
|
||||
return fmt.Errorf("hilbish: %s%s%s", e.Cmd, sep, e.Err.Error())
|
||||
}
|
||||
|
||||
func IsExecError(err error) (ExecError, bool) {
|
||||
if exErr, ok := err.(ExecError); ok {
|
||||
return exErr, true
|
||||
}
|
||||
|
||||
fields := strings.Split(err.Error(), ": ")
|
||||
knownTypes := []string{
|
||||
"not-found",
|
||||
"not-executable",
|
||||
}
|
||||
|
||||
if len(fields) > 1 && Contains(knownTypes, fields[1]) {
|
||||
var colon bool
|
||||
var e error
|
||||
switch fields[1] {
|
||||
case "not-found":
|
||||
e = ErrNotFound
|
||||
case "not-executable":
|
||||
colon = true
|
||||
e = ErrNotExec
|
||||
}
|
||||
|
||||
return ExecError{
|
||||
Cmd: fields[0],
|
||||
Typ: fields[1],
|
||||
Colon: colon,
|
||||
Err: e,
|
||||
}, true
|
||||
}
|
||||
|
||||
return ExecError{}, false
|
||||
}
|
||||
|
||||
// SetField sets a field in a table, adding docs for it.
|
||||
// It is accessible via the __docProp metatable. It is a table of the names of the fields.
|
||||
func SetField(rtm *rt.Runtime, module *rt.Table, field string, value rt.Value) {
|
||||
@ -100,15 +36,6 @@ func DoString(rtm *rt.Runtime, code string) (rt.Value, error) {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func MustDoString(rtm *rt.Runtime, code string) rt.Value {
|
||||
val, err := DoString(rtm, code)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// DoFile runs the contents of the file in the Lua runtime.
|
||||
func DoFile(rtm *rt.Runtime, path string) error {
|
||||
f, err := os.Open(path)
|
||||
@ -214,67 +141,3 @@ func AbbrevHome(path string) string {
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func LookPath(file string) (string, error) { // custom lookpath function so we know if a command is found *and* is executable
|
||||
var skip []string
|
||||
if runtime.GOOS == "windows" {
|
||||
skip = []string{"./", "../", "~/", "C:"}
|
||||
} else {
|
||||
skip = []string{"./", "/", "../", "~/"}
|
||||
}
|
||||
for _, s := range skip {
|
||||
if strings.HasPrefix(file, s) {
|
||||
return file, FindExecutable(file, false, false)
|
||||
}
|
||||
}
|
||||
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
|
||||
path := filepath.Join(dir, file)
|
||||
err := FindExecutable(path, true, false)
|
||||
if err == ErrNotExec {
|
||||
return "", err
|
||||
} else if err == nil {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
|
||||
func Contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if strings.ToLower(a) == strings.ToLower(e) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func HandleExecErr(err error) (exit uint8) {
|
||||
ctx := context.TODO()
|
||||
|
||||
switch x := err.(type) {
|
||||
case *exec.ExitError:
|
||||
// started, but errored - default to 1 if OS
|
||||
// doesn't have exit statuses
|
||||
if status, ok := x.Sys().(syscall.WaitStatus); ok {
|
||||
if status.Signaled() {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
exit = uint8(128 + status.Signal())
|
||||
return
|
||||
}
|
||||
exit = uint8(status.ExitStatus())
|
||||
return
|
||||
}
|
||||
exit = 1
|
||||
return
|
||||
case *exec.Error:
|
||||
// did not start
|
||||
//fmt.Fprintf(hc.Stderr, "%v\n", err)
|
||||
exit = 127
|
||||
default: return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
4
vars.go
4
vars.go
@ -11,8 +11,8 @@ var (
|
||||
|
||||
// Version info
|
||||
var (
|
||||
ver = "v2.4.0"
|
||||
releaseName = "Moonflower"
|
||||
ver = "v2.3.1"
|
||||
releaseName = "Alyssum"
|
||||
|
||||
gitCommit string
|
||||
gitBranch string
|
||||
|
@ -15,5 +15,7 @@ var (
|
||||
.. hilbish.userDir.config .. '/hilbish/?/?.lua;'
|
||||
.. hilbish.userDir.config .. '/hilbish/?.lua'`
|
||||
dataDir = "/usr/local/share/hilbish"
|
||||
preloadPath = dataDir + "/nature/init.lua"
|
||||
sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config
|
||||
defaultConfDir = getenv("XDG_CONFIG_HOME", "~/.config")
|
||||
)
|
||||
|
@ -14,6 +14,8 @@ var (
|
||||
.. hilbish.userDir.config .. '/hilbish/?/init.lua;'
|
||||
.. hilbish.userDir.config .. '/hilbish/?/?.lua;'
|
||||
.. hilbish.userDir.config .. '/hilbish/?.lua'`
|
||||
dataDir = ""
|
||||
dataDir = "/usr/local/share/hilbish"
|
||||
preloadPath = dataDir + "/nature/init.lua"
|
||||
sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config
|
||||
defaultConfDir = ""
|
||||
)
|
||||
|
@ -10,6 +10,8 @@ var (
|
||||
.. hilbish.userDir.config .. '\\Hilbish\\libs\\?\\init.lua;'
|
||||
.. hilbish.userDir.config .. '\\Hilbish\\libs\\?\\?.lua;'
|
||||
.. hilbish.userDir.config .. '\\Hilbish\\libs\\?.lua;'`
|
||||
dataDir = util.ExpandHome("~\\Appdata\\Roaming\\Hilbish") // ~ and \, gonna cry?
|
||||
dataDir = util.ExpandHome("~\\Appdata\\Roaming\\Hilbish") // ~ and \ gonna cry?
|
||||
preloadPath = dataDir + "\\nature\\init.lua"
|
||||
sampleConfPath = dataDir + "\\.hilbishrc.lua" // Path to default/sample config
|
||||
defaultConfDir = ""
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user