From c324f160a791736cde2b2adefb23020fa96c6b74 Mon Sep 17 00:00:00 2001 From: Hugo Wang Date: Sun, 23 May 2021 15:20:24 +0800 Subject: [PATCH] refine builtins redirections/pipelines (#32) * refine redirections on aliases * misc * wip - add set; minfd; refactoring * misc * when builtin output got captured * fix builtin in mid of pipeline * misc * more builtins updates 1 * more builtins updates 2 * more builtins updates 3 * finished updates on builtins * misc * misc --- CHANGELOG.md | 6 + Cargo.toml | 2 +- docs/builtins.md | 16 ++- src/builtins/alias.rs | 79 ++++++------ src/builtins/bg.rs | 43 ++++--- src/builtins/cd.rs | 48 ++++---- src/builtins/cinfo.rs | 14 ++- src/builtins/exec.rs | 25 ++-- src/builtins/exit.rs | 28 +++-- src/builtins/export.rs | 51 ++++---- src/builtins/fg.rs | 40 +++--- src/builtins/history.rs | 145 +++++++++++++++------- src/builtins/minfd.rs | 13 ++ src/builtins/mod.rs | 3 + src/builtins/read.rs | 29 +++-- src/builtins/set.rs | 47 +++++++ src/builtins/source.rs | 23 ++-- src/builtins/ulimit.rs | 159 +++++++++++++++++------- src/builtins/unalias.rs | 24 ++-- src/builtins/utils.rs | 163 ++++++++++++++++++++++++ src/builtins/vox.rs | 98 +++++++++------ src/completers/path.rs | 4 +- src/core.rs | 170 +++++++++++++++----------- src/execute.rs | 19 +-- src/scripting.rs | 2 +- src/shell.rs | 3 +- src/tools.rs | 13 +- src/types.rs | 16 +++ tests/scripts/redirections-001.sh | 81 ++++++++++++ tests/scripts/redirections-001.sh.out | 45 +++++++ 30 files changed, 1019 insertions(+), 390 deletions(-) create mode 100644 src/builtins/minfd.rs create mode 100644 src/builtins/set.rs create mode 100644 src/builtins/utils.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 468cc19..fe4b058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Logs +## 0.9.19 - 2021-05-23 + +- Refine redirections of aliases. +- Fix & imporve redirections/pipelines for builtins. +- Added new beta builtins: set, minfd. + ## 0.9.18 - 2021-05-04 - fix compiling issue on 32bit systems. diff --git a/Cargo.toml b/Cargo.toml index 4071783..16dc4f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ edition = "2018" build = "src/build.rs" name = "cicada" -version = "0.9.18" +version = "0.9.19" authors = ["Hugo Wang "] description = "A simple Bash-like Unix shell." diff --git a/docs/builtins.md b/docs/builtins.md index 9bea13d..1f6e303 100644 --- a/docs/builtins.md +++ b/docs/builtins.md @@ -11,7 +11,9 @@ - [fg](#user-content-fg) - [history](#user-content-history) - [jobs](#user-content-jobs) + - [minfd](#user-content-minfd) - [read](#user-content-read) + - [set](#user-content-set) - [source](#user-content-source) - [ulimit](#user-content-ulimit) - [unalias](#user-content-unalias) @@ -137,14 +139,19 @@ $ history add '' Listing all jobs in [job control](https://github.com/mitnk/cicada/blob/master/docs/jobc.md). See also `bg`, `fg`. +## minfd + +Get minimal file descriptor number in current shell process. This builtin +is hardly useful for users, mainly using in debugging cicada. + ## read +Read a line from the standard input and split it into fields. + ``` read [name ...] ``` -Read a line from the standard input and split it into fields. - Reads a single line from the standard input. The line is split into fields as with word splitting, and the first word is assigned to the first NAME, the second word to the second NAME, and so on, with any leftover words assigned to @@ -168,6 +175,11 @@ $ IFS=:@ read a b c $ echo $c $b $a ``` +## set + +(in BETA) Set shell options. Currently ony support `set -e`, same effects +as Bash. + ## source Read and execute commands from the `filename` argument in the current shell diff --git a/src/builtins/alias.rs b/src/builtins/alias.rs index d3ab01f..ca80eed 100644 --- a/src/builtins/alias.rs +++ b/src/builtins/alias.rs @@ -1,43 +1,33 @@ -use std::io::Write; - use regex::Regex; use crate::shell; use crate::tools; -use crate::types::Tokens; +use crate::types::{Command, CommandLine, CommandResult}; +use crate::builtins::utils::print_stderr_with_capture; +use crate::builtins::utils::print_stdout_with_capture; + +pub fn run(sh: &mut shell::Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let mut cr = CommandResult::new(); + let tokens = cmd.tokens.clone(); -pub fn run(sh: &mut shell::Shell, tokens: &Tokens) -> i32 { if tokens.len() == 1 { - return show_alias_list(sh); + return show_alias_list(sh, cmd, cl, capture); } + if tokens.len() > 2 { - println_stderr!("alias syntax error"); - println_stderr!("alias usage example: alias foo='echo foo'"); - return 1; + let info = "alias syntax error: usage: alias foo='echo foo'"; + print_stderr_with_capture(info, &mut cr, cl, cmd, capture); + return cr; } let input = &tokens[1].1; - let re_single_read; - match Regex::new(r"^[a-zA-Z0-9_\.-]+$") { - Ok(x) => re_single_read = x, - Err(e) => { - println!("cicada: Regex error: {:?}", e); - return 1; - } - } + let re_single_read = Regex::new(r"^[a-zA-Z0-9_\.-]+$").unwrap(); if re_single_read.is_match(input) { - return show_single_alias(sh, input); - } - - let re_to_add; - match Regex::new(r"^([a-zA-Z0-9_\.-]+)=(.*)$") { - Ok(x) => re_to_add = x, - Err(e) => { - println!("cicada: Regex error: {:?}", e); - return 1; - } + return show_single_alias(sh, input, cmd, cl, capture); } + let re_to_add = Regex::new(r"^([a-zA-Z0-9_\.-]+)=(.*)$").unwrap(); for cap in re_to_add.captures_iter(input) { let name = tools::unquote(&cap[1]); // due to limitation of `parses::parser_line`, @@ -50,27 +40,32 @@ pub fn run(sh: &mut shell::Shell, tokens: &Tokens) -> i32 { }; sh.add_alias(name.as_str(), value.as_str()); } - 0 + + CommandResult::new() } -fn show_alias_list(sh: &shell::Shell) -> i32 { +fn show_alias_list(sh: &shell::Shell, cmd: &Command, + cl: &CommandLine, capture: bool) -> CommandResult { + let mut lines = Vec::new(); for (name, value) in sh.get_alias_list() { - println!("alias {}='{}'", name, value); + let line = format!("alias {}='{}'", name, value); + lines.push(line); } - 0 + let buffer = lines.join("\n"); + let mut cr = CommandResult::new(); + print_stdout_with_capture(&buffer, &mut cr, cl, cmd, capture); + cr } -fn show_single_alias(sh: &shell::Shell, name_to_find: &str) -> i32 { - let mut found = false; - for (name, value) in sh.get_alias_list() { - if name_to_find == name { - println!("alias {}='{}'", name, value); - found = true; - } - } - if !found { - println_stderr!("cicada: alias: {}: not found", name_to_find); - return 1; +fn show_single_alias(sh: &shell::Shell, name_to_find: &str, cmd: &Command, + cl: &CommandLine, capture: bool) -> CommandResult { + let mut cr = CommandResult::new(); + if let Some(content) = sh.get_alias_content(name_to_find) { + let info = format!("alias {}='{}'", name_to_find, content); + print_stdout_with_capture(&info, &mut cr, cl, cmd, capture); + } else { + let info = format!("cicada: alias: {}: not found", name_to_find); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); } - 0 + cr } diff --git a/src/builtins/bg.rs b/src/builtins/bg.rs index 243680b..a8cee10 100644 --- a/src/builtins/bg.rs +++ b/src/builtins/bg.rs @@ -1,14 +1,18 @@ -use std::io::Write; - +use crate::builtins::utils::print_stderr_with_capture; use crate::jobc; use crate::libc; -use crate::shell; -use crate::types; +use crate::shell::Shell; +use crate::types::{CommandResult, CommandLine, Command}; + +pub fn run(sh: &mut Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let tokens = cmd.tokens.clone(); + let mut cr = CommandResult::new(); -pub fn run(sh: &mut shell::Shell, tokens: &types::Tokens) -> i32 { if sh.jobs.is_empty() { - println_stderr!("cicada: bg: no job found"); - return 0; + let info = "cicada: bg: no job found"; + print_stderr_with_capture(info, &mut cr, cl, cmd, capture); + return cr; } let mut job_id = -1; @@ -28,13 +32,16 @@ pub fn run(sh: &mut shell::Shell, tokens: &types::Tokens) -> i32 { match job_str.parse::() { Ok(n) => job_id = n, Err(_) => { - println_stderr!("cicada: bg: invalid job id"); - return 1; + let info = "cicada: bg: invalid job id"; + print_stderr_with_capture(info, &mut cr, cl, cmd, capture); + return cr; } } } if job_id == -1 { - println_stderr!("cicada: not job id found"); + let info = "cicada: bg: not such job"; + print_stderr_with_capture(info, &mut cr, cl, cmd, capture); + return cr; } let gid: i32; @@ -48,29 +55,31 @@ pub fn run(sh: &mut shell::Shell, tokens: &types::Tokens) -> i32 { match result { Some(job) => { - let cmd = if job.cmd.ends_with(" &") { + let info_cmd = if job.cmd.ends_with(" &") { job.cmd.clone() } else { format!("{} &", job.cmd) }; - println_stderr!("{}", &cmd); + print_stderr_with_capture(&info_cmd, &mut cr, cl, cmd, capture); unsafe { libc::killpg(job.gid, libc::SIGCONT); gid = job.gid; if job.status == "Running" { - println_stderr!("cicada: bg: job {} already in background", job.id); - return 0; + let info = format!("cicada: bg: job {} already in background", job.id); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } } } None => { - println_stderr!("cicada: bg: no such job"); - return 1; + let info = "cicada: bg: not such job"; + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } } } jobc::mark_job_as_running(sh, gid, true); - return 0; + return cr; } diff --git a/src/builtins/cd.rs b/src/builtins/cd.rs index a6eda96..2b27740 100644 --- a/src/builtins/cd.rs +++ b/src/builtins/cd.rs @@ -1,18 +1,22 @@ use std::env; -use std::io::Write; use std::path::Path; +use crate::builtins::utils::print_stderr_with_capture; use crate::parsers; use crate::shell; use crate::tools; +use crate::types::{Command, CommandLine, CommandResult}; -use crate::types::Tokens; - -pub fn run(sh: &mut shell::Shell, tokens: &Tokens) -> i32 { +pub fn run(sh: &mut shell::Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let tokens = cmd.tokens.clone(); + let mut cr = CommandResult::new(); let args = parsers::parser_line::tokens_to_args(&tokens); + if args.len() > 2 { - println_stderr!("cicada: cd: too many argument"); - return 1; + let info = "cicada: cd: too many argument"; + print_stderr_with_capture(info, &mut cr, cl, cmd, capture); + return cr; } let str_current_dir = tools::get_current_dir(); @@ -26,32 +30,32 @@ pub fn run(sh: &mut shell::Shell, tokens: &Tokens) -> i32 { if dir_to == "-" { if sh.previous_dir == "" { - println_stderr!("no previous dir"); - return 1; + let info = "no previous dir"; + print_stderr_with_capture(info, &mut cr, cl, cmd, capture); + return cr; } dir_to = sh.previous_dir.clone(); } else if !dir_to.starts_with('/') { dir_to = format!("{}/{}", str_current_dir, dir_to); } + if !Path::new(&dir_to).exists() { + let info = format!("cicada: cd: {}: No such file or directory", &args[1]); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; + } + match Path::new(&dir_to).canonicalize() { Ok(p) => { dir_to = p.as_path().to_string_lossy().to_string(); } Err(e) => { - println_stderr!("cicada: cd: error: {:?}", e); - return 1; + let info = format!("cicada: cd: error: {}", e); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } } - if !Path::new(&dir_to).exists() { - println_stderr!( - "cicada: cd: {}: No such file or directory", - args[1..].join("") - ); - return 1; - } - match env::set_current_dir(&dir_to) { Ok(_) => { sh.current_dir = dir_to.clone(); @@ -59,11 +63,13 @@ pub fn run(sh: &mut shell::Shell, tokens: &Tokens) -> i32 { sh.previous_dir = str_current_dir.clone(); env::set_var("PWD", &sh.current_dir); }; - 0 + cr.status = 0; + cr } Err(e) => { - println_stderr!("cicada: cd: {}", e); - 1 + let info = format!("cicada: cd: {}", e); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + cr } } } diff --git a/src/builtins/cinfo.rs b/src/builtins/cinfo.rs index 9563903..da9b7f2 100644 --- a/src/builtins/cinfo.rs +++ b/src/builtins/cinfo.rs @@ -1,8 +1,12 @@ +use crate::builtins::utils::print_stdout_with_capture; use crate::history; use crate::libs; use crate::rcfile; +use crate::shell::Shell; +use crate::types::{Command, CommandLine, CommandResult}; -pub fn run() -> i32 { +pub fn run(_sh: &mut Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { let mut info = vec![]; const VERSION: &str = env!("CARGO_PKG_VERSION"); info.push(("version", VERSION)); @@ -35,9 +39,13 @@ pub fn run() -> i32 { info.push(("built-with", env!("BUILD_RUSTC_VERSION"))); info.push(("built-at", env!("BUILD_DATE"))); + let mut lines = Vec::new(); for (k, v) in &info { // longest key above is 12-char length - println!("{: >12}: {}", k, v); + lines.push(format!("{: >12}: {}", k, v)); } - 0 + let buffer = lines.join("\n"); + let mut cr = CommandResult::new(); + print_stdout_with_capture(&buffer, &mut cr, cl, cmd, capture); + cr } diff --git a/src/builtins/exec.rs b/src/builtins/exec.rs index 39c7dcb..19535d6 100644 --- a/src/builtins/exec.rs +++ b/src/builtins/exec.rs @@ -1,17 +1,24 @@ -use crate::parsers; -use crate::types::Tokens; use exec; -pub fn run(tokens: &Tokens) -> i32 { +use crate::builtins::utils::print_stderr_with_capture; +use crate::parsers; +use crate::shell::Shell; +use crate::types::{CommandResult, CommandLine, Command}; + +pub fn run(_sh: &Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let mut cr = CommandResult::new(); + let tokens = cmd.tokens.clone(); let args = parsers::parser_line::tokens_to_args(&tokens); let len = args.len(); if len == 1 { - println!("invalid command"); - return 1; + print_stderr_with_capture("invalid usage", &mut cr, cl, cmd, capture); + return cr; } - let mut cmd = exec::Command::new(&args[1]); - let err = cmd.args(&args[2..len]).exec(); - println!("exec error: {:?}", err); - 0 + let mut _cmd = exec::Command::new(&args[1]); + let err = _cmd.args(&args[2..len]).exec(); + let info = format!("cicada: exec: {}", err); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + cr } diff --git a/src/builtins/exit.rs b/src/builtins/exit.rs index 0eb920f..cbfe2fc 100644 --- a/src/builtins/exit.rs +++ b/src/builtins/exit.rs @@ -1,14 +1,18 @@ #![allow(unreachable_code)] -use std::io::Write; use std::process; -use crate::shell; -use crate::types::Tokens; +use crate::builtins::utils::print_stderr_with_capture; +use crate::shell::Shell; +use crate::types::{CommandResult, CommandLine, Command}; -pub fn run(sh: &shell::Shell, tokens: &Tokens) -> i32 { +pub fn run(sh: &Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let mut cr = CommandResult::new(); + let tokens = cmd.tokens.clone(); if tokens.len() > 2 { - println_stderr!("cicada: exit: too many arguments"); - return 1; + let info = "cicada: exit: too many arguments"; + print_stderr_with_capture(info, &mut cr, cl, cmd, capture); + return cr; } if tokens.len() == 2 { @@ -18,7 +22,8 @@ pub fn run(sh: &shell::Shell, tokens: &Tokens) -> i32 { process::exit(x); } Err(_) => { - println_stderr!("cicada: exit: {}: numeric argument required", _code); + let info = format!("cicada: exit: {}: numeric argument required", _code); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); process::exit(255); } } @@ -26,11 +31,14 @@ pub fn run(sh: &shell::Shell, tokens: &Tokens) -> i32 { for (_i, job) in sh.jobs.iter() { if !job.cmd.starts_with("nohup ") { - println_stderr!("There are background jobs."); - println_stderr!("Run `jobs` to see details; `exit 1` to force quit."); - return 0; + let mut info = String::new(); + info.push_str("There are background jobs."); + info.push_str("Run `jobs` to see details; `exit 1` to force quit."); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } } process::exit(0); + cr } diff --git a/src/builtins/export.rs b/src/builtins/export.rs index 3853279..bbdf1e2 100644 --- a/src/builtins/export.rs +++ b/src/builtins/export.rs @@ -1,42 +1,47 @@ use regex::Regex; use std::env; -use std::io::Write; use crate::libs; use crate::parsers; -use crate::shell; use crate::tools; -use crate::types::Tokens; -pub fn run(_sh: &shell::Shell, tokens: &Tokens) -> i32 { +use crate::builtins::utils::print_stderr_with_capture; +use crate::shell::Shell; +use crate::types::{CommandResult, CommandLine, Command}; + +pub fn run(_sh: &Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let mut cr = CommandResult::new(); + let tokens = cmd.tokens.clone(); + + let re_name_ptn = Regex::new(r"^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$").unwrap(); for (_, text) in tokens.iter() { if text == "export" { continue; } if !tools::is_env(text) { - println!("export: invalid command"); - println!("usage: export XXX=YYY"); - return 1; + let mut info = String::new(); + info.push_str("export: invalid command\n"); + info.push_str("usage: export XXX=YYY"); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } - if let Ok(re) = Regex::new(r"^([a-zA-Z0-9_]+)=(.*)$") { - if !re.is_match(text) { - println!("export: invalid command"); - println!("usage: export XXX=YYY ZZ=123"); - return 1; - } + if !re_name_ptn.is_match(text) { + let mut info = String::new(); + info.push_str("export: invalid command\n"); + info.push_str("usage: export XXX=YYY ZZ=123"); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; + } - for cap in re.captures_iter(text) { - let name = cap[1].to_string(); - let token = parsers::parser_line::unquote(&cap[2]); - let value = libs::path::expand_home(&token); - env::set_var(name, &value); - } - } else { - println_stderr!("cicada: re new error"); - return 2; + for cap in re_name_ptn.captures_iter(text) { + let name = cap[1].to_string(); + let token = parsers::parser_line::unquote(&cap[2]); + let value = libs::path::expand_home(&token); + env::set_var(name, &value); } } - 0 + cr } diff --git a/src/builtins/fg.rs b/src/builtins/fg.rs index 4e38f88..9c03ddc 100644 --- a/src/builtins/fg.rs +++ b/src/builtins/fg.rs @@ -1,16 +1,20 @@ -use std::io::Write; - use libc; +use crate::builtins::utils::print_stderr_with_capture; use crate::jobc; -use crate::shell; +use crate::shell::{self, Shell}; use crate::tools::clog; -use crate::types; +use crate::types::{self, CommandResult, CommandLine, Command}; + +pub fn run(sh: &mut Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let tokens = cmd.tokens.clone(); + let mut cr = CommandResult::new(); -pub fn run(sh: &mut shell::Shell, tokens: &types::Tokens) -> i32 { if sh.jobs.is_empty() { - println_stderr!("cicada: fg: no job found"); - return 0; + let info = "cicada: fg: no job found"; + print_stderr_with_capture(info, &mut cr, cl, cmd, capture); + return cr; } let mut job_id = -1; @@ -30,14 +34,17 @@ pub fn run(sh: &mut shell::Shell, tokens: &types::Tokens) -> i32 { match job_str.parse::() { Ok(n) => job_id = n, Err(_) => { - println_stderr!("cicada: fg: invalid job id"); - return 1; + let info = "cicada: fg: invalid job id"; + print_stderr_with_capture(info, &mut cr, cl, cmd, capture); + return cr; } } } if job_id == -1 { - println_stderr!("cicada: not job id found"); + let info = "cicada: not job id found"; + print_stderr_with_capture(info, &mut cr, cl, cmd, capture); + return cr; } let gid: i32; @@ -52,12 +59,12 @@ pub fn run(sh: &mut shell::Shell, tokens: &types::Tokens) -> i32 { match result { Some(job) => { - let cmd = job.cmd.trim_matches('&').trim(); - println_stderr!("{}", cmd); + let _cmd = job.cmd.trim_matches('&').trim(); + print_stderr_with_capture(&_cmd, &mut cr, cl, cmd, capture); unsafe { if !shell::give_terminal_to(job.gid) { - return 1; + return CommandResult::error(); } libc::killpg(job.gid, libc::SIGCONT); @@ -66,8 +73,9 @@ pub fn run(sh: &mut shell::Shell, tokens: &types::Tokens) -> i32 { } } None => { - println_stderr!("cicada: fg: no such job"); - return 1; + let info = "cicada: fg: no such job"; + print_stderr_with_capture(info, &mut cr, cl, cmd, capture); + return cr; } } } @@ -88,6 +96,6 @@ pub fn run(sh: &mut shell::Shell, tokens: &types::Tokens) -> i32 { if !shell::give_terminal_to(gid_shell) { log!("failed to give term to back to shell : {}", gid_shell); } - return status; + return CommandResult::from_status(0, status); } } diff --git a/src/builtins/history.rs b/src/builtins/history.rs index 4bae72b..6e5f6df 100644 --- a/src/builtins/history.rs +++ b/src/builtins/history.rs @@ -1,4 +1,3 @@ -use std::io::Write; use std::path::Path; use chrono::{DateTime, NaiveDateTime, Local, Utc}; @@ -6,10 +5,13 @@ use rusqlite::Connection as Conn; use rusqlite::NO_PARAMS; use structopt::StructOpt; +use crate::builtins::utils::print_stderr_with_capture; +use crate::builtins::utils::print_stdout_with_capture; use crate::history; use crate::parsers; -use crate::shell; -use crate::types::Command; +use crate::shell::Shell; +use crate::tools::clog; +use crate::types::{CommandResult, CommandLine, Command}; #[derive(Debug, StructOpt)] #[structopt(name = "history", about = "History in cicada shell")] @@ -59,49 +61,88 @@ enum SubCommand { } } -pub fn run(sh: &shell::Shell, cmd: &Command) -> i32 { +pub fn run(sh: &mut Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let mut cr = CommandResult::new(); let hfile = history::get_history_file(); let path = Path::new(hfile.as_str()); if !path.exists() { - println_stderr!("no history file."); - return 1; + let info = "no history file"; + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } let conn = match Conn::open(&hfile) { Ok(x) => x, Err(e) => { - println!("sqlite conn open error: {:?}", e); - return 1; + let info = format!("history: sqlite error: {:?}", e); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } }; - let tokens = &cmd.tokens; - let args = parsers::parser_line::tokens_to_args(tokens); - - let opt = OptMain::from_iter(args); - match opt.cmd { - Some(SubCommand::Delete {rowid: rowids}) => { - for rowid in rowids { - delete_history_item(&conn, rowid); + let tokens = cmd.tokens.clone(); + let args = parsers::parser_line::tokens_to_args(&tokens); + + let show_usage = args.len() > 1 && (args[1] == "-h" || args[1] == "--help"); + let opt = OptMain::from_iter_safe(args); + match opt { + Ok(opt) => { + match opt.cmd { + Some(SubCommand::Delete {rowid: rowids}) => { + let mut _count = 0; + for rowid in rowids { + let _deleted = delete_history_item(&conn, rowid); + if _deleted { + _count += 1; + } + } + if _count > 0 { + let info = format!("deleted {} items", _count); + print_stdout_with_capture(&info, &mut cr, cl, cmd, capture); + } + return cr; + } + Some(SubCommand::Add {timestamp: ts, input}) => { + let ts = ts.unwrap_or(0 as f64); + add_history(sh, ts, &input); + return cr; + } + None => { + let (str_out, str_err) = list_current_history(sh, &conn, &opt); + if !str_out.is_empty() { + print_stdout_with_capture(&str_out, &mut cr, cl, cmd, capture); + } + if !str_err.is_empty() { + print_stderr_with_capture(&str_err, &mut cr, cl, cmd, capture); + } + return cr; + } } - return 0; - } - Some(SubCommand::Add {timestamp: ts, input}) => { - let ts = ts.unwrap_or(0 as f64); - return add_history(sh, ts, &input); } - None => { - return list_current_history(sh, &conn, &opt); + Err(e) => { + let info = format!("{}", e); + if show_usage { + print_stdout_with_capture(&info, &mut cr, cl, cmd, capture); + cr.status = 0; + } else { + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + cr.status = 1; + } + return cr; } } } -fn add_history(sh: &shell::Shell, ts: f64, input: &str) -> i32 { +fn add_history(sh: &Shell, ts: f64, input: &str) { let (tsb, tse) = (ts, ts + 1.0); history::add_raw(sh, input, 0, tsb, tse); - 0 } -fn list_current_history(sh: &shell::Shell, conn: &Conn, opt: &OptMain) -> i32 { +fn list_current_history(sh: &Shell, conn: &Conn, + opt: &OptMain) -> (String, String) { + let mut result_stderr = String::new(); + let result_stdout = String::new(); + let history_table = history::get_history_table(); let mut sql = format!("SELECT ROWID, inp, tsb FROM {} WHERE ROWID > 0", history_table); @@ -125,19 +166,22 @@ fn list_current_history(sh: &shell::Shell, conn: &Conn, opt: &OptMain) -> i32 { let mut stmt = match conn.prepare(&sql) { Ok(x) => x, Err(e) => { - println_stderr!("history: prepare select error: {:?}", e); - return 1; + let info = format!("history: prepare select error: {:?}", e); + result_stderr.push_str(&info); + return (result_stdout, result_stderr); } }; let mut rows = match stmt.query(NO_PARAMS) { Ok(x) => x, Err(e) => { - println_stderr!("history: query error: {:?}", e); - return 1; + let info = format!("history: query error: {:?}", e); + result_stderr.push_str(&info); + return (result_stdout, result_stderr); } }; + let mut lines = Vec::new(); loop { match rows.next() { Ok(_rows) => { @@ -145,58 +189,67 @@ fn list_current_history(sh: &shell::Shell, conn: &Conn, opt: &OptMain) -> i32 { let row_id: i32 = match row.get(0) { Ok(x) => x, Err(e) => { - println_stderr!("history: error: {:?}", e); - return 1; + let info = format!("history: error: {:?}", e); + result_stderr.push_str(&info); + return (result_stdout, result_stderr); } }; let inp: String = match row.get(1) { Ok(x) => x, Err(e) => { - println_stderr!("history: error: {:?}", e); - return 1; + let info = format!("history: error: {:?}", e); + result_stderr.push_str(&info); + return (result_stdout, result_stderr); } }; if opt.no_id { - println!("{}", inp); + lines.push(format!("{}", inp)); } else if opt.only_id { - println!("{}", row_id); + lines.push(format!("{}", row_id)); } else if opt.show_date { let tsb: f64 = match row.get(2) { Ok(x) => x, Err(e) => { - println_stderr!("history: error: {:?}", e); - return 1; + let info = format!("history: error: {:?}", e); + result_stderr.push_str(&info); + return (result_stdout, result_stderr); } }; let dt = DateTime::::from_utc( NaiveDateTime::from_timestamp(tsb as i64, 0), Utc ).with_timezone(&Local); - println!("{}: {}: {}", row_id, dt.format("%Y-%m-%d %H:%M:%S"), inp); + lines.push(format!("{}: {}: {}", row_id, dt.format("%Y-%m-%d %H:%M:%S"), inp)); } else { - println!("{}: {}", row_id, inp); + lines.push(format!("{}: {}", row_id, inp)); } } else { - return 0; + break; } } Err(e) => { - println_stderr!("history: rows next error: {:?}", e); - return 1; + let info = format!("history: rows next error: {:?}", e); + result_stderr.push_str(&info); + return (result_stdout, result_stderr); } } } + + let buffer = lines.join("\n"); + + (buffer, result_stderr) } -fn delete_history_item(conn: &Conn, rowid: usize) { +fn delete_history_item(conn: &Conn, rowid: usize) -> bool { let history_table = history::get_history_table(); let sql = format!("DELETE from {} where rowid = {}", history_table, rowid); match conn.execute(&sql, NO_PARAMS) { Ok(_) => { - println!("item deleted."); + return true; } Err(e) => { - println_stderr!("history: prepare error - {:?}", e); + log!("history: prepare error - {:?}", e); + return false; } } } diff --git a/src/builtins/minfd.rs b/src/builtins/minfd.rs new file mode 100644 index 0000000..aa2af48 --- /dev/null +++ b/src/builtins/minfd.rs @@ -0,0 +1,13 @@ +use crate::shell::Shell; +use crate::builtins::utils::print_stdout_with_capture; +use crate::types::{CommandResult, CommandLine, Command}; + +pub fn run(_sh: &mut Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let _fd = unsafe { libc::dup(1) }; + let mut cr = CommandResult::new(); + let info = format!("{}", _fd); + print_stdout_with_capture(&info, &mut cr, cl, cmd, capture); + unsafe { libc::close(_fd); } + cr +} diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index baf450b..57c2752 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -13,3 +13,6 @@ pub mod source; pub mod unalias; pub mod vox; pub mod ulimit; +pub mod minfd; +pub mod set; +pub mod utils; diff --git a/src/builtins/read.rs b/src/builtins/read.rs index 7239136..773ed3a 100644 --- a/src/builtins/read.rs +++ b/src/builtins/read.rs @@ -1,8 +1,9 @@ -use std::io::{self, Write}; +use std::io; -use crate::shell; +use crate::builtins::utils::print_stderr_with_capture; +use crate::shell::Shell; use crate::libs::re::re_contains; -use crate::types::CommandLine; +use crate::types::{CommandResult, CommandLine, Command}; use crate::tools; fn _find_invalid_identifier(name_list: &Vec) -> Option { @@ -14,8 +15,9 @@ fn _find_invalid_identifier(name_list: &Vec) -> Option { None } -pub fn run(sh: &mut shell::Shell, cl: &CommandLine) -> i32 { - let cmd = &cl.commands[0]; +pub fn run(sh: &mut Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let mut cr = CommandResult::new(); let tokens = cmd.tokens.clone(); let name_list: Vec; @@ -24,8 +26,9 @@ pub fn run(sh: &mut shell::Shell, cl: &CommandLine) -> i32 { } else { name_list = tokens[1..].iter().map(|x| x.1.clone()).collect(); if let Some(id_) = _find_invalid_identifier(&name_list) { - println_stderr!("cicada: read: `{}': not a valid identifier", id_); - return 1; + let info = format!("cicada: read: `{}': not a valid identifier", id_); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } } @@ -40,8 +43,9 @@ pub fn run(sh: &mut shell::Shell, cl: &CommandLine) -> i32 { match io::stdin().read_line(&mut buffer) { Ok(_) => {} Err(e) => { - println_stderr!("cicada: read: error in reading stdin: {:?}", e); - return 1; + let info = format!("cicada: read: error in reading stdin: {:?}", e); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } } } @@ -53,8 +57,9 @@ pub fn run(sh: &mut shell::Shell, cl: &CommandLine) -> i32 { for i in 0..idx_2rd_last { let name = name_list.get(i); if name.is_none() { - println_stderr!("cicada: read: name index error"); - return 1; + let info = "cicada: read: name index error"; + print_stderr_with_capture(info, &mut cr, cl, cmd, capture); + return cr; } let name = name.unwrap(); @@ -69,5 +74,5 @@ pub fn run(sh: &mut shell::Shell, cl: &CommandLine) -> i32 { String::new() }; sh.set_env(name_last, &value_left); - 0 + cr } diff --git a/src/builtins/set.rs b/src/builtins/set.rs new file mode 100644 index 0000000..be58d7a --- /dev/null +++ b/src/builtins/set.rs @@ -0,0 +1,47 @@ +use structopt::StructOpt; + +use crate::builtins::utils::print_stderr_with_capture; +use crate::builtins::utils::print_stdout_with_capture; +use crate::parsers; +use crate::shell::Shell; +use crate::types::{CommandResult, CommandLine, Command}; + +#[derive(Debug, StructOpt)] +#[structopt(name = "set", about = "Set shell options (BETA)")] +struct OptMain { + #[structopt(short, help = "exit on error status")] + exit_on_error: bool, +} + +pub fn run(sh: &mut Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let mut cr = CommandResult::new(); + let tokens = &cmd.tokens; + let args = parsers::parser_line::tokens_to_args(tokens); + let show_usage = args.len() > 1 && (args[1] == "-h" || args[1] == "--help"); + + let opt = OptMain::from_iter_safe(args); + match opt { + Ok(opt) => { + if opt.exit_on_error { + sh.exit_on_error = true; + return cr; + } else { + let info = "cicada: set: option not implemented"; + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; + } + } + Err(e) => { + let info = format!("{}", e); + if show_usage { + print_stdout_with_capture(&info, &mut cr, cl, cmd, capture); + cr.status = 0; + } else { + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + cr.status = 1; + } + return cr; + } + } +} diff --git a/src/builtins/source.rs b/src/builtins/source.rs index 32845ed..c039e8b 100644 --- a/src/builtins/source.rs +++ b/src/builtins/source.rs @@ -1,15 +1,22 @@ -use std::io::Write; - +use crate::builtins::utils::print_stderr_with_capture; use crate::parsers; use crate::scripting; -use crate::shell; -use crate::types::Tokens; +use crate::shell::Shell; +use crate::types::{CommandResult, CommandLine, Command}; -pub fn run(sh: &mut shell::Shell, tokens: &Tokens) -> i32 { +pub fn run(sh: &mut Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let mut cr = CommandResult::new(); + let tokens = &cmd.tokens; let args = parsers::parser_line::tokens_to_args(&tokens); + if args.len() < 2 { - println_stderr!("cicada: source: no file specified"); - return 1; + let info = "cicada: source: no file specified"; + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } - return scripting::run_script(sh, &args); + + let status = scripting::run_script(sh, &args); + cr.status = status; + cr } diff --git a/src/builtins/ulimit.rs b/src/builtins/ulimit.rs index b5015e1..bd18e8c 100644 --- a/src/builtins/ulimit.rs +++ b/src/builtins/ulimit.rs @@ -2,14 +2,17 @@ use clap::{value_t, Arg, App}; use libc; use std::io::Error; -use std::io::Write; -use crate::shell; +use crate::builtins::utils::print_stderr_with_capture; +use crate::builtins::utils::print_stdout_with_capture; +use crate::shell::Shell; use crate::parsers; -use crate::types::Tokens; +use crate::types::{CommandResult, CommandLine, Command}; - -pub fn run(_sh: &shell::Shell, tokens: &Tokens) -> i32 { +pub fn run(_sh: &mut Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let mut cr = CommandResult::new(); + let tokens = &cmd.tokens; let args = parsers::parser_line::tokens_to_args(tokens); // NOTE: these default_value -1 is for reporting only // we cannot change the limit less then zero. @@ -37,8 +40,8 @@ pub fn run(_sh: &shell::Shell, tokens: &Tokens) -> i32 { use std::io; let mut out = io::stdout(); app.write_help(&mut out).expect("failed to write to stdout"); - println!(""); - return 0; + print!("\n"); + return CommandResult::new(); } let _matches = app.get_matches_from_safe(&args); @@ -48,8 +51,9 @@ pub fn run(_sh: &shell::Shell, tokens: &Tokens) -> i32 { matches = x; } Err(e) => { - println_stderr!("ulimit error: {}", e.message); - return 1; + let info = format!("ulimit error: {}", e.message); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } } @@ -57,8 +61,9 @@ pub fn run(_sh: &shell::Shell, tokens: &Tokens) -> i32 { match value_t!(matches, "open_files", i64) { Ok(x) => open_files = x, Err(_) => { - println_stderr!("cicada: ulimit: invalid params"); - return 1; + let info = "cicada: ulimit: invalid params"; + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } } @@ -66,8 +71,9 @@ pub fn run(_sh: &shell::Shell, tokens: &Tokens) -> i32 { match value_t!(matches, "core_file_size", i64) { Ok(x) => core_file_size = x, Err(_) => { - println_stderr!("cicada: ulimit: invalid params"); - return 1; + let info = "cicada: ulimit: invalid params"; + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } } @@ -80,28 +86,42 @@ pub fn run(_sh: &shell::Shell, tokens: &Tokens) -> i32 { let for_hard = matches.is_present("for_hard"); if matches.is_present("report_all") || options.len() == 0 { - report_all(for_hard); - return 0; + let (_out, _err) = report_all(for_hard); + if !_out.is_empty() { + print_stdout_with_capture(&_out, &mut cr, cl, cmd, capture); + } + if !_err.is_empty() { + print_stderr_with_capture(&_err, &mut cr, cl, cmd, capture); + } + return cr; } if open_files > -1 { - let ok = set_limit("open_files", open_files as u64, for_hard); - if !ok { - return 1; + let _err = set_limit("open_files", open_files as u64, for_hard); + if !_err.is_empty() { + print_stderr_with_capture(&_err, &mut cr, cl, cmd, capture); + return cr; } } if core_file_size > -1 { - let ok = set_limit("core_file_size", core_file_size as u64, for_hard); - if !ok { - return 1; + let _err = set_limit("core_file_size", core_file_size as u64, for_hard); + if !_err.is_empty() { + print_stderr_with_capture(&_err, &mut cr, cl, cmd, capture); + return cr; } } - report_needed(&options, for_hard, open_files, core_file_size); - 0 + let (_out, _err) = report_needed(&options, for_hard, open_files, core_file_size); + if !_out.is_empty() { + print_stdout_with_capture(&_out, &mut cr, cl, cmd, capture); + } + if !_err.is_empty() { + print_stderr_with_capture(&_err, &mut cr, cl, cmd, capture); + } + cr } -fn set_limit(limit_name: &str, value: u64, for_hard: bool) -> bool { +fn set_limit(limit_name: &str, value: u64, for_hard: bool) -> String { // Since libc::RLIMIT_NOFILE etc has different types on different OS // so we cannot pass them via params, see issue: // https://github.com/rust-lang/libc/issues/2029 @@ -111,8 +131,7 @@ fn set_limit(limit_name: &str, value: u64, for_hard: bool) -> bool { } else if limit_name == "core_file_size" { limit_id = libc::RLIMIT_CORE } else { - println_stderr!("invalid limit name"); - return false; + return String::from("invalid limit name"); } let mut rlp = libc::rlimit {rlim_cur: 0, rlim_max: 0}; @@ -120,8 +139,9 @@ fn set_limit(limit_name: &str, value: u64, for_hard: bool) -> bool { unsafe { let res = libc::getrlimit(limit_id, rlim); if res != 0 { - println_stderr!("cicada: ulimit: error when getting limit: {}", - Error::last_os_error()); + let info = format!("cicada: ulimit: error when getting limit: {}", + Error::last_os_error()); + return String::from(&info); } } @@ -140,15 +160,20 @@ fn set_limit(limit_name: &str, value: u64, for_hard: bool) -> bool { unsafe { let res = libc::setrlimit(limit_id, rlim); if res != 0 { - println_stderr!("cicada: ulimit: error when setting limit: {}", - Error::last_os_error()); - return false; + let info = format!("cicada: ulimit: error when setting limit: {}", + Error::last_os_error()); + return String::from(&info); } } - return true; + + String::new() } -fn print_limit(limit_name: &str, single_print: bool, for_hard: bool) { +fn get_limit(limit_name: &str, single_print: bool, + for_hard: bool) -> (String, String) { + let mut result_stderr = String::new(); + let mut result_stdout = String::new(); + let desc; let limit_id; if limit_name == "open_files" { @@ -158,8 +183,9 @@ fn print_limit(limit_name: &str, single_print: bool, for_hard: bool) { desc = "core file size"; limit_id = libc::RLIMIT_CORE; } else { - println_stderr!("ulimit: error: invalid limit name"); - return; + let info = "ulimit: error: invalid limit name"; + result_stderr.push_str(&info); + return (result_stdout, result_stderr); } let mut rlp = libc::rlimit {rlim_cur: 0, rlim_max: 0}; @@ -167,7 +193,9 @@ fn print_limit(limit_name: &str, single_print: bool, for_hard: bool) { unsafe { let res = libc::getrlimit(limit_id, r); if res != 0 { - println_stderr!("error when getting limit: {}", Error::last_os_error()); + let info = format!("error when getting limit: {}", Error::last_os_error()); + result_stderr.push_str(&info); + return (result_stdout, result_stderr); } let to_print; @@ -179,34 +207,73 @@ fn print_limit(limit_name: &str, single_print: bool, for_hard: bool) { if single_print { if to_print == libc::RLIM_INFINITY { - println!("unlimited"); + result_stdout.push_str("unlimited\n"); } else { - println!("{}", to_print); + let info = format!("{}\n", to_print); + result_stdout.push_str(&info); } } else { if to_print == libc::RLIM_INFINITY { - println!("{}\t\tunlimited", desc); + let info = format!("{}\t\tunlimited\n", desc); + result_stdout.push_str(&info); } else { - println!("{}\t\t{}", desc, to_print); + let info = format!("{}\t\t{}\n", desc, to_print); + result_stdout.push_str(&info); } } } + + (result_stdout, result_stderr) } -fn report_all(for_hard: bool) { - print_limit("open_files", false, for_hard); - print_limit("core_file_size", false, for_hard); +fn report_all(for_hard: bool) -> (String, String) { + let mut result_stderr = String::new(); + let mut result_stdout = String::new(); + + let (_out, _err) = get_limit("open_files", false, for_hard); + if !_out.is_empty() { + result_stdout.push_str(&_out); + } + if !_err.is_empty() { + result_stderr.push_str(&_err); + } + let (_out, _err) = get_limit("core_file_size", false, for_hard); + if !_out.is_empty() { + result_stdout.push_str(&_out); + } + if !_err.is_empty() { + result_stderr.push_str(&_err); + } + + (result_stdout, result_stderr) } fn report_needed(options: &Vec<&String>, for_hard: bool, open_files: i64, - core_file_size: i64) { + core_file_size: i64) -> (String, String) { + let mut result_stderr = String::new(); + let mut result_stdout = String::new(); + let single_print = options.len() == 1; for o in options { if *o == "-n" && open_files == -1 { - print_limit("open_files", single_print, for_hard); + let (_out, _err) = get_limit("open_files", single_print, for_hard); + if !_out.is_empty() { + result_stdout.push_str(&_out); + } + if !_err.is_empty() { + result_stderr.push_str(&_err); + } } if *o == "-c" && core_file_size == -1 { - print_limit("core_file_size", single_print, for_hard); + let (_out, _err) = get_limit("core_file_size", single_print, for_hard); + if !_out.is_empty() { + result_stdout.push_str(&_out); + } + if !_err.is_empty() { + result_stderr.push_str(&_err); + } } } + + (result_stdout, result_stderr) } diff --git a/src/builtins/unalias.rs b/src/builtins/unalias.rs index 607445e..09a1e5e 100644 --- a/src/builtins/unalias.rs +++ b/src/builtins/unalias.rs @@ -1,19 +1,23 @@ -use std::io::Write; +use crate::builtins::utils::print_stderr_with_capture; +use crate::shell::Shell; +use crate::types::{CommandResult, CommandLine, Command}; -use crate::shell; -use crate::types::Tokens; +pub fn run(sh: &mut Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let tokens = cmd.tokens.clone(); + let mut cr = CommandResult::new(); -pub fn run(sh: &mut shell::Shell, tokens: &Tokens) -> i32 { if tokens.len() != 2 { - println_stderr!("unalias syntax error"); - println_stderr!("unalias usage example: alias foo"); - return 1; + let info = "cicada: unalias: syntax error"; + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } let input = &tokens[1].1; if !sh.remove_alias(input) { - println_stderr!("cicada: unalias: {}: not found", input); - return 1; + let info = format!("cicada: unalias: {}: not found", input); + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } - 0 + cr } diff --git a/src/builtins/utils.rs b/src/builtins/utils.rs new file mode 100644 index 0000000..c708aed --- /dev/null +++ b/src/builtins/utils.rs @@ -0,0 +1,163 @@ +use std::fs::File; +use std::io::Write; +use std::os::unix::io::{FromRawFd, RawFd}; + +use crate::tools; +use crate::tools::clog; +use crate::types::{Command, CommandLine, CommandResult, Redirection}; + +/// Helper function to get (stdout, stderr) pairs for redirections, +/// e.g. `alias foo 1>/dev/null 2>&1 > foo.txt` +/// (i.e. [ +/// ("1", ">", "/dev/null"), +/// ("2", ">", "&1"), +/// ("1", ">", "foo.txt"), +/// ]) +fn _get_std_fds(redirects: &[Redirection]) -> (Option, Option) { + if redirects.is_empty() { + return (None, None); + } + + let mut fd_out = None; + let mut fd_err = None; + + for i in 0..redirects.len() { + let item = &redirects[i]; + if item.0 == "1" { + // 1>&2 + let mut _fd_candidate = None; + + if item.2 == "&2" { + let (_fd_out, _fd_err) = _get_std_fds(&redirects[i+1..]); + if let Some(fd) = _fd_err { + _fd_candidate = Some(fd); + } else { + _fd_candidate = unsafe { Some(libc::dup(2)) }; + } + } else { // 1> foo.log + let append = item.1 == ">>"; + if let Ok(fd) = tools::create_raw_fd_from_file(&item.2, append) { + _fd_candidate = Some(fd); + } + } + + // for command like this: `alias > a.txt > b.txt > c.txt`, + // we need to return the last one, but close the previous two. + if let Some(fd) = fd_out { + unsafe { libc::close(fd); } + } + + fd_out = _fd_candidate; + } + + if item.0 == "2" { + // 2>&1 + let mut _fd_candidate = None; + + if item.2 == "&1" { + if let Some(fd) = fd_out { + _fd_candidate = unsafe { Some(libc::dup(fd)) }; + } + } else { // 2>foo.log + let append = item.1 == ">>"; + if let Ok(fd) = tools::create_raw_fd_from_file(&item.2, append) { + _fd_candidate = Some(fd); + } + } + + if let Some(fd) = fd_err { + unsafe { libc::close(fd); } + } + + fd_err = _fd_candidate; + } + } + + (fd_out, fd_err) +} + +fn _get_dupped_stdout_fd(cmd: &Command, cl: &CommandLine) -> RawFd { + // if with pipeline, e.g. `history | grep foo`, then we don't need to + // dup stdout since it is running in a sperated process, whose fd can + // be dropped after use. + if cl.with_pipeline() { + return 1; + } + + let (_fd_out, _fd_err) = _get_std_fds(&cmd.redirects_to); + if let Some(fd) = _fd_err { + unsafe { libc::close(fd); } + } + if let Some(fd) = _fd_out { + fd + } else { + unsafe { libc::dup(1) } + } +} + +fn _get_dupped_stderr_fd(cmd: &Command, cl: &CommandLine) -> RawFd { + if cl.with_pipeline() { + return 2; + } + + let (_fd_out, _fd_err) = _get_std_fds(&cmd.redirects_to); + if let Some(fd) = _fd_out { + unsafe { libc::close(fd); } + } + + if let Some(fd) = _fd_err { + fd + } else { + unsafe { libc::dup(2) } + } +} + +pub fn print_stdout(info: &str, cmd: &Command, cl: &CommandLine) { + let fd = _get_dupped_stdout_fd(cmd, cl); + log!("created stdout fd: {:?}", fd); + + unsafe { + let mut f = File::from_raw_fd(fd); + let info = info.trim_end_matches('\n'); + f.write_all(info.as_bytes()).unwrap(); + if !info.is_empty() { + f.write_all(b"\n").unwrap(); + } + } +} + +pub fn print_stderr(info: &str, cmd: &Command, cl: &CommandLine) { + let fd = _get_dupped_stderr_fd(cmd, cl); + log!("created stderr fd: {}", fd); + + unsafe { + let mut f = File::from_raw_fd(fd); + let info = info.trim_end_matches('\n'); + f.write_all(info.as_bytes()).unwrap(); + if !info.is_empty() { + f.write_all(b"\n").unwrap(); + } + } +} + +pub fn print_stderr_with_capture(info: &str, cr: &mut CommandResult, + cl: &CommandLine, cmd: &Command, + capture: bool) { + cr.status = 1; + if capture { + cr.stderr = info.to_string(); + } else { + print_stderr(info, cmd, cl); + } +} + +pub fn print_stdout_with_capture(info: &str, cr: &mut CommandResult, + cl: &CommandLine, cmd: &Command, + capture: bool) { + cr.status = 0; + if capture { + cr.stdout = info.to_string(); + } else { + print_stdout(info, cmd, cl); + } +} diff --git a/src/builtins/vox.rs b/src/builtins/vox.rs index a35482e..97afac5 100644 --- a/src/builtins/vox.rs +++ b/src/builtins/vox.rs @@ -1,11 +1,12 @@ use std::env; -use std::fs::{self, read_dir}; -use std::io::Write; +use std::fs; use std::path::Path; +use crate::builtins::utils::print_stderr_with_capture; +use crate::builtins::utils::print_stdout_with_capture; use crate::parsers; -use crate::shell; -use crate::types; +use crate::shell::{self, Shell}; +use crate::types::{self, CommandResult, CommandLine, Command}; fn in_env() -> bool { if let Ok(x) = env::var("VIRTUAL_ENV") { @@ -29,21 +30,25 @@ fn get_envs_home() -> String { home_envs } -fn list_envs() -> i32 { +fn get_all_venvs() -> Result, String> { let home_envs = get_envs_home(); if home_envs.is_empty() { - println_stderr!("you need to set VIRTUALENV_HOME to use vox"); - return 1; + let info = String::from("you need to set VIRTUALENV_HOME to use vox"); + return Err(info); } if !Path::new(home_envs.as_str()).exists() { match fs::create_dir_all(home_envs.as_str()) { Ok(_) => {} - Err(e) => println_stderr!("fs create_dir_all failed: {:?}", e), + Err(e) => { + let info = format!("fs create_dir_all failed: {:?}", e); + return Err(info); + } } } + let mut venvs = Vec::new(); let pdir = home_envs.clone(); - if let Ok(list) = read_dir(home_envs) { + if let Ok(list) = fs::read_dir(home_envs) { for ent in list { if let Ok(ent) = ent { let ent_name = ent.file_name(); @@ -52,24 +57,24 @@ fn list_envs() -> i32 { if !Path::new(full_path.as_str()).exists() { continue; } - println!("{}", path); + venvs.push(path); } } } } - 0 + + Ok(venvs) } -fn enter_env(sh: &shell::Shell, path: &str) -> i32 { +fn enter_env(sh: &Shell, path: &str) -> String { if in_env() { - println_stderr!("vox: already in env"); - return 1; + return format!("vox: already in env"); } + let home_envs = get_envs_home(); let full_path = format!("{}/{}/bin/activate", home_envs, path); if !Path::new(full_path.as_str()).exists() { - println_stderr!("no such env: {}", full_path); - return 1; + return format!("no such env: {}", full_path); } let path_env = format!("{}/{}", home_envs, path); @@ -79,22 +84,22 @@ fn enter_env(sh: &shell::Shell, path: &str) -> i32 { tokens.push((String::new(), path_new)); shell::expand_env(sh, &mut tokens); env::set_var("PATH", &tokens[0].1); - 0 + String::new() } -fn exit_env(sh: &shell::Shell) -> i32 { +fn exit_env(sh: &Shell) -> String { if !in_env() { - println_stderr!("vox: not in an env"); - return 0; + return String::from("vox: not in an env"); } + let env_path; match env::var("PATH") { Ok(x) => env_path = x, Err(_) => { - println_stderr!("vox: cannot read PATH env"); - return 1; + return String::from("vox: cannot read PATH env"); } } + let mut _tokens: Vec<&str> = env_path.split(':').collect(); let mut path_virtual_env = String::from("${VIRTUAL_ENV}/bin"); // shell::extend_env(sh, &mut path_virtual_env); @@ -109,26 +114,47 @@ fn exit_env(sh: &shell::Shell) -> i32 { let env_path_new = _tokens.join(":"); env::set_var("PATH", &env_path_new); env::set_var("VIRTUAL_ENV", ""); - 0 + + String::new() } -pub fn run(sh: &shell::Shell, tokens: &types::Tokens) -> i32 { - let args = parsers::parser_line::tokens_to_args(tokens); +pub fn run(sh: &mut Shell, cl: &CommandLine, cmd: &Command, + capture: bool) -> CommandResult { + let mut cr = CommandResult::new(); + let tokens = cmd.tokens.clone(); + let args = parsers::parser_line::tokens_to_args(&tokens); let len = args.len(); - if len == 1 { - return list_envs(); + let subcmd = if len > 1 { &args[1] } else { "" }; + + if len == 1 || (len == 2 && subcmd == "ls") { + match get_all_venvs() { + Ok(venvs) => { + let info = venvs.join("\n"); + print_stdout_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; + } + Err(reason) => { + print_stderr_with_capture(&reason, &mut cr, cl, cmd, capture); + return cr; + } + } } - let subcmd = &args[1]; - if len == 2 && subcmd == "ls" { - list_envs() - } else if len == 3 && subcmd == "enter" { - enter_env(sh, args[2].as_str()) + if len == 3 && subcmd == "enter" { + let _err = enter_env(sh, args[2].as_str()); + if !_err.is_empty() { + print_stderr_with_capture(&_err, &mut cr, cl, cmd, capture); + } + return cr; } else if len == 2 && subcmd == "exit" { - exit_env(sh) + let _err = exit_env(sh); + if !_err.is_empty() { + print_stderr_with_capture(&_err, &mut cr, cl, cmd, capture); + } + return cr; } else { - println_stderr!("vox: invalid command"); - println_stderr!("usage: vox (ls | enter | exit)"); - 1 + let info = "cicada: vox: invalid option"; + print_stderr_with_capture(&info, &mut cr, cl, cmd, capture); + return cr; } } diff --git a/src/completers/path.rs b/src/completers/path.rs index 6846a4f..66b4f15 100644 --- a/src/completers/path.rs +++ b/src/completers/path.rs @@ -190,9 +190,11 @@ fn complete_bin(sh: &shell::Shell, path: &str) -> Vec { suffix: Suffix::Default, }); } + let builtins = vec![ "alias", "bg", "cd", "cinfo", "exec", "exit", "export", "fg", - "history", "jobs", "source", "ulimit", "unalias", "vox", + "history", "jobs", "read", "source", "ulimit", "unalias", "vox", + "minfd", "set", ]; for item in &builtins { if !item.starts_with(fname) { diff --git a/src/core.rs b/src/core.rs index 3b3464f..c306e01 100644 --- a/src/core.rs +++ b/src/core.rs @@ -18,56 +18,80 @@ use crate::parsers; use crate::scripting; use crate::shell::{self, Shell}; use crate::tools::{self, clog}; -use crate::types::{self, CommandLine, CommandOptions, CommandResult, Tokens}; +use crate::types::{self, CommandLine, CommandOptions, CommandResult}; -fn with_pipeline(tokens: &Tokens) -> bool { - for item in tokens { - if item.1 == "|" || item.1 == ">" { - return true; - } +fn try_run_builtin_in_subprocess(sh: &mut Shell, cl: &CommandLine, + idx_cmd: usize, capture: bool) -> Option { + if let Some(cr) = try_run_builtin(sh, cl, idx_cmd, capture) { + return Some(cr.status); } - false + None } -fn try_run_builtin(sh: &mut Shell, cl: &CommandLine) -> Option { - let tokens = cl.commands[0].tokens.clone(); - let cmd = tokens[0].1.clone(); - if cmd == "alias" && !with_pipeline(&tokens) { - let status = builtins::alias::run(sh, &tokens); - return Some(CommandResult::from_status(0, status)); - } else if cmd == "bg" { - let status = builtins::bg::run(sh, &tokens); - return Some(CommandResult::from_status(0, status)); - } else if cmd == "cd" { - let status = builtins::cd::run(sh, &tokens); - return Some(CommandResult::from_status(0, status)); - } else if cmd == "export" { - let status = builtins::export::run(sh, &tokens); - return Some(CommandResult::from_status(0, status)); - } else if cmd == "exec" { - let status = builtins::exec::run(&tokens); - return Some(CommandResult::from_status(0, status)); - } else if cmd == "exit" { - let status = builtins::exit::run(sh, &tokens); - return Some(CommandResult::from_status(0, status)); - } else if cmd == "fg" { - let status = builtins::fg::run(sh, &tokens); - return Some(CommandResult::from_status(0, status)); - } else if cmd == "read" { - let status = builtins::read::run(sh, cl); - return Some(CommandResult::from_status(0, status)); - } else if cmd == "vox" && tokens.len() > 1 && (tokens[1].1 == "enter" || tokens[1].1 == "exit") { - let status = builtins::vox::run(sh, &tokens); - return Some(CommandResult::from_status(0, status)); - } else if (cmd == "source" || cmd == ".") && tokens.len() <= 2 { - let status = builtins::source::run(sh, &tokens); - return Some(CommandResult::from_status(0, status)); - } else if cmd == "ulimit" { - let status = builtins::ulimit::run(sh, &tokens); - return Some(CommandResult::from_status(0, status)); - } else if cmd == "unalias" { - let status = builtins::unalias::run(sh, &tokens); +fn try_run_builtin(sh: &mut Shell, cl: &CommandLine, + idx_cmd: usize, capture: bool) -> Option { + // for builtin, only capture its outputs when it locates at the end + let capture = capture && idx_cmd +1 == cl.commands.len(); + + if idx_cmd >= cl.commands.len() { + println_stderr!("unexpected error in try_run_builtin"); + return None; + } + + let cmd = &cl.commands[idx_cmd]; + let tokens = cmd.tokens.clone(); + let cname = tokens[0].1.clone(); + if cname == "alias" { + let cr = builtins::alias::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "bg" { + let cr = builtins::bg::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "cd" { + let cr = builtins::cd::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "cinfo" { + let cr = builtins::cinfo::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "exec" { + let cr = builtins::exec::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "exit" { + let cr = builtins::exit::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "export" { + let cr = builtins::export::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "fg" { + let cr = builtins::fg::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "history" { + let cr = builtins::history::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "jobs" { + let status = builtins::jobs::run(sh); return Some(CommandResult::from_status(0, status)); + } else if cname == "minfd" { + let cr = builtins::minfd::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "read" { + let cr = builtins::read::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "set" { + let cr = builtins::set::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "source" { + let cr = builtins::source::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "ulimit" { + let cr = builtins::ulimit::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "unalias" { + let cr = builtins::unalias::run(sh, cl, cmd, capture); + return Some(cr); + } else if cname == "vox" { + let cr = builtins::vox::run(sh, cl, cmd, capture); + return Some(cr); } None } @@ -77,11 +101,6 @@ fn try_run_builtin(sh: &mut Shell, cl: &CommandLine) -> Option { pub fn run_pipeline(sh: &mut shell::Shell, cl: &CommandLine, tty: bool, capture: bool, log_cmd: bool) -> (bool, CommandResult) { let mut term_given = false; - - if let Some(cr) = try_run_builtin(sh, cl) { - return (term_given, cr); - } - if cl.background && capture { println_stderr!("cicada: cannot capture output of background cmd"); return (term_given, CommandResult::error()); @@ -148,6 +167,10 @@ pub fn run_pipeline(sh: &mut shell::Shell, cl: &CommandLine, tty: bool, } } + if cl.is_single_and_builtin() { + return (false, cmd_result); + } + if cl.background { if let Some(job) = sh.get_job_by_gid(pgid) { println_stderr!("[{}] {}", job.id, job.gid); @@ -176,8 +199,18 @@ fn _run_single_command(sh: &mut shell::Shell, cl: &CommandLine, idx_cmd: usize, options: &CommandOptions, pgid: &mut i32, term_given: &mut bool, cmd_result: &mut CommandResult, pipes: &Vec<(RawFd, RawFd)>) -> i32 { - let cmd = cl.commands.get(idx_cmd).unwrap(); - // log!(" - run cmd: {:?}", &cmd.tokens); + let capture = options.capture_output; + if cl.is_single_and_builtin() { + if let Some(cr) = try_run_builtin(sh, cl, idx_cmd, capture) { + *cmd_result = cr; + return unsafe { libc::getpid() }; + } + + println_stderr!("cicada: error when run singler builtin"); + log!("error when run singler builtin: {:?}", cl); + return 1; + } + let pipes_count = pipes.len(); let mut fds_capture_stdout = None; let mut fds_capture_stderr = None; @@ -186,6 +219,8 @@ fn _run_single_command(sh: &mut shell::Shell, cl: &CommandLine, idx_cmd: usize, fds_capture_stdout = tools::create_fds(); fds_capture_stderr = tools::create_fds(); } + + let cmd = cl.commands.get(idx_cmd).unwrap(); if cmd.has_here_string() { fds_stdin = tools::create_fds(); } @@ -319,28 +354,10 @@ fn _run_single_command(sh: &mut shell::Shell, cl: &CommandLine, idx_cmd: usize, } } - let program = &cmd.tokens[0].1; - if program == "history" { - let status = builtins::history::run(sh, &cmd); - process::exit(status); - } else if program == "vox" { - let status = builtins::vox::run(sh, &cmd.tokens); - process::exit(status); - } else if program == "cinfo" { - let status = builtins::cinfo::run(); - process::exit(status); - } else if program == "jobs" { - let status = builtins::jobs::run(sh); - process::exit(status); - } else if program == "source" || program == "." { - // NOTE: do pipeline on source would make processes forked, - // which may not get correct results (e.g. `echo $$`), - // (e.g. cannot make new $PROMPT take effects). - let status = builtins::source::run(sh, &cmd.tokens); - process::exit(status); - } else if program == "alias" { - let status = builtins::alias::run(sh, &cmd.tokens); - process::exit(status); + if cmd.is_builtin() { + if let Some(status) = try_run_builtin_in_subprocess(sh, cl, idx_cmd, capture) { + process::exit(status); + } } // our strings do not have '\x00' bytes in them, @@ -356,6 +373,7 @@ fn _run_single_command(sh: &mut shell::Shell, cl: &CommandLine, idx_cmd: usize, ); } + let program = &cmd.tokens[0].1; let path = if program.contains('/') { program.clone() } else { @@ -510,6 +528,10 @@ fn _run_single_command(sh: &mut shell::Shell, cl: &CommandLine, idx_cmd: usize, fn try_run_func(sh: &mut Shell, cl: &CommandLine, capture: bool, log_cmd: bool) -> Option { + if cl.is_empty() { + return None; + } + let command = &cl.commands[0]; if let Some(func_body) = sh.get_func(&command.tokens[0].1) { let mut args = vec!["cicada".to_string()]; diff --git a/src/execute.rs b/src/execute.rs index 96cf8f9..989b80e 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -93,18 +93,19 @@ fn set_shell_vars(sh: &mut Shell, envs: &HashMap) { /// example 2: `ls | wc` fn run_proc(sh: &mut Shell, line: &str, tty: bool, capture: bool) -> CommandResult { - let (tokens, envs) = line_to_tokens(sh, line); - if tokens.is_empty() { - // empty tokens means, only envs are exising e.g. - // $ FOO=1 BAR=2 - // then we need to define these **Shell Variables**. - set_shell_vars(sh, &envs); - return CommandResult::new(); - } - let log_cmd = !sh.cmd.starts_with(' '); match CommandLine::from_line(&line, sh) { Ok(cl) => { + if cl.is_empty() { + // for commands with only envs, e.g. + // $ FOO=1 BAR=2 + // we need to define these **Shell Variables**. + if !cl.envs.is_empty() { + set_shell_vars(sh, &cl.envs); + } + return CommandResult::new(); + } + let (term_given, cr) = core::run_pipeline(sh, &cl, tty, capture, log_cmd); if term_given { unsafe { diff --git a/src/scripting.rs b/src/scripting.rs index dd3743e..f1c0e82 100644 --- a/src/scripting.rs +++ b/src/scripting.rs @@ -417,7 +417,7 @@ fn run_exp(sh: &mut shell::Shell, cr_list.append(&mut _cr_list); if let Some(last) = cr_list.last() { let status = last.status; - if status != 0 { + if status != 0 && sh.exit_on_error { return (cr_list, false, false); } } diff --git a/src/shell.rs b/src/shell.rs index 78c6486..00839f9 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -28,6 +28,7 @@ pub struct Shell { pub previous_cmd: String, pub previous_status: i32, pub is_login: bool, + pub exit_on_error: bool, pub session_id: String, } @@ -47,6 +48,7 @@ impl Shell { previous_cmd: String::new(), previous_status: 0, is_login: false, + exit_on_error: false, session_id: session_id.to_string(), } } @@ -737,7 +739,6 @@ fn do_command_substitution_for_dollar(sh: &mut Shell, tokens: &mut types::Tokens } } - log!("run subcmd 1: {:?}", &cmd); let cmd_result; match CommandLine::from_line(&cmd, sh) { Ok(c) => { diff --git a/src/tools.rs b/src/tools.rs index a3de6c4..2ee21f7 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -50,7 +50,7 @@ pub fn clog(s: &str) { let pid = unsafe { libc::getpid() }; let now = Local::now(); let s = format!( - "[{:04}-{:02}-{:02} {:02}:{:02}:{:02}][{}]{}", + "[{:04}-{:02}-{:02} {:02}:{:02}:{:02}][{}] {}", now.year(), now.month(), now.day(), @@ -136,7 +136,7 @@ pub fn unquote(s: &str) -> String { } pub fn is_env(line: &str) -> bool { - re_contains(line, r"^[a-zA-Z0-9_]+=.*$") + re_contains(line, r"^[a-zA-Z_][a-zA-Z0-9_]*=.*$") } // #[allow(clippy::trivial_regex)] @@ -367,6 +367,15 @@ pub fn create_fds() -> Option<(RawFd, RawFd)> { } } +pub fn is_builtin(s: &str) -> bool { + let builtins = vec![ + "alias", "bg", "cd", "cinfo", "exec", "exit", "export", "fg", + "history", "jobs", "read", "source", "ulimit", "unalias", "vox", + "minfd", "set", + ]; + builtins.contains(&s) +} + #[cfg(test)] mod tests { use super::escape_path; diff --git a/src/types.rs b/src/types.rs index dcee449..9acd21f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -5,6 +5,7 @@ use crate::parsers; use crate::parsers::parser_line::tokens_to_redirections; use crate::shell; use crate::libs; +use crate::tools; pub const STOPPED: i32 = 148; @@ -104,6 +105,9 @@ impl Command { self.redirect_from.clone().unwrap().0 == "<<<" } + pub fn is_builtin(&self) -> bool { + tools::is_builtin(&self.tokens[0].1) + } } #[derive(Debug, Clone, Default)] @@ -239,4 +243,16 @@ impl CommandLine { background: background, }) } + + pub fn is_empty(&self) -> bool { + self.commands.is_empty() + } + + pub fn with_pipeline(&self) -> bool { + self.commands.len() > 1 + } + + pub fn is_single_and_builtin(&self) -> bool { + self.commands.len() == 1 && self.commands[0].is_builtin() + } } diff --git a/tests/scripts/redirections-001.sh b/tests/scripts/redirections-001.sh index 82e3473..8fdad73 100644 --- a/tests/scripts/redirections-001.sh +++ b/tests/scripts/redirections-001.sh @@ -23,3 +23,84 @@ cat <<< hello | wc <<< a | wc cat <<< a | wc > /dev/null | wc echo ==2== + +# test builtin redirections +echo check minfd 1 +# this would be 4, since 3 is occupied by file of current script +minfd # check min fd + +alias foo='echo 135' +alias foo + +echo no output +alias foo >/dev/null + +alias foo >/dev/null 2>&1 > foo-alias.txt +echo result in file +cat foo-alias.txt +rm -f foo-alias.txt + +echo check minfd 2 +minfd # check min fd + +echo ==3== + +alias bar-not-exist # not found, print to stderr +alias bar-not-exist >/dev/null 2>&1 # not output at all + +alias bar-not-exist >bar-alias.txt 2>&1 # not output at all +rm -f bar-alias.txt + +alias bar-not-exist 2>bar1-alias.txt 2>bar2-alias.txt +echo check bar1 +cat bar1-alias.txt +echo check bar2 +cat bar2-alias.txt +echo after check bar2 + +rm -f bar*-alias.txt + +# run some random pipeline before minfd to cover pipes closing +echo hi | wc -l | wc | cat | cat >/dev/null 2>&1 + +echo check minfd err +minfd # check min fd + +echo ==4== + +alias sec5_1="echo xsec51" +echo one alias +alias sec5_1 + +echo one alias with grep +alias sec5_1 | grep -o xsec51 + +echo all alias with grep +alias | grep -o xsec51 + +echo builtin alias in mid +echo hi | alias | grep -o xsec51 + +echo check minfd err +minfd # check min fd + +echo ==5== + +alias sec6_1 2>&1 | grep -o sec6 +bg 12345 2>&1 | grep -o 'no job' +cd sec6_foo 2>&1 | grep -o 'sec6....' +cinfo | grep -o 'os.name' +exec abcfoo 2>&1 | grep -o 'No such' +exit foo bar 2>&1 | grep -o 'too many' +export 2fa=bad 2>&1 | grep -o 'usage' +fg 12345 2>&1 | grep -o 'no job' +history -h | grep -o limit +read 2foo 2>&1 | grep -o identifier +set -h | grep -o error +ulimit | grep -o open +unalias 2>&1 | grep -o syntax +vox enter not-exists 2>&1 | grep -o not +echo check minfd err 6 +minfd + +echo ==6== diff --git a/tests/scripts/redirections-001.sh.out b/tests/scripts/redirections-001.sh.out index a81753c..12ddd67 100644 --- a/tests/scripts/redirections-001.sh.out +++ b/tests/scripts/redirections-001.sh.out @@ -10,3 +10,48 @@ foo bar 1 3 25 0 0 0 ==2== +check minfd 1 +4 +alias foo='echo 135' +no output +result in file +alias foo='echo 135' +check minfd 2 +4 +==3== +check bar1 +check bar2 +cicada: alias: bar-not-exist: not found +after check bar2 +check minfd err +4 +==4== +one alias +alias sec5_1='echo xsec51' +one alias with grep +xsec51 +all alias with grep +xsec51 +builtin alias in mid +xsec51 +check minfd err +4 +==5== +sec6 +no job +sec6_foo +os-name +No such +too many +usage +no job +limit +limit +identifier +error +open +syntax +not +check minfd err 6 +4 +==6==