mirror of
https://github.com/Hilbis/Hilbish
synced 2025-04-21 13:03:22 +00:00
Compare commits
No commits in common. "master" and "v2.3.0" have entirely different histories.
11
.github/workflows/docs.yml
vendored
11
.github/workflows/docs.yml
vendored
@ -10,18 +10,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
- name: Download Task
|
- name: Run docgen
|
||||||
run: 'sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d'
|
|
||||||
- name: Build
|
|
||||||
run: ./bin/task
|
|
||||||
- name: Run docgen (go-written)
|
|
||||||
run: go run cmd/docgen/docgen.go
|
run: go run cmd/docgen/docgen.go
|
||||||
- name: Run docgen (lua-written)
|
|
||||||
run: ./hilbish cmd/docgen/docgen.lua
|
|
||||||
- name: Commit new docs
|
- name: Commit new docs
|
||||||
uses: stefanzweifel/git-auto-commit-action@v4
|
uses: stefanzweifel/git-auto-commit-action@v4
|
||||||
with:
|
with:
|
||||||
|
2
.github/workflows/website.yml
vendored
2
.github/workflows/website.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set branch name
|
- name: Set branch name
|
||||||
id: branch
|
id: branch
|
||||||
run: echo "BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/*/}}" >> "$GITHUB_ENV"
|
run: echo "BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Fix base URL
|
- name: Fix base URL
|
||||||
if: env.BRANCH_NAME != 'master' && github.repository_owner == 'Rosettea'
|
if: env.BRANCH_NAME != 'master' && github.repository_owner == 'Rosettea'
|
||||||
|
36
CHANGELOG.md
36
CHANGELOG.md
@ -1,37 +1,5 @@
|
|||||||
# 🎀 Changelog
|
# 🎀 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.
|
|
||||||
Displayed tips can be modified via the `hilbish.tips` table.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Fix a minor regression related to the cd command not working with relative paths
|
|
||||||
- Updated the motd for 2.3
|
|
||||||
|
|
||||||
## [2.3.0] - 2024-07-20
|
## [2.3.0] - 2024-07-20
|
||||||
### Added
|
### Added
|
||||||
- `commander.registry` function to get all registered commanders.
|
- `commander.registry` function to get all registered commanders.
|
||||||
@ -790,10 +758,6 @@ This input for example will prompt for more input to complete:
|
|||||||
|
|
||||||
First "stable" release of Hilbish.
|
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.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
|
[2.2.3]: https://github.com/Rosettea/Hilbish/compare/v2.2.2...v2.2.3
|
||||||
[2.2.2]: https://github.com/Rosettea/Hilbish/compare/v2.2.1...v2.2.2
|
[2.2.2]: https://github.com/Rosettea/Hilbish/compare/v2.2.1...v2.2.2
|
||||||
|
18
README.md
18
README.md
@ -13,23 +13,19 @@
|
|||||||
<br>
|
<br>
|
||||||
|
|
||||||
Hilbish is an extensible shell designed to be highly customizable.
|
Hilbish is an extensible shell designed to be highly customizable.
|
||||||
|
It is configured in Lua and provides a good range of features.
|
||||||
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
|
||||||
It aims to be easy to use for anyone, and powerful enough for
|
those who need it.
|
||||||
those who need more.
|
|
||||||
|
|
||||||
The motivation for choosing Lua was that its simpler and better to use
|
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,
|
than old shell script. It's fine for basic interactive shell uses,
|
||||||
and supports [both Lua and Sh interactively](https://rosettea.github.io/Hilbish/docs/features/runner-mode/).
|
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!
|
||||||
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!
|
|
||||||
|
|
||||||
# Screenshots
|
# Screenshots
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="gallery/tab.png">
|
<img src="gallery/tab.png">
|
||||||
|
<img src="gallery/pillprompt.png">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
# Getting Hilbish
|
# Getting Hilbish
|
||||||
|
152
api.go
152
api.go
@ -13,9 +13,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
//"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -27,9 +28,9 @@ import (
|
|||||||
|
|
||||||
rt "github.com/arnodel/golua/runtime"
|
rt "github.com/arnodel/golua/runtime"
|
||||||
"github.com/arnodel/golua/lib/packagelib"
|
"github.com/arnodel/golua/lib/packagelib"
|
||||||
//"github.com/arnodel/golua/lib/iolib"
|
"github.com/arnodel/golua/lib/iolib"
|
||||||
"github.com/maxlandon/readline"
|
"github.com/maxlandon/readline"
|
||||||
//"mvdan.cc/sh/v3/interp"
|
"mvdan.cc/sh/v3/interp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var exports = map[string]util.LuaExport{
|
var exports = map[string]util.LuaExport{
|
||||||
@ -38,6 +39,7 @@ var exports = map[string]util.LuaExport{
|
|||||||
"complete": {hlcomplete, 2, false},
|
"complete": {hlcomplete, 2, false},
|
||||||
"cwd": {hlcwd, 0, false},
|
"cwd": {hlcwd, 0, false},
|
||||||
"exec": {hlexec, 1, false},
|
"exec": {hlexec, 1, false},
|
||||||
|
"runnerMode": {hlrunnerMode, 1, false},
|
||||||
"goro": {hlgoro, 1, true},
|
"goro": {hlgoro, 1, true},
|
||||||
"highlighter": {hlhighlighter, 1, false},
|
"highlighter": {hlhighlighter, 1, false},
|
||||||
"hinter": {hlhinter, 1, false},
|
"hinter": {hlhinter, 1, false},
|
||||||
@ -47,6 +49,7 @@ var exports = map[string]util.LuaExport{
|
|||||||
"inputMode": {hlinputMode, 1, false},
|
"inputMode": {hlinputMode, 1, false},
|
||||||
"interval": {hlinterval, 2, false},
|
"interval": {hlinterval, 2, false},
|
||||||
"read": {hlread, 1, false},
|
"read": {hlread, 1, false},
|
||||||
|
"run": {hlrun, 1, true},
|
||||||
"timeout": {hltimeout, 2, false},
|
"timeout": {hltimeout, 2, false},
|
||||||
"which": {hlwhich, 1, false},
|
"which": {hlwhich, 1, false},
|
||||||
}
|
}
|
||||||
@ -131,9 +134,6 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) {
|
|||||||
pluginModule := moduleLoader(rtm)
|
pluginModule := moduleLoader(rtm)
|
||||||
mod.Set(rt.StringValue("module"), rt.TableValue(pluginModule))
|
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
|
return rt.TableValue(mod), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +154,6 @@ func unsetVimMode() {
|
|||||||
util.SetField(l, hshMod, "vimMode", rt.NilValue)
|
util.SetField(l, hshMod, "vimMode", rt.NilValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
func handleStream(v rt.Value, strms *streams, errStream bool) error {
|
func handleStream(v rt.Value, strms *streams, errStream bool) error {
|
||||||
ud, ok := v.TryUserData()
|
ud, ok := v.TryUserData()
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -183,7 +182,112 @@ func handleStream(v rt.Value, strms *streams, errStream bool) error {
|
|||||||
|
|
||||||
return nil
|
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
|
// cwd() -> string
|
||||||
// Returns the current directory of the shell.
|
// Returns the current directory of the shell.
|
||||||
@ -300,7 +404,7 @@ hilbish.multiprompt '-->'
|
|||||||
*/
|
*/
|
||||||
func hlmultiprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
func hlmultiprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||||
if err := c.Check1Arg(); err != nil {
|
if err := c.Check1Arg(); err != nil {
|
||||||
return c.PushingNext1(t.Runtime, rt.StringValue(multilinePrompt)), nil
|
return nil, err
|
||||||
}
|
}
|
||||||
prompt, err := c.StringArg(0)
|
prompt, err := c.StringArg(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -404,7 +508,7 @@ func hlexec(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
}
|
}
|
||||||
cmdArgs, _ := splitInput(cmd)
|
cmdArgs, _ := splitInput(cmd)
|
||||||
if runtime.GOOS != "windows" {
|
if runtime.GOOS != "windows" {
|
||||||
cmdPath, err := util.LookPath(cmdArgs[0])
|
cmdPath, err := exec.LookPath(cmdArgs[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
// if we get here, cmdPath will be nothing
|
// 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
|
return c.PushingNext1(t.Runtime, rt.StringValue(cmd)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
path, err := util.LookPath(cmd)
|
path, err := exec.LookPath(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Next(), nil
|
return c.Next(), nil
|
||||||
}
|
}
|
||||||
@ -638,6 +742,34 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
return c.Next(), nil
|
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)
|
// hinter(line, pos)
|
||||||
// The command line hint handler. It gets called on every key insert to
|
// 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
|
// 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",
|
"commander": "c",
|
||||||
"bait": "b",
|
"bait": "b",
|
||||||
"terminal": "term",
|
"terminal": "term",
|
||||||
"snail": "snail",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTagsAndDocs(docs string) (map[string][]tag, []string) {
|
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 {
|
func setupDoc(mod string, fun *doc.Func) *docPiece {
|
||||||
if fun.Doc == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
docs := strings.TrimSpace(fun.Doc)
|
docs := strings.TrimSpace(fun.Doc)
|
||||||
tags, parts := getTagsAndDocs(docs)
|
tags, parts := getTagsAndDocs(docs)
|
||||||
|
|
||||||
@ -304,28 +299,10 @@ start:
|
|||||||
func main() {
|
func main() {
|
||||||
fset := token.NewFileSet()
|
fset := token.NewFileSet()
|
||||||
os.Mkdir("docs", 0777)
|
os.Mkdir("docs", 0777)
|
||||||
os.RemoveAll("docs/api")
|
|
||||||
os.Mkdir("docs/api", 0777)
|
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)
|
os.Mkdir("emmyLuaDocs", 0777)
|
||||||
|
|
||||||
dirs := []string{"./", "./util"}
|
dirs := []string{"./"}
|
||||||
filepath.Walk("golibs/", func (path string, info os.FileInfo, err error) error {
|
filepath.Walk("golibs/", func (path string, info os.FileInfo, err error) error {
|
||||||
if !info.IsDir() {
|
if !info.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
@ -352,7 +329,7 @@ provided by Hilbish.
|
|||||||
pieces := []docPiece{}
|
pieces := []docPiece{}
|
||||||
typePieces := []docPiece{}
|
typePieces := []docPiece{}
|
||||||
mod := l
|
mod := l
|
||||||
if mod == "main" || mod == "util" {
|
if mod == "main" {
|
||||||
mod = "hilbish"
|
mod = "hilbish"
|
||||||
}
|
}
|
||||||
var hasInterfaces bool
|
var hasInterfaces bool
|
||||||
@ -436,14 +413,6 @@ provided by Hilbish.
|
|||||||
interfaceModules[modname].Types = append(interfaceModules[modname].Types, piece)
|
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{
|
docs[mod] = module{
|
||||||
Types: filteredTypePieces,
|
Types: filteredTypePieces,
|
||||||
Docs: filteredPieces,
|
Docs: filteredPieces,
|
||||||
@ -454,7 +423,6 @@ provided by Hilbish.
|
|||||||
Fields: docPieceTag("field", tags),
|
Fields: docPieceTag("field", tags),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for key, mod := range interfaceModules {
|
for key, mod := range interfaceModules {
|
||||||
docs[key] = *mod
|
docs[key] = *mod
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
local fs = require 'fs'
|
local fs = require 'fs'
|
||||||
local emmyPattern = '^%-%-%- (.+)'
|
local emmyPattern = '^%-%-%- (.+)'
|
||||||
local emmyPattern2 = '^%-%- (.+)'
|
local modpattern = '^%-+ @module (%w+)'
|
||||||
local modpattern = '^%-+ @module (.+)'
|
|
||||||
local pieces = {}
|
local pieces = {}
|
||||||
local descriptions = {}
|
|
||||||
|
|
||||||
local files = fs.readdir 'nature'
|
local files = fs.readdir 'nature'
|
||||||
for _, fname in ipairs(files) do
|
for _, fname in ipairs(files) do
|
||||||
@ -15,25 +13,18 @@ for _, fname in ipairs(files) do
|
|||||||
local mod = header:match(modpattern)
|
local mod = header:match(modpattern)
|
||||||
if not mod then goto continue end
|
if not mod then goto continue end
|
||||||
|
|
||||||
|
print(fname, mod)
|
||||||
pieces[mod] = {}
|
pieces[mod] = {}
|
||||||
descriptions[mod] = {}
|
|
||||||
|
|
||||||
local docPiece = {}
|
local docPiece = {}
|
||||||
local lines = {}
|
local lines = {}
|
||||||
local lineno = 0
|
local lineno = 0
|
||||||
local doingDescription = true
|
|
||||||
|
|
||||||
for line in f:lines() do
|
for line in f:lines() do
|
||||||
lineno = lineno + 1
|
lineno = lineno + 1
|
||||||
lines[lineno] = line
|
lines[lineno] = line
|
||||||
|
|
||||||
if line == header then goto continue2 end
|
if line == header then goto continue2 end
|
||||||
if line:match(emmyPattern) or line:match(emmyPattern2) then
|
if not line:match(emmyPattern) then
|
||||||
if doingDescription then
|
|
||||||
table.insert(descriptions[mod], line:match(emmyPattern) or line:match(emmyPattern2))
|
|
||||||
end
|
|
||||||
else
|
|
||||||
doingDescription = false
|
|
||||||
if line:match '^function' then
|
if line:match '^function' then
|
||||||
local pattern = (string.format('^function %s%%.', mod) .. '(%w+)')
|
local pattern = (string.format('^function %s%%.', mod) .. '(%w+)')
|
||||||
local funcName = line:match(pattern)
|
local funcName = line:match(pattern)
|
||||||
@ -41,12 +32,10 @@ for _, fname in ipairs(files) do
|
|||||||
|
|
||||||
local dps = {
|
local dps = {
|
||||||
description = {},
|
description = {},
|
||||||
example = {},
|
|
||||||
params = {}
|
params = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
local offset = 1
|
local offset = 1
|
||||||
local doingExample = false
|
|
||||||
while true do
|
while true do
|
||||||
local prev = lines[lineno - offset]
|
local prev = lines[lineno - offset]
|
||||||
|
|
||||||
@ -62,31 +51,19 @@ for _, fname in ipairs(files) do
|
|||||||
if emmy == 'param' then
|
if emmy == 'param' then
|
||||||
table.insert(dps.params, 1, {
|
table.insert(dps.params, 1, {
|
||||||
name = emmythings[1],
|
name = emmythings[1],
|
||||||
type = emmythings[2],
|
type = emmythings[2]
|
||||||
-- the +1 accounts for space.
|
|
||||||
description = table.concat(emmythings, ' '):sub(emmythings[1]:len() + 1 + emmythings[2]:len() + 1)
|
|
||||||
})
|
})
|
||||||
end
|
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
|
else
|
||||||
table.insert(dps.description, 1, docline)
|
table.insert(dps.description, 1, docline)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
offset = offset + 1
|
offset = offset + 1
|
||||||
else
|
else
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
table.insert(pieces[mod], {funcName, dps})
|
pieces[mod][funcName] = dps
|
||||||
end
|
end
|
||||||
docPiece = {}
|
docPiece = {}
|
||||||
goto continue2
|
goto continue2
|
||||||
@ -104,82 +81,29 @@ description: %s
|
|||||||
layout: doc
|
layout: doc
|
||||||
menu:
|
menu:
|
||||||
docs:
|
docs:
|
||||||
parent: "%s"
|
parent: "Nature"
|
||||||
---
|
---
|
||||||
|
|
||||||
]]
|
]]
|
||||||
|
|
||||||
for iface, dps in pairs(pieces) do
|
for iface, dps in pairs(pieces) do
|
||||||
local mod = iface:match '(%w+)%.' or 'nature'
|
local mod = iface:match '(%w+)%.' or 'nature'
|
||||||
local docParent = 'Nature'
|
local path = string.format('docs/%s/%s.md', mod, iface)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
fs.mkdir(fs.dir(path), true)
|
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)
|
print(mod, path)
|
||||||
local newOrNotNature = (exists and mod ~= 'nature') or iface == 'hilbish'
|
|
||||||
|
|
||||||
local f <close> = io.open(path, newOrNotNature and 'r+' or 'w+')
|
for func, docs in pairs(dps) do
|
||||||
local tocPos
|
f:write(string.format('<hr>\n<div id=\'%s\'>', func))
|
||||||
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]
|
|
||||||
local sig = string.format('%s.%s(', iface, func)
|
local sig = string.format('%s.%s(', iface, func)
|
||||||
local params = ''
|
|
||||||
for idx, param in ipairs(docs.params) do
|
for idx, param in ipairs(docs.params) do
|
||||||
sig = sig .. param.name:gsub('%?$', '')
|
sig = sig .. ((param.name:gsub('%?$', '')))
|
||||||
params = params .. param.name:gsub('%?$', '')
|
if idx ~= #docs.params then sig = sig .. ', ' end
|
||||||
if idx ~= #docs.params then
|
|
||||||
sig = sig .. ', '
|
|
||||||
params = params .. ', '
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
sig = sig .. ')'
|
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([[
|
f:write(string.format([[
|
||||||
<h4 class='heading'>
|
<h4 class='heading'>
|
||||||
%s
|
%s
|
||||||
@ -196,12 +120,7 @@ for iface, dps in pairs(pieces) do
|
|||||||
f:write 'This function has no parameters. \n'
|
f:write 'This function has no parameters. \n'
|
||||||
end
|
end
|
||||||
for _, param in ipairs(docs.params) do
|
for _, param in ipairs(docs.params) do
|
||||||
f:write(string.format('`%s` **`%s`** \n', param.name:gsub('%?$', ''), param.type))
|
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')))
|
|
||||||
end
|
end
|
||||||
--[[
|
--[[
|
||||||
local params = table.filter(docs, function(t)
|
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 {
|
if len(fileCompletions) != 0 {
|
||||||
for _, f := range fileCompletions {
|
for _, f := range fileCompletions {
|
||||||
fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref)))
|
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
|
continue
|
||||||
}
|
}
|
||||||
completions = append(completions, f)
|
completions = append(completions, f)
|
||||||
@ -115,7 +115,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
|
|||||||
// get basename from matches
|
// get basename from matches
|
||||||
for _, match := range matches {
|
for _, match := range matches {
|
||||||
// check if we have execute permissions for our match
|
// check if we have execute permissions for our match
|
||||||
err := util.FindExecutable(match, true, false)
|
err := findExecutable(match, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -157,12 +157,9 @@ func matchPath(query string) ([]string, string) {
|
|||||||
|
|
||||||
files, _ := os.ReadDir(path)
|
files, _ := os.ReadDir(path)
|
||||||
for _, entry := range files {
|
for _, entry := range files {
|
||||||
|
// should we handle errors here?
|
||||||
file, err := entry.Info()
|
file, err := entry.Info()
|
||||||
if err != nil {
|
if err == nil && file.Mode() & os.ModeSymlink != 0 {
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if file.Mode() & os.ModeSymlink != 0 {
|
|
||||||
path, err := filepath.EvalSymlinks(filepath.Join(path, file.Name()))
|
path, err := filepath.EvalSymlinks(filepath.Join(path, file.Name()))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
file, err = os.Lstat(path)
|
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="#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="#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="#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="#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="#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
|
## Static module fields
|
||||||
|||
|
|||
|
||||||
@ -408,6 +408,72 @@ Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs.
|
|||||||
`string` **`prompt?`**
|
`string` **`prompt?`**
|
||||||
Text to print before input, can be empty.
|
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>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
@ -453,7 +519,8 @@ Will return the path of the binary, or a basename if it's a commander.
|
|||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
## Sink
|
## 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
|
### Methods
|
||||||
#### autoFlush(auto)
|
#### autoFlush(auto)
|
||||||
@ -475,65 +542,3 @@ Writes data to a sink.
|
|||||||
#### writeln(str)
|
#### writeln(str)
|
||||||
Writes data to a sink with a newline at the end.
|
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
|
## 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.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.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.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.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.|
|
|<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>
|
<hr>
|
||||||
<div id='editor.getLine'>
|
<div id='editor.getLine'>
|
||||||
<h4 class='heading'>
|
<h4 class='heading'>
|
||||||
@ -114,9 +96,6 @@ hilbish.editor.setVimRegister(register, text)
|
|||||||
Sets the vim register at `register` to hold the passed text.
|
Sets the vim register at `register` to hold the passed text.
|
||||||
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
`string` **`register`**
|
|
||||||
|
|
||||||
|
|
||||||
`string` **`text`**
|
`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.
|
All are not required, only the useful ones the runner needs to return.
|
||||||
(So if there isn't an error, just omit `err`.)
|
(So if there isn't an error, just omit `err`.)
|
||||||
|
|
||||||
- `exitCode` (number): Exit code of the command
|
- `exitCode` (number): A numerical code to indicate the exit result.
|
||||||
- `input` (string): The text input of the user. This is used by Hilbish to append extra input, in case
|
- `input` (string): The user input. This will be used to add
|
||||||
more is requested.
|
to the history.
|
||||||
- `err` (string): A string that represents an error from the runner.
|
- `err` (string): A string to indicate an interal error for 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:
|
||||||
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-found` will throw a `command.not-found` hook
|
|
||||||
based on what `\<command>` is.
|
`[command]: not-executable` will throw a command.not-executable hook.
|
||||||
- `\<command>: not-executable` will throw a `command.not-executable` hook.
|
- `continue` (boolean): Whether to prompt the user for more input.
|
||||||
- `continue` (boolean): Whether Hilbish should prompt the user for no input
|
|
||||||
- `newline` (boolean): Whether a newline should be added at the end of `input`.
|
|
||||||
|
|
||||||
Here is a simple example of a fennel runner. It falls back to
|
Here is a simple example of a fennel runner. It falls back to
|
||||||
shell script if fennel eval has an error.
|
shell script if fennel eval has an error.
|
||||||
@ -54,16 +52,29 @@ end)
|
|||||||
## Functions
|
## 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="#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="#runner.sh">sh(cmd)</a>|Runs a command in Hilbish's shell script interpreter.|
|
||||||
|<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.|
|
<hr>
|
||||||
|<a href="#set">set(name, runner)</a>|*Sets* a runner by name. The difference between this function and|
|
<div id='runner.setMode'>
|
||||||
|<a href="#run">run(input, priv)</a>|Runs `input` with the currently set Hilbish runner.|
|
<h4 class='heading'>
|
||||||
|<a href="#getCurrent">getCurrent()</a>|Returns the current runner by name.|
|
hilbish.runner.setMode(cb)
|
||||||
|<a href="#get">get(name)</a>|Get a runner by name.|
|
<a href="#runner.setMode" class='heading-link'>
|
||||||
|<a href="#exec">exec(cmd, runnerName)</a>|Executes `cmd` with a runner.|
|
<i class="fas fa-paperclip"></i>
|
||||||
|<a href="#add">add(name, runner)</a>|Adds a runner to the table of available runners.|
|
</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>
|
<hr>
|
||||||
<div id='runner.lua'>
|
<div id='runner.lua'>
|
||||||
@ -84,164 +95,20 @@ or `load`, but is appropriated for the runner interface.
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<div id='add'>
|
<div id='runner.sh'>
|
||||||
<h4 class='heading'>
|
<h4 class='heading'>
|
||||||
hilbish.runner.add(name, runner)
|
hilbish.runner.sh(cmd)
|
||||||
<a href="#add" class='heading-link'>
|
<a href="#runner.sh" class='heading-link'>
|
||||||
<i class="fas fa-paperclip"></i>
|
<i class="fas fa-paperclip"></i>
|
||||||
</a>
|
</a>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
Adds a runner to the table of available runners.
|
Runs a command in Hilbish's shell script interpreter.
|
||||||
If runner is a table, it must have the run function in it.
|
This is the equivalent of using `source`.
|
||||||
#### Parameters
|
|
||||||
`name` **`string`**
|
|
||||||
Name of the runner
|
|
||||||
|
|
||||||
`runner` **`function|table`**
|
#### Parameters
|
||||||
|
`string` **`cmd`**
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</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`
|
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.
|
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
|
## Functions
|
||||||
These are the "low level" functions for the `hilbish.runner` interface.
|
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:
|
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`
|
`cp /usr/share/hilbish/.hilbishrc.lua ~/.config/hilbish/init.lua`
|
||||||
|
|
||||||
Now we can get to customization!
|
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
|
||||||
If we closely examine a small snippet of the default config:
|
from Lua 5.4 are available. Hilbish has some custom and modules that are
|
||||||
```lua
|
available. To see them, you can run the `doc` command. This also works as
|
||||||
-- Default Hilbish config
|
general documentation for other things.
|
||||||
-- .. 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.
|
|
||||||
|
@ -43,29 +43,5 @@ The notification. The properties are defined in the link above.
|
|||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
## hilbish.cd
|
+ `hilbish.vimAction` -> actionName, args > Sent when the user does a "vim action," being something
|
||||||
Sent when the current directory of the shell is changed (via interactive means.)
|
like yanking or pasting text. See `doc vim-mode actions` for more info.
|
||||||
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>
|
|
||||||
|
@ -1,25 +1,40 @@
|
|||||||
---
|
---
|
||||||
title: Module dirs
|
title: Module dirs
|
||||||
description: internal directory management
|
description: No description.
|
||||||
layout: doc
|
layout: doc
|
||||||
menu:
|
menu:
|
||||||
docs:
|
docs:
|
||||||
parent: "Nature"
|
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
|
Sets the old directory string.
|
||||||
The dirs module defines a small set of functions to store and manage
|
#### Parameters
|
||||||
directories.
|
`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>
|
<hr>
|
||||||
<div id='peak'>
|
<div id='peak'>
|
||||||
<h4 class='heading'>
|
<h4 class='heading'>
|
||||||
@ -30,11 +45,8 @@ dirs.peak(num)
|
|||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
Look at `num` amount of recent directories, starting from the latest.
|
Look at `num` amount of recent directories, starting from the latest.
|
||||||
This returns a table of recent directories, up to the `num` amount.
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
`num` **`number`**
|
`num` **`number`**
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
@ -49,24 +61,6 @@ dirs.pop(num)
|
|||||||
Remove the specified amount of dirs from the recent directories list.
|
Remove the specified amount of dirs from the recent directories list.
|
||||||
#### Parameters
|
#### Parameters
|
||||||
`num` **`number`**
|
`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>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
@ -81,23 +75,5 @@ dirs.recent(idx)
|
|||||||
Get entry from recent directories list based on index.
|
Get entry from recent directories list based on index.
|
||||||
#### Parameters
|
#### Parameters
|
||||||
`idx` **`number`**
|
`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>
|
</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},
|
"getVimRegister": {editorGetRegister, 2, false},
|
||||||
"getLine": {editorGetLine, 0, false},
|
"getLine": {editorGetLine, 0, false},
|
||||||
"readChar": {editorReadChar, 0, false},
|
"readChar": {editorReadChar, 0, false},
|
||||||
"deleteByAmount": {editorDeleteByAmount, 1, false},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mod := rt.NewTable()
|
mod := rt.NewTable()
|
||||||
@ -48,7 +47,7 @@ func editorInsert(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
// #interface editor
|
// #interface editor
|
||||||
// setVimRegister(register, text)
|
// setVimRegister(register, text)
|
||||||
// Sets the vim register at `register` to hold the passed text.
|
// Sets the vim register at `register` to hold the passed text.
|
||||||
// #param register string
|
// #aram register string
|
||||||
// #param text string
|
// #param text string
|
||||||
func editorSetRegister(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
func editorSetRegister(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||||
if err := c.Check1Arg(); err != nil {
|
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
|
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
|
--- @param cmd string
|
||||||
function hilbish.aliases.add(alias, cmd) end
|
function hilbish.aliases.add(alias, cmd) end
|
||||||
|
|
||||||
--- Deletes characters in the line by the given amount.
|
--- This is the same as the `hilbish.runnerMode` function.
|
||||||
function hilbish.editor.deleteByAmount(amount) end
|
--- 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.
|
--- Returns the current input line.
|
||||||
function hilbish.editor.getLine() end
|
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.
|
--- Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs.
|
||||||
function hilbish.read(prompt) end
|
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`.
|
--- Executed the `cb` function after a period of `time`.
|
||||||
--- This creates a Timer that starts ticking immediately.
|
--- This creates a Timer that starts ticking immediately.
|
||||||
function hilbish.timeout(cb, time) end
|
function hilbish.timeout(cb, time) end
|
||||||
@ -147,6 +168,28 @@ function hilbish.jobs:foreground() end
|
|||||||
--- or `load`, but is appropriated for the runner interface.
|
--- or `load`, but is appropriated for the runner interface.
|
||||||
function hilbish.runner.lua(cmd) end
|
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.
|
--- Starts running the job.
|
||||||
function hilbish.jobs:start() end
|
function hilbish.jobs:start() end
|
||||||
|
|
||||||
@ -157,6 +200,10 @@ function hilbish.jobs:stop() end
|
|||||||
--- It will throw if any error occurs.
|
--- It will throw if any error occurs.
|
||||||
function hilbish.module.load(path) end
|
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.
|
--- Starts a timer.
|
||||||
function hilbish.timers:start() end
|
function hilbish.timers:start() end
|
||||||
|
|
||||||
@ -215,26 +262,4 @@ function hilbish.timers.create(type, time, callback) end
|
|||||||
--- Retrieves a timer via its ID.
|
--- Retrieves a timer via its ID.
|
||||||
function hilbish.timers.get(id) end
|
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
|
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
|
|
535
exec.go
535
exec.go
@ -1,26 +1,208 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"os/exec"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hilbish/util"
|
||||||
|
|
||||||
rt "github.com/arnodel/golua/runtime"
|
rt "github.com/arnodel/golua/runtime"
|
||||||
|
"mvdan.cc/sh/v3/shell"
|
||||||
//"github.com/yuin/gopher-lua/parse"
|
//"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 errNotExec = errors.New("not executable")
|
||||||
var errNotFound = errors.New("not found")
|
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) {
|
func runInput(input string, priv bool) {
|
||||||
running = true
|
running = true
|
||||||
runnerRun := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("run"))
|
cmdString := aliases.Resolve(input)
|
||||||
_, err := rt.Call1(l.MainThread(), runnerRun, rt.StringValue(input), rt.BoolValue(priv))
|
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 {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
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) {
|
func handleLua(input string) (string, uint8, error) {
|
||||||
@ -50,13 +232,336 @@ func handleLua(input string) (string, uint8, error) {
|
|||||||
return cmdString, 125, err
|
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) {
|
func splitInput(input string) ([]string, string) {
|
||||||
// end my suffering
|
// end my suffering
|
||||||
// TODO: refactor this garbage
|
// TODO: refactor this garbage
|
||||||
quoted := false
|
quoted := false
|
||||||
|
startlastcmd := false
|
||||||
|
lastcmddone := false
|
||||||
cmdArgs := []string{}
|
cmdArgs := []string{}
|
||||||
sb := &strings.Builder{}
|
sb := &strings.Builder{}
|
||||||
cmdstr := &strings.Builder{}
|
cmdstr := &strings.Builder{}
|
||||||
|
lastcmd := "" //readline.GetHistory(readline.HistorySize() - 1)
|
||||||
|
|
||||||
for _, r := range input {
|
for _, r := range input {
|
||||||
if r == '"' {
|
if r == '"' {
|
||||||
@ -72,6 +577,22 @@ func splitInput(input string) ([]string, string) {
|
|||||||
// if not quoted and there's a space then add to cmdargs
|
// if not quoted and there's a space then add to cmdargs
|
||||||
cmdArgs = append(cmdArgs, sb.String())
|
cmdArgs = append(cmdArgs, sb.String())
|
||||||
sb.Reset()
|
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 {
|
} else {
|
||||||
sb.WriteRune(r)
|
sb.WriteRune(r)
|
||||||
}
|
}
|
||||||
@ -83,3 +604,11 @@ func splitInput(input string) ([]string, string) {
|
|||||||
|
|
||||||
return cmdArgs, cmdstr.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
|
//go:build unix
|
||||||
|
|
||||||
package util
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"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)
|
f, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -20,5 +25,5 @@ func FindExecutable(path string, inPath, dirs bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ErrNotExec
|
return errNotExec
|
||||||
}
|
}
|
@ -1,13 +1,18 @@
|
|||||||
//go:build windows
|
//go:build windows
|
||||||
|
|
||||||
package util
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"os"
|
"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)
|
nameExt := filepath.Ext(path)
|
||||||
pathExts := filepath.SplitList(os.Getenv("PATHEXT"))
|
pathExts := filepath.SplitList(os.Getenv("PATHEXT"))
|
||||||
if inPath {
|
if inPath {
|
||||||
@ -21,15 +26,15 @@ func FindExecutable(path string, inPath, dirs bool) error {
|
|||||||
} else {
|
} else {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if Contains(pathExts, nameExt) { return nil }
|
if contains(pathExts, nameExt) { return nil }
|
||||||
return ErrNotExec
|
return errNotExec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if Contains(pathExts, nameExt) { return nil }
|
if contains(pathExts, nameExt) { return nil }
|
||||||
return ErrNotExec
|
return errNotExec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
go.mod
4
go.mod
@ -28,10 +28,10 @@ require (
|
|||||||
golang.org/x/text v0.14.0 // indirect
|
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 github.com/maxlandon/readline => ./readline
|
||||||
|
|
||||||
replace layeh.com/gopher-luar => github.com/layeh/gopher-luar v1.0.10
|
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-20240427174124-d239074c1749 h1:jIFnWBTsYw8s7RX7H2AOXjDVhWP3ol7OzUVaPN2KnGI=
|
||||||
github.com/Rosettea/golua v0.0.0-20241104031959-5551ea280f23/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE=
|
github.com/Rosettea/golua v0.0.0-20240427174124-d239074c1749/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.20240720131751-805c301321fd h1:THNle0FR2g7DMO1y3Bx1Zr7rYeiLXt3st3UkxEsMzL4=
|
||||||
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240815163633-562273e09b73/go.mod h1:YZalN5H7WNQw3DGij6IvHsEhn5YMW7M2FCwG6gnfKy4=
|
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 h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
||||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
|
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=
|
github.com/arnodel/strftime v0.1.6 h1:0hc0pUvk8KhEMXE+htyaOUV42zNcf/csIbjzEFCJqsw=
|
||||||
|
@ -19,25 +19,38 @@ import (
|
|||||||
rt "github.com/arnodel/golua/runtime"
|
rt "github.com/arnodel/golua/runtime"
|
||||||
"github.com/arnodel/golua/lib/packagelib"
|
"github.com/arnodel/golua/lib/packagelib"
|
||||||
"github.com/arnodel/golua/lib/iolib"
|
"github.com/arnodel/golua/lib/iolib"
|
||||||
|
"mvdan.cc/sh/v3/interp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Loader = packagelib.Loader{
|
type fs struct{
|
||||||
Load: loaderFunc,
|
runner *interp.Runner
|
||||||
Name: "fs",
|
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{
|
exports := map[string]util.LuaExport{
|
||||||
"cd": util.LuaExport{fcd, 1, false},
|
"cd": util.LuaExport{f.fcd, 1, false},
|
||||||
"mkdir": util.LuaExport{fmkdir, 2, false},
|
"mkdir": util.LuaExport{f.fmkdir, 2, false},
|
||||||
"stat": util.LuaExport{fstat, 1, false},
|
"stat": util.LuaExport{f.fstat, 1, false},
|
||||||
"readdir": util.LuaExport{freaddir, 1, false},
|
"readdir": util.LuaExport{f.freaddir, 1, false},
|
||||||
"abs": util.LuaExport{fabs, 1, false},
|
"abs": util.LuaExport{f.fabs, 1, false},
|
||||||
"basename": util.LuaExport{fbasename, 1, false},
|
"basename": util.LuaExport{f.fbasename, 1, false},
|
||||||
"dir": util.LuaExport{fdir, 1, false},
|
"dir": util.LuaExport{f.fdir, 1, false},
|
||||||
"glob": util.LuaExport{fglob, 1, false},
|
"glob": util.LuaExport{f.fglob, 1, false},
|
||||||
"join": util.LuaExport{fjoin, 0, true},
|
"join": util.LuaExport{f.fjoin, 0, true},
|
||||||
"pipe": util.LuaExport{fpipe, 0, false},
|
"pipe": util.LuaExport{f.fpipe, 0, false},
|
||||||
}
|
}
|
||||||
mod := rt.NewTable()
|
mod := rt.NewTable()
|
||||||
util.SetExports(rtm, mod, exports)
|
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`.
|
// This can be used to resolve short paths like `..` to `/home/user`.
|
||||||
// #param path string
|
// #param path string
|
||||||
// #returns 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)
|
path, err := c.StringArg(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -72,7 +85,7 @@ func fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
// `.` will be returned.
|
// `.` will be returned.
|
||||||
// #param path string Path to get the base name of.
|
// #param path string Path to get the base name of.
|
||||||
// #returns string
|
// #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 {
|
if err := c.Check1Arg(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -87,7 +100,7 @@ func fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
// cd(dir)
|
// cd(dir)
|
||||||
// Changes Hilbish's directory to `dir`.
|
// Changes Hilbish's directory to `dir`.
|
||||||
// #param dir string Path to change directory to.
|
// #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 {
|
if err := c.Check1Arg(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -101,6 +114,7 @@ func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
interp.Dir(path)(f.runner)
|
||||||
|
|
||||||
return c.Next(), err
|
return c.Next(), err
|
||||||
}
|
}
|
||||||
@ -110,7 +124,7 @@ func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
// `~/Documents/doc.txt` then this function will return `~/Documents`.
|
// `~/Documents/doc.txt` then this function will return `~/Documents`.
|
||||||
// #param path string Path to get the directory for.
|
// #param path string Path to get the directory for.
|
||||||
// #returns string
|
// #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 {
|
if err := c.Check1Arg(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -141,7 +155,7 @@ print(matches)
|
|||||||
-- -> {'init.lua', 'code.lua'}
|
-- -> {'init.lua', 'code.lua'}
|
||||||
#example
|
#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 {
|
if err := c.Check1Arg(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -175,7 +189,7 @@ print(fs.join(hilbish.userDir.config, 'hilbish'))
|
|||||||
-- -> '/home/user/.config/hilbish' on Linux
|
-- -> '/home/user/.config/hilbish' on Linux
|
||||||
#example
|
#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()))
|
strs := make([]string, len(c.Etc()))
|
||||||
for i, v := range c.Etc() {
|
for i, v := range c.Etc() {
|
||||||
if v.Type() != rt.StringType {
|
if v.Type() != rt.StringType {
|
||||||
@ -202,7 +216,7 @@ func fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
fs.mkdir('./foo/bar', true)
|
fs.mkdir('./foo/bar', true)
|
||||||
#example
|
#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 {
|
if err := c.CheckNArgs(2); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -233,7 +247,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.
|
// The type returned is a Lua file, same as returned from `io` functions.
|
||||||
// #returns File
|
// #returns File
|
||||||
// #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()
|
rf, wf, err := os.Pipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -248,7 +262,7 @@ func fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
// Returns a list of all files and directories in the provided path.
|
// Returns a list of all files and directories in the provided path.
|
||||||
// #param dir string
|
// #param dir string
|
||||||
// #returns table
|
// #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 {
|
if err := c.Check1Arg(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -296,7 +310,7 @@ Would print the following:
|
|||||||
]]--
|
]]--
|
||||||
#example
|
#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 {
|
if err := c.Check1Arg(); err != nil {
|
||||||
return nil, err
|
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)
|
j.setHandle(&cmd)
|
||||||
}
|
}
|
||||||
// bgProcAttr is defined in job_<os>.go, it holds a procattr struct
|
// bgProcAttr is defined in execfile_<os>.go, it holds a procattr struct
|
||||||
// in a simple explanation, it makes signals from hilbish (like sigint)
|
// in a simple explanation, it makes signals from hilbish (sigint)
|
||||||
// not go to it (child process)
|
// not go to it (child process)
|
||||||
j.handle.SysProcAttr = bgProcAttr
|
j.handle.SysProcAttr = bgProcAttr
|
||||||
// reset output buffers
|
// reset output buffers
|
||||||
@ -136,7 +136,7 @@ func luaStartJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
|
|
||||||
if !j.running {
|
if !j.running {
|
||||||
err := j.start()
|
err := j.start()
|
||||||
exit := util.HandleExecErr(err)
|
exit := handleExecErr(err)
|
||||||
j.exitCode = int(exit)
|
j.exitCode = int(exit)
|
||||||
j.finish()
|
j.finish()
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,6 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
|
|
||||||
Setpgid: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *job) foreground() error {
|
func (j *job) foreground() error {
|
||||||
if jobs.foreground {
|
if jobs.foreground {
|
||||||
return errors.New("(another) job already foregrounded")
|
return errors.New("(another) job already foregrounded")
|
||||||
|
@ -4,13 +4,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"syscall"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
|
|
||||||
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *job) foreground() error {
|
func (j *job) foreground() error {
|
||||||
return errors.New("not supported on windows")
|
return errors.New("not supported on windows")
|
||||||
}
|
}
|
||||||
|
10
lua.go
10
lua.go
@ -3,13 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"hilbish/util"
|
"hilbish/util"
|
||||||
"hilbish/golibs/bait"
|
"hilbish/golibs/bait"
|
||||||
"hilbish/golibs/commander"
|
"hilbish/golibs/commander"
|
||||||
"hilbish/golibs/fs"
|
"hilbish/golibs/fs"
|
||||||
"hilbish/golibs/snail"
|
|
||||||
"hilbish/golibs/terminal"
|
"hilbish/golibs/terminal"
|
||||||
|
|
||||||
rt "github.com/arnodel/golua/runtime"
|
rt "github.com/arnodel/golua/runtime"
|
||||||
@ -25,14 +23,16 @@ func luaInit() {
|
|||||||
MessageHandler: debuglib.Traceback,
|
MessageHandler: debuglib.Traceback,
|
||||||
})
|
})
|
||||||
lib.LoadAll(l)
|
lib.LoadAll(l)
|
||||||
|
setupSinkType(l)
|
||||||
|
|
||||||
lib.LoadLibs(l, hilbishLoader)
|
lib.LoadLibs(l, hilbishLoader)
|
||||||
// yes this is stupid, i know
|
// yes this is stupid, i know
|
||||||
util.DoString(l, "hilbish = require 'hilbish'")
|
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, terminal.Loader)
|
||||||
lib.LoadLibs(l, snail.Loader)
|
|
||||||
|
|
||||||
cmds = commander.New(l)
|
cmds = commander.New(l)
|
||||||
lib.LoadLibs(l, cmds.Loader)
|
lib.LoadLibs(l, cmds.Loader)
|
||||||
@ -64,7 +64,7 @@ func luaInit() {
|
|||||||
|
|
||||||
err1 := util.DoFile(l, "nature/init.lua")
|
err1 := util.DoFile(l, "nature/init.lua")
|
||||||
if err1 != nil {
|
if err1 != nil {
|
||||||
err2 := util.DoFile(l, filepath.Join(dataDir, "nature", "init.lua"))
|
err2 := util.DoFile(l, preloadPath)
|
||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
fmt.Fprintln(os.Stderr, "Missing nature module, some functionality and builtins will be missing.")
|
fmt.Fprintln(os.Stderr, "Missing nature module, some functionality and builtins will be missing.")
|
||||||
fmt.Fprintln(os.Stderr, "local error:", err1)
|
fmt.Fprintln(os.Stderr, "local error:", err1)
|
||||||
|
55
main.go
55
main.go
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/pborman/getopt"
|
"github.com/pborman/getopt"
|
||||||
"github.com/maxlandon/readline"
|
"github.com/maxlandon/readline"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
|
"mvdan.cc/sh/v3/interp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -37,27 +38,16 @@ var (
|
|||||||
cmds *commander.Commander
|
cmds *commander.Commander
|
||||||
defaultConfPath string
|
defaultConfPath string
|
||||||
defaultHistPath string
|
defaultHistPath string
|
||||||
|
runner *interp.Runner
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if runtime.GOOS == "linux" {
|
runner, _ = interp.New()
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
curuser, _ = user.Current()
|
curuser, _ = user.Current()
|
||||||
|
homedir := curuser.HomeDir
|
||||||
confDir, _ = os.UserConfigDir()
|
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
|
// i honestly dont know what directories to use for this
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
@ -151,11 +141,10 @@ func main() {
|
|||||||
confpath := ".hilbishrc.lua"
|
confpath := ".hilbishrc.lua"
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If it wasnt found, go to the real sample conf
|
// If it wasnt found, go to the real sample conf
|
||||||
sampleConfigPath := filepath.Join(dataDir, ".hilbishrc.lua")
|
_, err = os.ReadFile(sampleConfPath)
|
||||||
_, err = os.ReadFile(sampleConfigPath)
|
confpath = sampleConfPath
|
||||||
confpath = sampleConfigPath
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("could not find .hilbishrc.lua or", sampleConfigPath)
|
fmt.Println("could not find .hilbishrc.lua or", sampleConfPath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,9 +223,8 @@ input:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasSuffix(input, "\\") {
|
if strings.HasSuffix(input, "\\") {
|
||||||
print("\n")
|
|
||||||
for {
|
for {
|
||||||
input, err = continuePrompt(strings.TrimSuffix(input, "\\") + "\n", false)
|
input, err = continuePrompt(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
running = true
|
running = true
|
||||||
lr.SetPrompt(fmtPrompt(prompt))
|
lr.SetPrompt(fmtPrompt(prompt))
|
||||||
@ -260,24 +248,16 @@ input:
|
|||||||
exit(0)
|
exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func continuePrompt(prev string, newline bool) (string, error) {
|
func continuePrompt(prev string) (string, error) {
|
||||||
hooks.Emit("multiline", nil)
|
hooks.Emit("multiline", nil)
|
||||||
lr.SetPrompt(multilinePrompt)
|
lr.SetPrompt(multilinePrompt)
|
||||||
|
|
||||||
cont, err := lr.Read()
|
cont, err := lr.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
cont = strings.TrimSpace(cont)
|
||||||
|
|
||||||
if newline {
|
return prev + strings.TrimSuffix(cont, "\n"), nil
|
||||||
cont = "\n" + cont
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(cont, "\\") {
|
|
||||||
cont = strings.TrimSuffix(cont, "\\") + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
return prev + cont, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This semi cursed function formats our prompt (obviously)
|
// This semi cursed function formats our prompt (obviously)
|
||||||
@ -324,6 +304,15 @@ func removeDupes(slice []string) []string {
|
|||||||
return newSlice
|
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) {
|
func exit(code int) {
|
||||||
jobs.stopAll()
|
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 fs = require 'fs'
|
||||||
local dirs = require 'nature.dirs'
|
local dirs = require 'nature.dirs'
|
||||||
|
|
||||||
|
dirs.old = hilbish.cwd()
|
||||||
commander.register('cd', function (args, sinks)
|
commander.register('cd', function (args, sinks)
|
||||||
local oldPath = hilbish.cwd()
|
|
||||||
|
|
||||||
if #args > 1 then
|
if #args > 1 then
|
||||||
sinks.out:writeln("cd: too many arguments")
|
sinks.out:writeln("cd: too many arguments")
|
||||||
return 1
|
return 1
|
||||||
@ -17,13 +16,13 @@ commander.register('cd', function (args, sinks)
|
|||||||
sinks.out:writeln(path)
|
sinks.out:writeln(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
local absPath = fs.abs(path)
|
dirs.setOld(hilbish.cwd())
|
||||||
|
dirs.push(path)
|
||||||
|
|
||||||
local ok, err = pcall(function() fs.cd(path) end)
|
local ok, err = pcall(function() fs.cd(path) end)
|
||||||
if not ok then
|
if not ok then
|
||||||
sinks.out:writeln(err)
|
sinks.out:writeln(err)
|
||||||
return 1
|
return 1
|
||||||
end
|
end
|
||||||
|
bait.throw('cd', path)
|
||||||
bait.throw('cd', path, oldPath)
|
|
||||||
bait.throw('hilbish.cd', absPath, oldPath)
|
|
||||||
end)
|
end)
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
-- @module dirs
|
-- @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 fs = require 'fs'
|
||||||
|
|
||||||
local dirs = {}
|
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 = ''
|
dirs.old = ''
|
||||||
--- Table of recent directories. For use, look at public functions.
|
--- Table of recent directories. For use, look at public functions.
|
||||||
dirs.recentDirs = {}
|
dirs.recentDirs = {}
|
||||||
@ -38,21 +35,19 @@ function dirRecents(num, remove)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Look at `num` amount of recent directories, starting from the latest.
|
--- 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
|
-- @param num? number
|
||||||
function dirs.peak(num)
|
function dirs.peak(num)
|
||||||
return dirRecents(num)
|
return dirRecents(num)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Add `dir` to the recent directories list.
|
--- Add `d` to the recent directories list.
|
||||||
--- @param dir string
|
function dirs.push(d)
|
||||||
function dirs.push(dir)
|
|
||||||
dirs.recentDirs[dirs.recentSize + 1] = nil
|
dirs.recentDirs[dirs.recentSize + 1] = nil
|
||||||
if dirs.recentDirs[#dirs.recentDirs - 1] ~= dir then
|
if dirs.recentDirs[#dirs.recentDirs - 1] ~= d then
|
||||||
local ok, dir = pcall(fs.abs, dir)
|
ok, d = pcall(fs.abs, d)
|
||||||
assert(ok, 'could not turn "' .. dir .. '"into an absolute path')
|
assert(ok, 'could not turn "' .. d .. '"into an absolute path')
|
||||||
|
|
||||||
table.insert(dirs.recentDirs, 1, dir)
|
table.insert(dirs.recentDirs, 1, d)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -78,9 +73,4 @@ function dirs.setOld(d)
|
|||||||
dirs.old = d
|
dirs.old = d
|
||||||
end
|
end
|
||||||
|
|
||||||
bait.catch('hilbish.cd', function(path, oldPath)
|
|
||||||
dirs.setOld(oldPath)
|
|
||||||
dirs.push(path)
|
|
||||||
end)
|
|
||||||
|
|
||||||
return dirs
|
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 lunacolors = require 'lunacolors'
|
||||||
|
|
||||||
local doc = {}
|
local M = {}
|
||||||
|
|
||||||
--- Performs basic Lua code highlighting.
|
function M.highlight(text)
|
||||||
--- @param text string Code/text to do highlighting on.
|
|
||||||
function doc.highlight(text)
|
|
||||||
return text:gsub('\'.-\'', lunacolors.yellow)
|
return text:gsub('\'.-\'', lunacolors.yellow)
|
||||||
--:gsub('%-%- .-', lunacolors.black)
|
--:gsub('%-%- .-', lunacolors.black)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Assembles and renders a code block. This returns
|
function M.renderCodeBlock(text)
|
||||||
--- 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)
|
|
||||||
local longest = 0
|
local longest = 0
|
||||||
local lines = string.split(text:gsub('\t', ' '), '\n')
|
local lines = string.split(text:gsub('\t', ' '), '\n')
|
||||||
|
|
||||||
@ -29,18 +17,14 @@ function doc.renderCodeBlock(text)
|
|||||||
end
|
end
|
||||||
|
|
||||||
for i, line in ipairs(lines) do
|
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()) .. ' ')
|
.. string.rep(' ', longest - line:len()) .. ' ')
|
||||||
end
|
end
|
||||||
|
|
||||||
return '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n'
|
return '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n'
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Renders an info block. An info block is a block of text with
|
function M.renderInfoBlock(type, text)
|
||||||
--- 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)
|
|
||||||
local longest = 0
|
local longest = 0
|
||||||
local lines = string.split(text:gsub('\t', ' '), '\n')
|
local lines = string.split(text:gsub('\t', ' '), '\n')
|
||||||
|
|
||||||
@ -50,7 +34,7 @@ function doc.renderInfoBlock(type, text)
|
|||||||
end
|
end
|
||||||
|
|
||||||
for i, line in ipairs(lines) do
|
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()) .. ' '
|
.. string.rep(' ', longest - line:len()) .. ' '
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -60,4 +44,4 @@ function doc.renderInfoBlock(type, text)
|
|||||||
end
|
end
|
||||||
return '\n' .. heading .. '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n'
|
return '\n' .. heading .. '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n'
|
||||||
end
|
end
|
||||||
return doc
|
return M
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
-- @module greenhouse
|
-- Greenhouse is a simple text scrolling handler for terminal programs.
|
||||||
-- Greenhouse is a simple text scrolling handler (pager) for terminal programs.
|
|
||||||
-- The idea is that it can be set a region to do its scrolling and paging
|
-- 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.
|
-- job and then the user can draw whatever outside it.
|
||||||
-- This reduces code duplication for the message viewer
|
-- This reduces code duplication for the message viewer
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
-- @module greenhouse.page
|
|
||||||
local Object = require 'nature.object'
|
local Object = require 'nature.object'
|
||||||
|
|
||||||
local Page = Object:extend()
|
local Page = Object:extend()
|
||||||
@ -11,7 +10,6 @@ function Page:new(title, text)
|
|||||||
self.children = {}
|
self.children = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function Page:setText(text)
|
function Page:setText(text)
|
||||||
self.lines = string.split(text, '\n')
|
self.lines = string.split(text, '\n')
|
||||||
end
|
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 bait = require 'bait'
|
||||||
local commander = require 'commander'
|
local commander = require 'commander'
|
||||||
local lunacolors = require 'lunacolors'
|
local lunacolors = require 'lunacolors'
|
||||||
@ -55,8 +44,6 @@ function hilbish.messages.send(message)
|
|||||||
bait.throw('hilbish.notification', message)
|
bait.throw('hilbish.notification', message)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Marks a message at `idx` as read.
|
|
||||||
--- @param idx number
|
|
||||||
function hilbish.messages.read(idx)
|
function hilbish.messages.read(idx)
|
||||||
local msg = M._messages[idx]
|
local msg = M._messages[idx]
|
||||||
if msg then
|
if msg then
|
||||||
@ -65,20 +52,16 @@ function hilbish.messages.read(idx)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Marks all messages as read.
|
function hilbish.messages.readAll(idx)
|
||||||
function hilbish.messages.readAll()
|
|
||||||
for _, msg in ipairs(hilbish.messages.all()) do
|
for _, msg in ipairs(hilbish.messages.all()) do
|
||||||
hilbish.messages.read(msg.index)
|
hilbish.messages.read(msg.index)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Returns the amount of unread messages.
|
|
||||||
function hilbish.messages.unreadCount()
|
function hilbish.messages.unreadCount()
|
||||||
return unread
|
return unread
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Deletes the message at `idx`.
|
|
||||||
--- @param idx number
|
|
||||||
function hilbish.messages.delete(idx)
|
function hilbish.messages.delete(idx)
|
||||||
local msg = M._messages[idx]
|
local msg = M._messages[idx]
|
||||||
if not msg then
|
if not msg then
|
||||||
@ -88,14 +71,12 @@ function hilbish.messages.delete(idx)
|
|||||||
M._messages[idx] = nil
|
M._messages[idx] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Deletes all messages.
|
|
||||||
function hilbish.messages.clear()
|
function hilbish.messages.clear()
|
||||||
for _, msg in ipairs(hilbish.messages.all()) do
|
for _, msg in ipairs(hilbish.messages.all()) do
|
||||||
hilbish.messages.delete(msg.index)
|
hilbish.messages.delete(msg.index)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Returns all messages.
|
|
||||||
function hilbish.messages.all()
|
function hilbish.messages.all()
|
||||||
return M._messages
|
return M._messages
|
||||||
end
|
end
|
||||||
|
@ -18,15 +18,12 @@ table.insert(package.searchers, function(module)
|
|||||||
return function() return hilbish.module.load(path) end, path
|
return function() return hilbish.module.load(path) end, path
|
||||||
end)
|
end)
|
||||||
|
|
||||||
require 'nature.hilbish'
|
|
||||||
|
|
||||||
require 'nature.commands'
|
require 'nature.commands'
|
||||||
require 'nature.completions'
|
require 'nature.completions'
|
||||||
require 'nature.opts'
|
require 'nature.opts'
|
||||||
require 'nature.vim'
|
require 'nature.vim'
|
||||||
require 'nature.runner'
|
require 'nature.runner'
|
||||||
require 'nature.hummingbird'
|
require 'nature.hummingbird'
|
||||||
require 'nature.abbr'
|
|
||||||
|
|
||||||
local shlvl = tonumber(os.getenv 'SHLVL')
|
local shlvl = tonumber(os.getenv 'SHLVL')
|
||||||
if shlvl ~= nil then
|
if shlvl ~= nil then
|
||||||
|
@ -14,8 +14,7 @@ The nice lil shell for {blue}Lua{reset} fanatics!
|
|||||||
motd = true,
|
motd = true,
|
||||||
fuzzy = false,
|
fuzzy = false,
|
||||||
notifyJobFinish = true,
|
notifyJobFinish = true,
|
||||||
crimmas = true,
|
crimmas = true
|
||||||
tips = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for optsName, default in pairs(defaultOpts) do
|
for optsName, default in pairs(defaultOpts) do
|
||||||
|
@ -2,7 +2,8 @@ local bait = require 'bait'
|
|||||||
local lunacolors = require 'lunacolors'
|
local lunacolors = require 'lunacolors'
|
||||||
|
|
||||||
hilbish.motd = [[
|
hilbish.motd = [[
|
||||||
{magenta}Hilbish{reset} blooms in the {blue}midnight.{reset}
|
Finally at {red}v2.2!{reset} So much {green}documentation improvements{reset}
|
||||||
|
and 1 single fix for Windows! {blue}.. and a feature they can't use.{reset}
|
||||||
]]
|
]]
|
||||||
|
|
||||||
bait.catch('hilbish.init', function()
|
bait.catch('hilbish.init', function()
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
local bait = require 'bait'
|
|
||||||
local lunacolors = require 'lunacolors'
|
|
||||||
|
|
||||||
local postamble = [[
|
|
||||||
{yellow}These tips can be disabled with {reset}{invert} hilbish.opts.tips = false {reset}
|
|
||||||
]]
|
|
||||||
|
|
||||||
hilbish.tips = {
|
|
||||||
'Join the discord and say hi! {blue}https://discord.gg/3PDdcQz{reset}',
|
|
||||||
'{green}hilbish.alias{reset} interface manages shell aliases. See more detail by running {blue}doc api hilbish.alias.',
|
|
||||||
'{green}hilbish.appendPath(\'path\'){reset} -> Appends the provided dir to the command path ($PATH)',
|
|
||||||
'{green}hilbish.completions{reset} -> Used to control suggestions when tab completing.',
|
|
||||||
'{green}hilbish.message{reset} -> Simple notification system which can be used by other plugins and parts of the shell to notify the user of various actions.',
|
|
||||||
[[
|
|
||||||
{green}hilbish.opts{reset} -> Simple toggle or value options a user can set.
|
|
||||||
You may disable the startup greeting by {invert}hilbish.opts.greeting = false{reset}
|
|
||||||
]],
|
|
||||||
[[
|
|
||||||
{green}hilbish.runner{reset} -> The runner interface contains functions to
|
|
||||||
manage how Hilbish interprets interactive input. The default runners can run
|
|
||||||
shell script and Lua code!
|
|
||||||
]],
|
|
||||||
[[
|
|
||||||
Add Lua-written commands with the commander module!
|
|
||||||
Check the command {blue}doc api commander{reset} or the web docs:
|
|
||||||
https://rosettea.github.io/Hilbish/docs/api/commander/
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
|
|
||||||
bait.catch('hilbish.init', function()
|
|
||||||
if hilbish.interactive and hilbish.opts.tips then
|
|
||||||
local idx = math.random(1, #hilbish.tips)
|
|
||||||
print(lunacolors.format('{yellow}🛈 Tip:{reset} ' .. hilbish.tips[idx] .. '\n' .. postamble))
|
|
||||||
end
|
|
||||||
end)
|
|
@ -1,5 +1,4 @@
|
|||||||
-- @module hilbish.runner
|
--- hilbish.runner
|
||||||
local snail = require 'snail'
|
|
||||||
local currentRunner = 'hybrid'
|
local currentRunner = 'hybrid'
|
||||||
local runners = {}
|
local runners = {}
|
||||||
|
|
||||||
@ -7,7 +6,7 @@ local runners = {}
|
|||||||
hilbish = hilbish
|
hilbish = hilbish
|
||||||
|
|
||||||
--- Get a runner by name.
|
--- Get a runner by name.
|
||||||
--- @param name string Name of the runner to retrieve.
|
--- @param name string
|
||||||
--- @return table
|
--- @return table
|
||||||
function hilbish.runner.get(name)
|
function hilbish.runner.get(name)
|
||||||
local r = runners[name]
|
local r = runners[name]
|
||||||
@ -19,10 +18,10 @@ function hilbish.runner.get(name)
|
|||||||
return r
|
return r
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Adds a runner to the table of available runners.
|
--- Adds a runner to the table of available runners. If runner is a table,
|
||||||
--- If runner is a table, it must have the run function in it.
|
--- it must have the run function in it.
|
||||||
--- @param name string Name of the runner
|
--- @param name string
|
||||||
--- @param runner function|table
|
--- @param runner function | table
|
||||||
function hilbish.runner.add(name, runner)
|
function hilbish.runner.add(name, runner)
|
||||||
if type(name) ~= 'string' then
|
if type(name) ~= 'string' then
|
||||||
error 'expected runner name to be a table'
|
error 'expected runner name to be a table'
|
||||||
@ -43,9 +42,7 @@ function hilbish.runner.add(name, runner)
|
|||||||
hilbish.runner.set(name, runner)
|
hilbish.runner.set(name, runner)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- *Sets* a runner by name. The difference between this function and
|
--- Sets a runner by name. The runner table must have the run function in it.
|
||||||
--- add, is set will *not* check if the named runner exists.
|
|
||||||
--- The runner table must have the run function in it.
|
|
||||||
--- @param name string
|
--- @param name string
|
||||||
--- @param runner table
|
--- @param runner table
|
||||||
function hilbish.runner.set(name, runner)
|
function hilbish.runner.set(name, runner)
|
||||||
@ -56,11 +53,11 @@ function hilbish.runner.set(name, runner)
|
|||||||
runners[name] = runner
|
runners[name] = runner
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Executes `cmd` with a runner.
|
--- Executes cmd with a runner. If runnerName isn't passed, it uses
|
||||||
--- If `runnerName` is not specified, it uses the default Hilbish runner.
|
--- the user's current runner.
|
||||||
--- @param cmd string
|
--- @param cmd string
|
||||||
--- @param runnerName string?
|
--- @param runnerName string?
|
||||||
--- @return table
|
--- @return string, number, string
|
||||||
function hilbish.runner.exec(cmd, runnerName)
|
function hilbish.runner.exec(cmd, runnerName)
|
||||||
if not runnerName then runnerName = currentRunner end
|
if not runnerName then runnerName = currentRunner end
|
||||||
|
|
||||||
@ -69,11 +66,13 @@ function hilbish.runner.exec(cmd, runnerName)
|
|||||||
return r.run(cmd)
|
return r.run(cmd)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Sets Hilbish's runner mode by name.
|
--- Sets the current interactive/command line runner mode.
|
||||||
--- @param name string
|
--- @param name string
|
||||||
function hilbish.runner.setCurrent(name)
|
function hilbish.runner.setCurrent(name)
|
||||||
hilbish.runner.get(name) -- throws if it doesnt exist.
|
local r = hilbish.runner.get(name)
|
||||||
currentRunner = name
|
currentRunner = name
|
||||||
|
|
||||||
|
hilbish.runner.setMode(r.run)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Returns the current runner by name.
|
--- Returns the current runner by name.
|
||||||
@ -82,81 +81,6 @@ function hilbish.runner.getCurrent()
|
|||||||
return currentRunner
|
return currentRunner
|
||||||
end
|
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)
|
hilbish.runner.add('hybrid', function(input)
|
||||||
local cmdStr = hilbish.aliases.resolve(input)
|
local cmdStr = hilbish.aliases.resolve(input)
|
||||||
|
|
||||||
@ -183,5 +107,7 @@ hilbish.runner.add('lua', function(input)
|
|||||||
return hilbish.runner.lua(cmdStr)
|
return hilbish.runner.lua(cmdStr)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
hilbish.runner.add('sh', hilbish.runner.sh)
|
hilbish.runner.add('sh', function(input)
|
||||||
hilbish.runner.setCurrent 'hybrid'
|
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.hintY = 0
|
||||||
rl.hintText = []rune{}
|
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()
|
rl.renderHelpers()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.insertHintText()
|
|
||||||
|
|
||||||
if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) ||
|
if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) ||
|
||||||
(rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) {
|
(rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) {
|
||||||
rl.moveCursorByAdjust(1)
|
rl.moveCursorByAdjust(1)
|
||||||
|
@ -142,10 +142,6 @@ func (rl *Instance) viDeleteByAdjust(adjust int) {
|
|||||||
rl.updateHelpers()
|
rl.updateHelpers()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *Instance) DeleteByAmount(adjust int) {
|
|
||||||
rl.viDeleteByAdjust(adjust)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rl *Instance) vimDeleteToken(r rune) bool {
|
func (rl *Instance) vimDeleteToken(r rune) bool {
|
||||||
tokens, _, _ := tokeniseSplitSpaces(rl.line, 0)
|
tokens, _, _ := tokeniseSplitSpaces(rl.line, 0)
|
||||||
pos := int(r) - 48 // convert ASCII to integer
|
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.
|
All are not required, only the useful ones the runner needs to return.
|
||||||
(So if there isn't an error, just omit `err`.)
|
(So if there isn't an error, just omit `err`.)
|
||||||
|
|
||||||
- `exitCode` (number): Exit code of the command
|
- `exitCode` (number): A numerical code to indicate the exit result.
|
||||||
- `input` (string): The text input of the user. This is used by Hilbish to append extra input, in case
|
- `input` (string): The user input. This will be used to add
|
||||||
more is requested.
|
to the history.
|
||||||
- `err` (string): A string that represents an error from the runner.
|
- `err` (string): A string to indicate an interal error for 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:
|
||||||
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-found` will throw a `command.not-found` hook
|
|
||||||
based on what `<command>` is.
|
`[command]: not-executable` will throw a command.not-executable hook.
|
||||||
- `<command>: not-executable` will throw a `command.not-executable` hook.
|
- `continue` (boolean): Whether to prompt the user for more input.
|
||||||
- `continue` (boolean): Whether Hilbish should prompt the user for no input
|
|
||||||
- `newline` (boolean): Whether a newline should be added at the end of `input`.
|
|
||||||
|
|
||||||
Here is a simple example of a fennel runner. It falls back to
|
Here is a simple example of a fennel runner. It falls back to
|
||||||
shell script if fennel eval has an error.
|
shell script if fennel eval has an error.
|
||||||
@ -53,7 +51,9 @@ end)
|
|||||||
*/
|
*/
|
||||||
func runnerModeLoader(rtm *rt.Runtime) *rt.Table {
|
func runnerModeLoader(rtm *rt.Runtime) *rt.Table {
|
||||||
exports := map[string]util.LuaExport{
|
exports := map[string]util.LuaExport{
|
||||||
|
"sh": {shRunner, 1, false},
|
||||||
"lua": {luaRunner, 1, false},
|
"lua": {luaRunner, 1, false},
|
||||||
|
"setMode": {hlrunnerMode, 1, false},
|
||||||
}
|
}
|
||||||
|
|
||||||
mod := rt.NewTable()
|
mod := rt.NewTable()
|
||||||
@ -62,6 +62,43 @@ func runnerModeLoader(rtm *rt.Runtime) *rt.Table {
|
|||||||
return mod
|
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
|
// #interface runner
|
||||||
// lua(cmd)
|
// lua(cmd)
|
||||||
// Evaluates `cmd` as Lua input. This is the same as using `dofile`
|
// Evaluates `cmd` as Lua input. This is the same as using `dofile`
|
||||||
|
@ -1,32 +1,35 @@
|
|||||||
package util
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"hilbish/util"
|
||||||
|
|
||||||
rt "github.com/arnodel/golua/runtime"
|
rt "github.com/arnodel/golua/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
var sinkMetaKey = rt.StringValue("hshsink")
|
var sinkMetaKey = rt.StringValue("hshsink")
|
||||||
|
|
||||||
// #type
|
// #type
|
||||||
// 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
|
||||||
type Sink struct{
|
// a desination.
|
||||||
Rw *bufio.ReadWriter
|
type sink struct{
|
||||||
|
writer *bufio.Writer
|
||||||
|
reader *bufio.Reader
|
||||||
file *os.File
|
file *os.File
|
||||||
UserData *rt.UserData
|
ud *rt.UserData
|
||||||
autoFlush bool
|
autoFlush bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func SinkLoader(rtm *rt.Runtime) *rt.Table {
|
func setupSinkType(rtm *rt.Runtime) {
|
||||||
sinkMeta := rt.NewTable()
|
sinkMeta := rt.NewTable()
|
||||||
|
|
||||||
sinkMethods := rt.NewTable()
|
sinkMethods := rt.NewTable()
|
||||||
sinkFuncs := map[string]LuaExport{
|
sinkFuncs := map[string]util.LuaExport{
|
||||||
"flush": {luaSinkFlush, 1, false},
|
"flush": {luaSinkFlush, 1, false},
|
||||||
"read": {luaSinkRead, 1, false},
|
"read": {luaSinkRead, 1, false},
|
||||||
"readAll": {luaSinkReadAll, 1, false},
|
"readAll": {luaSinkReadAll, 1, false},
|
||||||
@ -34,7 +37,7 @@ func SinkLoader(rtm *rt.Runtime) *rt.Table {
|
|||||||
"write": {luaSinkWrite, 2, false},
|
"write": {luaSinkWrite, 2, false},
|
||||||
"writeln": {luaSinkWriteln, 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) {
|
sinkIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
||||||
s, _ := sinkArg(c, 0)
|
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)))
|
sinkMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(sinkIndex, "__index", 2, false)))
|
||||||
rtm.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta))
|
l.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta))
|
||||||
|
|
||||||
exports := map[string]LuaExport{
|
|
||||||
"new": {luaSinkNew, 0, false},
|
|
||||||
}
|
|
||||||
|
|
||||||
mod := rt.NewTable()
|
|
||||||
SetExports(rtm, mod, exports)
|
|
||||||
|
|
||||||
return mod
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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
|
// #member
|
||||||
// readAll() -> string
|
// readAll() -> string
|
||||||
// --- @returns string
|
// --- @returns string
|
||||||
@ -94,17 +82,11 @@ func luaSinkReadAll(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.autoFlush {
|
|
||||||
s.Rw.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := []string{}
|
lines := []string{}
|
||||||
for {
|
for {
|
||||||
line, err := s.Rw.ReadString('\n')
|
line, err := s.reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
// We still want to add the data we read
|
|
||||||
lines = append(lines, line)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +113,7 @@ func luaSinkRead(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
str, _ := s.Rw.ReadString('\n')
|
str, _ := s.reader.ReadString('\n')
|
||||||
|
|
||||||
return c.PushingNext1(t.Runtime, rt.StringValue(str)), nil
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Rw.Write([]byte(data))
|
s.writer.Write([]byte(data))
|
||||||
if s.autoFlush {
|
if s.autoFlush {
|
||||||
s.Rw.Flush()
|
s.writer.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Next(), nil
|
return c.Next(), nil
|
||||||
@ -178,9 +160,9 @@ func luaSinkWriteln(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Rw.Write([]byte(data + "\n"))
|
s.writer.Write([]byte(data + "\n"))
|
||||||
if s.autoFlush {
|
if s.autoFlush {
|
||||||
s.Rw.Flush()
|
s.writer.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Next(), nil
|
return c.Next(), nil
|
||||||
@ -199,7 +181,7 @@ func luaSinkFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Rw.Flush()
|
s.writer.Flush()
|
||||||
|
|
||||||
return c.Next(), nil
|
return c.Next(), nil
|
||||||
}
|
}
|
||||||
@ -230,25 +212,11 @@ func luaSinkAutoFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|||||||
return c.Next(), nil
|
return c.Next(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSink(rtm *rt.Runtime, Rw io.ReadWriter) *Sink {
|
func newSinkInput(r io.Reader) *sink {
|
||||||
s := &Sink{
|
s := &sink{
|
||||||
Rw: bufio.NewReadWriter(bufio.NewReader(Rw), bufio.NewWriter(Rw)),
|
reader: bufio.NewReader(r),
|
||||||
autoFlush: true,
|
|
||||||
}
|
}
|
||||||
s.UserData = sinkUserData(rtm, s)
|
s.ud = sinkUserData(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)
|
|
||||||
|
|
||||||
if f, ok := r.(*os.File); ok {
|
if f, ok := r.(*os.File); ok {
|
||||||
s.file = f
|
s.file = f
|
||||||
@ -257,17 +225,17 @@ func NewSinkInput(rtm *rt.Runtime, r io.Reader) *Sink {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSinkOutput(rtm *rt.Runtime, w io.Writer) *Sink {
|
func newSinkOutput(w io.Writer) *sink {
|
||||||
s := &Sink{
|
s := &sink{
|
||||||
Rw: bufio.NewReadWriter(nil, bufio.NewWriter(w)),
|
writer: bufio.NewWriter(w),
|
||||||
autoFlush: true,
|
autoFlush: true,
|
||||||
}
|
}
|
||||||
s.UserData = sinkUserData(rtm, s)
|
s.ud = sinkUserData(s)
|
||||||
|
|
||||||
return 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))
|
s, ok := valueToSink(c.Arg(arg))
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("#%d must be a sink", arg + 1)
|
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
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func valueToSink(val rt.Value) (*Sink, bool) {
|
func valueToSink(val rt.Value) (*sink, bool) {
|
||||||
u, ok := val.TryUserData()
|
u, ok := val.TryUserData()
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
s, ok := u.Value().(*Sink)
|
s, ok := u.Value().(*sink)
|
||||||
return s, ok
|
return s, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func sinkUserData(rtm *rt.Runtime, s *Sink) *rt.UserData {
|
func sinkUserData(s *sink) *rt.UserData {
|
||||||
sinkMeta := rtm.Registry(sinkMetaKey)
|
sinkMeta := l.Registry(sinkMetaKey)
|
||||||
return rt.NewUserData(s, sinkMeta.AsTable())
|
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 (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"os/user"
|
"os/user"
|
||||||
"runtime"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
rt "github.com/arnodel/golua/runtime"
|
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.
|
// 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.
|
// 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) {
|
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
|
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.
|
// DoFile runs the contents of the file in the Lua runtime.
|
||||||
func DoFile(rtm *rt.Runtime, path string) error {
|
func DoFile(rtm *rt.Runtime, path string) error {
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
@ -214,67 +141,3 @@ func AbbrevHome(path string) string {
|
|||||||
|
|
||||||
return path
|
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
|
// Version info
|
||||||
var (
|
var (
|
||||||
ver = "v2.4.0"
|
ver = "v2.3.0"
|
||||||
releaseName = "Moonflower"
|
releaseName = "Alyssum"
|
||||||
|
|
||||||
gitCommit string
|
gitCommit string
|
||||||
gitBranch string
|
gitBranch string
|
||||||
|
@ -15,5 +15,7 @@ var (
|
|||||||
.. hilbish.userDir.config .. '/hilbish/?/?.lua;'
|
.. hilbish.userDir.config .. '/hilbish/?/?.lua;'
|
||||||
.. hilbish.userDir.config .. '/hilbish/?.lua'`
|
.. hilbish.userDir.config .. '/hilbish/?.lua'`
|
||||||
dataDir = "/usr/local/share/hilbish"
|
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")
|
defaultConfDir = getenv("XDG_CONFIG_HOME", "~/.config")
|
||||||
)
|
)
|
||||||
|
@ -14,6 +14,8 @@ var (
|
|||||||
.. hilbish.userDir.config .. '/hilbish/?/init.lua;'
|
.. hilbish.userDir.config .. '/hilbish/?/init.lua;'
|
||||||
.. hilbish.userDir.config .. '/hilbish/?/?.lua;'
|
.. hilbish.userDir.config .. '/hilbish/?/?.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 = ""
|
defaultConfDir = ""
|
||||||
)
|
)
|
||||||
|
@ -10,6 +10,8 @@ var (
|
|||||||
.. hilbish.userDir.config .. '\\Hilbish\\libs\\?\\init.lua;'
|
.. hilbish.userDir.config .. '\\Hilbish\\libs\\?\\init.lua;'
|
||||||
.. hilbish.userDir.config .. '\\Hilbish\\libs\\?\\?.lua;'
|
.. hilbish.userDir.config .. '\\Hilbish\\libs\\?\\?.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 = ""
|
defaultConfDir = ""
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user