├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── dd_test.rs ├── pipes.rs ├── pipes.sh ├── rust_cookbook.rs ├── tetris.rs └── tetris.sh ├── macros ├── Cargo.toml └── src │ ├── lexer.rs │ ├── lib.rs │ └── parser.rs ├── src ├── builtins.rs ├── child.rs ├── io.rs ├── lib.rs ├── logger.rs ├── process.rs └── thread_local.rs └── tests └── test_macros.rs /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | **/*.rs.bk 4 | **/*.swp 5 | -------------------------------------------------------------------------------- /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 = "1.9.5" 12 | authors = ["rust-shell-script "] 13 | edition = "2018" 14 | 15 | [workspace] 16 | members = ["macros"] 17 | 18 | [dependencies] 19 | cmd_lib_macros = { version = "1.9.5", path = "./macros" } 20 | lazy_static = "1.4.0" 21 | log = "0.4.20" 22 | faccess = "0.2.4" 23 | os_pipe = "1.1.4" 24 | env_logger = "0.10.0" 25 | 26 | [dev-dependencies] 27 | rayon = "1.8.0" 28 | clap = { version = "4", features = ["derive"] } 29 | byte-unit = "4.0.19" 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 looks like 36 | 37 | To get a first impression, here is an example from 38 | [examples/dd_test.rs](https://github.com/rust-shell-script/rust_cmd_lib/blob/master/examples/dd_test.rs): 39 | 40 | ```rust 41 | run_cmd! ( 42 | info "Dropping caches at first"; 43 | sudo bash -c "echo 3 > /proc/sys/vm/drop_caches"; 44 | info "Running with thread_num: $thread_num, block_size: $block_size"; 45 | )?; 46 | let cnt = DATA_SIZE / thread_num / block_size; 47 | let now = Instant::now(); 48 | (0..thread_num).into_par_iter().for_each(|i| { 49 | let off = cnt * i; 50 | let bandwidth = run_fun!( 51 | sudo bash -c "dd if=$file of=/dev/null bs=$block_size skip=$off count=$cnt 2>&1" 52 | | awk r#"/copied/{print $(NF-1) " " $NF}"# 53 | ) 54 | .unwrap_or_else(|_| cmd_die!("thread $i failed")); 55 | info!("thread {i} bandwidth: {bandwidth}"); 56 | }); 57 | let total_bandwidth = Byte::from_bytes((DATA_SIZE / now.elapsed().as_secs()) as u128).get_appropriate_unit(true); 58 | info!("Total bandwidth: {total_bandwidth}/s"); 59 | ``` 60 | 61 | Output will be like this: 62 | 63 | ```console 64 | ➜ rust_cmd_lib git:(master) ✗ cargo run --example dd_test -- -b 4096 -f /dev/nvme0n1 -t 4 65 | Finished dev [unoptimized + debuginfo] target(s) in 0.04s 66 | Running `target/debug/examples/dd_test -b 4096 -f /dev/nvme0n1 -t 4` 67 | [INFO ] Dropping caches at first 68 | [INFO ] Running with thread_num: 4, block_size: 4096 69 | [INFO ] thread 3 bandwidth: 317 MB/s 70 | [INFO ] thread 1 bandwidth: 289 MB/s 71 | [INFO ] thread 0 bandwidth: 281 MB/s 72 | [INFO ] thread 2 bandwidth: 279 MB/s 73 | [INFO ] Total bandwidth: 1.11 GiB/s 74 | ``` 75 | 76 | ### What this library provides 77 | 78 | #### Macros to run external commands 79 | - [`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) 80 | 81 | ```rust 82 | let msg = "I love rust"; 83 | run_cmd!(echo $msg)?; 84 | run_cmd!(echo "This is the message: $msg")?; 85 | 86 | // pipe commands are also supported 87 | let dir = "/var/log"; 88 | run_cmd!(du -ah $dir | sort -hr | head -n 10)?; 89 | 90 | // or a group of commands 91 | // if any command fails, just return Err(...) 92 | let file = "/tmp/f"; 93 | let keyword = "rust"; 94 | run_cmd! { 95 | cat ${file} | grep ${keyword}; 96 | echo "bad cmd" >&2; 97 | ignore ls /nofile; 98 | date; 99 | ls oops; 100 | cat oops; 101 | }?; 102 | ``` 103 | 104 | - [`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) 105 | 106 | ```rust 107 | let version = run_fun!(rustc --version)?; 108 | eprintln!("Your rust version is {}", version); 109 | 110 | // with pipes 111 | let n = run_fun!(echo "the quick brown fox jumped over the lazy dog" | wc -w)?; 112 | eprintln!("There are {} words in above sentence", n); 113 | ``` 114 | 115 | #### Abstraction without overhead 116 | 117 | Since all the macros' lexical analysis and syntactic analysis happen at compile time, it can 118 | basically generate code the same as calling `std::process` APIs manually. It also includes 119 | command type checking, so most of the errors can be found at compile time instead of at 120 | runtime. With tools like `rust-analyzer`, it can give you real-time feedback for broken 121 | commands being used. 122 | 123 | You can use `cargo expand` to check the generated code. 124 | 125 | #### Intuitive parameters passing 126 | When passing parameters to `run_cmd!` and `run_fun!` macros, if they are not part to rust 127 | [String literals](https://doc.rust-lang.org/reference/tokens.html#string-literals), they will be 128 | converted to string as an atomic component, so you don't need to quote them. The parameters will be 129 | like `$a` or `${a}` in `run_cmd!` or `run_fun!` macros. 130 | 131 | ```rust 132 | let dir = "my folder"; 133 | run_cmd!(echo "Creating $dir at /tmp")?; 134 | run_cmd!(mkdir -p /tmp/$dir)?; 135 | 136 | // or with group commands: 137 | let dir = "my folder"; 138 | run_cmd!(echo "Creating $dir at /tmp"; mkdir -p /tmp/$dir)?; 139 | ``` 140 | You can consider "" as glue, so everything inside the quotes will be treated as a single atomic component. 141 | 142 | If they are part of [Raw string literals](https://doc.rust-lang.org/reference/tokens.html#raw-string-literals), 143 | there will be no string interpolation, the same as in idiomatic rust. However, you can always use `format!` macro 144 | to form the new string. For example: 145 | ```rust 146 | // string interpolation 147 | let key_word = "time"; 148 | let awk_opts = format!(r#"/{}/ {{print $(NF-3) " " $(NF-1) " " $NF}}"#, key_word); 149 | run_cmd!(ping -c 10 www.google.com | awk $awk_opts)?; 150 | ``` 151 | Notice here `$awk_opts` will be treated as single option passing to awk command. 152 | 153 | If you want to use dynamic parameters, you can use `$[]` to access vector variable: 154 | ```rust 155 | let gopts = vec![vec!["-l", "-a", "/"], vec!["-a", "/var"]]; 156 | for opts in gopts { 157 | run_cmd!(ls $[opts])?; 158 | } 159 | ``` 160 | 161 | #### Redirection and Piping 162 | Right now piping and stdin, stdout, stderr redirection are supported. Most parts are the same as in 163 | [bash scripts](https://www.gnu.org/software/bash/manual/html_node/Redirections.html#Redirections). 164 | 165 | #### Logging 166 | 167 | This library provides convenient macros and builtin commands for logging. All messages which 168 | are printed to stderr will be logged. It will also include the full running commands in the error 169 | result. 170 | 171 | ```rust 172 | let dir: &str = "folder with spaces"; 173 | run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir)?; 174 | run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir; rmdir /tmp/$dir)?; 175 | // output: 176 | // [INFO ] mkdir: cannot create directory ‘/tmp/folder with spaces’: File exists 177 | // Error: Running ["mkdir" "/tmp/folder with spaces"] exited with error; status code: 1 178 | ``` 179 | 180 | It is using rust [log crate](https://crates.io/crates/log), and you can use your actual favorite 181 | logger implementation. Notice that if you don't provide any logger, it will use env_logger to print 182 | messages from process's stderr. 183 | 184 | You can also mark your `main()` function with `#[cmd_lib::main]`, which will log error from 185 | main() by default. Like this: 186 | ```console 187 | [ERROR] FATAL: Running ["mkdir" "/tmp/folder with spaces"] exited with error; status code: 1 188 | ``` 189 | 190 | #### Builtin commands 191 | ##### cd 192 | cd: set process current directory. 193 | ```rust 194 | run_cmd! ( 195 | cd /tmp; 196 | ls | wc -l; 197 | )?; 198 | ``` 199 | Notice that builtin `cd` will only change with current scope 200 | and it will restore the previous current directory when it 201 | exits the scope. 202 | 203 | Use `std::env::set_current_dir` if you want to change the current 204 | working directory for the whole program. 205 | 206 | ##### ignore 207 | 208 | Ignore errors for command execution. 209 | 210 | ##### echo 211 | Print messages to stdout. 212 | ```console 213 | -n do not output the trailing newline 214 | ``` 215 | 216 | ##### error, warn, info, debug, trace 217 | 218 | Print messages to logging with different levels. You can also use the normal logging macros, 219 | if you don't need to do logging inside the command group. 220 | 221 | ```rust 222 | run_cmd!(error "This is an error message")?; 223 | run_cmd!(warn "This is a warning message")?; 224 | run_cmd!(info "This is an information message")?; 225 | // output: 226 | // [ERROR] This is an error message 227 | // [WARN ] This is a warning message 228 | // [INFO ] This is an information message 229 | ``` 230 | 231 | #### Low-level process spawning macros 232 | 233 | [`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 234 | default, stdin, stdout and stderr are inherited from the parent. The process will run in the 235 | 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 236 | for the process to finish. 237 | 238 | With [`spawn_with_output!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.spawn_with_output.html) you can get output by calling 239 | [`wait_with_output()`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.FunChildren.html#method.wait_with_output), 240 | [`wait_with_all()`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.FunChildren.html#method.wait_with_all) 241 | or even do stream 242 | processing with [`wait_with_pipe()`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.FunChildren.html#method.wait_with_pipe). 243 | 244 | There are also other useful APIs, and you can check the docs for more details. 245 | 246 | ```rust 247 | let mut proc = spawn!(ping -c 10 192.168.0.1)?; 248 | // do other stuff 249 | // ... 250 | proc.wait()?; 251 | 252 | let mut proc = spawn_with_output!(/bin/cat file.txt | sed s/a/b/)?; 253 | // do other stuff 254 | // ... 255 | let output = proc.wait_with_output()?; 256 | 257 | spawn_with_output!(journalctl)?.wait_with_pipe(&mut |pipe| { 258 | BufReader::new(pipe) 259 | .lines() 260 | .filter_map(|line| line.ok()) 261 | .filter(|line| line.find("usb").is_some()) 262 | .take(10) 263 | .for_each(|line| println!("{}", line)); 264 | })?; 265 | ``` 266 | 267 | #### Macro to register your own commands 268 | 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: 269 | 270 | ```rust 271 | fn my_cmd(env: &mut CmdEnv) -> CmdResult { 272 | let args = env.get_args(); 273 | let (res, stdout, stderr) = spawn_with_output! { 274 | orig_cmd $[args] 275 | --long-option xxx 276 | --another-option yyy 277 | }? 278 | .wait_with_all(); 279 | writeln!(env.stdout(), "{}", stdout)?; 280 | writeln!(env.stderr(), "{}", stderr)?; 281 | res 282 | } 283 | 284 | use_custom_cmd!(my_cmd); 285 | ``` 286 | 287 | #### Macros to define, get and set thread-local global variables 288 | - [`tls_init!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_init.html) to define thread local global variable 289 | - [`tls_get!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_get.html) to get the value 290 | - [`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) to set the value 291 | ```rust 292 | tls_init!(DELAY, f64, 1.0); 293 | const DELAY_FACTOR: f64 = 0.8; 294 | tls_set!(DELAY, |d| *d *= DELAY_FACTOR); 295 | let d = tls_get!(DELAY); 296 | // check more examples in examples/tetris.rs 297 | ``` 298 | 299 | ### Other Notes 300 | 301 | #### Environment Variables 302 | 303 | You can use [std::env::var](https://doc.rust-lang.org/std/env/fn.var.html) to fetch the environment variable 304 | key from the current process. It will report error if the environment variable is not present, and it also 305 | includes other checks to avoid silent failures. 306 | 307 | To set environment variables, you can use [std::env::set_var](https://doc.rust-lang.org/std/env/fn.set_var.html). 308 | There are also other related APIs in the [std::env](https://doc.rust-lang.org/std/env/index.html) module. 309 | 310 | To set environment variables for the command only, you can put the assignments before the command. 311 | Like this: 312 | ```rust 313 | run_cmd!(FOO=100 /tmp/test_run_cmd_lib.sh)?; 314 | ``` 315 | 316 | #### Security Notes 317 | Using macros can actually avoid command injection, since we do parsing before variable substitution. 318 | For example, below code is fine even without any quotes: 319 | ```rust 320 | fn cleanup_uploaded_file(file: &Path) -> CmdResult { 321 | run_cmd!(/bin/rm -f /var/upload/$file) 322 | } 323 | ``` 324 | It is not the case in bash, which will always do variable substitution at first. 325 | 326 | #### Glob/Wildcard 327 | 328 | This library does not provide glob functions, to avoid silent errors and other surprises. 329 | You can use the [glob](https://github.com/rust-lang-nursery/glob) package instead. 330 | 331 | #### Thread Safety 332 | 333 | This library tries very hard to not set global states, so parallel `cargo test` can be executed just fine. 334 | The only known APIs not supported in multi-thread environment are the 335 | [`tls_init!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_init.html)/[`tls_get!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_get.html)/[`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) macros, and you should only use them for *thread local* variables. 336 | 337 | 338 | License: MIT OR Apache-2.0 339 | -------------------------------------------------------------------------------- /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 cmd_lib::*; 19 | use rayon::prelude::*; 20 | use std::time::Instant; 21 | use clap::Parser; 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 | -------------------------------------------------------------------------------- /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 69 | // 0: up, 1: right, 2: down, 3: left 70 | tls_init!(n, Vec, vec![]); // new directions 71 | tls_init!(v, Vec, vec![]); // current types 72 | tls_init!(c, Vec, vec![]); // current escape codes 73 | 74 | // selected pipes' 75 | tls_init!(V, Vec, vec![0]); // types (indexes to sets[]) 76 | tls_init!(C, Vec, vec![1, 2, 3, 4, 5, 6, 7, 0]); // color indices for tput setaf 77 | tls_init!(VN, i32, 1); // number of selected types 78 | tls_init!(CN, i32, 8); // number of selected colors 79 | tls_init!(E, Vec, vec![]); // pre-generated escape codes from BOLD, NOCOLOR, and C 80 | 81 | // switches 82 | tls_init!(RNDSTART, bool, false); // randomize starting position and direction 83 | tls_init!(BOLD, bool, true); 84 | tls_init!(NOCOLOR, bool, false); 85 | tls_init!(KEEPCT, bool, false); // keep pipe color and type 86 | 87 | fn prog_name() -> String { 88 | let arg0 = std::env::args().next().unwrap(); 89 | run_fun!(basename $arg0).unwrap() 90 | } 91 | 92 | // print help message in 72-char width 93 | fn print_help() { 94 | let prog = prog_name(); 95 | let max_type = tls_get!(sets).len() - 1; 96 | let cgap = " ".repeat(15 - format!("{}", tls_get!(COLORS)).chars().count()); 97 | let colors = run_fun!(tput colors).unwrap(); 98 | let term = std::env::var("TERM").unwrap(); 99 | #[rustfmt::skip] 100 | eprintln!(" 101 | Usage: {prog} [OPTION]... 102 | Animated pipes terminal screensaver. 103 | 104 | -p [1-] number of pipes (D=1) 105 | -t [0-{max_type}] pipe type (D=0) 106 | -t c[16 chars] custom pipe type 107 | -c [0-{colors}]{cgap}pipe color INDEX (TERM={term}), can be 108 | hexadecimal with '#' prefix 109 | (D=-c 1 -c 2 ... -c 7 -c 0) 110 | -f [20-100] framerate (D=75) 111 | -s [5-15] going straight probability, 1 in (D=13) 112 | -r [0-] reset after (D=2000) characters, 0 if no reset 113 | -R randomize starting position and direction 114 | -B no bold effect 115 | -C no color 116 | -K keep pipe color and type when crossing edges 117 | -h print this help message 118 | -v print version number 119 | 120 | Note: -t and -c can be used more than once."); 121 | } 122 | 123 | // parse command-line options 124 | // It depends on a valid COLORS which is set by _CP_init_termcap_vars 125 | fn parse() -> CmdResult { 126 | // test if $1 is a natural number in decimal, an integer >= 0 127 | fn is_N(arg_opt: Option) -> (bool, i32) { 128 | if let Some(arg) = arg_opt { 129 | if let Ok(vv) = arg.parse::() { 130 | return (vv >= 0, vv); 131 | } 132 | } 133 | (false, 0) 134 | } 135 | 136 | // test if $1 is a hexadecimal string 137 | fn is_hex(arg: &str) -> (bool, i32) { 138 | if let Ok(vv) = i32::from_str_radix(&arg, 16) { 139 | return (true, vv); 140 | } 141 | (false, 0) 142 | } 143 | 144 | // print error message for invalid argument to standard error, this 145 | // - mimics getopts error message 146 | // - use all positional parameters as error message 147 | // - has a newline appended 148 | // $arg and $OPTARG are the option name and argument set by getopts. 149 | fn pearg(arg: &str, msg: &str) -> ! { 150 | let arg0 = prog_name(); 151 | info!("{arg0}: -{arg} invalid argument; {msg}"); 152 | print_help(); 153 | std::process::exit(1) 154 | } 155 | 156 | let mut args = std::env::args().skip(1); 157 | while let Some(arg) = args.next() { 158 | match arg.as_str() { 159 | "-p" => { 160 | let (is_valid, vv) = is_N(args.next()); 161 | if is_valid && vv > 0 { 162 | tls_set!(p, |np| *np = vv); 163 | } else { 164 | pearg(&arg, "must be an integer and greater than 0"); 165 | } 166 | } 167 | "-t" => { 168 | let arg_opt = args.next(); 169 | let (is_valid, vv) = is_N(arg_opt.clone()); 170 | let arg_str = arg_opt.unwrap_or_default(); 171 | let len = tls_get!(sets).len() as i32; 172 | if arg_str.chars().count() == 16 { 173 | tls_set!(V, |nv| nv.push(len)); 174 | tls_set!(sets, |ns| ns.push(arg_str)); 175 | } else if is_valid && vv < len { 176 | tls_set!(V, |nv| nv.push(vv)); 177 | } else { 178 | pearg( 179 | &arg, 180 | &format!("must be an integer and from 0 to {}; or a custom type", len), 181 | ); 182 | } 183 | } 184 | "-c" => { 185 | let arg_opt = args.next(); 186 | let (is_valid, vv) = is_N(arg_opt.clone()); 187 | let arg_str = arg_opt.unwrap_or_default(); 188 | if arg_str.starts_with("#") { 189 | let (is_valid_hex, hv) = is_hex(&arg_str[1..]); 190 | if !is_valid_hex { 191 | pearg(&arg, "unrecognized hexadecimal string"); 192 | } 193 | if hv >= tls_get!(COLORS) { 194 | pearg( 195 | &arg, 196 | &format!("hexadecimal must be from #0 to {:X}", tls_get!(COLORS) - 1), 197 | ); 198 | } 199 | tls_set!(C, |nc| nc.push(hv)); 200 | } else if is_valid && vv < tls_get!(COLORS) { 201 | tls_set!(C, |nc| nc.push(vv)); 202 | } else { 203 | pearg( 204 | &arg, 205 | &format!( 206 | "must be an integer and from 0 to {}; 207 | or a hexadecimal string with # prefix", 208 | tls_get!(COLORS) - 1 209 | ), 210 | ); 211 | } 212 | } 213 | "-f" => { 214 | let (is_valid, vv) = is_N(args.next()); 215 | if is_valid && vv >= 20 && vv <= 100 { 216 | tls_set!(f, |nf| *nf = vv); 217 | } else { 218 | pearg(&arg, "must be an integer and from 20 to 100"); 219 | } 220 | } 221 | "-s" => { 222 | let (is_valid, vv) = is_N(args.next()); 223 | if is_valid && vv >= 5 && vv <= 15 { 224 | tls_set!(r, |nr| *nr = vv); 225 | } else { 226 | pearg(&arg, "must be a non-negative integer"); 227 | } 228 | } 229 | "-r" => { 230 | let (is_valid, vv) = is_N(args.next()); 231 | if is_valid && vv > 0 { 232 | tls_set!(r, |nr| *nr = vv); 233 | } else { 234 | pearg(&arg, "must be a non-negative integer"); 235 | } 236 | } 237 | "-R" => tls_set!(RNDSTART, |nr| *nr = true), 238 | "-B" => tls_set!(BOLD, |nb| *nb = false), 239 | "-C" => tls_set!(NOCOLOR, |nc| *nc = true), 240 | "-K" => tls_set!(KEEPCT, |nk| *nk = true), 241 | "-h" => { 242 | print_help(); 243 | std::process::exit(0); 244 | } 245 | "-v" => { 246 | let arg0 = std::env::args().next().unwrap(); 247 | let prog = run_fun!(basename $arg0)?; 248 | run_cmd!(echo $prog $VERSION)?; 249 | std::process::exit(0); 250 | } 251 | _ => { 252 | pearg( 253 | &arg, 254 | &format!("illegal arguments -- {}; no arguments allowed", arg), 255 | ); 256 | } 257 | } 258 | } 259 | Ok(()) 260 | } 261 | 262 | fn cleanup() -> CmdResult { 263 | let sgr0 = tls_get!(SGR0); 264 | run_cmd!( 265 | tput reset; // fix for konsole, see pipeseroni/pipes.sh#43 266 | tput rmcup; 267 | tput cnorm; 268 | stty echo; 269 | echo $sgr0; 270 | )?; 271 | 272 | Ok(()) 273 | } 274 | 275 | fn resize() -> CmdResult { 276 | let cols = run_fun!(tput cols)?.parse().unwrap(); 277 | let lines = run_fun!(tput lines)?.parse().unwrap(); 278 | tls_set!(w, |nw| *nw = cols); 279 | tls_set!(h, |nh| *nh = lines); 280 | Ok(()) 281 | } 282 | 283 | fn init_pipes() { 284 | // +_CP_init_pipes 285 | let mut ci = if tls_get!(KEEPCT) { 286 | 0 287 | } else { 288 | tls_get!(CN) * rand() / M 289 | }; 290 | 291 | let mut vi = if tls_get!(RNDSTART) { 292 | 0 293 | } else { 294 | tls_get!(VN) * rand() / M 295 | }; 296 | 297 | for _ in 0..tls_get!(p) as usize { 298 | tls_set!(n, |nn| nn.push(0)); 299 | tls_set!(l, |nl| nl.push(if tls_get!(RNDSTART) { 300 | rand() % 4 301 | } else { 302 | 0 303 | })); 304 | tls_set!(x, |nx| nx.push(if tls_get!(RNDSTART) { 305 | tls_get!(w) * rand() / M 306 | } else { 307 | tls_get!(w) / 2 308 | })); 309 | tls_set!(y, |ny| ny.push(if tls_get!(RNDSTART) { 310 | tls_get!(h) * rand() / M 311 | } else { 312 | tls_get!(h) / 2 313 | })); 314 | tls_set!(v, |nv| nv.push(tls_get!(V)[vi as usize])); 315 | tls_set!(c, |nc| nc.push(tls_get!(E)[ci as usize].clone())); 316 | ci = (ci + 1) % tls_get!(CN); 317 | vi = (vi + 1) % tls_get!(VN); 318 | } 319 | // -_CP_init_pipes 320 | } 321 | 322 | fn init_screen() -> CmdResult { 323 | run_cmd!( 324 | stty -echo -isig -icanon min 0 time 0; 325 | tput smcup; 326 | tput civis; 327 | tput clear; 328 | )?; 329 | resize()?; 330 | Ok(()) 331 | } 332 | 333 | tls_init!(SGR0, String, String::new()); 334 | tls_init!(SGR_BOLD, String, String::new()); 335 | tls_init!(COLORS, i32, 0); 336 | 337 | fn rand() -> i32 { 338 | run_fun!(bash -c r"echo $RANDOM").unwrap().parse().unwrap() 339 | } 340 | 341 | #[cmd_lib::main] 342 | fn main() -> CmdResult { 343 | // simple pre-check of TERM, tput's error message should be enough 344 | let term = std::env::var("TERM").unwrap(); 345 | run_cmd!(tput -T $term sgr0 >/dev/null)?; 346 | 347 | // +_CP_init_termcap_vars 348 | let colors = run_fun!(tput colors)?.parse().unwrap(); 349 | tls_set!(COLORS, |nc| *nc = colors); // COLORS - 1 == maximum color index for -c argument 350 | tls_set!(SGR0, |ns| *ns = run_fun!(tput sgr0).unwrap()); 351 | tls_set!(SGR_BOLD, |nb| *nb = run_fun!(tput bold).unwrap()); 352 | // -_CP_init_termcap_vars 353 | 354 | parse()?; 355 | 356 | // +_CP_init_VC 357 | // set default values if not by options 358 | tls_set!(VN, |vn| *vn = tls_get!(V).len() as i32); 359 | tls_set!(CN, |cn| *cn = tls_get!(C).len() as i32); 360 | // -_CP_init_VC 361 | 362 | // +_CP_init_E 363 | // generate E[] based on BOLD (SGR_BOLD), NOCOLOR, and C for each element in 364 | // C, a corresponding element in E[] = 365 | // SGR0 366 | // + SGR_BOLD, if BOLD 367 | // + tput setaf C, if !NOCOLOR 368 | for i in 0..(tls_get!(CN) as usize) { 369 | tls_set!(E, |ne| ne.push(tls_get!(SGR0))); 370 | if tls_get!(BOLD) { 371 | tls_set!(E, |ne| ne[i] += &tls_get!(SGR_BOLD)); 372 | } 373 | if !tls_get!(NOCOLOR) { 374 | let cc = tls_get!(C)[i]; 375 | let setaf = run_fun!(tput setaf $cc)?; 376 | tls_set!(E, |ne| ne[i] += &setaf); 377 | } 378 | } 379 | // -_CP_init_E 380 | 381 | // +_CP_init_SETS 382 | for i in 0..tls_get!(sets).len() { 383 | for j in 0..16 { 384 | let cc = tls_get!(sets)[i].chars().nth(j).unwrap(); 385 | tls_set!(SETS, |ns| ns.push(cc)); 386 | } 387 | } 388 | // -_CP_init_SETS 389 | 390 | init_screen()?; 391 | init_pipes(); 392 | 393 | loop { 394 | thread::sleep(time::Duration::from_millis(1000 / tls_get!(f) as u64)); 395 | let mut buffer = String::new(); 396 | if std::io::stdin().read_to_string(&mut buffer).is_ok() { 397 | match buffer.as_str() { 398 | "q" | "\u{1b}" | "\u{3}" => { 399 | cleanup()?; // q, ESC or Ctrl-C to exit 400 | break; 401 | } 402 | "P" => tls_set!(s, |ns| *ns = if *ns < 15 { *ns + 1 } else { *ns }), 403 | "O" => tls_set!(s, |ns| *ns = if *ns > 3 { *ns - 1 } else { *ns }), 404 | "F" => tls_set!(f, |nf| *nf = if *nf < 100 { *nf + 1 } else { *nf }), 405 | "D" => tls_set!(f, |nf| *nf = if *nf > 20 { *nf - 1 } else { *nf }), 406 | "B" => tls_set!(BOLD, |nb| *nb = !*nb), 407 | "C" => tls_set!(NOCOLOR, |nc| *nc = !*nc), 408 | "K" => tls_set!(KEEPCT, |nk| *nk = !*nk), 409 | _ => (), 410 | } 411 | } 412 | for i in 0..(tls_get!(p) as usize) { 413 | // New position: 414 | // l[] direction = 0: up, 1: right, 2: down, 3: left 415 | // +_CP_newpos 416 | if tls_get!(l)[i] % 2 == 1 { 417 | tls_set!(x, |nx| nx[i] += -tls_get!(l)[i] + 2); 418 | } else { 419 | tls_set!(y, |ny| ny[i] += tls_get!(l)[i] - 1); 420 | } 421 | // -_CP_newpos 422 | 423 | // Loop on edges (change color on loop): 424 | // +_CP_warp 425 | if !tls_get!(KEEPCT) { 426 | if tls_get!(x)[i] >= tls_get!(w) 427 | || tls_get!(x)[i] < 0 428 | || tls_get!(y)[i] >= tls_get!(h) 429 | || tls_get!(y)[i] < 0 430 | { 431 | tls_set!(c, |nc| nc[i] = 432 | tls_get!(E)[(tls_get!(CN) * rand() / M) as usize].clone()); 433 | tls_set!(v, |nv| nv[i] = 434 | tls_get!(V)[(tls_get!(VN) * rand() / M) as usize].clone()); 435 | } 436 | } 437 | tls_set!(x, |nx| nx[i] = (nx[i] + tls_get!(w)) % tls_get!(w)); 438 | tls_set!(y, |ny| ny[i] = (ny[i] + tls_get!(h)) % tls_get!(h)); 439 | // -_CP_warp 440 | 441 | // new turning direction: 442 | // $((s - 1)) in $s, going straight, therefore n[i] == l[i]; 443 | // and 1 in $s that pipe makes a right or left turn 444 | // 445 | // s * rand() / M - 1 == 0 446 | // n[i] == -1 447 | // => n[i] == l[i] + 1 or l[i] - 1 448 | // +_CP_newdir 449 | tls_set!(n, |nn| nn[i] = tls_get!(s) * rand() / M - 1); 450 | tls_set!(n, |nn| nn[i] = if nn[i] >= 0 { 451 | tls_get!(l)[i] 452 | } else { 453 | tls_get!(l)[i] + (2 * (rand() % 2) - 1) 454 | }); 455 | tls_set!(n, |nn| nn[i] = (nn[i] + 4) % 4); 456 | // -_CP_newdir 457 | 458 | // Print: 459 | // +_CP_print 460 | let ii = tls_get!(v)[i] * 16 + tls_get!(l)[i] * 4 + tls_get!(n)[i]; 461 | eprint!( 462 | "\u{1b}[{};{}H{}{}", 463 | tls_get!(y)[i] + 1, 464 | tls_get!(x)[i] + 1, 465 | tls_get!(c)[i], 466 | tls_get!(SETS)[ii as usize] 467 | ); 468 | // -_CP_print 469 | tls_set!(l, |nl| nl[i] = tls_get!(n)[i]); 470 | } 471 | 472 | if tls_get!(r) > 0 && tls_get!(t) * tls_get!(p) >= tls_get!(r) { 473 | run_cmd!( 474 | tput reset; 475 | tput civis; 476 | stty -echo -isig -icanon min 0 time 0; 477 | )?; 478 | tls_set!(t, |nt| *nt = 0); 479 | } else { 480 | tls_set!(t, |nt| *nt += 1); 481 | } 482 | } 483 | Ok(()) 484 | } 485 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | tls_init!(lines_completed, i32, 0); // 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!(screen_buffer, String, "".to_string()); 84 | 85 | fn puts(changes: &str) { 86 | tls_set!(screen_buffer, |s| s.push_str(changes)); 87 | } 88 | 89 | fn flush_screen() { 90 | eprint!("{}", tls_get!(screen_buffer)); 91 | tls_set!(screen_buffer, |s| s.clear()); 92 | } 93 | 94 | const ESC: char = '\x1b'; // escape key, '\033' in bash or c 95 | 96 | // move cursor to (x,y) and print string 97 | // (1,1) is upper left corner of the screen 98 | fn xyprint(x: i32, y: i32, s: &str) { 99 | puts(&format!("{}[{};{}H{}", ESC, y, x, s)); 100 | } 101 | 102 | fn show_cursor() { 103 | eprint!("{}[?25h", ESC); 104 | } 105 | 106 | fn hide_cursor() { 107 | eprint!("{}[?25l", ESC); 108 | } 109 | 110 | // foreground color 111 | fn set_fg(color: i32) { 112 | if tls_get!(use_color) { 113 | puts(&format!("{}[3{}m", ESC, color)); 114 | } 115 | } 116 | 117 | // background color 118 | fn set_bg(color: i32) { 119 | if tls_get!(use_color) { 120 | puts(&format!("{}[4{}m", ESC, color)); 121 | } 122 | } 123 | 124 | fn reset_colors() { 125 | puts(&format!("{}[0m", ESC)); 126 | } 127 | 128 | fn set_bold() { 129 | puts(&format!("{}[1m", ESC)); 130 | } 131 | 132 | // playfield is an array, each row is represented by integer 133 | // each cell occupies 3 bits (empty if 0, other values encode color) 134 | // playfield is initialized with 0s (empty cells) 135 | tls_init!( 136 | playfield, 137 | [i32; PLAYFIELD_H as usize], 138 | [0; PLAYFIELD_H as usize] 139 | ); 140 | 141 | fn redraw_playfield() { 142 | for y in 0..PLAYFIELD_H { 143 | xyprint(PLAYFIELD_X, PLAYFIELD_Y + y, ""); 144 | for x in 0..PLAYFIELD_W { 145 | let color = (tls_get!(playfield)[y as usize] >> (x * 3)) & 7; 146 | if color == 0 { 147 | puts(empty_cell); 148 | } else { 149 | set_fg(color); 150 | set_bg(color); 151 | puts(filled_cell); 152 | reset_colors(); 153 | } 154 | } 155 | } 156 | } 157 | 158 | // Arguments: lines - number of completed lines 159 | fn update_score(lines: i32) { 160 | tls_set!(lines_completed, |l| *l += lines); 161 | // Unfortunately I don't know scoring algorithm of original tetris 162 | // Here score is incremented with squared number of lines completed 163 | // this seems reasonable since it takes more efforts to complete several lines at once 164 | tls_set!(score, |s| *s += lines * lines); 165 | if tls_get!(score) > LEVEL_UP * tls_get!(level) { 166 | // if level should be increased 167 | tls_set!(level, |l| *l += 1); // increment level 168 | tls_set!(DELAY, |d| *d *= DELAY_FACTOR); // delay decreased 169 | } 170 | set_bold(); 171 | set_fg(SCORE_COLOR); 172 | xyprint( 173 | SCORE_X, 174 | SCORE_Y, 175 | &format!("Lines completed: {}", tls_get!(lines_completed)), 176 | ); 177 | xyprint( 178 | SCORE_X, 179 | SCORE_Y + 1, 180 | &format!("Level: {}", tls_get!(level)), 181 | ); 182 | xyprint( 183 | SCORE_X, 184 | SCORE_Y + 2, 185 | &format!("Score: {}", tls_get!(score)), 186 | ); 187 | reset_colors(); 188 | } 189 | 190 | const help: [&str; 9] = [ 191 | " Use cursor keys", 192 | " or", 193 | " k: rotate", 194 | "h: left, l: right", 195 | " j: drop", 196 | " q: quit", 197 | " c: toggle color", 198 | "n: toggle show next", 199 | "H: toggle this help", 200 | ]; 201 | 202 | tls_init!(help_on, i32, 1); // if this flag is 1 help is shown 203 | 204 | fn draw_help() { 205 | set_bold(); 206 | set_fg(HELP_COLOR); 207 | for (i, &h) in help.iter().enumerate() { 208 | // ternary assignment: if help_on is 1 use string as is, 209 | // otherwise substitute all characters with spaces 210 | let s = if tls_get!(help_on) == 1 { 211 | h.to_owned() 212 | } else { 213 | " ".repeat(h.len()) 214 | }; 215 | xyprint(HELP_X, HELP_Y + i as i32, &s); 216 | } 217 | reset_colors(); 218 | } 219 | 220 | fn toggle_help() { 221 | tls_set!(help_on, |h| *h ^= 1); 222 | draw_help(); 223 | } 224 | 225 | // this array holds all possible pieces that can be used in the game 226 | // each piece consists of 4 cells numbered from 0x0 to 0xf: 227 | // 0123 228 | // 4567 229 | // 89ab 230 | // cdef 231 | // each string is sequence of cells for different orientations 232 | // depending on piece symmetry there can be 1, 2 or 4 orientations 233 | // relative coordinates are calculated as follows: 234 | // x=((cell & 3)); y=((cell >> 2)) 235 | const piece_data: [&str; 7] = [ 236 | "1256", // square 237 | "159d4567", // line 238 | "45120459", // s 239 | "01561548", // z 240 | "159a845601592654", // l 241 | "159804562159a654", // inverted l 242 | "1456159645694159", // t 243 | ]; 244 | 245 | fn draw_piece(x: i32, y: i32, ctype: i32, rotation: i32, cell: &str) { 246 | // loop through piece cells: 4 cells, each has 2 coordinates 247 | for i in 0..4 { 248 | let c = piece_data[ctype as usize] 249 | .chars() 250 | .nth((i + rotation * 4) as usize) 251 | .unwrap() 252 | .to_digit(16) 253 | .unwrap() as i32; 254 | // relative coordinates are retrieved based on orientation and added to absolute coordinates 255 | let nx = x + (c & 3) * 2; 256 | let ny = y + (c >> 2); 257 | xyprint(nx, ny, cell); 258 | } 259 | } 260 | 261 | tls_init!(next_piece, i32, 0); 262 | tls_init!(next_piece_rotation, i32, 0); 263 | tls_init!(next_piece_color, i32, 0); 264 | 265 | tls_init!(next_on, i32, 1); // if this flag is 1 next piece is shown 266 | 267 | // Argument: visible - visibility (0 - no, 1 - yes), 268 | // if this argument is skipped $next_on is used 269 | fn draw_next(visible: i32) { 270 | let mut s = filled_cell.to_string(); 271 | if visible == 1 { 272 | set_fg(tls_get!(next_piece_color)); 273 | set_bg(tls_get!(next_piece_color)); 274 | } else { 275 | s = " ".repeat(s.len()); 276 | } 277 | draw_piece( 278 | NEXT_X, 279 | NEXT_Y, 280 | tls_get!(next_piece), 281 | tls_get!(next_piece_rotation), 282 | &s, 283 | ); 284 | reset_colors(); 285 | } 286 | 287 | fn toggle_next() { 288 | tls_set!(next_on, |x| *x ^= 1); 289 | draw_next(tls_get!(next_on)); 290 | } 291 | 292 | tls_init!(current_piece, i32, 0); 293 | tls_init!(current_piece_x, i32, 0); 294 | tls_init!(current_piece_y, i32, 0); 295 | tls_init!(current_piece_color, i32, 0); 296 | tls_init!(current_piece_rotation, i32, 0); 297 | 298 | // Arguments: cell - string to draw single cell 299 | fn draw_current(cell: &str) { 300 | // factor 2 for x because each cell is 2 characters wide 301 | draw_piece( 302 | tls_get!(current_piece_x) * 2 + PLAYFIELD_X, 303 | tls_get!(current_piece_y) + PLAYFIELD_Y, 304 | tls_get!(current_piece), 305 | tls_get!(current_piece_rotation), 306 | cell, 307 | ); 308 | } 309 | 310 | fn show_current() { 311 | set_fg(tls_get!(current_piece_color)); 312 | set_bg(tls_get!(current_piece_color)); 313 | draw_current(filled_cell); 314 | reset_colors(); 315 | } 316 | 317 | fn clear_current() { 318 | draw_current(empty_cell); 319 | } 320 | 321 | // Arguments: x_test - new x coordinate of the piece, y_test - new y coordinate of the piece 322 | // test if piece can be moved to new location 323 | fn new_piece_location_ok(x_test: i32, y_test: i32) -> bool { 324 | for i in 0..4 { 325 | let c = piece_data[tls_get!(current_piece) as usize] 326 | .chars() 327 | .nth((i + tls_get!(current_piece_rotation) * 4) as usize) 328 | .unwrap() 329 | .to_digit(16) 330 | .unwrap() as i32; 331 | // new x and y coordinates of piece cell 332 | let y = (c >> 2) + y_test; 333 | let x = (c & 3) + x_test; 334 | // check if we are out of the play field 335 | if y < 0 || y >= PLAYFIELD_H || x < 0 || x >= PLAYFIELD_W { 336 | return false; 337 | } 338 | // check if location is already ocupied 339 | if ((tls_get!(playfield)[y as usize] >> (x * 3)) & 7) != 0 { 340 | return false; 341 | } 342 | } 343 | true 344 | } 345 | 346 | fn rand() -> i32 { 347 | run_fun!(bash -c r"echo $RANDOM").unwrap().parse().unwrap() 348 | } 349 | 350 | fn get_random_next() { 351 | // next piece becomes current 352 | tls_set!(current_piece, |cur| *cur = tls_get!(next_piece)); 353 | tls_set!(current_piece_rotation, |cur| *cur = 354 | tls_get!(next_piece_rotation)); 355 | tls_set!(current_piece_color, |cur| *cur = tls_get!(next_piece_color)); 356 | // place current at the top of play field, approximately at the center 357 | tls_set!(current_piece_x, |cur| *cur = (PLAYFIELD_W - 4) / 2); 358 | tls_set!(current_piece_y, |cur| *cur = 0); 359 | // check if piece can be placed at this location, if not - game over 360 | if !new_piece_location_ok(tls_get!(current_piece_x), tls_get!(current_piece_y)) { 361 | cmd_exit(); 362 | } 363 | show_current(); 364 | 365 | draw_next(0); 366 | // now let's get next piece 367 | tls_set!(next_piece, |nxt| *nxt = rand() % (piece_data.len() as i32)); 368 | let rotations = piece_data[tls_get!(next_piece) as usize].len() as i32 / 4; 369 | tls_set!(next_piece_rotation, |nxt| *nxt = 370 | ((rand() % rotations) as u8) as i32); 371 | tls_set!(next_piece_color, |nxt| *nxt = 372 | colors[(rand() as usize) % colors.len()]); 373 | draw_next(tls_get!(next_on)); 374 | } 375 | 376 | fn draw_border() { 377 | set_bold(); 378 | set_fg(BORDER_COLOR); 379 | let x1 = PLAYFIELD_X - 2; // 2 here is because border is 2 characters thick 380 | let x2 = PLAYFIELD_X + PLAYFIELD_W * 2; // 2 here is because each cell on play field is 2 characters wide 381 | for i in 0..=PLAYFIELD_H { 382 | let y = i + PLAYFIELD_Y; 383 | xyprint(x1, y, "<|"); 384 | xyprint(x2, y, "|>"); 385 | } 386 | 387 | let y = PLAYFIELD_Y + PLAYFIELD_H; 388 | for i in 0..PLAYFIELD_W { 389 | let x1 = i * 2 + PLAYFIELD_X; // 2 here is because each cell on play field is 2 characters wide 390 | xyprint(x1, y, "=="); 391 | xyprint(x1, y + 1, "\\/"); 392 | } 393 | reset_colors(); 394 | } 395 | 396 | fn redraw_screen() { 397 | draw_next(1); 398 | update_score(0); 399 | draw_help(); 400 | draw_border(); 401 | redraw_playfield(); 402 | show_current(); 403 | } 404 | 405 | fn toggle_color() { 406 | tls_set!(use_color, |x| *x = !*x); 407 | redraw_screen(); 408 | } 409 | 410 | fn init() { 411 | run_cmd!(clear).unwrap(); 412 | hide_cursor(); 413 | get_random_next(); 414 | get_random_next(); 415 | redraw_screen(); 416 | flush_screen(); 417 | } 418 | 419 | // this function updates occupied cells in playfield array after piece is dropped 420 | fn flatten_playfield() { 421 | for i in 0..4 { 422 | let c: i32 = piece_data[tls_get!(current_piece) as usize] 423 | .chars() 424 | .nth((i + tls_get!(current_piece_rotation) * 4) as usize) 425 | .unwrap() 426 | .to_digit(16) 427 | .unwrap() as i32; 428 | let y = (c >> 2) + tls_get!(current_piece_y); 429 | let x = (c & 3) + tls_get!(current_piece_x); 430 | tls_set!(playfield, |f| f[y as usize] |= 431 | tls_get!(current_piece_color) << (x * 3)); 432 | } 433 | } 434 | 435 | // this function takes row number as argument and checks if has empty cells 436 | fn line_full(y: i32) -> bool { 437 | let row = tls_get!(playfield)[y as usize]; 438 | for x in 0..PLAYFIELD_W { 439 | if ((row >> (x * 3)) & 7) == 0 { 440 | return false; 441 | } 442 | } 443 | true 444 | } 445 | 446 | // this function goes through playfield array and eliminates lines without empty cells 447 | fn process_complete_lines() -> i32 { 448 | let mut complete_lines = 0; 449 | let mut last_idx = PLAYFIELD_H - 1; 450 | for y in (0..PLAYFIELD_H).rev() { 451 | if !line_full(y) { 452 | if last_idx != y { 453 | tls_set!(playfield, |f| f[last_idx as usize] = f[y as usize]); 454 | } 455 | last_idx -= 1; 456 | } else { 457 | complete_lines += 1; 458 | } 459 | } 460 | for y in 0..complete_lines { 461 | tls_set!(playfield, |f| f[y as usize] = 0); 462 | } 463 | complete_lines 464 | } 465 | 466 | fn process_fallen_piece() { 467 | flatten_playfield(); 468 | let lines = process_complete_lines(); 469 | if lines == 0 { 470 | return; 471 | } else { 472 | update_score(lines); 473 | } 474 | redraw_playfield(); 475 | } 476 | 477 | // arguments: nx - new x coordinate, ny - new y coordinate 478 | fn move_piece(nx: i32, ny: i32) -> bool { 479 | // moves the piece to the new location if possible 480 | if new_piece_location_ok(nx, ny) { 481 | // if new location is ok 482 | clear_current(); // let's wipe out piece current location 483 | tls_set!( 484 | current_piece_x, // update x ... 485 | |x| *x = nx 486 | ); 487 | tls_set!( 488 | current_piece_y, // ... and y of new location 489 | |y| *y = ny 490 | ); 491 | show_current(); // and draw piece in new location 492 | return true; // nothing more to do here 493 | } // if we could not move piece to new location 494 | if ny == tls_get!(current_piece_y) { 495 | return true; // and this was not horizontal move 496 | } 497 | process_fallen_piece(); // let's finalize this piece 498 | get_random_next(); // and start the new one 499 | false 500 | } 501 | 502 | fn cmd_right() { 503 | move_piece(tls_get!(current_piece_x) + 1, tls_get!(current_piece_y)); 504 | } 505 | 506 | fn cmd_left() { 507 | move_piece(tls_get!(current_piece_x) - 1, tls_get!(current_piece_y)); 508 | } 509 | 510 | fn cmd_rotate() { 511 | // local available_rotations old_rotation new_rotation 512 | // number of orientations for this piece 513 | let available_rotations = piece_data[tls_get!(current_piece) as usize].len() as i32 / 4; 514 | let old_rotation = tls_get!(current_piece_rotation); // preserve current orientation 515 | let new_rotation = (old_rotation + 1) % available_rotations; // calculate new orientation 516 | tls_set!(current_piece_rotation, |r| *r = new_rotation); // set orientation to new 517 | if new_piece_location_ok( 518 | tls_get!(current_piece_x), // check if new orientation is ok 519 | tls_get!(current_piece_y), 520 | ) { 521 | tls_set!(current_piece_rotation, |r| *r = old_rotation); // if yes - restore old orientation 522 | clear_current(); // clear piece image 523 | tls_set!(current_piece_rotation, |r| *r = new_rotation); // set new orientation 524 | show_current(); // draw piece with new orientation 525 | } else { 526 | // if new orientation is not ok 527 | tls_set!(current_piece_rotation, |r| *r = old_rotation); // restore old orientation 528 | } 529 | } 530 | 531 | fn cmd_down() { 532 | move_piece(tls_get!(current_piece_x), tls_get!(current_piece_y) + 1); 533 | } 534 | 535 | fn cmd_drop() { 536 | // move piece all way down 537 | // loop body is empty 538 | // loop condition is done at least once 539 | // loop runs until loop condition would return non zero exit code 540 | loop { 541 | if !move_piece(tls_get!(current_piece_x), tls_get!(current_piece_y) + 1) { 542 | break; 543 | } 544 | } 545 | } 546 | 547 | tls_init!(old_stty_cfg, String, String::new()); 548 | 549 | fn cmd_exit() { 550 | xyprint(GAMEOVER_X, GAMEOVER_Y, "Game over!"); 551 | xyprint(GAMEOVER_X, GAMEOVER_Y + 1, ""); // reset cursor position 552 | flush_screen(); // ... print final message ... 553 | show_cursor(); 554 | let stty_g = tls_get!(old_stty_cfg); 555 | run_cmd!(stty $stty_g).unwrap(); // ... and restore terminal state 556 | std::process::exit(0); 557 | } 558 | 559 | #[cmd_lib::main] 560 | fn main() -> CmdResult { 561 | #[rustfmt::skip] 562 | let old_cfg = run_fun!(stty -g)?; // let's save terminal state ... 563 | tls_set!(old_stty_cfg, |cfg| *cfg = old_cfg); 564 | run_cmd!(stty raw -echo -isig -icanon min 0 time 0)?; 565 | 566 | init(); 567 | let mut tick = 0; 568 | loop { 569 | let mut buffer = String::new(); 570 | if std::io::stdin().read_to_string(&mut buffer).is_ok() { 571 | match buffer.as_str() { 572 | "q" | "\u{1b}" | "\u{3}" => cmd_exit(), // q, ESC or Ctrl-C to exit 573 | "h" | "\u{1b}[D" => cmd_left(), 574 | "l" | "\u{1b}[C" => cmd_right(), 575 | "j" | "\u{1b}[B" => cmd_drop(), 576 | "k" | "\u{1b}[A" => cmd_rotate(), 577 | "H" => toggle_help(), 578 | "n" => toggle_next(), 579 | "c" => toggle_color(), 580 | _ => (), 581 | } 582 | } 583 | tick += 1; 584 | if tick >= (600.0 * tls_get!(DELAY)) as i32 { 585 | tick = 0; 586 | cmd_down(); 587 | } 588 | flush_screen(); 589 | thread::sleep(time::Duration::from_millis(1)); 590 | } 591 | } 592 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 = "1.9.5" 9 | authors = ["Tao Guo "] 10 | edition = "2018" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | syn = { version = "2", features = ["full"] } 17 | quote = "1" 18 | proc-macro2 = "1" 19 | proc-macro-error2 = "2" 20 | 21 | [dev-dependencies] 22 | cmd_lib = { path = ".." } 23 | -------------------------------------------------------------------------------- /macros/src/lexer.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{ParseArg, Parser}; 2 | use proc_macro2::{token_stream, Delimiter, Ident, Literal, Span, TokenStream, TokenTree}; 3 | use proc_macro_error2::abort; 4 | use quote::quote; 5 | use std::ffi::OsString; 6 | use std::iter::Peekable; 7 | 8 | // Scan string literal to tokenstream, used by most of the macros 9 | // 10 | // - support ${var} or $var for interpolation 11 | // - to escape '$' itself, use "$$" 12 | // - support normal rust character escapes: 13 | // https://doc.rust-lang.org/reference/tokens.html#ascii-escapes 14 | pub fn scan_str_lit(lit: &Literal) -> TokenStream { 15 | let s = lit.to_string(); 16 | if !s.starts_with('\"') { 17 | return quote!(::cmd_lib::CmdString::from(#lit)); 18 | } 19 | let mut iter = s[1..s.len() - 1] // To trim outside "" 20 | .chars() 21 | .peekable(); 22 | let mut output = quote!(::cmd_lib::CmdString::default()); 23 | let mut last_part = OsString::new(); 24 | fn seal_last_part(last_part: &mut OsString, output: &mut TokenStream) { 25 | if !last_part.is_empty() { 26 | let lit_str = format!("\"{}\"", last_part.to_str().unwrap()); 27 | let l = syn::parse_str::(&lit_str).unwrap(); 28 | output.extend(quote!(.append(#l))); 29 | last_part.clear(); 30 | } 31 | } 32 | 33 | while let Some(ch) = iter.next() { 34 | if ch == '$' { 35 | if iter.peek() == Some(&'$') { 36 | iter.next(); 37 | last_part.push("$"); 38 | continue; 39 | } 40 | 41 | seal_last_part(&mut last_part, &mut output); 42 | let mut with_brace = false; 43 | if iter.peek() == Some(&'{') { 44 | with_brace = true; 45 | iter.next(); 46 | } 47 | let mut var = String::new(); 48 | while let Some(&c) = iter.peek() { 49 | if !c.is_ascii_alphanumeric() && c != '_' { 50 | break; 51 | } 52 | if var.is_empty() && c.is_ascii_digit() { 53 | break; 54 | } 55 | var.push(c); 56 | iter.next(); 57 | } 58 | if with_brace { 59 | if iter.peek() != Some(&'}') { 60 | abort!(lit.span(), "bad substitution"); 61 | } else { 62 | iter.next(); 63 | } 64 | } 65 | if !var.is_empty() { 66 | let var = syn::parse_str::(&var).unwrap(); 67 | output.extend(quote!(.append(#var.as_os_str()))); 68 | } else { 69 | output.extend(quote!(.append("$"))); 70 | } 71 | } else { 72 | last_part.push(ch.to_string()); 73 | } 74 | } 75 | seal_last_part(&mut last_part, &mut output); 76 | output 77 | } 78 | 79 | enum SepToken { 80 | Space, 81 | SemiColon, 82 | Pipe, 83 | } 84 | 85 | enum RedirectFd { 86 | Stdin, 87 | Stdout { append: bool }, 88 | Stderr { append: bool }, 89 | StdoutErr { append: bool }, 90 | } 91 | 92 | pub struct Lexer { 93 | iter: TokenStreamPeekable, 94 | args: Vec, 95 | last_arg_str: TokenStream, 96 | last_redirect: Option<(RedirectFd, Span)>, 97 | seen_redirect: (bool, bool, bool), 98 | } 99 | 100 | impl Lexer { 101 | pub fn new(input: TokenStream) -> Self { 102 | Self { 103 | args: vec![], 104 | last_arg_str: TokenStream::new(), 105 | last_redirect: None, 106 | seen_redirect: (false, false, false), 107 | iter: TokenStreamPeekable { 108 | peekable: input.into_iter().peekable(), 109 | span: Span::call_site(), 110 | }, 111 | } 112 | } 113 | 114 | pub fn scan(mut self) -> Parser> { 115 | while let Some(item) = self.iter.next() { 116 | match item { 117 | TokenTree::Group(_) => { 118 | abort!(self.iter.span(), "grouping is only allowed for variables"); 119 | } 120 | TokenTree::Literal(lit) => { 121 | self.scan_literal(lit); 122 | } 123 | TokenTree::Ident(ident) => { 124 | let s = ident.to_string(); 125 | self.extend_last_arg(quote!(#s)); 126 | } 127 | TokenTree::Punct(punct) => { 128 | let ch = punct.as_char(); 129 | if ch == ';' { 130 | self.add_arg_with_token(SepToken::SemiColon, self.iter.span()); 131 | } else if ch == '|' { 132 | self.scan_pipe(); 133 | } else if ch == '<' { 134 | self.set_redirect(self.iter.span(), RedirectFd::Stdin); 135 | } else if ch == '>' { 136 | self.scan_redirect_out(1); 137 | } else if ch == '&' { 138 | self.scan_ampersand(); 139 | } else if ch == '$' { 140 | self.scan_dollar(); 141 | } else { 142 | let s = ch.to_string(); 143 | self.extend_last_arg(quote!(#s)); 144 | } 145 | } 146 | } 147 | 148 | if self.iter.peek_no_gap().is_none() && !self.last_arg_str.is_empty() { 149 | self.add_arg_with_token(SepToken::Space, self.iter.span()); 150 | } 151 | } 152 | self.add_arg_with_token(SepToken::Space, self.iter.span()); 153 | Parser::from(self.args.into_iter().peekable()) 154 | } 155 | 156 | fn add_arg_with_token(&mut self, token: SepToken, token_span: Span) { 157 | let last_arg_str = &self.last_arg_str; 158 | if let Some((redirect, span)) = self.last_redirect.take() { 159 | if last_arg_str.is_empty() { 160 | abort!(span, "wrong redirection format: missing target"); 161 | } 162 | 163 | let mut stdouterr = false; 164 | let (fd, append) = match redirect { 165 | RedirectFd::Stdin => (0, false), 166 | RedirectFd::Stdout { append } => (1, append), 167 | RedirectFd::Stderr { append } => (2, append), 168 | RedirectFd::StdoutErr { append } => { 169 | stdouterr = true; 170 | (1, append) 171 | } 172 | }; 173 | self.args 174 | .push(ParseArg::RedirectFile(fd, quote!(#last_arg_str), append)); 175 | if stdouterr { 176 | self.args.push(ParseArg::RedirectFd(2, 1)); 177 | } 178 | } else if !last_arg_str.is_empty() { 179 | self.args.push(ParseArg::ArgStr(quote!(#last_arg_str))); 180 | } 181 | let mut new_redirect = (false, false, false); 182 | match token { 183 | SepToken::Space => new_redirect = self.seen_redirect, 184 | SepToken::SemiColon => self.args.push(ParseArg::Semicolon), 185 | SepToken::Pipe => { 186 | Self::check_set_redirect(&mut self.seen_redirect.1, "stdout", token_span); 187 | self.args.push(ParseArg::Pipe); 188 | new_redirect.0 = true; 189 | } 190 | } 191 | self.seen_redirect = new_redirect; 192 | self.last_arg_str = TokenStream::new(); 193 | } 194 | 195 | fn extend_last_arg(&mut self, stream: TokenStream) { 196 | if self.last_arg_str.is_empty() { 197 | self.last_arg_str = quote!(::cmd_lib::CmdString::default()); 198 | } 199 | self.last_arg_str.extend(quote!(.append(#stream))); 200 | } 201 | 202 | fn check_set_redirect(redirect: &mut bool, name: &str, span: Span) { 203 | if *redirect { 204 | abort!(span, "already set {} redirection", name); 205 | } 206 | *redirect = true; 207 | } 208 | 209 | fn set_redirect(&mut self, span: Span, fd: RedirectFd) { 210 | if self.last_redirect.is_some() { 211 | abort!(span, "wrong double redirection format"); 212 | } 213 | match fd { 214 | RedirectFd::Stdin => Self::check_set_redirect(&mut self.seen_redirect.0, "stdin", span), 215 | RedirectFd::Stdout { append: _ } => { 216 | Self::check_set_redirect(&mut self.seen_redirect.1, "stdout", span) 217 | } 218 | RedirectFd::Stderr { append: _ } => { 219 | Self::check_set_redirect(&mut self.seen_redirect.2, "stderr", span) 220 | } 221 | RedirectFd::StdoutErr { append: _ } => { 222 | Self::check_set_redirect(&mut self.seen_redirect.1, "stdout", span); 223 | Self::check_set_redirect(&mut self.seen_redirect.2, "stderr", span); 224 | } 225 | } 226 | self.last_redirect = Some((fd, span)); 227 | } 228 | 229 | fn scan_literal(&mut self, lit: Literal) { 230 | let s = lit.to_string(); 231 | if s.starts_with('\"') || s.starts_with('r') { 232 | // string literal 233 | let ss = scan_str_lit(&lit); 234 | self.extend_last_arg(quote!(#ss.into_os_string())); 235 | } else { 236 | let mut is_redirect = false; 237 | if s == "1" || s == "2" { 238 | if let Some(TokenTree::Punct(ref p)) = self.iter.peek_no_gap() { 239 | if p.as_char() == '>' { 240 | self.iter.next(); 241 | self.scan_redirect_out(if s == "1" { 1 } else { 2 }); 242 | is_redirect = true; 243 | } 244 | } 245 | } 246 | if !is_redirect { 247 | self.extend_last_arg(quote!(#s)); 248 | } 249 | } 250 | } 251 | 252 | fn scan_pipe(&mut self) { 253 | if let Some(TokenTree::Punct(p)) = self.iter.peek_no_gap() { 254 | if p.as_char() == '&' { 255 | if let Some(ref redirect) = self.last_redirect { 256 | abort!(redirect.1, "invalid '&': found previous redirect"); 257 | } 258 | Self::check_set_redirect(&mut self.seen_redirect.2, "stderr", p.span()); 259 | self.args.push(ParseArg::RedirectFd(2, 1)); 260 | self.iter.next(); 261 | } 262 | } 263 | 264 | // expect new command 265 | match self.iter.peek() { 266 | Some(TokenTree::Punct(np)) => { 267 | if np.as_char() == '|' || np.as_char() == ';' { 268 | abort!(np.span(), "expect new command after '|'"); 269 | } 270 | } 271 | None => { 272 | abort!(self.iter.span(), "expect new command after '|'"); 273 | } 274 | _ => {} 275 | } 276 | self.add_arg_with_token(SepToken::Pipe, self.iter.span()); 277 | } 278 | 279 | fn scan_redirect_out(&mut self, fd: i32) { 280 | let append = self.check_append(); 281 | self.set_redirect( 282 | self.iter.span(), 283 | if fd == 1 { 284 | RedirectFd::Stdout { append } 285 | } else { 286 | RedirectFd::Stderr { append } 287 | }, 288 | ); 289 | if let Some(TokenTree::Punct(p)) = self.iter.peek_no_gap() { 290 | if p.as_char() == '&' { 291 | if append { 292 | abort!(p.span(), "raw fd not allowed for append redirection"); 293 | } 294 | self.iter.next(); 295 | if let Some(TokenTree::Literal(lit)) = self.iter.peek_no_gap() { 296 | let s = lit.to_string(); 297 | if s.starts_with('\"') || s.starts_with('r') { 298 | abort!(lit.span(), "invalid literal string after &"); 299 | } 300 | if &s == "1" { 301 | self.args.push(ParseArg::RedirectFd(fd, 1)); 302 | } else if &s == "2" { 303 | self.args.push(ParseArg::RedirectFd(fd, 2)); 304 | } else { 305 | abort!(lit.span(), "Only &1 or &2 is supported"); 306 | } 307 | self.last_redirect = None; 308 | self.iter.next(); 309 | } else { 310 | abort!(self.iter.span(), "expect &1 or &2"); 311 | } 312 | } 313 | } 314 | } 315 | 316 | fn scan_ampersand(&mut self) { 317 | if let Some(tt) = self.iter.peek_no_gap() { 318 | if let TokenTree::Punct(p) = tt { 319 | let span = p.span(); 320 | if p.as_char() == '>' { 321 | self.iter.next(); 322 | let append = self.check_append(); 323 | self.set_redirect(span, RedirectFd::StdoutErr { append }); 324 | } else { 325 | abort!(span, "invalid punctuation"); 326 | } 327 | } else { 328 | abort!(tt.span(), "invalid format after '&'"); 329 | } 330 | } else if self.last_redirect.is_some() { 331 | abort!( 332 | self.iter.span(), 333 | "wrong redirection format: no spacing permitted before '&'" 334 | ); 335 | } else if self.iter.peek().is_some() { 336 | abort!(self.iter.span(), "invalid spacing after '&'"); 337 | } else { 338 | abort!(self.iter.span(), "invalid '&' at the end"); 339 | } 340 | } 341 | 342 | fn scan_dollar(&mut self) { 343 | let peek_no_gap = self.iter.peek_no_gap().map(|tt| tt.to_owned()); 344 | // let peek_no_gap = None; 345 | if let Some(TokenTree::Ident(var)) = peek_no_gap { 346 | self.extend_last_arg(quote!(#var.as_os_str())); 347 | } else if let Some(TokenTree::Group(g)) = peek_no_gap { 348 | if g.delimiter() != Delimiter::Brace && g.delimiter() != Delimiter::Bracket { 349 | abort!( 350 | g.span(), 351 | "invalid grouping: found {:?}, only \"brace/bracket\" is allowed", 352 | format!("{:?}", g.delimiter()).to_lowercase() 353 | ); 354 | } 355 | let mut found_var = false; 356 | for tt in g.stream() { 357 | let span = tt.span(); 358 | if let TokenTree::Ident(ref var) = tt { 359 | if found_var { 360 | abort!(span, "more than one variable in grouping"); 361 | } 362 | if g.delimiter() == Delimiter::Brace { 363 | self.extend_last_arg(quote!(#var.as_os_str())); 364 | } else { 365 | if !self.last_arg_str.is_empty() { 366 | abort!(span, "vector variable can only be used alone"); 367 | } 368 | self.args.push(ParseArg::ArgVec(quote!(#var))); 369 | } 370 | found_var = true; 371 | } else { 372 | abort!(span, "invalid grouping: extra tokens"); 373 | } 374 | } 375 | } else { 376 | abort!(self.iter.span(), "invalid token after $"); 377 | } 378 | self.iter.next(); 379 | } 380 | 381 | fn check_append(&mut self) -> bool { 382 | let mut append = false; 383 | if let Some(TokenTree::Punct(p)) = self.iter.peek_no_gap() { 384 | if p.as_char() == '>' { 385 | append = true; 386 | self.iter.next(); 387 | } 388 | } 389 | append 390 | } 391 | } 392 | 393 | struct TokenStreamPeekable> { 394 | peekable: Peekable, 395 | span: Span, 396 | } 397 | 398 | impl> Iterator for TokenStreamPeekable { 399 | type Item = I::Item; 400 | fn next(&mut self) -> Option { 401 | if let Some(tt) = self.peekable.next() { 402 | self.span = tt.span(); 403 | Some(tt) 404 | } else { 405 | None 406 | } 407 | } 408 | } 409 | 410 | impl> TokenStreamPeekable { 411 | fn peek(&mut self) -> Option<&TokenTree> { 412 | self.peekable.peek() 413 | } 414 | 415 | // peek next token which has no spaces between 416 | fn peek_no_gap(&mut self) -> Option<&TokenTree> { 417 | match self.peekable.peek() { 418 | None => None, 419 | Some(item) => { 420 | let (_, cur_end) = Self::span_location(&self.span); 421 | let (new_start, _) = Self::span_location(&item.span()); 422 | if new_start > cur_end { 423 | None 424 | } else { 425 | Some(item) 426 | } 427 | } 428 | } 429 | } 430 | 431 | fn span(&self) -> Span { 432 | self.span 433 | } 434 | 435 | // helper function to get (start, end) of Span 436 | fn span_location(span: &Span) -> (usize, usize) { 437 | let mut start = 0; 438 | let mut end = 0; 439 | let mut parse_start = true; 440 | format!("{:?}", span) // output is like this: #0 bytes(95..97) 441 | .chars() 442 | .skip_while(|c| *c != '(') 443 | .skip(1) 444 | .take_while(|c| *c != ')') 445 | .for_each(|c| { 446 | if c == '.' { 447 | parse_start = false; 448 | } else if c.is_ascii_digit() { 449 | let digit = c.to_digit(10).unwrap() as usize; 450 | if parse_start { 451 | start = start * 10 + digit; 452 | } else { 453 | end = end * 10 + digit; 454 | } 455 | } 456 | }); 457 | (start, end) 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{TokenStream, TokenTree}; 2 | use proc_macro_error2::{abort, proc_macro_error}; 3 | use quote::quote; 4 | 5 | /// Mark main function to log error result by default. 6 | /// 7 | /// ```no_run 8 | /// # use cmd_lib::*; 9 | /// 10 | /// #[cmd_lib::main] 11 | /// fn main() -> CmdResult { 12 | /// run_cmd!(bad_cmd)?; 13 | /// Ok(()) 14 | /// } 15 | /// // output: 16 | /// // [ERROR] FATAL: Running ["bad_cmd"] failed: No such file or directory (os error 2) 17 | /// ``` 18 | #[proc_macro_attribute] 19 | pub fn main( 20 | _args: proc_macro::TokenStream, 21 | item: proc_macro::TokenStream, 22 | ) -> proc_macro::TokenStream { 23 | let orig_function: syn::ItemFn = syn::parse2(item.into()).unwrap(); 24 | let orig_main_return_type = orig_function.sig.output; 25 | let orig_main_block = orig_function.block; 26 | 27 | quote! ( 28 | fn main() { 29 | fn cmd_lib_main() #orig_main_return_type { 30 | #orig_main_block 31 | } 32 | 33 | cmd_lib_main().unwrap_or_else(|err| { 34 | ::cmd_lib::error!("FATAL: {err}"); 35 | std::process::exit(1); 36 | }); 37 | } 38 | 39 | ) 40 | .into() 41 | } 42 | 43 | /// Import user registered custom command. 44 | /// ```no_run 45 | /// # use cmd_lib::*; 46 | /// # use std::io::Write; 47 | /// fn my_cmd(env: &mut CmdEnv) -> CmdResult { 48 | /// let msg = format!("msg from foo(), args: {:?}", env.get_args()); 49 | /// writeln!(env.stderr(), "{msg}")?; 50 | /// writeln!(env.stdout(), "bar") 51 | /// } 52 | /// 53 | /// use_custom_cmd!(my_cmd); 54 | /// run_cmd!(my_cmd)?; 55 | /// # Ok::<(), std::io::Error>(()) 56 | /// ``` 57 | /// Here we import the previous defined `my_cmd` command, so we can run it like a normal command. 58 | #[proc_macro] 59 | #[proc_macro_error] 60 | pub fn use_custom_cmd(item: proc_macro::TokenStream) -> proc_macro::TokenStream { 61 | let item: proc_macro2::TokenStream = item.into(); 62 | let mut cmd_fns = vec![]; 63 | for t in item { 64 | if let TokenTree::Punct(ref ch) = t { 65 | if ch.as_char() != ',' { 66 | abort!(t, "only comma is allowed"); 67 | } 68 | } else if let TokenTree::Ident(cmd) = t { 69 | let cmd_name = cmd.to_string(); 70 | cmd_fns.push(quote!(&#cmd_name, #cmd)); 71 | } else { 72 | abort!(t, "expect a list of comma separated commands"); 73 | } 74 | } 75 | 76 | quote! ( 77 | #(::cmd_lib::register_cmd(#cmd_fns);)* 78 | ) 79 | .into() 80 | } 81 | 82 | /// Run commands, returning [`CmdResult`](../cmd_lib/type.CmdResult.html) to check status. 83 | /// ```no_run 84 | /// # use cmd_lib::run_cmd; 85 | /// let msg = "I love rust"; 86 | /// run_cmd!(echo $msg)?; 87 | /// run_cmd!(echo "This is the message: $msg")?; 88 | /// 89 | /// // pipe commands are also supported 90 | /// run_cmd!(du -ah . | sort -hr | head -n 10)?; 91 | /// 92 | /// // or a group of commands 93 | /// // if any command fails, just return Err(...) 94 | /// let file = "/tmp/f"; 95 | /// let keyword = "rust"; 96 | /// if run_cmd! { 97 | /// cat ${file} | grep ${keyword}; 98 | /// echo "bad cmd" >&2; 99 | /// ignore ls /nofile; 100 | /// date; 101 | /// ls oops; 102 | /// cat oops; 103 | /// }.is_err() { 104 | /// // your error handling code 105 | /// } 106 | /// # Ok::<(), std::io::Error>(()) 107 | /// ``` 108 | #[proc_macro] 109 | #[proc_macro_error] 110 | pub fn run_cmd(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 111 | let cmds = lexer::Lexer::new(input.into()).scan().parse(false); 112 | quote! ({ 113 | use ::cmd_lib::AsOsStr; 114 | #cmds.run_cmd() 115 | }) 116 | .into() 117 | } 118 | 119 | /// Run commands, returning [`FunResult`](../cmd_lib/type.FunResult.html) to capture output and to check status. 120 | /// ```no_run 121 | /// # use cmd_lib::run_fun; 122 | /// let version = run_fun!(rustc --version)?; 123 | /// println!("Your rust version is {}", version); 124 | /// 125 | /// // with pipes 126 | /// let n = run_fun!(echo "the quick brown fox jumped over the lazy dog" | wc -w)?; 127 | /// println!("There are {} words in above sentence", n); 128 | /// # Ok::<(), std::io::Error>(()) 129 | /// ``` 130 | #[proc_macro] 131 | #[proc_macro_error] 132 | pub fn run_fun(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 133 | let cmds = lexer::Lexer::new(input.into()).scan().parse(false); 134 | quote! ({ 135 | use ::cmd_lib::AsOsStr; 136 | #cmds.run_fun() 137 | }) 138 | .into() 139 | } 140 | 141 | /// Run commands with/without pipes as a child process, returning [`CmdChildren`](../cmd_lib/struct.CmdChildren.html) result. 142 | /// ```no_run 143 | /// # use cmd_lib::*; 144 | /// 145 | /// let mut handle = spawn!(ping -c 10 192.168.0.1)?; 146 | /// // ... 147 | /// if handle.wait().is_err() { 148 | /// // ... 149 | /// } 150 | /// # Ok::<(), std::io::Error>(()) 151 | #[proc_macro] 152 | #[proc_macro_error] 153 | pub fn spawn(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 154 | let cmds = lexer::Lexer::new(input.into()).scan().parse(true); 155 | quote! ({ 156 | use ::cmd_lib::AsOsStr; 157 | #cmds.spawn(false) 158 | }) 159 | .into() 160 | } 161 | 162 | /// Run commands with/without pipes as a child process, returning [`FunChildren`](../cmd_lib/struct.FunChildren.html) result. 163 | /// ```no_run 164 | /// # use cmd_lib::*; 165 | /// let mut procs = vec![]; 166 | /// for _ in 0..4 { 167 | /// let proc = spawn_with_output!( 168 | /// sudo bash -c "dd if=/dev/nvmen0 of=/dev/null bs=4096 skip=0 count=1024 2>&1" 169 | /// | awk r#"/copied/{print $(NF-1) " " $NF}"# 170 | /// )?; 171 | /// procs.push(proc); 172 | /// } 173 | /// 174 | /// for (i, mut proc) in procs.into_iter().enumerate() { 175 | /// let bandwidth = proc.wait_with_output()?; 176 | /// info!("thread {i} bandwidth: {bandwidth} MB/s"); 177 | /// } 178 | /// # Ok::<(), std::io::Error>(()) 179 | /// ``` 180 | #[proc_macro] 181 | #[proc_macro_error] 182 | pub fn spawn_with_output(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 183 | let cmds = lexer::Lexer::new(input.into()).scan().parse(true); 184 | quote! ({ 185 | use ::cmd_lib::AsOsStr; 186 | #cmds.spawn_with_output() 187 | }) 188 | .into() 189 | } 190 | 191 | #[proc_macro] 192 | #[proc_macro_error] 193 | /// Log a fatal message at the error level, and exit process. 194 | /// 195 | /// e.g: 196 | /// ```no_run 197 | /// # use cmd_lib::*; 198 | /// let file = "bad_file"; 199 | /// cmd_die!("could not open file: $file"); 200 | /// // output: 201 | /// // [ERROR] FATAL: could not open file: bad_file 202 | /// ``` 203 | /// format should be string literals, and variable interpolation is supported. 204 | /// Note that this macro is just for convenience. The process will exit with 1 and print 205 | /// "FATAL: ..." messages to error console. If you want to exit with other code, you 206 | /// should probably define your own macro or functions. 207 | pub fn cmd_die(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 208 | let msg = parse_msg(input.into()); 209 | quote!({ 210 | ::cmd_lib::error!("FATAL: {} at {}:{}", #msg, file!(), line!()); 211 | std::process::exit(1) 212 | }) 213 | .into() 214 | } 215 | 216 | fn parse_msg(input: TokenStream) -> TokenStream { 217 | let mut iter = input.into_iter(); 218 | let mut output = TokenStream::new(); 219 | let mut valid = false; 220 | if let Some(ref tt) = iter.next() { 221 | if let TokenTree::Literal(lit) = tt { 222 | let s = lit.to_string(); 223 | if s.starts_with('\"') || s.starts_with('r') { 224 | let str_lit = lexer::scan_str_lit(lit); 225 | output.extend(quote!(#str_lit)); 226 | valid = true; 227 | } 228 | } 229 | if !valid { 230 | abort!(tt, "invalid format: expect string literal"); 231 | } 232 | if let Some(tt) = iter.next() { 233 | abort!( 234 | tt, 235 | "expect string literal only, found extra {}", 236 | tt.to_string() 237 | ); 238 | } 239 | } 240 | output 241 | } 242 | 243 | mod lexer; 244 | mod parser; 245 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/builtins.rs: -------------------------------------------------------------------------------- 1 | use crate::{debug, error, info, trace, warn}; 2 | use crate::{CmdEnv, CmdResult}; 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 | -------------------------------------------------------------------------------- /src/child.rs: -------------------------------------------------------------------------------- 1 | use crate::{info, warn}; 2 | use crate::{process, CmdResult, FunResult}; 3 | use os_pipe::PipeReader; 4 | use std::io::{BufRead, BufReader, Error, ErrorKind, Read, Result}; 5 | use std::process::{Child, ExitStatus}; 6 | use std::thread::JoinHandle; 7 | 8 | /// Representation of running or exited children processes, connected with pipes 9 | /// optionally. 10 | /// 11 | /// Calling [`spawn!`](../cmd_lib/macro.spawn.html) macro will return `Result` 12 | pub struct CmdChildren { 13 | children: Vec, 14 | ignore_error: bool, 15 | } 16 | 17 | impl CmdChildren { 18 | pub(crate) fn new(children: Vec, ignore_error: bool) -> Self { 19 | Self { 20 | children, 21 | ignore_error, 22 | } 23 | } 24 | 25 | pub(crate) fn into_fun_children(self) -> FunChildren { 26 | FunChildren { 27 | children: self.children, 28 | ignore_error: self.ignore_error, 29 | } 30 | } 31 | 32 | /// Waits for the children processes to exit completely, returning the status that they exited with. 33 | pub fn wait(&mut self) -> CmdResult { 34 | // wait for the last child result 35 | let handle = self.children.pop().unwrap(); 36 | if let Err(e) = handle.wait(true) { 37 | let _ = Self::wait_children(&mut self.children); 38 | return Err(e); 39 | } 40 | Self::wait_children(&mut self.children) 41 | } 42 | 43 | fn wait_children(children: &mut Vec) -> CmdResult { 44 | let mut ret = Ok(()); 45 | while let Some(child_handle) = children.pop() { 46 | if let Err(e) = child_handle.wait(false) { 47 | ret = Err(e); 48 | } 49 | } 50 | ret 51 | } 52 | 53 | /// Forces the children processes to exit. 54 | pub fn kill(&mut self) -> CmdResult { 55 | let mut ret = Ok(()); 56 | while let Some(child_handle) = self.children.pop() { 57 | if let Err(e) = child_handle.kill() { 58 | ret = Err(e); 59 | } 60 | } 61 | ret 62 | } 63 | 64 | /// Returns the OS-assigned process identifiers associated with these children processes 65 | pub fn pids(&self) -> Vec { 66 | self.children.iter().filter_map(|x| x.pid()).collect() 67 | } 68 | } 69 | 70 | /// Representation of running or exited children processes with output, connected with pipes 71 | /// optionally. 72 | /// 73 | /// Calling [spawn_with_output!](../cmd_lib/macro.spawn_with_output.html) macro will return `Result` 74 | pub struct FunChildren { 75 | children: Vec, 76 | ignore_error: bool, 77 | } 78 | 79 | impl FunChildren { 80 | /// Waits for the children processes to exit completely, returning the command result, stdout 81 | /// content string and stderr content string. 82 | pub fn wait_with_all(&mut self) -> (CmdResult, String, String) { 83 | self.inner_wait_with_all(true) 84 | } 85 | 86 | /// Waits for the children processes to exit completely, returning the stdout output. 87 | pub fn wait_with_output(&mut self) -> FunResult { 88 | let (res, stdout, _) = self.inner_wait_with_all(false); 89 | if let Err(e) = res { 90 | if !self.ignore_error { 91 | return Err(e); 92 | } 93 | } 94 | Ok(stdout) 95 | } 96 | 97 | /// Waits for the children processes to exit completely, and read all bytes from stdout into `buf`. 98 | pub fn wait_with_raw_output(&mut self, buf: &mut Vec) -> CmdResult { 99 | // wait for the last child result 100 | let handle = self.children.pop().unwrap(); 101 | let wait_last = handle.wait_with_raw_output(self.ignore_error, buf); 102 | match wait_last { 103 | Err(e) => { 104 | let _ = CmdChildren::wait_children(&mut self.children); 105 | Err(e) 106 | } 107 | Ok(_) => { 108 | let ret = CmdChildren::wait_children(&mut self.children); 109 | if self.ignore_error { 110 | Ok(()) 111 | } else { 112 | ret 113 | } 114 | } 115 | } 116 | } 117 | 118 | /// Waits for the children processes to exit completely, pipe content will be processed by 119 | /// provided function. 120 | pub fn wait_with_pipe(&mut self, f: &mut dyn FnMut(Box)) -> CmdResult { 121 | let child = self.children.pop().unwrap(); 122 | let stderr_thread = 123 | StderrThread::new(&child.cmd, &child.file, child.line, child.stderr, false); 124 | match child.handle { 125 | CmdChildHandle::Proc(mut proc) => { 126 | if let Some(stdout) = child.stdout { 127 | f(Box::new(stdout)); 128 | let _ = proc.kill(); 129 | } 130 | } 131 | CmdChildHandle::Thread(_) => { 132 | if let Some(stdout) = child.stdout { 133 | f(Box::new(stdout)); 134 | } 135 | } 136 | CmdChildHandle::SyncFn => { 137 | if let Some(stdout) = child.stdout { 138 | f(Box::new(stdout)); 139 | } 140 | } 141 | }; 142 | drop(stderr_thread); 143 | CmdChildren::wait_children(&mut self.children) 144 | } 145 | 146 | /// Returns the OS-assigned process identifiers associated with these children processes. 147 | pub fn pids(&self) -> Vec { 148 | self.children.iter().filter_map(|x| x.pid()).collect() 149 | } 150 | 151 | fn inner_wait_with_all(&mut self, capture_stderr: bool) -> (CmdResult, String, String) { 152 | // wait for the last child result 153 | let last_handle = self.children.pop().unwrap(); 154 | let mut stdout_buf = Vec::new(); 155 | let mut stderr = String::new(); 156 | let last_res = last_handle.wait_with_all(capture_stderr, &mut stdout_buf, &mut stderr); 157 | let res = CmdChildren::wait_children(&mut self.children); 158 | let mut stdout: String = String::from_utf8_lossy(&stdout_buf).into(); 159 | if stdout.ends_with('\n') { 160 | stdout.pop(); 161 | } 162 | if res.is_err() && !self.ignore_error && process::pipefail_enabled() { 163 | (res, stdout, stderr) 164 | } else { 165 | (last_res, stdout, stderr) 166 | } 167 | } 168 | } 169 | 170 | pub(crate) struct CmdChild { 171 | handle: CmdChildHandle, 172 | cmd: String, 173 | file: String, 174 | line: u32, 175 | stdout: Option, 176 | stderr: Option, 177 | } 178 | 179 | impl CmdChild { 180 | pub(crate) fn new( 181 | handle: CmdChildHandle, 182 | cmd: String, 183 | file: String, 184 | line: u32, 185 | stdout: Option, 186 | stderr: Option, 187 | ) -> Self { 188 | Self { 189 | file, 190 | line, 191 | handle, 192 | cmd, 193 | stdout, 194 | stderr, 195 | } 196 | } 197 | 198 | fn wait(mut self, is_last: bool) -> CmdResult { 199 | let _stderr_thread = 200 | StderrThread::new(&self.cmd, &self.file, self.line, self.stderr.take(), false); 201 | let res = self.handle.wait(&self.cmd, &self.file, self.line); 202 | if let Err(e) = res { 203 | if is_last || process::pipefail_enabled() { 204 | return Err(e); 205 | } 206 | } 207 | Ok(()) 208 | } 209 | 210 | fn wait_with_raw_output(self, ignore_error: bool, stdout_buf: &mut Vec) -> CmdResult { 211 | let mut _stderr = String::new(); 212 | let res = self.wait_with_all(false, stdout_buf, &mut _stderr); 213 | if ignore_error { 214 | return Ok(()); 215 | } 216 | res 217 | } 218 | 219 | fn wait_with_all( 220 | mut self, 221 | capture_stderr: bool, 222 | stdout_buf: &mut Vec, 223 | stderr_buf: &mut String, 224 | ) -> CmdResult { 225 | let mut stderr_thread = StderrThread::new( 226 | &self.cmd, 227 | &self.file, 228 | self.line, 229 | self.stderr.take(), 230 | capture_stderr, 231 | ); 232 | let mut stdout_res = Ok(()); 233 | if let Some(mut stdout) = self.stdout.take() { 234 | if let Err(e) = stdout.read_to_end(stdout_buf) { 235 | stdout_res = Err(e) 236 | } 237 | } 238 | *stderr_buf = stderr_thread.join(); 239 | let wait_res = self.handle.wait(&self.cmd, &self.file, self.line); 240 | wait_res.and(stdout_res) 241 | } 242 | 243 | fn kill(self) -> CmdResult { 244 | self.handle.kill(&self.cmd, &self.file, self.line) 245 | } 246 | 247 | fn pid(&self) -> Option { 248 | self.handle.pid() 249 | } 250 | } 251 | 252 | pub(crate) enum CmdChildHandle { 253 | Proc(Child), 254 | Thread(JoinHandle), 255 | SyncFn, 256 | } 257 | 258 | impl CmdChildHandle { 259 | fn wait(self, cmd: &str, file: &str, line: u32) -> CmdResult { 260 | match self { 261 | CmdChildHandle::Proc(mut proc) => { 262 | let status = proc.wait(); 263 | match status { 264 | Err(e) => return Err(process::new_cmd_io_error(&e, cmd, file, line)), 265 | Ok(status) => { 266 | if !status.success() { 267 | return Err(Self::status_to_io_error(status, cmd, file, line)); 268 | } 269 | } 270 | } 271 | } 272 | CmdChildHandle::Thread(thread) => { 273 | let status = thread.join(); 274 | match status { 275 | Ok(result) => { 276 | if let Err(e) = result { 277 | return Err(process::new_cmd_io_error(&e, cmd, file, line)); 278 | } 279 | } 280 | Err(e) => { 281 | return Err(Error::new( 282 | ErrorKind::Other, 283 | format!( 284 | "Running [{cmd}] thread joined with error: {e:?} at {file}:{line}" 285 | ), 286 | )) 287 | } 288 | } 289 | } 290 | CmdChildHandle::SyncFn => {} 291 | } 292 | Ok(()) 293 | } 294 | 295 | fn status_to_io_error(status: ExitStatus, cmd: &str, file: &str, line: u32) -> Error { 296 | if let Some(code) = status.code() { 297 | Error::new( 298 | ErrorKind::Other, 299 | format!("Running [{cmd}] exited with error; status code: {code} at {file}:{line}"), 300 | ) 301 | } else { 302 | Error::new( 303 | ErrorKind::Other, 304 | format!( 305 | "Running [{cmd}] exited with error; terminated by {status} at {file}:{line}" 306 | ), 307 | ) 308 | } 309 | } 310 | 311 | fn kill(self, cmd: &str, file: &str, line: u32) -> CmdResult { 312 | match self { 313 | CmdChildHandle::Proc(mut proc) => proc.kill().map_err(|e| { 314 | Error::new( 315 | e.kind(), 316 | format!("Killing process [{cmd}] failed with error: {e} at {file}:{line}"), 317 | ) 318 | }), 319 | CmdChildHandle::Thread(_thread) => Err(Error::new( 320 | ErrorKind::Other, 321 | format!("Killing thread [{cmd}] failed: not supported at {file}:{line}"), 322 | )), 323 | CmdChildHandle::SyncFn => Ok(()), 324 | } 325 | } 326 | 327 | fn pid(&self) -> Option { 328 | match self { 329 | CmdChildHandle::Proc(proc) => Some(proc.id()), 330 | _ => None, 331 | } 332 | } 333 | } 334 | 335 | struct StderrThread { 336 | thread: Option>, 337 | cmd: String, 338 | file: String, 339 | line: u32, 340 | } 341 | 342 | impl StderrThread { 343 | fn new(cmd: &str, file: &str, line: u32, stderr: Option, capture: bool) -> Self { 344 | if let Some(stderr) = stderr { 345 | let thread = std::thread::spawn(move || { 346 | let mut output = String::new(); 347 | BufReader::new(stderr) 348 | .lines() 349 | .map_while(Result::ok) 350 | .for_each(|line| { 351 | if !capture { 352 | info!("{line}"); 353 | } else { 354 | if !output.is_empty() { 355 | output.push('\n'); 356 | } 357 | output.push_str(&line); 358 | } 359 | }); 360 | output 361 | }); 362 | Self { 363 | cmd: cmd.into(), 364 | file: file.into(), 365 | line, 366 | thread: Some(thread), 367 | } 368 | } else { 369 | Self { 370 | cmd: cmd.into(), 371 | file: file.into(), 372 | line, 373 | thread: None, 374 | } 375 | } 376 | } 377 | 378 | fn join(&mut self) -> String { 379 | if let Some(thread) = self.thread.take() { 380 | match thread.join() { 381 | Err(e) => { 382 | warn!( 383 | "Running [{}] stderr thread joined with error: {:?} at {}:{}", 384 | self.cmd, e, self.file, self.line 385 | ); 386 | } 387 | Ok(output) => return output, 388 | } 389 | } 390 | "".into() 391 | } 392 | } 393 | 394 | impl Drop for StderrThread { 395 | fn drop(&mut self) { 396 | self.join(); 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 looks like 34 | //! 35 | //! To get a first impression, here is an example from 36 | //! [examples/dd_test.rs](https://github.com/rust-shell-script/rust_cmd_lib/blob/master/examples/dd_test.rs): 37 | //! 38 | //! ```no_run 39 | //! # use byte_unit::Byte; 40 | //! # use cmd_lib::*; 41 | //! # use rayon::prelude::*; 42 | //! # use std::time::Instant; 43 | //! # const DATA_SIZE: u64 = 10 * 1024 * 1024 * 1024; // 10GB data 44 | //! # let mut file = String::new(); 45 | //! # let mut block_size: u64 = 4096; 46 | //! # let mut thread_num: u64 = 1; 47 | //! run_cmd! ( 48 | //! info "Dropping caches at first"; 49 | //! sudo bash -c "echo 3 > /proc/sys/vm/drop_caches"; 50 | //! info "Running with thread_num: $thread_num, block_size: $block_size"; 51 | //! )?; 52 | //! let cnt = DATA_SIZE / thread_num / block_size; 53 | //! let now = Instant::now(); 54 | //! (0..thread_num).into_par_iter().for_each(|i| { 55 | //! let off = cnt * i; 56 | //! let bandwidth = run_fun!( 57 | //! sudo bash -c "dd if=$file of=/dev/null bs=$block_size skip=$off count=$cnt 2>&1" 58 | //! | awk r#"/copied/{print $(NF-1) " " $NF}"# 59 | //! ) 60 | //! .unwrap_or_else(|_| cmd_die!("thread $i failed")); 61 | //! info!("thread {i} bandwidth: {bandwidth}"); 62 | //! }); 63 | //! let total_bandwidth = Byte::from_bytes((DATA_SIZE / now.elapsed().as_secs()) as u128).get_appropriate_unit(true); 64 | //! info!("Total bandwidth: {total_bandwidth}/s"); 65 | //! # Ok::<(), std::io::Error>(()) 66 | //! ``` 67 | //! 68 | //! Output will be like this: 69 | //! 70 | //! ```console 71 | //! ➜ rust_cmd_lib git:(master) ✗ cargo run --example dd_test -- -b 4096 -f /dev/nvme0n1 -t 4 72 | //! Finished dev [unoptimized + debuginfo] target(s) in 0.04s 73 | //! Running `target/debug/examples/dd_test -b 4096 -f /dev/nvme0n1 -t 4` 74 | //! [INFO ] Dropping caches at first 75 | //! [INFO ] Running with thread_num: 4, block_size: 4096 76 | //! [INFO ] thread 3 bandwidth: 317 MB/s 77 | //! [INFO ] thread 1 bandwidth: 289 MB/s 78 | //! [INFO ] thread 0 bandwidth: 281 MB/s 79 | //! [INFO ] thread 2 bandwidth: 279 MB/s 80 | //! [INFO ] Total bandwidth: 1.11 GiB/s 81 | //! ``` 82 | //! 83 | //! ## What this library provides 84 | //! 85 | //! ### Macros to run external commands 86 | //! - [`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) 87 | //! 88 | //! ```no_run 89 | //! # use cmd_lib::run_cmd; 90 | //! let msg = "I love rust"; 91 | //! run_cmd!(echo $msg)?; 92 | //! run_cmd!(echo "This is the message: $msg")?; 93 | //! 94 | //! // pipe commands are also supported 95 | //! let dir = "/var/log"; 96 | //! run_cmd!(du -ah $dir | sort -hr | head -n 10)?; 97 | //! 98 | //! // or a group of commands 99 | //! // if any command fails, just return Err(...) 100 | //! let file = "/tmp/f"; 101 | //! let keyword = "rust"; 102 | //! run_cmd! { 103 | //! cat ${file} | grep ${keyword}; 104 | //! echo "bad cmd" >&2; 105 | //! ignore ls /nofile; 106 | //! date; 107 | //! ls oops; 108 | //! cat oops; 109 | //! }?; 110 | //! # Ok::<(), std::io::Error>(()) 111 | //! ``` 112 | //! 113 | //! - [`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) 114 | //! 115 | //! ``` 116 | //! # use cmd_lib::run_fun; 117 | //! let version = run_fun!(rustc --version)?; 118 | //! eprintln!("Your rust version is {}", version); 119 | //! 120 | //! // with pipes 121 | //! let n = run_fun!(echo "the quick brown fox jumped over the lazy dog" | wc -w)?; 122 | //! eprintln!("There are {} words in above sentence", n); 123 | //! # Ok::<(), std::io::Error>(()) 124 | //! ``` 125 | //! 126 | //! ### Abstraction without overhead 127 | //! 128 | //! Since all the macros' lexical analysis and syntactic analysis happen at compile time, it can 129 | //! basically generate code the same as calling `std::process` APIs manually. It also includes 130 | //! command type checking, so most of the errors can be found at compile time instead of at 131 | //! runtime. With tools like `rust-analyzer`, it can give you real-time feedback for broken 132 | //! commands being used. 133 | //! 134 | //! You can use `cargo expand` to check the generated code. 135 | //! 136 | //! ### Intuitive parameters passing 137 | //! When passing parameters to `run_cmd!` and `run_fun!` macros, if they are not part to rust 138 | //! [String literals](https://doc.rust-lang.org/reference/tokens.html#string-literals), they will be 139 | //! converted to string as an atomic component, so you don't need to quote them. The parameters will be 140 | //! like `$a` or `${a}` in `run_cmd!` or `run_fun!` macros. 141 | //! 142 | //! ```no_run 143 | //! # use cmd_lib::run_cmd; 144 | //! let dir = "my folder"; 145 | //! run_cmd!(echo "Creating $dir at /tmp")?; 146 | //! run_cmd!(mkdir -p /tmp/$dir)?; 147 | //! 148 | //! // or with group commands: 149 | //! let dir = "my folder"; 150 | //! run_cmd!(echo "Creating $dir at /tmp"; mkdir -p /tmp/$dir)?; 151 | //! # Ok::<(), std::io::Error>(()) 152 | //! ``` 153 | //! You can consider "" as glue, so everything inside the quotes will be treated as a single atomic component. 154 | //! 155 | //! If they are part of [Raw string literals](https://doc.rust-lang.org/reference/tokens.html#raw-string-literals), 156 | //! there will be no string interpolation, the same as in idiomatic rust. However, you can always use `format!` macro 157 | //! to form the new string. For example: 158 | //! ```no_run 159 | //! # use cmd_lib::run_cmd; 160 | //! // string interpolation 161 | //! let key_word = "time"; 162 | //! let awk_opts = format!(r#"/{}/ {{print $(NF-3) " " $(NF-1) " " $NF}}"#, key_word); 163 | //! run_cmd!(ping -c 10 www.google.com | awk $awk_opts)?; 164 | //! # Ok::<(), std::io::Error>(()) 165 | //! ``` 166 | //! Notice here `$awk_opts` will be treated as single option passing to awk command. 167 | //! 168 | //! If you want to use dynamic parameters, you can use `$[]` to access vector variable: 169 | //! ```no_run 170 | //! # use cmd_lib::run_cmd; 171 | //! let gopts = vec![vec!["-l", "-a", "/"], vec!["-a", "/var"]]; 172 | //! for opts in gopts { 173 | //! run_cmd!(ls $[opts])?; 174 | //! } 175 | //! # Ok::<(), std::io::Error>(()) 176 | //! ``` 177 | //! 178 | //! ### Redirection and Piping 179 | //! Right now piping and stdin, stdout, stderr redirection are supported. Most parts are the same as in 180 | //! [bash scripts](https://www.gnu.org/software/bash/manual/html_node/Redirections.html#Redirections). 181 | //! 182 | //! ### Logging 183 | //! 184 | //! This library provides convenient macros and builtin commands for logging. All messages which 185 | //! are printed to stderr will be logged. It will also include the full running commands in the error 186 | //! result. 187 | //! 188 | //! ```no_run 189 | //! # use cmd_lib::*; 190 | //! let dir: &str = "folder with spaces"; 191 | //! run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir)?; 192 | //! run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir; rmdir /tmp/$dir)?; 193 | //! // output: 194 | //! // [INFO ] mkdir: cannot create directory ‘/tmp/folder with spaces’: File exists 195 | //! // Error: Running ["mkdir" "/tmp/folder with spaces"] exited with error; status code: 1 196 | //! # Ok::<(), std::io::Error>(()) 197 | //! ``` 198 | //! 199 | //! It is using rust [log crate](https://crates.io/crates/log), and you can use your actual favorite 200 | //! logger implementation. Notice that if you don't provide any logger, it will use env_logger to print 201 | //! messages from process's stderr. 202 | //! 203 | //! You can also mark your `main()` function with `#[cmd_lib::main]`, which will log error from 204 | //! main() by default. Like this: 205 | //! ```console 206 | //! [ERROR] FATAL: Running ["mkdir" "/tmp/folder with spaces"] exited with error; status code: 1 207 | //! ``` 208 | //! 209 | //! ### Builtin commands 210 | //! #### cd 211 | //! cd: set process current directory. 212 | //! ```no_run 213 | //! # use cmd_lib::run_cmd; 214 | //! run_cmd! ( 215 | //! cd /tmp; 216 | //! ls | wc -l; 217 | //! )?; 218 | //! # Ok::<(), std::io::Error>(()) 219 | //! ``` 220 | //! Notice that builtin `cd` will only change with current scope 221 | //! and it will restore the previous current directory when it 222 | //! exits the scope. 223 | //! 224 | //! Use `std::env::set_current_dir` if you want to change the current 225 | //! working directory for the whole program. 226 | //! 227 | //! #### ignore 228 | //! 229 | //! Ignore errors for command execution. 230 | //! 231 | //! #### echo 232 | //! Print messages to stdout. 233 | //! ```console 234 | //! -n do not output the trailing newline 235 | //! ``` 236 | //! 237 | //! #### error, warn, info, debug, trace 238 | //! 239 | //! Print messages to logging with different levels. You can also use the normal logging macros, 240 | //! if you don't need to do logging inside the command group. 241 | //! 242 | //! ```no_run 243 | //! # use cmd_lib::*; 244 | //! run_cmd!(error "This is an error message")?; 245 | //! run_cmd!(warn "This is a warning message")?; 246 | //! run_cmd!(info "This is an information message")?; 247 | //! // output: 248 | //! // [ERROR] This is an error message 249 | //! // [WARN ] This is a warning message 250 | //! // [INFO ] This is an information message 251 | //! # Ok::<(), std::io::Error>(()) 252 | //! ``` 253 | //! 254 | //! ### Low-level process spawning macros 255 | //! 256 | //! [`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 257 | //! default, stdin, stdout and stderr are inherited from the parent. The process will run in the 258 | //! 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 259 | //! for the process to finish. 260 | //! 261 | //! With [`spawn_with_output!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.spawn_with_output.html) you can get output by calling 262 | //! [`wait_with_output()`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.FunChildren.html#method.wait_with_output), 263 | //! [`wait_with_all()`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.FunChildren.html#method.wait_with_all) 264 | //! or even do stream 265 | //! processing with [`wait_with_pipe()`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.FunChildren.html#method.wait_with_pipe). 266 | //! 267 | //! There are also other useful APIs, and you can check the docs for more details. 268 | //! 269 | //! ```no_run 270 | //! # use cmd_lib::*; 271 | //! # use std::io::{BufRead, BufReader}; 272 | //! let mut proc = spawn!(ping -c 10 192.168.0.1)?; 273 | //! // do other stuff 274 | //! // ... 275 | //! proc.wait()?; 276 | //! 277 | //! let mut proc = spawn_with_output!(/bin/cat file.txt | sed s/a/b/)?; 278 | //! // do other stuff 279 | //! // ... 280 | //! let output = proc.wait_with_output()?; 281 | //! 282 | //! spawn_with_output!(journalctl)?.wait_with_pipe(&mut |pipe| { 283 | //! BufReader::new(pipe) 284 | //! .lines() 285 | //! .filter_map(|line| line.ok()) 286 | //! .filter(|line| line.find("usb").is_some()) 287 | //! .take(10) 288 | //! .for_each(|line| println!("{}", line)); 289 | //! })?; 290 | //! # Ok::<(), std::io::Error>(()) 291 | //! ``` 292 | //! 293 | //! ### Macro to register your own commands 294 | //! 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: 295 | //! 296 | //! ``` 297 | //! # use cmd_lib::*; 298 | //! # use std::io::Write; 299 | //! fn my_cmd(env: &mut CmdEnv) -> CmdResult { 300 | //! let args = env.get_args(); 301 | //! let (res, stdout, stderr) = spawn_with_output! { 302 | //! orig_cmd $[args] 303 | //! --long-option xxx 304 | //! --another-option yyy 305 | //! }? 306 | //! .wait_with_all(); 307 | //! writeln!(env.stdout(), "{}", stdout)?; 308 | //! writeln!(env.stderr(), "{}", stderr)?; 309 | //! res 310 | //! } 311 | //! 312 | //! use_custom_cmd!(my_cmd); 313 | //! # Ok::<(), std::io::Error>(()) 314 | //! ``` 315 | //! 316 | //! ### Macros to define, get and set thread-local global variables 317 | //! - [`tls_init!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_init.html) to define thread local global variable 318 | //! - [`tls_get!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_get.html) to get the value 319 | //! - [`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) to set the value 320 | //! ``` 321 | //! # use cmd_lib::{ tls_init, tls_get, tls_set }; 322 | //! tls_init!(DELAY, f64, 1.0); 323 | //! const DELAY_FACTOR: f64 = 0.8; 324 | //! tls_set!(DELAY, |d| *d *= DELAY_FACTOR); 325 | //! let d = tls_get!(DELAY); 326 | //! // check more examples in examples/tetris.rs 327 | //! ``` 328 | //! 329 | //! ## Other Notes 330 | //! 331 | //! ### Environment Variables 332 | //! 333 | //! You can use [std::env::var](https://doc.rust-lang.org/std/env/fn.var.html) to fetch the environment variable 334 | //! key from the current process. It will report error if the environment variable is not present, and it also 335 | //! includes other checks to avoid silent failures. 336 | //! 337 | //! To set environment variables, you can use [std::env::set_var](https://doc.rust-lang.org/std/env/fn.set_var.html). 338 | //! There are also other related APIs in the [std::env](https://doc.rust-lang.org/std/env/index.html) module. 339 | //! 340 | //! To set environment variables for the command only, you can put the assignments before the command. 341 | //! Like this: 342 | //! ```no_run 343 | //! # use cmd_lib::run_cmd; 344 | //! run_cmd!(FOO=100 /tmp/test_run_cmd_lib.sh)?; 345 | //! # Ok::<(), std::io::Error>(()) 346 | //! ``` 347 | //! 348 | //! ### Security Notes 349 | //! Using macros can actually avoid command injection, since we do parsing before variable substitution. 350 | //! For example, below code is fine even without any quotes: 351 | //! ``` 352 | //! # use cmd_lib::{run_cmd, CmdResult}; 353 | //! # use std::path::Path; 354 | //! fn cleanup_uploaded_file(file: &Path) -> CmdResult { 355 | //! run_cmd!(/bin/rm -f /var/upload/$file) 356 | //! } 357 | //! ``` 358 | //! It is not the case in bash, which will always do variable substitution at first. 359 | //! 360 | //! ### Glob/Wildcard 361 | //! 362 | //! This library does not provide glob functions, to avoid silent errors and other surprises. 363 | //! You can use the [glob](https://github.com/rust-lang-nursery/glob) package instead. 364 | //! 365 | //! ### Thread Safety 366 | //! 367 | //! This library tries very hard to not set global states, so parallel `cargo test` can be executed just fine. 368 | //! The only known APIs not supported in multi-thread environment are the 369 | //! [`tls_init!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_init.html)/[`tls_get!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_get.html)/[`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) macros, and you should only use them for *thread local* variables. 370 | //! 371 | 372 | pub use cmd_lib_macros::{ 373 | cmd_die, main, run_cmd, run_fun, spawn, spawn_with_output, use_custom_cmd, 374 | }; 375 | /// Return type for [`run_fun!()`] macro. 376 | pub type FunResult = std::io::Result; 377 | /// Return type for [`run_cmd!()`] macro. 378 | pub type CmdResult = std::io::Result<()>; 379 | pub use child::{CmdChildren, FunChildren}; 380 | pub use io::{CmdIn, CmdOut}; 381 | #[doc(hidden)] 382 | pub use log as inner_log; 383 | #[doc(hidden)] 384 | pub use logger::try_init_default_logger; 385 | #[doc(hidden)] 386 | pub use process::{register_cmd, AsOsStr, Cmd, CmdString, Cmds, GroupCmds, Redirect}; 387 | pub use process::{set_debug, set_pipefail, CmdEnv}; 388 | 389 | mod builtins; 390 | mod child; 391 | mod io; 392 | mod logger; 393 | mod process; 394 | mod thread_local; 395 | -------------------------------------------------------------------------------- /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 | $crate::inner_log::debug!($($arg)*); 43 | }} 44 | } 45 | 46 | #[doc(hidden)] 47 | #[macro_export] 48 | macro_rules! trace { 49 | ($($arg:tt)*) => {{ 50 | $crate::try_init_default_logger(); 51 | $crate::inner_log::trace!($($arg)*); 52 | }} 53 | } 54 | -------------------------------------------------------------------------------- /src/process.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins::*; 2 | use crate::child::{CmdChild, CmdChildHandle, CmdChildren, FunChildren}; 3 | use crate::io::{CmdIn, CmdOut}; 4 | use crate::{debug, warn}; 5 | use crate::{CmdResult, FunResult}; 6 | use faccess::{AccessMode, PathExt}; 7 | use lazy_static::lazy_static; 8 | use os_pipe::{self, PipeReader, PipeWriter}; 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, ErrorKind, Result}; 14 | use std::path::{Path, PathBuf}; 15 | use std::process::Command; 16 | use std::sync::Mutex; 17 | use std::thread; 18 | 19 | const CD_CMD: &str = "cd"; 20 | const IGNORE_CMD: &str = "ignore"; 21 | 22 | /// Environment for builtin or custom commands. 23 | pub struct CmdEnv { 24 | stdin: CmdIn, 25 | stdout: CmdOut, 26 | stderr: CmdOut, 27 | args: Vec, 28 | vars: HashMap, 29 | current_dir: PathBuf, 30 | } 31 | impl CmdEnv { 32 | /// Returns the name of this command. 33 | pub fn get_cmd_name(&self) -> &str { 34 | &self.args[0] 35 | } 36 | 37 | /// Returns the arguments for this command. 38 | pub fn get_args(&self) -> &[String] { 39 | &self.args[1..] 40 | } 41 | 42 | /// Fetches the environment variable key for this command. 43 | pub fn var(&self, key: &str) -> Option<&String> { 44 | self.vars.get(key) 45 | } 46 | 47 | /// Returns the current working directory for this command. 48 | pub fn current_dir(&self) -> &Path { 49 | &self.current_dir 50 | } 51 | 52 | /// Returns a new handle to the standard input for this command. 53 | pub fn stdin(&mut self) -> &mut CmdIn { 54 | &mut self.stdin 55 | } 56 | 57 | /// Returns a new handle to the standard output for this command. 58 | pub fn stdout(&mut self) -> &mut CmdOut { 59 | &mut self.stdout 60 | } 61 | 62 | /// Returns a new handle to the standard error for this command. 63 | pub fn stderr(&mut self) -> &mut CmdOut { 64 | &mut self.stderr 65 | } 66 | } 67 | 68 | type FnFun = fn(&mut CmdEnv) -> CmdResult; 69 | 70 | lazy_static! { 71 | static ref CMD_MAP: Mutex> = { 72 | // needs explicit type, or it won't compile 73 | let mut m: HashMap = HashMap::new(); 74 | m.insert("echo".into(), builtin_echo); 75 | m.insert("trace".into(), builtin_trace); 76 | m.insert("debug".into(), builtin_debug); 77 | m.insert("info".into(), builtin_info); 78 | m.insert("warn".into(), builtin_warn); 79 | m.insert("error".into(), builtin_error); 80 | m.insert("".into(), builtin_empty); 81 | 82 | Mutex::new(m) 83 | }; 84 | } 85 | 86 | #[doc(hidden)] 87 | pub fn register_cmd(cmd: &'static str, func: FnFun) { 88 | CMD_MAP.lock().unwrap().insert(OsString::from(cmd), func); 89 | } 90 | 91 | /// Set debug mode or not, false by default. 92 | /// 93 | /// Setting environment variable CMD_LIB_DEBUG=0|1 has the same effect 94 | pub fn set_debug(enable: bool) { 95 | std::env::set_var("CMD_LIB_DEBUG", if enable { "1" } else { "0" }); 96 | } 97 | 98 | /// Set pipefail or not, true by default. 99 | /// 100 | /// Setting environment variable CMD_LIB_PIPEFAIL=0|1 has the same effect 101 | pub fn set_pipefail(enable: bool) { 102 | std::env::set_var("CMD_LIB_PIPEFAIL", if enable { "1" } else { "0" }); 103 | } 104 | 105 | pub(crate) fn debug_enabled() -> bool { 106 | std::env::var("CMD_LIB_DEBUG") == Ok("1".into()) 107 | } 108 | 109 | pub(crate) fn pipefail_enabled() -> bool { 110 | std::env::var("CMD_LIB_PIPEFAIL") != Ok("0".into()) 111 | } 112 | 113 | #[doc(hidden)] 114 | #[derive(Default)] 115 | pub struct GroupCmds { 116 | group_cmds: Vec, 117 | current_dir: PathBuf, 118 | } 119 | 120 | impl GroupCmds { 121 | pub fn append(mut self, cmds: Cmds) -> Self { 122 | self.group_cmds.push(cmds); 123 | self 124 | } 125 | 126 | pub fn run_cmd(&mut self) -> CmdResult { 127 | for cmds in self.group_cmds.iter_mut() { 128 | if let Err(e) = cmds.run_cmd(&mut self.current_dir) { 129 | if !cmds.ignore_error { 130 | return Err(e); 131 | } 132 | } 133 | } 134 | Ok(()) 135 | } 136 | 137 | pub fn run_fun(&mut self) -> FunResult { 138 | // run previous commands 139 | let mut last_cmd = self.group_cmds.pop().unwrap(); 140 | self.run_cmd()?; 141 | // run last function command 142 | let ret = last_cmd.run_fun(&mut self.current_dir); 143 | if ret.is_err() && last_cmd.ignore_error { 144 | return Ok("".into()); 145 | } 146 | ret 147 | } 148 | 149 | pub fn spawn(mut self, with_output: bool) -> Result { 150 | assert_eq!(self.group_cmds.len(), 1); 151 | let mut cmds = self.group_cmds.pop().unwrap(); 152 | cmds.spawn(&mut self.current_dir, with_output) 153 | } 154 | 155 | pub fn spawn_with_output(self) -> Result { 156 | self.spawn(true).map(CmdChildren::into_fun_children) 157 | } 158 | } 159 | 160 | #[doc(hidden)] 161 | #[derive(Default)] 162 | pub struct Cmds { 163 | cmds: Vec>, 164 | full_cmds: String, 165 | ignore_error: bool, 166 | file: String, 167 | line: u32, 168 | } 169 | 170 | impl Cmds { 171 | pub fn pipe(mut self, cmd: Cmd) -> Self { 172 | if self.full_cmds.is_empty() { 173 | self.file = cmd.file.clone(); 174 | self.line = cmd.line; 175 | } else { 176 | self.full_cmds += " | "; 177 | } 178 | self.full_cmds += &cmd.cmd_str(); 179 | let (ignore_error, cmd) = cmd.gen_command(); 180 | if ignore_error { 181 | if self.cmds.is_empty() { 182 | // first command in the pipe 183 | self.ignore_error = true; 184 | } else { 185 | warn!( 186 | "Builtin {IGNORE_CMD:?} command at wrong position ({}:{})", 187 | self.file, self.line 188 | ); 189 | } 190 | } 191 | self.cmds.push(Some(cmd)); 192 | self 193 | } 194 | 195 | fn spawn(&mut self, current_dir: &mut PathBuf, with_output: bool) -> Result { 196 | let full_cmds = self.full_cmds.clone(); 197 | let file = self.file.clone(); 198 | let line = self.line; 199 | if debug_enabled() { 200 | debug!("Running [{full_cmds}] at {file}:{line} ..."); 201 | } 202 | 203 | // spawning all the sub-processes 204 | let mut children: Vec = Vec::new(); 205 | let len = self.cmds.len(); 206 | let mut prev_pipe_in = None; 207 | for (i, cmd_opt) in self.cmds.iter_mut().enumerate() { 208 | let mut cmd = cmd_opt.take().unwrap(); 209 | if i != len - 1 { 210 | // not the last, update redirects 211 | let (pipe_reader, pipe_writer) = 212 | os_pipe::pipe().map_err(|e| new_cmd_io_error(&e, &full_cmds, &file, line))?; 213 | cmd.setup_redirects(&mut prev_pipe_in, Some(pipe_writer), with_output) 214 | .map_err(|e| new_cmd_io_error(&e, &full_cmds, &file, line))?; 215 | prev_pipe_in = Some(pipe_reader); 216 | } else { 217 | cmd.setup_redirects(&mut prev_pipe_in, None, with_output) 218 | .map_err(|e| new_cmd_io_error(&e, &full_cmds, &file, line))?; 219 | } 220 | let child = cmd 221 | .spawn(full_cmds.clone(), current_dir, with_output) 222 | .map_err(|e| new_cmd_io_error(&e, &full_cmds, &file, line))?; 223 | children.push(child); 224 | } 225 | 226 | Ok(CmdChildren::new(children, self.ignore_error)) 227 | } 228 | 229 | fn spawn_with_output(&mut self, current_dir: &mut PathBuf) -> Result { 230 | self.spawn(current_dir, true) 231 | .map(CmdChildren::into_fun_children) 232 | } 233 | 234 | fn run_cmd(&mut self, current_dir: &mut PathBuf) -> CmdResult { 235 | self.spawn(current_dir, false)?.wait() 236 | } 237 | 238 | fn run_fun(&mut self, current_dir: &mut PathBuf) -> FunResult { 239 | self.spawn_with_output(current_dir)?.wait_with_output() 240 | } 241 | } 242 | 243 | #[doc(hidden)] 244 | pub enum Redirect { 245 | FileToStdin(PathBuf), 246 | StdoutToStderr, 247 | StderrToStdout, 248 | StdoutToFile(PathBuf, bool), 249 | StderrToFile(PathBuf, bool), 250 | } 251 | impl fmt::Debug for Redirect { 252 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 253 | match self { 254 | Redirect::FileToStdin(path) => f.write_str(&format!("<{:?}", path.display())), 255 | Redirect::StdoutToStderr => f.write_str(">&2"), 256 | Redirect::StderrToStdout => f.write_str("2>&1"), 257 | Redirect::StdoutToFile(path, append) => { 258 | if *append { 259 | f.write_str(&format!("1>>{:?}", path.display())) 260 | } else { 261 | f.write_str(&format!("1>{:?}", path.display())) 262 | } 263 | } 264 | Redirect::StderrToFile(path, append) => { 265 | if *append { 266 | f.write_str(&format!("2>>{:?}", path.display())) 267 | } else { 268 | f.write_str(&format!("2>{:?}", path.display())) 269 | } 270 | } 271 | } 272 | } 273 | } 274 | 275 | #[doc(hidden)] 276 | pub struct Cmd { 277 | // for parsing 278 | in_cmd_map: bool, 279 | args: Vec, 280 | vars: HashMap, 281 | redirects: Vec, 282 | file: String, 283 | line: u32, 284 | 285 | // for running 286 | std_cmd: Option, 287 | stdin_redirect: Option, 288 | stdout_redirect: Option, 289 | stderr_redirect: Option, 290 | stdout_logging: Option, 291 | stderr_logging: Option, 292 | } 293 | 294 | impl Default for Cmd { 295 | fn default() -> Self { 296 | Cmd { 297 | in_cmd_map: true, 298 | args: vec![], 299 | vars: HashMap::new(), 300 | redirects: vec![], 301 | file: "".into(), 302 | line: 0, 303 | std_cmd: None, 304 | stdin_redirect: None, 305 | stdout_redirect: None, 306 | stderr_redirect: None, 307 | stdout_logging: None, 308 | stderr_logging: None, 309 | } 310 | } 311 | } 312 | 313 | impl Cmd { 314 | pub fn with_location(mut self, file: &str, line: u32) -> Self { 315 | self.file = file.into(); 316 | self.line = line; 317 | self 318 | } 319 | 320 | pub fn add_arg(mut self, arg: O) -> Self 321 | where 322 | O: AsRef, 323 | { 324 | let arg = arg.as_ref(); 325 | if arg.is_empty() { 326 | // Skip empty arguments 327 | return self; 328 | } 329 | 330 | let arg_str = arg.to_string_lossy().to_string(); 331 | if arg_str != IGNORE_CMD && !self.args.iter().any(|cmd| *cmd != IGNORE_CMD) { 332 | let v: Vec<&str> = arg_str.split('=').collect(); 333 | if v.len() == 2 && v[0].chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { 334 | self.vars.insert(v[0].into(), v[1].into()); 335 | return self; 336 | } 337 | self.in_cmd_map = CMD_MAP.lock().unwrap().contains_key(arg); 338 | } 339 | self.args.push(arg.to_os_string()); 340 | self 341 | } 342 | 343 | pub fn add_args(mut self, args: I) -> Self 344 | where 345 | I: IntoIterator, 346 | O: AsRef, 347 | { 348 | for arg in args { 349 | self = self.add_arg(arg); 350 | } 351 | self 352 | } 353 | 354 | pub fn add_redirect(mut self, redirect: Redirect) -> Self { 355 | self.redirects.push(redirect); 356 | self 357 | } 358 | 359 | fn arg0(&self) -> OsString { 360 | let mut args = self.args.iter().skip_while(|cmd| *cmd == IGNORE_CMD); 361 | if let Some(arg) = args.next() { 362 | return arg.into(); 363 | } 364 | "".into() 365 | } 366 | 367 | fn cmd_str(&self) -> String { 368 | self.vars 369 | .iter() 370 | .map(|(k, v)| format!("{k}={v:?}")) 371 | .chain(self.args.iter().map(|s| format!("{s:?}"))) 372 | .chain(self.redirects.iter().map(|r| format!("{r:?}"))) 373 | .collect::>() 374 | .join(" ") 375 | } 376 | 377 | fn gen_command(mut self) -> (bool, Self) { 378 | let args: Vec = self 379 | .args 380 | .iter() 381 | .skip_while(|cmd| *cmd == IGNORE_CMD) 382 | .map(|s| s.into()) 383 | .collect(); 384 | if !self.in_cmd_map { 385 | let mut cmd = Command::new(&args[0]); 386 | cmd.args(&args[1..]); 387 | for (k, v) in self.vars.iter() { 388 | cmd.env(k, v); 389 | } 390 | self.std_cmd = Some(cmd); 391 | } 392 | (self.args.len() > args.len(), self) 393 | } 394 | 395 | fn spawn( 396 | mut self, 397 | full_cmds: String, 398 | current_dir: &mut PathBuf, 399 | with_output: bool, 400 | ) -> Result { 401 | let arg0 = self.arg0(); 402 | if arg0 == CD_CMD { 403 | self.run_cd_cmd(current_dir, &self.file, self.line)?; 404 | Ok(CmdChild::new( 405 | CmdChildHandle::SyncFn, 406 | full_cmds, 407 | self.file, 408 | self.line, 409 | self.stdout_logging, 410 | self.stderr_logging, 411 | )) 412 | } else if self.in_cmd_map { 413 | let pipe_out = self.stdout_logging.is_none(); 414 | let mut env = CmdEnv { 415 | args: self 416 | .args 417 | .into_iter() 418 | .skip_while(|cmd| *cmd == IGNORE_CMD) 419 | .map(|s| s.to_string_lossy().to_string()) 420 | .collect(), 421 | vars: self.vars, 422 | current_dir: if current_dir.as_os_str().is_empty() { 423 | std::env::current_dir()? 424 | } else { 425 | current_dir.clone() 426 | }, 427 | stdin: if let Some(redirect_in) = self.stdin_redirect.take() { 428 | redirect_in 429 | } else { 430 | CmdIn::pipe(os_pipe::dup_stdin()?) 431 | }, 432 | stdout: if let Some(redirect_out) = self.stdout_redirect.take() { 433 | redirect_out 434 | } else { 435 | CmdOut::pipe(os_pipe::dup_stdout()?) 436 | }, 437 | stderr: if let Some(redirect_err) = self.stderr_redirect.take() { 438 | redirect_err 439 | } else { 440 | CmdOut::pipe(os_pipe::dup_stderr()?) 441 | }, 442 | }; 443 | 444 | let internal_cmd = CMD_MAP.lock().unwrap()[&arg0]; 445 | if pipe_out || with_output { 446 | let handle = thread::Builder::new().spawn(move || internal_cmd(&mut env))?; 447 | Ok(CmdChild::new( 448 | CmdChildHandle::Thread(handle), 449 | full_cmds, 450 | self.file, 451 | self.line, 452 | self.stdout_logging, 453 | self.stderr_logging, 454 | )) 455 | } else { 456 | internal_cmd(&mut env)?; 457 | Ok(CmdChild::new( 458 | CmdChildHandle::SyncFn, 459 | full_cmds, 460 | self.file, 461 | self.line, 462 | self.stdout_logging, 463 | self.stderr_logging, 464 | )) 465 | } 466 | } else { 467 | let mut cmd = self.std_cmd.take().unwrap(); 468 | 469 | // setup current_dir 470 | if !current_dir.as_os_str().is_empty() { 471 | cmd.current_dir(current_dir.clone()); 472 | } 473 | 474 | // update stdin 475 | if let Some(redirect_in) = self.stdin_redirect.take() { 476 | cmd.stdin(redirect_in); 477 | } 478 | 479 | // update stdout 480 | if let Some(redirect_out) = self.stdout_redirect.take() { 481 | cmd.stdout(redirect_out); 482 | } 483 | 484 | // update stderr 485 | if let Some(redirect_err) = self.stderr_redirect.take() { 486 | cmd.stderr(redirect_err); 487 | } 488 | 489 | // spawning process 490 | let child = cmd.spawn()?; 491 | Ok(CmdChild::new( 492 | CmdChildHandle::Proc(child), 493 | full_cmds, 494 | self.file, 495 | self.line, 496 | self.stdout_logging, 497 | self.stderr_logging, 498 | )) 499 | } 500 | } 501 | 502 | fn run_cd_cmd(&self, current_dir: &mut PathBuf, file: &str, line: u32) -> CmdResult { 503 | if self.args.len() == 1 { 504 | return Err(Error::new( 505 | ErrorKind::Other, 506 | "{CD_CMD}: missing directory at {file}:{line}", 507 | )); 508 | } else if self.args.len() > 2 { 509 | let err_msg = format!("{CD_CMD}: too many arguments at {file}:{line}"); 510 | return Err(Error::new(ErrorKind::Other, err_msg)); 511 | } 512 | 513 | let dir = current_dir.join(&self.args[1]); 514 | if !dir.is_dir() { 515 | let err_msg = format!("{CD_CMD}: No such file or directory at {file}:{line}"); 516 | return Err(Error::new(ErrorKind::Other, err_msg)); 517 | } 518 | 519 | dir.access(AccessMode::EXECUTE)?; 520 | *current_dir = dir; 521 | Ok(()) 522 | } 523 | 524 | fn open_file(path: &Path, read_only: bool, append: bool) -> Result { 525 | if read_only { 526 | OpenOptions::new().read(true).open(path) 527 | } else { 528 | OpenOptions::new() 529 | .create(true) 530 | .truncate(!append) 531 | .write(true) 532 | .append(append) 533 | .open(path) 534 | } 535 | } 536 | 537 | fn setup_redirects( 538 | &mut self, 539 | pipe_in: &mut Option, 540 | pipe_out: Option, 541 | with_output: bool, 542 | ) -> CmdResult { 543 | // set up stdin pipe 544 | if let Some(pipe) = pipe_in.take() { 545 | self.stdin_redirect = Some(CmdIn::pipe(pipe)); 546 | } 547 | // set up stdout pipe 548 | if let Some(pipe) = pipe_out { 549 | self.stdout_redirect = Some(CmdOut::pipe(pipe)); 550 | } else if with_output { 551 | let (pipe_reader, pipe_writer) = os_pipe::pipe()?; 552 | self.stdout_redirect = Some(CmdOut::pipe(pipe_writer)); 553 | self.stdout_logging = Some(pipe_reader); 554 | } 555 | // set up stderr pipe 556 | let (pipe_reader, pipe_writer) = os_pipe::pipe()?; 557 | self.stderr_redirect = Some(CmdOut::pipe(pipe_writer)); 558 | self.stderr_logging = Some(pipe_reader); 559 | 560 | for redirect in self.redirects.iter() { 561 | match redirect { 562 | Redirect::FileToStdin(path) => { 563 | self.stdin_redirect = Some(if path == Path::new("/dev/null") { 564 | CmdIn::null() 565 | } else { 566 | CmdIn::file(Self::open_file(path, true, false)?) 567 | }); 568 | } 569 | Redirect::StdoutToStderr => { 570 | if let Some(ref redirect) = self.stderr_redirect { 571 | self.stdout_redirect = Some(redirect.try_clone()?); 572 | } else { 573 | self.stdout_redirect = Some(CmdOut::pipe(os_pipe::dup_stderr()?)); 574 | } 575 | } 576 | Redirect::StderrToStdout => { 577 | if let Some(ref redirect) = self.stdout_redirect { 578 | self.stderr_redirect = Some(redirect.try_clone()?); 579 | } else { 580 | self.stderr_redirect = Some(CmdOut::pipe(os_pipe::dup_stdout()?)); 581 | } 582 | } 583 | Redirect::StdoutToFile(path, append) => { 584 | self.stdout_redirect = Some(if path == Path::new("/dev/null") { 585 | CmdOut::null() 586 | } else { 587 | CmdOut::file(Self::open_file(path, false, *append)?) 588 | }); 589 | } 590 | Redirect::StderrToFile(path, append) => { 591 | self.stderr_redirect = Some(if path == Path::new("/dev/null") { 592 | CmdOut::null() 593 | } else { 594 | CmdOut::file(Self::open_file(path, false, *append)?) 595 | }); 596 | } 597 | } 598 | } 599 | Ok(()) 600 | } 601 | } 602 | 603 | #[doc(hidden)] 604 | pub trait AsOsStr { 605 | fn as_os_str(&self) -> OsString; 606 | } 607 | 608 | impl AsOsStr for T { 609 | fn as_os_str(&self) -> OsString { 610 | self.to_string().into() 611 | } 612 | } 613 | 614 | #[doc(hidden)] 615 | #[derive(Default)] 616 | pub struct CmdString(OsString); 617 | impl CmdString { 618 | pub fn append>(mut self, value: T) -> Self { 619 | self.0.push(value); 620 | self 621 | } 622 | 623 | pub fn into_os_string(self) -> OsString { 624 | self.0 625 | } 626 | 627 | pub fn into_path_buf(self) -> PathBuf { 628 | self.0.into() 629 | } 630 | } 631 | 632 | impl AsRef for CmdString { 633 | fn as_ref(&self) -> &OsStr { 634 | self.0.as_ref() 635 | } 636 | } 637 | 638 | impl> From<&T> for CmdString { 639 | fn from(s: &T) -> Self { 640 | Self(s.as_ref().into()) 641 | } 642 | } 643 | 644 | impl fmt::Display for CmdString { 645 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 646 | f.write_str(&self.0.to_string_lossy()) 647 | } 648 | } 649 | 650 | pub(crate) fn new_cmd_io_error(e: &Error, command: &str, file: &str, line: u32) -> Error { 651 | Error::new( 652 | e.kind(), 653 | format!("Running [{command}] failed: {e} at {file}:{line}"), 654 | ) 655 | } 656 | 657 | #[cfg(test)] 658 | mod tests { 659 | use super::*; 660 | 661 | #[test] 662 | fn test_run_piped_cmds() { 663 | let mut current_dir = PathBuf::new(); 664 | assert!(Cmds::default() 665 | .pipe(Cmd::default().add_args(["echo", "rust"])) 666 | .pipe(Cmd::default().add_args(["wc"])) 667 | .run_cmd(&mut current_dir) 668 | .is_ok()); 669 | } 670 | 671 | #[test] 672 | fn test_run_piped_funs() { 673 | let mut current_dir = PathBuf::new(); 674 | assert_eq!( 675 | Cmds::default() 676 | .pipe(Cmd::default().add_args(["echo", "rust"])) 677 | .run_fun(&mut current_dir) 678 | .unwrap(), 679 | "rust" 680 | ); 681 | 682 | assert_eq!( 683 | Cmds::default() 684 | .pipe(Cmd::default().add_args(["echo", "rust"])) 685 | .pipe(Cmd::default().add_args(["wc", "-c"])) 686 | .run_fun(&mut current_dir) 687 | .unwrap() 688 | .trim(), 689 | "5" 690 | ); 691 | } 692 | 693 | #[test] 694 | fn test_stdout_redirect() { 695 | let mut current_dir = PathBuf::new(); 696 | let tmp_file = "/tmp/file_echo_rust"; 697 | let mut write_cmd = Cmd::default().add_args(["echo", "rust"]); 698 | write_cmd = write_cmd.add_redirect(Redirect::StdoutToFile(PathBuf::from(tmp_file), false)); 699 | assert!(Cmds::default() 700 | .pipe(write_cmd) 701 | .run_cmd(&mut current_dir) 702 | .is_ok()); 703 | 704 | let read_cmd = Cmd::default().add_args(["cat", tmp_file]); 705 | assert_eq!( 706 | Cmds::default() 707 | .pipe(read_cmd) 708 | .run_fun(&mut current_dir) 709 | .unwrap(), 710 | "rust" 711 | ); 712 | 713 | let cleanup_cmd = Cmd::default().add_args(["rm", tmp_file]); 714 | assert!(Cmds::default() 715 | .pipe(cleanup_cmd) 716 | .run_cmd(&mut current_dir) 717 | .is_ok()); 718 | } 719 | } 720 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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!(run_cmd! { 21 | cd /bad_dir; 22 | ls | wc -l; 23 | } 24 | .is_err()); 25 | } 26 | 27 | #[test] 28 | fn test_run_cmds() { 29 | assert!(run_cmd! { 30 | cd /tmp; 31 | touch xxff; 32 | ls | wc -l; 33 | rm xxff; 34 | } 35 | .is_ok()); 36 | } 37 | 38 | #[test] 39 | fn test_run_fun() { 40 | assert!(run_fun!(uptime).is_ok()); 41 | } 42 | 43 | #[test] 44 | fn test_args_passing() { 45 | let dir: &str = "folder"; 46 | assert!(run_cmd!(rm -rf /tmp/$dir).is_ok()); 47 | assert!(run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir).is_ok()); 48 | assert!(run_cmd!(mkdir /tmp/"$dir"; ls /tmp/"$dir"; rmdir /tmp/"$dir").is_err()); 49 | assert!(run_cmd!(mkdir "/tmp/$dir"; ls "/tmp/$dir"; rmdir "/tmp/$dir").is_err()); 50 | assert!(run_cmd!(rmdir "/tmp/$dir").is_ok()); 51 | } 52 | 53 | #[test] 54 | fn test_args_with_spaces() { 55 | let dir: &str = "folder with spaces"; 56 | assert!(run_cmd!(rm -rf /tmp/$dir).is_ok()); 57 | assert!(run_cmd!(mkdir /tmp/"$dir"; ls /tmp/"$dir"; rmdir /tmp/"$dir").is_ok()); 58 | assert!(run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir).is_ok()); 59 | assert!(run_cmd!(mkdir /tmp/"$dir"; ls /tmp/"$dir"; rmdir /tmp/"$dir").is_err()); 60 | assert!(run_cmd!(mkdir "/tmp/$dir"; ls "/tmp/$dir"; rmdir "/tmp/$dir").is_err()); 61 | assert!(run_cmd!(rmdir "/tmp/$dir").is_ok()); 62 | } 63 | 64 | #[test] 65 | fn test_args_with_spaces_check_result() { 66 | let dir: &str = "folder with spaces2"; 67 | assert!(run_cmd!(rm -rf /tmp/$dir).is_ok()); 68 | assert!(run_cmd!(mkdir /tmp/$dir).is_ok()); 69 | assert!(run_cmd!(ls "/tmp/folder with spaces2").is_ok()); 70 | assert!(run_cmd!(rmdir /tmp/$dir).is_ok()); 71 | } 72 | 73 | #[test] 74 | fn test_non_string_args() { 75 | let a = 1; 76 | assert!(run_cmd!(sleep $a).is_ok()); 77 | } 78 | 79 | #[test] 80 | fn test_non_eng_args() { 81 | let msg = "你好!"; 82 | assert!(run_cmd!(echo "$msg").is_ok()); 83 | assert!(run_cmd!(echo $msg).is_ok()); 84 | assert!(run_cmd!(echo ${msg}).is_ok()); 85 | } 86 | 87 | #[test] 88 | fn test_vars_in_str0() { 89 | assert_eq!(run_fun!(echo "$").unwrap(), "$"); 90 | } 91 | 92 | #[test] 93 | fn test_vars_in_str1() { 94 | assert_eq!(run_fun!(echo "$$").unwrap(), "$"); 95 | assert_eq!(run_fun!(echo "$$a").unwrap(), "$a"); 96 | } 97 | 98 | #[test] 99 | fn test_vars_in_str2() { 100 | assert_eq!(run_fun!(echo "$ hello").unwrap(), "$ hello"); 101 | } 102 | 103 | #[test] 104 | fn test_vars_in_str3() { 105 | let msg = "hello"; 106 | assert_eq!(run_fun!(echo "$msg").unwrap(), "hello"); 107 | assert_eq!(run_fun!(echo "$ msg").unwrap(), "$ msg"); 108 | } 109 | 110 | #[test] 111 | /// ```compile_fail 112 | /// run_cmd!(echo "${msg0}").unwrap(); 113 | /// assert_eq!(run_fun!(echo "${ msg }").unwrap(), "${ msg }"); 114 | /// assert_eq!(run_fun!(echo "${}").unwrap(), "${}"); 115 | /// assert_eq!(run_fun!(echo "${").unwrap(), "${"); 116 | /// assert_eq!(run_fun!(echo "${msg").unwrap(), "${msg"); 117 | /// assert_eq!(run_fun!(echo "$}").unwrap(), "$}"); 118 | /// assert_eq!(run_fun!(echo "${}").unwrap(), "${}"); 119 | /// assert_eq!(run_fun!(echo "${").unwrap(), "${"); 120 | /// assert_eq!(run_fun!(echo "${0}").unwrap(), "${0}"); 121 | /// assert_eq!(run_fun!(echo "${ 0 }").unwrap(), "${ 0 }"); 122 | /// assert_eq!(run_fun!(echo "${0msg}").unwrap(), "${0msg}"); 123 | /// assert_eq!(run_fun!(echo "${msg 0}").unwrap(), "${msg 0}"); 124 | /// assert_eq!(run_fun!(echo "${msg 0}").unwrap(), "${msg 0}"); 125 | /// ``` 126 | fn test_vars_in_str4() {} 127 | 128 | #[test] 129 | fn test_tls_set() { 130 | tls_init!(V, Vec, vec![]); 131 | tls_set!(V, |v| v.push("a".to_string())); 132 | tls_set!(V, |v| v.push("b".to_string())); 133 | assert_eq!(tls_get!(V)[0], "a"); 134 | } 135 | 136 | #[test] 137 | fn test_pipe() { 138 | assert!(run_cmd!(echo "xx").is_ok()); 139 | assert_eq!(run_fun!(echo "xx").unwrap(), "xx"); 140 | assert!(run_cmd!(echo xx | wc).is_ok()); 141 | assert!(run_cmd!(echo xx | wc | wc | wc | wc).is_ok()); 142 | assert!(run_cmd!(seq 1 10000000 | head -1).is_err()); 143 | 144 | assert!(run_cmd!(false | wc).is_err()); 145 | assert!(run_cmd!(echo xx | false | wc | wc | wc).is_err()); 146 | 147 | set_pipefail(false); 148 | assert!(run_cmd!(du -ah . | sort -hr | head -n 10).is_ok()); 149 | set_pipefail(true); 150 | 151 | let wc_cmd = "wc"; 152 | assert!(run_cmd!(ls | $wc_cmd).is_ok()); 153 | 154 | // test pipefail 155 | assert!(run_cmd!(false | true).is_err()); 156 | assert!(run_fun!(false | true).is_err()); 157 | assert!(run_fun!(ignore false | true).is_ok()); 158 | set_pipefail(false); 159 | assert!(run_fun!(false | true).is_ok()); 160 | set_pipefail(true); 161 | } 162 | 163 | #[test] 164 | /// ```compile_fail 165 | /// run_cmd!(ls > >&1).unwrap(); 166 | /// run_cmd!(ls >>&1).unwrap(); 167 | /// run_cmd!(ls >>&2).unwrap(); 168 | /// ``` 169 | fn test_redirect() { 170 | let tmp_file = "/tmp/f"; 171 | assert!(run_cmd!(echo xxxx > $tmp_file).is_ok()); 172 | assert!(run_cmd!(echo yyyy >> $tmp_file).is_ok()); 173 | assert!(run_cmd!( 174 | ignore ls /x 2>/tmp/lsx.log; 175 | echo "dump file:"; 176 | cat /tmp/lsx.log; 177 | rm /tmp/lsx.log; 178 | ) 179 | .is_ok()); 180 | assert!(run_cmd!(ignore ls /x 2>/dev/null).is_ok()); 181 | assert!(run_cmd!(ignore ls /x &>$tmp_file).is_ok()); 182 | assert!(run_cmd!(wc -w < $tmp_file).is_ok()); 183 | assert!(run_cmd!(ls 1>&1).is_ok()); 184 | assert!(run_cmd!(ls 2>&2).is_ok()); 185 | let tmp_log = "/tmp/echo_test.log"; 186 | assert_eq!(run_fun!(ls &>$tmp_log).unwrap(), ""); 187 | assert!(run_cmd!(rm -f $tmp_file $tmp_log).is_ok()); 188 | } 189 | 190 | #[test] 191 | fn test_proc_env() { 192 | let output = run_fun!(FOO=100 printenv | grep FOO).unwrap(); 193 | assert_eq!(output, "FOO=100"); 194 | } 195 | 196 | #[test] 197 | fn test_export_cmd() { 198 | use std::io::Write; 199 | fn my_cmd(env: &mut CmdEnv) -> CmdResult { 200 | let msg = format!("msg from foo(), args: {:?}", env.get_args()); 201 | writeln!(env.stderr(), "{}", msg)?; 202 | writeln!(env.stdout(), "bar") 203 | } 204 | 205 | fn my_cmd2(env: &mut CmdEnv) -> CmdResult { 206 | let msg = format!("msg from foo2(), args: {:?}", env.get_args()); 207 | writeln!(env.stderr(), "{}", msg)?; 208 | writeln!(env.stdout(), "bar2") 209 | } 210 | use_custom_cmd!(my_cmd, my_cmd2); 211 | assert!(run_cmd!(echo "from" "builtin").is_ok()); 212 | assert!(run_cmd!(my_cmd arg1 arg2).is_ok()); 213 | assert!(run_cmd!(my_cmd2).is_ok()); 214 | } 215 | 216 | #[test] 217 | fn test_escape() { 218 | let xxx = 42; 219 | assert_eq!( 220 | run_fun!(/bin/echo "\"a你好${xxx}世界b\"").unwrap(), 221 | "\"a你好42世界b\"" 222 | ); 223 | } 224 | 225 | #[test] 226 | fn test_current_dir() { 227 | let path = run_fun!(ls /; cd /tmp; pwd).unwrap(); 228 | assert_eq!( 229 | std::fs::canonicalize(&path).unwrap(), 230 | std::fs::canonicalize("/tmp").unwrap() 231 | ); 232 | } 233 | 234 | #[test] 235 | /// ```compile_fail 236 | /// run_cmd!(ls / /x &>>> /tmp/f).unwrap(); 237 | /// run_cmd!(ls / /x &> > /tmp/f).unwrap(); 238 | /// run_cmd!(ls / /x > > /tmp/f).unwrap(); 239 | /// run_cmd!(ls / /x >> > /tmp/f).unwrap(); 240 | /// ``` 241 | fn test_redirect_fail() {} 242 | 243 | #[test] 244 | fn test_buitin_stdout_redirect() { 245 | let f = "/tmp/builtin"; 246 | let msg = run_fun!(echo xx &> $f).unwrap(); 247 | assert_eq!(msg, ""); 248 | assert_eq!("xx", run_fun!(cat $f).unwrap()); 249 | run_cmd!(rm -f $f).unwrap(); 250 | } 251 | 252 | #[test] 253 | fn test_path_as_var() { 254 | let dir = std::path::Path::new("/"); 255 | assert_eq!("/", run_fun!(cd $dir; pwd).unwrap()); 256 | 257 | let dir2 = std::path::PathBuf::from("/"); 258 | assert_eq!("/", run_fun!(cd $dir2; pwd).unwrap()); 259 | } 260 | 261 | #[test] 262 | fn test_empty_arg() { 263 | let opt = ""; 264 | assert!(run_cmd!(ls $opt).is_ok()); 265 | } 266 | --------------------------------------------------------------------------------