fully interactive shell

* Background task implementation
* Foreground an existing task implementation
* Documentation for both
* Refactors to signal handling, process pre-exec
This commit is contained in:
Ava Apples Affine 2023-05-18 06:53:23 +00:00
parent 69216db72a
commit c127118465
8 changed files with 535 additions and 107 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "relish"
version = "0.2.0"
version = "0.3.0"
authors = ["Ava <ava@sunnypup.io>"]
edition = "2021"
@ -12,7 +12,7 @@ nu-ansi-term = "0.47.0"
reedline = "0.17.0"
# these two used in posix shell layer (src/stl/posix.rs)
nix = "0.26.2"
ctrlc = { version = "3.0", features = ["termination"] }
# this one provides a global constant lookup table for simple
# string escaping in the lexer
phf = { version = "0.11", default-features = false, features = ["macros"] }
libc = "0.2.144"

View file

@ -12,7 +12,7 @@ The purpose of Relish is to create a highly portable, easy to integrate language
- Create a usable POSIX shell
- Create usable applications/scripts
- To have quality code coverage
- No unsafe code
- No unsafe code outside of the POSIX module
** Stretch Goals
- Create an interpreter that can be booted on one or more SOCs
@ -409,6 +409,10 @@ For a more complex example see file:snippets/avas-laptop-prompt.rls
Further configuration can be done by loading scripts that contain more functions and data to evaluate.
Variables and functions defined in an external script loaded by your interpreter will persist in the symbol table.
** Using Relish as your shell
As of version 0.3.0 Relish implements all the features of an interactive shell.
See further documentation in file:Shell.org.
#+BEGIN_SRC lisp
(call "my-extra-library-functions.rls")
#+END_SRC
@ -483,82 +487,44 @@ This contains any executable target of this project. Notably the main shell file
Note: this section will not show the status of each item unless you are viewing it with a proper orgmode viewer.
Note: this section only tracks the state of incomplete TODO items. Having everything on here would be cluttered.
** TODO Pre-alpha tasks
- Shell module (see branch: ~fully-interactive-shell~)
- be able to fg a bg process
- be able to list all background processes with j function
- Documentation!
- Dogfood this for quite some time
** TODO alpha tasks
- env prints without using columns
- lambdas shouldn't hit the env
- circuit should actually be ~(reduce (lambda (form state) (and state (bool form))) my-args)~
- error display must improve at all costs
- make presentation on relish
- lisp basics
- s expressions
- data structures
- control flow
- let, circuit, etc
- homoiconicity
- quote/eval
- lambda
- putting it all together
- CI tests
- repl configuration
- self documenting
- env
- help
- beyond lisp
- POSIX
- how to make a shell
- process groups
- tcgetpgrp/tcsetpgrp
- linemode
- getting things off of the path
- calls to exec system call
- signal management
- job control (and process groups)
- ergonomics of control flow as shell command
- circuit as progn
- circuit -> pipe
- let -> call-with
- putting it all together
- job control in shell
- shell scripts
- fancy prompts
- To infinity and beyond
- NGINX modules
- bootable?
- give a pretty pastel white, pink and blue theme
- store in repo after giving presentation
** TODO v1.0 tasks
- finish basic goals in file:snippets/interactive-devel.rls
- then write doc on interactive development
- lambdas shouldn't hit the env
- Write next_has member function for &Seg and simplify stdlib and probably also eval/sym
- Investigate has_next member function for &Seg and maybe simplify stdlib and probably also eval/sym
- Rename to Flesh
- Can pass args to relish scripts (via interpreter)
- Can pass args to relish scripts (via command line)
- History length configurable
- Search delim configurable
- Scripts can use shell
- History length configurable (env var?)
- Lex function
- Read function (Input + Lex)
- Make an icon if you feel like it
** TODO v1.1 tasks
** TODO v1.1 tasks (Stable 1)
- finish stretch goals in file:snippets/interactive-devel.rls
- execute configurable function on cd
- Post to relevant channels
- Custom ast pretty print
- Implement Compose for lambdas
- Document this in relevant readme sections
(add to readme)
- File operations
- read-to-string
- write-to-file
- file exists
- file exists?
- (add this all to the readme)
- color control library
- probably more escapes in the lexer
- just a snippet with a bunch of color constants
- Search delim configurable
** TODO v1.2 release tasks
- Emacs syntax highlighting and/or LSP implementation
- Bindings for the simplest possible UI library?
- Get on that bug tracke
- Network library
- HTTP Client
- TCP Stream client
@ -566,5 +532,3 @@ Note: this section only tracks the state of incomplete TODO items. Having everyt
- TCP Listener
- HTTP Listener
- UDP Listener
- Emacs syntax highlighting and/or LSP implementation
- Bindings for the simplest possible UI library?

