2
2
mirror of https://github.com/Hilbis/Hilbish synced 2025-04-21 04:53:24 +00:00

Compare commits

...

27 Commits

Author SHA1 Message Date
6827940466
chore: remove print and fix formatting 2025-04-17 22:36:29 -04:00
fde615ff3f
chore: merge 2025-04-17 22:34:23 -04:00
d002c82271
fix: hilbish.run return exitCode instead of runner output table 2025-04-17 22:34:17 -04:00
f64229b52c
fix: set autoflush to true by default for sinks, and flush when reading from sinks (closes #344) 2025-04-17 22:33:50 -04:00
James Dugan
3d5766ac33
fix: hilbish.sink.readAll() function now reads data that doesn't end in a newline (#345) 2025-04-17 22:06:21 -04:00
60edfc00ee
chore: delete some unneeded code and files 2025-04-03 09:08:27 -04:00
6cd294373c
feat: add abbreviations (#340) 2025-04-03 08:45:02 -04:00
02c89b99dd
refactor: decouple sh use in core exec code (#337) 2025-04-03 00:38:35 -04:00
fe4e972fbe
chore: update version info 2025-04-03 00:37:17 -04:00
e4a9e06d2a
chore: merge (again) 2025-04-02 11:12:13 -04:00
487b5fa4f8
ci: checkout with submodules 2025-04-02 11:12:09 -04:00
sammy-ette
946e8e7228 docs: [ci] generate new docs 2025-04-02 15:09:45 +00:00
04206b6a14
docs: upload docs 2025-04-02 11:09:18 -04:00
9ea2a2f332
chore: merge 2025-04-02 11:08:54 -04:00
sammy-ette
364cb1ca2e
fix: add . to dataDir as fallback 2025-04-02 11:08:41 -04:00
sammy-ette
2ba878713c docs: [ci] generate new docs 2025-04-02 14:55:27 +00:00
2678ec723e
fix: push docs 2025-04-02 10:55:11 -04:00
dbf2d80863
fix: remove unused vars for other targets 2025-04-02 10:51:53 -04:00
fab98bc613
feat: search XDG_DATA_DIRS for hilbish files 2025-04-02 10:49:25 -04:00
sammy-ette
7b16cde540 docs: [ci] generate new docs 2025-04-02 13:42:07 +00:00
4150001d8b
fix: make lua implemented hilbish interfaces documented (#335)
* fix: make lua implemented hilbish interfaces documented

* fix: signature link in table of contents

* fix: reduce function list to match in go generated docs

* fix: toc appending

* docs: enable docs for hilbish.messages

* feat: add description gen, and more spacing between param listing

* docs: add more detailed documentation for lua modules

* docs: update hilbish.messages docs

* fix: add description for lua doc'd modules, remove duplicate docs

* docs: add back hilbish.jobs doc

* feat: generate toc for lua modules

* fix: add table heading

* ci: add lua docgen

* docs: put dirs.old doc on 1 line
2025-04-02 09:41:37 -04:00
32ed0d2348
docs: add a bit of extra info in the getting started doc 2025-03-14 18:45:56 -04:00
8731b1a7d1
chore: revert "chore: revert "chore: add 2.4 motd (work in progress)""
revertception
This reverts commit 7fc3f4a569405c86675978341a0c008069b994b9.
2024-12-29 21:44:05 -04:00
4743222044
chore: forward master in sync to v2.3.4 2024-12-28 19:58:00 -04:00
a02cd1d7ef
fix: use global env variables when executing 2024-12-28 19:50:06 -04:00
c969f5ed15
feat: complete hint text on right arrow (#328) 2024-12-22 12:09:57 -04:00
CelestialCrafter
36ce05e85a
fix: handle completion info check error (#330)
* fix: handle completion info check error
fixes Rosettea/Hilbish#329

* make changelog more descriptive
2024-11-22 20:20:43 -04:00
66 changed files with 2143 additions and 3307 deletions

View File

@ -10,9 +10,18 @@ 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: Run docgen - name: Download Task
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:

View File

@ -1,9 +1,14 @@
# 🎀 Changelog # 🎀 Changelog
## Unreleased
### Added
- Forward/Right arrow key will fill in hint text (#327)
## [2.3.4] - 2024-12-28 ## [2.3.4] - 2024-12-28
### Fixed ### Fixed
- Skip over file and prevent panic if info cannot be retrieved during file completion (due to permission error or anything else) - 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 - 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 ## [2.3.3] - 2024-11-04
### Fixed ### Fixed

152
api.go
View File

@ -13,10 +13,9 @@
package main package main
import ( import (
"bytes" //"bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
@ -28,9 +27,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{
@ -39,7 +38,6 @@ 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},
@ -49,7 +47,6 @@ 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},
} }
@ -134,6 +131,9 @@ 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,6 +154,7 @@ 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 {
@ -182,112 +183,7 @@ 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.
@ -404,7 +300,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 nil, err return c.PushingNext1(t.Runtime, rt.StringValue(multilinePrompt)), nil
} }
prompt, err := c.StringArg(0) prompt, err := c.StringArg(0)
if err != nil { if err != nil {
@ -508,7 +404,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 := exec.LookPath(cmdArgs[0]) cmdPath, err := util.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
@ -706,7 +602,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 := exec.LookPath(cmd) path, err := util.LookPath(cmd)
if err != nil { if err != nil {
return c.Next(), nil return c.Next(), nil
} }
@ -742,34 +638,6 @@ 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

View File

@ -84,6 +84,7 @@ 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) {
@ -208,6 +209,10 @@ 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)
@ -299,10 +304,28 @@ 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{"./"} dirs := []string{"./", "./util"}
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
@ -329,7 +352,7 @@ func main() {
pieces := []docPiece{} pieces := []docPiece{}
typePieces := []docPiece{} typePieces := []docPiece{}
mod := l mod := l
if mod == "main" { if mod == "main" || mod == "util" {
mod = "hilbish" mod = "hilbish"
} }
var hasInterfaces bool var hasInterfaces bool
@ -413,6 +436,14 @@ func main() {
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,
@ -423,6 +454,7 @@ func main() {
Fields: docPieceTag("field", tags), Fields: docPieceTag("field", tags),
} }
} }
}
for key, mod := range interfaceModules { for key, mod := range interfaceModules {
docs[key] = *mod docs[key] = *mod

View File

@ -1,7 +1,9 @@
local fs = require 'fs' local fs = require 'fs'
local emmyPattern = '^%-%-%- (.+)' local emmyPattern = '^%-%-%- (.+)'
local modpattern = '^%-+ @module (%w+)' local emmyPattern2 = '^%-%- (.+)'
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
@ -13,18 +15,25 @@ 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 not line:match(emmyPattern) then if line:match(emmyPattern) or line:match(emmyPattern2) then
if doingDescription then
table.insert(descriptions[mod], line:match(emmyPattern) or line:match(emmyPattern2))
end
else
doingDescription = false
if 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)
@ -32,10 +41,12 @@ 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]
@ -51,19 +62,31 @@ 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
pieces[mod][funcName] = dps table.insert(pieces[mod], {funcName, dps})
end end
docPiece = {} docPiece = {}
goto continue2 goto continue2
@ -81,29 +104,82 @@ description: %s
layout: doc layout: doc
menu: menu:
docs: docs:
parent: "Nature" parent: "%s"
--- ---
]] ]]
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 path = string.format('docs/%s/%s.md', mod, iface) local docParent = 'Nature'
path = string.format('docs/%s/%s.md', mod, iface)
if mod ~= 'nature' then
docParent = "API"
path = string.format('docs/api/%s/%s.md', mod, iface)
end
if iface == 'hilbish' then
docParent = "API"
path = string.format('docs/api/hilbish/_index.md', mod, iface)
end
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)
print(mod, path) local exists = pcall(fs.stat, path)
local newOrNotNature = (exists and mod ~= 'nature') or iface == 'hilbish'
for func, docs in pairs(dps) do local f <close> = io.open(path, newOrNotNature and 'r+' or 'w+')
f:write(string.format('<hr>\n<div id=\'%s\'>', func)) local tocPos
if not newOrNotNature then
f:write(string.format(header, 'Module', iface, (descriptions[iface] and #descriptions[iface] > 0) and descriptions[iface][1] or 'No description.', docParent))
if descriptions[iface] and #descriptions[iface] > 0 then
table.remove(descriptions[iface], 1)
f:write(string.format('\n## Introduction\n%s\n\n', table.concat(descriptions[iface], '\n')))
f:write('## Functions\n')
f:write([[|||
|----|----|
]])
tocPos = f:seek()
end
end
local tocSearch = false
for line in f:lines() do
if line:match '^## Functions' then
tocSearch = true
end
if tocSearch and line == '' then
tocSearch = false
tocPos = f:seek() - 1
end
end
table.sort(dps, function(a, b) return a[1] < b[1] end)
for _, piece in pairs(dps) do
local func = piece[1]
local docs = piece[2]
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('%?$', '')
if idx ~= #docs.params then sig = sig .. ', ' end params = params .. param.name:gsub('%?$', '')
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
@ -121,6 +197,11 @@ for iface, dps in pairs(pieces) do
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)

View File

@ -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 := findExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil { if err := util.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 := findExecutable(match, true, false) err := util.FindExecutable(match, true, false)
if err != nil { if err != nil {
continue continue
} }

View File

@ -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,72 +408,6 @@ 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>
@ -519,8 +453,7 @@ 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 sink is a structure that has input and/or output to/from a desination.
a desination.
### Methods ### Methods
#### autoFlush(auto) #### autoFlush(auto)
@ -542,3 +475,65 @@ 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>

View File

@ -0,0 +1,67 @@
---
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>

View File

@ -14,12 +14,30 @@ 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'>
@ -96,6 +114,9 @@ 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`**

View File

@ -0,0 +1,135 @@
---
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>

View File

@ -54,29 +54,16 @@ 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="#runner.sh">sh(cmd)</a>|Runs a command in Hilbish's shell script interpreter.| |<a href="#sh">sh()</a>|nil|
|<a href="#setMode">setMode(mode)</a>|**NOTE: This function is deprecated and will be removed in 3.0**|
<hr> |<a href="#setCurrent">setCurrent(name)</a>|Sets Hilbish's runner mode by name.|
<div id='runner.setMode'> |<a href="#set">set(name, runner)</a>|*Sets* a runner by name. The difference between this function and|
<h4 class='heading'> |<a href="#run">run(input, priv)</a>|Runs `input` with the currently set Hilbish runner.|
hilbish.runner.setMode(cb) |<a href="#getCurrent">getCurrent()</a>|Returns the current runner by name.|
<a href="#runner.setMode" class='heading-link'> |<a href="#get">get(name)</a>|Get a runner by name.|
<i class="fas fa-paperclip"></i> |<a href="#exec">exec(cmd, runnerName)</a>|Executes `cmd` with a runner.|
</a> |<a href="#add">add(name, runner)</a>|Adds a runner to the table of available runners.|
</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'>
@ -97,20 +84,164 @@ or `load`, but is appropriated for the runner interface.
</div> </div>
<hr> <hr>
<div id='runner.sh'> <div id='add'>
<h4 class='heading'> <h4 class='heading'>
hilbish.runner.sh(cmd) hilbish.runner.add(name, runner)
<a href="#runner.sh" class='heading-link'> <a href="#add" class='heading-link'>
<i class="fas fa-paperclip"></i> <i class="fas fa-paperclip"></i>
</a> </a>
</h4> </h4>
Runs a command in Hilbish's shell script interpreter. Adds a runner to the table of available runners.
This is the equivalent of using `source`. If runner is a table, it must have the run function in it.
#### Parameters #### Parameters
`string` **`cmd`** `name` **`string`**
Name of the runner
`runner` **`function|table`**
</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>

50
docs/api/snail.md Normal file
View File

@ -0,0 +1,50 @@
---
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.

View File

@ -53,8 +53,39 @@ 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 you can get to editing it. Since it's just a Lua file, having basic Now we can get to customization!
knowledge of Lua would help. All of Lua's standard libraries and functions
from Lua 5.4 are available. Hilbish has some custom and modules that are If we closely examine a small snippet of the default config:
available. To see them, you can run the `doc` command. This also works as ```lua
general documentation for other things. -- Default Hilbish config
-- .. with some omitted code .. --
local function doPrompt(fail)
hilbish.prompt(lunacolors.format(
'{blue}%u {cyan}%d ' .. (fail and '{red}' or '{green}') .. '∆ '
))
end
doPrompt()
bait.catch('command.exit', function(code)
doPrompt(code ~= 0)
end)
```
We see a whopping **three** Hilbish libraries being used in this part of code.
First is of course, named after the shell itself, [`hilbish`](../api/hilbish). This is kind of a
"catch-all" namespace for functions that directly related to shell functionality/settings.
And as we can see, the [hilbish.prompt](../api/hilbish/#prompt) function is used
to change our prompt. Change our prompt to what, exactly?
The doc for the function states that the verbs `%u` and `%d`are used for username and current directory
of the shell, respectively.
We wrap this in the [`lunacolors.format`](../lunacolors) function, to give
our prompt some nice color.
But you might have also noticed that this is in the `doPrompt` function, which is called once,
and then used again in a [bait](../api/bait) hook. Specifically, the `command.exit` hook,
which is called after a command exits, so when it finishes running.

View File

@ -43,5 +43,29 @@ The notification. The properties are defined in the link above.
<hr> <hr>
+ `hilbish.vimAction` -> actionName, args > Sent when the user does a "vim action," being something ## hilbish.cd
like yanking or pasting text. See `doc vim-mode actions` for more info. Sent when the current directory of the shell is changed (via interactive means.)
If you are implementing a custom command that changes the directory of the shell,
you must throw this hook manually for correctness.
#### Variables
`string` **`path`**
Absolute path of the directory that was changed to.
`string` **`oldPath`**
Absolute path of the directory Hilbish *was* in.
<hr>
## hilbish.vimAction
Sent when the user does a "vim action," being something like yanking or pasting text.
See `doc vim-mode actions` for more info.
#### Variables
`string` **`actionName`**
Absolute path of the directory that was changed to.
`table` **`args`**
Table of args relating to the Vim action.
<hr>

View File

@ -1,40 +1,25 @@
--- ---
title: Module dirs title: Module dirs
description: No description. description: internal directory management
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>
Sets the old directory string. ## Introduction
#### Parameters The dirs module defines a small set of functions to store and manage
`d` **`string`** directories.
</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'>
@ -45,8 +30,11 @@ 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>
@ -61,6 +49,24 @@ 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>
@ -75,5 +81,23 @@ 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>

76
docs/nature/doc.md Normal file
View File

@ -0,0 +1,76 @@
---
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>

View File

@ -17,6 +17,7 @@ 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()
@ -47,7 +48,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.
// #aram register string // #param 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 {
@ -106,3 +107,22 @@ 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
}

View File

@ -7,11 +7,8 @@ local hilbish = {}
--- @param cmd string --- @param cmd string
function hilbish.aliases.add(alias, cmd) end function hilbish.aliases.add(alias, cmd) end
--- This is the same as the `hilbish.runnerMode` function. --- Deletes characters in the line by the given amount.
--- It takes a callback, which will be used to execute all interactive input. function hilbish.editor.deleteByAmount(amount) end
--- 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
@ -131,24 +128,6 @@ 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
@ -168,28 +147,6 @@ 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
@ -200,10 +157,6 @@ 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
@ -262,4 +215,26 @@ 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

16
emmyLuaDocs/snail.lua Normal file
View File

@ -0,0 +1,16 @@
--- @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

83
emmyLuaDocs/util.lua Normal file
View File

@ -0,0 +1,83 @@
--- @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

532
exec.go
View File

@ -1,215 +1,26 @@
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.StringValue("hybrid") var runnerMode rt.Value = rt.NilValue
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
cmdString := aliases.Resolve(input) runnerRun := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("run"))
hooks.Emit("command.preexec", input, cmdString) _, err := rt.Call1(l.MainThread(), runnerRun, rt.StringValue(input), rt.BoolValue(priv))
rerun:
var exitCode uint8
var err error
var cont bool
var newline 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, newline, 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, newline, err = handleSh(input)
}
} else {
// can only be a string or function so
var runnerErr error
input, exitCode, cont, newline, 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 = continuePrompt(input, newline)
if err == nil {
goto rerun
} else if err == io.EOF {
lr.SetPrompt(fmtPrompt(prompt))
}
}
if err != nil && err != io.EOF {
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, newline bool) (string, error) {
for {
/*
if strings.HasSuffix(input, "\\") {
input = strings.TrimSuffix(input, "\\") + "\n"
}
*/
in, err := continuePrompt(input, newline)
if err != nil {
lr.SetPrompt(fmtPrompt(prompt))
return input, err
}
return in, nil
}
}
func runLuaRunner(runr rt.Value, userInput string) (input string, exitCode uint8, continued bool, newline 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, 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
}
if nl, ok := runner.Get(rt.StringValue("newline")).TryBool(); ok {
newline = nl
}
return
} }
func handleLua(input string) (string, uint8, error) { func handleLua(input string) (string, uint8, error) {
@ -239,326 +50,13 @@ 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, newline bool, runErr error) {
shRunner := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("sh"))
var err error
input, exitCode, cont, newline, runErr, err = runLuaRunner(shRunner, cmdString)
if err != nil {
runErr = err
}
return
}
func execSh(cmdString string) (input string, exitcode uint8, cont bool, newline bool, e 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, false, err
}
newline := false
if strings.Contains(err.Error(), "unclosed here-document") {
newline = true
}
return cmdString, 126, true, newline, err
} else {
if code, ok := interp.IsExitStatus(err); ok {
return cmdString, code, false, false, nil
} else {
return cmdString, 126, false, false, err
}
}
}
return cmdString, 0, false, 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)
interp.Env(nil)(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)
}
path, 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
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 := 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) (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 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 == '"' {
@ -574,22 +72,6 @@ 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)
} }
@ -601,11 +83,3 @@ 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)
}

View File

@ -19,38 +19,25 @@ 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"
) )
type fs struct{ var Loader = packagelib.Loader{
runner *interp.Runner Load: loaderFunc,
Loader packagelib.Loader
}
func New(runner *interp.Runner) *fs {
f := &fs{
runner: runner,
}
f.Loader = packagelib.Loader{
Load: f.loaderFunc,
Name: "fs", Name: "fs",
} }
return f func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
}
func (f *fs) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
exports := map[string]util.LuaExport{ exports := map[string]util.LuaExport{
"cd": util.LuaExport{f.fcd, 1, false}, "cd": util.LuaExport{fcd, 1, false},
"mkdir": util.LuaExport{f.fmkdir, 2, false}, "mkdir": util.LuaExport{fmkdir, 2, false},
"stat": util.LuaExport{f.fstat, 1, false}, "stat": util.LuaExport{fstat, 1, false},
"readdir": util.LuaExport{f.freaddir, 1, false}, "readdir": util.LuaExport{freaddir, 1, false},
"abs": util.LuaExport{f.fabs, 1, false}, "abs": util.LuaExport{fabs, 1, false},
"basename": util.LuaExport{f.fbasename, 1, false}, "basename": util.LuaExport{fbasename, 1, false},
"dir": util.LuaExport{f.fdir, 1, false}, "dir": util.LuaExport{fdir, 1, false},
"glob": util.LuaExport{f.fglob, 1, false}, "glob": util.LuaExport{fglob, 1, false},
"join": util.LuaExport{f.fjoin, 0, true}, "join": util.LuaExport{fjoin, 0, true},
"pipe": util.LuaExport{f.fpipe, 0, false}, "pipe": util.LuaExport{fpipe, 0, false},
} }
mod := rt.NewTable() mod := rt.NewTable()
util.SetExports(rtm, mod, exports) util.SetExports(rtm, mod, exports)
@ -65,7 +52,7 @@ func (f *fs) 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 (f *fs) fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { func 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
@ -85,7 +72,7 @@ func (f *fs) 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 (f *fs) fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { func 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
} }
@ -100,7 +87,7 @@ func (f *fs) 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 (f *fs) fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { func 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
} }
@ -110,12 +97,10 @@ func (f *fs) fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
} }
path = util.ExpandHome(strings.TrimSpace(path)) path = util.ExpandHome(strings.TrimSpace(path))
abspath, _ := filepath.Abs(path)
err = os.Chdir(path) err = os.Chdir(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
interp.Dir(abspath)(f.runner)
return c.Next(), err return c.Next(), err
} }
@ -125,7 +110,7 @@ func (f *fs) 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 (f *fs) fdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { func 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
} }
@ -156,7 +141,7 @@ print(matches)
-- -> {'init.lua', 'code.lua'} -- -> {'init.lua', 'code.lua'}
#example #example
*/ */
func (f *fs) fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { func 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
} }
@ -190,7 +175,7 @@ print(fs.join(hilbish.userDir.config, 'hilbish'))
-- -> '/home/user/.config/hilbish' on Linux -- -> '/home/user/.config/hilbish' on Linux
#example #example
*/ */
func (f *fs) fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { func 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 {
@ -217,7 +202,7 @@ func (f *fs) fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
fs.mkdir('./foo/bar', true) fs.mkdir('./foo/bar', true)
#example #example
*/ */
func (f *fs) fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { func 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
} }
@ -248,7 +233,7 @@ func (f *fs) 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 (f *fs) fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { func 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
@ -263,7 +248,7 @@ func (f *fs) 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 (f *fs) freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { func 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
} }
@ -311,7 +296,7 @@ Would print the following:
]]-- ]]--
#example #example
*/ */
func (f *fs) fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { func 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
} }

221
golibs/snail/lua.go Normal file
View File

@ -0,0 +1,221 @@
// 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())
}

302
golibs/snail/snail.go Normal file
View File

@ -0,0 +1,302 @@
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
View File

@ -56,8 +56,8 @@ func (j *job) start() error {
} }
j.setHandle(&cmd) j.setHandle(&cmd)
} }
// bgProcAttr is defined in execfile_<os>.go, it holds a procattr struct // bgProcAttr is defined in job_<os>.go, it holds a procattr struct
// in a simple explanation, it makes signals from hilbish (sigint) // in a simple explanation, it makes signals from hilbish (like 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 := handleExecErr(err) exit := util.HandleExecErr(err)
j.exitCode = int(exit) j.exitCode = int(exit)
j.finish() j.finish()
} }

View File

@ -10,6 +10,10 @@ 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")

View File

@ -4,8 +4,13 @@ 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
View File

@ -3,11 +3,13 @@ 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"
@ -23,16 +25,14 @@ 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'")
// Add fs and terminal module module to Lua lib.LoadLibs(l, fs.Loader)
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, preloadPath) err2 := util.DoFile(l, filepath.Join(dataDir, "nature", "init.lua"))
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)

38
main.go
View File

@ -21,7 +21,6 @@ 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 (
@ -38,16 +37,27 @@ var (
cmds *commander.Commander cmds *commander.Commander
defaultConfPath string defaultConfPath string
defaultHistPath string defaultHistPath string
runner *interp.Runner
) )
func main() { func main() {
runner, _ = interp.New() if runtime.GOOS == "linux" {
// dataDir should only be empty on linux to allow XDG_DATA_DIRS searching.
// but since it might be set on some distros (nixos) we should still check if its really is empty.
if dataDir == "" {
searchableDirs := getenv("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/")
dataDir = "."
for _, path := range strings.Split(searchableDirs, ":") {
_, err := os.Stat(filepath.Join(path, "hilbish", ".hilbishrc.lua"))
if err == nil {
dataDir = filepath.Join(path, "hilbish")
break
}
}
}
}
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 {
@ -141,10 +151,11 @@ 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
_, err = os.ReadFile(sampleConfPath) sampleConfigPath := filepath.Join(dataDir, ".hilbishrc.lua")
confpath = sampleConfPath _, err = os.ReadFile(sampleConfigPath)
confpath = sampleConfigPath
if err != nil { if err != nil {
fmt.Println("could not find .hilbishrc.lua or", sampleConfPath) fmt.Println("could not find .hilbishrc.lua or", sampleConfigPath)
return return
} }
} }
@ -313,15 +324,6 @@ 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()

61
nature/abbr.lua Normal file
View File

@ -0,0 +1,61 @@
-- @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)

View File

@ -3,8 +3,9 @@ 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
@ -16,13 +17,13 @@ commander.register('cd', function (args, sinks)
sinks.out:writeln(path) sinks.out:writeln(path)
end end
dirs.setOld(hilbish.cwd()) local absPath = fs.abs(path)
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)

View File

@ -1,10 +1,13 @@
-- @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 --- Last (current working) directory. Separate from recentDirs mainly for easier use.
--- 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 = {}
@ -35,19 +38,21 @@ 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 `d` to the recent directories list. --- Add `dir` to the recent directories list.
function dirs.push(d) --- @param dir string
function dirs.push(dir)
dirs.recentDirs[dirs.recentSize + 1] = nil dirs.recentDirs[dirs.recentSize + 1] = nil
if dirs.recentDirs[#dirs.recentDirs - 1] ~= d then if dirs.recentDirs[#dirs.recentDirs - 1] ~= dir then
ok, d = pcall(fs.abs, d) local ok, dir = pcall(fs.abs, dir)
assert(ok, 'could not turn "' .. d .. '"into an absolute path') assert(ok, 'could not turn "' .. dir .. '"into an absolute path')
table.insert(dirs.recentDirs, 1, d) table.insert(dirs.recentDirs, 1, dir)
end end
end end
@ -73,4 +78,9 @@ 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

View File

@ -1,13 +1,25 @@
-- @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 M = {} local doc = {}
function M.highlight(text) --- Performs basic Lua code highlighting.
--- @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
function M.renderCodeBlock(text) --- Assembles and renders a code block. This returns
--- the supplied text based on the number of command line columns,
--- and styles it to resemble a code block.
--- @param text string
function doc.renderCodeBlock(text)
local longest = 0 local longest = 0
local lines = string.split(text:gsub('\t', ' '), '\n') local lines = string.split(text:gsub('\t', ' '), '\n')
@ -17,14 +29,18 @@ function M.renderCodeBlock(text)
end end
for i, line in ipairs(lines) do for i, line in ipairs(lines) do
lines[i] = lunacolors.format('{greyBg}' .. ' ' .. M.highlight(line:sub(0, longest)) lines[i] = lunacolors.format('{greyBg}' .. ' ' .. doc.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
function M.renderInfoBlock(type, text) --- Renders an info block. An info block is a block of text with
--- an icon and styled text block.
--- @param type string Type of info block. The only one specially styled is the `warning`.
--- @param text string
function doc.renderInfoBlock(type, text)
local longest = 0 local longest = 0
local lines = string.split(text:gsub('\t', ' '), '\n') local lines = string.split(text:gsub('\t', ' '), '\n')
@ -34,7 +50,7 @@ function M.renderInfoBlock(type, text)
end end
for i, line in ipairs(lines) do for i, line in ipairs(lines) do
lines[i] = ' ' .. M.highlight(line:sub(0, longest)) lines[i] = ' ' .. doc.highlight(line:sub(0, longest))
.. string.rep(' ', longest - line:len()) .. ' ' .. string.rep(' ', longest - line:len()) .. ' '
end end
@ -44,4 +60,4 @@ function M.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 M return doc

View File

@ -1,4 +1,5 @@
-- Greenhouse is a simple text scrolling handler for terminal programs. -- @module greenhouse
-- 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

View File

@ -1,3 +1,4 @@
-- @module greenhouse.page
local Object = require 'nature.object' local Object = require 'nature.object'
local Page = Object:extend() local Page = Object:extend()
@ -10,6 +11,7 @@ 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

78
nature/hilbish.lua Normal file
View File

@ -0,0 +1,78 @@
-- @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

View File

@ -1,3 +1,14 @@
-- @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'
@ -44,6 +55,8 @@ 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
@ -52,16 +65,20 @@ function hilbish.messages.read(idx)
end end
end end
function hilbish.messages.readAll(idx) --- Marks all messages as read.
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
@ -71,12 +88,14 @@ 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

View File

@ -18,12 +18,15 @@ 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

View File

@ -2,9 +2,7 @@ local bait = require 'bait'
local lunacolors = require 'lunacolors' local lunacolors = require 'lunacolors'
hilbish.motd = [[ hilbish.motd = [[
Wait ... {magenta}2.3{reset} is basically the same as {red}2.2?{reset} {magenta}Hilbish{reset} blooms in the {blue}midnight.{reset}
Erm.. {blue}Ctrl-C works for Commanders,{reset} {cyan}and the sh runner has some fixes.{reset}
Just trust me bro, this is an important bug fix release. {red}- 🌺 sammyette{reset}
]] ]]
bait.catch('hilbish.init', function() bait.catch('hilbish.init', function()

View File

@ -1,4 +1,5 @@
--- hilbish.runner -- @module hilbish.runner
local snail = require 'snail'
local currentRunner = 'hybrid' local currentRunner = 'hybrid'
local runners = {} local runners = {}
@ -6,7 +7,7 @@ local runners = {}
hilbish = hilbish hilbish = hilbish
--- Get a runner by name. --- Get a runner by name.
--- @param name string --- @param name string Name of the runner to retrieve.
--- @return table --- @return table
function hilbish.runner.get(name) function hilbish.runner.get(name)
local r = runners[name] local r = runners[name]
@ -18,9 +19,9 @@ function hilbish.runner.get(name)
return r return r
end end
--- Adds a runner to the table of available runners. If runner is a table, --- Adds a runner to the table of available runners.
--- it must have the run function in it. --- If runner is a table, it must have the run function in it.
--- @param name string --- @param name string Name of the runner
--- @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
@ -42,7 +43,9 @@ function hilbish.runner.add(name, runner)
hilbish.runner.set(name, runner) hilbish.runner.set(name, runner)
end end
--- Sets a runner by name. The runner table must have the run function in it. --- *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.
--- @param name string --- @param name string
--- @param runner table --- @param runner table
function hilbish.runner.set(name, runner) function hilbish.runner.set(name, runner)
@ -53,11 +56,11 @@ function hilbish.runner.set(name, runner)
runners[name] = runner runners[name] = runner
end end
--- Executes cmd with a runner. If runnerName isn't passed, it uses --- Executes `cmd` with a runner.
--- the user's current runner. --- If `runnerName` is not specified, it uses the default Hilbish runner.
--- @param cmd string --- @param cmd string
--- @param runnerName string? --- @param runnerName string?
--- @return string, number, string --- @return table
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
@ -66,13 +69,11 @@ function hilbish.runner.exec(cmd, runnerName)
return r.run(cmd) return r.run(cmd)
end end
--- Sets the current interactive/command line runner mode. --- Sets Hilbish's runner mode by name.
--- @param name string --- @param name string
function hilbish.runner.setCurrent(name) function hilbish.runner.setCurrent(name)
local r = hilbish.runner.get(name) hilbish.runner.get(name) -- throws if it doesnt exist.
currentRunner = name currentRunner = name
hilbish.runner.setMode(r.run)
end end
--- Returns the current runner by name. --- Returns the current runner by name.
@ -81,6 +82,81 @@ 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)
@ -107,7 +183,5 @@ hilbish.runner.add('lua', function(input)
return hilbish.runner.lua(cmdStr) return hilbish.runner.lua(cmdStr)
end) end)
hilbish.runner.add('sh', function(input) hilbish.runner.add('sh', hilbish.runner.sh)
return hilbish.runner.sh(input) hilbish.runner.setCurrent 'hybrid'
end)

View File

@ -1,23 +0,0 @@
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
}

View File

@ -1,124 +0,0 @@
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
}

View File

@ -1,180 +0,0 @@
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
)

View File

@ -1,205 +0,0 @@
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))
}

View File

@ -1,77 +0,0 @@
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
}

View File

@ -1,548 +0,0 @@
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)
}

View File

@ -1,151 +0,0 @@
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
}

View File

@ -1,289 +0,0 @@
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
}

View File

@ -1,109 +0,0 @@
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
}

View File

@ -1,315 +0,0 @@
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))
}

View File

@ -1,171 +0,0 @@
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
}

View File

@ -56,3 +56,10 @@ 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)
}
}

View File

@ -707,6 +707,9 @@ 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)

View File

@ -142,6 +142,10 @@ 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

View File

@ -53,9 +53,7 @@ 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()
@ -64,44 +62,6 @@ 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, newline, 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("newline"), rt.BoolValue(newline))
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`

View File

@ -1,9 +0,0 @@
package main
import (
rt "github.com/arnodel/golua/runtime"
)
func Loader(rtm *rt.Runtime) rt.Value {
return rt.StringValue("hello world!")
}

Binary file not shown.

View File

@ -1,35 +1,32 @@
package main package util
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 sink is a structure that has input and/or output to/from a desination.
// a desination. type Sink struct{
type sink struct{ Rw *bufio.ReadWriter
writer *bufio.Writer
reader *bufio.Reader
file *os.File file *os.File
ud *rt.UserData UserData *rt.UserData
autoFlush bool autoFlush bool
} }
func setupSinkType(rtm *rt.Runtime) { func SinkLoader(rtm *rt.Runtime) *rt.Table {
sinkMeta := rt.NewTable() sinkMeta := rt.NewTable()
sinkMethods := rt.NewTable() sinkMethods := rt.NewTable()
sinkFuncs := map[string]util.LuaExport{ sinkFuncs := map[string]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},
@ -37,7 +34,7 @@ func setupSinkType(rtm *rt.Runtime) {
"write": {luaSinkWrite, 2, false}, "write": {luaSinkWrite, 2, false},
"writeln": {luaSinkWriteln, 2, false}, "writeln": {luaSinkWriteln, 2, false},
} }
util.SetExports(l, sinkMethods, sinkFuncs) SetExports(rtm, 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)
@ -64,9 +61,24 @@ func setupSinkType(rtm *rt.Runtime) {
} }
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)))
l.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta)) rtm.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
@ -82,11 +94,17 @@ 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.reader.ReadString('\n') line, err := s.Rw.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
} }
@ -113,7 +131,7 @@ func luaSinkRead(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return nil, err return nil, err
} }
str, _ := s.reader.ReadString('\n') str, _ := s.Rw.ReadString('\n')
return c.PushingNext1(t.Runtime, rt.StringValue(str)), nil return c.PushingNext1(t.Runtime, rt.StringValue(str)), nil
} }
@ -135,9 +153,9 @@ func luaSinkWrite(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return nil, err return nil, err
} }
s.writer.Write([]byte(data)) s.Rw.Write([]byte(data))
if s.autoFlush { if s.autoFlush {
s.writer.Flush() s.Rw.Flush()
} }
return c.Next(), nil return c.Next(), nil
@ -160,9 +178,9 @@ func luaSinkWriteln(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return nil, err return nil, err
} }
s.writer.Write([]byte(data + "\n")) s.Rw.Write([]byte(data + "\n"))
if s.autoFlush { if s.autoFlush {
s.writer.Flush() s.Rw.Flush()
} }
return c.Next(), nil return c.Next(), nil
@ -181,7 +199,7 @@ func luaSinkFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return nil, err return nil, err
} }
s.writer.Flush() s.Rw.Flush()
return c.Next(), nil return c.Next(), nil
} }
@ -212,11 +230,25 @@ func luaSinkAutoFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return c.Next(), nil return c.Next(), nil
} }
func newSinkInput(r io.Reader) *sink { func NewSink(rtm *rt.Runtime, Rw io.ReadWriter) *Sink {
s := &sink{ s := &Sink{
reader: bufio.NewReader(r), Rw: bufio.NewReadWriter(bufio.NewReader(Rw), bufio.NewWriter(Rw)),
autoFlush: true,
} }
s.ud = sinkUserData(s) s.UserData = sinkUserData(rtm, s)
if f, ok := Rw.(*os.File); ok {
s.file = f
}
return s
}
func NewSinkInput(rtm *rt.Runtime, r io.Reader) *Sink {
s := &Sink{
Rw: bufio.NewReadWriter(bufio.NewReader(r), nil),
}
s.UserData = sinkUserData(rtm, s)
if f, ok := r.(*os.File); ok { if f, ok := r.(*os.File); ok {
s.file = f s.file = f
@ -225,17 +257,17 @@ func newSinkInput(r io.Reader) *sink {
return s return s
} }
func newSinkOutput(w io.Writer) *sink { func NewSinkOutput(rtm *rt.Runtime, w io.Writer) *Sink {
s := &sink{ s := &Sink{
writer: bufio.NewWriter(w), Rw: bufio.NewReadWriter(nil, bufio.NewWriter(w)),
autoFlush: true, autoFlush: true,
} }
s.ud = sinkUserData(s) s.UserData = sinkUserData(rtm, 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)
@ -244,17 +276,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(s *sink) *rt.UserData { func sinkUserData(rtm *rt.Runtime, s *Sink) *rt.UserData {
sinkMeta := l.Registry(sinkMetaKey) sinkMeta := rtm.Registry(sinkMetaKey)
return rt.NewUserData(s, sinkMeta.AsTable()) return rt.NewUserData(s, sinkMeta.AsTable())
} }

11
util/streams.go Normal file
View File

@ -0,0 +1,11 @@
package util
import (
"io"
)
type Streams struct {
Stdout io.Writer
Stderr io.Writer
Stdin io.Reader
}

View File

@ -2,14 +2,78 @@ 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) {
@ -36,6 +100,15 @@ 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)
@ -141,3 +214,67 @@ 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
}

View File

@ -1,17 +1,12 @@
//go:build unix //go:build unix
package main package util
import ( import (
"os" "os"
"syscall"
) )
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{ func FindExecutable(path string, inPath, dirs bool) error {
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
@ -25,5 +20,5 @@ func findExecutable(path string, inPath, dirs bool) error {
return nil return nil
} }
} }
return errNotExec return ErrNotExec
} }

View File

@ -1,18 +1,13 @@
//go:build windows //go:build windows
package main package util
import ( import (
"path/filepath" "path/filepath"
"os" "os"
"syscall"
) )
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{ func FindExecutable(path string, inPath, dirs bool) error {
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 {
@ -26,15 +21,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
} }
} }

View File

@ -11,8 +11,8 @@ var (
// Version info // Version info
var ( var (
ver = "v2.3.4" ver = "v2.4.0"
releaseName = "Alyssum" releaseName = "Moonflower"
gitCommit string gitCommit string
gitBranch string gitBranch string

View File

@ -15,7 +15,5 @@ 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")
) )

View File

@ -14,8 +14,6 @@ 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 = "/usr/local/share/hilbish" dataDir = ""
preloadPath = dataDir + "/nature/init.lua"
sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config
defaultConfDir = "" defaultConfDir = ""
) )

View File

@ -10,8 +10,6 @@ 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 = ""
) )