2
2
mirror of https://github.com/Hilbis/Hilbish synced 2025-04-14 17:43:22 +00:00

refactor: decouple sh use in core exec code (#337)

This commit is contained in:
sammyette 2025-04-03 00:38:35 -04:00 committed by GitHub
parent fe4e972fbe
commit 02c89b99dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1631 additions and 1066 deletions

152
api.go
View File

@ -13,10 +13,9 @@
package main
import (
"bytes"
//"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"runtime"
@ -28,9 +27,9 @@ import (
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
"github.com/arnodel/golua/lib/iolib"
//"github.com/arnodel/golua/lib/iolib"
"github.com/maxlandon/readline"
"mvdan.cc/sh/v3/interp"
//"mvdan.cc/sh/v3/interp"
)
var exports = map[string]util.LuaExport{
@ -39,7 +38,6 @@ var exports = map[string]util.LuaExport{
"complete": {hlcomplete, 2, false},
"cwd": {hlcwd, 0, false},
"exec": {hlexec, 1, false},
"runnerMode": {hlrunnerMode, 1, false},
"goro": {hlgoro, 1, true},
"highlighter": {hlhighlighter, 1, false},
"hinter": {hlhinter, 1, false},
@ -49,7 +47,6 @@ var exports = map[string]util.LuaExport{
"inputMode": {hlinputMode, 1, false},
"interval": {hlinterval, 2, false},
"read": {hlread, 1, false},
"run": {hlrun, 1, true},
"timeout": {hltimeout, 2, false},
"which": {hlwhich, 1, false},
}
@ -134,6 +131,9 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) {
pluginModule := moduleLoader(rtm)
mod.Set(rt.StringValue("module"), rt.TableValue(pluginModule))
sinkModule := util.SinkLoader(l)
mod.Set(rt.StringValue("sink"), rt.TableValue(sinkModule))
return rt.TableValue(mod), nil
}
@ -154,6 +154,7 @@ func unsetVimMode() {
util.SetField(l, hshMod, "vimMode", rt.NilValue)
}
/*
func handleStream(v rt.Value, strms *streams, errStream bool) error {
ud, ok := v.TryUserData()
if !ok {
@ -182,112 +183,7 @@ func handleStream(v rt.Value, strms *streams, errStream bool) error {
return nil
}
// run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)
// Runs `cmd` in Hilbish's shell script interpreter.
// The `streams` parameter specifies the output and input streams the command should use.
// For example, to write command output to a sink.
// As a table, the caller can directly specify the standard output, error, and input
// streams of the command with the table keys `out`, `err`, and `input` respectively.
// As a boolean, it specifies whether the command should use standard output or return its output streams.
// #param cmd string
// #param streams table|boolean
// #returns number, string, string
// #example
/*
// This code is the same as `ls -l | wc -l`
local fs = require 'fs'
local pr, pw = fs.pipe()
hilbish.run('ls -l', {
stdout = pw,
stderr = pw,
})
pw:close()
hilbish.run('wc -l', {
stdin = pr
})
*/
// #example
func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// TODO: ON BREAKING RELEASE, DO NOT ACCEPT `streams` AS A BOOLEAN.
if err := c.Check1Arg(); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
strms := &streams{}
var terminalOut bool
if len(c.Etc()) != 0 {
tout := c.Etc()[0]
var ok bool
terminalOut, ok = tout.TryBool()
if !ok {
luastreams, ok := tout.TryTable()
if !ok {
return nil, errors.New("bad argument to run (expected boolean or table, got " + tout.TypeName() + ")")
}
handleStream(luastreams.Get(rt.StringValue("out")), strms, false)
handleStream(luastreams.Get(rt.StringValue("err")), strms, true)
stdinstrm := luastreams.Get(rt.StringValue("input"))
if !stdinstrm.IsNil() {
ud, ok := stdinstrm.TryUserData()
if !ok {
return nil, errors.New("bad type as run stdin stream (expected userdata as either sink or file, got " + stdinstrm.TypeName() + ")")
}
val := ud.Value()
var varstrm io.Reader
if f, ok := val.(*iolib.File); ok {
varstrm = f.Handle()
}
if f, ok := val.(*sink); ok {
varstrm = f.reader
}
if varstrm == nil {
return nil, errors.New("bad type as run stdin stream (expected userdata as either sink or file)")
}
strms.stdin = varstrm
}
} else {
if !terminalOut {
strms = &streams{
stdout: new(bytes.Buffer),
stderr: new(bytes.Buffer),
}
}
}
}
var exitcode uint8
stdout, stderr, err := execCommand(cmd, strms)
if code, ok := interp.IsExitStatus(err); ok {
exitcode = code
} else if err != nil {
exitcode = 1
}
var stdoutStr, stderrStr string
if stdoutBuf, ok := stdout.(*bytes.Buffer); ok {
stdoutStr = stdoutBuf.String()
}
if stderrBuf, ok := stderr.(*bytes.Buffer); ok {
stderrStr = stderrBuf.String()
}
return c.PushingNext(t.Runtime, rt.IntValue(int64(exitcode)), rt.StringValue(stdoutStr), rt.StringValue(stderrStr)), nil
}
// cwd() -> string
// Returns the current directory of the shell.
@ -404,7 +300,7 @@ hilbish.multiprompt '-->'
*/
func hlmultiprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
return c.PushingNext1(t.Runtime, rt.StringValue(multilinePrompt)), nil
}
prompt, err := c.StringArg(0)
if err != nil {
@ -508,7 +404,7 @@ func hlexec(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
}
cmdArgs, _ := splitInput(cmd)
if runtime.GOOS != "windows" {
cmdPath, err := exec.LookPath(cmdArgs[0])
cmdPath, err := util.LookPath(cmdArgs[0])
if err != nil {
fmt.Println(err)
// 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
}
path, err := exec.LookPath(cmd)
path, err := util.LookPath(cmd)
if err != nil {
return c.Next(), nil
}
@ -742,34 +638,6 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return c.Next(), nil
}
// runnerMode(mode)
// Sets the execution/runner mode for interactive Hilbish.
// This determines whether Hilbish wll try to run input as Lua
// and/or sh or only do one of either.
// Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua),
// sh, and lua. It also accepts a function, to which if it is passed one
// will call it to execute user input instead.
// Read [about runner mode](../features/runner-mode) for more information.
// #param mode string|function
func hlrunnerMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
mode := c.Arg(0)
switch mode.Type() {
case rt.StringType:
switch mode.AsString() {
case "hybrid", "hybridRev", "lua", "sh": runnerMode = mode
default: return nil, errors.New("execMode: expected either a function or hybrid, hybridRev, lua, sh. Received " + mode.AsString())
}
case rt.FunctionType: runnerMode = mode
default: return nil, errors.New("execMode: expected either a function or hybrid, hybridRev, lua, sh. Received " + mode.TypeName())
}
return c.Next(), nil
}
// hinter(line, pos)
// The command line hint handler. It gets called on every key insert to
// determine what text to use as an inline hint. It is passed the current

View File

@ -84,6 +84,7 @@ var prefix = map[string]string{
"commander": "c",
"bait": "b",
"terminal": "term",
"snail": "snail",
}
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 {
if fun.Doc == "" {
return nil
}
docs := strings.TrimSpace(fun.Doc)
tags, parts := getTagsAndDocs(docs)
@ -320,7 +325,7 @@ provided by Hilbish.
os.Mkdir("emmyLuaDocs", 0777)
dirs := []string{"./"}
dirs := []string{"./", "./util"}
filepath.Walk("golibs/", func (path string, info os.FileInfo, err error) error {
if !info.IsDir() {
return nil
@ -347,7 +352,7 @@ provided by Hilbish.
pieces := []docPiece{}
typePieces := []docPiece{}
mod := l
if mod == "main" {
if mod == "main" || mod == "util" {
mod = "hilbish"
}
var hasInterfaces bool
@ -431,14 +436,23 @@ provided by Hilbish.
interfaceModules[modname].Types = append(interfaceModules[modname].Types, piece)
}
docs[mod] = module{
Types: filteredTypePieces,
Docs: filteredPieces,
ShortDescription: shortDesc,
Description: strings.Join(desc, "\n"),
HasInterfaces: hasInterfaces,
Properties: docPieceTag("property", tags),
Fields: docPieceTag("field", tags),
fmt.Println(filteredTypePieces)
if newDoc, ok := docs[mod]; ok {
oldMod := docs[mod]
newDoc.Types = append(filteredTypePieces, oldMod.Types...)
newDoc.Docs = append(filteredPieces, oldMod.Docs...)
docs[mod] = newDoc
} else {
docs[mod] = module{
Types: filteredTypePieces,
Docs: filteredPieces,
ShortDescription: shortDesc,
Description: strings.Join(desc, "\n"),
HasInterfaces: hasInterfaces,
Properties: docPieceTag("property", tags),
Fields: docPieceTag("field", tags),
}
}
}

View File

@ -15,7 +15,6 @@ for _, fname in ipairs(files) do
local mod = header:match(modpattern)
if not mod then goto continue end
print(fname, mod)
pieces[mod] = {}
descriptions[mod] = {}
@ -42,10 +41,12 @@ for _, fname in ipairs(files) do
local dps = {
description = {},
example = {},
params = {}
}
local offset = 1
local doingExample = false
while true do
local prev = lines[lineno - offset]
@ -59,17 +60,25 @@ for _, fname in ipairs(files) do
if emmy then
if emmy == 'param' then
print('bruh', emmythings[1], emmythings[2])
table.insert(dps.params, 1, {
name = emmythings[1],
type = emmythings[2],
-- the +1 accounts for space.
description = table.concat(emmythings, ' '):sub(emmythings[1]:len() + 1 + emmythings[2]:len() + 1)
})
print(table.concat(emmythings, '/'))
end
else
table.insert(dps.description, 1, docline)
if docline:match '#example' then
doingExample = not doingExample
end
if not docline:match '#example' then
if doingExample then
table.insert(dps.example, 1, docline)
else
table.insert(dps.description, 1, docline)
end
end
end
offset = offset + 1
else
@ -77,7 +86,7 @@ for _, fname in ipairs(files) do
end
end
pieces[mod][funcName] = dps
table.insert(pieces[mod], {funcName, dps})
end
docPiece = {}
goto continue2
@ -109,11 +118,15 @@ for iface, dps in pairs(pieces) do
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)
local exists = pcall(fs.stat, path)
local newOrNotNature = exists and mod ~= 'nature'
local newOrNotNature = (exists and mod ~= 'nature') or iface == 'hilbish'
local f <close> = io.open(path, newOrNotNature and 'r+' or 'w+')
local tocPos
@ -129,9 +142,6 @@ for iface, dps in pairs(pieces) do
tocPos = f:seek()
end
end
print(f)
print('mod and path:', mod, path)
local tocSearch = false
for line in f:lines() do
@ -144,7 +154,10 @@ for iface, dps in pairs(pieces) do
end
end
for func, docs in pairs(dps) do
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 params = ''
for idx, param in ipairs(docs.params) do
@ -186,6 +199,10 @@ for iface, dps in pairs(pieces) do
f:write(string.format('`%s` **`%s`** \n', param.name:gsub('%?$', ''), param.type))
f:write(string.format('%s\n\n', param.description))
end
if #docs.example ~= 0 then
f:write '#### Example\n'
f:write(string.format('```lua\n%s\n```\n', table.concat(docs.example, '\n')))
end
--[[
local params = table.filter(docs, function(t)
return t:match '^%-%-%- @param'

View File

@ -98,7 +98,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
if len(fileCompletions) != 0 {
for _, f := range fileCompletions {
fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref)))
if err := findExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil {
if err := util.FindExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil {
continue
}
completions = append(completions, f)
@ -115,7 +115,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
// get basename from matches
for _, match := range matches {
// check if we have execute permissions for our match
err := findExecutable(match, true, false)
err := util.FindExecutable(match, true, false)
if err != nil {
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="#prompt">prompt(str, typ)</a>|Changes the shell prompt to the provided string.|
|<a href="#read">read(prompt) -> input (string)</a>|Read input from the user, using Hilbish's line editor/input reader.|
|<a href="#run">run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)</a>|Runs `cmd` in Hilbish's shell script interpreter.|
|<a href="#runnerMode">runnerMode(mode)</a>|Sets the execution/runner mode for interactive Hilbish.|
|<a href="#timeout">timeout(cb, time) -> @Timer</a>|Executed the `cb` function after a period of `time`.|
|<a href="#which">which(name) -> string</a>|Checks if `name` is a valid command.|
|<a href="#runnerMode">runnerMode(mode)</a>|Sets the execution/runner mode for interactive Hilbish.|
|<a href="#run">run(cmd, streams)</a>|Runs `cmd` in Hilbish's shell script interpreter.|
## Static module fields
|||
@ -408,72 +408,6 @@ Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs.
`string` **`prompt?`**
Text to print before input, can be empty.
</div>
<hr>
<div id='run'>
<h4 class='heading'>
hilbish.run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)
<a href="#run" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Runs `cmd` in Hilbish's shell script interpreter.
The `streams` parameter specifies the output and input streams the command should use.
For example, to write command output to a sink.
As a table, the caller can directly specify the standard output, error, and input
streams of the command with the table keys `out`, `err`, and `input` respectively.
As a boolean, it specifies whether the command should use standard output or return its output streams.
#### Parameters
`string` **`cmd`**
`table|boolean` **`streams`**
#### Example
```lua
// This code is the same as `ls -l | wc -l`
local fs = require 'fs'
local pr, pw = fs.pipe()
hilbish.run('ls -l', {
stdout = pw,
stderr = pw,
})
pw:close()
hilbish.run('wc -l', {
stdin = pr
})
```
</div>
<hr>
<div id='runnerMode'>
<h4 class='heading'>
hilbish.runnerMode(mode)
<a href="#runnerMode" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Sets the execution/runner mode for interactive Hilbish.
This determines whether Hilbish wll try to run input as Lua
and/or sh or only do one of either.
Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua),
sh, and lua. It also accepts a function, to which if it is passed one
will call it to execute user input instead.
Read [about runner mode](../features/runner-mode) for more information.
#### Parameters
`string|function` **`mode`**
</div>
<hr>
@ -519,8 +453,7 @@ Will return the path of the binary, or a basename if it's a commander.
<hr>
## Sink
A sink is a structure that has input and/or output to/from
a desination.
A sink is a structure that has input and/or output to/from a desination.
### Methods
#### autoFlush(auto)
@ -542,3 +475,65 @@ Writes data to a sink.
#### writeln(str)
Writes data to a sink with a newline at the end.
<hr>
<div id='run'>
<h4 class='heading'>
hilbish.run(cmd, streams)
<a href="#run" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Runs `cmd` in Hilbish's shell script interpreter.
The `streams` parameter specifies the output and input streams the command should use.
For example, to write command output to a sink.
As a table, the caller can directly specify the standard output, error, and input
streams of the command with the table keys `out`, `err`, and `input` respectively.
As a boolean, it specifies whether the command should use standard output or return its output streams.
#### Parameters
`cmd` **`string`**
`streams` **`table|boolean`**
#### Example
```lua
-- This code is the same as `ls -l | wc -l`
local fs = require 'fs'
local pr, pw = fs.pipe()
hilbish.run('ls -l', {
stdout = pw,
stderr = pw,
})
pw:close()
hilbish.run('wc -l', {
stdin = pr
})
```
</div>
<hr>
<div id='runnerMode'>
<h4 class='heading'>
hilbish.runnerMode(mode)
<a href="#runnerMode" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Sets the execution/runner mode for interactive Hilbish.
**NOTE: This function is deprecated and will be removed in 3.0**
Use `hilbish.runner.setCurrent` instead.
This determines whether Hilbish wll try to run input as Lua
and/or sh or only do one of either.
Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua),
sh, and lua. It also accepts a function, to which if it is passed one
will call it to execute user input instead.
Read [about runner mode](../features/runner-mode) for more information.
#### Parameters
`mode` **`string|function`**
</div>

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
|||
|----|----|
|<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.sh">sh(cmd)</a>|Runs a command in Hilbish's shell script interpreter.|
<hr>
<div id='runner.setMode'>
<h4 class='heading'>
hilbish.runner.setMode(cb)
<a href="#runner.setMode" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
This is the same as the `hilbish.runnerMode` function.
It takes a callback, which will be used to execute all interactive input.
In normal cases, neither callbacks should be overrided by the user,
as the higher level functions listed below this will handle it.
#### Parameters
`function` **`cb`**
</div>
|<a href="#sh">sh()</a>|nil|
|<a href="#setMode">setMode(mode)</a>|**NOTE: This function is deprecated and will be removed in 3.0**|
|<a href="#setCurrent">setCurrent(name)</a>|Sets Hilbish's runner mode by name.|
|<a href="#set">set(name, runner)</a>|*Sets* a runner by name. The difference between this function and|
|<a href="#run">run(input, priv)</a>|Runs `input` with the currently set Hilbish runner.|
|<a href="#getCurrent">getCurrent()</a>|Returns the current runner by name.|
|<a href="#get">get(name)</a>|Get a runner by name.|
|<a href="#exec">exec(cmd, runnerName)</a>|Executes `cmd` with a runner.|
|<a href="#add">add(name, runner)</a>|Adds a runner to the table of available runners.|
<hr>
<div id='runner.lua'>
@ -97,20 +84,164 @@ or `load`, but is appropriated for the runner interface.
</div>
<hr>
<div id='runner.sh'>
<div id='add'>
<h4 class='heading'>
hilbish.runner.sh(cmd)
<a href="#runner.sh" class='heading-link'>
hilbish.runner.add(name, runner)
<a href="#add" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Runs a command in Hilbish's shell script interpreter.
This is the equivalent of using `source`.
Adds a runner to the table of available runners.
If runner is a table, it must have the run function in it.
#### Parameters
`string` **`cmd`**
`name` **`string`**
Name of the runner
`runner` **`function|table`**
</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

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

View File

@ -15,43 +15,11 @@ directories.
## 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.|
|<a href="#push">push(dir)</a>|Add `dir` to the recent directories list.|
|<a href="#setOld">setOld(d)</a>|Sets the old directory string.|
<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>
<hr>
<div id='push'>
<h4 class='heading'>
dirs.push(dir)
<a href="#push" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Add `dir` to the recent directories list.
#### Parameters
`dir` **`string`**
</div>
<hr>
<div id='peak'>
<h4 class='heading'>
@ -83,6 +51,22 @@ Remove the specified amount of dirs from the recent directories list.
`num` **`number`**
</div>
<hr>
<div id='push'>
<h4 class='heading'>
dirs.push(dir)
<a href="#push" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Add `dir` to the recent directories list.
#### Parameters
`dir` **`string`**
</div>
<hr>
@ -101,3 +85,19 @@ Get entry from recent directories list based on index.
</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>

View File

@ -17,29 +17,9 @@ 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.|
|<a href="#renderInfoBlock">renderInfoBlock(type, text)</a>|Renders an info block. An info block is a block of text with|
<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>
<hr>
<div id='highlight'>
<h4 class='heading'>
@ -74,3 +54,23 @@ and styles it to resemble a code block.
</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

@ -7,12 +7,6 @@ local hilbish = {}
--- @param cmd string
function hilbish.aliases.add(alias, cmd) end
--- This is the same as the `hilbish.runnerMode` function.
--- It takes a callback, which will be used to execute all interactive input.
--- In normal cases, neither callbacks should be overrided by the user,
--- as the higher level functions listed below this will handle it.
function hilbish.runner.setMode(cb) end
--- Returns the current input line.
function hilbish.editor.getLine() end
@ -131,24 +125,6 @@ function hilbish.prompt(str, typ) end
--- Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs.
function hilbish.read(prompt) end
--- Runs `cmd` in Hilbish's shell script interpreter.
--- The `streams` parameter specifies the output and input streams the command should use.
--- For example, to write command output to a sink.
--- As a table, the caller can directly specify the standard output, error, and input
--- streams of the command with the table keys `out`, `err`, and `input` respectively.
--- As a boolean, it specifies whether the command should use standard output or return its output streams.
---
function hilbish.run(cmd, streams) end
--- Sets the execution/runner mode for interactive Hilbish.
--- This determines whether Hilbish wll try to run input as Lua
--- and/or sh or only do one of either.
--- Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua),
--- sh, and lua. It also accepts a function, to which if it is passed one
--- will call it to execute user input instead.
--- Read [about runner mode](../features/runner-mode) for more information.
function hilbish.runnerMode(mode) end
--- Executed the `cb` function after a period of `time`.
--- This creates a Timer that starts ticking immediately.
function hilbish.timeout(cb, time) end
@ -168,28 +144,6 @@ function hilbish.jobs:foreground() end
--- or `load`, but is appropriated for the runner interface.
function hilbish.runner.lua(cmd) end
--- Sets/toggles the option of automatically flushing output.
--- A call with no argument will toggle the value.
--- @param auto boolean|nil
function hilbish:autoFlush(auto) end
--- Flush writes all buffered input to the sink.
function hilbish:flush() end
--- Reads a liine of input from the sink.
--- @returns string
function hilbish:read() end
--- Reads all input from the sink.
--- @returns string
function hilbish:readAll() end
--- Writes data to a sink.
function hilbish:write(str) end
--- Writes data to a sink with a newline at the end.
function hilbish:writeln(str) end
--- Starts running the job.
function hilbish.jobs:start() end
@ -200,10 +154,6 @@ function hilbish.jobs:stop() end
--- It will throw if any error occurs.
function hilbish.module.load(path) end
--- Runs a command in Hilbish's shell script interpreter.
--- This is the equivalent of using `source`.
function hilbish.runner.sh(cmd) end
--- Starts a timer.
function hilbish.timers:start() end
@ -262,4 +212,26 @@ function hilbish.timers.create(type, time, callback) end
--- Retrieves a timer via its ID.
function hilbish.timers.get(id) end
--- Sets/toggles the option of automatically flushing output.
--- A call with no argument will toggle the value.
--- @param auto boolean|nil
function hilbish:autoFlush(auto) end
--- Flush writes all buffered input to the sink.
function hilbish:flush() end
--- Reads a liine of input from the sink.
--- @returns string
function hilbish:read() end
--- Reads all input from the sink.
--- @returns string
function hilbish:readAll() end
--- Writes data to a sink.
function hilbish:write(str) end
--- Writes data to a sink with a newline at the end.
function hilbish:writeln(str) end
return hilbish

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

534
exec.go
View File

@ -1,215 +1,26 @@
package main
import (
"bytes"
"context"
"errors"
"os/exec"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
"mvdan.cc/sh/v3/shell"
//"github.com/yuin/gopher-lua/parse"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
"mvdan.cc/sh/v3/expand"
)
var errNotExec = errors.New("not executable")
var errNotFound = errors.New("not found")
var runnerMode rt.Value = rt.StringValue("hybrid")
type streams struct {
stdout io.Writer
stderr io.Writer
stdin io.Reader
}
type execError struct{
typ string
cmd string
code int
colon bool
err error
}
func (e execError) Error() string {
return fmt.Sprintf("%s: %s", e.cmd, e.typ)
}
func (e execError) sprint() error {
sep := " "
if e.colon {
sep = ": "
}
return fmt.Errorf("hilbish: %s%s%s", e.cmd, sep, e.err.Error())
}
func isExecError(err error) (execError, bool) {
if exErr, ok := err.(execError); ok {
return exErr, true
}
fields := strings.Split(err.Error(), ": ")
knownTypes := []string{
"not-found",
"not-executable",
}
if len(fields) > 1 && contains(knownTypes, fields[1]) {
var colon bool
var e error
switch fields[1] {
case "not-found":
e = errNotFound
case "not-executable":
colon = true
e = errNotExec
}
return execError{
cmd: fields[0],
typ: fields[1],
colon: colon,
err: e,
}, true
}
return execError{}, false
}
var runnerMode rt.Value = rt.NilValue
func runInput(input string, priv bool) {
running = true
cmdString := aliases.Resolve(input)
hooks.Emit("command.preexec", input, cmdString)
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 {
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)
runnerRun := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("run"))
_, err := rt.Call1(l.MainThread(), runnerRun, rt.StringValue(input), rt.BoolValue(priv))
if err != nil {
return "", 124, false, false, nil, err
fmt.Fprintln(os.Stderr, 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) {
@ -239,326 +50,13 @@ func handleLua(input string) (string, uint8, error) {
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) {
// end my suffering
// TODO: refactor this garbage
quoted := false
startlastcmd := false
lastcmddone := false
cmdArgs := []string{}
sb := &strings.Builder{}
cmdstr := &strings.Builder{}
lastcmd := "" //readline.GetHistory(readline.HistorySize() - 1)
for _, r := range input {
if r == '"' {
@ -574,22 +72,6 @@ func splitInput(input string) ([]string, string) {
// if not quoted and there's a space then add to cmdargs
cmdArgs = append(cmdArgs, sb.String())
sb.Reset()
} else if !quoted && r == '^' && startlastcmd && !lastcmddone {
// if ^ is found, isnt in quotes and is
// the second occurence of the character and is
// the first time "^^" has been used
cmdstr.WriteString(lastcmd)
sb.WriteString(lastcmd)
startlastcmd = !startlastcmd
lastcmddone = !lastcmddone
continue
} else if !quoted && r == '^' && !lastcmddone {
// if ^ is found, isnt in quotes and is the
// first time of starting "^^"
startlastcmd = !startlastcmd
continue
} else {
sb.WriteRune(r)
}
@ -601,11 +83,3 @@ func splitInput(input string) ([]string, string) {
return cmdArgs, cmdstr.String()
}
func cmdFinish(code uint8, cmdstr string, private bool) {
util.SetField(l, hshMod, "exitCode", rt.IntValue(int64(code)))
// using AsValue (to convert to lua type) on an interface which is an int
// results in it being unknown in lua .... ????
// so we allow the hook handler to take lua runtime Values
hooks.Emit("command.exit", rt.IntValue(int64(code)), cmdstr, private)
}

View File

@ -19,38 +19,25 @@ import (
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
"github.com/arnodel/golua/lib/iolib"
"mvdan.cc/sh/v3/interp"
)
type fs struct{
runner *interp.Runner
Loader packagelib.Loader
var Loader = packagelib.Loader{
Load: loaderFunc,
Name: "fs",
}
func New(runner *interp.Runner) *fs {
f := &fs{
runner: runner,
}
f.Loader = packagelib.Loader{
Load: f.loaderFunc,
Name: "fs",
}
return f
}
func (f *fs) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
exports := map[string]util.LuaExport{
"cd": util.LuaExport{f.fcd, 1, false},
"mkdir": util.LuaExport{f.fmkdir, 2, false},
"stat": util.LuaExport{f.fstat, 1, false},
"readdir": util.LuaExport{f.freaddir, 1, false},
"abs": util.LuaExport{f.fabs, 1, false},
"basename": util.LuaExport{f.fbasename, 1, false},
"dir": util.LuaExport{f.fdir, 1, false},
"glob": util.LuaExport{f.fglob, 1, false},
"join": util.LuaExport{f.fjoin, 0, true},
"pipe": util.LuaExport{f.fpipe, 0, false},
"cd": util.LuaExport{fcd, 1, false},
"mkdir": util.LuaExport{fmkdir, 2, false},
"stat": util.LuaExport{fstat, 1, false},
"readdir": util.LuaExport{freaddir, 1, false},
"abs": util.LuaExport{fabs, 1, false},
"basename": util.LuaExport{fbasename, 1, false},
"dir": util.LuaExport{fdir, 1, false},
"glob": util.LuaExport{fglob, 1, false},
"join": util.LuaExport{fjoin, 0, true},
"pipe": util.LuaExport{fpipe, 0, false},
}
mod := rt.NewTable()
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`.
// #param path 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)
if err != nil {
return nil, err
@ -85,7 +72,7 @@ func (f *fs) fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// `.` will be returned.
// #param path string Path to get the base name of.
// #returns string
func (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 {
return nil, err
}
@ -100,7 +87,7 @@ func (f *fs) fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// cd(dir)
// Changes Hilbish's directory to `dir`.
// #param dir string Path to change directory to.
func (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 {
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))
abspath, _ := filepath.Abs(path)
err = os.Chdir(path)
if err != nil {
return nil, err
}
interp.Dir(abspath)(f.runner)
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`.
// #param path string Path to get the directory for.
// #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 {
return nil, err
}
@ -156,7 +141,7 @@ print(matches)
-- -> {'init.lua', 'code.lua'}
#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 {
return nil, err
}
@ -190,7 +175,7 @@ print(fs.join(hilbish.userDir.config, 'hilbish'))
-- -> '/home/user/.config/hilbish' on Linux
#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()))
for i, v := range c.Etc() {
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)
#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 {
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.
// #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()
if err != nil {
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.
// #param dir string
// #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 {
return nil, err
}
@ -311,7 +296,7 @@ Would print the following:
]]--
#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 {
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)
}
// bgProcAttr is defined in execfile_<os>.go, it holds a procattr struct
// in a simple explanation, it makes signals from hilbish (sigint)
// bgProcAttr is defined in job_<os>.go, it holds a procattr struct
// in a simple explanation, it makes signals from hilbish (like sigint)
// not go to it (child process)
j.handle.SysProcAttr = bgProcAttr
// reset output buffers
@ -136,7 +136,7 @@ func luaStartJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if !j.running {
err := j.start()
exit := handleExecErr(err)
exit := util.HandleExecErr(err)
j.exitCode = int(exit)
j.finish()
}

View File

@ -10,6 +10,10 @@ import (
"golang.org/x/sys/unix"
)
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
func (j *job) foreground() error {
if jobs.foreground {
return errors.New("(another) job already foregrounded")

View File

@ -4,8 +4,13 @@ package main
import (
"errors"
"syscall"
)
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
func (j *job) foreground() error {
return errors.New("not supported on windows")
}

7
lua.go
View File

@ -9,6 +9,7 @@ import (
"hilbish/golibs/bait"
"hilbish/golibs/commander"
"hilbish/golibs/fs"
"hilbish/golibs/snail"
"hilbish/golibs/terminal"
rt "github.com/arnodel/golua/runtime"
@ -24,16 +25,14 @@ func luaInit() {
MessageHandler: debuglib.Traceback,
})
lib.LoadAll(l)
setupSinkType(l)
lib.LoadLibs(l, hilbishLoader)
// yes this is stupid, i know
util.DoString(l, "hilbish = require 'hilbish'")
// Add fs and terminal module module to Lua
f := fs.New(runner)
lib.LoadLibs(l, f.Loader)
lib.LoadLibs(l, fs.Loader)
lib.LoadLibs(l, terminal.Loader)
lib.LoadLibs(l, snail.Loader)
cmds = commander.New(l)
lib.LoadLibs(l, cmds.Loader)

12
main.go
View File

@ -21,7 +21,6 @@ import (
"github.com/pborman/getopt"
"github.com/maxlandon/readline"
"golang.org/x/term"
"mvdan.cc/sh/v3/interp"
)
var (
@ -38,7 +37,6 @@ var (
cmds *commander.Commander
defaultConfPath string
defaultHistPath string
runner *interp.Runner
)
func main() {
@ -58,7 +56,6 @@ func main() {
}
}
runner, _ = interp.New()
curuser, _ = user.Current()
confDir, _ = os.UserConfigDir()
@ -327,15 +324,6 @@ func removeDupes(slice []string) []string {
return newSlice
}
func contains(s []string, e string) bool {
for _, a := range s {
if strings.ToLower(a) == strings.ToLower(e) {
return true
}
}
return false
}
func exit(code int) {
jobs.stopAll()

View File

@ -3,8 +3,9 @@ local commander = require 'commander'
local fs = require 'fs'
local dirs = require 'nature.dirs'
dirs.old = hilbish.cwd()
commander.register('cd', function (args, sinks)
local oldPath = hilbish.cwd()
if #args > 1 then
sinks.out:writeln("cd: too many arguments")
return 1
@ -16,13 +17,13 @@ commander.register('cd', function (args, sinks)
sinks.out:writeln(path)
end
dirs.setOld(hilbish.cwd())
dirs.push(path)
local absPath = fs.abs(path)
local ok, err = pcall(function() fs.cd(path) end)
if not ok then
sinks.out:writeln(err)
return 1
end
bait.throw('cd', path)
bait.throw('cd', path, oldPath)
bait.throw('hilbish.cd', absPath, oldPath)
end)

View File

@ -2,6 +2,7 @@
-- internal directory management
-- The dirs module defines a small set of functions to store and manage
-- directories.
local bait = require 'bait'
local fs = require 'fs'
local dirs = {}
@ -47,11 +48,11 @@ end
--- @param dir string
function dirs.push(dir)
dirs.recentDirs[dirs.recentSize + 1] = nil
if dirs.recentDirs[#dirs.recentDirs - 1] ~= d then
ok, d = pcall(fs.abs, d)
assert(ok, 'could not turn "' .. d .. '"into an absolute path')
if dirs.recentDirs[#dirs.recentDirs - 1] ~= dir then
local ok, dir = pcall(fs.abs, dir)
assert(ok, 'could not turn "' .. dir .. '"into an absolute path')
table.insert(dirs.recentDirs, 1, d)
table.insert(dirs.recentDirs, 1, dir)
end
end
@ -77,4 +78,9 @@ function dirs.setOld(d)
dirs.old = d
end
bait.catch('hilbish.cd', function(path, oldPath)
dirs.setOld(oldPath)
dirs.push(path)
end)
return dirs

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}
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

@ -18,6 +18,8 @@ table.insert(package.searchers, function(module)
return function() return hilbish.module.load(path) end, path
end)
require 'nature.hilbish'
require 'nature.commands'
require 'nature.completions'
require 'nature.opts'

View File

@ -1,4 +1,5 @@
-- @module hilbish.runner
local snail = require 'snail'
local currentRunner = 'hybrid'
local runners = {}
@ -71,10 +72,8 @@ end
--- Sets Hilbish's runner mode by name.
--- @param name string
function hilbish.runner.setCurrent(name)
local r = hilbish.runner.get(name)
hilbish.runner.get(name) -- throws if it doesnt exist.
currentRunner = name
hilbish.runner.setMode(r.run)
end
--- Returns the current runner by name.
@ -83,6 +82,81 @@ function hilbish.runner.getCurrent()
return currentRunner
end
--- **NOTE: This function is deprecated and will be removed in 3.0**
--- Use `hilbish.runner.setCurrent` instead.
--- This is the same as the `hilbish.runnerMode` function.
--- It takes a callback, which will be used to execute all interactive input.
--- Or a string which names the runner mode to use.
-- @param mode string|function
function hilbish.runner.setMode(mode)
hilbish.runnerMode(mode)
end
local function finishExec(exitCode, input, priv)
hilbish.exitCode = exitCode
bait.throw('command.exit', exitCode, input, priv)
end
local function continuePrompt(prev, newline)
local multilinePrompt = hilbish.multiprompt()
-- the return of hilbish.read is nil when error or ctrl-d
local cont = hilbish.read(multilinePrompt)
if not cont then
return
end
if newline then
cont = '\n' .. cont
end
if cont:match '\\$' then
cont = cont:gsub('\\$', '') .. '\n'
end
return prev .. cont
end
--- Runs `input` with the currently set Hilbish runner.
--- This method is how Hilbish executes commands.
--- `priv` is an optional boolean used to state if the input should be saved to history.
-- @param input string
-- @param priv bool
function hilbish.runner.run(input, priv)
local command = hilbish.aliases.resolve(input)
bait.throw('command.preexec', input, command)
::rerun::
local runner = hilbish.runner.get(currentRunner)
local ok, out = pcall(runner.run, input)
if not ok then
io.stderr:write(out .. '\n')
finishExec(124, out.input, priv)
return
end
if out.continue then
local contInput = continuePrompt(input, out.newline)
if contInput then
input = contInput
goto rerun
end
end
if out.err then
local fields = string.split(out.err, ': ')
if fields[2] == 'not-found' or fields[2] == 'not-executable' then
bait.throw('command.' .. fields[2], fields[1])
else
io.stderr:write(out.err .. '\n')
end
end
finishExec(out.exitCode, out.input, priv)
end
function hilbish.runner.sh(input)
return hilbish.snail:run(input)
end
hilbish.runner.add('hybrid', function(input)
local cmdStr = hilbish.aliases.resolve(input)
@ -109,7 +183,5 @@ hilbish.runner.add('lua', function(input)
return hilbish.runner.lua(cmdStr)
end)
hilbish.runner.add('sh', function(input)
return hilbish.runner.sh(input)
end)
hilbish.runner.add('sh', hilbish.runner.sh)
hilbish.runner.setCurrent 'hybrid'

View File

@ -53,9 +53,7 @@ end)
*/
func runnerModeLoader(rtm *rt.Runtime) *rt.Table {
exports := map[string]util.LuaExport{
"sh": {shRunner, 1, false},
"lua": {luaRunner, 1, false},
"setMode": {hlrunnerMode, 1, false},
}
mod := rt.NewTable()
@ -64,44 +62,6 @@ func runnerModeLoader(rtm *rt.Runtime) *rt.Table {
return mod
}
// #interface runner
// setMode(cb)
// This is the same as the `hilbish.runnerMode` function.
// It takes a callback, which will be used to execute all interactive input.
// In normal cases, neither callbacks should be overrided by the user,
// as the higher level functions listed below this will handle it.
// #param cb function
func _runnerMode() {}
// #interface runner
// sh(cmd)
// Runs a command in Hilbish's shell script interpreter.
// This is the equivalent of using `source`.
// #param cmd string
func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
_, exitCode, cont, 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
// lua(cmd)
// Evaluates `cmd` as Lua input. This is the same as using `dofile`

View File

@ -1,35 +1,32 @@
package main
package util
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"strings"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
)
var sinkMetaKey = rt.StringValue("hshsink")
// #type
// A sink is a structure that has input and/or output to/from
// a desination.
type sink struct{
writer *bufio.Writer
reader *bufio.Reader
// A sink is a structure that has input and/or output to/from a desination.
type Sink struct{
Rw *bufio.ReadWriter
file *os.File
ud *rt.UserData
UserData *rt.UserData
autoFlush bool
}
func setupSinkType(rtm *rt.Runtime) {
func SinkLoader(rtm *rt.Runtime) *rt.Table {
sinkMeta := rt.NewTable()
sinkMethods := rt.NewTable()
sinkFuncs := map[string]util.LuaExport{
sinkFuncs := map[string]LuaExport{
"flush": {luaSinkFlush, 1, false},
"read": {luaSinkRead, 1, false},
"readAll": {luaSinkReadAll, 1, false},
@ -37,7 +34,7 @@ func setupSinkType(rtm *rt.Runtime) {
"write": {luaSinkWrite, 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) {
s, _ := sinkArg(c, 0)
@ -64,10 +61,25 @@ func setupSinkType(rtm *rt.Runtime) {
}
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
// readAll() -> string
// --- @returns string
@ -84,7 +96,7 @@ func luaSinkReadAll(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
lines := []string{}
for {
line, err := s.reader.ReadString('\n')
line, err := s.Rw.ReadString('\n')
if err != nil {
if err == io.EOF {
break
@ -113,7 +125,7 @@ func luaSinkRead(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return nil, err
}
str, _ := s.reader.ReadString('\n')
str, _ := s.Rw.ReadString('\n')
return c.PushingNext1(t.Runtime, rt.StringValue(str)), nil
}
@ -135,9 +147,9 @@ func luaSinkWrite(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return nil, err
}
s.writer.Write([]byte(data))
s.Rw.Write([]byte(data))
if s.autoFlush {
s.writer.Flush()
s.Rw.Flush()
}
return c.Next(), nil
@ -160,9 +172,9 @@ func luaSinkWriteln(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return nil, err
}
s.writer.Write([]byte(data + "\n"))
s.Rw.Write([]byte(data + "\n"))
if s.autoFlush {
s.writer.Flush()
s.Rw.Flush()
}
return c.Next(), nil
@ -181,7 +193,7 @@ func luaSinkFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return nil, err
}
s.writer.Flush()
s.Rw.Flush()
return c.Next(), nil
}
@ -212,11 +224,24 @@ func luaSinkAutoFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return c.Next(), nil
}
func newSinkInput(r io.Reader) *sink {
s := &sink{
reader: bufio.NewReader(r),
func NewSink(rtm *rt.Runtime, Rw io.ReadWriter) *Sink {
s := &Sink{
Rw: bufio.NewReadWriter(bufio.NewReader(Rw), bufio.NewWriter(Rw)),
}
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 {
s.file = f
@ -225,17 +250,17 @@ func newSinkInput(r io.Reader) *sink {
return s
}
func newSinkOutput(w io.Writer) *sink {
s := &sink{
writer: bufio.NewWriter(w),
func NewSinkOutput(rtm *rt.Runtime, w io.Writer) *Sink {
s := &Sink{
Rw: bufio.NewReadWriter(nil, bufio.NewWriter(w)),
autoFlush: true,
}
s.ud = sinkUserData(s)
s.UserData = sinkUserData(rtm, s)
return s
}
func sinkArg(c *rt.GoCont, arg int) (*sink, error) {
func sinkArg(c *rt.GoCont, arg int) (*Sink, error) {
s, ok := valueToSink(c.Arg(arg))
if !ok {
return nil, fmt.Errorf("#%d must be a sink", arg + 1)
@ -244,17 +269,17 @@ func sinkArg(c *rt.GoCont, arg int) (*sink, error) {
return s, nil
}
func valueToSink(val rt.Value) (*sink, bool) {
func valueToSink(val rt.Value) (*Sink, bool) {
u, ok := val.TryUserData()
if !ok {
return nil, false
}
s, ok := u.Value().(*sink)
s, ok := u.Value().(*Sink)
return s, ok
}
func sinkUserData(s *sink) *rt.UserData {
sinkMeta := l.Registry(sinkMetaKey)
func sinkUserData(rtm *rt.Runtime, s *Sink) *rt.UserData {
sinkMeta := rtm.Registry(sinkMetaKey)
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 (
"bufio"
"context"
"errors"
"fmt"
"io"
"path/filepath"
"strings"
"os"
"os/exec"
"os/user"
"runtime"
"syscall"
rt "github.com/arnodel/golua/runtime"
)
var ErrNotExec = errors.New("not executable")
var ErrNotFound = errors.New("not found")
type ExecError struct{
Typ string
Cmd string
Code int
Colon bool
Err error
}
func (e ExecError) Error() string {
return fmt.Sprintf("%s: %s", e.Cmd, e.Typ)
}
func (e ExecError) sprint() error {
sep := " "
if e.Colon {
sep = ": "
}
return fmt.Errorf("hilbish: %s%s%s", e.Cmd, sep, e.Err.Error())
}
func IsExecError(err error) (ExecError, bool) {
if exErr, ok := err.(ExecError); ok {
return exErr, true
}
fields := strings.Split(err.Error(), ": ")
knownTypes := []string{
"not-found",
"not-executable",
}
if len(fields) > 1 && Contains(knownTypes, fields[1]) {
var colon bool
var e error
switch fields[1] {
case "not-found":
e = ErrNotFound
case "not-executable":
colon = true
e = ErrNotExec
}
return ExecError{
Cmd: fields[0],
Typ: fields[1],
Colon: colon,
Err: e,
}, true
}
return ExecError{}, false
}
// SetField sets a field in a table, adding docs for it.
// It is accessible via the __docProp metatable. It is a table of the names of the fields.
func SetField(rtm *rt.Runtime, module *rt.Table, field string, value rt.Value) {
@ -36,6 +100,15 @@ func DoString(rtm *rt.Runtime, code string) (rt.Value, error) {
return ret, err
}
func MustDoString(rtm *rt.Runtime, code string) rt.Value {
val, err := DoString(rtm, code)
if err != nil {
panic(err)
}
return val
}
// DoFile runs the contents of the file in the Lua runtime.
func DoFile(rtm *rt.Runtime, path string) error {
f, err := os.Open(path)
@ -141,3 +214,67 @@ func AbbrevHome(path string) string {
return path
}
func LookPath(file string) (string, error) { // custom lookpath function so we know if a command is found *and* is executable
var skip []string
if runtime.GOOS == "windows" {
skip = []string{"./", "../", "~/", "C:"}
} else {
skip = []string{"./", "/", "../", "~/"}
}
for _, s := range skip {
if strings.HasPrefix(file, s) {
return file, FindExecutable(file, false, false)
}
}
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
path := filepath.Join(dir, file)
err := FindExecutable(path, true, false)
if err == ErrNotExec {
return "", err
} else if err == nil {
return path, nil
}
}
return "", os.ErrNotExist
}
func Contains(s []string, e string) bool {
for _, a := range s {
if strings.ToLower(a) == strings.ToLower(e) {
return true
}
}
return false
}
func HandleExecErr(err error) (exit uint8) {
ctx := context.TODO()
switch x := err.(type) {
case *exec.ExitError:
// started, but errored - default to 1 if OS
// doesn't have exit statuses
if status, ok := x.Sys().(syscall.WaitStatus); ok {
if status.Signaled() {
if ctx.Err() != nil {
return
}
exit = uint8(128 + status.Signal())
return
}
exit = uint8(status.ExitStatus())
return
}
exit = 1
return
case *exec.Error:
// did not start
//fmt.Fprintf(hc.Stderr, "%v\n", err)
exit = 127
default: return
}
return
}

View File

@ -1,17 +1,12 @@
//go:build unix
package main
package util
import (
"os"
"syscall"
)
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
func findExecutable(path string, inPath, dirs bool) error {
func FindExecutable(path string, inPath, dirs bool) error {
f, err := os.Stat(path)
if err != nil {
return err
@ -25,5 +20,5 @@ func findExecutable(path string, inPath, dirs bool) error {
return nil
}
}
return errNotExec
return ErrNotExec
}

View File

@ -1,18 +1,13 @@
//go:build windows
package main
package util
import (
"path/filepath"
"os"
"syscall"
)
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
func findExecutable(path string, inPath, dirs bool) error {
func FindExecutable(path string, inPath, dirs bool) error {
nameExt := filepath.Ext(path)
pathExts := filepath.SplitList(os.Getenv("PATHEXT"))
if inPath {
@ -26,15 +21,15 @@ func findExecutable(path string, inPath, dirs bool) error {
} else {
_, err := os.Stat(path)
if err == nil {
if contains(pathExts, nameExt) { return nil }
return errNotExec
if Contains(pathExts, nameExt) { return nil }
return ErrNotExec
}
}
} else {
_, err := os.Stat(path)
if err == nil {
if contains(pathExts, nameExt) { return nil }
return errNotExec
if Contains(pathExts, nameExt) { return nil }
return ErrNotExec
}
}