178
Shell.org Normal file
View file

@ -0,0 +1,178 @@
#+Title: Relish as a Shell
#+Author: Ava Hahn
* Description
This document outlines the ways that Relish can be used as a shell for daily administration or scripting purposes. Readers should have already read through the [[file:Readme.org][general Relish documentation]]. With the exception of the ~circuit~ function, all facilities introduced in this document apply only to relish interpreters compiled with the [[file:/src/stl/posix.rs][POSIX module]] enabled and ~CFG_RELISH_POSIX~ set at load time.
** Launch a command
For the most common uses (executing shell commands) a function is provided to find and load binaries from entries in the ~PATH~ variable. This function may be called either with ~load~ or ~l~.
#+BEGIN_SRC lisp
(load htop) ;; executes the htop binary, if htop is found on the users PATH
(l emacs) ;; executes the emacs binary, if emacs is found on the users PATH
#+END_SRC
The load command takes an infinite number of arguments and uses the following rules to construct a shell command from them:
+ symbols that are not set are taken as strings
#+BEGIN_SRC lisp
(l emacs -nw) ;; 'emacs' and '-nw' not defined
#+END_SRC
+ symbols that are set are replaced by their values
#+BEGIN_SRC lisp
(l ls -la HOME)
(let ((ping-count 4)
(domain "sunnypup.io"))
(l ping -c ping-count domain)
#+END_SRC
+ nested forms are evaluated
#+BEGIN_SRC lisp
(l cat (concat HOME "/notes.txt"))
#+END_SRC
Shell command evaluation rules apply to the following functions:
+ ~load~ / ~l~
+ ~pipe~
+ ~load-to-string~
+ ~load-with~
+ ~bg~
With the exception of the ~load-to-string~ function and the ~bg~ function, each of the aforementioned functions returns the exit code of a new process as an integer.
Symbols set in the Relish REPL are converted to strings and placed in the environment as well, so ~def~ can be used to define environment variables for a process (but not ~let~, which only creates form-local symbols).
** Special command forms
A number of forms are provided to offer a first class experience when running Relish as a shell.
*** Command short circuiting
In a shell such as Bash or Zsh, commands can be chained with the ~&&~ operator:
#+BEGIN_EXAMPLE shell
$ apt update && apt upgrade && echo "Success!"
#+END_EXAMPLE
In these chains, if one command fails the next one(s) are not run. Colloquially, the command short-circuits. A similar construct is offered in Relish called ~circuit~. Circuit will evaluate one or more forms (all expected to evaluate to either an integer (shell command) or a boolean (more general form). If a form returns false (or non-zero) no other forms are evaluated. The printed error message will identify where in the sequence evaluation was halted.
#+BEGIN_EXAMPLE lisp
(circuit
(l apt update) ;; if this fails, no upgrade is made
(l apt upgrade) ;; if this fails, "Success!" is not printed
(l echo "Success!"))
#+END_EXAMPLE
*** Command piping
In a shell such as Bash or Zsh, the output of one command may be automatically looped into the input of another command. Below is an example of three shell commands piped together. On execution this example counts the number of running Relish processes:
#+BEGIN_EXAMPLE shell
$ ps aux | grep relish | wc -l
#+END_EXAMPLE
In order to provide such a facility in Relish, the ~pipe~ function is provided.
#+BEGIN_EXAMPLE lisp
(pipe
(ps aux)
(grep relish)
(wc -l))
#+END_EXAMPLE
*** Processing command output
There will be many times a user will want to directly process command output other than the process exit code. For this a function ~load-to-string~ is included. Below is an example of a series of commands that leverage this function to print a symbolic token based on the presence and status of a Git repository.
#+BEGIN_EXAMPLE shell
(def in-a-git-repo?
'returns true or false depending on if currently in a git repo'
() (eq? (load-to-string git rev-parse --is-inside-work-tree) "true"))
(def git-repo-is-dirty?
'returns true or false depending on if current dir is a dirty git repo'
() (not (eq? (load-to-string git diff '--stat') "")))
(def git-status 'returns "(git:<branch>{!,})" if dir is in a git repository'
()
(if (in-a-git-repo?)
(concat
"(git:"
(load-to-string git rev-parse --abbrev-ref HEAD)
(if (git-repo-is-dirty?)
"!"
"")
")")
''))
(git-status)
#+END_EXAMPLE
(Example is from [[file:snippets/avas-laptop-prompt.rls][Ava's Laptop Prompt]])
*** Redirecting command output to or from files
Another common shell feature is the redirection of input/output to/from files. For example:
#+BEGIN_EXAMPLE shell
$ find / -iname "needle.haystack" 2> /dev/null
#+END_EXAMPLE
Relish can redirect "stdin", "stdout", or "stderr" of a shell command using the ~load-with~ function.
#+BEGIN_EXAMPLE lisp
(load-with (("stderr" "/dev/null"))
(find / -iname "needle.haystack"))
#+END_EXAMPLE
Or, a more comprehensive example using hypothetical commands and data:
#+BEGIN_EXAMPLE lisp
(load-with (("stdin" "img.png")
("stdout" "img.jpg")
("stderr" "/dev/null"))
(my-img-convert))
#+END_EXAMPLE
** Control background and foreground processes
Relish implements fully interactive job control.
To launch a background process use the ~bg~ function:
#+BEGIN_EXAMPLE lisp
(bg emacs -nw)
#+END_EXAMPLE
To get all jobs in your shell use the system ~ps~ binary:
#+BEGIN_EXAMPLE lisp
(l ps)
#+END_EXAMPLE
To foreground a background process use the ~fg~ function:
#+BEGIN_EXAMPLE
(fg <pid>)
#+END_EXAMPLE
** Changing directories
Relish also provides a ~cd~ utility to change current working directory:
#+BEGIN_EXAMPLE lisp
(cd (concat HOME '/repositories')) ;; $ cd ~/repositories
#+END_EXAMPLE
** Creating bindings for shell commands
Daily Relish users will long for first class shell commands that are accounted for in autocomplete.
A simple solution:
#+BEGIN_EXAMPLE lisp
(def lis 'shortcut for ls -la'
(lambda (dir) (l ls -la dir)))
(lis HOME)
#+END_EXAMPLE
The reader may also wish for a utility that allows them to create first-class shortcuts to shell subcommands. It is for this purpose that [[file:snippets/genbind.rls][genbind]] was written. Genbind allows the user to create globally defined functions that reference shell commands (more specifically, subcommands) from their shell. The reader is advised to refer to the documentation in Genbind for more information.
#+BEGIN_EXAMPLE lisp
(def g-add 'shortcut for git add'
(gen-binding "git" "add"))
(g-add ("src" "docs"))
#+END_EXAMPLE
Or:
#+BEGIN_EXAMPLE lisp
;; REQUIRES USERLIB FOR PREPEND (or write your own)
(def pacman-search 'shortcut for sudo pacman -Ss'
(lambda (many-dirs) ((gen-binding "sudo" "pacman") (prepend "-Ss" many-dirs))))
(pacman-search "xfce4")
#+END_EXAMPLE
** Using shell commands in the configuration file
A warning to the reader:
+ the ~.relishrc~ configuration file is loaded before any POSIX shell functions are added
+ direct calls to these functions will throw errors
+ the user can still configure functions and data that relies on shell commands, just not directly
+ lambdas can be used to encapsulate shell commands and run them during the prompt or as shortcuts/functions at the REPL
+ note the examples in this document, as well as [[file:snippets/avas-laptop-prompt.rls][Ava's Laptop Prompt]]

View file

@ -80,7 +80,7 @@
'returns true or false depending on if current dir is a dirty git repo'
() (not (eq? (load-to-string git diff '--stat') "")))
(def git-status 'returns "(git:<branch>(!))" if dir is in a git repository'
(def git-status 'returns "(git:<branch>{!,})" if dir is in a git repository'
()
(if (in-a-git-repo?)
(concat

View file

@ -0,0 +1,81 @@
extern crate signal_hook;
extern crate nix;
use std::process::{Command, Stdio};
use signal_hook::{consts, iterator::Signals};
use std::os::unix::process::CommandExt;
use std::thread::spawn;
use nix::{unistd, unistd::Pid, sys::termios::{tcgetattr, tcsetattr, SetArg}};
fn main() {
// setup signal handlers
let signals = Signals::new([
/* glibc says to block SIGCHLD but since I already have a backgroun thread
* it seems like a perfect way to know when background jobs have stopped...
*/
consts::SIGINT, consts::SIGQUIT, //consts::SIGCHLD,
consts::SIGTSTP, consts::SIGTTOU, consts::SIGTTIN,
]);
if let Err(e) = signals {
println!("couldn't spawn signal handler: {}", e);
return
}
spawn(move || {
for sig in signals.unwrap().forever() {
println!("Received signal {:?}", sig);
}
});
let parent_pid = unistd::getpid();
let attr = tcgetattr(0).unwrap();
let term_owner = unistd::tcgetpgrp(0).unwrap();
let parent_pgid = unistd::getpgid(Some(parent_pid)).unwrap();
if parent_pgid != term_owner {
nix::sys::signal::kill(
term_owner,
nix::sys::signal::Signal::SIGTTIN,
).expect("cant seize terminal");
}
if parent_pid != parent_pgid {
unistd::setpgid(parent_pid, parent_pid).unwrap()
}
unistd::tcsetpgrp(0, parent_pid).unwrap();
// kick off child process in its own PG
let mut chldproc = Command::new("/bin/bash")
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.process_group(0)
.spawn()
.unwrap();
let pid = chldproc.id();
unistd::setpgid(
Pid::from_raw(pid as i32),
Pid::from_raw(pid as i32),
).unwrap();
// give child terminal
unistd::tcsetpgrp(
0, Pid::from_raw((chldproc.id() as i32).into()),
).unwrap();
// cant use i.wait() because it closes stdin
loop {
if let Ok(maybe) = chldproc.try_wait() {
if let Some(_) = maybe {
unistd::tcsetpgrp(0, parent_pid).unwrap();
tcsetattr(0, SetArg::TCSADRAIN, &attr).ok();
break;
}
} else {
panic!()
}
}
println!("finished successfully!");
}

View file

@ -25,7 +25,7 @@ use {
stdlib::{
static_stdlib, dynamic_stdlib
},
aux::ShellState,
aux::{ShellState, check_jobs},
},
std::{
cell::RefCell,
@ -169,7 +169,6 @@ impl Completer for CustomCompleter {
if e.file_name()
.to_string_lossy()
.starts_with(path.file_name()
// TODO: dont
.expect("bad file somehow?")
.to_string_lossy()
.to_mut()
@ -220,12 +219,16 @@ fn main() {
let hist_file_name = home_dir.clone() + HIST_FILE;
let cfg_file_name = home_dir + CONFIG_FILE_DEFAULT;
// TODO: the next two decls should probably be conditional on CFG_RELISH_POSIX
// but in both cases the data must live for the whole program
let shell_state_bindings = Rc::new(RefCell::from(ShellState {
parent_pid: unistd::Pid::from_raw(0),
parent_pgid: unistd::Pid::from_raw(0),
parent_sid: unistd::Pid::from_raw(0),
children: vec![],
last_exit_code: 0,
attr: None,
}));
let prompt_ss = shell_state_bindings.clone();
// setup symtable
let mut syms = SymTable::new();
@ -281,8 +284,10 @@ fn main() {
.with_style(Style::new().italic().fg(Color::LightGray)),
)).with_validator(Box::new(DefaultValidator));
// repl :)
loop {
{
check_jobs(&mut prompt_ss.borrow_mut());
}
let readline_prompt = make_prompt(&mut syms);
let completer = make_completer(&mut syms);
// realloc with each loop because syms can change

View file

@ -37,4 +37,5 @@ pub mod stdlib {
pub mod aux {
pub use crate::stl::posix::args_from_ast;
pub use crate::stl::posix::ShellState;
pub use crate::stl::posix::check_jobs;
}

View file

@ -17,7 +17,9 @@
use {
crate::{
segment::{Ctr, Seg},
segment::{
Ctr, Seg, Type
},
sym::{
SymTable, ValueType,
Symbol, Args,
@ -25,32 +27,93 @@ use {
eval::eval,
run,
},
libc::{
sigaddset, sigemptyset, sigprocmask,
SIGINT, SIGCHLD, SIGTTOU, SIGTTIN, SIGQUIT, SIGTSTP,
SIG_BLOCK, SIG_UNBLOCK
},
std::{
collections::VecDeque,
cell::{
RefCell, RefMut,
BorrowMutError,
},
io::Result as IOResult,
rc::Rc,
fs::{File, OpenOptions},
fs::{
File, OpenOptions
},
path::Path,
process::{
Command, Child,
Stdio,
Command, Stdio, Child
},
env::set_current_dir,
os::unix::process::CommandExt,
mem,
},
nix::{
unistd, unistd::Pid,
sys::termios::{
Termios, tcgetattr,
tcsetattr, SetArg,
},
},
nix::unistd,
ctrlc,
};
pub struct ShellState {
pub parent_pid: unistd::Pid,
pub parent_pgid: unistd::Pid,
pub parent_pid: Pid,
pub parent_sid: Pid,
pub children: Vec<Child>,
pub last_exit_code: i32,
pub attr: Option<Termios>,
}
pub fn ign_sigs() {
unsafe {
let mut mask = mem::zeroed();
sigemptyset(&mut mask);
sigaddset(&mut mask, SIGINT);
sigaddset(&mut mask, SIGQUIT);
sigaddset(&mut mask, SIGCHLD);
sigaddset(&mut mask, SIGTSTP);
sigaddset(&mut mask, SIGTTOU);
sigaddset(&mut mask, SIGTTIN);
sigprocmask(
SIG_BLOCK,
&mask as *const libc::sigset_t,
std::ptr::null_mut()
);
}
}
pub fn unign_sigs() {
unsafe {
let mut mask = mem::zeroed();
sigemptyset(&mut mask);
sigaddset(&mut mask, SIGINT);
sigaddset(&mut mask, SIGQUIT);
sigaddset(&mut mask, SIGCHLD);
sigaddset(&mut mask, SIGTSTP);
sigaddset(&mut mask, SIGTTOU);
sigaddset(&mut mask, SIGTTIN);
sigprocmask(
SIG_UNBLOCK,
&mask as *const libc::sigset_t,
std::ptr::null_mut()
);
}
}
// TODO: trigger this on SIGCHLD instead of traditional shell once per input
pub fn check_jobs(state: &mut ShellState) {
let mut idx = 0 as usize;
while idx < state.children.len() {
if let Ok(_) = state.children[idx].try_wait() {
state.children.remove(idx);
continue;
}
idx += 1;
}
}
pub fn args_from_ast(ast: &Seg, syms: &mut SymTable) -> Vec<String> {
@ -107,14 +170,42 @@ fn launch_command(
background: bool,
state: &mut ShellState
) -> Result<(), String> {
let newchld = Command::new(path)
.args(args)
.stdin(infd)
.stdout(outfd)
.stderr(errfd).spawn();
let newchld: Result<std::process::Child, std::io::Error>;
unsafe {
newchld = Command::new(path)
.pre_exec(move || -> IOResult<()> {
let pid = unistd::getpid();
if let Err(_) = unistd::setpgid(pid, pid) {
// crying would be nice... if you had eyes
}
if !background {
if let Err(_) = unistd::tcsetpgrp(0, pid) {
// you wish to scream, but fork() has taken from you your throat
}
}
unign_sigs();
Ok(())
})
.args(args)
.stdin(infd)
.stdout(outfd)
.stderr(errfd)
.process_group(0)
.spawn();
}
// glibc says to do this in both parent and child
if let Ok(child) = newchld {
let pid = child.id();
if let Err(e) = unistd::setpgid(
Pid::from_raw(pid as i32),
Pid::from_raw(pid as i32),
) {
eprintln!("parent couldnt setpgid on chld: {}", e);
}
state.children.push(child);
if !background {
make_foreground(pid, state)
@ -130,23 +221,57 @@ fn launch_command(
fn make_foreground(pid: u32, state: &mut ShellState) -> Result<(), String>{
for i in &mut state.children {
if i.id() == pid {
let exit = i.wait().unwrap();
if let Some(code) = exit.code() {
state.last_exit_code = code;
} else {
state.last_exit_code = -1;
let pid_fancy = Pid::from_raw(pid as i32);
if let Err(e) = unistd::tcsetpgrp(0, pid_fancy) {
eprintln!("couldn't give child the terminal: {}!", e);
}
if let Err(e) = unistd::tcsetpgrp(0, state.parent_pgid) {
return Err(format!("error setting terminal: {}!", e));
nix::sys::signal::kill(
pid_fancy,
nix::sys::signal::Signal::SIGCONT,
).expect(
"couldnt sigcont... better get another tty."
);
loop {
match i.try_wait() {
Ok(maybe) => {
if let Some(exit) = maybe {
if let Err(e) = unistd::tcsetpgrp(0, state.parent_pid) {
return Err(format!(
"error re-acquiring terminal: {}!", e
));
}
// TODO: this could be more elegant
if let Some(ref attr) = state.attr {
tcsetattr(0, SetArg::TCSADRAIN, attr).ok();
} else {
panic!("somehow no attrs")
}
state.last_exit_code = exit
.code()
.or(Some(-1))
.unwrap();
break;
}
},
Err(e) => {
return Err(format!("error waiting on child: {}", e))
}
}
}
}
}
if let Some(pos) = state.children.iter().position(|x| x.id() == pid) {
state.children.remove(pos);
Ok(())
} else {
Err(format!("pid not found in active children"))
}
Ok(())
}
const LOAD_DOCSTRING: &str = "Calls a binary off disk with given arguments.
@ -185,7 +310,6 @@ fn load_callback(ast: &Seg, syms: &mut SymTable, state: &mut ShellState) -> Resu
}
}
// TODO: maybe flesh out this docstring a bit
const PIPE_DOCSTRING: &str = "Calls a sequence of shell commands.
Each one has their output redirected to the input of the next one.
@ -212,14 +336,14 @@ fn pipe_callback(ast: &Seg, syms: &mut SymTable, state: &mut ShellState) -> Resu
if let Some(filepath) = run::find_on_path(args.pop_front().unwrap()) {
let mut newchld = Command::new(filepath);
newchld.args(Vec::from(args.make_contiguous()))
.stderr(Stdio::inherit())
.stdout(Stdio::piped());
.stdout(Stdio::piped())
.process_group(0);
if let Some(pos) = state.children
.iter()
.position(|x| x.id() == lastpid)
{
newchld.stdin(state.children.remove(pos).stdout.unwrap());
newchld.stdin(state.children.remove(pos).stdout.take().unwrap());
}
let chld_spwn = newchld.spawn();
@ -458,9 +582,62 @@ fn q_callback(_ast: &Seg, _syms: &SymTable, state: &mut ShellState) -> Result<Ct
Ok(Ctr::Integer(state.last_exit_code.into()))
}
const BG_DOCSTRING: &str = "";
fn bg_callback(_ast: &Seg, _syms: &mut SymTable, _state: &mut ShellState) -> Result<Ctr, String> {
unimplemented!()
const BG_DOCSTRING: &str = "Calls a binary off disk with given arguments.
Arguments may be of any type except lambda. If a symbol is not defined it will be passed as a string.
first argument (command name) will be found on path (or an error returned).
The command executed in the background while your shell remains active.
Output from your command may appear in the terminal, but it will not be able to read from the terminal.
examples:
(bg ping -c4 google.com)
(bg vim) ;; vim waits patiently for you to foreground the process with 'fg'
";
fn bg_callback(ast: &Seg, syms: &mut SymTable, state: &mut ShellState) -> Result<Ctr, String> {
if ast.is_empty() {
Err("need at least one argument".to_string())
} else {
let mut args = VecDeque::from(args_from_ast(ast, syms));
if args.is_empty() {
Err("empty command".to_string())
} else {
if let Some(filepath) = run::find_on_path(args.pop_front().unwrap()) {
launch_command(
filepath,
&Vec::from(args.make_contiguous()),
Stdio::inherit(),
Stdio::inherit(),
Stdio::inherit(),
true,
state,
)?;
Ok(Ctr::Integer(state.last_exit_code.into()))
} else {
Err("file not found".to_string())
}
}
}
}
pub const FG_DOCSTRING: &str = "takes one argument (an integer).
Integer is presumed to be a PID of a running background job.
If PID corresponds with a background job, fg will try to foreground it.
If PID is not a current running background job, fg will return an error.
Examples:
(bg vim) (fg 13871)
";
pub fn fg_callback(
ast: &Seg,
_syms: &mut SymTable,
shell_state: &mut ShellState
) -> Result <Ctr, String> {
if let Ctr::Integer(i) = *ast.car {
make_foreground(i as u32, shell_state)?;
Ok(Ctr::None)
} else {
Err(format!("illegal args to fg"))
}
}
pub const CD_DOCSTRING: &str =
@ -481,21 +658,31 @@ pub fn cd_callback(ast: &Seg, _syms: &mut SymTable) -> Result<Ctr, String> {
pub fn load_posix_shell(syms: &mut SymTable, shell_state: Rc<RefCell<ShellState>>) {
let pid = unistd::getpid();
let pgid_res = unistd::getpgid(Some(pid));
if !pgid_res.is_ok() {
let sid_res = unistd::getsid(Some(pid));
if !pgid_res.is_ok() || !sid_res.is_ok() {
panic!("couldn't get pgid")
}
let pgid = pgid_res.ok().unwrap();
let sid = sid_res.ok().unwrap();
let shattr: Termios;
// one mut borrow
{
let mut state = shell_state.borrow_mut();
state.parent_pid = pid;
state.parent_pgid = pgid;
state.parent_sid = sid;
if let Ok(attr) = tcgetattr(0) {
state.attr = Some(attr.clone());
shattr = attr.clone();
} else {
panic!("couldn't get term attrs");
}
}
let term_pgrp_res = unistd::tcgetpgrp(0);
if !term_pgrp_res.is_ok() {
panic!("couldn't get terminal's pgrp")
panic!("couldn't get terminal's pgrp: {:?}", term_pgrp_res)
}
let term_owner = term_pgrp_res.ok().unwrap();
@ -509,14 +696,13 @@ pub fn load_posix_shell(syms: &mut SymTable, shell_state: Rc<RefCell<ShellState>
if pid != pgid {
if let Err(e) = unistd::setpgid(pid, pid) {
panic!("couldn't set PGID: {}", e)
};
}
}
if let Err(e) = unistd::tcsetpgrp(0, pid) {
panic!("couldn't grab terminal: {}", e)
}
// these clones are needed for callbacks which move references
let load_ss = shell_state.clone();
let bg_ss = shell_state.clone();
@ -524,6 +710,9 @@ pub fn load_posix_shell(syms: &mut SymTable, shell_state: Rc<RefCell<ShellState>
let lw_ss = shell_state.clone();
let lts_ss = shell_state.clone();
let p_ss = shell_state.clone();
let fg_ss = shell_state.clone();
ign_sigs();
syms.insert(
String::from("l"),
@ -567,6 +756,20 @@ pub fn load_posix_shell(syms: &mut SymTable, shell_state: Rc<RefCell<ShellState>
},
);
syms.insert(
String::from("fg"),
Symbol {
name: String::from("foreground"),
args: Args::Strict(vec![Type::Integer]),
conditional_branches: true,
docs: String::from(FG_DOCSTRING),
value: ValueType::Internal(Rc::new(move |ast: &Seg, symtable: &mut SymTable| -> Result<Ctr, String> {
fg_callback(ast, symtable, &mut fg_ss.borrow_mut())
})),
..Default::default()
},
);
syms.insert(
String::from("?"),
Symbol {
@ -612,11 +815,11 @@ pub fn load_posix_shell(syms: &mut SymTable, shell_state: Rc<RefCell<ShellState>
.or(Ok::<RefMut<'_, ShellState>, BorrowMutError>(
RefCell::from(ShellState{
parent_pid: pid,
parent_pgid: pgid,
parent_sid: pgid,
children: vec![],
last_exit_code: 0,
}).borrow_mut()
))
attr: Some(shattr.clone()),
}).borrow_mut()))
.unwrap()
)
})),
@ -637,8 +840,4 @@ pub fn load_posix_shell(syms: &mut SymTable, shell_state: Rc<RefCell<ShellState>
..Default::default()
},
);
if let Err(e) = ctrlc::set_handler(move || println!("POSIX layer caught SIG-something")) {
eprintln!("WARNING: couldn't set sig handler: {}", e);
}
}