// the core Hilbish API // The Hilbish module includes the core API, containing // interfaces and functions which directly relate to shell functionality. // #field ver The version of Hilbish // #field goVersion The version of Go that Hilbish was compiled with // #field user Username of the user // #field host Hostname of the machine // #field dataDir Directory for Hilbish data files, including the docs and default modules // #field interactive Is Hilbish in an interactive shell? // #field login Is Hilbish the login shell? // #field vimMode Current Vim input mode of Hilbish (will be nil if not in Vim input mode) // #field exitCode Exit code of the last executed command package main import ( "bytes" "errors" "fmt" "io" "os" "os/exec" "runtime" "strings" "syscall" "time" "hilbish/util" rt "github.com/arnodel/golua/runtime" "github.com/arnodel/golua/lib/packagelib" "github.com/arnodel/golua/lib/iolib" "github.com/maxlandon/readline" "mvdan.cc/sh/v3/interp" ) var exports = map[string]util.LuaExport{ "alias": {hlalias, 2, false}, "appendPath": {hlappendPath, 1, false}, "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}, "multiprompt": {hlmultiprompt, 1, false}, "prependPath": {hlprependPath, 1, false}, "prompt": {hlprompt, 1, true}, "inputMode": {hlinputMode, 1, false}, "interval": {hlinterval, 2, false}, "read": {hlread, 1, false}, "run": {hlrun, 1, true}, "timeout": {hltimeout, 2, false}, "which": {hlwhich, 1, false}, } var hshMod *rt.Table var hilbishLoader = packagelib.Loader{ Load: hilbishLoad, Name: "hilbish", } func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) { mod := rt.NewTable() util.SetExports(rtm, mod, exports) hshMod = mod host, _ := os.Hostname() username := curuser.Username if runtime.GOOS == "windows" { username = strings.Split(username, "\\")[1] // for some reason Username includes the hostname on windows } util.SetField(rtm, mod, "ver", rt.StringValue(getVersion())) util.SetField(rtm, mod, "goVersion", rt.StringValue(runtime.Version())) util.SetField(rtm, mod, "user", rt.StringValue(username)) util.SetField(rtm, mod, "host", rt.StringValue(host)) util.SetField(rtm, mod, "home", rt.StringValue(curuser.HomeDir)) util.SetField(rtm, mod, "dataDir", rt.StringValue(dataDir)) util.SetField(rtm, mod, "interactive", rt.BoolValue(interactive)) util.SetField(rtm, mod, "login", rt.BoolValue(login)) util.SetField(rtm, mod, "vimMode", rt.NilValue) util.SetField(rtm, mod, "exitCode", rt.IntValue(0)) // hilbish.userDir table hshuser := userDirLoader(rtm) mod.Set(rt.StringValue("userDir"), rt.TableValue(hshuser)) // hilbish.os table hshos := hshosLoader(rtm) mod.Set(rt.StringValue("os"), rt.TableValue(hshos)) // hilbish.aliases table aliases = newAliases() aliasesModule := aliases.Loader(rtm) mod.Set(rt.StringValue("aliases"), rt.TableValue(aliasesModule)) // hilbish.history table historyModule := lr.Loader(rtm) mod.Set(rt.StringValue("history"), rt.TableValue(historyModule)) // hilbish.completion table hshcomp := completionLoader(rtm) // TODO: REMOVE "completion" AND ONLY USE "completions" WITH AN S mod.Set(rt.StringValue("completion"), rt.TableValue(hshcomp)) mod.Set(rt.StringValue("completions"), rt.TableValue(hshcomp)) // hilbish.runner table runnerModule := runnerModeLoader(rtm) mod.Set(rt.StringValue("runner"), rt.TableValue(runnerModule)) // hilbish.jobs table jobs = newJobHandler() jobModule := jobs.loader(rtm) mod.Set(rt.StringValue("jobs"), rt.TableValue(jobModule)) // hilbish.timers table timers = newTimersModule() timersModule := timers.loader(rtm) mod.Set(rt.StringValue("timers"), rt.TableValue(timersModule)) editorModule := editorLoader(rtm) mod.Set(rt.StringValue("editor"), rt.TableValue(editorModule)) versionModule := rt.NewTable() util.SetField(rtm, versionModule, "branch", rt.StringValue(gitBranch)) util.SetField(rtm, versionModule, "full", rt.StringValue(getVersion())) util.SetField(rtm, versionModule, "commit", rt.StringValue(gitCommit)) util.SetField(rtm, versionModule, "release", rt.StringValue(releaseName)) mod.Set(rt.StringValue("version"), rt.TableValue(versionModule)) pluginModule := moduleLoader(rtm) mod.Set(rt.StringValue("module"), rt.TableValue(pluginModule)) return rt.TableValue(mod), nil } func getenv(key, fallback string) string { value := os.Getenv(key) if len(value) == 0 { return fallback } return value } func setVimMode(mode string) { util.SetField(l, hshMod, "vimMode", rt.StringValue(mode)) hooks.Emit("hilbish.vimMode", mode) } 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 { return errors.New("expected metatable argument") } val := ud.Value() var varstrm io.Writer if f, ok := val.(*iolib.File); ok { varstrm = f.Handle() } if f, ok := val.(*sink); ok { varstrm = f.writer } if varstrm == nil { return errors.New("expected either a sink or file") } if errStream { strms.stderr = varstrm } else { strms.stdout = varstrm } 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. // #returns string func hlcwd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { cwd, _ := os.Getwd() return c.PushingNext1(t.Runtime, rt.StringValue(cwd)), nil } // read(prompt) -> input (string) // Read input from the user, using Hilbish's line editor/input reader. // This is a separate instance from the one Hilbish actually uses. // Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs. // #param prompt? string Text to print before input, can be empty. // #returns string|nil func hlread(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { luaprompt := c.Arg(0) if typ := luaprompt.Type(); typ != rt.StringType && typ != rt.NilType { return nil, errors.New("expected #1 to be a string") } prompt, ok := luaprompt.TryString() if !ok { // if we are here and `luaprompt` is not a string, it's nil // substitute with an empty string prompt = "" } lualr := &lineReader{ rl: readline.NewInstance(), } lualr.SetPrompt(prompt) input, err := lualr.Read() if err != nil { return c.Next(), nil } return c.PushingNext1(t.Runtime, rt.StringValue(input)), nil } /* prompt(str, typ) Changes the shell prompt to the provided string. There are a few verbs that can be used in the prompt text. These will be formatted and replaced with the appropriate values. `%d` - Current working directory `%u` - Name of current user `%h` - Hostname of device #param str string #param typ? string Type of prompt, being left or right. Left by default. #example -- the default hilbish prompt without color hilbish.prompt '%u %d ∆' -- or something of old: hilbish.prompt '%u@%h :%d $' -- prompt: user@hostname: ~/directory $ #example */ func hlprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { err := c.Check1Arg() if err != nil { return nil, err } p, err := c.StringArg(0) if err != nil { return nil, err } typ := "left" // optional 2nd arg if len(c.Etc()) != 0 { ltyp := c.Etc()[0] var ok bool typ, ok = ltyp.TryString() if !ok { return nil, errors.New("bad argument to run (expected string, got " + ltyp.TypeName() + ")") } } switch typ { case "left": prompt = p lr.SetPrompt(fmtPrompt(prompt)) case "right": lr.SetRightPrompt(fmtPrompt(p)) default: return nil, errors.New("expected prompt type to be right or left, got " + typ) } return c.Next(), nil } // multiprompt(str) // Changes the text prompt when Hilbish asks for more input. // This will show up when text is incomplete, like a missing quote // #param str string /* #example --[[ imagine this is your text input: user ~ ∆ echo "hey but there's a missing quote! hilbish will now prompt you so the terminal will look like: user ~ ∆ echo "hey --> ...!" so then you get user ~ ∆ echo "hey --> ...!" hey ...! ]]-- hilbish.multiprompt '-->' #example */ func hlmultiprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } prompt, err := c.StringArg(0) if err != nil { return nil, err } multilinePrompt = prompt return c.Next(), nil } // alias(cmd, orig) // Sets an alias, with a name of `cmd` to another command. // #param cmd string Name of the alias // #param orig string Command that will be aliased /* #example -- With this, "ga file" will turn into "git add file" hilbish.alias('ga', 'git add') -- Numbered substitutions are supported here! hilbish.alias('dircount', 'ls %1 | wc -l') -- "dircount ~" would count how many files are in ~ (home directory). #example */ func hlalias(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.CheckNArgs(2); err != nil { return nil, err } cmd, err := c.StringArg(0) if err != nil { return nil, err } orig, err := c.StringArg(1) if err != nil { return nil, err } aliases.Add(cmd, orig) return c.Next(), nil } // appendPath(dir) // Appends the provided dir to the command path (`$PATH`) // #param dir string|table Directory (or directories) to append to path /* #example hilbish.appendPath '~/go/bin' -- Will add ~/go/bin to the command path. -- Or do multiple: hilbish.appendPath { '~/go/bin', '~/.local/bin' } #example */ func hlappendPath(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } arg := c.Arg(0) // check if dir is a table or a string if arg.Type() == rt.TableType { util.ForEach(arg.AsTable(), func(k rt.Value, v rt.Value) { if v.Type() == rt.StringType { appendPath(v.AsString()) } }) } else if arg.Type() == rt.StringType { appendPath(arg.AsString()) } else { return nil, errors.New("bad argument to appendPath (expected string or table, got " + arg.TypeName() + ")") } return c.Next(), nil } func appendPath(dir string) { dir = strings.Replace(dir, "~", curuser.HomeDir, 1) pathenv := os.Getenv("PATH") // if dir isnt already in $PATH, add it if !strings.Contains(pathenv, dir) { os.Setenv("PATH", pathenv + string(os.PathListSeparator) + dir) } } // exec(cmd) // Replaces the currently running Hilbish instance with the supplied command. // This can be used to do an in-place restart. // #param cmd string func hlexec(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 } cmdArgs, _ := splitInput(cmd) if runtime.GOOS != "windows" { cmdPath, err := exec.LookPath(cmdArgs[0]) if err != nil { fmt.Println(err) // if we get here, cmdPath will be nothing // therefore nothing will run } // syscall.Exec requires an absolute path to a binary // path, args, string slice of environments syscall.Exec(cmdPath, cmdArgs, os.Environ()) } else { cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin cmd.Run() os.Exit(0) } return c.Next(), nil } // goro(fn) // Puts `fn` in a Goroutine. // This can be used to run any function in another thread at the same time as other Lua code. // **NOTE: THIS FUNCTION MAY CRASH HILBISH IF OUTSIDE VARIABLES ARE ACCESSED.** // **This is a limitation of the Lua runtime.** // #param fn function func hlgoro(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } fn, err := c.ClosureArg(0) if err != nil { return nil, err } // call fn go func() { defer func() { if r := recover(); r != nil { // do something here? } }() _, err := rt.Call1(l.MainThread(), rt.FunctionValue(fn), c.Etc()...) if err != nil { fmt.Fprintln(os.Stderr, "Error in goro function:\n\n", err) } }() return c.Next(), nil } // timeout(cb, time) -> @Timer // Executed the `cb` function after a period of `time`. // This creates a Timer that starts ticking immediately. // #param cb function // #param time number Time to run in milliseconds. // #returns Timer func hltimeout(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.CheckNArgs(2); err != nil { return nil, err } cb, err := c.ClosureArg(0) if err != nil { return nil, err } ms, err := c.IntArg(1) if err != nil { return nil, err } interval := time.Duration(ms) * time.Millisecond timer := timers.create(timerTimeout, interval, cb) timer.start() return c.PushingNext1(t.Runtime, rt.UserDataValue(timer.ud)), nil } // interval(cb, time) -> @Timer // Runs the `cb` function every specified amount of `time`. // This creates a timer that ticking immediately. // #param cb function // #param time number Time in milliseconds. // #return Timer func hlinterval(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.CheckNArgs(2); err != nil { return nil, err } cb, err := c.ClosureArg(0) if err != nil { return nil, err } ms, err := c.IntArg(1) if err != nil { return nil, err } interval := time.Duration(ms) * time.Millisecond timer := timers.create(timerInterval, interval, cb) timer.start() return c.PushingNext1(t.Runtime, rt.UserDataValue(timer.ud)), nil } // complete(scope, cb) // Registers a completion handler for the specified scope. // A `scope` is expected to be `command.`, // replacing with the name of the command (for example `command.git`). // The documentation for completions, under Features/Completions or `doc completions` // provides more details. // #param scope string // #param cb function /* #example -- This is a very simple example. Read the full doc for completions for details. hilbish.complete('command.sudo', function(query, ctx, fields) if #fields == 0 then -- complete for commands local comps, pfx = hilbish.completion.bins(query, ctx, fields) local compGroup = { items = comps, -- our list of items to complete type = 'grid' -- what our completions will look like. } return {compGroup}, pfx end -- otherwise just be boring and return files local comps, pfx = hilbish.completion.files(query, ctx, fields) local compGroup = { items = comps, type = 'grid' } return {compGroup}, pfx end) #example */ func hlcomplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { scope, cb, err := util.HandleStrCallback(t, c) if err != nil { return nil, err } luaCompletions[scope] = cb return c.Next(), nil } // prependPath(dir) // Prepends `dir` to $PATH. // #param dir string func hlprependPath(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } dir, err := c.StringArg(0) if err != nil { return nil, err } dir = strings.Replace(dir, "~", curuser.HomeDir, 1) pathenv := os.Getenv("PATH") // if dir isnt already in $PATH, add in if !strings.Contains(pathenv, dir) { os.Setenv("PATH", dir + string(os.PathListSeparator) + pathenv) } return c.Next(), nil } // which(name) -> string // Checks if `name` is a valid command. // Will return the path of the binary, or a basename if it's a commander. // #param name string // #returns string func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } name, err := c.StringArg(0) if err != nil { return nil, err } // itll return either the original command or what was passed // if name isnt empty its not an issue alias := aliases.Resolve(name) cmd := strings.Split(alias, " ")[0] // check for commander if cmds.Commands[cmd] != nil { // they dont resolve to a path, so just send the cmd return c.PushingNext1(t.Runtime, rt.StringValue(cmd)), nil } path, err := exec.LookPath(cmd) if err != nil { return c.Next(), nil } return c.PushingNext1(t.Runtime, rt.StringValue(path)), nil } // inputMode(mode) // Sets the input mode for Hilbish's line reader. // `emacs` is the default. Setting it to `vim` changes behavior of input to be // Vim-like with modes and Vim keybinds. // #param mode string Can be set to either `emacs` or `vim` func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } mode, err := c.StringArg(0) if err != nil { return nil, err } switch mode { case "emacs": unsetVimMode() lr.rl.InputMode = readline.Emacs case "vim": setVimMode("insert") lr.rl.InputMode = readline.Vim default: return nil, errors.New("inputMode: expected vim or emacs, received " + mode) } 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 // line and cursor position. It is expected to return a string which is used // as the text for the hint. This is by default a shim. To set hints, // override this function with your custom handler. // #param line string // #param pos number Position of cursor in line. Usually equals string.len(line) /* #example -- this will display "hi" after the cursor in a dimmed color. function hilbish.hinter(line, pos) return 'hi' end #example */ func hlhinter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } // highlighter(line) // Line highlighter handler. // This is mainly for syntax highlighting, but in reality could set the input // of the prompt to *display* anything. The callback is passed the current line // and is expected to return a line that will be used as the input display. // Note that to set a highlighter, one has to override this function. // #example // --This code will highlight all double quoted strings in green. // function hilbish.highlighter(line) // return line:gsub('"%w+"', function(c) return lunacolors.green(c) end) // end // #example // #param line string func hlhighlighter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } line, err := c.StringArg(0) if err != nil { return nil, err } return c.PushingNext1(t.Runtime, rt.StringValue(line)), nil }