├── macros ├── LICENSE ├── Cargo.toml └── src │ ├── parser.rs │ ├── lib.rs │ └── lexer.rs ├── rustfmt.toml ├── .gitignore ├── examples ├── progress.rs ├── tracing.rs ├── rust_cookbook.rs ├── dd_test.rs ├── pipes.sh ├── tetris.sh ├── pipes.rs └── tetris.rs ├── .github └── workflows │ └── rust.yml ├── Cargo.toml ├── src ├── builtins.rs ├── logger.rs ├── thread_local.rs ├── io.rs ├── lib.rs ├── child.rs └── process.rs ├── LICENSE ├── tests └── test_macros.rs └── README.md /macros/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | style_edition = "2024" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | **/*.rs.bk 4 | **/*.swp 5 | -------------------------------------------------------------------------------- /examples/progress.rs: -------------------------------------------------------------------------------- 1 | use cmd_lib::{CmdResult, run_cmd}; 2 | 3 | #[cmd_lib::main] 4 | fn main() -> CmdResult { 5 | run_cmd!(dd if=/dev/urandom of=/dev/null bs=1M status=progress)?; 6 | 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cmd_lib_macros" 3 | description = "Common rust commandline macros and utils, to write shell script like tasks easily" 4 | license = "MIT OR Apache-2.0" 5 | homepage = "https://github.com/rust-shell-script/rust_cmd_lib" 6 | repository = "https://github.com/rust-shell-script/rust_cmd_lib" 7 | keywords = ["shell", "script", "cli", "process", "pipe"] 8 | version = "2.0.0" 9 | authors = ["Tao Guo "] 10 | edition = "2024" 11 | rust-version = "1.88" 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | syn = { version = "2", features = ["full"] } 18 | quote = "1" 19 | proc-macro2 = { version = "1.0.86", features = ["span-locations"] } 20 | proc-macro-error2 = "2" 21 | 22 | [dev-dependencies] 23 | cmd_lib = { path = ".." } 24 | -------------------------------------------------------------------------------- /examples/tracing.rs: -------------------------------------------------------------------------------- 1 | use cmd_lib::{CmdResult, run_cmd}; 2 | use tracing::level_filters::LevelFilter; 3 | use tracing_subscriber::{EnvFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _}; 4 | 5 | #[cmd_lib::main] 6 | fn main() -> CmdResult { 7 | tracing_subscriber::registry() 8 | .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr)) 9 | .with( 10 | EnvFilter::builder() 11 | .with_default_directive(LevelFilter::INFO.into()) 12 | .from_env_lossy(), 13 | ) 14 | .init(); 15 | 16 | copy_thing()?; 17 | 18 | Ok(()) 19 | } 20 | 21 | #[tracing::instrument] 22 | fn copy_thing() -> CmdResult { 23 | // Log output from stderr inherits the `copy_thing` span from this function 24 | run_cmd!(dd if=/dev/urandom of=/dev/null bs=1M count=1000)?; 25 | 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cmd_lib" 3 | description = "Common rust commandline macros and utils, to write shell script like tasks easily" 4 | license = "MIT OR Apache-2.0" 5 | homepage = "https://github.com/rust-shell-script/rust_cmd_lib" 6 | repository = "https://github.com/rust-shell-script/rust_cmd_lib" 7 | documentation = "https://docs.rs/cmd_lib" 8 | keywords = ["shell", "script", "cli", "process", "pipe"] 9 | categories = ["command-line-interface", "command-line-utilities"] 10 | readme = "README.md" 11 | version = "2.0.0" 12 | authors = ["rust-shell-script "] 13 | edition = "2024" 14 | rust-version = "1.88" 15 | 16 | [workspace] 17 | members = ["macros"] 18 | 19 | [dependencies] 20 | cmd_lib_macros = { version = "2.0.0", path = "./macros" } 21 | log = "0.4.27" 22 | faccess = "0.2.4" 23 | os_pipe = "1.2.2" 24 | env_logger = "0.11.8" 25 | build-print = { version = "1.0", optional = true } 26 | tracing = { version = "0.1.41", optional = true } 27 | 28 | [dev-dependencies] 29 | rayon = "1.11.0" 30 | clap = { version = "4", features = ["derive"] } 31 | byte-unit = "4.0.19" 32 | tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } 33 | 34 | [features] 35 | build-print = ["dep:build-print"] 36 | 37 | [[example]] 38 | name = "tracing" 39 | required-features = ["tracing"] 40 | -------------------------------------------------------------------------------- /src/builtins.rs: -------------------------------------------------------------------------------- 1 | use crate::{CmdEnv, CmdResult}; 2 | use crate::{debug, error, info, trace, warn}; 3 | use std::io::{Read, Write}; 4 | 5 | pub(crate) fn builtin_echo(env: &mut CmdEnv) -> CmdResult { 6 | let args = env.get_args(); 7 | let msg = if !args.is_empty() && args[0] == "-n" { 8 | args[1..].join(" ") 9 | } else { 10 | args.join(" ") + "\n" 11 | }; 12 | 13 | write!(env.stdout(), "{}", msg) 14 | } 15 | 16 | pub(crate) fn builtin_error(env: &mut CmdEnv) -> CmdResult { 17 | error!("{}", env.get_args().join(" ")); 18 | Ok(()) 19 | } 20 | 21 | pub(crate) fn builtin_warn(env: &mut CmdEnv) -> CmdResult { 22 | warn!("{}", env.get_args().join(" ")); 23 | Ok(()) 24 | } 25 | 26 | pub(crate) fn builtin_info(env: &mut CmdEnv) -> CmdResult { 27 | info!("{}", env.get_args().join(" ")); 28 | Ok(()) 29 | } 30 | 31 | pub(crate) fn builtin_debug(env: &mut CmdEnv) -> CmdResult { 32 | debug!("{}", env.get_args().join(" ")); 33 | Ok(()) 34 | } 35 | 36 | pub(crate) fn builtin_trace(env: &mut CmdEnv) -> CmdResult { 37 | trace!("{}", env.get_args().join(" ")); 38 | Ok(()) 39 | } 40 | 41 | pub(crate) fn builtin_empty(env: &mut CmdEnv) -> CmdResult { 42 | let mut buf = vec![]; 43 | env.stdin().read_to_end(&mut buf)?; 44 | env.stdout().write_all(&buf)?; 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /examples/rust_cookbook.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Rewrite examples with rust_cmd_lib from 3 | // https://rust-lang-nursery.github.io/rust-cookbook/os/external.html 4 | // 5 | use cmd_lib::*; 6 | use std::io::{BufRead, BufReader}; 7 | 8 | #[cmd_lib::main] 9 | fn main() -> CmdResult { 10 | cmd_lib::set_pipefail(false); // do not fail due to pipe errors 11 | 12 | // Run an external command and process stdout 13 | run_cmd!(git log --oneline | head -5)?; 14 | 15 | // Run an external command passing it stdin and check for an error code 16 | run_cmd!(echo "import this; copyright(); credits(); exit()" | python)?; 17 | 18 | // Run piped external commands 19 | let directory = std::env::current_dir()?; 20 | println!( 21 | "Top 10 biggest files and directories in '{}':\n{}", 22 | directory.display(), 23 | run_fun!(du -ah . | sort -hr | head -n 10)? 24 | ); 25 | 26 | // Redirect both stdout and stderr of child process to the same file 27 | run_cmd!(ignore ls . oops &>out.txt)?; 28 | run_cmd!(rm -f out.txt)?; 29 | 30 | // Continuously process child process' outputs 31 | spawn_with_output!(journalctl)?.wait_with_pipe(&mut |pipe| { 32 | BufReader::new(pipe) 33 | .lines() 34 | .filter_map(|line| line.ok()) 35 | .filter(|line| line.find("usb").is_some()) 36 | .take(10) 37 | .for_each(|line| println!("{}", line)); 38 | })?; 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | use env_logger::Env; 2 | 3 | pub fn try_init_default_logger() { 4 | let _ = env_logger::Builder::from_env(Env::default().default_filter_or("info")) 5 | .format_target(false) 6 | .format_timestamp(None) 7 | .try_init(); 8 | } 9 | 10 | #[doc(hidden)] 11 | #[macro_export] 12 | macro_rules! error { 13 | ($($arg:tt)*) => {{ 14 | $crate::try_init_default_logger(); 15 | $crate::inner_log::error!($($arg)*); 16 | }} 17 | } 18 | 19 | #[doc(hidden)] 20 | #[macro_export] 21 | macro_rules! warn { 22 | ($($arg:tt)*) => {{ 23 | $crate::try_init_default_logger(); 24 | $crate::inner_log::warn!($($arg)*); 25 | }} 26 | } 27 | 28 | #[doc(hidden)] 29 | #[macro_export] 30 | macro_rules! info { 31 | ($($arg:tt)*) => {{ 32 | $crate::try_init_default_logger(); 33 | $crate::inner_log::info!($($arg)*); 34 | }} 35 | } 36 | 37 | #[doc(hidden)] 38 | #[macro_export] 39 | macro_rules! debug { 40 | ($($arg:tt)*) => {{ 41 | $crate::try_init_default_logger(); 42 | #[cfg(feature = "build-print")] 43 | $crate::inner_log::info!($($arg)*); 44 | #[cfg(not(feature = "build-print"))] 45 | $crate::inner_log::debug!($($arg)*); 46 | }} 47 | } 48 | 49 | #[doc(hidden)] 50 | #[macro_export] 51 | macro_rules! trace { 52 | ($($arg:tt)*) => {{ 53 | $crate::try_init_default_logger(); 54 | #[cfg(feature = "build-print")] 55 | $crate::inner_log::info!($($arg)*); 56 | #[cfg(not(feature = "build-print"))] 57 | $crate::inner_log::trace!($($arg)*); 58 | }} 59 | } 60 | -------------------------------------------------------------------------------- /examples/dd_test.rs: -------------------------------------------------------------------------------- 1 | // get disk read bandwidth with multiple threads 2 | // 3 | // Usage: dd_test [-b ] [-t ] -f 4 | // 5 | // e.g: 6 | //! ➜ rust_cmd_lib git:(master) ✗ cargo run --example dd_test -- -b 4096 -f /dev/nvme0n1 -t 4 7 | //! Finished dev [unoptimized + debuginfo] target(s) in 0.04s 8 | //! Running `target/debug/examples/dd_test -b 4096 -f /dev/nvme0n1 -t 4` 9 | //! [INFO ] Dropping caches at first 10 | //! [INFO ] Running with thread_num: 4, block_size: 4096 11 | //! [INFO ] thread 3 bandwidth: 317 MB/s 12 | //! [INFO ] thread 1 bandwidth: 289 MB/s 13 | //! [INFO ] thread 0 bandwidth: 281 MB/s 14 | //! [INFO ] thread 2 bandwidth: 279 MB/s 15 | //! [INFO ] Total bandwidth: 1.11 GiB/s 16 | //! ``` 17 | use byte_unit::Byte; 18 | use clap::Parser; 19 | use cmd_lib::*; 20 | use rayon::prelude::*; 21 | use std::time::Instant; 22 | 23 | const DATA_SIZE: u64 = 10 * 1024 * 1024 * 1024; // 10GB data 24 | 25 | #[derive(Parser)] 26 | #[clap(name = "dd_test", about = "Get disk read bandwidth.")] 27 | struct Opt { 28 | #[clap(short, default_value = "4096")] 29 | block_size: u64, 30 | #[clap(short, default_value = "1")] 31 | thread_num: u64, 32 | #[clap(short)] 33 | file: String, 34 | } 35 | 36 | #[cmd_lib::main] 37 | fn main() -> CmdResult { 38 | let Opt { 39 | block_size, 40 | thread_num, 41 | file, 42 | } = Opt::parse(); 43 | 44 | run_cmd! ( 45 | info "Dropping caches at first"; 46 | sudo bash -c "echo 3 > /proc/sys/vm/drop_caches"; 47 | info "Running with thread_num: $thread_num, block_size: $block_size"; 48 | )?; 49 | let cnt = DATA_SIZE / thread_num / block_size; 50 | let now = Instant::now(); 51 | (0..thread_num).into_par_iter().for_each(|i| { 52 | let off = cnt * i; 53 | let bandwidth = run_fun!( 54 | sudo bash -c "dd if=$file of=/dev/null bs=$block_size skip=$off count=$cnt 2>&1" 55 | | awk r#"/copied/{print $(NF-1) " " $NF}"# 56 | ) 57 | .unwrap_or_else(|_| cmd_die!("thread $i failed")); 58 | info!("thread {i} bandwidth: {bandwidth}"); 59 | }); 60 | let total_bandwidth = 61 | Byte::from_bytes((DATA_SIZE / now.elapsed().as_secs()) as u128).get_appropriate_unit(true); 62 | info!("Total bandwidth: {total_bandwidth}/s"); 63 | 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /src/thread_local.rs: -------------------------------------------------------------------------------- 1 | /// Declare a new thread local storage variable. 2 | /// ``` 3 | /// # use cmd_lib::*; 4 | /// use std::collections::HashMap; 5 | /// tls_init!(LEN, u32, 100); 6 | /// tls_init!(MAP, HashMap, HashMap::new()); 7 | /// ``` 8 | #[macro_export] 9 | macro_rules! tls_init { 10 | ($vis:vis $var:ident, $t:ty, $($var_init:tt)*) => { 11 | thread_local!{ 12 | $vis static $var: std::cell::RefCell<$t> = 13 | std::cell::RefCell::new($($var_init)*); 14 | } 15 | }; 16 | } 17 | 18 | /// Get the value of a thread local storage variable. 19 | /// 20 | /// ``` 21 | /// # use cmd_lib::*; 22 | /// // from examples/tetris.rs: 23 | /// tls_init!(screen_buffer, String, "".to_string()); 24 | /// eprint!("{}", tls_get!(screen_buffer)); 25 | /// 26 | /// tls_init!(use_color, bool, true); // true if we use color, false if not 27 | /// if tls_get!(use_color) { 28 | /// // ... 29 | /// } 30 | /// ``` 31 | #[macro_export] 32 | macro_rules! tls_get { 33 | ($var:ident) => { 34 | $var.with(|var| var.borrow().clone()) 35 | }; 36 | } 37 | 38 | /// Set the value of a thread local storage variable. 39 | /// ``` 40 | /// # use cmd_lib::*; 41 | /// # let changes = ""; 42 | /// tls_init!(screen_buffer, String, "".to_string()); 43 | /// tls_set!(screen_buffer, |s| s.push_str(changes)); 44 | /// 45 | /// tls_init!(use_color, bool, true); // true if we use color, false if not 46 | /// fn toggle_color() { 47 | /// tls_set!(use_color, |x| *x = !*x); 48 | /// // redraw_screen(); 49 | /// } 50 | /// ``` 51 | #[macro_export] 52 | macro_rules! tls_set { 53 | ($var:ident, |$v:ident| $($var_update:tt)*) => { 54 | $var.with(|$v| { 55 | let mut $v = $v.borrow_mut(); 56 | $($var_update)*; 57 | }); 58 | }; 59 | } 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | #[test] 64 | fn test_proc_var_u32() { 65 | tls_init!(LEN, u32, 100); 66 | tls_set!(LEN, |x| *x = 300); 67 | assert_eq!(tls_get!(LEN), 300); 68 | } 69 | 70 | #[test] 71 | fn test_proc_var_map() { 72 | use std::collections::HashMap; 73 | tls_init!(MAP, HashMap, HashMap::new()); 74 | tls_set!(MAP, |x| x.insert("a".to_string(), "b".to_string())); 75 | assert_eq!(tls_get!(MAP)["a"], "b".to_string()); 76 | } 77 | 78 | #[test] 79 | fn test_proc_var_vec() { 80 | tls_init!(V, Vec, vec![]); 81 | tls_set!(V, |v| v.push(100)); 82 | tls_set!(V, |v| v.push(200)); 83 | assert_eq!(tls_get!(V)[0], 100); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/io.rs: -------------------------------------------------------------------------------- 1 | use os_pipe::*; 2 | use std::fs::File; 3 | use std::io::{Read, Result, Write}; 4 | use std::process::Stdio; 5 | 6 | /// Standard input stream for custom command implementation, which is part of [`CmdEnv`](crate::CmdEnv). 7 | pub struct CmdIn(CmdInInner); 8 | 9 | impl Read for CmdIn { 10 | fn read(&mut self, buf: &mut [u8]) -> Result { 11 | match &mut self.0 { 12 | CmdInInner::Null => Ok(0), 13 | CmdInInner::File(file) => file.read(buf), 14 | CmdInInner::Pipe(pipe) => pipe.read(buf), 15 | } 16 | } 17 | } 18 | 19 | impl From for Stdio { 20 | fn from(cmd_in: CmdIn) -> Stdio { 21 | match cmd_in.0 { 22 | CmdInInner::Null => Stdio::null(), 23 | CmdInInner::File(file) => Stdio::from(file), 24 | CmdInInner::Pipe(pipe) => Stdio::from(pipe), 25 | } 26 | } 27 | } 28 | 29 | impl CmdIn { 30 | pub(crate) fn null() -> Self { 31 | Self(CmdInInner::Null) 32 | } 33 | 34 | pub(crate) fn file(f: File) -> Self { 35 | Self(CmdInInner::File(f)) 36 | } 37 | 38 | pub(crate) fn pipe(p: PipeReader) -> Self { 39 | Self(CmdInInner::Pipe(p)) 40 | } 41 | 42 | pub fn try_clone(&self) -> Result { 43 | match &self.0 { 44 | CmdInInner::Null => Ok(Self(CmdInInner::Null)), 45 | CmdInInner::File(file) => file.try_clone().map(|f| Self(CmdInInner::File(f))), 46 | CmdInInner::Pipe(pipe) => pipe.try_clone().map(|p| Self(CmdInInner::Pipe(p))), 47 | } 48 | } 49 | } 50 | 51 | enum CmdInInner { 52 | Null, 53 | File(File), 54 | Pipe(PipeReader), 55 | } 56 | 57 | /// Standard output stream for custom command implementation, which is part of [`CmdEnv`](crate::CmdEnv). 58 | pub struct CmdOut(CmdOutInner); 59 | 60 | impl Write for CmdOut { 61 | fn write(&mut self, buf: &[u8]) -> Result { 62 | match &mut self.0 { 63 | CmdOutInner::Null => Ok(buf.len()), 64 | CmdOutInner::File(file) => file.write(buf), 65 | CmdOutInner::Pipe(pipe) => pipe.write(buf), 66 | } 67 | } 68 | 69 | fn flush(&mut self) -> Result<()> { 70 | match &mut self.0 { 71 | CmdOutInner::Null => Ok(()), 72 | CmdOutInner::File(file) => file.flush(), 73 | CmdOutInner::Pipe(pipe) => pipe.flush(), 74 | } 75 | } 76 | } 77 | 78 | impl CmdOut { 79 | pub(crate) fn null() -> Self { 80 | Self(CmdOutInner::Null) 81 | } 82 | 83 | pub(crate) fn file(f: File) -> Self { 84 | Self(CmdOutInner::File(f)) 85 | } 86 | 87 | pub(crate) fn pipe(p: PipeWriter) -> Self { 88 | Self(CmdOutInner::Pipe(p)) 89 | } 90 | 91 | pub fn try_clone(&self) -> Result { 92 | match &self.0 { 93 | CmdOutInner::Null => Ok(Self(CmdOutInner::Null)), 94 | CmdOutInner::File(file) => file.try_clone().map(|f| Self(CmdOutInner::File(f))), 95 | CmdOutInner::Pipe(pipe) => pipe.try_clone().map(|p| Self(CmdOutInner::Pipe(p))), 96 | } 97 | } 98 | } 99 | 100 | impl From for Stdio { 101 | fn from(cmd_out: CmdOut) -> Stdio { 102 | match cmd_out.0 { 103 | CmdOutInner::Null => Stdio::null(), 104 | CmdOutInner::File(file) => Stdio::from(file), 105 | CmdOutInner::Pipe(pipe) => Stdio::from(pipe), 106 | } 107 | } 108 | } 109 | 110 | enum CmdOutInner { 111 | Null, 112 | File(File), 113 | Pipe(PipeWriter), 114 | } 115 | -------------------------------------------------------------------------------- /macros/src/parser.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use std::iter::Peekable; 4 | 5 | #[derive(Debug)] 6 | pub enum ParseArg { 7 | Pipe, 8 | Semicolon, 9 | RedirectFd(i32, i32), // fd1, fd2 10 | RedirectFile(i32, TokenStream, bool), // fd1, file, append? 11 | ArgStr(TokenStream), 12 | ArgVec(TokenStream), 13 | } 14 | 15 | pub struct Parser> { 16 | iter: Peekable, 17 | } 18 | 19 | impl> Parser { 20 | pub fn from(iter: Peekable) -> Self { 21 | Self { iter } 22 | } 23 | 24 | pub fn parse(mut self, for_spawn: bool) -> TokenStream { 25 | let mut ret = quote!(::cmd_lib::GroupCmds::default()); 26 | while self.iter.peek().is_some() { 27 | let cmd = self.parse_cmd(); 28 | if !cmd.is_empty() { 29 | ret.extend(quote!(.append(#cmd))); 30 | assert!( 31 | !(for_spawn && self.iter.peek().is_some()), 32 | "wrong spawning format: group command not allowed" 33 | ); 34 | } 35 | } 36 | ret 37 | } 38 | 39 | fn parse_cmd(&mut self) -> TokenStream { 40 | let mut cmds = quote!(::cmd_lib::Cmds::default()); 41 | while self.iter.peek().is_some() { 42 | let cmd = self.parse_pipe(); 43 | cmds.extend(quote!(.pipe(#cmd))); 44 | if !matches!(self.iter.peek(), Some(ParseArg::Pipe)) { 45 | self.iter.next(); 46 | break; 47 | } 48 | self.iter.next(); 49 | } 50 | cmds 51 | } 52 | 53 | fn parse_pipe(&mut self) -> TokenStream { 54 | // TODO: get accurate line number once `proc_macro::Span::line()` API is stable 55 | let mut ret = quote!(::cmd_lib::Cmd::default().with_location(file!(), line!())); 56 | while let Some(arg) = self.iter.peek() { 57 | match arg { 58 | ParseArg::RedirectFd(fd1, fd2) => { 59 | if fd1 != fd2 { 60 | let mut redirect = quote!(::cmd_lib::Redirect); 61 | match (fd1, fd2) { 62 | (1, 2) => redirect.extend(quote!(::StdoutToStderr)), 63 | (2, 1) => redirect.extend(quote!(::StderrToStdout)), 64 | _ => panic!("unsupported fd numbers: {} {}", fd1, fd2), 65 | } 66 | ret.extend(quote!(.add_redirect(#redirect))); 67 | } 68 | } 69 | ParseArg::RedirectFile(fd1, file, append) => { 70 | let mut redirect = quote!(::cmd_lib::Redirect); 71 | match fd1 { 72 | 0 => redirect.extend(quote!(::FileToStdin(#file.into_path_buf()))), 73 | 1 => { 74 | redirect.extend(quote!(::StdoutToFile(#file.into_path_buf(), #append))) 75 | } 76 | 2 => { 77 | redirect.extend(quote!(::StderrToFile(#file.into_path_buf(), #append))) 78 | } 79 | _ => panic!("unsupported fd ({}) redirect to file {}", fd1, file), 80 | } 81 | ret.extend(quote!(.add_redirect(#redirect))); 82 | } 83 | ParseArg::ArgStr(opt) => { 84 | ret.extend(quote!(.add_arg(#opt))); 85 | } 86 | ParseArg::ArgVec(opts) => { 87 | ret.extend(quote! (.add_args(#opts))); 88 | } 89 | ParseArg::Pipe | ParseArg::Semicolon => break, 90 | } 91 | self.iter.next(); 92 | } 93 | ret 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! cmd_lib_macros - Procedural macros for cmd_lib 2 | //! 3 | //! ## Invalid syntax examples that should fail to compile 4 | //! 5 | //! This section contains documentation tests that demonstrate invalid macro syntax 6 | //! which should result in compilation errors. These serve as tests to ensure 7 | //! the macros properly reject invalid input. 8 | //! 9 | //! ### Invalid variable expansion syntax 10 | //! 11 | //! Variable names cannot start with numbers: 12 | //! ```compile_fail 13 | //! # use cmd_lib::*; 14 | //! run_cmd!(echo "${msg0}"); 15 | //! ``` 16 | //! 17 | //! Invalid spacing in variable expansion: 18 | //! ```compile_fail 19 | //! # use cmd_lib::*; 20 | //! run_fun!(echo "${ msg }"); 21 | //! ``` 22 | //! 23 | //! Unclosed variable expansion: 24 | //! ```compile_fail 25 | //! # use cmd_lib::*; 26 | //! run_fun!(echo "${"); 27 | //! ``` 28 | //! 29 | //! Unclosed variable name: 30 | //! ```compile_fail 31 | //! # use cmd_lib::*; 32 | //! run_fun!(echo "${msg"); 33 | //! ``` 34 | //! 35 | //! Variable names cannot be numbers: 36 | //! ```compile_fail 37 | //! # use cmd_lib::*; 38 | //! run_fun!(echo "${0}"); 39 | //! ``` 40 | //! 41 | //! Variable names cannot have spaces: 42 | //! ```compile_fail 43 | //! # use cmd_lib::*; 44 | //! run_fun!(echo "${ 0 }"); 45 | //! ``` 46 | //! 47 | //! Variable names cannot start with numbers: 48 | //! ```compile_fail 49 | //! # use cmd_lib::*; 50 | //! run_fun!(echo "${0msg}"); 51 | //! ``` 52 | //! 53 | //! Variable names cannot contain spaces: 54 | //! ```compile_fail 55 | //! # use cmd_lib::*; 56 | //! run_fun!(echo "${msg 0}"); 57 | //! ``` 58 | //! 59 | //! ### Invalid vector variable syntax 60 | //! 61 | //! Vector variables cannot have text immediately before them: 62 | //! ```compile_fail 63 | //! # use cmd_lib::*; 64 | //! let opts = ["-a", "-f"]; 65 | //! run_cmd!(ls xxx$[opts]); 66 | //! ``` 67 | //! 68 | //! ```compile_fail 69 | //! # use cmd_lib::*; 70 | //! let msg = "hello"; 71 | //! let opts = ["-a", "-f"]; 72 | //! run_cmd!(ls $msg$[opts]); 73 | //! ``` 74 | //! 75 | //! ```compile_fail 76 | //! # use cmd_lib::*; 77 | //! let opts = ["-a", "-f"]; 78 | //! run_cmd!(echo$[opts]); 79 | //! ``` 80 | //! 81 | //! ### Invalid redirect syntax 82 | //! 83 | //! Invalid redirect operator spacing: 84 | //! ```compile_fail 85 | //! # use cmd_lib::*; 86 | //! run_cmd!(ls > >&1); 87 | //! ``` 88 | //! 89 | //! Invalid redirect to stdout: 90 | //! ```compile_fail 91 | //! # use cmd_lib::*; 92 | //! run_cmd!(ls >>&1); 93 | //! ``` 94 | //! 95 | //! Invalid redirect to stderr: 96 | //! ```compile_fail 97 | //! # use cmd_lib::*; 98 | //! run_cmd!(ls >>&2); 99 | //! ``` 100 | //! 101 | //! ### Double redirect errors 102 | //! 103 | //! Triple redirect operator: 104 | //! ```compile_fail 105 | //! # use cmd_lib::*; 106 | //! run_cmd!(ls / /x &>>> /tmp/f); 107 | //! ``` 108 | //! 109 | //! Double redirect with space: 110 | //! ```compile_fail 111 | //! # use cmd_lib::*; 112 | //! run_cmd!(ls / /x &> > /tmp/f); 113 | //! ``` 114 | //! 115 | //! Double output redirect: 116 | //! ```compile_fail 117 | //! # use cmd_lib::*; 118 | //! run_cmd!(ls / /x > > /tmp/f); 119 | //! ``` 120 | //! 121 | //! Append and output redirect: 122 | //! ```compile_fail 123 | //! # use cmd_lib::*; 124 | //! run_cmd!(ls / /x >> > /tmp/f); 125 | //! ``` 126 | 127 | use proc_macro_error2::{abort, proc_macro_error}; 128 | use proc_macro2::{TokenStream, TokenTree}; 129 | use quote::quote; 130 | 131 | /// Mark main function to log error result by default. 132 | /// 133 | /// ```no_run 134 | /// # use cmd_lib::*; 135 | /// 136 | /// #[cmd_lib::main] 137 | /// fn main() -> CmdResult { 138 | /// run_cmd!(bad_cmd)?; 139 | /// Ok(()) 140 | /// } 141 | /// // output: 142 | /// // [ERROR] FATAL: Running ["bad_cmd"] failed: No such file or directory (os error 2) 143 | /// ``` 144 | #[proc_macro_attribute] 145 | pub fn main( 146 | _args: proc_macro::TokenStream, 147 | item: proc_macro::TokenStream, 148 | ) -> proc_macro::TokenStream { 149 | let orig_function: syn::ItemFn = syn::parse2(item.into()).unwrap(); 150 | let orig_main_return_type = orig_function.sig.output; 151 | let orig_main_block = orig_function.block; 152 | 153 | quote! ( 154 | fn main() { 155 | fn cmd_lib_main() #orig_main_return_type { 156 | #orig_main_block 157 | } 158 | 159 | cmd_lib_main().unwrap_or_else(|err| { 160 | ::cmd_lib::error!("FATAL: {err}"); 161 | std::process::exit(1); 162 | }); 163 | } 164 | 165 | ) 166 | .into() 167 | } 168 | 169 | /// Import user registered custom command. 170 | /// ```no_run 171 | /// # use cmd_lib::*; 172 | /// # use std::io::Write; 173 | /// fn my_cmd(env: &mut CmdEnv) -> CmdResult { 174 | /// let msg = format!("msg from foo(), args: {:?}", env.get_args()); 175 | /// writeln!(env.stderr(), "{msg}")?; 176 | /// writeln!(env.stdout(), "bar") 177 | /// } 178 | /// 179 | /// use_custom_cmd!(my_cmd); 180 | /// run_cmd!(my_cmd)?; 181 | /// # Ok::<(), std::io::Error>(()) 182 | /// ``` 183 | /// Here we import the previous defined `my_cmd` command, so we can run it like a normal command. 184 | #[proc_macro] 185 | #[proc_macro_error] 186 | pub fn use_custom_cmd(item: proc_macro::TokenStream) -> proc_macro::TokenStream { 187 | let item: proc_macro2::TokenStream = item.into(); 188 | let mut cmd_fns = vec![]; 189 | for t in item { 190 | if let TokenTree::Punct(ref ch) = t { 191 | if ch.as_char() != ',' { 192 | abort!(t, "only comma is allowed"); 193 | } 194 | } else if let TokenTree::Ident(cmd) = t { 195 | let cmd_name = cmd.to_string(); 196 | cmd_fns.push(quote!(&#cmd_name, #cmd)); 197 | } else { 198 | abort!(t, "expect a list of comma separated commands"); 199 | } 200 | } 201 | 202 | quote! ( 203 | #(::cmd_lib::register_cmd(#cmd_fns);)* 204 | ) 205 | .into() 206 | } 207 | 208 | /// Run commands, returning [`CmdResult`](../cmd_lib/type.CmdResult.html) to check status. 209 | /// ```no_run 210 | /// # use cmd_lib::run_cmd; 211 | /// let msg = "I love rust"; 212 | /// run_cmd!(echo $msg)?; 213 | /// run_cmd!(echo "This is the message: $msg")?; 214 | /// 215 | /// // pipe commands are also supported 216 | /// run_cmd!(du -ah . | sort -hr | head -n 10)?; 217 | /// 218 | /// // or a group of commands 219 | /// // if any command fails, just return Err(...) 220 | /// let file = "/tmp/f"; 221 | /// let keyword = "rust"; 222 | /// if run_cmd! { 223 | /// cat ${file} | grep ${keyword}; 224 | /// echo "bad cmd" >&2; 225 | /// ignore ls /nofile; 226 | /// date; 227 | /// ls oops; 228 | /// cat oops; 229 | /// }.is_err() { 230 | /// // your error handling code 231 | /// } 232 | /// # Ok::<(), std::io::Error>(()) 233 | /// ``` 234 | #[proc_macro] 235 | #[proc_macro_error] 236 | pub fn run_cmd(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 237 | let cmds = lexer::Lexer::new(input.into()).scan().parse(false); 238 | quote! ({ 239 | use ::cmd_lib::AsOsStr; 240 | #cmds.run_cmd() 241 | }) 242 | .into() 243 | } 244 | 245 | /// Run commands, returning [`FunResult`](../cmd_lib/type.FunResult.html) to capture output and to check status. 246 | /// ```no_run 247 | /// # use cmd_lib::run_fun; 248 | /// let version = run_fun!(rustc --version)?; 249 | /// println!("Your rust version is {}", version); 250 | /// 251 | /// // with pipes 252 | /// let n = run_fun!(echo "the quick brown fox jumped over the lazy dog" | wc -w)?; 253 | /// println!("There are {} words in above sentence", n); 254 | /// # Ok::<(), std::io::Error>(()) 255 | /// ``` 256 | #[proc_macro] 257 | #[proc_macro_error] 258 | pub fn run_fun(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 259 | let cmds = lexer::Lexer::new(input.into()).scan().parse(false); 260 | quote! ({ 261 | use ::cmd_lib::AsOsStr; 262 | #cmds.run_fun() 263 | }) 264 | .into() 265 | } 266 | 267 | /// Run commands with/without pipes as a child process, returning [`CmdChildren`](../cmd_lib/struct.CmdChildren.html) result. 268 | /// ```no_run 269 | /// # use cmd_lib::*; 270 | /// 271 | /// let mut handle = spawn!(ping -c 10 192.168.0.1)?; 272 | /// // ... 273 | /// if handle.wait().is_err() { 274 | /// // ... 275 | /// } 276 | /// # Ok::<(), std::io::Error>(()) 277 | #[proc_macro] 278 | #[proc_macro_error] 279 | pub fn spawn(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 280 | let cmds = lexer::Lexer::new(input.into()).scan().parse(true); 281 | quote! ({ 282 | use ::cmd_lib::AsOsStr; 283 | #cmds.spawn(false) 284 | }) 285 | .into() 286 | } 287 | 288 | /// Run commands with/without pipes as a child process, returning [`FunChildren`](../cmd_lib/struct.FunChildren.html) result. 289 | /// ```no_run 290 | /// # use cmd_lib::*; 291 | /// let mut procs = vec![]; 292 | /// for _ in 0..4 { 293 | /// let proc = spawn_with_output!( 294 | /// sudo bash -c "dd if=/dev/nvmen0 of=/dev/null bs=4096 skip=0 count=1024 2>&1" 295 | /// | awk r#"/copied/{print $(NF-1) " " $NF}"# 296 | /// )?; 297 | /// procs.push(proc); 298 | /// } 299 | /// 300 | /// for (i, mut proc) in procs.into_iter().enumerate() { 301 | /// let bandwidth = proc.wait_with_output()?; 302 | /// info!("thread {i} bandwidth: {bandwidth} MB/s"); 303 | /// } 304 | /// # Ok::<(), std::io::Error>(()) 305 | /// ``` 306 | #[proc_macro] 307 | #[proc_macro_error] 308 | pub fn spawn_with_output(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 309 | let cmds = lexer::Lexer::new(input.into()).scan().parse(true); 310 | quote! ({ 311 | use ::cmd_lib::AsOsStr; 312 | #cmds.spawn_with_output() 313 | }) 314 | .into() 315 | } 316 | 317 | #[proc_macro] 318 | #[proc_macro_error] 319 | /// Log a fatal message at the error level, and exit process. 320 | /// 321 | /// e.g: 322 | /// ```no_run 323 | /// # use cmd_lib::*; 324 | /// let file = "bad_file"; 325 | /// cmd_die!("could not open file: $file"); 326 | /// // output: 327 | /// // [ERROR] FATAL: could not open file: bad_file 328 | /// ``` 329 | /// format should be string literals, and variable interpolation is supported. 330 | /// Note that this macro is just for convenience. The process will exit with 1 and print 331 | /// "FATAL: ..." messages to error console. If you want to exit with other code, you 332 | /// should probably define your own macro or functions. 333 | pub fn cmd_die(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 334 | let msg = parse_msg(input.into()); 335 | quote!({ 336 | ::cmd_lib::error!("FATAL: {} at {}:{}", #msg, file!(), line!()); 337 | std::process::exit(1) 338 | }) 339 | .into() 340 | } 341 | 342 | fn parse_msg(input: TokenStream) -> TokenStream { 343 | let mut iter = input.into_iter(); 344 | let mut output = TokenStream::new(); 345 | let mut valid = false; 346 | if let Some(ref tt) = iter.next() { 347 | if let TokenTree::Literal(lit) = tt { 348 | let s = lit.to_string(); 349 | if s.starts_with('\"') || s.starts_with('r') { 350 | let str_lit = lexer::scan_str_lit(lit); 351 | output.extend(quote!(#str_lit)); 352 | valid = true; 353 | } 354 | } 355 | if !valid { 356 | abort!(tt, "invalid format: expect string literal"); 357 | } 358 | if let Some(tt) = iter.next() { 359 | abort!( 360 | tt, 361 | "expect string literal only, found extra {}", 362 | tt.to_string() 363 | ); 364 | } 365 | } 366 | output 367 | } 368 | 369 | mod lexer; 370 | mod parser; 371 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /examples/pipes.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # pipes.sh: Animated pipes terminal screensaver. 3 | # from https://github.com/pipeseroni/pipes.sh 4 | # @05373c6a93b36fa45937ef4e11f4f917fdd122c0 5 | # 6 | # Copyright (c) 2015-2018 Pipeseroni/pipes.sh contributors 7 | # Copyright (c) 2013-2015 Yu-Jie Lin 8 | # Copyright (c) 2010 Matthew Simpson 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | 28 | 29 | VERSION=1.3.0 30 | 31 | M=32768 # Bash RANDOM maximum + 1 32 | p=1 # number of pipes 33 | f=75 # frame rate 34 | s=13 # probability of straight fitting 35 | r=2000 # characters limit 36 | t=0 # iteration counter for -r character limit 37 | w=80 # terminal size 38 | h=24 39 | 40 | # ab -> sets[][idx] = a*4 + b 41 | # 0: up, 1: right, 2: down, 3: left 42 | # 00 means going up , then going up -> ┃ 43 | # 12 means going right, then going down -> ┓ 44 | sets=( 45 | "┃┏ ┓┛━┓ ┗┃┛┗ ┏━" 46 | "│╭ ╮╯─╮ ╰│╯╰ ╭─" 47 | "│┌ ┐┘─┐ └│┘└ ┌─" 48 | "║╔ ╗╝═╗ ╚║╝╚ ╔═" 49 | "|+ ++-+ +|++ +-" 50 | "|/ \/-\ \|/\ /-" 51 | ".. .... .... .." 52 | ".o oo.o o.oo o." 53 | "-\ /\|/ /-\/ \|" # railway 54 | "╿┍ ┑┚╼┒ ┕╽┙┖ ┎╾" # knobby pipe 55 | ) 56 | SETS=() # rearranged all pipe chars into individul elements for easier access 57 | 58 | # pipes' 59 | x=() # current position 60 | y=() 61 | l=() # current directions 62 | # 0: up, 1: right, 2: down, 3: left 63 | n=() # new directions 64 | v=() # current types 65 | c=() # current escape codes 66 | 67 | # selected pipes' 68 | V=() # types (indexes to sets[]) 69 | C=() # color indices for tput setaf 70 | VN=0 # number of selected types 71 | CN=0 # number of selected colors 72 | E=() # pre-generated escape codes from BOLD, NOCOLOR, and C 73 | 74 | # switches 75 | RNDSTART=0 # randomize starting position and direction 76 | BOLD=1 77 | NOCOLOR=0 78 | KEEPCT=0 # keep pipe color and type 79 | 80 | 81 | # print help message in 72-char width 82 | print_help() { 83 | local cgap 84 | printf -v cgap '%*s' $((15 - ${#COLORS})) '' 85 | cat <= 0 114 | is_N() { 115 | [[ -n $1 && -z ${1//[0-9]} ]] 116 | } 117 | 118 | 119 | # test if $1 is a hexadecimal string 120 | is_hex() { 121 | [[ -n $1 && -z ${1//[0-9A-Fa-f]} ]] 122 | } 123 | 124 | 125 | # print error message for invalid argument to standard error, this 126 | # - mimics getopts error message 127 | # - use all positional parameters as error message 128 | # - has a newline appended 129 | # $arg and $OPTARG are the option name and argument set by getopts. 130 | pearg() { 131 | printf "%s: -$arg invalid argument -- $OPTARG; %s\n" "$0" "$*" >&2 132 | } 133 | 134 | 135 | OPTIND=1 136 | while getopts "p:t:c:f:s:r:RBCKhv" arg; do 137 | case $arg in 138 | p) 139 | if is_N "$OPTARG" && ((OPTARG > 0)); then 140 | p=$OPTARG 141 | else 142 | pearg 'must be an integer and greater than 0' 143 | return 1 144 | fi 145 | ;; 146 | t) 147 | if [[ "$OPTARG" = c???????????????? ]]; then 148 | V+=(${#sets[@]}) 149 | sets+=("${OPTARG:1}") 150 | elif is_N "$OPTARG" && ((OPTARG < ${#sets[@]})); then 151 | V+=($OPTARG) 152 | else 153 | pearg 'must be an integer and from 0 to' \ 154 | "$((${#sets[@]} - 1)); or a custom type" 155 | return 1 156 | fi 157 | ;; 158 | c) 159 | if [[ $OPTARG == '#'* ]]; then 160 | if ! is_hex "${OPTARG:1}"; then 161 | pearg 'unrecognized hexadecimal string' 162 | return 1 163 | fi 164 | if ((16$OPTARG >= COLORS)); then 165 | pearg 'hexadecimal must be from #0 to' \ 166 | "#$(printf '%X' $((COLORS - 1)))" 167 | return 1 168 | fi 169 | C+=($((16$OPTARG))) 170 | elif is_N "$OPTARG" && ((OPTARG < COLORS)); then 171 | C+=($OPTARG) 172 | else 173 | pearg "must be an integer and from 0 to $((COLORS - 1));" \ 174 | 'or a hexadecimal string with # prefix' 175 | return 1 176 | fi 177 | ;; 178 | f) 179 | if is_N "$OPTARG" && ((OPTARG >= 20 && OPTARG <= 100)); then 180 | f=$OPTARG 181 | else 182 | pearg 'must be an integer and from 20 to 100' 183 | return 1 184 | fi 185 | ;; 186 | s) 187 | if is_N "$OPTARG" && ((OPTARG >= 5 && OPTARG <= 15)); then 188 | s=$OPTARG 189 | else 190 | pearg 'must be an integer and from 5 to 15' 191 | return 1 192 | fi 193 | ;; 194 | r) 195 | if is_N "$OPTARG"; then 196 | r=$OPTARG 197 | else 198 | pearg 'must be a non-negative integer' 199 | return 1 200 | fi 201 | ;; 202 | R) RNDSTART=1;; 203 | B) BOLD=0;; 204 | C) NOCOLOR=1;; 205 | K) KEEPCT=1;; 206 | h) 207 | print_help 208 | exit 0 209 | ;; 210 | v) echo "$(basename -- "$0") $VERSION" 211 | exit 0 212 | ;; 213 | *) 214 | return 1 215 | esac 216 | done 217 | 218 | shift $((OPTIND - 1)) 219 | if (($#)); then 220 | printf "$0: illegal arguments -- $*; no arguments allowed\n" >&2 221 | return 1 222 | fi 223 | } 224 | 225 | 226 | cleanup() { 227 | # clear out standard input 228 | read -t 0.001 && cat /dev/null 229 | 230 | tput reset # fix for konsole, see pipeseroni/pipes.sh#43 231 | tput rmcup 232 | tput cnorm 233 | stty echo 234 | printf "$SGR0" 235 | exit 0 236 | } 237 | 238 | 239 | resize() { 240 | w=$(tput cols) h=$(tput lines) 241 | } 242 | 243 | 244 | init_pipes() { 245 | # +_CP_init_pipes 246 | local i 247 | 248 | ci=$((KEEPCT ? 0 : CN * RANDOM / M)) 249 | vi=$((KEEPCT ? 0 : VN * RANDOM / M)) 250 | for ((i = 0; i < p; i++)); do 251 | (( 252 | n[i] = 0, 253 | l[i] = RNDSTART ? RANDOM % 4 : 0, 254 | x[i] = RNDSTART ? w * RANDOM / M : w / 2, 255 | y[i] = RNDSTART ? h * RANDOM / M : h / 2, 256 | v[i] = V[vi] 257 | )) 258 | c[i]=${E[ci]} 259 | ((ci = (ci + 1) % CN, vi = (vi + 1) % VN)) 260 | done 261 | # -_CP_init_pipes 262 | } 263 | 264 | 265 | init_screen() { 266 | stty -echo 267 | tput smcup 268 | tput civis 269 | tput clear 270 | trap cleanup HUP TERM 271 | 272 | resize 273 | trap resize SIGWINCH 274 | } 275 | 276 | 277 | main() { 278 | # simple pre-check of TERM, tput's error message should be enough 279 | tput -T "$TERM" sgr0 >/dev/null || return $? 280 | 281 | # +_CP_init_termcap_vars 282 | COLORS=$(tput colors) # COLORS - 1 == maximum color index for -c argument 283 | SGR0=$(tput sgr0) 284 | SGR_BOLD=$(tput bold) 285 | # -_CP_init_termcap_vars 286 | 287 | parse "$@" || return $? 288 | 289 | # +_CP_init_VC 290 | # set default values if not by options 291 | ((${#V[@]})) || V=(0) 292 | VN=${#V[@]} 293 | ((${#C[@]})) || C=(1 2 3 4 5 6 7 0) 294 | CN=${#C[@]} 295 | # -_CP_init_VC 296 | 297 | # +_CP_init_E 298 | # generate E[] based on BOLD (SGR_BOLD), NOCOLOR, and C for each element in 299 | # C, a corresponding element in E[] = 300 | # SGR0 301 | # + SGR_BOLD, if BOLD 302 | # + tput setaf C, if !NOCOLOR 303 | local i 304 | for ((i = 0; i < CN; i++)) { 305 | E[i]=$SGR0 306 | ((BOLD)) && E[i]+=$SGR_BOLD 307 | ((NOCOLOR)) || E[i]+=$(tput setaf ${C[i]}) 308 | } 309 | # -_CP_init_E 310 | 311 | # +_CP_init_SETS 312 | local i j 313 | for ((i = 0; i < ${#sets[@]}; i++)) { 314 | for ((j = 0; j < 16; j++)) { 315 | SETS+=("${sets[i]:j:1}") 316 | } 317 | } 318 | unset i j 319 | # -_CP_init_SETS 320 | 321 | init_screen 322 | init_pipes 323 | 324 | # any key press exits the loop and this script 325 | trap 'break 2' INT 326 | 327 | local i 328 | while REPLY=; do 329 | read -t 0.0$((1000 / f)) -n 1 2>/dev/null 330 | case "$REPLY" in 331 | P) ((s = s < 15 ? s + 1 : s));; 332 | O) ((s = s > 3 ? s - 1 : s));; 333 | F) ((f = f < 100 ? f + 1 : f));; 334 | D) ((f = f > 20 ? f - 1 : f));; 335 | B) ((BOLD = (BOLD + 1) % 2));; 336 | C) ((NOCOLOR = (NOCOLOR + 1) % 2));; 337 | K) ((KEEPCT = (KEEPCT + 1) % 2));; 338 | ?) break;; 339 | esac 340 | for ((i = 0; i < p; i++)); do 341 | # New position: 342 | # l[] direction = 0: up, 1: right, 2: down, 3: left 343 | # +_CP_newpos 344 | ((l[i] % 2)) && ((x[i] += -l[i] + 2, 1)) || ((y[i] += l[i] - 1)) 345 | # -_CP_newpos 346 | 347 | # Loop on edges (change color on loop): 348 | # +_CP_warp 349 | ((!KEEPCT && (x[i] >= w || x[i] < 0 || y[i] >= h || y[i] < 0))) \ 350 | && { c[i]=${E[CN * RANDOM / M]}; ((v[i] = V[VN * RANDOM / M])); } 351 | ((x[i] = (x[i] + w) % w, 352 | y[i] = (y[i] + h) % h)) 353 | # -_CP_warp 354 | 355 | # new turning direction: 356 | # $((s - 1)) in $s, going straight, therefore n[i] == l[i]; 357 | # and 1 in $s that pipe makes a right or left turn 358 | # 359 | # s * RANDOM / M - 1 == 0 360 | # n[i] == -1 361 | # => n[i] == l[i] + 1 or l[i] - 1 362 | # +_CP_newdir 363 | (( 364 | n[i] = s * RANDOM / M - 1, 365 | n[i] = n[i] >= 0 ? l[i] : l[i] + (2 * (RANDOM % 2) - 1), 366 | n[i] = (n[i] + 4) % 4 367 | )) 368 | # -_CP_newdir 369 | 370 | # Print: 371 | # +_CP_print 372 | printf '\e[%d;%dH%s%s' \ 373 | $((y[i] + 1)) $((x[i] + 1)) ${c[i]} \ 374 | "${SETS[v[i] * 16 + l[i] * 4 + n[i]]}" 375 | # -_CP_print 376 | l[i]=${n[i]} 377 | done 378 | ((r > 0 && t * p >= r)) && tput reset && tput civis && t=0 || ((t++)) 379 | done 380 | 381 | cleanup 382 | } 383 | 384 | 385 | # when being sourced, $0 == bash, only invoke main when they are the same 386 | [[ "$0" != "$BASH_SOURCE" ]] || main "$@" 387 | -------------------------------------------------------------------------------- /tests/test_macros.rs: -------------------------------------------------------------------------------- 1 | use cmd_lib::*; 2 | 3 | #[test] 4 | #[rustfmt::skip] 5 | fn test_run_single_cmds() { 6 | assert!(run_cmd!(touch /tmp/xxf).is_ok()); 7 | assert!(run_cmd!(rm /tmp/xxf).is_ok()); 8 | } 9 | 10 | #[test] 11 | fn test_run_single_cmd_with_quote() { 12 | assert_eq!( 13 | run_fun!(echo "hello, rust" | sed r"s/rust/cmd_lib1/g").unwrap(), 14 | "hello, cmd_lib1" 15 | ); 16 | } 17 | 18 | #[test] 19 | fn test_cd_fails() { 20 | assert!( 21 | run_cmd! { 22 | cd /bad_dir; 23 | ls | wc -l; 24 | } 25 | .is_err() 26 | ); 27 | } 28 | 29 | #[test] 30 | fn test_run_cmds() { 31 | assert!( 32 | run_cmd! { 33 | cd /tmp; 34 | touch xxff; 35 | ls | wc -l; 36 | rm xxff; 37 | } 38 | .is_ok() 39 | ); 40 | } 41 | 42 | #[test] 43 | fn test_run_fun() { 44 | assert!(run_fun!(uptime).is_ok()); 45 | } 46 | 47 | #[test] 48 | fn test_args_passing() { 49 | let dir: &str = "folder"; 50 | assert!(run_cmd!(rm -rf /tmp/$dir).is_ok()); 51 | assert!(run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir).is_ok()); 52 | assert!(run_cmd!(mkdir /tmp/"$dir"; ls /tmp/"$dir"; rmdir /tmp/"$dir").is_err()); 53 | assert!(run_cmd!(mkdir "/tmp/$dir"; ls "/tmp/$dir"; rmdir "/tmp/$dir").is_err()); 54 | assert!(run_cmd!(rmdir "/tmp/$dir").is_ok()); 55 | } 56 | 57 | #[test] 58 | fn test_args_with_spaces() { 59 | let dir: &str = "folder with spaces"; 60 | assert!(run_cmd!(rm -rf /tmp/$dir).is_ok()); 61 | assert!(run_cmd!(mkdir /tmp/"$dir"; ls /tmp/"$dir"; rmdir /tmp/"$dir").is_ok()); 62 | assert!(run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir).is_ok()); 63 | assert!(run_cmd!(mkdir /tmp/"$dir"; ls /tmp/"$dir"; rmdir /tmp/"$dir").is_err()); 64 | assert!(run_cmd!(mkdir "/tmp/$dir"; ls "/tmp/$dir"; rmdir "/tmp/$dir").is_err()); 65 | assert!(run_cmd!(rmdir "/tmp/$dir").is_ok()); 66 | } 67 | 68 | #[test] 69 | fn test_args_with_spaces_check_result() { 70 | let dir: &str = "folder with spaces2"; 71 | assert!(run_cmd!(rm -rf /tmp/$dir).is_ok()); 72 | assert!(run_cmd!(mkdir /tmp/$dir).is_ok()); 73 | assert!(run_cmd!(ls "/tmp/folder with spaces2").is_ok()); 74 | assert!(run_cmd!(rmdir /tmp/$dir).is_ok()); 75 | } 76 | 77 | #[test] 78 | fn test_non_string_args() { 79 | let a = 1; 80 | assert!(run_cmd!(sleep $a).is_ok()); 81 | } 82 | 83 | #[test] 84 | fn test_non_eng_args() { 85 | let msg = "你好!"; 86 | assert!(run_cmd!(echo "$msg").is_ok()); 87 | assert!(run_cmd!(echo $msg).is_ok()); 88 | assert!(run_cmd!(echo ${msg}).is_ok()); 89 | } 90 | 91 | #[test] 92 | fn test_vars_in_str0() { 93 | assert_eq!(run_fun!(echo "$").unwrap(), "$"); 94 | } 95 | 96 | #[test] 97 | fn test_vars_in_str1() { 98 | assert_eq!(run_fun!(echo "$$").unwrap(), "$"); 99 | assert_eq!(run_fun!(echo "$$a").unwrap(), "$a"); 100 | } 101 | 102 | #[test] 103 | fn test_vars_in_str2() { 104 | assert_eq!(run_fun!(echo "$ hello").unwrap(), "$ hello"); 105 | } 106 | 107 | #[test] 108 | fn test_vars_in_str3() { 109 | let msg = "hello"; 110 | assert_eq!(run_fun!(echo "$msg").unwrap(), "hello"); 111 | assert_eq!(run_fun!(echo "$ msg").unwrap(), "$ msg"); 112 | } 113 | 114 | #[test] 115 | fn test_tls_set() { 116 | tls_init!(V, Vec, vec![]); 117 | tls_set!(V, |v| v.push("a".to_string())); 118 | tls_set!(V, |v| v.push("b".to_string())); 119 | assert_eq!(tls_get!(V)[0], "a"); 120 | } 121 | 122 | #[test] 123 | fn test_pipe() { 124 | assert!(run_cmd!(echo "xx").is_ok()); 125 | assert_eq!(run_fun!(echo "xx").unwrap(), "xx"); 126 | assert!(run_cmd!(echo xx | wc).is_ok()); 127 | assert!(run_cmd!(echo xx | wc | wc | wc | wc).is_ok()); 128 | assert!(run_cmd!(seq 1 10000000 | head -1).is_err()); 129 | 130 | assert!(run_cmd!(false | wc).is_err()); 131 | assert!(run_cmd!(echo xx | false | wc | wc | wc).is_err()); 132 | 133 | let _pipefail = ScopedPipefail::set(false); 134 | assert!(run_cmd!(du -ah . | sort -hr | head -n 10).is_ok()); 135 | let _pipefail = ScopedPipefail::set(true); 136 | 137 | let wc_cmd = "wc"; 138 | assert!(run_cmd!(ls | $wc_cmd).is_ok()); 139 | } 140 | 141 | #[test] 142 | fn test_ignore_and_pipefail() { 143 | struct TestCase { 144 | /// Run the test case, returning whether the result `.is_ok()`. 145 | code: fn() -> bool, 146 | /// Stringified version of `code`, for identifying assertion failures. 147 | code_str: &'static str, 148 | /// Do we expect `.is_ok()` when pipefail is on? 149 | expected_ok_pipefail_on: bool, 150 | /// Do we expect `.is_ok()` when pipefail is off? 151 | expected_ok_pipefail_off: bool, 152 | } 153 | /// Make a function for [TestCase::code]. 154 | /// 155 | /// Usage: `code!((macro!(command)).extra)` 156 | /// - `(macro!(command)).extra` is an expression of type CmdResult 157 | macro_rules! code { 158 | (($macro:tt $bang:tt ($($command:tt)+)) $($after:tt)*) => { 159 | || $macro$bang($($command)+)$($after)*.is_ok() 160 | }; 161 | } 162 | /// Make a string for [TestCase::code_str]. 163 | /// 164 | /// Usage: `code_str!((macro!(command)).extra)` 165 | /// - `(macro!(command)).extra` is an expression of type CmdResult 166 | macro_rules! code_str { 167 | (($macro:tt $bang:tt ($($command:tt)+)) $($after:tt)*) => { 168 | stringify!($macro$bang($($command)+)$($after)*.is_ok()) 169 | }; 170 | } 171 | /// Make a [TestCase]. 172 | /// Usage: `test_case!(true/false, true/false, (macro!(command)).extra)` 173 | /// - the first `true/false` is TestCase::expected_ok_pipefail_on 174 | /// - the second `true/false` is TestCase::expected_ok_pipefail_off 175 | /// - `(macro!(command)).extra` is an expression of type CmdResult 176 | macro_rules! test_case { 177 | ($expected_ok_pipefail_on:expr, $expected_ok_pipefail_off:expr, ($macro:tt $bang:tt ($($command:tt)+)) $($after:tt)*) => { 178 | TestCase { 179 | code: code!(($macro $bang ($($command)+)) $($after)*), 180 | code_str: code_str!(($macro $bang ($($command)+)) $($after)*), 181 | expected_ok_pipefail_on: $expected_ok_pipefail_on, 182 | expected_ok_pipefail_off: $expected_ok_pipefail_off, 183 | } 184 | }; 185 | } 186 | /// Generate test cases for the given entry point. 187 | /// For each test case, every entry point should yield the same results. 188 | macro_rules! test_cases_for_entry_point { 189 | (($macro:tt $bang:tt (...)) $($after:tt)*) => { 190 | &[ 191 | // Use result of last command in pipeline, if all others exit successfully. 192 | test_case!(true, true, ($macro $bang (true)) $($after)*), 193 | test_case!(false, false, ($macro $bang (false)) $($after)*), 194 | test_case!(true, true, ($macro $bang (true | true)) $($after)*), 195 | test_case!(false, false, ($macro $bang (true | false)) $($after)*), 196 | // Use failure of other commands, if pipefail is on. 197 | test_case!(false, true, ($macro $bang (false | true)) $($after)*), 198 | // Use failure of last command in pipeline. 199 | test_case!(false, false, ($macro $bang (false | false)) $($after)*), 200 | // Ignore all failures, when using `ignore` command. 201 | test_case!(true, true, ($macro $bang (ignore true)) $($after)*), 202 | test_case!(true, true, ($macro $bang (ignore false)) $($after)*), 203 | test_case!(true, true, ($macro $bang (ignore true | true)) $($after)*), 204 | test_case!(true, true, ($macro $bang (ignore true | false)) $($after)*), 205 | test_case!(true, true, ($macro $bang (ignore false | true)) $($after)*), 206 | test_case!(true, true, ($macro $bang (ignore false | false)) $($after)*), 207 | // Built-ins should work too, without locking up. 208 | test_case!(true, true, ($macro $bang (echo)) $($after)*), 209 | test_case!(true, true, ($macro $bang (echo | true)) $($after)*), 210 | test_case!(false, false, ($macro $bang (echo | false)) $($after)*), 211 | test_case!(true, true, ($macro $bang (true | echo)) $($after)*), 212 | test_case!(false, true, ($macro $bang (false | echo)) $($after)*), 213 | test_case!(true, true, ($macro $bang (cd /)) $($after)*), 214 | test_case!(true, true, ($macro $bang (cd / | true)) $($after)*), 215 | test_case!(false, false, ($macro $bang (cd / | false)) $($after)*), 216 | test_case!(true, true, ($macro $bang (true | cd /)) $($after)*), 217 | test_case!(false, true, ($macro $bang (false | cd /)) $($after)*), 218 | ] 219 | }; 220 | } 221 | 222 | let test_cases: &[&[TestCase]] = &[ 223 | test_cases_for_entry_point!((run_cmd!(...))), 224 | test_cases_for_entry_point!((run_fun!(...)).map(|_stdout| ())), 225 | test_cases_for_entry_point!((spawn!(...)).unwrap().wait()), 226 | test_cases_for_entry_point!((spawn_with_output!(...)).unwrap().wait_with_all().0), 227 | test_cases_for_entry_point!( 228 | (spawn_with_output!(...)) 229 | .unwrap() 230 | .wait_with_output() 231 | .map(|_stdout| ()) 232 | ), 233 | test_cases_for_entry_point!( 234 | (spawn_with_output!(...)) 235 | .unwrap() 236 | .wait_with_raw_output(&mut vec![]) 237 | ), 238 | test_cases_for_entry_point!( 239 | (spawn_with_output!(...)) 240 | .unwrap() 241 | .wait_with_pipe(&mut |_stdout| {}) 242 | ), 243 | ]; 244 | 245 | macro_rules! check_eq { 246 | ($left:expr, $right:expr, $($rest:tt)+) => {{ 247 | let left = $left; 248 | let right = $right; 249 | if left != right { 250 | eprintln!("assertion failed ({} != {}): {}", left, right, format!($($rest)+)); 251 | false 252 | } else { 253 | true 254 | } 255 | }}; 256 | } 257 | 258 | let mut ok = true; 259 | for case in test_cases.iter().flat_map(|items| items.iter()) { 260 | ok &= check_eq!( 261 | (case.code)(), 262 | case.expected_ok_pipefail_on, 263 | "{} when pipefail is on", 264 | case.code_str 265 | ); 266 | let _pipefail = ScopedPipefail::set(false); 267 | ok &= check_eq!( 268 | (case.code)(), 269 | case.expected_ok_pipefail_off, 270 | "{} when pipefail is off", 271 | case.code_str 272 | ); 273 | let _pipefail = ScopedPipefail::set(true); 274 | } 275 | 276 | assert!(ok); 277 | } 278 | 279 | #[test] 280 | fn test_redirect() { 281 | let tmp_file = "/tmp/f"; 282 | assert!(run_cmd!(echo xxxx > $tmp_file).is_ok()); 283 | assert!(run_cmd!(echo yyyy >> $tmp_file).is_ok()); 284 | assert!( 285 | run_cmd!( 286 | ignore ls /x 2>/tmp/lsx.log; 287 | echo "dump file:"; 288 | cat /tmp/lsx.log; 289 | rm /tmp/lsx.log; 290 | ) 291 | .is_ok() 292 | ); 293 | assert!(run_cmd!(ignore ls /x 2>/dev/null).is_ok()); 294 | assert!(run_cmd!(ignore ls /x &>$tmp_file).is_ok()); 295 | assert!(run_cmd!(wc -w < $tmp_file).is_ok()); 296 | assert!(run_cmd!(ls 1>&1).is_ok()); 297 | assert!(run_cmd!(ls 2>&2).is_ok()); 298 | let tmp_log = "/tmp/echo_test.log"; 299 | assert_eq!(run_fun!(ls &>$tmp_log).unwrap(), ""); 300 | assert!(run_cmd!(rm -f $tmp_file $tmp_log).is_ok()); 301 | } 302 | 303 | #[test] 304 | fn test_proc_env() { 305 | let output = run_fun!(FOO=100 printenv | grep FOO).unwrap(); 306 | assert_eq!(output, "FOO=100"); 307 | } 308 | 309 | #[test] 310 | fn test_export_cmd() { 311 | use std::io::Write; 312 | fn my_cmd(env: &mut CmdEnv) -> CmdResult { 313 | let msg = format!("msg from foo(), args: {:?}", env.get_args()); 314 | writeln!(env.stderr(), "{}", msg)?; 315 | writeln!(env.stdout(), "bar") 316 | } 317 | 318 | fn my_cmd2(env: &mut CmdEnv) -> CmdResult { 319 | let msg = format!("msg from foo2(), args: {:?}", env.get_args()); 320 | writeln!(env.stderr(), "{}", msg)?; 321 | writeln!(env.stdout(), "bar2") 322 | } 323 | use_custom_cmd!(my_cmd, my_cmd2); 324 | assert!(run_cmd!(echo "from" "builtin").is_ok()); 325 | assert!(run_cmd!(my_cmd arg1 arg2).is_ok()); 326 | assert!(run_cmd!(my_cmd2).is_ok()); 327 | } 328 | 329 | #[test] 330 | fn test_escape() { 331 | let xxx = 42; 332 | assert_eq!( 333 | run_fun!(echo "\"a你好${xxx}世界b\"").unwrap(), 334 | "\"a你好42世界b\"" 335 | ); 336 | } 337 | 338 | #[test] 339 | fn test_current_dir() { 340 | let path = run_fun!(ls /; cd /tmp; pwd).unwrap(); 341 | assert_eq!( 342 | std::fs::canonicalize(&path).unwrap(), 343 | std::fs::canonicalize("/tmp").unwrap() 344 | ); 345 | } 346 | 347 | #[test] 348 | fn test_buitin_stdout_redirect() { 349 | let f = "/tmp/builtin"; 350 | let msg = run_fun!(echo xx &> $f).unwrap(); 351 | assert_eq!(msg, ""); 352 | assert_eq!("xx", run_fun!(cat $f).unwrap()); 353 | run_cmd!(rm -f $f).unwrap(); 354 | } 355 | 356 | #[test] 357 | fn test_path_as_var() { 358 | let dir = std::path::Path::new("/"); 359 | assert_eq!("/", run_fun!(cd $dir; pwd).unwrap()); 360 | 361 | let dir2 = std::path::PathBuf::from("/"); 362 | assert_eq!("/", run_fun!(cd $dir2; pwd).unwrap()); 363 | } 364 | 365 | #[test] 366 | fn test_empty_arg() { 367 | let opt = ""; 368 | assert!(run_cmd!(ls $opt).is_ok()); 369 | } 370 | 371 | #[test] 372 | fn test_env_var_with_equal_sign() { 373 | assert!(run_cmd!(A="-c B=c" echo).is_ok()); 374 | } 375 | 376 | #[test] 377 | fn test_vector_variable() { 378 | let opts = ["-n", "hello"]; 379 | assert!(run_cmd!(echo $[opts]).is_ok()) 380 | } 381 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cmd_lib 2 | 3 | ## Rust command-line library 4 | 5 | Common rust command-line macros and utilities, to write shell-script like tasks 6 | easily in rust programming language. Available at [crates.io](https://crates.io/crates/cmd_lib). 7 | 8 | [![Build status](https://github.com/rust-shell-script/rust_cmd_lib/workflows/ci/badge.svg)](https://github.com/rust-shell-script/rust_cmd_lib/actions) 9 | [![Crates.io](https://img.shields.io/crates/v/cmd_lib.svg)](https://crates.io/crates/cmd_lib) 10 | 11 | ### Why you need this 12 | If you need to run some external commands in rust, the 13 | [std::process::Command](https://doc.rust-lang.org/std/process/struct.Command.html) is a good 14 | abstraction layer on top of different OS syscalls. It provides fine-grained control over 15 | how a new process should be spawned, and it allows you to wait for process to finish and check the 16 | exit status or collect all of its output. However, when 17 | [Redirection](https://en.wikipedia.org/wiki/Redirection_(computing)) or 18 | [Piping](https://en.wikipedia.org/wiki/Redirection_(computing)#Piping) is needed, you need to 19 | set up the parent and child IO handles manually, like this in the 20 | [rust cookbook](https://rust-lang-nursery.github.io/rust-cookbook/os/external.html), which is often tedious 21 | and [error prone](https://github.com/ijackson/rust-rfcs/blob/command/text/0000-command-ergonomics.md#currently-accepted-wrong-programs). 22 | 23 | A lot of developers just choose shell(sh, bash, ...) scripts for such tasks, by using `<` to redirect input, 24 | `>` to redirect output and `|` to pipe outputs. In my experience, this is **the only good parts** of shell script. 25 | You can find all kinds of pitfalls and mysterious tricks to make other parts of shell script work. As the shell 26 | scripts grow, they will ultimately be unmaintainable and no one wants to touch them any more. 27 | 28 | This cmd_lib library is trying to provide the redirection and piping capabilities, and other facilities to make writing 29 | shell-script like tasks easily **without launching any shell**. For the 30 | [rust cookbook examples](https://rust-lang-nursery.github.io/rust-cookbook/os/external.html), 31 | they can usually be implemented as one line of rust macro with the help of this library, as in the 32 | [examples/rust_cookbook.rs](https://github.com/rust-shell-script/rust_cmd_lib/blob/master/examples/rust_cookbook.rs). 33 | Since they are rust code, you can always rewrite them in rust natively in the future, if necessary without spawning external commands. 34 | 35 | ### What this library provides 36 | 37 | #### Macros to run external commands 38 | - [`run_cmd!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.run_cmd.html) -> [`CmdResult`](https://docs.rs/cmd_lib/latest/cmd_lib/type.CmdResult.html) 39 | 40 | ```rust 41 | let msg = "I love rust"; 42 | run_cmd!(echo $msg)?; 43 | run_cmd!(echo "This is the message: $msg")?; 44 | 45 | // pipe commands are also supported 46 | let dir = "/var/log"; 47 | run_cmd!(du -ah $dir | sort -hr | head -n 10)?; 48 | 49 | // or a group of commands 50 | // if any command fails, just return Err(...), which is similar to bash's `set -euo pipefail` 51 | let file = "/tmp/f"; 52 | let keyword = "rust"; 53 | run_cmd! { 54 | cat ${file} | grep ${keyword}; 55 | echo "bad cmd" >&2; 56 | ignore ls /nofile; 57 | date; 58 | ls oops; 59 | cat oops; 60 | }?; 61 | ``` 62 | 63 | - [`run_fun!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.run_fun.html) -> [`FunResult`](https://docs.rs/cmd_lib/latest/cmd_lib/type.FunResult.html) 64 | 65 | ```rust 66 | let version = run_fun!(rustc --version | awk r"{print $2}")?; 67 | info!("Your rust version is {version}"); 68 | ``` 69 | 70 | #### Abstraction without overhead 71 | 72 | Since all the macros' lexical analysis and syntactic analysis happen at compile time, it can 73 | basically generate code the same as calling `std::process` APIs manually. It also includes 74 | command type checking, so most of the errors can be found at compile time instead of at 75 | runtime. With tools like `rust-analyzer`, it can give you real-time feedback for broken 76 | commands being used. 77 | 78 | You can use `cargo expand` to check the generated code. 79 | 80 | #### Intuitive parameters passing 81 | When passing parameters to `run_cmd!` and `run_fun!` macros, if they are not part to rust 82 | [String literals](https://doc.rust-lang.org/reference/tokens.html#string-literals), they will be 83 | converted to string as an atomic component, so you don't need to quote them. The parameters will be 84 | like `$a` or `${a}` in `run_cmd!` or `run_fun!` macros. 85 | 86 | ```rust 87 | let dir = "my folder"; 88 | run_cmd!(echo "Creating $dir at /tmp")?; 89 | run_cmd!(mkdir -p /tmp/$dir)?; 90 | 91 | // or with group commands: 92 | let dir = "my folder"; 93 | run_cmd!(echo "Creating $dir at /tmp"; mkdir -p /tmp/$dir)?; 94 | ``` 95 | You can consider "" as glue, so everything inside the quotes will be treated as a single atomic component. 96 | 97 | If they are part of [Raw string literals](https://doc.rust-lang.org/reference/tokens.html#raw-string-literals), 98 | there will be no string interpolation, the same as in idiomatic rust. However, you can always use `format!` macro 99 | to form the new string. For example: 100 | ```rust 101 | // string interpolation 102 | let key_word = "time"; 103 | let awk_opts = format!(r#"/{}/ {{print $(NF-3) " " $(NF-1) " " $NF}}"#, key_word); 104 | run_cmd!(ping -c 10 www.google.com | awk $awk_opts)?; 105 | ``` 106 | Notice here `$awk_opts` will be treated as single option passing to awk command. 107 | 108 | If you want to use dynamic parameters, you can use `$[]` to access vector variable: 109 | ```rust 110 | let gopts = vec![vec!["-l", "-a", "/"], vec!["-a", "/var"]]; 111 | for opts in gopts { 112 | run_cmd!(ls $[opts])?; 113 | } 114 | ``` 115 | 116 | #### Redirection and Piping 117 | Right now piping and stdin, stdout, stderr redirection are supported. Most parts are the same as in 118 | [bash scripts](https://www.gnu.org/software/bash/manual/html_node/Redirections.html#Redirections). 119 | 120 | #### Logging 121 | 122 | This library provides convenient macros and builtin commands for logging. All messages which 123 | are printed to stderr will be logged. It will also include the full running commands in the error 124 | result. 125 | 126 | ```rust 127 | let dir: &str = "folder with spaces"; 128 | run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir)?; 129 | run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir; rmdir /tmp/$dir)?; 130 | // output: 131 | // [INFO ] mkdir: cannot create directory ‘/tmp/folder with spaces’: File exists 132 | // Error: Running ["mkdir" "/tmp/folder with spaces"] exited with error; status code: 1 133 | ``` 134 | 135 | It is using rust [log crate](https://crates.io/crates/log), and you can use your actual favorite 136 | logger implementation. Notice that if you don't provide any logger, it will use env_logger to print 137 | messages from process's stderr. 138 | 139 | You can also mark your `main()` function with `#[cmd_lib::main]`, which will log error from 140 | main() by default. Like this: 141 | ```console 142 | [ERROR] FATAL: Running ["mkdir" "/tmp/folder with spaces"] exited with error; status code: 1 143 | ``` 144 | 145 | #### Builtin commands 146 | ##### cd 147 | cd: set process current directory. 148 | ```rust 149 | run_cmd! ( 150 | cd /tmp; 151 | ls | wc -l; 152 | )?; 153 | ``` 154 | Notice that builtin `cd` will only change with current scope 155 | and it will restore the previous current directory when it 156 | exits the scope. 157 | 158 | Use `std::env::set_current_dir` if you want to change the current 159 | working directory for the whole program. 160 | 161 | ##### ignore 162 | 163 | Ignore errors for command execution. 164 | 165 | ##### echo 166 | Print messages to stdout. 167 | ```console 168 | -n do not output the trailing newline 169 | ``` 170 | 171 | ##### error, warn, info, debug, trace 172 | 173 | Print messages to logging with different levels. You can also use the normal logging macros, 174 | if you don't need to do logging inside the command group. 175 | 176 | ```rust 177 | run_cmd!(error "This is an error message")?; 178 | run_cmd!(warn "This is a warning message")?; 179 | run_cmd!(info "This is an information message")?; 180 | // output: 181 | // [ERROR] This is an error message 182 | // [WARN ] This is a warning message 183 | // [INFO ] This is an information message 184 | ``` 185 | 186 | #### Low-level process spawning macros 187 | 188 | [`spawn!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.spawn.html) macro executes the whole command as a child process, returning a handle to it. By 189 | default, stdin, stdout and stderr are inherited from the parent. The process will run in the 190 | background, so you can run other stuff concurrently. You can call [`wait()`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.CmdChildren.html#method.wait) to wait 191 | for the process to finish. 192 | 193 | With [`spawn_with_output!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.spawn_with_output.html) you can get output by calling 194 | [`wait_with_output()`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.FunChildren.html#method.wait_with_output), 195 | [`wait_with_all()`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.FunChildren.html#method.wait_with_all) 196 | or even do stream 197 | processing with [`wait_with_pipe()`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.FunChildren.html#method.wait_with_pipe). 198 | 199 | There are also other useful APIs, and you can check the docs for more details. 200 | 201 | ```rust 202 | let mut proc = spawn!(ping -c 10 192.168.0.1)?; 203 | // do other stuff 204 | // ... 205 | proc.wait()?; 206 | 207 | let mut proc = spawn_with_output!(/bin/cat file.txt | sed s/a/b/)?; 208 | // do other stuff 209 | // ... 210 | let output = proc.wait_with_output()?; 211 | 212 | spawn_with_output!(journalctl)?.wait_with_pipe(&mut |pipe| { 213 | BufReader::new(pipe) 214 | .lines() 215 | .filter_map(|line| line.ok()) 216 | .filter(|line| line.find("usb").is_some()) 217 | .take(10) 218 | .for_each(|line| println!("{}", line)); 219 | })?; 220 | ``` 221 | 222 | #### Macro to register your own commands 223 | Declare your function with the right signature, and register it with [`use_custom_cmd!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.use_custom_cmd.html) macro: 224 | 225 | ```rust 226 | fn my_cmd(env: &mut CmdEnv) -> CmdResult { 227 | let args = env.get_args(); 228 | let (res, stdout, stderr) = spawn_with_output! { 229 | orig_cmd $[args] 230 | --long-option xxx 231 | --another-option yyy 232 | }? 233 | .wait_with_all(); 234 | writeln!(env.stdout(), "{}", stdout)?; 235 | writeln!(env.stderr(), "{}", stderr)?; 236 | res 237 | } 238 | 239 | use_custom_cmd!(my_cmd); 240 | ``` 241 | 242 | #### Macros to define, get and set thread-local global variables 243 | - [`tls_init!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_init.html) to define thread local global variable 244 | - [`tls_get!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_get.html) to get the value 245 | - [`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) to set the value 246 | ```rust 247 | tls_init!(DELAY, f64, 1.0); 248 | const DELAY_FACTOR: f64 = 0.8; 249 | tls_set!(DELAY, |d| *d *= DELAY_FACTOR); 250 | let d = tls_get!(DELAY); 251 | // check more examples in examples/tetris.rs 252 | ``` 253 | 254 | ### Other Notes 255 | 256 | #### Minimum supported Rust version 257 | 258 | cmd_lib(2.0)'s MSRV is 1.88. 259 | 260 | #### Environment Variables 261 | 262 | You can use [std::env::var](https://doc.rust-lang.org/std/env/fn.var.html) to fetch the environment variable 263 | key from the current process. It will report error if the environment variable is not present, and it also 264 | includes other checks to avoid silent failures. 265 | 266 | To set environment variables in **single-threaded programs**, you can use [std::env::set_var] and 267 | [std::env::remove_var]. While those functions **[must not be called]** if any other threads might be running, you can 268 | always set environment variables for one command at a time, by putting the assignments before the command: 269 | 270 | ```rust 271 | run_cmd!(FOO=100 /tmp/test_run_cmd_lib.sh)?; 272 | ``` 273 | 274 | #### Security Notes 275 | Using macros can actually avoid command injection, since we do parsing before variable substitution. 276 | For example, below code is fine even without any quotes: 277 | ```rust 278 | fn cleanup_uploaded_file(file: &Path) -> CmdResult { 279 | run_cmd!(/bin/rm -f /var/upload/$file) 280 | } 281 | ``` 282 | It is not the case in bash, which will always do variable substitution at first. 283 | 284 | #### Glob/Wildcard 285 | 286 | This library does not provide glob functions, to avoid silent errors and other surprises. 287 | You can use the [glob](https://github.com/rust-lang-nursery/glob) package instead. 288 | 289 | #### Thread Safety 290 | 291 | This library tries very hard to not set global state, so parallel `cargo test` can be executed just fine. 292 | That said, there are some limitations to be aware of: 293 | 294 | - [std::env::set_var] and [std::env::remove_var] **[must not be called]** in a multi-threaded program 295 | - [`tls_init!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_init.html), 296 | [`tls_get!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_get.html), and 297 | [`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) create *thread-local* variables, which means 298 | each thread will have its own independent version of the variable 299 | - [`set_debug`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_debug.html) and 300 | [`set_pipefail`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_pipefail.html) are *global* and affect all threads; 301 | to change those settings without affecting other threads, use 302 | [`ScopedDebug`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.ScopedDebug.html) and 303 | [`ScopedPipefail`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.ScopedPipefail.html) 304 | 305 | [std::env::set_var]: https://doc.rust-lang.org/std/env/fn.set_var.html 306 | [std::env::remove_var]: https://doc.rust-lang.org/std/env/fn.remove_var.html 307 | [must not be called]: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/newly-unsafe-functions.html#stdenvset_var-remove_var 308 | 309 | License: MIT OR Apache-2.0 310 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Rust command-line library 2 | //! 3 | //! Common rust command-line macros and utilities, to write shell-script like tasks 4 | //! easily in rust programming language. Available at [crates.io](https://crates.io/crates/cmd_lib). 5 | //! 6 | //! [![Build status](https://github.com/rust-shell-script/rust_cmd_lib/workflows/ci/badge.svg)](https://github.com/rust-shell-script/rust_cmd_lib/actions) 7 | //! [![Crates.io](https://img.shields.io/crates/v/cmd_lib.svg)](https://crates.io/crates/cmd_lib) 8 | //! 9 | //! ## Why you need this 10 | //! If you need to run some external commands in rust, the 11 | //! [std::process::Command](https://doc.rust-lang.org/std/process/struct.Command.html) is a good 12 | //! abstraction layer on top of different OS syscalls. It provides fine-grained control over 13 | //! how a new process should be spawned, and it allows you to wait for process to finish and check the 14 | //! exit status or collect all of its output. However, when 15 | //! [Redirection](https://en.wikipedia.org/wiki/Redirection_(computing)) or 16 | //! [Piping](https://en.wikipedia.org/wiki/Redirection_(computing)#Piping) is needed, you need to 17 | //! set up the parent and child IO handles manually, like this in the 18 | //! [rust cookbook](https://rust-lang-nursery.github.io/rust-cookbook/os/external.html), which is often tedious 19 | //! and [error prone](https://github.com/ijackson/rust-rfcs/blob/command/text/0000-command-ergonomics.md#currently-accepted-wrong-programs). 20 | //! 21 | //! A lot of developers just choose shell(sh, bash, ...) scripts for such tasks, by using `<` to redirect input, 22 | //! `>` to redirect output and `|` to pipe outputs. In my experience, this is **the only good parts** of shell script. 23 | //! You can find all kinds of pitfalls and mysterious tricks to make other parts of shell script work. As the shell 24 | //! scripts grow, they will ultimately be unmaintainable and no one wants to touch them any more. 25 | //! 26 | //! This cmd_lib library is trying to provide the redirection and piping capabilities, and other facilities to make writing 27 | //! shell-script like tasks easily **without launching any shell**. For the 28 | //! [rust cookbook examples](https://rust-lang-nursery.github.io/rust-cookbook/os/external.html), 29 | //! they can usually be implemented as one line of rust macro with the help of this library, as in the 30 | //! [examples/rust_cookbook.rs](https://github.com/rust-shell-script/rust_cmd_lib/blob/master/examples/rust_cookbook.rs). 31 | //! Since they are rust code, you can always rewrite them in rust natively in the future, if necessary without spawning external commands. 32 | //! 33 | //! ## What this library provides 34 | //! 35 | //! ### Macros to run external commands 36 | //! - [`run_cmd!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.run_cmd.html) -> [`CmdResult`](https://docs.rs/cmd_lib/latest/cmd_lib/type.CmdResult.html) 37 | //! 38 | //! ```no_run 39 | //! # use cmd_lib::run_cmd; 40 | //! let msg = "I love rust"; 41 | //! run_cmd!(echo $msg)?; 42 | //! run_cmd!(echo "This is the message: $msg")?; 43 | //! 44 | //! // pipe commands are also supported 45 | //! let dir = "/var/log"; 46 | //! run_cmd!(du -ah $dir | sort -hr | head -n 10)?; 47 | //! 48 | //! // or a group of commands 49 | //! // if any command fails, just return Err(...), which is similar to bash's `set -euo pipefail` 50 | //! let file = "/tmp/f"; 51 | //! let keyword = "rust"; 52 | //! run_cmd! { 53 | //! cat ${file} | grep ${keyword}; 54 | //! echo "bad cmd" >&2; 55 | //! ignore ls /nofile; 56 | //! date; 57 | //! ls oops; 58 | //! cat oops; 59 | //! }?; 60 | //! # Ok::<(), std::io::Error>(()) 61 | //! ``` 62 | //! 63 | //! - [`run_fun!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.run_fun.html) -> [`FunResult`](https://docs.rs/cmd_lib/latest/cmd_lib/type.FunResult.html) 64 | //! 65 | //! ```no_run 66 | //! # use cmd_lib::{info, run_fun}; 67 | //! let version = run_fun!(rustc --version | awk r"{print $2}")?; 68 | //! info!("Your rust version is {version}"); 69 | //! # Ok::<(), std::io::Error>(()) 70 | //! ``` 71 | //! 72 | //! ### Abstraction without overhead 73 | //! 74 | //! Since all the macros' lexical analysis and syntactic analysis happen at compile time, it can 75 | //! basically generate code the same as calling `std::process` APIs manually. It also includes 76 | //! command type checking, so most of the errors can be found at compile time instead of at 77 | //! runtime. With tools like `rust-analyzer`, it can give you real-time feedback for broken 78 | //! commands being used. 79 | //! 80 | //! You can use `cargo expand` to check the generated code. 81 | //! 82 | //! ### Intuitive parameters passing 83 | //! When passing parameters to `run_cmd!` and `run_fun!` macros, if they are not part to rust 84 | //! [String literals](https://doc.rust-lang.org/reference/tokens.html#string-literals), they will be 85 | //! converted to string as an atomic component, so you don't need to quote them. The parameters will be 86 | //! like `$a` or `${a}` in `run_cmd!` or `run_fun!` macros. 87 | //! 88 | //! ```no_run 89 | //! # use cmd_lib::run_cmd; 90 | //! let dir = "my folder"; 91 | //! run_cmd!(echo "Creating $dir at /tmp")?; 92 | //! run_cmd!(mkdir -p /tmp/$dir)?; 93 | //! 94 | //! // or with group commands: 95 | //! let dir = "my folder"; 96 | //! run_cmd!(echo "Creating $dir at /tmp"; mkdir -p /tmp/$dir)?; 97 | //! # Ok::<(), std::io::Error>(()) 98 | //! ``` 99 | //! You can consider "" as glue, so everything inside the quotes will be treated as a single atomic component. 100 | //! 101 | //! If they are part of [Raw string literals](https://doc.rust-lang.org/reference/tokens.html#raw-string-literals), 102 | //! there will be no string interpolation, the same as in idiomatic rust. However, you can always use `format!` macro 103 | //! to form the new string. For example: 104 | //! ```no_run 105 | //! # use cmd_lib::run_cmd; 106 | //! // string interpolation 107 | //! let key_word = "time"; 108 | //! let awk_opts = format!(r#"/{}/ {{print $(NF-3) " " $(NF-1) " " $NF}}"#, key_word); 109 | //! run_cmd!(ping -c 10 www.google.com | awk $awk_opts)?; 110 | //! # Ok::<(), std::io::Error>(()) 111 | //! ``` 112 | //! Notice here `$awk_opts` will be treated as single option passing to awk command. 113 | //! 114 | //! If you want to use dynamic parameters, you can use `$[]` to access vector variable: 115 | //! ```no_run 116 | //! # use cmd_lib::run_cmd; 117 | //! let gopts = vec![vec!["-l", "-a", "/"], vec!["-a", "/var"]]; 118 | //! for opts in gopts { 119 | //! run_cmd!(ls $[opts])?; 120 | //! } 121 | //! # Ok::<(), std::io::Error>(()) 122 | //! ``` 123 | //! 124 | //! ### Redirection and Piping 125 | //! Right now piping and stdin, stdout, stderr redirection are supported. Most parts are the same as in 126 | //! [bash scripts](https://www.gnu.org/software/bash/manual/html_node/Redirections.html#Redirections). 127 | //! 128 | //! ### Logging 129 | //! 130 | //! This library provides convenient macros and builtin commands for logging. All messages which 131 | //! are printed to stderr will be logged. It will also include the full running commands in the error 132 | //! result. 133 | //! 134 | //! ```no_run 135 | //! # use cmd_lib::*; 136 | //! let dir: &str = "folder with spaces"; 137 | //! run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir)?; 138 | //! run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir; rmdir /tmp/$dir)?; 139 | //! // output: 140 | //! // [INFO ] mkdir: cannot create directory ‘/tmp/folder with spaces’: File exists 141 | //! // Error: Running ["mkdir" "/tmp/folder with spaces"] exited with error; status code: 1 142 | //! # Ok::<(), std::io::Error>(()) 143 | //! ``` 144 | //! 145 | //! It is using rust [log crate](https://crates.io/crates/log), and you can use your actual favorite 146 | //! logger implementation. Notice that if you don't provide any logger, it will use env_logger to print 147 | //! messages from process's stderr. 148 | //! 149 | //! You can also mark your `main()` function with `#[cmd_lib::main]`, which will log error from 150 | //! main() by default. Like this: 151 | //! ```console 152 | //! [ERROR] FATAL: Running ["mkdir" "/tmp/folder with spaces"] exited with error; status code: 1 153 | //! ``` 154 | //! 155 | //! ### Builtin commands 156 | //! #### cd 157 | //! cd: set process current directory. 158 | //! ```no_run 159 | //! # use cmd_lib::run_cmd; 160 | //! run_cmd! ( 161 | //! cd /tmp; 162 | //! ls | wc -l; 163 | //! )?; 164 | //! # Ok::<(), std::io::Error>(()) 165 | //! ``` 166 | //! Notice that builtin `cd` will only change with current scope 167 | //! and it will restore the previous current directory when it 168 | //! exits the scope. 169 | //! 170 | //! Use `std::env::set_current_dir` if you want to change the current 171 | //! working directory for the whole program. 172 | //! 173 | //! #### ignore 174 | //! 175 | //! Ignore errors for command execution. 176 | //! 177 | //! #### echo 178 | //! Print messages to stdout. 179 | //! ```console 180 | //! -n do not output the trailing newline 181 | //! ``` 182 | //! 183 | //! #### error, warn, info, debug, trace 184 | //! 185 | //! Print messages to logging with different levels. You can also use the normal logging macros, 186 | //! if you don't need to do logging inside the command group. 187 | //! 188 | //! ```no_run 189 | //! # use cmd_lib::*; 190 | //! run_cmd!(error "This is an error message")?; 191 | //! run_cmd!(warn "This is a warning message")?; 192 | //! run_cmd!(info "This is an information message")?; 193 | //! // output: 194 | //! // [ERROR] This is an error message 195 | //! // [WARN ] This is a warning message 196 | //! // [INFO ] This is an information message 197 | //! # Ok::<(), std::io::Error>(()) 198 | //! ``` 199 | //! 200 | //! ### Low-level process spawning macros 201 | //! 202 | //! [`spawn!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.spawn.html) macro executes the whole command as a child process, returning a handle to it. By 203 | //! default, stdin, stdout and stderr are inherited from the parent. The process will run in the 204 | //! background, so you can run other stuff concurrently. You can call [`wait()`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.CmdChildren.html#method.wait) to wait 205 | //! for the process to finish. 206 | //! 207 | //! With [`spawn_with_output!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.spawn_with_output.html) you can get output by calling 208 | //! [`wait_with_output()`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.FunChildren.html#method.wait_with_output), 209 | //! [`wait_with_all()`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.FunChildren.html#method.wait_with_all) 210 | //! or even do stream 211 | //! processing with [`wait_with_pipe()`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.FunChildren.html#method.wait_with_pipe). 212 | //! 213 | //! There are also other useful APIs, and you can check the docs for more details. 214 | //! 215 | //! ```no_run 216 | //! # use cmd_lib::*; 217 | //! # use std::io::{BufRead, BufReader}; 218 | //! let mut proc = spawn!(ping -c 10 192.168.0.1)?; 219 | //! // do other stuff 220 | //! // ... 221 | //! proc.wait()?; 222 | //! 223 | //! let mut proc = spawn_with_output!(/bin/cat file.txt | sed s/a/b/)?; 224 | //! // do other stuff 225 | //! // ... 226 | //! let output = proc.wait_with_output()?; 227 | //! 228 | //! spawn_with_output!(journalctl)?.wait_with_pipe(&mut |pipe| { 229 | //! BufReader::new(pipe) 230 | //! .lines() 231 | //! .filter_map(|line| line.ok()) 232 | //! .filter(|line| line.find("usb").is_some()) 233 | //! .take(10) 234 | //! .for_each(|line| println!("{}", line)); 235 | //! })?; 236 | //! # Ok::<(), std::io::Error>(()) 237 | //! ``` 238 | //! 239 | //! ### Macro to register your own commands 240 | //! Declare your function with the right signature, and register it with [`use_custom_cmd!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.use_custom_cmd.html) macro: 241 | //! 242 | //! ```no_run 243 | //! # use cmd_lib::*; 244 | //! # use std::io::Write; 245 | //! fn my_cmd(env: &mut CmdEnv) -> CmdResult { 246 | //! let args = env.get_args(); 247 | //! let (res, stdout, stderr) = spawn_with_output! { 248 | //! orig_cmd $[args] 249 | //! --long-option xxx 250 | //! --another-option yyy 251 | //! }? 252 | //! .wait_with_all(); 253 | //! writeln!(env.stdout(), "{}", stdout)?; 254 | //! writeln!(env.stderr(), "{}", stderr)?; 255 | //! res 256 | //! } 257 | //! 258 | //! use_custom_cmd!(my_cmd); 259 | //! # Ok::<(), std::io::Error>(()) 260 | //! ``` 261 | //! 262 | //! ### Macros to define, get and set thread-local global variables 263 | //! - [`tls_init!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_init.html) to define thread local global variable 264 | //! - [`tls_get!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_get.html) to get the value 265 | //! - [`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) to set the value 266 | //! ```no_run 267 | //! # use cmd_lib::{ tls_init, tls_get, tls_set }; 268 | //! tls_init!(DELAY, f64, 1.0); 269 | //! const DELAY_FACTOR: f64 = 0.8; 270 | //! tls_set!(DELAY, |d| *d *= DELAY_FACTOR); 271 | //! let d = tls_get!(DELAY); 272 | //! // check more examples in examples/tetris.rs 273 | //! ``` 274 | //! 275 | //! ## Other Notes 276 | //! 277 | //! ### Minimum supported Rust version 278 | //! 279 | //! cmd_lib(2.0)'s MSRV is 1.88. 280 | //! 281 | //! ### Environment Variables 282 | //! 283 | //! You can use [std::env::var](https://doc.rust-lang.org/std/env/fn.var.html) to fetch the environment variable 284 | //! key from the current process. It will report error if the environment variable is not present, and it also 285 | //! includes other checks to avoid silent failures. 286 | //! 287 | //! To set environment variables in **single-threaded programs**, you can use [std::env::set_var] and 288 | //! [std::env::remove_var]. While those functions **[must not be called]** if any other threads might be running, you can 289 | //! always set environment variables for one command at a time, by putting the assignments before the command: 290 | //! 291 | //! ```no_run 292 | //! # use cmd_lib::run_cmd; 293 | //! run_cmd!(FOO=100 /tmp/test_run_cmd_lib.sh)?; 294 | //! # Ok::<(), std::io::Error>(()) 295 | //! ``` 296 | //! 297 | //! ### Security Notes 298 | //! Using macros can actually avoid command injection, since we do parsing before variable substitution. 299 | //! For example, below code is fine even without any quotes: 300 | //! ```no_run 301 | //! # use cmd_lib::{run_cmd, CmdResult}; 302 | //! # use std::path::Path; 303 | //! fn cleanup_uploaded_file(file: &Path) -> CmdResult { 304 | //! run_cmd!(/bin/rm -f /var/upload/$file) 305 | //! } 306 | //! ``` 307 | //! It is not the case in bash, which will always do variable substitution at first. 308 | //! 309 | //! ### Glob/Wildcard 310 | //! 311 | //! This library does not provide glob functions, to avoid silent errors and other surprises. 312 | //! You can use the [glob](https://github.com/rust-lang-nursery/glob) package instead. 313 | //! 314 | //! ### Thread Safety 315 | //! 316 | //! This library tries very hard to not set global state, so parallel `cargo test` can be executed just fine. 317 | //! That said, there are some limitations to be aware of: 318 | //! 319 | //! - [std::env::set_var] and [std::env::remove_var] **[must not be called]** in a multi-threaded program 320 | //! - [`tls_init!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_init.html), 321 | //! [`tls_get!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_get.html), and 322 | //! [`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) create *thread-local* variables, which means 323 | //! each thread will have its own independent version of the variable 324 | //! - [`set_debug`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_debug.html) and 325 | //! [`set_pipefail`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_pipefail.html) are *global* and affect all threads; 326 | //! to change those settings without affecting other threads, use 327 | //! [`ScopedDebug`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.ScopedDebug.html) and 328 | //! [`ScopedPipefail`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.ScopedPipefail.html) 329 | //! 330 | //! [std::env::set_var]: https://doc.rust-lang.org/std/env/fn.set_var.html 331 | //! [std::env::remove_var]: https://doc.rust-lang.org/std/env/fn.remove_var.html 332 | //! [must not be called]: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/newly-unsafe-functions.html#stdenvset_var-remove_var 333 | 334 | pub use cmd_lib_macros::{ 335 | cmd_die, main, run_cmd, run_fun, spawn, spawn_with_output, use_custom_cmd, 336 | }; 337 | /// Return type for [`run_fun!()`] macro. 338 | pub type FunResult = std::io::Result; 339 | /// Return type for [`run_cmd!()`] macro. 340 | pub type CmdResult = std::io::Result<()>; 341 | #[cfg(feature = "build-print")] 342 | #[doc(hidden)] 343 | pub use build_print as inner_log; 344 | pub use child::{CmdChildren, FunChildren}; 345 | pub use io::{CmdIn, CmdOut}; 346 | #[cfg(not(feature = "build-print"))] 347 | #[doc(hidden)] 348 | pub use log as inner_log; 349 | #[doc(hidden)] 350 | pub use logger::try_init_default_logger; 351 | #[doc(hidden)] 352 | pub use process::{AsOsStr, Cmd, CmdString, Cmds, GroupCmds, Redirect, register_cmd}; 353 | pub use process::{CmdEnv, ScopedDebug, ScopedPipefail, set_debug, set_pipefail}; 354 | 355 | mod builtins; 356 | mod child; 357 | mod io; 358 | mod logger; 359 | mod process; 360 | mod thread_local; 361 | -------------------------------------------------------------------------------- /examples/tetris.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Tetris game written in pure bash 4 | # 5 | # I tried to mimic as close as possible original tetris game 6 | # which was implemented on old soviet DVK computers (PDP-11 clones) 7 | # 8 | # Videos of this tetris can be found here: 9 | # 10 | # http://www.youtube.com/watch?v=O0gAgQQHFcQ 11 | # http://www.youtube.com/watch?v=iIQc1F3UuV4 12 | # 13 | # This script was created on ubuntu 13.04 x64 and bash 4.2.45(1)-release. 14 | # It was not tested on other unix like operating systems. 15 | # 16 | # Enjoy :-)! 17 | # 18 | # Author: Kirill Timofeev 19 | # 20 | # This program is free software. It comes without any warranty, to the extent 21 | # permitted by applicable law. You can redistribute it and/or modify it under 22 | # the terms of the Do What The Fuck You Want To Public License, Version 2, as 23 | # published by Sam Hocevar. See http://www.wtfpl.net/ for more details. 24 | 25 | set -u # non initialized variable is an error 26 | 27 | # Those are commands sent to controller by key press processing code 28 | # In controller they are used as index to retrieve actual functuon from array 29 | QUIT=0 30 | RIGHT=1 31 | LEFT=2 32 | ROTATE=3 33 | DOWN=4 34 | DROP=5 35 | TOGGLE_HELP=6 36 | TOGGLE_NEXT=7 37 | TOGGLE_COLOR=8 38 | 39 | DELAY=1000 # initial delay between piece movements (milliseconds) 40 | DELAY_FACTOR="8/10" # this value controls delay decrease for each level up 41 | 42 | # color codes 43 | RED=1 44 | GREEN=2 45 | YELLOW=3 46 | BLUE=4 47 | FUCHSIA=5 48 | CYAN=6 49 | WHITE=7 50 | 51 | # Location and size of playfield, color of border 52 | PLAYFIELD_W=10 53 | PLAYFIELD_H=20 54 | PLAYFIELD_X=30 55 | PLAYFIELD_Y=1 56 | BORDER_COLOR=$YELLOW 57 | 58 | # Location and color of score information 59 | SCORE_X=1 60 | SCORE_Y=2 61 | SCORE_COLOR=$GREEN 62 | 63 | # Location and color of help information 64 | HELP_X=58 65 | HELP_Y=1 66 | HELP_COLOR=$CYAN 67 | 68 | # Next piece location 69 | NEXT_X=14 70 | NEXT_Y=11 71 | 72 | # Location of "game over" in the end of the game 73 | GAMEOVER_X=1 74 | GAMEOVER_Y=$((PLAYFIELD_H + 3)) 75 | 76 | # Intervals after which game level (and game speed) is increased 77 | LEVEL_UP=20 78 | 79 | colors=($RED $GREEN $YELLOW $BLUE $FUCHSIA $CYAN $WHITE) 80 | 81 | use_color=1 # 1 if we use color, 0 if not 82 | empty_cell=" ." # how we draw empty cell 83 | filled_cell="[]" # how we draw filled cell 84 | 85 | score=0 # score variable initialization 86 | level=1 # level variable initialization 87 | lines_completed=0 # completed lines counter initialization 88 | 89 | # screen_buffer is variable, that accumulates all screen changes 90 | # this variable is printed in controller once per game cycle 91 | screen_buffer="" 92 | puts() { 93 | screen_buffer+=${1} 94 | } 95 | 96 | flush_screen() { 97 | echo -ne "$screen_buffer" 98 | screen_buffer="" 99 | } 100 | 101 | # move cursor to (x,y) and print string 102 | # (1,1) is upper left corner of the screen 103 | xyprint() { 104 | puts "\e[${2};${1}H${3}" 105 | } 106 | 107 | show_cursor() { 108 | echo -ne "\e[?25h" 109 | } 110 | 111 | hide_cursor() { 112 | echo -ne "\e[?25l" 113 | } 114 | 115 | # foreground color 116 | set_fg() { 117 | ((use_color)) && puts "\e[3${1}m" 118 | } 119 | 120 | # background color 121 | set_bg() { 122 | ((use_color)) && puts "\e[4${1}m" 123 | } 124 | 125 | reset_colors() { 126 | puts "\e[0m" 127 | } 128 | 129 | set_bold() { 130 | puts "\e[1m" 131 | } 132 | 133 | # playfield is an array, each row is represented by integer 134 | # each cell occupies 3 bits (empty if 0, other values encode color) 135 | redraw_playfield() { 136 | local x y color 137 | 138 | for ((y = 0; y < PLAYFIELD_H; y++)) { 139 | xyprint $PLAYFIELD_X $((PLAYFIELD_Y + y)) "" 140 | for ((x = 0; x < PLAYFIELD_W; x++)) { 141 | ((color = ((playfield[y] >> (x * 3)) & 7))) 142 | if ((color == 0)) ; then 143 | puts "$empty_cell" 144 | else 145 | set_fg $color 146 | set_bg $color 147 | puts "$filled_cell" 148 | reset_colors 149 | fi 150 | } 151 | } 152 | } 153 | 154 | update_score() { 155 | # Arguments: 1 - number of completed lines 156 | ((lines_completed += $1)) 157 | # Unfortunately I don't know scoring algorithm of original tetris 158 | # Here score is incremented with squared number of lines completed 159 | # this seems reasonable since it takes more efforts to complete several lines at once 160 | ((score += ($1 * $1))) 161 | if (( score > LEVEL_UP * level)) ; then # if level should be increased 162 | ((level++)) # increment level 163 | kill -SIGUSR1 $ticker_pid # and send SIGUSR1 signal to ticker process (please see ticker() function for more details) 164 | fi 165 | set_bold 166 | set_fg $SCORE_COLOR 167 | xyprint $SCORE_X $SCORE_Y "Lines completed: $lines_completed" 168 | xyprint $SCORE_X $((SCORE_Y + 1)) "Level: $level" 169 | xyprint $SCORE_X $((SCORE_Y + 2)) "Score: $score" 170 | reset_colors 171 | } 172 | 173 | help=( 174 | " Use cursor keys" 175 | " or" 176 | " s: rotate" 177 | "a: left, d: right" 178 | " space: drop" 179 | " q: quit" 180 | " c: toggle color" 181 | "n: toggle show next" 182 | "h: toggle this help" 183 | ) 184 | 185 | help_on=1 # if this flag is 1 help is shown 186 | 187 | draw_help() { 188 | local i s 189 | 190 | set_bold 191 | set_fg $HELP_COLOR 192 | for ((i = 0; i < ${#help[@]}; i++ )) { 193 | # ternary assignment: if help_on is 1 use string as is, otherwise substitute all characters with spaces 194 | ((help_on)) && s="${help[i]}" || s="${help[i]//?/ }" 195 | xyprint $HELP_X $((HELP_Y + i)) "$s" 196 | } 197 | reset_colors 198 | } 199 | 200 | toggle_help() { 201 | ((help_on ^= 1)) 202 | draw_help 203 | } 204 | 205 | # this array holds all possible pieces that can be used in the game 206 | # each piece consists of 4 cells numbered from 0x0 to 0xf: 207 | # 0123 208 | # 4567 209 | # 89ab 210 | # cdef 211 | # each string is sequence of cells for different orientations 212 | # depending on piece symmetry there can be 1, 2 or 4 orientations 213 | # relative coordinates are calculated as follows: 214 | # x=((cell & 3)); y=((cell >> 2)) 215 | piece_data=( 216 | "1256" # square 217 | "159d4567" # line 218 | "45120459" # s 219 | "01561548" # z 220 | "159a845601592654" # l 221 | "159804562159a654" # inverted l 222 | "1456159645694159" # t 223 | ) 224 | 225 | draw_piece() { 226 | # Arguments: 227 | # 1 - x, 2 - y, 3 - type, 4 - rotation, 5 - cell content 228 | local i x y c 229 | 230 | # loop through piece cells: 4 cells, each has 2 coordinates 231 | for ((i = 0; i < 4; i++)) { 232 | c=0x${piece_data[$3]:$((i + $4 * 4)):1} 233 | # relative coordinates are retrieved based on orientation and added to absolute coordinates 234 | ((x = $1 + (c & 3) * 2)) 235 | ((y = $2 + (c >> 2))) 236 | xyprint $x $y "$5" 237 | } 238 | } 239 | 240 | next_piece=0 241 | next_piece_rotation=0 242 | next_piece_color=0 243 | 244 | next_on=1 # if this flag is 1 next piece is shown 245 | 246 | draw_next() { 247 | # Argument: 1 - visibility (0 - no, 1 - yes), if this argument is skipped $next_on is used 248 | local s="$filled_cell" visible=${1:-$next_on} 249 | ((visible)) && { 250 | set_fg $next_piece_color 251 | set_bg $next_piece_color 252 | } || { 253 | s="${s//?/ }" 254 | } 255 | draw_piece $NEXT_X $NEXT_Y $next_piece $next_piece_rotation "$s" 256 | reset_colors 257 | } 258 | 259 | toggle_next() { 260 | draw_next $((next_on ^= 1)) 261 | } 262 | 263 | draw_current() { 264 | # Arguments: 1 - string to draw single cell 265 | # factor 2 for x because each cell is 2 characters wide 266 | draw_piece $((current_piece_x * 2 + PLAYFIELD_X)) $((current_piece_y + PLAYFIELD_Y)) $current_piece $current_piece_rotation "$1" 267 | } 268 | 269 | show_current() { 270 | set_fg $current_piece_color 271 | set_bg $current_piece_color 272 | draw_current "${filled_cell}" 273 | reset_colors 274 | } 275 | 276 | clear_current() { 277 | draw_current "${empty_cell}" 278 | } 279 | 280 | new_piece_location_ok() { 281 | # Arguments: 1 - new x coordinate of the piece, 2 - new y coordinate of the piece 282 | # test if piece can be moved to new location 283 | local i c x y x_test=$1 y_test=$2 284 | 285 | for ((i = 0; i < 4; i++)) { 286 | c=0x${piece_data[$current_piece]:$((i + current_piece_rotation * 4)):1} 287 | # new x and y coordinates of piece cell 288 | ((y = (c >> 2) + y_test)) 289 | ((x = (c & 3) + x_test)) 290 | ((y < 0 || y >= PLAYFIELD_H || x < 0 || x >= PLAYFIELD_W )) && return 1 # check if we are out of the play field 291 | ((((playfield[y] >> (x * 3)) & 7) != 0 )) && return 1 # check if location is already ocupied 292 | } 293 | return 0 294 | } 295 | 296 | get_random_next() { 297 | # next piece becomes current 298 | current_piece=$next_piece 299 | current_piece_rotation=$next_piece_rotation 300 | current_piece_color=$next_piece_color 301 | # place current at the top of play field, approximately at the center 302 | ((current_piece_x = (PLAYFIELD_W - 4) / 2)) 303 | ((current_piece_y = 0)) 304 | # check if piece can be placed at this location, if not - game over 305 | new_piece_location_ok $current_piece_x $current_piece_y || exit 306 | show_current 307 | 308 | draw_next 0 309 | # now let's get next piece 310 | ((next_piece = RANDOM % ${#piece_data[@]})) 311 | ((next_piece_rotation = RANDOM % (${#piece_data[$next_piece]} / 4))) 312 | ((next_piece_color = colors[RANDOM % ${#colors[@]}])) 313 | draw_next 314 | } 315 | 316 | draw_border() { 317 | local i x1 x2 y 318 | 319 | set_bold 320 | set_fg $BORDER_COLOR 321 | ((x1 = PLAYFIELD_X - 2)) # 2 here is because border is 2 characters thick 322 | ((x2 = PLAYFIELD_X + PLAYFIELD_W * 2)) # 2 here is because each cell on play field is 2 characters wide 323 | for ((i = 0; i < PLAYFIELD_H + 1; i++)) { 324 | ((y = i + PLAYFIELD_Y)) 325 | xyprint $x1 $y "<|" 326 | xyprint $x2 $y "|>" 327 | } 328 | 329 | ((y = PLAYFIELD_Y + PLAYFIELD_H)) 330 | for ((i = 0; i < PLAYFIELD_W; i++)) { 331 | ((x1 = i * 2 + PLAYFIELD_X)) # 2 here is because each cell on play field is 2 characters wide 332 | xyprint $x1 $y '==' 333 | xyprint $x1 $((y + 1)) "\/" 334 | } 335 | reset_colors 336 | } 337 | 338 | redraw_screen() { 339 | draw_next 340 | update_score 0 341 | draw_help 342 | draw_border 343 | redraw_playfield 344 | show_current 345 | } 346 | 347 | toggle_color() { 348 | ((use_color ^= 1)) 349 | redraw_screen 350 | } 351 | 352 | init() { 353 | local i 354 | 355 | # playfield is initialized with -1s (empty cells) 356 | for ((i = 0; i < PLAYFIELD_H; i++)) { 357 | playfield[$i]=0 358 | } 359 | 360 | clear 361 | hide_cursor 362 | get_random_next 363 | get_random_next 364 | redraw_screen 365 | flush_screen 366 | } 367 | 368 | # this function updates occupied cells in playfield array after piece is dropped 369 | flatten_playfield() { 370 | local i c x y 371 | for ((i = 0; i < 4; i++)) { 372 | c=0x${piece_data[$current_piece]:$((i + current_piece_rotation * 4)):1} 373 | ((y = (c >> 2) + current_piece_y)) 374 | ((x = (c & 3) + current_piece_x)) 375 | ((playfield[y] |= (current_piece_color << (x * 3)))) 376 | } 377 | } 378 | 379 | # this function takes row number as argument and checks if has empty cells 380 | line_full() { 381 | local row=${playfield[$1]} x 382 | for ((x = 0; x < PLAYFIELD_W; x++)) { 383 | ((((row >> (x * 3)) & 7) == 0)) && return 1 384 | } 385 | return 0 386 | } 387 | 388 | # this function goes through playfield array and eliminates lines without empty cells 389 | process_complete_lines() { 390 | local y complete_lines=0 391 | for ((y = PLAYFIELD_H - 1; y > -1; y--)) { 392 | line_full $y && { 393 | unset playfield[$y] 394 | ((complete_lines++)) 395 | } 396 | } 397 | for ((y = 0; y < complete_lines; y++)) { 398 | playfield=(0 ${playfield[@]}) 399 | } 400 | return $complete_lines 401 | } 402 | 403 | process_fallen_piece() { 404 | flatten_playfield 405 | process_complete_lines && return 406 | update_score $? 407 | redraw_playfield 408 | } 409 | 410 | move_piece() { 411 | # arguments: 1 - new x coordinate, 2 - new y coordinate 412 | # moves the piece to the new location if possible 413 | if new_piece_location_ok $1 $2 ; then # if new location is ok 414 | clear_current # let's wipe out piece current location 415 | current_piece_x=$1 # update x ... 416 | current_piece_y=$2 # ... and y of new location 417 | show_current # and draw piece in new location 418 | return 0 # nothing more to do here 419 | fi # if we could not move piece to new location 420 | (($2 == current_piece_y)) && return 0 # and this was not horizontal move 421 | process_fallen_piece # let's finalize this piece 422 | get_random_next # and start the new one 423 | return 1 424 | } 425 | 426 | cmd_right() { 427 | move_piece $((current_piece_x + 1)) $current_piece_y 428 | } 429 | 430 | cmd_left() { 431 | move_piece $((current_piece_x - 1)) $current_piece_y 432 | } 433 | 434 | cmd_rotate() { 435 | local available_rotations old_rotation new_rotation 436 | 437 | available_rotations=$((${#piece_data[$current_piece]} / 4)) # number of orientations for this piece 438 | old_rotation=$current_piece_rotation # preserve current orientation 439 | new_rotation=$(((old_rotation + 1) % available_rotations)) # calculate new orientation 440 | current_piece_rotation=$new_rotation # set orientation to new 441 | if new_piece_location_ok $current_piece_x $current_piece_y ; then # check if new orientation is ok 442 | current_piece_rotation=$old_rotation # if yes - restore old orientation 443 | clear_current # clear piece image 444 | current_piece_rotation=$new_rotation # set new orientation 445 | show_current # draw piece with new orientation 446 | else # if new orientation is not ok 447 | current_piece_rotation=$old_rotation # restore old orientation 448 | fi 449 | } 450 | 451 | cmd_down() { 452 | move_piece $current_piece_x $((current_piece_y + 1)) 453 | } 454 | 455 | cmd_drop() { 456 | # move piece all way down 457 | # this is example of do..while loop in bash 458 | # loop body is empty 459 | # loop condition is done at least once 460 | # loop runs until loop condition would return non zero exit code 461 | while move_piece $current_piece_x $((current_piece_y + 1)) ; do : ; done 462 | } 463 | 464 | stty_g=$(stty -g) # let's save terminal state ... 465 | 466 | at_exit() { 467 | kill $ticker_pid # let's kill ticker process ... 468 | xyprint $GAMEOVER_X $GAMEOVER_Y "Game over!" 469 | echo -e "$screen_buffer" # ... print final message ... 470 | show_cursor 471 | stty $stty_g # ... and restore terminal state 472 | } 473 | 474 | # this function runs in separate process 475 | # it sends SIGUSR1 signals to the main process with appropriate delay 476 | ticker() { 477 | # on SIGUSR1 delay should be decreased, this happens during level ups 478 | trap 'DELAY=$(($DELAY * $DELAY_FACTOR))' SIGUSR1 479 | trap exit TERM 480 | 481 | while sleep $((DELAY / 1000)).$(printf "%03d" $((DELAY % 1000))); do kill -SIGUSR1 $1 || exit; done 2>/dev/null 482 | } 483 | 484 | do_tick() { 485 | $tick_blocked && tick_scheduled=true && return 486 | cmd_down 487 | flush_screen 488 | } 489 | 490 | main() { 491 | local -u key a='' b='' esc_ch=$'\x1b' 492 | local cmd 493 | # commands is associative array, which maps pressed keys to commands, sent to controller 494 | local -A commands=([A]=cmd_rotate [C]=cmd_right [D]=cmd_left 495 | [_S]=cmd_rotate [_A]=cmd_left [_D]=cmd_right 496 | [_]=cmd_drop [_Q]=exit [_H]=toggle_help [_N]=toggle_next [_C]=toggle_color) 497 | 498 | trap at_exit EXIT 499 | trap do_tick SIGUSR1 500 | init 501 | ticker $$ & 502 | ticker_pid=$! 503 | tick_blocked=false 504 | tick_scheduled=false 505 | 506 | while read -s -n 1 key ; do 507 | case "$a$b$key" in 508 | "${esc_ch}["[ACD]) cmd=${commands[$key]} ;; # cursor key 509 | *${esc_ch}${esc_ch}) cmd=exit ;; # exit on 2 escapes 510 | *) cmd=${commands[_$key]:-} ;; # regular key. If space was pressed $key is empty 511 | esac 512 | a=$b # preserve previous keys 513 | b=$key 514 | [ -n "$cmd" ] && { 515 | tick_blocked=true 516 | $cmd 517 | tick_blocked=false 518 | $tick_scheduled && tick_scheduled=false && do_tick 519 | flush_screen 520 | } 521 | done 522 | } 523 | 524 | main 525 | -------------------------------------------------------------------------------- /examples/pipes.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_snake_case)] 3 | use cmd_lib::*; 4 | use std::io::Read; 5 | use std::{thread, time}; 6 | 7 | // Converted from bash script, original comments: 8 | // 9 | // pipes.sh: Animated pipes terminal screensaver. 10 | // https://github.com/pipeseroni/pipes.sh 11 | // 12 | // Copyright (c) 2015-2018 Pipeseroni/pipes.sh contributors 13 | // Copyright (c) 2013-2015 Yu-Jie Lin 14 | // Copyright (c) 2010 Matthew Simpson 15 | // 16 | // Permission is hereby granted, free of charge, to any person obtaining a copy 17 | // of this software and associated documentation files (the "Software"), to deal 18 | // in the Software without restriction, including without limitation the rights 19 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | // copies of the Software, and to permit persons to whom the Software is 21 | // furnished to do so, subject to the following conditions: 22 | // 23 | // The above copyright notice and this permission notice shall be included in 24 | // all copies or substantial portions of the Software. 25 | // 26 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | // SOFTWARE. 33 | 34 | const VERSION: &str = "1.3.0"; 35 | 36 | const M: i32 = 32768; // Bash RANDOM maximum + 1 37 | tls_init!(p, i32, 1); // number of pipes 38 | tls_init!(f, i32, 75); // frame rate 39 | tls_init!(s, i32, 13); // probability of straight fitting 40 | tls_init!(r, i32, 2000); // characters limit 41 | tls_init!(t, i32, 0); // iteration counter for -r character limit 42 | tls_init!(w, i32, 80); // terminal size 43 | tls_init!(h, i32, 24); 44 | 45 | // ab -> sets[][idx] = a*4 + b 46 | // 0: up, 1: right, 2: down, 3: left 47 | // 00 means going up , then going up -> ┃ 48 | // 12 means going right, then going down -> ┓ 49 | #[rustfmt::skip] 50 | tls_init!(sets, Vec, [ 51 | r"┃┏ ┓┛━┓ ┗┃┛┗ ┏━", 52 | r"│╭ ╮╯─╮ ╰│╯╰ ╭─", 53 | r"│┌ ┐┘─┐ └│┘└ ┌─", 54 | r"║╔ ╗╝═╗ ╚║╝╚ ╔═", 55 | r"|+ ++-+ +|++ +-", 56 | r"|/ \/-\ \|/\ /-", 57 | r".. .... .... ..", 58 | r".o oo.o o.oo o.", 59 | r"-\ /\|/ /-\/ \|", // railway 60 | r"╿┍ ┑┚╼┒ ┕╽┙┖ ┎╾", // knobby pipe 61 | ].iter().map(|ns| ns.to_string()).collect()); 62 | // rearranged all pipe chars into individual elements for easier access 63 | tls_init!(SETS, Vec, vec![]); 64 | 65 | // pipes' 66 | tls_init!(x, Vec, vec![]); // current position 67 | tls_init!(y, Vec, vec![]); 68 | tls_init!(l, Vec, vec![]); // current directions, 0: up, 1: right, 2: down, 3: left 69 | tls_init!(n, Vec, vec![]); // new directions 70 | tls_init!(v, Vec, vec![]); // current types 71 | tls_init!(c, Vec, vec![]); // current escape codes 72 | 73 | // selected pipes' 74 | tls_init!(V, Vec, vec![0]); // types (indexes to sets[]) 75 | tls_init!(C, Vec, vec![1, 2, 3, 4, 5, 6, 7, 0]); // color indices for tput setaf 76 | tls_init!(VN, i32, 1); // number of selected types 77 | tls_init!(CN, i32, 8); // number of selected colors 78 | tls_init!(E, Vec, vec![]); // pre-generated escape codes from BOLD, NOCOLOR, and C 79 | 80 | // switches 81 | tls_init!(RNDSTART, bool, false); // randomize starting position and direction 82 | tls_init!(BOLD, bool, true); 83 | tls_init!(NOCOLOR, bool, false); 84 | tls_init!(KEEPCT, bool, false); // keep pipe color and type 85 | 86 | fn prog_name() -> String { 87 | let arg0 = std::env::args().next().unwrap(); 88 | run_fun!(basename $arg0).unwrap() 89 | } 90 | 91 | // print help message in 72-char width 92 | fn print_help() { 93 | let prog = prog_name(); 94 | let max_type = tls_get!(sets).len() - 1; 95 | let cgap = " ".repeat(15 - format!("{}", tls_get!(COLORS)).chars().count()); 96 | let colors = run_fun!(tput colors).unwrap(); 97 | let term = std::env::var("TERM").unwrap(); 98 | #[rustfmt::skip] 99 | eprintln!(" 100 | Usage: {prog} [OPTION]... 101 | Animated pipes terminal screensaver. 102 | 103 | -p [1-] number of pipes (D=1) 104 | -t [0-{max_type}] pipe type (D=0) 105 | -t c[16 chars] custom pipe type 106 | -c [0-{colors}]{cgap}pipe color INDEX (TERM={term}), can be 107 | hexadecimal with '#' prefix 108 | (D=-c 1 -c 2 ... -c 7 -c 0) 109 | -f [20-100] framerate (D=75) 110 | -s [5-15] going straight probability, 1 in (D=13) 111 | -r [0-] reset after (D=2000) characters, 0 if no reset 112 | -R randomize starting position and direction 113 | -B no bold effect 114 | -C no color 115 | -K keep pipe color and type when crossing edges 116 | -h print this help message 117 | -v print version number 118 | 119 | Note: -t and -c can be used more than once."); 120 | } 121 | 122 | // parse command-line options 123 | // It depends on a valid COLORS which is set by _CP_init_termcap_vars 124 | fn parse() -> CmdResult { 125 | // test if $1 is a natural number in decimal, an integer >= 0 126 | fn is_N(arg_opt: Option) -> (bool, i32) { 127 | if let Some(arg) = arg_opt { 128 | if let Ok(vv) = arg.parse::() { 129 | return (vv >= 0, vv); 130 | } 131 | } 132 | (false, 0) 133 | } 134 | 135 | // test if $1 is a hexadecimal string 136 | fn is_hex(arg: &str) -> (bool, i32) { 137 | if let Ok(vv) = i32::from_str_radix(&arg, 16) { 138 | return (true, vv); 139 | } 140 | (false, 0) 141 | } 142 | 143 | // print error message for invalid argument to standard error, this 144 | // - mimics getopts error message 145 | // - use all positional parameters as error message 146 | // - has a newline appended 147 | // $arg and $OPTARG are the option name and argument set by getopts. 148 | fn pearg(arg: &str, msg: &str) -> ! { 149 | let arg0 = prog_name(); 150 | info!("{arg0}: -{arg} invalid argument; {msg}"); 151 | print_help(); 152 | std::process::exit(1) 153 | } 154 | 155 | let mut args = std::env::args().skip(1); 156 | while let Some(arg) = args.next() { 157 | match arg.as_str() { 158 | "-p" => { 159 | let (is_valid, vv) = is_N(args.next()); 160 | if is_valid && vv > 0 { 161 | tls_set!(p, |np| *np = vv); 162 | } else { 163 | pearg(&arg, "must be an integer and greater than 0"); 164 | } 165 | } 166 | "-t" => { 167 | let arg_opt = args.next(); 168 | let (is_valid, vv) = is_N(arg_opt.clone()); 169 | let arg_str = arg_opt.unwrap_or_default(); 170 | let len = tls_get!(sets).len() as i32; 171 | if arg_str.chars().count() == 16 { 172 | tls_set!(V, |nv| nv.push(len)); 173 | tls_set!(sets, |ns| ns.push(arg_str)); 174 | } else if is_valid && vv < len { 175 | tls_set!(V, |nv| nv.push(vv)); 176 | } else { 177 | pearg( 178 | &arg, 179 | &format!("must be an integer and from 0 to {}; or a custom type", len), 180 | ); 181 | } 182 | } 183 | "-c" => { 184 | let arg_opt = args.next(); 185 | let (is_valid, vv) = is_N(arg_opt.clone()); 186 | let arg_str = arg_opt.unwrap_or_default(); 187 | if arg_str.starts_with("#") { 188 | let (is_valid_hex, hv) = is_hex(&arg_str[1..]); 189 | if !is_valid_hex { 190 | pearg(&arg, "unrecognized hexadecimal string"); 191 | } 192 | if hv >= tls_get!(COLORS) { 193 | pearg( 194 | &arg, 195 | &format!("hexadecimal must be from #0 to {:X}", tls_get!(COLORS) - 1), 196 | ); 197 | } 198 | tls_set!(C, |nc| nc.push(hv)); 199 | } else if is_valid && vv < tls_get!(COLORS) { 200 | tls_set!(C, |nc| nc.push(vv)); 201 | } else { 202 | pearg( 203 | &arg, 204 | &format!( 205 | "must be an integer and from 0 to {}; 206 | or a hexadecimal string with # prefix", 207 | tls_get!(COLORS) - 1 208 | ), 209 | ); 210 | } 211 | } 212 | "-f" => { 213 | let (is_valid, vv) = is_N(args.next()); 214 | if is_valid && vv >= 20 && vv <= 100 { 215 | tls_set!(f, |nf| *nf = vv); 216 | } else { 217 | pearg(&arg, "must be an integer and from 20 to 100"); 218 | } 219 | } 220 | "-s" => { 221 | let (is_valid, vv) = is_N(args.next()); 222 | if is_valid && vv >= 5 && vv <= 15 { 223 | tls_set!(r, |nr| *nr = vv); 224 | } else { 225 | pearg(&arg, "must be a non-negative integer"); 226 | } 227 | } 228 | "-r" => { 229 | let (is_valid, vv) = is_N(args.next()); 230 | if is_valid && vv > 0 { 231 | tls_set!(r, |nr| *nr = vv); 232 | } else { 233 | pearg(&arg, "must be a non-negative integer"); 234 | } 235 | } 236 | "-R" => tls_set!(RNDSTART, |nr| *nr = true), 237 | "-B" => tls_set!(BOLD, |nb| *nb = false), 238 | "-C" => tls_set!(NOCOLOR, |nc| *nc = true), 239 | "-K" => tls_set!(KEEPCT, |nk| *nk = true), 240 | "-h" => { 241 | print_help(); 242 | std::process::exit(0); 243 | } 244 | "-v" => { 245 | let arg0 = std::env::args().next().unwrap(); 246 | let prog = run_fun!(basename $arg0)?; 247 | run_cmd!(echo $prog $VERSION)?; 248 | std::process::exit(0); 249 | } 250 | _ => { 251 | pearg( 252 | &arg, 253 | &format!("illegal arguments -- {}; no arguments allowed", arg), 254 | ); 255 | } 256 | } 257 | } 258 | Ok(()) 259 | } 260 | 261 | fn cleanup() -> CmdResult { 262 | let sgr0 = tls_get!(SGR0); 263 | run_cmd!( 264 | tput reset; // fix for konsole, see pipeseroni/pipes.sh#43 265 | tput rmcup; 266 | tput cnorm; 267 | stty echo; 268 | echo $sgr0; 269 | )?; 270 | 271 | Ok(()) 272 | } 273 | 274 | fn resize() -> CmdResult { 275 | let cols = run_fun!(tput cols)?.parse().unwrap(); 276 | let lines = run_fun!(tput lines)?.parse().unwrap(); 277 | tls_set!(w, |nw| *nw = cols); 278 | tls_set!(h, |nh| *nh = lines); 279 | Ok(()) 280 | } 281 | 282 | fn init_pipes() { 283 | // +_CP_init_pipes 284 | let mut ci = if tls_get!(KEEPCT) { 285 | 0 286 | } else { 287 | tls_get!(CN) * rand() / M 288 | }; 289 | 290 | let mut vi = if tls_get!(RNDSTART) { 291 | 0 292 | } else { 293 | tls_get!(VN) * rand() / M 294 | }; 295 | 296 | for _ in 0..tls_get!(p) as usize { 297 | tls_set!(n, |nn| nn.push(0)); 298 | tls_set!(l, |nl| nl.push(if tls_get!(RNDSTART) { 299 | rand() % 4 300 | } else { 301 | 0 302 | })); 303 | tls_set!(x, |nx| nx.push(if tls_get!(RNDSTART) { 304 | tls_get!(w) * rand() / M 305 | } else { 306 | tls_get!(w) / 2 307 | })); 308 | tls_set!(y, |ny| ny.push(if tls_get!(RNDSTART) { 309 | tls_get!(h) * rand() / M 310 | } else { 311 | tls_get!(h) / 2 312 | })); 313 | tls_set!(v, |nv| nv.push(tls_get!(V)[vi as usize])); 314 | tls_set!(c, |nc| nc.push(tls_get!(E)[ci as usize].clone())); 315 | ci = (ci + 1) % tls_get!(CN); 316 | vi = (vi + 1) % tls_get!(VN); 317 | } 318 | // -_CP_init_pipes 319 | } 320 | 321 | fn init_screen() -> CmdResult { 322 | run_cmd!( 323 | stty -echo -isig -icanon min 0 time 0; 324 | tput smcup; 325 | tput civis; 326 | tput clear; 327 | )?; 328 | resize()?; 329 | Ok(()) 330 | } 331 | 332 | tls_init!(SGR0, String, String::new()); 333 | tls_init!(SGR_BOLD, String, String::new()); 334 | tls_init!(COLORS, i32, 0); 335 | 336 | fn rand() -> i32 { 337 | run_fun!(bash -c r"echo $RANDOM").unwrap().parse().unwrap() 338 | } 339 | 340 | #[cmd_lib::main] 341 | fn main() -> CmdResult { 342 | // simple pre-check of TERM, tput's error message should be enough 343 | let term = std::env::var("TERM").unwrap(); 344 | run_cmd!(tput -T $term sgr0 >/dev/null)?; 345 | 346 | // +_CP_init_termcap_vars 347 | let colors = run_fun!(tput colors)?.parse().unwrap(); 348 | tls_set!(COLORS, |nc| *nc = colors); // COLORS - 1 == maximum color index for -c argument 349 | tls_set!(SGR0, |ns| *ns = run_fun!(tput sgr0).unwrap()); 350 | tls_set!(SGR_BOLD, |nb| *nb = run_fun!(tput bold).unwrap()); 351 | // -_CP_init_termcap_vars 352 | 353 | parse()?; 354 | 355 | // +_CP_init_VC 356 | // set default values if not by options 357 | tls_set!(VN, |vn| *vn = tls_get!(V).len() as i32); 358 | tls_set!(CN, |cn| *cn = tls_get!(C).len() as i32); 359 | // -_CP_init_VC 360 | 361 | // +_CP_init_E 362 | // generate E[] based on BOLD (SGR_BOLD), NOCOLOR, and C for each element in 363 | // C, a corresponding element in E[] = 364 | // SGR0 365 | // + SGR_BOLD, if BOLD 366 | // + tput setaf C, if !NOCOLOR 367 | for i in 0..(tls_get!(CN) as usize) { 368 | tls_set!(E, |ne| ne.push(tls_get!(SGR0))); 369 | if tls_get!(BOLD) { 370 | tls_set!(E, |ne| ne[i] += &tls_get!(SGR_BOLD)); 371 | } 372 | if !tls_get!(NOCOLOR) { 373 | let cc = tls_get!(C)[i]; 374 | let setaf = run_fun!(tput setaf $cc)?; 375 | tls_set!(E, |ne| ne[i] += &setaf); 376 | } 377 | } 378 | // -_CP_init_E 379 | 380 | // +_CP_init_SETS 381 | for i in 0..tls_get!(sets).len() { 382 | for j in 0..16 { 383 | let cc = tls_get!(sets)[i].chars().nth(j).unwrap(); 384 | tls_set!(SETS, |ns| ns.push(cc)); 385 | } 386 | } 387 | // -_CP_init_SETS 388 | 389 | init_screen()?; 390 | init_pipes(); 391 | 392 | loop { 393 | thread::sleep(time::Duration::from_millis(1000 / tls_get!(f) as u64)); 394 | let mut buffer = String::new(); 395 | if std::io::stdin().read_to_string(&mut buffer).is_ok() { 396 | match buffer.as_str() { 397 | "q" | "\u{1b}" | "\u{3}" => { 398 | cleanup()?; // q, ESC or Ctrl-C to exit 399 | break; 400 | } 401 | "P" => tls_set!(s, |ns| *ns = if *ns < 15 { *ns + 1 } else { *ns }), 402 | "O" => tls_set!(s, |ns| *ns = if *ns > 3 { *ns - 1 } else { *ns }), 403 | "F" => tls_set!(f, |nf| *nf = if *nf < 100 { *nf + 1 } else { *nf }), 404 | "D" => tls_set!(f, |nf| *nf = if *nf > 20 { *nf - 1 } else { *nf }), 405 | "B" => tls_set!(BOLD, |nb| *nb = !*nb), 406 | "C" => tls_set!(NOCOLOR, |nc| *nc = !*nc), 407 | "K" => tls_set!(KEEPCT, |nk| *nk = !*nk), 408 | _ => (), 409 | } 410 | } 411 | for i in 0..(tls_get!(p) as usize) { 412 | // New position: 413 | // l[] direction = 0: up, 1: right, 2: down, 3: left 414 | // +_CP_newpos 415 | if tls_get!(l)[i] % 2 == 1 { 416 | tls_set!(x, |nx| nx[i] += -tls_get!(l)[i] + 2); 417 | } else { 418 | tls_set!(y, |ny| ny[i] += tls_get!(l)[i] - 1); 419 | } 420 | // -_CP_newpos 421 | 422 | // Loop on edges (change color on loop): 423 | // +_CP_warp 424 | if !tls_get!(KEEPCT) { 425 | if tls_get!(x)[i] >= tls_get!(w) 426 | || tls_get!(x)[i] < 0 427 | || tls_get!(y)[i] >= tls_get!(h) 428 | || tls_get!(y)[i] < 0 429 | { 430 | tls_set!(c, |nc| nc[i] = 431 | tls_get!(E)[(tls_get!(CN) * rand() / M) as usize].clone()); 432 | tls_set!(v, |nv| nv[i] = 433 | tls_get!(V)[(tls_get!(VN) * rand() / M) as usize].clone()); 434 | } 435 | } 436 | tls_set!(x, |nx| nx[i] = (nx[i] + tls_get!(w)) % tls_get!(w)); 437 | tls_set!(y, |ny| ny[i] = (ny[i] + tls_get!(h)) % tls_get!(h)); 438 | // -_CP_warp 439 | 440 | // new turning direction: 441 | // $((s - 1)) in $s, going straight, therefore n[i] == l[i]; 442 | // and 1 in $s that pipe makes a right or left turn 443 | // 444 | // s * rand() / M - 1 == 0 445 | // n[i] == -1 446 | // => n[i] == l[i] + 1 or l[i] - 1 447 | // +_CP_newdir 448 | tls_set!(n, |nn| nn[i] = tls_get!(s) * rand() / M - 1); 449 | tls_set!(n, |nn| nn[i] = if nn[i] >= 0 { 450 | tls_get!(l)[i] 451 | } else { 452 | tls_get!(l)[i] + (2 * (rand() % 2) - 1) 453 | }); 454 | tls_set!(n, |nn| nn[i] = (nn[i] + 4) % 4); 455 | // -_CP_newdir 456 | 457 | // Print: 458 | // +_CP_print 459 | let ii = tls_get!(v)[i] * 16 + tls_get!(l)[i] * 4 + tls_get!(n)[i]; 460 | eprint!( 461 | "\u{1b}[{};{}H{}{}", 462 | tls_get!(y)[i] + 1, 463 | tls_get!(x)[i] + 1, 464 | tls_get!(c)[i], 465 | tls_get!(SETS)[ii as usize] 466 | ); 467 | // -_CP_print 468 | tls_set!(l, |nl| nl[i] = tls_get!(n)[i]); 469 | } 470 | 471 | if tls_get!(r) > 0 && tls_get!(t) * tls_get!(p) >= tls_get!(r) { 472 | run_cmd!( 473 | tput reset; 474 | tput civis; 475 | stty -echo -isig -icanon min 0 time 0; 476 | )?; 477 | tls_set!(t, |nt| *nt = 0); 478 | } else { 479 | tls_set!(t, |nt| *nt += 1); 480 | } 481 | } 482 | Ok(()) 483 | } 484 | -------------------------------------------------------------------------------- /src/child.rs: -------------------------------------------------------------------------------- 1 | use crate::{CmdResult, FunResult, process}; 2 | use crate::{info, warn}; 3 | use os_pipe::PipeReader; 4 | use std::any::Any; 5 | use std::fmt::Display; 6 | use std::io::{BufRead, BufReader, Error, Read, Result}; 7 | use std::process::{Child, ExitStatus}; 8 | use std::thread::JoinHandle; 9 | 10 | /// Representation of running or exited children processes, connected with pipes 11 | /// optionally. 12 | /// 13 | /// Calling [`spawn!`](../cmd_lib/macro.spawn.html) macro will return `Result` 14 | pub struct CmdChildren { 15 | children: Vec, 16 | ignore_error: bool, 17 | } 18 | 19 | impl CmdChildren { 20 | pub(crate) fn new(children: Vec, ignore_error: bool) -> Self { 21 | Self { 22 | children, 23 | ignore_error, 24 | } 25 | } 26 | 27 | pub(crate) fn into_fun_children(self) -> FunChildren { 28 | FunChildren { 29 | children: self.children, 30 | ignore_error: self.ignore_error, 31 | } 32 | } 33 | 34 | /// Waits for the children processes to exit completely, returning the status that they exited with. 35 | pub fn wait(&mut self) -> CmdResult { 36 | let last_child = self.children.pop().unwrap(); 37 | let last_child_res = last_child.wait(true); 38 | let other_children_res = Self::wait_children(&mut self.children); 39 | 40 | if self.ignore_error { 41 | Ok(()) 42 | } else { 43 | last_child_res.and(other_children_res) 44 | } 45 | } 46 | 47 | fn wait_children(children: &mut Vec) -> CmdResult { 48 | let mut ret = Ok(()); 49 | while let Some(child_handle) = children.pop() { 50 | if let Err(e) = child_handle.wait(false) { 51 | ret = Err(e); 52 | } 53 | } 54 | ret 55 | } 56 | 57 | /// Forces the children processes to exit. 58 | pub fn kill(&mut self) -> CmdResult { 59 | let mut ret = Ok(()); 60 | while let Some(child_handle) = self.children.pop() { 61 | if let Err(e) = child_handle.kill() { 62 | ret = Err(e); 63 | } 64 | } 65 | ret 66 | } 67 | 68 | /// Returns the OS-assigned process identifiers associated with these children processes 69 | pub fn pids(&self) -> Vec { 70 | self.children.iter().filter_map(|x| x.pid()).collect() 71 | } 72 | } 73 | 74 | /// Representation of running or exited children processes with output, connected with pipes 75 | /// optionally. 76 | /// 77 | /// Calling [spawn_with_output!](../cmd_lib/macro.spawn_with_output.html) macro will return `Result` 78 | pub struct FunChildren { 79 | children: Vec, 80 | ignore_error: bool, 81 | } 82 | 83 | impl FunChildren { 84 | /// Waits for the children processes to exit completely, returning the command result, stdout 85 | /// content string and stderr content string. 86 | pub fn wait_with_all(&mut self) -> (CmdResult, String, String) { 87 | self.inner_wait_with_all(true) 88 | } 89 | 90 | /// Waits for the children processes to exit completely, returning the stdout output. 91 | pub fn wait_with_output(&mut self) -> FunResult { 92 | let (res, stdout, _) = self.inner_wait_with_all(false); 93 | if let Err(e) = res 94 | && !self.ignore_error 95 | { 96 | return Err(e); 97 | } 98 | Ok(stdout) 99 | } 100 | 101 | /// Waits for the children processes to exit completely, and read all bytes from stdout into `buf`. 102 | pub fn wait_with_raw_output(&mut self, buf: &mut Vec) -> CmdResult { 103 | // wait for the last child result 104 | let handle = self.children.pop().unwrap(); 105 | let wait_last = handle.wait_with_raw_output(self.ignore_error, buf); 106 | match wait_last { 107 | Err(e) => { 108 | let _ = CmdChildren::wait_children(&mut self.children); 109 | Err(e) 110 | } 111 | Ok(_) => { 112 | let ret = CmdChildren::wait_children(&mut self.children); 113 | if self.ignore_error { Ok(()) } else { ret } 114 | } 115 | } 116 | } 117 | 118 | /// Pipes stdout from the last child in the pipeline to the given function, which runs in 119 | /// the current thread, then waits for all of the children to exit. 120 | /// 121 | /// If the function returns early, without reading from stdout until the last child exits, 122 | /// then the rest of stdout is automatically read and discarded to allow the child to finish. 123 | pub fn wait_with_pipe(&mut self, f: &mut dyn FnMut(&mut Box)) -> CmdResult { 124 | let mut last_child = self.children.pop().unwrap(); 125 | let mut stderr_thread = StderrThread::new( 126 | &last_child.cmd, 127 | &last_child.file, 128 | last_child.line, 129 | last_child.stderr.take(), 130 | false, 131 | ); 132 | let last_child_res = if let Some(stdout) = last_child.stdout { 133 | let mut stdout: Box = Box::new(stdout); 134 | f(&mut stdout); 135 | // The provided function may have left some of stdout unread. 136 | // Continue reading stdout on its behalf, until the child exits. 137 | let mut buf = vec![0; 65536]; 138 | let outcome: Box = loop { 139 | match last_child.handle { 140 | CmdChildHandle::Proc(ref mut child) => { 141 | if let Some(result) = child.try_wait().transpose() { 142 | break Box::new(ProcWaitOutcome::from(result)); 143 | } 144 | } 145 | CmdChildHandle::Thread(ref mut join_handle) => { 146 | if let Some(handle) = join_handle.take() { 147 | if handle.is_finished() { 148 | break Box::new(ThreadJoinOutcome::from(handle.join())); 149 | } else { 150 | join_handle.replace(handle); 151 | } 152 | } 153 | } 154 | CmdChildHandle::SyncFn => { 155 | break Box::new(SyncFnOutcome); 156 | } 157 | } 158 | let _ = stdout.read(&mut buf); 159 | }; 160 | outcome.to_io_result(&last_child.cmd, &last_child.file, last_child.line) 161 | } else { 162 | last_child.wait(true) 163 | }; 164 | let other_children_res = CmdChildren::wait_children(&mut self.children); 165 | let _ = stderr_thread.join(); 166 | 167 | if self.ignore_error { 168 | Ok(()) 169 | } else { 170 | last_child_res.and(other_children_res) 171 | } 172 | } 173 | 174 | /// Returns the OS-assigned process identifiers associated with these children processes. 175 | pub fn pids(&self) -> Vec { 176 | self.children.iter().filter_map(|x| x.pid()).collect() 177 | } 178 | 179 | fn inner_wait_with_all(&mut self, capture_stderr: bool) -> (CmdResult, String, String) { 180 | let mut stdout = Vec::new(); 181 | let mut stderr = String::new(); 182 | 183 | let last_child = self.children.pop().unwrap(); 184 | let last_child_res = last_child.wait_with_all(capture_stderr, &mut stdout, &mut stderr); 185 | let other_children_res = CmdChildren::wait_children(&mut self.children); 186 | let cmd_result = if self.ignore_error { 187 | Ok(()) 188 | } else { 189 | last_child_res.and(other_children_res) 190 | }; 191 | 192 | let mut stdout: String = String::from_utf8_lossy(&stdout).into(); 193 | if stdout.ends_with('\n') { 194 | stdout.pop(); 195 | } 196 | 197 | (cmd_result, stdout, stderr) 198 | } 199 | } 200 | 201 | pub(crate) struct CmdChild { 202 | handle: CmdChildHandle, 203 | cmd: String, 204 | file: String, 205 | line: u32, 206 | stdout: Option, 207 | stderr: Option, 208 | } 209 | 210 | impl CmdChild { 211 | pub(crate) fn new( 212 | handle: CmdChildHandle, 213 | cmd: String, 214 | file: String, 215 | line: u32, 216 | stdout: Option, 217 | stderr: Option, 218 | ) -> Self { 219 | Self { 220 | file, 221 | line, 222 | handle, 223 | cmd, 224 | stdout, 225 | stderr, 226 | } 227 | } 228 | 229 | fn wait(mut self, is_last: bool) -> CmdResult { 230 | let _stderr_thread = 231 | StderrThread::new(&self.cmd, &self.file, self.line, self.stderr.take(), false); 232 | let res = self.handle.wait(&self.cmd, &self.file, self.line); 233 | if let Err(e) = res 234 | && (is_last || process::pipefail_enabled()) 235 | { 236 | return Err(e); 237 | } 238 | Ok(()) 239 | } 240 | 241 | fn wait_with_raw_output(self, ignore_error: bool, stdout_buf: &mut Vec) -> CmdResult { 242 | let mut _stderr = String::new(); 243 | let res = self.wait_with_all(false, stdout_buf, &mut _stderr); 244 | if ignore_error { 245 | return Ok(()); 246 | } 247 | res 248 | } 249 | 250 | fn wait_with_all( 251 | mut self, 252 | capture_stderr: bool, 253 | stdout_buf: &mut Vec, 254 | stderr_buf: &mut String, 255 | ) -> CmdResult { 256 | let mut stderr_thread = StderrThread::new( 257 | &self.cmd, 258 | &self.file, 259 | self.line, 260 | self.stderr.take(), 261 | capture_stderr, 262 | ); 263 | let mut stdout_res = Ok(()); 264 | if let Some(mut stdout) = self.stdout.take() 265 | && let Err(e) = stdout.read_to_end(stdout_buf) 266 | { 267 | stdout_res = Err(e) 268 | } 269 | *stderr_buf = stderr_thread.join(); 270 | let wait_res = self.handle.wait(&self.cmd, &self.file, self.line); 271 | wait_res.and(stdout_res) 272 | } 273 | 274 | fn kill(self) -> CmdResult { 275 | self.handle.kill(&self.cmd, &self.file, self.line) 276 | } 277 | 278 | fn pid(&self) -> Option { 279 | self.handle.pid() 280 | } 281 | } 282 | 283 | pub(crate) enum CmdChildHandle { 284 | Proc(Child), 285 | Thread(Option>), 286 | SyncFn, 287 | } 288 | 289 | #[derive(Debug)] 290 | struct ProcWaitOutcome(std::io::Result); 291 | impl From> for ProcWaitOutcome { 292 | fn from(result: std::io::Result) -> Self { 293 | Self(result) 294 | } 295 | } 296 | impl Display for ProcWaitOutcome { 297 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 298 | match &self.0 { 299 | Ok(status) => { 300 | if status.success() { 301 | write!(f, "Command process succeeded") 302 | } else if let Some(code) = status.code() { 303 | write!(f, "Command process exited normally with status code {code}") 304 | } else { 305 | write!(f, "Command process exited abnormally: {status}") 306 | } 307 | } 308 | Err(error) => write!(f, "Failed to wait for command process: {error:?}"), 309 | } 310 | } 311 | } 312 | #[derive(Debug)] 313 | enum ThreadJoinOutcome { 314 | Ok, 315 | Err(std::io::Error), 316 | Panic(Box), 317 | } 318 | impl From> for ThreadJoinOutcome { 319 | fn from(result: std::thread::Result) -> Self { 320 | match result { 321 | Ok(Ok(())) => Self::Ok, 322 | Ok(Err(error)) => Self::Err(error), 323 | Err(panic) => Self::Panic(panic), 324 | } 325 | } 326 | } 327 | impl Display for ThreadJoinOutcome { 328 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 329 | match self { 330 | Self::Ok => write!(f, "Command thread succeeded"), 331 | Self::Err(error) => write!(f, "Command thread returned error: {error:?}"), 332 | Self::Panic(panic) => write!(f, "Command thread panicked: {panic:?}"), 333 | } 334 | } 335 | } 336 | #[derive(Debug)] 337 | struct SyncFnOutcome; 338 | impl Display for SyncFnOutcome { 339 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 340 | write!(f, "Command finished") 341 | } 342 | } 343 | trait ChildOutcome: Display { 344 | fn success(&self) -> bool; 345 | fn to_io_result(&self, cmd: &str, file: &str, line: u32) -> std::io::Result<()> { 346 | if self.success() { 347 | Ok(()) 348 | } else { 349 | Err(Error::other(format!( 350 | "Running [{cmd}] exited with error; {self} at {file}:{line}" 351 | ))) 352 | } 353 | } 354 | } 355 | impl ChildOutcome for ProcWaitOutcome { 356 | fn success(&self) -> bool { 357 | self.0.as_ref().is_ok_and(|status| status.success()) 358 | } 359 | } 360 | impl ChildOutcome for ThreadJoinOutcome { 361 | fn success(&self) -> bool { 362 | matches!(self, Self::Ok) 363 | } 364 | } 365 | impl ChildOutcome for SyncFnOutcome { 366 | fn success(&self) -> bool { 367 | true 368 | } 369 | } 370 | 371 | impl CmdChildHandle { 372 | fn wait(self, cmd: &str, file: &str, line: u32) -> CmdResult { 373 | let outcome: Box = match self { 374 | CmdChildHandle::Proc(mut proc) => Box::new(ProcWaitOutcome::from(proc.wait())), 375 | CmdChildHandle::Thread(mut thread) => { 376 | if let Some(thread) = thread.take() { 377 | Box::new(ThreadJoinOutcome::from(thread.join())) 378 | } else { 379 | unreachable!() 380 | } 381 | } 382 | CmdChildHandle::SyncFn => return Ok(()), 383 | }; 384 | outcome.to_io_result(cmd, file, line) 385 | } 386 | 387 | fn kill(self, cmd: &str, file: &str, line: u32) -> CmdResult { 388 | match self { 389 | CmdChildHandle::Proc(mut proc) => proc.kill().map_err(|e| { 390 | Error::new( 391 | e.kind(), 392 | format!("Killing process [{cmd}] failed with error: {e} at {file}:{line}"), 393 | ) 394 | }), 395 | CmdChildHandle::Thread(_thread) => Err(Error::other(format!( 396 | "Killing thread [{cmd}] failed: not supported at {file}:{line}" 397 | ))), 398 | CmdChildHandle::SyncFn => Ok(()), 399 | } 400 | } 401 | 402 | fn pid(&self) -> Option { 403 | match self { 404 | CmdChildHandle::Proc(proc) => Some(proc.id()), 405 | _ => None, 406 | } 407 | } 408 | } 409 | 410 | struct StderrThread { 411 | thread: Option>, 412 | cmd: String, 413 | file: String, 414 | line: u32, 415 | } 416 | 417 | impl StderrThread { 418 | fn new(cmd: &str, file: &str, line: u32, stderr: Option, capture: bool) -> Self { 419 | #[cfg(feature = "tracing")] 420 | let span = tracing::Span::current(); 421 | if let Some(stderr) = stderr { 422 | let file_ = file.to_owned(); 423 | let thread = std::thread::spawn(move || { 424 | #[cfg(feature = "tracing")] 425 | let _entered = span.enter(); 426 | if capture { 427 | let mut output = String::new(); 428 | BufReader::new(stderr) 429 | .lines() 430 | .map_while(Result::ok) 431 | .for_each(|line| { 432 | if !output.is_empty() { 433 | output.push('\n'); 434 | } 435 | output.push_str(&line); 436 | }); 437 | return output; 438 | } 439 | 440 | // Log output one line at a time, including progress output separated by CR 441 | let mut reader = BufReader::new(stderr); 442 | let mut buffer = vec![]; 443 | loop { 444 | // Unconditionally try to read more data, since the BufReader buffer is empty 445 | let result = match reader.fill_buf() { 446 | Ok(buffer) => buffer, 447 | Err(error) => { 448 | warn!("Error reading from child process: {error:?} at {file_}:{line}"); 449 | break; 450 | } 451 | }; 452 | // Add the result onto our own buffer 453 | buffer.extend(result); 454 | // Empty the BufReader 455 | let read_len = result.len(); 456 | reader.consume(read_len); 457 | 458 | // Log output. Take whole “lines” at every LF or CR (for progress bars etc), 459 | // but leave any incomplete lines in our buffer so we can try to complete them. 460 | while let Some(offset) = buffer.iter().position(|&b| b == b'\n' || b == b'\r') { 461 | let line = &buffer[..offset]; 462 | let line = str::from_utf8(line).map_err(|_| line); 463 | match line { 464 | Ok(string) => info!("{string}"), 465 | Err(bytes) => info!("{bytes:?}"), 466 | } 467 | buffer = buffer.split_off(offset + 1); 468 | } 469 | 470 | if read_len == 0 { 471 | break; 472 | } 473 | } 474 | 475 | // Log any remaining incomplete line 476 | if !buffer.is_empty() { 477 | let line = &buffer; 478 | let line = str::from_utf8(line).map_err(|_| line); 479 | match line { 480 | Ok(string) => info!("{string}"), 481 | Err(bytes) => info!("{bytes:?}"), 482 | } 483 | } 484 | 485 | "".to_owned() 486 | }); 487 | Self { 488 | cmd: cmd.into(), 489 | file: file.into(), 490 | line, 491 | thread: Some(thread), 492 | } 493 | } else { 494 | Self { 495 | cmd: cmd.into(), 496 | file: file.into(), 497 | line, 498 | thread: None, 499 | } 500 | } 501 | } 502 | 503 | fn join(&mut self) -> String { 504 | if let Some(thread) = self.thread.take() { 505 | match thread.join() { 506 | Err(e) => { 507 | warn!( 508 | "Running [{}] stderr thread joined with error: {:?} at {}:{}", 509 | self.cmd, e, self.file, self.line 510 | ); 511 | } 512 | Ok(output) => return output, 513 | } 514 | } 515 | "".into() 516 | } 517 | } 518 | 519 | impl Drop for StderrThread { 520 | fn drop(&mut self) { 521 | self.join(); 522 | } 523 | } 524 | -------------------------------------------------------------------------------- /macros/src/lexer.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{ParseArg, Parser}; 2 | use proc_macro_error2::abort; 3 | use proc_macro2::{Delimiter, Ident, Literal, Span, TokenStream, TokenTree, token_stream}; 4 | use quote::quote; 5 | use std::iter::Peekable; 6 | use std::str::Chars; 7 | 8 | /// Scan string literal to tokenstream, used by most of the macros 9 | /// 10 | /// - support $var, ${var} or ${var:fmt} for interpolation, where `fmt` can be any 11 | /// of the standard Rust formatting specifiers (e.g., `?`, `x`, `X`, `o`, `b`, `p`, `e`, `E`). 12 | /// - to escape '$' itself, use "$$" 13 | /// - support normal rust character escapes: 14 | /// https://doc.rust-lang.org/reference/tokens.html#ascii-escapes 15 | pub fn scan_str_lit(lit: &Literal) -> TokenStream { 16 | let s = lit.to_string(); 17 | 18 | // If the literal is not a string (e.g., a number literal), treat it as a direct CmdString. 19 | if !s.starts_with('\"') { 20 | return quote!(::cmd_lib::CmdString::from(#lit)); 21 | } 22 | 23 | // Extract the inner string by trimming the surrounding quotes. 24 | let inner_str = &s[1..s.len() - 1]; 25 | let mut chars = inner_str.chars().peekable(); 26 | let mut output = quote!(::cmd_lib::CmdString::default()); 27 | let mut current_literal_part = String::new(); 28 | 29 | // Helper function to append the accumulated literal part to the output TokenStream 30 | // and clear the current_literal_part. 31 | let seal_current_literal_part = |output: &mut TokenStream, last_part: &mut String| { 32 | if !last_part.is_empty() { 33 | let lit_str = format!("\"{}\"", last_part); 34 | // It's safe to unwrap parse_str because we are constructing a valid string literal. 35 | let literal_token = syn::parse_str::(&lit_str).unwrap(); 36 | output.extend(quote!(.append(#literal_token))); 37 | last_part.clear(); 38 | } 39 | }; 40 | 41 | while let Some(ch) = chars.next() { 42 | if ch == '$' { 43 | // Handle "$$" for escaping '$' 44 | if chars.peek() == Some(&'$') { 45 | chars.next(); // Consume the second '$' 46 | current_literal_part.push('$'); 47 | continue; 48 | } 49 | 50 | // Before handling a variable, append any accumulated literal part. 51 | seal_current_literal_part(&mut output, &mut current_literal_part); 52 | 53 | let mut format_specifier = String::new(); // To store the fmt specifier (e.g., "?", "x", "#x") 54 | let mut is_braced_interpolation = false; 55 | 56 | // Check for '{' to start a braced interpolation 57 | if chars.peek() == Some(&'{') { 58 | is_braced_interpolation = true; 59 | chars.next(); // Consume '{' 60 | } 61 | 62 | let var_name = parse_variable_name(&mut chars); 63 | 64 | if is_braced_interpolation { 65 | // If it's braced, we might have a format specifier or it might just be empty braces. 66 | if chars.peek() == Some(&':') { 67 | chars.next(); // Consume ':' 68 | // Read the format specifier until '}' 69 | while let Some(&c) = chars.peek() { 70 | if c == '}' { 71 | break; 72 | } 73 | format_specifier.push(c); 74 | chars.next(); // Consume the character of the specifier 75 | } 76 | } 77 | 78 | // Expect '}' to close the braced interpolation 79 | if chars.next() != Some('}') { 80 | abort!(lit.span(), "bad substitution: expected '}'"); 81 | } 82 | } 83 | 84 | if !var_name.is_empty() { 85 | let var_ident = syn::parse_str::(&var_name).unwrap(); 86 | 87 | // To correctly handle all format specifiers (like {:02X}), we need to insert the 88 | // entire format string *as a literal* into the format! macro. 89 | // The `format_specifier` string itself needs to be embedded. 90 | let format_macro_call = if format_specifier.is_empty() { 91 | quote! { 92 | .append(format!("{}", #var_ident)) 93 | } 94 | } else { 95 | let format_literal_str = format!("{{:{}}}", format_specifier); 96 | let format_literal_token = Literal::string(&format_literal_str); 97 | quote! { 98 | .append(format!(#format_literal_token, #var_ident)) 99 | } 100 | }; 101 | output.extend(format_macro_call); 102 | } else { 103 | // This covers cases like "${}" or "${:?}" with empty variable name 104 | output.extend(quote!(.append("$"))); 105 | } 106 | } else { 107 | current_literal_part.push(ch); 108 | } 109 | } 110 | 111 | // Append any remaining literal part after the loop finishes. 112 | seal_current_literal_part(&mut output, &mut current_literal_part); 113 | output 114 | } 115 | 116 | /// Parses a variable name from the character iterator. 117 | /// A variable name consists of alphanumeric characters and underscores, 118 | /// and cannot start with a digit. 119 | fn parse_variable_name(chars: &mut Peekable>) -> String { 120 | let mut var = String::new(); 121 | while let Some(&c) = chars.peek() { 122 | if !(c.is_ascii_alphanumeric() || c == '_') { 123 | break; 124 | } 125 | if var.is_empty() && c.is_ascii_digit() { 126 | // Variable names cannot start with a digit 127 | break; 128 | } 129 | var.push(c); 130 | chars.next(); // Consume the character 131 | } 132 | var 133 | } 134 | 135 | enum SepToken { 136 | Space, 137 | SemiColon, 138 | Pipe, 139 | } 140 | 141 | enum RedirectFd { 142 | Stdin, 143 | Stdout { append: bool }, 144 | Stderr { append: bool }, 145 | StdoutErr { append: bool }, 146 | } 147 | 148 | pub struct Lexer { 149 | iter: TokenStreamPeekable, 150 | args: Vec, 151 | last_arg_str: TokenStream, 152 | last_redirect: Option<(RedirectFd, Span)>, 153 | seen_redirect: (bool, bool, bool), 154 | } 155 | 156 | impl Lexer { 157 | pub fn new(input: TokenStream) -> Self { 158 | Self { 159 | args: vec![], 160 | last_arg_str: TokenStream::new(), 161 | last_redirect: None, 162 | seen_redirect: (false, false, false), 163 | iter: TokenStreamPeekable { 164 | peekable: input.into_iter().peekable(), 165 | span: Span::call_site(), 166 | }, 167 | } 168 | } 169 | 170 | pub fn scan(mut self) -> Parser> { 171 | while let Some(item) = self.iter.next() { 172 | match item { 173 | TokenTree::Group(_) => { 174 | abort!(self.iter.span(), "grouping is only allowed for variables"); 175 | } 176 | TokenTree::Literal(lit) => { 177 | self.scan_literal(lit); 178 | } 179 | TokenTree::Ident(ident) => { 180 | let s = ident.to_string(); 181 | self.extend_last_arg(quote!(#s)); 182 | } 183 | TokenTree::Punct(punct) => { 184 | let ch = punct.as_char(); 185 | if ch == ';' { 186 | self.add_arg_with_token(SepToken::SemiColon, self.iter.span()); 187 | } else if ch == '|' { 188 | self.scan_pipe(); 189 | } else if ch == '<' { 190 | self.set_redirect(self.iter.span(), RedirectFd::Stdin); 191 | } else if ch == '>' { 192 | self.scan_redirect_out(1); 193 | } else if ch == '&' { 194 | self.scan_ampersand(); 195 | } else if ch == '$' { 196 | self.scan_dollar(); 197 | } else { 198 | let s = ch.to_string(); 199 | self.extend_last_arg(quote!(#s)); 200 | } 201 | } 202 | } 203 | 204 | if self.iter.peek_no_gap().is_none() && !self.last_arg_str.is_empty() { 205 | self.add_arg_with_token(SepToken::Space, self.iter.span()); 206 | } 207 | } 208 | self.add_arg_with_token(SepToken::Space, self.iter.span()); 209 | Parser::from(self.args.into_iter().peekable()) 210 | } 211 | 212 | fn add_arg_with_token(&mut self, token: SepToken, token_span: Span) { 213 | let last_arg_str = &self.last_arg_str; 214 | if let Some((redirect, span)) = self.last_redirect.take() { 215 | if last_arg_str.is_empty() { 216 | abort!(span, "wrong redirection format: missing target"); 217 | } 218 | 219 | let mut stdouterr = false; 220 | let (fd, append) = match redirect { 221 | RedirectFd::Stdin => (0, false), 222 | RedirectFd::Stdout { append } => (1, append), 223 | RedirectFd::Stderr { append } => (2, append), 224 | RedirectFd::StdoutErr { append } => { 225 | stdouterr = true; 226 | (1, append) 227 | } 228 | }; 229 | self.args 230 | .push(ParseArg::RedirectFile(fd, quote!(#last_arg_str), append)); 231 | if stdouterr { 232 | self.args.push(ParseArg::RedirectFd(2, 1)); 233 | } 234 | } else if !last_arg_str.is_empty() { 235 | self.args.push(ParseArg::ArgStr(quote!(#last_arg_str))); 236 | } 237 | let mut new_redirect = (false, false, false); 238 | match token { 239 | SepToken::Space => new_redirect = self.seen_redirect, 240 | SepToken::SemiColon => self.args.push(ParseArg::Semicolon), 241 | SepToken::Pipe => { 242 | Self::check_set_redirect(&mut self.seen_redirect.1, "stdout", token_span); 243 | self.args.push(ParseArg::Pipe); 244 | new_redirect.0 = true; 245 | } 246 | } 247 | self.seen_redirect = new_redirect; 248 | self.last_arg_str = TokenStream::new(); 249 | } 250 | 251 | fn extend_last_arg(&mut self, stream: TokenStream) { 252 | if self.last_arg_str.is_empty() { 253 | self.last_arg_str = quote!(::cmd_lib::CmdString::default()); 254 | } 255 | self.last_arg_str.extend(quote!(.append(#stream))); 256 | } 257 | 258 | fn check_set_redirect(redirect: &mut bool, name: &str, span: Span) { 259 | if *redirect { 260 | abort!(span, "already set {} redirection", name); 261 | } 262 | *redirect = true; 263 | } 264 | 265 | fn set_redirect(&mut self, span: Span, fd: RedirectFd) { 266 | if self.last_redirect.is_some() { 267 | abort!(span, "wrong double redirection format"); 268 | } 269 | match fd { 270 | RedirectFd::Stdin => Self::check_set_redirect(&mut self.seen_redirect.0, "stdin", span), 271 | RedirectFd::Stdout { append: _ } => { 272 | Self::check_set_redirect(&mut self.seen_redirect.1, "stdout", span) 273 | } 274 | RedirectFd::Stderr { append: _ } => { 275 | Self::check_set_redirect(&mut self.seen_redirect.2, "stderr", span) 276 | } 277 | RedirectFd::StdoutErr { append: _ } => { 278 | Self::check_set_redirect(&mut self.seen_redirect.1, "stdout", span); 279 | Self::check_set_redirect(&mut self.seen_redirect.2, "stderr", span); 280 | } 281 | } 282 | self.last_redirect = Some((fd, span)); 283 | } 284 | 285 | fn scan_literal(&mut self, lit: Literal) { 286 | let s = lit.to_string(); 287 | if s.starts_with('\"') || s.starts_with('r') { 288 | // string literal 289 | let ss = scan_str_lit(&lit); 290 | self.extend_last_arg(quote!(#ss.into_os_string())); 291 | } else { 292 | let mut is_redirect = false; 293 | if (s == "1" || s == "2") 294 | && let Some(TokenTree::Punct(p)) = self.iter.peek_no_gap() 295 | && p.as_char() == '>' 296 | { 297 | self.iter.next(); 298 | self.scan_redirect_out(if s == "1" { 1 } else { 2 }); 299 | is_redirect = true; 300 | } 301 | if !is_redirect { 302 | self.extend_last_arg(quote!(#s)); 303 | } 304 | } 305 | } 306 | 307 | fn scan_pipe(&mut self) { 308 | if let Some(TokenTree::Punct(p)) = self.iter.peek_no_gap() 309 | && p.as_char() == '&' 310 | { 311 | if let Some(ref redirect) = self.last_redirect { 312 | abort!(redirect.1, "invalid '&': found previous redirect"); 313 | } 314 | Self::check_set_redirect(&mut self.seen_redirect.2, "stderr", p.span()); 315 | self.args.push(ParseArg::RedirectFd(2, 1)); 316 | self.iter.next(); 317 | } 318 | 319 | // expect new command 320 | match self.iter.peek() { 321 | Some(TokenTree::Punct(np)) => { 322 | if np.as_char() == '|' || np.as_char() == ';' { 323 | abort!(np.span(), "expect new command after '|'"); 324 | } 325 | } 326 | None => { 327 | abort!(self.iter.span(), "expect new command after '|'"); 328 | } 329 | _ => {} 330 | } 331 | self.add_arg_with_token(SepToken::Pipe, self.iter.span()); 332 | } 333 | 334 | fn scan_redirect_out(&mut self, fd: i32) { 335 | let append = self.check_append(); 336 | self.set_redirect( 337 | self.iter.span(), 338 | if fd == 1 { 339 | RedirectFd::Stdout { append } 340 | } else { 341 | RedirectFd::Stderr { append } 342 | }, 343 | ); 344 | if let Some(TokenTree::Punct(p)) = self.iter.peek_no_gap() 345 | && p.as_char() == '&' 346 | { 347 | if append { 348 | abort!(p.span(), "raw fd not allowed for append redirection"); 349 | } 350 | self.iter.next(); 351 | if let Some(TokenTree::Literal(lit)) = self.iter.peek_no_gap() { 352 | let s = lit.to_string(); 353 | if s.starts_with('\"') || s.starts_with('r') { 354 | abort!(lit.span(), "invalid literal string after &"); 355 | } 356 | if &s == "1" { 357 | self.args.push(ParseArg::RedirectFd(fd, 1)); 358 | } else if &s == "2" { 359 | self.args.push(ParseArg::RedirectFd(fd, 2)); 360 | } else { 361 | abort!(lit.span(), "Only &1 or &2 is supported"); 362 | } 363 | self.last_redirect = None; 364 | self.iter.next(); 365 | } else { 366 | abort!(self.iter.span(), "expect &1 or &2"); 367 | } 368 | } 369 | } 370 | 371 | fn scan_ampersand(&mut self) { 372 | if let Some(tt) = self.iter.peek_no_gap() { 373 | if let TokenTree::Punct(p) = tt { 374 | let span = p.span(); 375 | if p.as_char() == '>' { 376 | self.iter.next(); 377 | let append = self.check_append(); 378 | self.set_redirect(span, RedirectFd::StdoutErr { append }); 379 | } else { 380 | abort!(span, "invalid punctuation"); 381 | } 382 | } else { 383 | abort!(tt.span(), "invalid format after '&'"); 384 | } 385 | } else if self.last_redirect.is_some() { 386 | abort!( 387 | self.iter.span(), 388 | "wrong redirection format: no spacing permitted before '&'" 389 | ); 390 | } else if self.iter.peek().is_some() { 391 | abort!(self.iter.span(), "invalid spacing after '&'"); 392 | } else { 393 | abort!(self.iter.span(), "invalid '&' at the end"); 394 | } 395 | } 396 | 397 | fn scan_dollar(&mut self) { 398 | let peek_no_gap = self.iter.peek_no_gap().map(|tt| tt.to_owned()); 399 | // let peek_no_gap = None; 400 | if let Some(TokenTree::Ident(var)) = peek_no_gap { 401 | self.extend_last_arg(quote!(#var.as_os_str())); 402 | } else if let Some(TokenTree::Group(g)) = peek_no_gap { 403 | if g.delimiter() != Delimiter::Brace && g.delimiter() != Delimiter::Bracket { 404 | abort!( 405 | g.span(), 406 | "invalid grouping: found {:?}, only \"brace/bracket\" is allowed", 407 | format!("{:?}", g.delimiter()).to_lowercase() 408 | ); 409 | } 410 | let mut found_var = false; 411 | for tt in g.stream() { 412 | let span = tt.span(); 413 | if let TokenTree::Ident(ref var) = tt { 414 | if found_var { 415 | abort!(span, "more than one variable in grouping"); 416 | } 417 | if g.delimiter() == Delimiter::Brace { 418 | self.extend_last_arg(quote!(#var.as_os_str())); 419 | } else { 420 | if !self.last_arg_str.is_empty() { 421 | abort!( 422 | span, 423 | "vector variable cannot have content immediately before it" 424 | ); 425 | } 426 | self.args.push(ParseArg::ArgVec(quote!(#var))); 427 | } 428 | found_var = true; 429 | } else { 430 | abort!(span, "invalid grouping: extra tokens"); 431 | } 432 | } 433 | } else { 434 | abort!(self.iter.span(), "invalid token after $"); 435 | } 436 | self.iter.next(); 437 | } 438 | 439 | fn check_append(&mut self) -> bool { 440 | let mut append = false; 441 | if let Some(TokenTree::Punct(p)) = self.iter.peek_no_gap() 442 | && p.as_char() == '>' 443 | { 444 | append = true; 445 | self.iter.next(); 446 | } 447 | append 448 | } 449 | } 450 | 451 | struct TokenStreamPeekable> { 452 | peekable: Peekable, 453 | span: Span, 454 | } 455 | 456 | impl> Iterator for TokenStreamPeekable { 457 | type Item = I::Item; 458 | fn next(&mut self) -> Option { 459 | if let Some(tt) = self.peekable.next() { 460 | self.span = tt.span(); 461 | Some(tt) 462 | } else { 463 | None 464 | } 465 | } 466 | } 467 | 468 | impl> TokenStreamPeekable { 469 | fn peek(&mut self) -> Option<&TokenTree> { 470 | self.peekable.peek() 471 | } 472 | 473 | // peek next token which has no spaces between 474 | fn peek_no_gap(&mut self) -> Option<&TokenTree> { 475 | match self.peekable.peek() { 476 | None => None, 477 | Some(item) => { 478 | if self.span.end() == item.span().start() { 479 | Some(item) 480 | } else { 481 | None 482 | } 483 | } 484 | } 485 | } 486 | 487 | fn span(&self) -> Span { 488 | self.span 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /examples/tetris.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | use cmd_lib::*; 3 | use std::io::Read; 4 | use std::{thread, time}; 5 | 6 | // Tetris game converted from bash version: 7 | // https://github.com/kt97679/tetris 8 | // @6fcb9400e7808189869efd4b745febed81313949 9 | 10 | // Original comments: 11 | // Tetris game written in pure bash 12 | // I tried to mimic as close as possible original tetris game 13 | // which was implemented on old soviet DVK computers (PDP-11 clones) 14 | // 15 | // Videos of this tetris can be found here: 16 | // 17 | // http://www.youtube.com/watch?v=O0gAgQQHFcQ 18 | // http://www.youtube.com/watch?v=iIQc1F3UuV4 19 | // 20 | // This script was created on ubuntu 13.04 x64 and bash 4.2.45(1)-release. 21 | // It was not tested on other unix like operating systems. 22 | // 23 | // Enjoy :-)! 24 | // 25 | // Author: Kirill Timofeev 26 | // 27 | // This program is free software. It comes without any warranty, to the extent 28 | // permitted by applicable law. You can redistribute it and/or modify it under 29 | // the terms of the Do What The Fuck You Want To Public License, Version 2, as 30 | // published by Sam Hocevar. See http://www.wtfpl.net/ for more details. 31 | 32 | tls_init!(DELAY, f64, 1.0); // initial delay between piece movements 33 | const DELAY_FACTOR: f64 = 0.8; // this value controld delay decrease for each level up 34 | 35 | // color codes 36 | const RED: i32 = 1; 37 | const GREEN: i32 = 2; 38 | const YELLOW: i32 = 3; 39 | const BLUE: i32 = 4; 40 | const FUCHSIA: i32 = 5; 41 | const CYAN: i32 = 6; 42 | const WHITE: i32 = 7; 43 | 44 | // Location and size of playfield, color of border 45 | const PLAYFIELD_W: i32 = 10; 46 | const PLAYFIELD_H: i32 = 20; 47 | const PLAYFIELD_X: i32 = 30; 48 | const PLAYFIELD_Y: i32 = 1; 49 | const BORDER_COLOR: i32 = YELLOW; 50 | 51 | // Location and color of SCORE information 52 | const SCORE_X: i32 = 1; 53 | const SCORE_Y: i32 = 2; 54 | const SCORE_COLOR: i32 = GREEN; 55 | 56 | // Location and color of help information 57 | const HELP_X: i32 = 58; 58 | const HELP_Y: i32 = 1; 59 | const HELP_COLOR: i32 = CYAN; 60 | 61 | // Next piece location 62 | const NEXT_X: i32 = 14; 63 | const NEXT_Y: i32 = 11; 64 | 65 | // Location of "game over" in the end of the game 66 | const GAMEOVER_X: i32 = 1; 67 | const GAMEOVER_Y: i32 = PLAYFIELD_H + 3; 68 | 69 | // Intervals after which game level (and game speed) is increased 70 | const LEVEL_UP: i32 = 20; 71 | 72 | const colors: [i32; 7] = [RED, GREEN, YELLOW, BLUE, FUCHSIA, CYAN, WHITE]; 73 | 74 | const empty_cell: &str = " ."; // how we draw empty cell 75 | const filled_cell: &str = "[]"; // how we draw filled cell 76 | 77 | tls_init!(use_color, bool, true); // true if we use color, false if not 78 | tls_init!(score, i32, 0); // score variable initialization 79 | tls_init!(level, i32, 1); // level variable initialization 80 | // completed lines counter initialization 81 | // screen_buffer is variable, that accumulates all screen changes 82 | // this variable is printed in controller once per game cycle 83 | tls_init!(lines_completed, i32, 0); 84 | tls_init!(screen_buffer, String, "".to_string()); 85 | 86 | fn puts(changes: &str) { 87 | tls_set!(screen_buffer, |s| s.push_str(changes)); 88 | } 89 | 90 | fn flush_screen() { 91 | eprint!("{}", tls_get!(screen_buffer)); 92 | tls_set!(screen_buffer, |s| s.clear()); 93 | } 94 | 95 | const ESC: char = '\x1b'; // escape key, '\033' in bash or c 96 | 97 | // move cursor to (x,y) and print string 98 | // (1,1) is upper left corner of the screen 99 | fn xyprint(x: i32, y: i32, s: &str) { 100 | puts(&format!("{}[{};{}H{}", ESC, y, x, s)); 101 | } 102 | 103 | fn show_cursor() { 104 | eprint!("{}[?25h", ESC); 105 | } 106 | 107 | fn hide_cursor() { 108 | eprint!("{}[?25l", ESC); 109 | } 110 | 111 | // foreground color 112 | fn set_fg(color: i32) { 113 | if tls_get!(use_color) { 114 | puts(&format!("{}[3{}m", ESC, color)); 115 | } 116 | } 117 | 118 | // background color 119 | fn set_bg(color: i32) { 120 | if tls_get!(use_color) { 121 | puts(&format!("{}[4{}m", ESC, color)); 122 | } 123 | } 124 | 125 | fn reset_colors() { 126 | puts(&format!("{}[0m", ESC)); 127 | } 128 | 129 | fn set_bold() { 130 | puts(&format!("{}[1m", ESC)); 131 | } 132 | 133 | // playfield is an array, each row is represented by integer 134 | // each cell occupies 3 bits (empty if 0, other values encode color) 135 | // playfield is initialized with 0s (empty cells) 136 | tls_init!( 137 | playfield, 138 | [i32; PLAYFIELD_H as usize], 139 | [0; PLAYFIELD_H as usize] 140 | ); 141 | 142 | fn redraw_playfield() { 143 | for y in 0..PLAYFIELD_H { 144 | xyprint(PLAYFIELD_X, PLAYFIELD_Y + y, ""); 145 | for x in 0..PLAYFIELD_W { 146 | let color = (tls_get!(playfield)[y as usize] >> (x * 3)) & 7; 147 | if color == 0 { 148 | puts(empty_cell); 149 | } else { 150 | set_fg(color); 151 | set_bg(color); 152 | puts(filled_cell); 153 | reset_colors(); 154 | } 155 | } 156 | } 157 | } 158 | 159 | // Arguments: lines - number of completed lines 160 | fn update_score(lines: i32) { 161 | tls_set!(lines_completed, |l| *l += lines); 162 | // Unfortunately I don't know scoring algorithm of original tetris 163 | // Here score is incremented with squared number of lines completed 164 | // this seems reasonable since it takes more efforts to complete several lines at once 165 | tls_set!(score, |s| *s += lines * lines); 166 | if tls_get!(score) > LEVEL_UP * tls_get!(level) { 167 | // if level should be increased 168 | tls_set!(level, |l| *l += 1); // increment level 169 | tls_set!(DELAY, |d| *d *= DELAY_FACTOR); // delay decreased 170 | } 171 | set_bold(); 172 | set_fg(SCORE_COLOR); 173 | xyprint( 174 | SCORE_X, 175 | SCORE_Y, 176 | &format!("Lines completed: {}", tls_get!(lines_completed)), 177 | ); 178 | xyprint( 179 | SCORE_X, 180 | SCORE_Y + 1, 181 | &format!("Level: {}", tls_get!(level)), 182 | ); 183 | xyprint( 184 | SCORE_X, 185 | SCORE_Y + 2, 186 | &format!("Score: {}", tls_get!(score)), 187 | ); 188 | reset_colors(); 189 | } 190 | 191 | const help: [&str; 9] = [ 192 | " Use cursor keys", 193 | " or", 194 | " k: rotate", 195 | "h: left, l: right", 196 | " j: drop", 197 | " q: quit", 198 | " c: toggle color", 199 | "n: toggle show next", 200 | "H: toggle this help", 201 | ]; 202 | 203 | tls_init!(help_on, i32, 1); // if this flag is 1 help is shown 204 | 205 | fn draw_help() { 206 | set_bold(); 207 | set_fg(HELP_COLOR); 208 | for (i, &h) in help.iter().enumerate() { 209 | // ternary assignment: if help_on is 1 use string as is, 210 | // otherwise substitute all characters with spaces 211 | let s = if tls_get!(help_on) == 1 { 212 | h.to_owned() 213 | } else { 214 | " ".repeat(h.len()) 215 | }; 216 | xyprint(HELP_X, HELP_Y + i as i32, &s); 217 | } 218 | reset_colors(); 219 | } 220 | 221 | fn toggle_help() { 222 | tls_set!(help_on, |h| *h ^= 1); 223 | draw_help(); 224 | } 225 | 226 | // this array holds all possible pieces that can be used in the game 227 | // each piece consists of 4 cells numbered from 0x0 to 0xf: 228 | // 0123 229 | // 4567 230 | // 89ab 231 | // cdef 232 | // each string is sequence of cells for different orientations 233 | // depending on piece symmetry there can be 1, 2 or 4 orientations 234 | // relative coordinates are calculated as follows: 235 | // x=((cell & 3)); y=((cell >> 2)) 236 | const piece_data: [&str; 7] = [ 237 | "1256", // square 238 | "159d4567", // line 239 | "45120459", // s 240 | "01561548", // z 241 | "159a845601592654", // l 242 | "159804562159a654", // inverted l 243 | "1456159645694159", // t 244 | ]; 245 | 246 | fn draw_piece(x: i32, y: i32, ctype: i32, rotation: i32, cell: &str) { 247 | // loop through piece cells: 4 cells, each has 2 coordinates 248 | for i in 0..4 { 249 | let c = piece_data[ctype as usize] 250 | .chars() 251 | .nth((i + rotation * 4) as usize) 252 | .unwrap() 253 | .to_digit(16) 254 | .unwrap() as i32; 255 | // relative coordinates are retrieved based on orientation and added to absolute coordinates 256 | let nx = x + (c & 3) * 2; 257 | let ny = y + (c >> 2); 258 | xyprint(nx, ny, cell); 259 | } 260 | } 261 | 262 | tls_init!(next_piece, i32, 0); 263 | tls_init!(next_piece_rotation, i32, 0); 264 | tls_init!(next_piece_color, i32, 0); 265 | 266 | tls_init!(next_on, i32, 1); // if this flag is 1 next piece is shown 267 | 268 | // Argument: visible - visibility (0 - no, 1 - yes), 269 | // if this argument is skipped $next_on is used 270 | fn draw_next(visible: i32) { 271 | let mut s = filled_cell.to_string(); 272 | if visible == 1 { 273 | set_fg(tls_get!(next_piece_color)); 274 | set_bg(tls_get!(next_piece_color)); 275 | } else { 276 | s = " ".repeat(s.len()); 277 | } 278 | draw_piece( 279 | NEXT_X, 280 | NEXT_Y, 281 | tls_get!(next_piece), 282 | tls_get!(next_piece_rotation), 283 | &s, 284 | ); 285 | reset_colors(); 286 | } 287 | 288 | fn toggle_next() { 289 | tls_set!(next_on, |x| *x ^= 1); 290 | draw_next(tls_get!(next_on)); 291 | } 292 | 293 | tls_init!(current_piece, i32, 0); 294 | tls_init!(current_piece_x, i32, 0); 295 | tls_init!(current_piece_y, i32, 0); 296 | tls_init!(current_piece_color, i32, 0); 297 | tls_init!(current_piece_rotation, i32, 0); 298 | 299 | // Arguments: cell - string to draw single cell 300 | fn draw_current(cell: &str) { 301 | // factor 2 for x because each cell is 2 characters wide 302 | draw_piece( 303 | tls_get!(current_piece_x) * 2 + PLAYFIELD_X, 304 | tls_get!(current_piece_y) + PLAYFIELD_Y, 305 | tls_get!(current_piece), 306 | tls_get!(current_piece_rotation), 307 | cell, 308 | ); 309 | } 310 | 311 | fn show_current() { 312 | set_fg(tls_get!(current_piece_color)); 313 | set_bg(tls_get!(current_piece_color)); 314 | draw_current(filled_cell); 315 | reset_colors(); 316 | } 317 | 318 | fn clear_current() { 319 | draw_current(empty_cell); 320 | } 321 | 322 | // Arguments: x_test - new x coordinate of the piece, y_test - new y coordinate of the piece 323 | // test if piece can be moved to new location 324 | fn new_piece_location_ok(x_test: i32, y_test: i32) -> bool { 325 | for i in 0..4 { 326 | let c = piece_data[tls_get!(current_piece) as usize] 327 | .chars() 328 | .nth((i + tls_get!(current_piece_rotation) * 4) as usize) 329 | .unwrap() 330 | .to_digit(16) 331 | .unwrap() as i32; 332 | // new x and y coordinates of piece cell 333 | let y = (c >> 2) + y_test; 334 | let x = (c & 3) + x_test; 335 | // check if we are out of the play field 336 | if y < 0 || y >= PLAYFIELD_H || x < 0 || x >= PLAYFIELD_W { 337 | return false; 338 | } 339 | // check if location is already ocupied 340 | if ((tls_get!(playfield)[y as usize] >> (x * 3)) & 7) != 0 { 341 | return false; 342 | } 343 | } 344 | true 345 | } 346 | 347 | fn rand() -> i32 { 348 | run_fun!(bash -c r"echo $RANDOM").unwrap().parse().unwrap() 349 | } 350 | 351 | fn get_random_next() { 352 | // next piece becomes current 353 | tls_set!(current_piece, |cur| *cur = tls_get!(next_piece)); 354 | tls_set!(current_piece_rotation, |cur| *cur = 355 | tls_get!(next_piece_rotation)); 356 | tls_set!(current_piece_color, |cur| *cur = tls_get!(next_piece_color)); 357 | // place current at the top of play field, approximately at the center 358 | tls_set!(current_piece_x, |cur| *cur = (PLAYFIELD_W - 4) / 2); 359 | tls_set!(current_piece_y, |cur| *cur = 0); 360 | // check if piece can be placed at this location, if not - game over 361 | if !new_piece_location_ok(tls_get!(current_piece_x), tls_get!(current_piece_y)) { 362 | cmd_exit(); 363 | } 364 | show_current(); 365 | 366 | draw_next(0); 367 | // now let's get next piece 368 | tls_set!(next_piece, |nxt| *nxt = rand() % (piece_data.len() as i32)); 369 | let rotations = piece_data[tls_get!(next_piece) as usize].len() as i32 / 4; 370 | tls_set!(next_piece_rotation, |nxt| *nxt = 371 | ((rand() % rotations) as u8) as i32); 372 | tls_set!(next_piece_color, |nxt| *nxt = 373 | colors[(rand() as usize) % colors.len()]); 374 | draw_next(tls_get!(next_on)); 375 | } 376 | 377 | fn draw_border() { 378 | set_bold(); 379 | set_fg(BORDER_COLOR); 380 | let x1 = PLAYFIELD_X - 2; // 2 here is because border is 2 characters thick 381 | let x2 = PLAYFIELD_X + PLAYFIELD_W * 2; // 2 here is because each cell on play field is 2 characters wide 382 | for i in 0..=PLAYFIELD_H { 383 | let y = i + PLAYFIELD_Y; 384 | xyprint(x1, y, "<|"); 385 | xyprint(x2, y, "|>"); 386 | } 387 | 388 | let y = PLAYFIELD_Y + PLAYFIELD_H; 389 | for i in 0..PLAYFIELD_W { 390 | let x1 = i * 2 + PLAYFIELD_X; // 2 here is because each cell on play field is 2 characters wide 391 | xyprint(x1, y, "=="); 392 | xyprint(x1, y + 1, "\\/"); 393 | } 394 | reset_colors(); 395 | } 396 | 397 | fn redraw_screen() { 398 | draw_next(1); 399 | update_score(0); 400 | draw_help(); 401 | draw_border(); 402 | redraw_playfield(); 403 | show_current(); 404 | } 405 | 406 | fn toggle_color() { 407 | tls_set!(use_color, |x| *x = !*x); 408 | redraw_screen(); 409 | } 410 | 411 | fn init() { 412 | run_cmd!(clear).unwrap(); 413 | hide_cursor(); 414 | get_random_next(); 415 | get_random_next(); 416 | redraw_screen(); 417 | flush_screen(); 418 | } 419 | 420 | // this function updates occupied cells in playfield array after piece is dropped 421 | fn flatten_playfield() { 422 | for i in 0..4 { 423 | let c: i32 = piece_data[tls_get!(current_piece) as usize] 424 | .chars() 425 | .nth((i + tls_get!(current_piece_rotation) * 4) as usize) 426 | .unwrap() 427 | .to_digit(16) 428 | .unwrap() as i32; 429 | let y = (c >> 2) + tls_get!(current_piece_y); 430 | let x = (c & 3) + tls_get!(current_piece_x); 431 | tls_set!(playfield, |f| f[y as usize] |= 432 | tls_get!(current_piece_color) << (x * 3)); 433 | } 434 | } 435 | 436 | // this function takes row number as argument and checks if has empty cells 437 | fn line_full(y: i32) -> bool { 438 | let row = tls_get!(playfield)[y as usize]; 439 | for x in 0..PLAYFIELD_W { 440 | if ((row >> (x * 3)) & 7) == 0 { 441 | return false; 442 | } 443 | } 444 | true 445 | } 446 | 447 | // this function goes through playfield array and eliminates lines without empty cells 448 | fn process_complete_lines() -> i32 { 449 | let mut complete_lines = 0; 450 | let mut last_idx = PLAYFIELD_H - 1; 451 | for y in (0..PLAYFIELD_H).rev() { 452 | if !line_full(y) { 453 | if last_idx != y { 454 | tls_set!(playfield, |f| f[last_idx as usize] = f[y as usize]); 455 | } 456 | last_idx -= 1; 457 | } else { 458 | complete_lines += 1; 459 | } 460 | } 461 | for y in 0..complete_lines { 462 | tls_set!(playfield, |f| f[y as usize] = 0); 463 | } 464 | complete_lines 465 | } 466 | 467 | fn process_fallen_piece() { 468 | flatten_playfield(); 469 | let lines = process_complete_lines(); 470 | if lines == 0 { 471 | return; 472 | } else { 473 | update_score(lines); 474 | } 475 | redraw_playfield(); 476 | } 477 | 478 | // arguments: nx - new x coordinate, ny - new y coordinate 479 | fn move_piece(nx: i32, ny: i32) -> bool { 480 | // moves the piece to the new location if possible 481 | if new_piece_location_ok(nx, ny) { 482 | // if new location is ok 483 | clear_current(); // let's wipe out piece current location 484 | tls_set!( 485 | current_piece_x, // update x ... 486 | |x| *x = nx 487 | ); 488 | tls_set!( 489 | current_piece_y, // ... and y of new location 490 | |y| *y = ny 491 | ); 492 | show_current(); // and draw piece in new location 493 | return true; // nothing more to do here 494 | } // if we could not move piece to new location 495 | if ny == tls_get!(current_piece_y) { 496 | return true; // and this was not horizontal move 497 | } 498 | process_fallen_piece(); // let's finalize this piece 499 | get_random_next(); // and start the new one 500 | false 501 | } 502 | 503 | fn cmd_right() { 504 | move_piece(tls_get!(current_piece_x) + 1, tls_get!(current_piece_y)); 505 | } 506 | 507 | fn cmd_left() { 508 | move_piece(tls_get!(current_piece_x) - 1, tls_get!(current_piece_y)); 509 | } 510 | 511 | fn cmd_rotate() { 512 | // local available_rotations old_rotation new_rotation 513 | // number of orientations for this piece 514 | let available_rotations = piece_data[tls_get!(current_piece) as usize].len() as i32 / 4; 515 | let old_rotation = tls_get!(current_piece_rotation); // preserve current orientation 516 | let new_rotation = (old_rotation + 1) % available_rotations; // calculate new orientation 517 | tls_set!(current_piece_rotation, |r| *r = new_rotation); // set orientation to new 518 | if new_piece_location_ok( 519 | tls_get!(current_piece_x), // check if new orientation is ok 520 | tls_get!(current_piece_y), 521 | ) { 522 | tls_set!(current_piece_rotation, |r| *r = old_rotation); // if yes - restore old orientation 523 | clear_current(); // clear piece image 524 | tls_set!(current_piece_rotation, |r| *r = new_rotation); // set new orientation 525 | show_current(); // draw piece with new orientation 526 | } else { 527 | // if new orientation is not ok 528 | tls_set!(current_piece_rotation, |r| *r = old_rotation); // restore old orientation 529 | } 530 | } 531 | 532 | fn cmd_down() { 533 | move_piece(tls_get!(current_piece_x), tls_get!(current_piece_y) + 1); 534 | } 535 | 536 | fn cmd_drop() { 537 | // move piece all way down 538 | // loop body is empty 539 | // loop condition is done at least once 540 | // loop runs until loop condition would return non zero exit code 541 | loop { 542 | if !move_piece(tls_get!(current_piece_x), tls_get!(current_piece_y) + 1) { 543 | break; 544 | } 545 | } 546 | } 547 | 548 | tls_init!(old_stty_cfg, String, String::new()); 549 | 550 | fn cmd_exit() { 551 | xyprint(GAMEOVER_X, GAMEOVER_Y, "Game over!"); 552 | xyprint(GAMEOVER_X, GAMEOVER_Y + 1, ""); // reset cursor position 553 | flush_screen(); // ... print final message ... 554 | show_cursor(); 555 | let stty_g = tls_get!(old_stty_cfg); 556 | run_cmd!(stty $stty_g).unwrap(); // ... and restore terminal state 557 | std::process::exit(0); 558 | } 559 | 560 | #[cmd_lib::main] 561 | fn main() -> CmdResult { 562 | #[rustfmt::skip] 563 | let old_cfg = run_fun!(stty -g)?; // let's save terminal state ... 564 | tls_set!(old_stty_cfg, |cfg| *cfg = old_cfg); 565 | run_cmd!(stty raw -echo -isig -icanon min 0 time 0)?; 566 | 567 | init(); 568 | let mut tick = 0; 569 | loop { 570 | let mut buffer = String::new(); 571 | if std::io::stdin().read_to_string(&mut buffer).is_ok() { 572 | match buffer.as_str() { 573 | "q" | "\u{1b}" | "\u{3}" => cmd_exit(), // q, ESC or Ctrl-C to exit 574 | "h" | "\u{1b}[D" => cmd_left(), 575 | "l" | "\u{1b}[C" => cmd_right(), 576 | "j" | "\u{1b}[B" => cmd_drop(), 577 | "k" | "\u{1b}[A" => cmd_rotate(), 578 | "H" => toggle_help(), 579 | "n" => toggle_next(), 580 | "c" => toggle_color(), 581 | _ => (), 582 | } 583 | } 584 | tick += 1; 585 | if tick >= (600.0 * tls_get!(DELAY)) as i32 { 586 | tick = 0; 587 | cmd_down(); 588 | } 589 | flush_screen(); 590 | thread::sleep(time::Duration::from_millis(1)); 591 | } 592 | } 593 | -------------------------------------------------------------------------------- /src/process.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins::*; 2 | use crate::child::{CmdChild, CmdChildHandle, CmdChildren, FunChildren}; 3 | use crate::io::{CmdIn, CmdOut}; 4 | use crate::{CmdResult, FunResult}; 5 | use crate::{debug, warn}; 6 | use faccess::{AccessMode, PathExt}; 7 | use os_pipe::{self, PipeReader, PipeWriter}; 8 | use std::cell::Cell; 9 | use std::collections::HashMap; 10 | use std::ffi::{OsStr, OsString}; 11 | use std::fmt; 12 | use std::fs::{File, OpenOptions}; 13 | use std::io::{Error, Result}; 14 | use std::marker::PhantomData; 15 | use std::mem::take; 16 | use std::path::{Path, PathBuf}; 17 | use std::process::Command; 18 | use std::sync::atomic::AtomicBool; 19 | use std::sync::atomic::Ordering::SeqCst; 20 | use std::sync::{LazyLock, Mutex}; 21 | use std::thread; 22 | 23 | const CD_CMD: &str = "cd"; 24 | const IGNORE_CMD: &str = "ignore"; 25 | 26 | /// Environment for builtin or custom commands. 27 | pub struct CmdEnv { 28 | stdin: CmdIn, 29 | stdout: CmdOut, 30 | stderr: CmdOut, 31 | args: Vec, 32 | vars: HashMap, 33 | current_dir: PathBuf, 34 | } 35 | impl CmdEnv { 36 | /// Returns the name of this command. 37 | pub fn get_cmd_name(&self) -> &str { 38 | &self.args[0] 39 | } 40 | 41 | /// Returns the arguments for this command. 42 | pub fn get_args(&self) -> &[String] { 43 | &self.args[1..] 44 | } 45 | 46 | /// Fetches the environment variable key for this command. 47 | pub fn var(&self, key: &str) -> Option<&String> { 48 | self.vars.get(key) 49 | } 50 | 51 | /// Returns the current working directory for this command. 52 | pub fn current_dir(&self) -> &Path { 53 | &self.current_dir 54 | } 55 | 56 | /// Returns a new handle to the standard input for this command. 57 | pub fn stdin(&mut self) -> &mut CmdIn { 58 | &mut self.stdin 59 | } 60 | 61 | /// Returns a new handle to the standard output for this command. 62 | pub fn stdout(&mut self) -> &mut CmdOut { 63 | &mut self.stdout 64 | } 65 | 66 | /// Returns a new handle to the standard error for this command. 67 | pub fn stderr(&mut self) -> &mut CmdOut { 68 | &mut self.stderr 69 | } 70 | } 71 | 72 | type FnFun = fn(&mut CmdEnv) -> CmdResult; 73 | 74 | static CMD_MAP: LazyLock>> = LazyLock::new(|| { 75 | let mut m: HashMap = HashMap::new(); 76 | m.insert("echo".into(), builtin_echo); 77 | m.insert("trace".into(), builtin_trace); 78 | m.insert("debug".into(), builtin_debug); 79 | m.insert("info".into(), builtin_info); 80 | m.insert("warn".into(), builtin_warn); 81 | m.insert("error".into(), builtin_error); 82 | m.insert("".into(), builtin_empty); 83 | 84 | Mutex::new(m) 85 | }); 86 | 87 | #[doc(hidden)] 88 | pub fn register_cmd(cmd: &'static str, func: FnFun) { 89 | CMD_MAP.lock().unwrap().insert(OsString::from(cmd), func); 90 | } 91 | 92 | /// Whether debug mode is enabled globally. 93 | /// Can be overridden by the thread-local setting in [`DEBUG_OVERRIDE`]. 94 | static DEBUG_ENABLED: LazyLock = 95 | LazyLock::new(|| AtomicBool::new(std::env::var("CMD_LIB_DEBUG") == Ok("1".into()))); 96 | 97 | /// Whether debug mode is enabled globally. 98 | /// Can be overridden by the thread-local setting in [`PIPEFAIL_OVERRIDE`]. 99 | static PIPEFAIL_ENABLED: LazyLock = 100 | LazyLock::new(|| AtomicBool::new(std::env::var("CMD_LIB_PIPEFAIL") != Ok("0".into()))); 101 | 102 | /// Set debug mode or not, false by default. 103 | /// 104 | /// This is **global**, and affects all threads. To set it for the current thread only, use [`ScopedDebug`]. 105 | /// 106 | /// Setting environment variable CMD_LIB_DEBUG=0|1 has the same effect, but the environment variable is only 107 | /// checked once at an unspecified time, so the only reliable way to do that is when the program is first started. 108 | /// 109 | /// ## Example 110 | /// ```console 111 | /// λ test_cmd_lib git:(master) ✗ cat src/main.rs 112 | /// use cmd_lib::*; 113 | /// 114 | /// #[cmd_lib::main] 115 | /// fn main() -> CmdResult { 116 | /// run_cmd!(cat src/main.rs | wc -l) 117 | /// } 118 | /// λ test_cmd_lib git:(master) ✗ RUST_LOG=debug CMD_LIB_DEBUG=1 cargo r 119 | /// Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s 120 | /// Running `target/debug/test_cmd_lib` 121 | /// [DEBUG] Running ["cat" "src/main.rs" | "wc" "-l"] at src/main.rs:5 ... 122 | /// 6 123 | /// ``` 124 | pub fn set_debug(enable: bool) { 125 | DEBUG_ENABLED.store(enable, SeqCst); 126 | } 127 | 128 | /// Set pipefail or not, true by default. 129 | /// 130 | /// This is **global**, and affects all threads. To set it for the current thread only, use [`ScopedPipefail`]. 131 | /// 132 | /// Setting environment variable CMD_LIB_PIPEFAIL=0|1 has the same effect, but the environment variable is only 133 | /// checked once at an unspecified time, so the only reliable way to do that is when the program is first started. 134 | pub fn set_pipefail(enable: bool) { 135 | PIPEFAIL_ENABLED.store(enable, SeqCst); 136 | } 137 | 138 | pub(crate) fn debug_enabled() -> bool { 139 | DEBUG_OVERRIDE 140 | .get() 141 | .unwrap_or_else(|| DEBUG_ENABLED.load(SeqCst)) 142 | } 143 | 144 | pub(crate) fn pipefail_enabled() -> bool { 145 | PIPEFAIL_OVERRIDE 146 | .get() 147 | .unwrap_or_else(|| PIPEFAIL_ENABLED.load(SeqCst)) 148 | } 149 | 150 | thread_local! { 151 | /// Whether debug mode is enabled in the current thread. 152 | /// None means to use the global setting in [`DEBUG_ENABLED`]. 153 | static DEBUG_OVERRIDE: Cell> = const { Cell::new(None) }; 154 | 155 | /// Whether pipefail mode is enabled in the current thread. 156 | /// None means to use the global setting in [`PIPEFAIL_ENABLED`]. 157 | static PIPEFAIL_OVERRIDE: Cell> = const { Cell::new(None) }; 158 | } 159 | 160 | /// Overrides the debug mode in the current thread, while the value is in scope. 161 | /// 162 | /// Each override restores the previous value when dropped, so they can be nested. 163 | /// Since overrides are thread-local, these values can’t be sent across threads. 164 | /// 165 | /// ``` 166 | /// # use cmd_lib::{ScopedDebug, run_cmd}; 167 | /// // Must give the variable a name, not just `_` 168 | /// let _debug = ScopedDebug::set(true); 169 | /// run_cmd!(echo hello world)?; // Will have debug on 170 | /// # Ok::<(), std::io::Error>(()) 171 | /// ``` 172 | // PhantomData field is equivalent to `impl !Send for Self {}` 173 | pub struct ScopedDebug(Option, PhantomData<*const ()>); 174 | 175 | /// Overrides the pipefail mode in the current thread, while the value is in scope. 176 | /// 177 | /// Each override restores the previous value when dropped, so they can be nested. 178 | /// Since overrides are thread-local, these values can’t be sent across threads. 179 | // PhantomData field is equivalent to `impl !Send for Self {}` 180 | /// 181 | /// ``` 182 | /// # use cmd_lib::{ScopedPipefail, run_cmd}; 183 | /// // Must give the variable a name, not just `_` 184 | /// let _debug = ScopedPipefail::set(false); 185 | /// run_cmd!(false | true)?; // Will have pipefail off 186 | /// # Ok::<(), std::io::Error>(()) 187 | /// ``` 188 | pub struct ScopedPipefail(Option, PhantomData<*const ()>); 189 | 190 | impl ScopedDebug { 191 | /// ```compile_fail 192 | /// let _: Box = Box::new(cmd_lib::ScopedDebug::set(true)); 193 | /// ``` 194 | /// ```compile_fail 195 | /// let _: Box = Box::new(cmd_lib::ScopedDebug::set(true)); 196 | /// ``` 197 | #[doc(hidden)] 198 | pub fn test_not_send_not_sync() {} 199 | 200 | pub fn set(enabled: bool) -> Self { 201 | let result = Self(DEBUG_OVERRIDE.get(), PhantomData); 202 | DEBUG_OVERRIDE.set(Some(enabled)); 203 | result 204 | } 205 | } 206 | impl Drop for ScopedDebug { 207 | fn drop(&mut self) { 208 | DEBUG_OVERRIDE.set(self.0) 209 | } 210 | } 211 | 212 | impl ScopedPipefail { 213 | /// ```compile_fail 214 | /// let _: Box = Box::new(cmd_lib::ScopedPipefail::set(true)); 215 | /// ``` 216 | /// ```compile_fail 217 | /// let _: Box = Box::new(cmd_lib::ScopedPipefail::set(true)); 218 | /// ``` 219 | #[doc(hidden)] 220 | pub fn test_not_send_not_sync() {} 221 | 222 | pub fn set(enabled: bool) -> Self { 223 | let result = Self(PIPEFAIL_OVERRIDE.get(), PhantomData); 224 | PIPEFAIL_OVERRIDE.set(Some(enabled)); 225 | result 226 | } 227 | } 228 | impl Drop for ScopedPipefail { 229 | fn drop(&mut self) { 230 | PIPEFAIL_OVERRIDE.set(self.0) 231 | } 232 | } 233 | 234 | #[doc(hidden)] 235 | #[derive(Default)] 236 | pub struct GroupCmds { 237 | group_cmds: Vec, 238 | current_dir: PathBuf, 239 | } 240 | 241 | impl GroupCmds { 242 | pub fn append(mut self, cmds: Cmds) -> Self { 243 | self.group_cmds.push(cmds); 244 | self 245 | } 246 | 247 | pub fn run_cmd(&mut self) -> CmdResult { 248 | for cmds in self.group_cmds.iter_mut() { 249 | if let Err(e) = cmds.run_cmd(&mut self.current_dir) 250 | && !cmds.ignore_error 251 | { 252 | return Err(e); 253 | } 254 | } 255 | Ok(()) 256 | } 257 | 258 | pub fn run_fun(&mut self) -> FunResult { 259 | // run previous commands 260 | let mut last_cmd = self.group_cmds.pop().unwrap(); 261 | self.run_cmd()?; 262 | // run last function command 263 | let ret = last_cmd.run_fun(&mut self.current_dir); 264 | if ret.is_err() && last_cmd.ignore_error { 265 | return Ok("".into()); 266 | } 267 | ret 268 | } 269 | 270 | pub fn spawn(mut self, with_output: bool) -> Result { 271 | assert_eq!(self.group_cmds.len(), 1); 272 | let mut cmds = self.group_cmds.pop().unwrap(); 273 | cmds.spawn(&mut self.current_dir, with_output) 274 | } 275 | 276 | pub fn spawn_with_output(self) -> Result { 277 | self.spawn(true).map(CmdChildren::into_fun_children) 278 | } 279 | } 280 | 281 | #[doc(hidden)] 282 | #[derive(Default)] 283 | pub struct Cmds { 284 | cmds: Vec, 285 | full_cmds: String, 286 | ignore_error: bool, 287 | file: String, 288 | line: u32, 289 | } 290 | 291 | impl Cmds { 292 | pub fn pipe(mut self, cmd: Cmd) -> Self { 293 | if self.full_cmds.is_empty() { 294 | self.file = cmd.file.clone(); 295 | self.line = cmd.line; 296 | } else { 297 | self.full_cmds += " | "; 298 | } 299 | self.full_cmds += &cmd.cmd_str(); 300 | let (ignore_error, cmd) = cmd.gen_command(); 301 | if ignore_error { 302 | if self.cmds.is_empty() { 303 | // first command in the pipe 304 | self.ignore_error = true; 305 | } else { 306 | warn!( 307 | "Builtin {IGNORE_CMD:?} command at wrong position ({}:{})", 308 | self.file, self.line 309 | ); 310 | } 311 | } 312 | self.cmds.push(cmd); 313 | self 314 | } 315 | 316 | fn spawn(&mut self, current_dir: &mut PathBuf, with_output: bool) -> Result { 317 | let full_cmds = self.full_cmds.clone(); 318 | let file = self.file.clone(); 319 | let line = self.line; 320 | if debug_enabled() { 321 | debug!("Running [{full_cmds}] at {file}:{line} ..."); 322 | } 323 | 324 | // spawning all the sub-processes 325 | let mut children: Vec = Vec::new(); 326 | let len = self.cmds.len(); 327 | let mut prev_pipe_in = None; 328 | for (i, mut cmd) in take(&mut self.cmds).into_iter().enumerate() { 329 | if i != len - 1 { 330 | // not the last, update redirects 331 | let (pipe_reader, pipe_writer) = 332 | os_pipe::pipe().map_err(|e| new_cmd_io_error(&e, &full_cmds, &file, line))?; 333 | cmd.setup_redirects(&mut prev_pipe_in, Some(pipe_writer), with_output) 334 | .map_err(|e| new_cmd_io_error(&e, &full_cmds, &file, line))?; 335 | prev_pipe_in = Some(pipe_reader); 336 | } else { 337 | cmd.setup_redirects(&mut prev_pipe_in, None, with_output) 338 | .map_err(|e| new_cmd_io_error(&e, &full_cmds, &file, line))?; 339 | } 340 | let child = cmd 341 | .spawn(full_cmds.clone(), current_dir, with_output) 342 | .map_err(|e| new_cmd_io_error(&e, &full_cmds, &file, line))?; 343 | children.push(child); 344 | } 345 | 346 | Ok(CmdChildren::new(children, self.ignore_error)) 347 | } 348 | 349 | fn spawn_with_output(&mut self, current_dir: &mut PathBuf) -> Result { 350 | self.spawn(current_dir, true) 351 | .map(CmdChildren::into_fun_children) 352 | } 353 | 354 | fn run_cmd(&mut self, current_dir: &mut PathBuf) -> CmdResult { 355 | self.spawn(current_dir, false)?.wait() 356 | } 357 | 358 | fn run_fun(&mut self, current_dir: &mut PathBuf) -> FunResult { 359 | self.spawn_with_output(current_dir)?.wait_with_output() 360 | } 361 | } 362 | 363 | #[doc(hidden)] 364 | pub enum Redirect { 365 | FileToStdin(PathBuf), 366 | StdoutToStderr, 367 | StderrToStdout, 368 | StdoutToFile(PathBuf, bool), 369 | StderrToFile(PathBuf, bool), 370 | } 371 | impl fmt::Debug for Redirect { 372 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 373 | match self { 374 | Redirect::FileToStdin(path) => f.write_str(&format!("<{:?}", path.display())), 375 | Redirect::StdoutToStderr => f.write_str(">&2"), 376 | Redirect::StderrToStdout => f.write_str("2>&1"), 377 | Redirect::StdoutToFile(path, append) => { 378 | if *append { 379 | f.write_str(&format!("1>>{:?}", path.display())) 380 | } else { 381 | f.write_str(&format!("1>{:?}", path.display())) 382 | } 383 | } 384 | Redirect::StderrToFile(path, append) => { 385 | if *append { 386 | f.write_str(&format!("2>>{:?}", path.display())) 387 | } else { 388 | f.write_str(&format!("2>{:?}", path.display())) 389 | } 390 | } 391 | } 392 | } 393 | } 394 | 395 | #[doc(hidden)] 396 | pub struct Cmd { 397 | // for parsing 398 | in_cmd_map: bool, 399 | args: Vec, 400 | vars: HashMap, 401 | redirects: Vec, 402 | file: String, 403 | line: u32, 404 | 405 | // for running 406 | std_cmd: Option, 407 | stdin_redirect: Option, 408 | stdout_redirect: Option, 409 | stderr_redirect: Option, 410 | stdout_logging: Option, 411 | stderr_logging: Option, 412 | } 413 | 414 | impl Default for Cmd { 415 | fn default() -> Self { 416 | Cmd { 417 | in_cmd_map: true, 418 | args: vec![], 419 | vars: HashMap::new(), 420 | redirects: vec![], 421 | file: "".into(), 422 | line: 0, 423 | std_cmd: None, 424 | stdin_redirect: None, 425 | stdout_redirect: None, 426 | stderr_redirect: None, 427 | stdout_logging: None, 428 | stderr_logging: None, 429 | } 430 | } 431 | } 432 | 433 | impl Cmd { 434 | pub fn with_location(mut self, file: &str, line: u32) -> Self { 435 | self.file = file.into(); 436 | self.line = line; 437 | self 438 | } 439 | 440 | pub fn add_arg(mut self, arg: O) -> Self 441 | where 442 | O: AsRef, 443 | { 444 | let arg = arg.as_ref(); 445 | if arg.is_empty() { 446 | // Skip empty arguments 447 | return self; 448 | } 449 | 450 | let arg_str = arg.to_string_lossy().to_string(); 451 | if arg_str != IGNORE_CMD && !self.args.iter().any(|cmd| *cmd != IGNORE_CMD) { 452 | if let Some((key, value)) = arg_str.split_once('=') 453 | && key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') 454 | { 455 | self.vars.insert(key.into(), value.into()); 456 | return self; 457 | } 458 | self.in_cmd_map = CMD_MAP.lock().unwrap().contains_key(arg); 459 | } 460 | self.args.push(arg.to_os_string()); 461 | self 462 | } 463 | 464 | pub fn add_args(mut self, args: I) -> Self 465 | where 466 | I: IntoIterator, 467 | O: AsRef, 468 | { 469 | for arg in args { 470 | self = self.add_arg(arg); 471 | } 472 | self 473 | } 474 | 475 | pub fn add_redirect(mut self, redirect: Redirect) -> Self { 476 | self.redirects.push(redirect); 477 | self 478 | } 479 | 480 | fn arg0(&self) -> OsString { 481 | let mut args = self.args.iter().skip_while(|cmd| *cmd == IGNORE_CMD); 482 | if let Some(arg) = args.next() { 483 | return arg.into(); 484 | } 485 | "".into() 486 | } 487 | 488 | fn cmd_str(&self) -> String { 489 | self.vars 490 | .iter() 491 | .map(|(k, v)| format!("{k}={v:?}")) 492 | .chain(self.args.iter().map(|s| format!("{s:?}"))) 493 | .chain(self.redirects.iter().map(|r| format!("{r:?}"))) 494 | .collect::>() 495 | .join(" ") 496 | } 497 | 498 | fn gen_command(mut self) -> (bool, Self) { 499 | let args: Vec = self 500 | .args 501 | .iter() 502 | .skip_while(|cmd| *cmd == IGNORE_CMD) 503 | .map(|s| s.into()) 504 | .collect(); 505 | if !self.in_cmd_map { 506 | let mut cmd = Command::new(&args[0]); 507 | cmd.args(&args[1..]); 508 | for (k, v) in self.vars.iter() { 509 | cmd.env(k, v); 510 | } 511 | self.std_cmd = Some(cmd); 512 | } 513 | (self.args.len() > args.len(), self) 514 | } 515 | 516 | fn spawn( 517 | mut self, 518 | full_cmds: String, 519 | current_dir: &mut PathBuf, 520 | with_output: bool, 521 | ) -> Result { 522 | let arg0 = self.arg0(); 523 | if arg0 == CD_CMD { 524 | self.run_cd_cmd(current_dir, &self.file, self.line)?; 525 | Ok(CmdChild::new( 526 | CmdChildHandle::SyncFn, 527 | full_cmds, 528 | self.file, 529 | self.line, 530 | self.stdout_logging, 531 | self.stderr_logging, 532 | )) 533 | } else if self.in_cmd_map { 534 | let pipe_out = self.stdout_logging.is_none(); 535 | let mut env = CmdEnv { 536 | args: self 537 | .args 538 | .into_iter() 539 | .skip_while(|cmd| *cmd == IGNORE_CMD) 540 | .map(|s| s.to_string_lossy().to_string()) 541 | .collect(), 542 | vars: self.vars, 543 | current_dir: if current_dir.as_os_str().is_empty() { 544 | std::env::current_dir()? 545 | } else { 546 | current_dir.clone() 547 | }, 548 | stdin: if let Some(redirect_in) = self.stdin_redirect.take() { 549 | redirect_in 550 | } else { 551 | CmdIn::pipe(os_pipe::dup_stdin()?) 552 | }, 553 | stdout: if let Some(redirect_out) = self.stdout_redirect.take() { 554 | redirect_out 555 | } else { 556 | CmdOut::pipe(os_pipe::dup_stdout()?) 557 | }, 558 | stderr: if let Some(redirect_err) = self.stderr_redirect.take() { 559 | redirect_err 560 | } else { 561 | CmdOut::pipe(os_pipe::dup_stderr()?) 562 | }, 563 | }; 564 | 565 | let internal_cmd = CMD_MAP.lock().unwrap()[&arg0]; 566 | if pipe_out || with_output { 567 | let handle = thread::Builder::new().spawn(move || internal_cmd(&mut env))?; 568 | Ok(CmdChild::new( 569 | CmdChildHandle::Thread(Some(handle)), 570 | full_cmds, 571 | self.file, 572 | self.line, 573 | self.stdout_logging, 574 | self.stderr_logging, 575 | )) 576 | } else { 577 | internal_cmd(&mut env)?; 578 | Ok(CmdChild::new( 579 | CmdChildHandle::SyncFn, 580 | full_cmds, 581 | self.file, 582 | self.line, 583 | self.stdout_logging, 584 | self.stderr_logging, 585 | )) 586 | } 587 | } else { 588 | let mut cmd = self.std_cmd.take().unwrap(); 589 | 590 | // setup current_dir 591 | if !current_dir.as_os_str().is_empty() { 592 | cmd.current_dir(current_dir.clone()); 593 | } 594 | 595 | // update stdin 596 | if let Some(redirect_in) = self.stdin_redirect.take() { 597 | cmd.stdin(redirect_in); 598 | } 599 | 600 | // update stdout 601 | if let Some(redirect_out) = self.stdout_redirect.take() { 602 | cmd.stdout(redirect_out); 603 | } 604 | 605 | // update stderr 606 | if let Some(redirect_err) = self.stderr_redirect.take() { 607 | cmd.stderr(redirect_err); 608 | } 609 | 610 | // spawning process 611 | let child = cmd.spawn()?; 612 | Ok(CmdChild::new( 613 | CmdChildHandle::Proc(child), 614 | full_cmds, 615 | self.file, 616 | self.line, 617 | self.stdout_logging, 618 | self.stderr_logging, 619 | )) 620 | } 621 | } 622 | 623 | fn run_cd_cmd(&self, current_dir: &mut PathBuf, file: &str, line: u32) -> CmdResult { 624 | if self.args.len() == 1 { 625 | return Err(Error::other("{CD_CMD}: missing directory at {file}:{line}")); 626 | } else if self.args.len() > 2 { 627 | let err_msg = format!("{CD_CMD}: too many arguments at {file}:{line}"); 628 | return Err(Error::other(err_msg)); 629 | } 630 | 631 | let dir = current_dir.join(&self.args[1]); 632 | if !dir.is_dir() { 633 | let err_msg = format!("{CD_CMD}: No such file or directory at {file}:{line}"); 634 | return Err(Error::other(err_msg)); 635 | } 636 | 637 | dir.access(AccessMode::EXECUTE)?; 638 | *current_dir = dir; 639 | Ok(()) 640 | } 641 | 642 | fn open_file(path: &Path, read_only: bool, append: bool) -> Result { 643 | if read_only { 644 | OpenOptions::new().read(true).open(path) 645 | } else { 646 | OpenOptions::new() 647 | .create(true) 648 | .truncate(!append) 649 | .write(true) 650 | .append(append) 651 | .open(path) 652 | } 653 | } 654 | 655 | fn setup_redirects( 656 | &mut self, 657 | pipe_in: &mut Option, 658 | pipe_out: Option, 659 | with_output: bool, 660 | ) -> CmdResult { 661 | // set up stdin pipe 662 | if let Some(pipe) = pipe_in.take() { 663 | self.stdin_redirect = Some(CmdIn::pipe(pipe)); 664 | } 665 | // set up stdout pipe 666 | if let Some(pipe) = pipe_out { 667 | self.stdout_redirect = Some(CmdOut::pipe(pipe)); 668 | } else if with_output { 669 | let (pipe_reader, pipe_writer) = os_pipe::pipe()?; 670 | self.stdout_redirect = Some(CmdOut::pipe(pipe_writer)); 671 | self.stdout_logging = Some(pipe_reader); 672 | } 673 | // set up stderr pipe 674 | let (pipe_reader, pipe_writer) = os_pipe::pipe()?; 675 | self.stderr_redirect = Some(CmdOut::pipe(pipe_writer)); 676 | self.stderr_logging = Some(pipe_reader); 677 | 678 | for redirect in self.redirects.iter() { 679 | match redirect { 680 | Redirect::FileToStdin(path) => { 681 | self.stdin_redirect = Some(if path == Path::new("/dev/null") { 682 | CmdIn::null() 683 | } else { 684 | CmdIn::file(Self::open_file(path, true, false)?) 685 | }); 686 | } 687 | Redirect::StdoutToStderr => { 688 | if let Some(ref redirect) = self.stderr_redirect { 689 | self.stdout_redirect = Some(redirect.try_clone()?); 690 | } else { 691 | self.stdout_redirect = Some(CmdOut::pipe(os_pipe::dup_stderr()?)); 692 | } 693 | } 694 | Redirect::StderrToStdout => { 695 | if let Some(ref redirect) = self.stdout_redirect { 696 | self.stderr_redirect = Some(redirect.try_clone()?); 697 | } else { 698 | self.stderr_redirect = Some(CmdOut::pipe(os_pipe::dup_stdout()?)); 699 | } 700 | } 701 | Redirect::StdoutToFile(path, append) => { 702 | self.stdout_redirect = Some(if path == Path::new("/dev/null") { 703 | CmdOut::null() 704 | } else { 705 | CmdOut::file(Self::open_file(path, false, *append)?) 706 | }); 707 | } 708 | Redirect::StderrToFile(path, append) => { 709 | self.stderr_redirect = Some(if path == Path::new("/dev/null") { 710 | CmdOut::null() 711 | } else { 712 | CmdOut::file(Self::open_file(path, false, *append)?) 713 | }); 714 | } 715 | } 716 | } 717 | Ok(()) 718 | } 719 | } 720 | 721 | #[doc(hidden)] 722 | pub trait AsOsStr { 723 | fn as_os_str(&self) -> OsString; 724 | } 725 | 726 | impl AsOsStr for T { 727 | fn as_os_str(&self) -> OsString { 728 | self.to_string().into() 729 | } 730 | } 731 | 732 | #[doc(hidden)] 733 | #[derive(Default)] 734 | pub struct CmdString(OsString); 735 | impl CmdString { 736 | pub fn append>(mut self, value: T) -> Self { 737 | self.0.push(value); 738 | self 739 | } 740 | 741 | pub fn into_os_string(self) -> OsString { 742 | self.0 743 | } 744 | 745 | pub fn into_path_buf(self) -> PathBuf { 746 | self.0.into() 747 | } 748 | } 749 | 750 | impl AsRef for CmdString { 751 | fn as_ref(&self) -> &OsStr { 752 | self.0.as_ref() 753 | } 754 | } 755 | 756 | impl> From<&T> for CmdString { 757 | fn from(s: &T) -> Self { 758 | Self(s.as_ref().into()) 759 | } 760 | } 761 | 762 | impl fmt::Display for CmdString { 763 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 764 | f.write_str(&self.0.to_string_lossy()) 765 | } 766 | } 767 | 768 | pub(crate) fn new_cmd_io_error(e: &Error, command: &str, file: &str, line: u32) -> Error { 769 | Error::new( 770 | e.kind(), 771 | format!("Running [{command}] failed: {e} at {file}:{line}"), 772 | ) 773 | } 774 | 775 | #[cfg(test)] 776 | mod tests { 777 | use super::*; 778 | 779 | #[test] 780 | fn test_run_piped_cmds() { 781 | let mut current_dir = PathBuf::new(); 782 | assert!( 783 | Cmds::default() 784 | .pipe(Cmd::default().add_args(["echo", "rust"])) 785 | .pipe(Cmd::default().add_args(["wc"])) 786 | .run_cmd(&mut current_dir) 787 | .is_ok() 788 | ); 789 | } 790 | 791 | #[test] 792 | fn test_run_piped_funs() { 793 | let mut current_dir = PathBuf::new(); 794 | assert_eq!( 795 | Cmds::default() 796 | .pipe(Cmd::default().add_args(["echo", "rust"])) 797 | .run_fun(&mut current_dir) 798 | .unwrap(), 799 | "rust" 800 | ); 801 | 802 | assert_eq!( 803 | Cmds::default() 804 | .pipe(Cmd::default().add_args(["echo", "rust"])) 805 | .pipe(Cmd::default().add_args(["wc", "-c"])) 806 | .run_fun(&mut current_dir) 807 | .unwrap() 808 | .trim(), 809 | "5" 810 | ); 811 | } 812 | 813 | #[test] 814 | fn test_stdout_redirect() { 815 | let mut current_dir = PathBuf::new(); 816 | let tmp_file = "/tmp/file_echo_rust"; 817 | let mut write_cmd = Cmd::default().add_args(["echo", "rust"]); 818 | write_cmd = write_cmd.add_redirect(Redirect::StdoutToFile(PathBuf::from(tmp_file), false)); 819 | assert!( 820 | Cmds::default() 821 | .pipe(write_cmd) 822 | .run_cmd(&mut current_dir) 823 | .is_ok() 824 | ); 825 | 826 | let read_cmd = Cmd::default().add_args(["cat", tmp_file]); 827 | assert_eq!( 828 | Cmds::default() 829 | .pipe(read_cmd) 830 | .run_fun(&mut current_dir) 831 | .unwrap(), 832 | "rust" 833 | ); 834 | 835 | let cleanup_cmd = Cmd::default().add_args(["rm", tmp_file]); 836 | assert!( 837 | Cmds::default() 838 | .pipe(cleanup_cmd) 839 | .run_cmd(&mut current_dir) 840 | .is_ok() 841 | ); 842 | } 843 | } 844 | --------------------------------------------------------------------------------