├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── command-macros-plugin ├── Cargo.toml ├── README.md ├── src │ ├── lib.rs │ └── syntax.rs └── tests │ └── lib.rs └── command-macros ├── Cargo.toml ├── README.md ├── src └── lib.rs └── tests └── reexport.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .*.swp 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | script: 5 | - cargo build --verbose 6 | - cargo test --package command-macros-plugin --verbose 7 | - cargo test --features nightly --package command-macros --verbose 8 | cache: cargo 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # 0.2.9 — 2021-06-08 4 | 5 | * Update for the latest nightly: Stop using deprecated syntax for external doc. 6 | 7 | # 0.2.7 — 2019-09-16 8 | 9 | * Update for the latest nightly: Fix panic in span calculation. The fix is 10 | compatible with older nightlies too. 11 | * Fix `unused_mut` warning in `cmd!` macro. 12 | 13 | # 0.2.6 — 2018-10-09 14 | 15 | Update for the latest nightly: 16 | 17 | * `proc_macro_non_items` feature gate changed to `proc_macro_hygiene` 18 | 19 | # 0.2.5 — 2018-10-05 20 | 21 | Update for the latest nightly: 22 | 23 | * `Span::def_site` now requires separate `proc_macro_def_site` feature gate 24 | * `use_extern_macros` is no longer needed (since 1.30) 25 | 26 | # 0.2.4 — 2018-07-22 27 | 28 | Update for the latest nightly due to stabilization of 29 | `proc_macro` feature: 30 | 31 | * `proc_macro` feature is no longer needed 32 | * `use_extern_macros` is now needed instead 33 | * Not the whole `proc_macro` API was stabilized, so implementation 34 | uses `proc_macro_span` and `proc_macro_diagnostics` features. 35 | 36 | # 0.2.3 — 2018-05-21 37 | 38 | * Update for the latest nightly 39 | * Remove explicit link to documentation 40 | 41 | # 0.2.0 — 2018-05-02 42 | 43 | * Reimplemented `command` using new `proc_macro` 44 | * Moved `command` macro impl to separate crate 45 | (it's a limitation of `proc_macro`) 46 | * Removed the flag shorthand for `cmd` macro `(--foo)` 47 | * Removed auto-reffing expression in `[args]` 48 | (because args has started(?) to use IntoIterator, 49 | and `&` would limit flexibility)) 50 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "command-macros", 5 | "command-macros-plugin", 6 | ] 7 | 8 | [patch.crates-io] 9 | command-macros-plugin = { path = "command-macros-plugin" } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Michał Krasnoborski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | command-macros/README.md -------------------------------------------------------------------------------- /command-macros-plugin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "command-macros-plugin" 3 | version = "0.2.7" 4 | authors = ["Michał Krasnoborski "] 5 | 6 | description = "Implementation crate for command-macros" 7 | 8 | repository = "https://github.com/krdln/command-macros" 9 | readme = "README.md" 10 | license = "MIT" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | itertools = "0.7.8" 17 | -------------------------------------------------------------------------------- /command-macros-plugin/README.md: -------------------------------------------------------------------------------- 1 | # command-macros-plugin 2 | 3 | This crate implements a procedural macro version of `command` 4 | from `command-macros` crate. This macro exists in a separate 5 | crate, because currently procedural macros has to be implemented 6 | in their own crates. 7 | 8 | For documentation, see `command-macros`: 9 | * [github.com](https://github.com/krdln/command-macros) 10 | * [docs.rs](https://docs.rs/command-macros) 11 | * [crates.io](https://crates.io/crates/command-macros) 12 | -------------------------------------------------------------------------------- /command-macros-plugin/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_span, proc_macro_diagnostic, proc_macro_def_site)] 2 | 3 | #![doc = include_str!("../README.md")] 4 | 5 | extern crate proc_macro; 6 | extern crate itertools; 7 | 8 | use itertools::Itertools; 9 | 10 | mod syntax; 11 | 12 | use proc_macro::{ 13 | TokenStream, 14 | TokenTree, 15 | Span, 16 | Group, 17 | Delimiter, 18 | Literal, 19 | Spacing, 20 | }; 21 | 22 | use self::syntax::{ 23 | Expr, 24 | Pat, 25 | Stmt, 26 | from_source, 27 | new_ident, 28 | new_spanned_ident, 29 | new_block, 30 | surround, 31 | }; 32 | 33 | use std::iter::once; 34 | use std::iter::FromIterator; 35 | use std::collections::VecDeque; 36 | 37 | type Result = ::std::result::Result; 38 | 39 | /// Full-featured macro for creating `Command` 40 | /// 41 | /// Please read the syntax description in the crate's [documentation](index.html). 42 | /// 43 | /// This macro is intended to be used via reexport in `command_macros` 44 | /// crate with a "nightly" feature enabled. 45 | /// 46 | /// # Examples 47 | /// 48 | /// ``` 49 | /// #![feature(proc_macro_hygiene)] 50 | /// 51 | /// // also reexported in extern crate command_macros; 52 | /// extern crate command_macros_plugin; 53 | /// 54 | /// // also reexported as command_macros::command 55 | /// use command_macros_plugin::command; 56 | /// 57 | /// fn main() { 58 | /// command!(echo foo --bar ((2+2))=4).status().unwrap(); 59 | /// // should echo: foo --bar 4=4 60 | /// } 61 | /// ``` 62 | /// 63 | /// # Stability 64 | /// 65 | /// This is an experimental, nightly-only version of `cmd!` macro, 66 | /// so it might break with a nightly update from time to time. 67 | /// However, it uses a new `proc_macro` interface rather than 68 | /// compiler internals, so the breakage shouldn't occur too often. 69 | /// 70 | /// In future, when the `proc_macro` interface is stabilized, 71 | /// this macro should work on stable without significant changes. 72 | #[proc_macro] 73 | pub fn command(input: TokenStream) -> TokenStream { 74 | match try_expand_command(input) { 75 | Ok(stream) => stream, 76 | Err(()) => "::std::process::Command::new(\"dummy\")".parse().unwrap(), 77 | } 78 | } 79 | 80 | fn try_expand_command(input: TokenStream) -> Result { 81 | let trees = Parser::new(input).parse()?; 82 | Ok(generate(trees)?.into_stream()) 83 | } 84 | 85 | // Data ----------------------------------------------------------------------- 86 | 87 | #[derive(Debug)] 88 | enum Condition { 89 | Bool(Expr), 90 | IfLet(TokenTree, Pat, TokenTree, Expr), 91 | } 92 | 93 | #[derive(Debug)] 94 | struct Spanned { 95 | elem: T, 96 | span: Span, 97 | } 98 | 99 | #[allow(non_snake_case)] 100 | fn Spanned(elem: T, span: Span) -> Spanned { 101 | Spanned { elem, span } 102 | } 103 | 104 | #[derive(Debug)] 105 | struct Block(Spanned>); 106 | 107 | #[derive(Debug)] 108 | enum Splice { 109 | Word(String), // x 110 | Literal(Literal), // 'x', "x" 111 | ToStr(Expr), // (x) 112 | AsOsStr(Expr), // ((x)) 113 | } 114 | 115 | #[derive(Debug)] 116 | enum Arg { 117 | Single(Spanned), 118 | Touching(Vec>), 119 | } 120 | 121 | #[derive(Debug)] 122 | struct For { 123 | for_span: Span, 124 | pat: Pat, 125 | in_tt: TokenTree, 126 | expr: Expr, 127 | block: Block, 128 | } 129 | 130 | #[derive(Debug)] 131 | struct If { 132 | if_span: Span, 133 | cond: Condition, 134 | then_block: Block, 135 | else_block: Block, 136 | } 137 | 138 | type Arm = (Pat, (TokenTree, TokenTree), Block); 139 | 140 | #[derive(Debug)] 141 | struct Match { 142 | match_span: Span, 143 | expr: Expr, 144 | block_span: Span, 145 | arms: Vec, 146 | } 147 | 148 | #[derive(Debug)] 149 | enum Tree { 150 | Arg(Arg), 151 | Args(Spanned), // [x] 152 | Cmd(Expr), // {x} 153 | If(If), 154 | Match(Match), 155 | For(For), 156 | } 157 | 158 | impl Arg { 159 | fn span(&self) -> Span { 160 | match self { 161 | Arg::Single(splice) => splice.span, 162 | Arg::Touching(splices) => { 163 | splices.first().unwrap().span.join( 164 | splices.last().unwrap().span 165 | ).unwrap() 166 | } 167 | } 168 | } 169 | 170 | fn into_vec(self) -> Vec> { 171 | match self { 172 | Arg::Single(splice) => vec![splice], 173 | Arg::Touching(splices) => splices, 174 | } 175 | } 176 | } 177 | 178 | impl Tree { 179 | fn span(&self) -> Span { 180 | match self { 181 | Tree::Arg(arg) => arg.span(), 182 | Tree::Args(args) => args.span, 183 | Tree::Cmd(expr) => expr.span(), 184 | Tree::If(if_) => if_.span(), 185 | Tree::Match(match_) => match_.span(), 186 | Tree::For(for_) => for_.span(), 187 | } 188 | } 189 | } 190 | 191 | impl From for Tree { 192 | fn from(arg: Arg) -> Tree { 193 | Tree::Arg(arg) 194 | } 195 | } 196 | 197 | impl From> for Arg { 198 | fn from(splice: Spanned) -> Arg { 199 | Arg::Single(splice) 200 | } 201 | } 202 | 203 | impl From> for Tree { 204 | fn from(splice: Spanned) -> Tree { 205 | Arg::from(splice).into() 206 | } 207 | } 208 | 209 | impl Block { 210 | fn new(trees: Vec, span: Span) -> Block { Block(Spanned(trees, span)) } 211 | } 212 | 213 | impl If { 214 | fn span(&self) -> Span { 215 | let last_span = if self.else_block.0.elem.is_empty() { 216 | self.then_block.0.span 217 | } else { 218 | self.else_block.0.span 219 | }; 220 | self.if_span.join(last_span).unwrap_or(self.if_span) 221 | } 222 | } 223 | 224 | impl For { 225 | fn span(&self) -> Span { 226 | self.for_span.join(self.block.0.span).unwrap_or(self.for_span) 227 | } 228 | } 229 | 230 | impl Match { 231 | fn span(&self) -> Span { 232 | self.match_span.join(self.block_span).unwrap_or(self.match_span) 233 | } 234 | } 235 | 236 | // Generation ----------------------------------------------------------------- 237 | 238 | fn generate(mut trees: Vec) -> Result { 239 | if trees.is_empty() { 240 | Span::call_site().error("This macro needs at least the command name").emit(); 241 | return Err(()); 242 | } 243 | 244 | let cmd_tree = trees.remove(0); 245 | let cmd_expr: Expr = match cmd_tree { 246 | Tree::Arg(arg) => { 247 | let span = arg.span(); 248 | let str_expr = generate_os_str(arg)?; 249 | Expr::call(from_source("::std::process::Command::new", span), str_expr, span) 250 | } 251 | Tree::Cmd(cmd) => cmd, 252 | other => { 253 | other.span().error("Command name should be `cmd` `(cmd_name_expr)` or `{Command_expr}`").emit(); 254 | return Err(()) 255 | } 256 | }; 257 | 258 | let cmd_var: TokenTree = new_ident("cmd"); 259 | 260 | let init_stmt = Stmt::new_let(&cmd_var, cmd_expr); 261 | let mut stmts: Vec = vec![init_stmt]; 262 | stmts.extend(generate_stmts(&cmd_var, trees)?); 263 | 264 | let block = Expr::block(stmts, Expr::from_tt(cmd_var), Span::call_site()); 265 | 266 | Ok(block) 267 | } 268 | 269 | fn generate_stmts(cmd_var: &TokenTree, trees: Vec) -> Result> { 270 | trees 271 | .into_iter() 272 | .map(|tree| { 273 | match tree { 274 | Tree::Arg(arg) => generate_arg(cmd_var, arg), 275 | Tree::Args(args) => generate_args(cmd_var, args), 276 | Tree::For(pieces) => generate_for(cmd_var, pieces), 277 | Tree::If(pieces) => generate_if(cmd_var, pieces), 278 | Tree::Match(pieces) => generate_match(cmd_var, pieces), 279 | Tree::Cmd(expr) => { 280 | expr.span() 281 | .error("Command block could be used only at the beginning") 282 | .emit(); 283 | return Err(()) 284 | } 285 | } 286 | }) 287 | .collect() 288 | } 289 | 290 | fn generate_block(cmd_var: &TokenTree, block: Block) -> Result { 291 | let stmts = generate_stmts(cmd_var, block.0.elem)?; 292 | Ok(new_block(stmts, block.0.span)) 293 | } 294 | 295 | fn generate_arg(cmd: &TokenTree, arg: Arg) -> Result { 296 | let span = arg.span(); 297 | let os_str = generate_os_str(arg)?; 298 | let call_expr = Expr::call_method_on(cmd, "arg", os_str, span); 299 | Ok(call_expr.into_stmt()) 300 | } 301 | 302 | fn generate_args(cmd: &TokenTree, Spanned { elem: expr, span }: Spanned) -> Result { 303 | let call_expr = Expr::call_method_on(cmd, "args", expr, span); 304 | Ok(call_expr.into_stmt()) 305 | } 306 | 307 | fn generate_for(cmd_var: &TokenTree, For { for_span, pat, in_tt, expr, block }: For) -> Result { 308 | let stream = once(new_spanned_ident("for", for_span)) 309 | .chain(pat.0) 310 | .chain(once(in_tt)) 311 | .chain(expr.into_stream()) 312 | .chain(once(generate_block(cmd_var, block)?)) 313 | .collect(); 314 | Ok(Stmt::from_stream(stream)) 315 | } 316 | 317 | fn generate_if(cmd_var: &TokenTree, If { if_span, cond, then_block, else_block }: If) -> Result { 318 | let cond_stream = match cond { 319 | Condition::Bool(expr) => expr.into_stream(), 320 | Condition::IfLet(let_tt, pat, equals_tt, expr) => { 321 | once(let_tt) 322 | .chain(pat.0) 323 | .chain(once(equals_tt)) 324 | .chain(expr.into_stream()) 325 | .collect() 326 | } 327 | }; 328 | let stream = once(new_spanned_ident("if", if_span)) 329 | .chain(cond_stream) 330 | .chain(once(generate_block(cmd_var, then_block)?)) 331 | .chain(once(new_spanned_ident("else", Span::call_site()))) 332 | .chain(once(generate_block(cmd_var, else_block)?)) 333 | .collect(); 334 | Ok(Stmt::from_stream(stream)) 335 | } 336 | 337 | fn generate_match(cmd_var: &TokenTree, Match { match_span, expr, block_span, arms }: Match) -> Result { 338 | let mut arm_stream = Vec::new(); 339 | for arm in arms { 340 | arm_stream.extend(generate_arm(cmd_var, arm)?); 341 | } 342 | let arm_stream = arm_stream.into_iter().collect(); 343 | 344 | let block = surround(arm_stream, Delimiter::Brace, block_span); 345 | 346 | let stream = once(new_spanned_ident("match", match_span)) 347 | .chain(expr.into_stream()) 348 | .chain(once(block)) 349 | .collect(); 350 | 351 | Ok(Stmt::from_stream(stream)) 352 | } 353 | 354 | fn generate_arm(cmd_var: &TokenTree, (pat, arrows, block): Arm) -> Result> { 355 | Ok( 356 | pat.0.into_iter() 357 | .chain(once(arrows.0)) 358 | .chain(once(arrows.1)) 359 | .chain(once(generate_block(cmd_var, block)?)) 360 | ) 361 | } 362 | 363 | fn generate_splice(Spanned { elem: splice, span }: Spanned) -> Result { 364 | let expr = match splice { 365 | Splice::Word(word) => Expr::string_literal(&word), 366 | Splice::Literal(lit) => generate_literal(lit)?, 367 | Splice::AsOsStr(expr) => Expr::reference(expr, span), 368 | Splice::ToStr(expr) => Expr::call( 369 | from_source("ToString::to_string", span), 370 | Expr::reference(expr, span), 371 | span 372 | ), 373 | }; 374 | Ok(expr) 375 | } 376 | 377 | fn generate_literal(literal: Literal) -> Result { 378 | let repr = literal.to_string(); 379 | if repr.starts_with("'") { 380 | literal.span().error("Use string literals instead").emit(); 381 | Ok(Expr::string_literal("")) 382 | } else if repr.contains("\"") { 383 | Ok(Expr::from_tt(literal.into())) 384 | } else if repr.contains("'") { 385 | literal.span().error("Unsupported literal").emit(); 386 | Ok(Expr::string_literal("")) 387 | } else { 388 | Ok(Expr::string_literal(&literal.to_string())) 389 | } 390 | } 391 | 392 | fn generate_os_str(arg: Arg) -> Result { 393 | let full_span = arg.span(); 394 | match arg { 395 | Arg::Single(splice) => generate_splice(splice), 396 | Arg::Touching(splices) => { 397 | let os_string = Expr::from_source("::std::ffi::OsString::new()", full_span); 398 | let buf_var = new_ident("buf"); 399 | let init_stmt = Stmt::new_let(&buf_var, os_string); 400 | let mut stmts = vec![init_stmt]; 401 | 402 | for splice in splices { 403 | let span = splice.span; 404 | stmts.push(Expr::call_method_on( 405 | &buf_var, 406 | "push", 407 | Expr::reference(generate_splice(splice)?, span), 408 | span, 409 | ).into_stmt()) 410 | } 411 | 412 | Ok(Expr::block(stmts, Expr::from_tt(buf_var), full_span)) 413 | } 414 | } 415 | } 416 | 417 | // Parsing -------------------------------------------------------------------- 418 | 419 | #[derive(Debug)] 420 | struct Parser { 421 | last_span: Option, 422 | stream: VecDeque, 423 | } 424 | 425 | impl Parser { 426 | pub fn new(stream: TokenStream) -> Self { 427 | Parser { 428 | stream: VecDeque::from_iter(stream), 429 | last_span: None 430 | } 431 | } 432 | 433 | pub fn parse(&mut self) -> Result> { 434 | let trees = self.parse_trees()?; 435 | Ok(join_touching(trees)) 436 | } 437 | } 438 | 439 | fn join_touching(trees: Vec) -> Vec { 440 | trees.into_iter() 441 | .coalesce(|left, right| { 442 | match (left, right) { 443 | (Tree::Arg(left), Tree::Arg(right)) => { 444 | if are_separated_spans(left.span(), right.span()) { 445 | Err((left.into(), right.into())) 446 | } else { 447 | let mut splices = left.into_vec(); 448 | let right = match right { 449 | Arg::Single(s) => s, 450 | _ => unreachable!(), 451 | }; 452 | splices.push(right); 453 | Ok(Arg::Touching(splices).into()) 454 | } 455 | } 456 | (left, right) => Err((left, right)) 457 | } 458 | }) 459 | .collect() 460 | } 461 | 462 | impl Parser { 463 | fn peek(&self) -> Option<&TokenTree> { 464 | self.stream.front() 465 | } 466 | 467 | fn peek_two(&self) -> Option<(&TokenTree, &TokenTree)> { 468 | match (self.stream.get(0), self.stream.get(1)) { 469 | (Some(first), Some(second)) => Some((first, second)), 470 | _ => None 471 | } 472 | } 473 | 474 | fn next(&mut self) -> Option { 475 | self.stream.pop_front().map(|tt| { 476 | self.last_span = Some(tt.span()); 477 | tt 478 | }) 479 | } 480 | 481 | fn parse_block(&mut self) -> Result { 482 | let last_span = self.last_span; 483 | let orig_block = match self.next() { 484 | Some(TokenTree::Group(group)) => group, 485 | _ => { 486 | last_span.unwrap_or(Span::call_site()) 487 | .error("Expected a {} block after this token") 488 | .emit(); 489 | return Err(()) 490 | } 491 | }; 492 | let trees = Parser::new(orig_block.stream()).parse()?; 493 | Ok(Block::new(trees, orig_block.span())) 494 | } 495 | 496 | fn parse_trees(&mut self) -> Result> { 497 | let mut trees = Vec::new(); 498 | while !self.stream.is_empty() { 499 | trees.push(self.parse_tree()?); 500 | } 501 | Ok(trees) 502 | } 503 | 504 | fn parse_tree(&mut self) -> Result { 505 | let previous_span = self.last_span; 506 | let tt = self.next().unwrap(); 507 | let span = tt.span(); 508 | match tt { 509 | TokenTree::Group(group) => Ok(Parser::parse_splice(group)?.into()), 510 | 511 | TokenTree::Ident(ident) => { 512 | let next_span = self.peek().map(|next| next.span()); 513 | 514 | let word = ident.to_string(); 515 | let warn_keyword = || { 516 | if !is_separated_span(previous_span, span, next_span) { 517 | span.warning(format!("Keyword `{}` not separated by whitespace", word)) 518 | .note("Keywords should be separated by whitespace to avoid confusion") 519 | .help("To interpret as a string, surround it with quotes") 520 | .emit(); 521 | } 522 | }; 523 | 524 | match word.as_str() { 525 | "let" => { 526 | warn_keyword(); 527 | span.error("Let statements are not supported") 528 | .note("You can emulate them with `match`") 529 | .emit(); 530 | Err(()) 531 | } 532 | "if" => { 533 | warn_keyword(); 534 | self.parse_if(span) 535 | } 536 | "for" => { 537 | warn_keyword(); 538 | self.parse_for(span) 539 | } 540 | "match" => { 541 | warn_keyword(); 542 | self.parse_match(span) 543 | } 544 | word => { 545 | Ok(Spanned(Splice::Word(word.into()), span).into()) 546 | } 547 | } 548 | } 549 | 550 | TokenTree::Literal(lit) => Ok(Spanned(Splice::Literal(lit), span).into()), 551 | 552 | TokenTree::Punct(punct) => { 553 | match punct.as_char() { 554 | '$' => { 555 | punct.span() 556 | .error("Dollar sign interpollation is not supported") 557 | .help("To insert a variable, use `(var)` or `((var))`") 558 | .emit(); 559 | } 560 | '>' | '<' => { 561 | punct.span().error("File redirection is not supported").emit(); 562 | } 563 | '|' => { 564 | punct.span().error("Pipe redirection is not supported").emit(); 565 | } 566 | '&' => { 567 | punct.span().error("The `&` and `&&` operators are not supported").emit(); 568 | } 569 | ';' => { 570 | punct.span() 571 | .error("Unexpected semicolon") 572 | .help("To interpret literally, surround in quotes") 573 | .note("Semicolon is not needed in this macro") 574 | .emit(); 575 | } 576 | ch => { 577 | return Ok(Spanned(Splice::Word(ch.to_string()), span).into()); 578 | } 579 | } 580 | Err(()) 581 | } 582 | } 583 | } 584 | 585 | fn parse_splice(mut group: Group) -> Result { 586 | let span = group.span(); 587 | let stream = group.stream(); 588 | 589 | let tree = match group.delimiter() { 590 | Delimiter::Brace if is_really_empty(&group) => { 591 | return Ok(Spanned(Splice::Word("{}".into()), span).into()) 592 | } 593 | Delimiter::Brace => Tree::Cmd(Expr::from_stream(stream)), 594 | Delimiter::Parenthesis => { 595 | match try_into_singleton(&stream) { 596 | Some(TokenTree::Group(ref inner)) 597 | if inner.delimiter() == Delimiter::Parenthesis 598 | => { 599 | // The clone needed because of pattern guard :( 600 | group = inner.clone(); 601 | Spanned(Splice::ToStr(Expr::from_stream(group.stream())), span).into() 602 | } 603 | _ => Spanned(Splice::AsOsStr(Expr::from_stream(stream)), span).into(), 604 | } 605 | } 606 | Delimiter::Bracket => Tree::Args(Spanned(Expr::from_stream(stream), span)), 607 | Delimiter::None => { 608 | span.error("You've probably tried to use a nested macro.\ 609 | This is not supported").emit(); 610 | return Err(()) 611 | } 612 | }; 613 | 614 | if group.stream().is_empty() { 615 | group.span().error("Rust expression expected inside this block").emit(); 616 | return Err(()) 617 | } 618 | 619 | Ok(tree) 620 | } 621 | 622 | fn parse_if(&mut self, if_span: Span) -> Result { 623 | let cond = if self.is_ident_next("let") { 624 | let let_tt = self.next().unwrap(); 625 | let pat = Pat(self.parse_until( 626 | |parser| match parser.peek() { 627 | Some(TokenTree::Punct(punct)) 628 | if punct.as_char() == '=' && punct.spacing() == Spacing::Alone => true, 629 | _ => false, 630 | }, 631 | "`=`", 632 | "a pattern", 633 | )?); 634 | let equals_tt = self.next().unwrap(); 635 | let expr = self.parse_until_block()?; 636 | Condition::IfLet(let_tt, pat, equals_tt, expr) 637 | } else { 638 | Condition::Bool(self.parse_until_block()?) 639 | }; 640 | let then_block = self.parse_block()?; 641 | 642 | let else_block = if self.is_ident_next("else") { 643 | let _ = self.next().unwrap(); 644 | if self.is_block_next() { 645 | self.parse_block()? 646 | } else if self.is_ident_next("if") { 647 | let if_tt = self.next().unwrap(); 648 | let inner_if = self.parse_if(if_tt.span())?; 649 | let inner_span = inner_if.span(); 650 | Block::new(vec![inner_if], inner_span).into() 651 | } else { 652 | self.last_span.unwrap() 653 | .error("Expected `if` or {} block after this `else`") 654 | .emit(); 655 | return Err(()); 656 | } 657 | } else { 658 | Block::new(Vec::new(), Span::def_site()) 659 | }; 660 | 661 | Ok(Tree::If(If { if_span, cond, then_block, else_block })) 662 | } 663 | 664 | fn parse_for(&mut self, for_span: Span) -> Result { 665 | let pat = Pat(self.parse_until_ident("in", "a pattern")?); 666 | let in_tt = self.next().unwrap(); 667 | let expr = self.parse_until_block()?; 668 | let block = self.parse_block()?; 669 | Ok(Tree::For(For { for_span, pat, in_tt, expr, block })) 670 | } 671 | 672 | fn parse_match(&mut self, match_span: Span) -> Result { 673 | use self::Spacing::{Alone, Joint}; 674 | 675 | let expr = self.parse_until_block()?; 676 | 677 | let block = match self.next() { 678 | Some(TokenTree::Group(group)) => group, 679 | _ => unreachable!(), 680 | }; 681 | let block_span = block.span(); 682 | 683 | // Prevent accidental use of outer parser. 684 | let _dont_use_self = self; 685 | let mut parser = Parser::new(block.stream()); 686 | let mut arms = Vec::new(); 687 | 688 | while !parser.stream.is_empty() { 689 | let pat = Pat(parser.parse_until( 690 | |parser| match parser.peek_two() { 691 | Some((TokenTree::Punct(left), TokenTree::Punct(right))) 692 | if left.as_char() == '=' && right.as_char() == '>' 693 | && left.spacing() == Joint && right.spacing() == Alone => true, 694 | _ => false, 695 | }, 696 | "`=>`", 697 | "a pattern", 698 | )?); 699 | let arrow = ( 700 | parser.next().unwrap(), 701 | parser.next().unwrap(), 702 | ); 703 | let block = parser.parse_block()?; 704 | arms.push((pat, arrow, block)) 705 | } 706 | 707 | drop(_dont_use_self); 708 | 709 | Ok(Tree::Match(Match { match_span, expr, block_span, arms })) 710 | } 711 | 712 | fn parse_until_block(&mut self) -> Result { 713 | if self.is_block_next() { 714 | return Ok( Expr::from_tt(self.next().unwrap()) ) 715 | } 716 | let stream = self.parse_until(Parser::is_block_next, "{} block", "an expression")?; 717 | Ok(Expr::from_stream(stream)) 718 | } 719 | 720 | fn is_block_next(&self) -> bool { 721 | match self.peek() { 722 | Some(&TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Brace => true, 723 | _ => false, 724 | } 725 | } 726 | 727 | fn is_ident_next(&self, expected: &str) -> bool { 728 | match self.peek() { 729 | Some(TokenTree::Ident(actual)) if actual.to_string() == expected => true, 730 | _ => false, 731 | } 732 | } 733 | 734 | fn parse_until_ident(&mut self, until_ident: &str, what: &str) -> Result { 735 | self.parse_until( 736 | |parser| parser.is_ident_next(until_ident), 737 | &format!("`{}`", until_ident), 738 | what 739 | ) 740 | } 741 | 742 | fn parse_until(&mut self, until: F, until_str: &str, what: &str) -> Result 743 | where F: Fn(&Parser) -> bool 744 | { 745 | let mut tts = Vec::new(); 746 | 747 | while !until(self) { 748 | match self.next() { 749 | Some(tt) => tts.push(tt), 750 | None => { 751 | self.last_span.unwrap() 752 | .error(format!("Found end of macro when looking for {}", until_str)) 753 | .emit(); 754 | return Err(()) 755 | } 756 | } 757 | } 758 | 759 | if tts.is_empty() { 760 | self.peek().unwrap().span() 761 | .error(format!("Expected {} before {}", what, until_str)) 762 | .emit(); 763 | return Err(()) 764 | } 765 | 766 | Ok(tts.into_iter().collect()) 767 | } 768 | } 769 | 770 | fn are_separated_spans(left: Span, right: Span) -> bool { 771 | left.end().line != right.start().line || 772 | left.end().column < right.start().column 773 | } 774 | 775 | fn is_separated_span(left: Option, this: Span, right: Option) -> bool { 776 | left.map_or(true, |left| are_separated_spans(left, this)) && 777 | right.map_or(true, |right| are_separated_spans(this, right)) 778 | } 779 | 780 | fn is_really_empty(group: &Group) -> bool { 781 | let span = group.span(); 782 | let start = span.start(); 783 | let end = span.end(); 784 | group.stream().is_empty() && start.line == end.line && start.column + 2 == end.column 785 | } 786 | 787 | fn try_into_singleton(stream: &TokenStream) -> Option { 788 | let mut stream = stream.clone().into_iter(); 789 | let tt = stream.next()?; 790 | match stream.next() { 791 | None => Some(tt), 792 | _ => None 793 | } 794 | } 795 | -------------------------------------------------------------------------------- /command-macros-plugin/src/syntax.rs: -------------------------------------------------------------------------------- 1 | //! Helper module with some ad-hoc newtypes over TokenStream and 2 | //! TokenTree with some helper functions. 3 | 4 | use proc_macro::{ 5 | TokenStream, 6 | TokenTree, 7 | Span, 8 | Group, 9 | Delimiter, 10 | Literal, 11 | Punct, 12 | Ident, 13 | Spacing, 14 | }; 15 | 16 | use std::iter::once; 17 | 18 | #[derive(Debug)] 19 | pub struct Pat(pub TokenStream); 20 | 21 | #[derive(Debug)] 22 | pub struct Expr(TokenTree); 23 | 24 | #[derive(Debug)] 25 | pub struct Stmt(TokenTree); 26 | 27 | impl Expr { 28 | pub fn into_stmt(self) -> Stmt { 29 | let stream = once(self.into_tt()) 30 | .chain(once(new_punct(';'))) 31 | .collect(); 32 | Stmt::from_stream(stream) 33 | } 34 | 35 | pub fn into_tt(self) -> TokenTree { 36 | let Expr(tt) = self; 37 | tt 38 | } 39 | 40 | pub fn into_stream(self) -> TokenStream { 41 | match self { 42 | Expr(TokenTree::Group(group)) => { 43 | if group.delimiter() == Delimiter::None { 44 | group.stream() 45 | } else { 46 | once(TokenTree::from(group)).collect() 47 | } 48 | } 49 | expr => once(expr.into_tt()).collect() 50 | } 51 | } 52 | 53 | pub fn grouped(self) -> Expr { 54 | if let Expr(TokenTree::Group(ref old_group)) = self { 55 | if old_group.delimiter() == Delimiter::None { 56 | let mut new_group = Group::new(Delimiter::Parenthesis, old_group.stream()); 57 | new_group.set_span(old_group.span()); 58 | return Expr(new_group.into()) 59 | } 60 | } 61 | self 62 | } 63 | 64 | pub fn span(&self) -> Span { 65 | self.0.span() 66 | } 67 | 68 | pub fn from_tt(tt: TokenTree) -> Self { 69 | Expr(tt) 70 | } 71 | } 72 | 73 | impl Stmt { 74 | fn into_stream(self) -> TokenStream { 75 | // Just reusing Expr's implementation 76 | let Stmt(tt) = self; 77 | Expr(tt).into_stream() 78 | } 79 | } 80 | 81 | // Constructors 82 | 83 | impl Expr { 84 | pub fn from_stream(stream: TokenStream) -> Expr { 85 | let span = { 86 | if stream.is_empty() { 87 | Span::def_site() 88 | } else { 89 | let mut stream = stream.clone().into_iter(); 90 | let span = stream.next().unwrap().span(); 91 | if let Some(last) = stream.last() { 92 | // If join fails, one of the spans is most likely def_site. 93 | // There's no good choice in picking span of the whole expression then, 94 | // so we pick _something_. 95 | span.join(last.span()).unwrap_or_else(|| last.span()) 96 | } else { 97 | span 98 | } 99 | } 100 | }; 101 | Expr(surround(stream, Delimiter::None, span)) 102 | } 103 | 104 | pub fn from_source(source: &'static str, span: Span) -> Expr { 105 | Expr::from_stream(from_source(source, span).collect()) 106 | } 107 | 108 | pub fn call(function: Iter, expr: Expr, span: Span) -> Expr 109 | where Iter: Iterator 110 | { 111 | let stream = expr.into_stream(); 112 | let stream = function 113 | .chain(once(surround(stream, Delimiter::Parenthesis, span))) 114 | .collect(); 115 | Expr::from_stream(stream) 116 | } 117 | 118 | pub fn call_method(caller: Expr, method: &str, arg: Expr, span: Span) -> Expr { 119 | let function = caller.grouped().into_stream().into_iter() 120 | .chain(once(new_spanned_punct('.', span))) 121 | .chain(once(new_spanned_ident(method, span))); 122 | Expr::call(function, arg, span) 123 | } 124 | 125 | pub fn call_method_on(caller: &TokenTree, method: &str, arg: Expr, span: Span) -> Expr { 126 | Expr::call_method(Expr::from_tt(caller.clone()), method, arg, span) 127 | } 128 | 129 | pub fn block(stmts: Vec, expr: Expr, span: Span) -> Expr { 130 | let block = surround( 131 | stmts.into_iter() 132 | .map(|Stmt(tt)| tt) 133 | .chain(once(expr.into_tt())) 134 | .collect(), 135 | Delimiter::Brace, 136 | span, 137 | ); 138 | Expr(block) 139 | } 140 | 141 | pub fn string_literal(word: &str) -> Expr { 142 | Expr(Literal::string(word).into()) 143 | } 144 | 145 | pub fn reference(inner: Expr, span: Span) -> Expr { 146 | let stream = once(new_spanned_punct('&', span)) 147 | .chain(inner.grouped().into_stream()) 148 | .collect(); 149 | Expr::from_stream(stream) 150 | } 151 | } 152 | 153 | impl Stmt { 154 | pub fn from_stream(stream: TokenStream) -> Stmt { 155 | Stmt(surround(stream, Delimiter::None, Span::def_site())) 156 | } 157 | 158 | pub fn new_let(var: &TokenTree, expr: Expr) -> Stmt { 159 | let stream = from_source("#[allow(unused)] let mut", Span::call_site()) 160 | .chain(once(var.clone())) 161 | .chain(once(new_punct('='))) 162 | .chain(expr.into_stream()) 163 | .chain(once(new_punct(';'))) 164 | .collect(); 165 | Stmt::from_stream(stream) 166 | } 167 | } 168 | 169 | pub fn surround(stream: TokenStream, delimiter: Delimiter, span: Span) -> TokenTree { 170 | let mut g = Group::new(delimiter, stream); 171 | g.set_span(span); 172 | g.into() 173 | } 174 | 175 | pub fn from_source(source: &'static str, span: Span) -> impl Iterator { 176 | source 177 | .parse::() 178 | .unwrap() 179 | .into_iter() 180 | .map(move |mut tt| { tt.set_span(span); tt }) 181 | } 182 | 183 | pub fn new_punct(punct: char) -> TokenTree { 184 | Punct::new(punct, Spacing::Alone).into() 185 | } 186 | 187 | pub fn new_spanned_punct(punct: char, span: Span) -> TokenTree { 188 | let mut punct = Punct::new(punct, Spacing::Alone); 189 | punct.set_span(span); 190 | punct.into() 191 | } 192 | 193 | pub fn new_ident(word: &str) -> TokenTree { 194 | Ident::new(word, Span::def_site()).into() 195 | } 196 | 197 | pub fn new_spanned_ident(word: &str, span: Span) -> TokenTree { 198 | let mut ident = Ident::new(word, Span::def_site()); 199 | ident.set_span(span); 200 | ident.into() 201 | } 202 | 203 | pub fn new_block(stmts: Vec, span: Span) -> TokenTree { 204 | let stream = stmts.into_iter().flat_map(|stmt| stmt.into_stream()).collect(); 205 | surround(stream, Delimiter::Brace, span) 206 | } 207 | -------------------------------------------------------------------------------- /command-macros-plugin/tests/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene)] 2 | 3 | extern crate command_macros_plugin; 4 | 5 | mod plugin { 6 | use std::process::Command; 7 | use command_macros_plugin::command; 8 | 9 | fn quicktest(mut echocmd: Command, target: &str) { 10 | let out = echocmd.output().expect("quicktest: can't echo").stdout; 11 | assert_eq!(String::from_utf8_lossy(&out).trim(), target); 12 | } 13 | 14 | #[test] 15 | fn good() { 16 | let bar = false; 17 | let option = Some(5); 18 | quicktest( 19 | command!(echo {} if bar {-=bar=-} else if let Some(a) = option {--number (a.to_string())} levano), 20 | "{} --number 5 levano" 21 | ); 22 | let bar = true; 23 | quicktest( 24 | command!(echo {} if bar {-=bar=-} else if let Some(a) = option {--number (a.to_string())} levano), 25 | "{} -=bar=- levano" 26 | ); 27 | } 28 | 29 | #[test] 30 | fn ffmpeg() { 31 | let moreargs = ["-pix_fmt", "yuv420p"]; 32 | let file = "file.mp4".to_string(); 33 | let preset = "slow"; 34 | let tmpname = "tmp.mkv"; 35 | quicktest( 36 | command!( 37 | echo -i (file) 38 | -c:v libx264 -preset (preset) [&moreargs] 39 | -c:a copy 40 | file:(tmpname) 41 | ), 42 | "-i file.mp4 -c:v libx264 -preset slow -pix_fmt yuv420p -c:a copy file:tmp.mkv" 43 | ); 44 | } 45 | 46 | #[test] 47 | fn strings() { 48 | quicktest(command!("echo" r"a~\b"), "a~\\b"); 49 | } 50 | 51 | #[test] 52 | fn ugly() { 53 | quicktest(command!(echo if {{}; false}{a}else{b}), "b"); 54 | // quicktest(command!(echo if-a=5 {} else {}), "if-a=5 {} else {}"); 55 | // quicktest(command!(echo else-a {} let-a {}), "else-a {} let-a {}"); 56 | } 57 | 58 | #[test] 59 | fn match_test() { 60 | for &(x, target) in &[ 61 | (Ok(1), ". 0101 ."), 62 | (Ok(5), ". small 5 ."), 63 | (Ok(10), ". 10 ."), 64 | (Err("bu"), ". err bu ."), 65 | ] { 66 | quicktest(command!( 67 | echo . match x { 68 | Ok(0) | Ok(1) => { 0101 }, 69 | Ok(x) if x < 7 => { small (x.to_string()) }, 70 | Ok(x) => { (x.to_string()) } 71 | Err(x) => { err (x) } 72 | } . 73 | ), 74 | target 75 | ); 76 | } 77 | } 78 | 79 | #[test] 80 | fn parenparen() { 81 | quicktest(command!(echo ((2+2))), "4"); 82 | fn inc(x: i32) -> String { (x + 1).to_string() }; 83 | quicktest(command!(echo ((inc)(3))), "4"); 84 | } 85 | 86 | #[test] 87 | fn touching() { 88 | quicktest(command![echo number((2+2))], "number4"); 89 | quicktest(command![("e")"ch"(('o')) number((2+2))], "number4"); 90 | quicktest(command![echo ("abc")-((5))-def.txt hij], "abc-5-def.txt hij"); 91 | } 92 | 93 | #[test] 94 | fn for_loop() { 95 | quicktest(command![ 96 | echo 97 | for (i, x) in ["a", "b"].iter().enumerate() { 98 | --add ((i+1)).(x).txt 99 | } 100 | end 101 | ], 102 | "--add 1.a.txt --add 2.b.txt end" 103 | ); 104 | // quicktest(command!(echo for-me), "for-me"); 105 | } 106 | 107 | #[test] 108 | fn not_moving() { 109 | let s = String::new(); 110 | command!((s)); 111 | command!(((s))); 112 | command!((s)); 113 | } 114 | 115 | #[test] 116 | fn hygiene() { 117 | let cmd = 42; 118 | quicktest(command![echo ((cmd))], "42"); 119 | } 120 | 121 | #[test] 122 | fn flags_warn() { 123 | // quicktest(command![echo . (--flag) (+c)], ". --flag +c"); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /command-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "command-macros" 3 | version = "0.2.9" 4 | authors = ["Michał Krasnoborski "] 5 | 6 | description = "Macros for creating std::process::Command with shell-like syntax" 7 | 8 | repository = "https://github.com/krdln/command-macros" 9 | readme = "README.md" 10 | keywords = ["command", "macro", "shell", "process", "dsl"] 11 | license = "MIT" 12 | 13 | [package.metadata.docs.rs] 14 | features = ["dox"] 15 | 16 | [features] 17 | nightly = ["command-macros-plugin"] 18 | dox = [] 19 | 20 | [dependencies] 21 | command-macros-plugin = { version = "0.2.7", optional = true } 22 | -------------------------------------------------------------------------------- /command-macros/README.md: -------------------------------------------------------------------------------- 1 | # command-macros 2 | 3 | [![**documentation**](https://docs.rs/command-macros/badge.svg)](https://docs.rs/command-macros), 4 | [**crate**](https://crates.io/crates/command-macros) 5 | 6 | Macros for creating [`std::process::Command`](https://static.rust-lang.org/doc/master/std/process/struct.Command.html) 7 | with shell-like syntax. 8 | Created to make using Rust as a scripting language more pleasant. 9 | 10 | This crate contains two macros, `command!()` – fully-featured, 11 | but requires nightly, and a simpler [`cmd!()`](#cmd), built by `macro_rules`. 12 | 13 | ## `command!` 14 | 15 | [![Build Status](https://travis-ci.org/krdln/command-macros.svg?branch=master)](https://travis-ci.org/krdln/command-macros) 16 | 17 | ### Installation 18 | 19 | This macro requires nightly Rust and 20 | enabling a "nightly" feature. 21 | Put the following in your `Cargo.toml`. 22 | 23 | ```toml 24 | [dependencies.command-macros] 25 | version = "0.2.9" 26 | features = ["nightly"] 27 | ``` 28 | 29 | And then add on top of your root module: 30 | ```rust 31 | #![feature(proc_macro_hygiene)] 32 | 33 | extern crate command_macros; 34 | 35 | use command_macros::command; 36 | ``` 37 | 38 | If you're not running the latest nightly, try the following versions: 39 | 40 | nightly date | command-macros version 41 | -------------|----------------------- 42 | 2020-05-20 — | 0.2.9 43 | 2018-10-06 — 2021-06-05 | 0.2.7 44 | 2018-10-06 — 2019-08-27 | 0.2.6 45 | 2018-10-04 — 2018-10-05 | 0.2.5 46 | 2018-07-17 — 2018-10-03 | 0.2.4 47 | 2018-05-17 — 2018-07-16 | 0.2.3 48 | 2018-04-07 — 2018-05-16 | 0.2.2 49 | 50 | ### Examples 51 | 52 | ```rust 53 | command!( 54 | ffmpeg -i (file) 55 | -c:v libx264 -preset (preset) [moreargs] 56 | -c:a copy 57 | file:(tmpname) 58 | ).status().unwrap(); 59 | ``` 60 | 61 | should be roughly equivalent to running 62 | 63 | ```rust 64 | std::process::Command::new("ffmpeg") 65 | .args(&["-i", &file]) 66 | .args(&["-c:v", "libx264", "-preset", &preset]) 67 | .args(moreargs) 68 | .args(&["-c:a", "copy"]) 69 | .arg(&format!("file:{}", tmpname)) 70 | .status() 71 | .unwrap(); 72 | ``` 73 | 74 | As you see, you use `(expr)` to create an argument (or a part of it) 75 | from arbitrary Rust expression and `[expr]` for multiple arguments. 76 | The `&` is added automatically, similarly to how `print!` works. 77 | 78 | Moreover, `command!` will handle `file` and `tmpname` being `OsStr` correctly, 79 | while the manual version would require some modifications. 80 | 81 | Additionally, you can use `if`, `if let`, `match` and `for`. 82 | This snippet also showcases `(( expr ))` feature. 83 | 84 | ```rust 85 | command!(make 86 | if let Some(n) = n_cores { -j ((n + 1)) } 87 | ).status().unwrap(); 88 | ``` 89 | 90 | Both macros return `Command` by value, so you can store them in a variable for later: 91 | 92 | ```rust 93 | let cmd = command!(mkv --fs); 94 | ``` 95 | 96 | If you have partially prepared command (`Command` or `&mut Command`), 97 | you can also pass it to this macro: 98 | 99 | ```rust 100 | let base: Command = prepare(); 101 | let cmd = command!({base} install (package)); 102 | ``` 103 | 104 | ## `cmd!` 105 | 106 | ### Installation 107 | 108 | Put the following in your `Cargo.toml`. 109 | 110 | ```toml 111 | [dependencies] 112 | command-macros = "0.2" 113 | ``` 114 | 115 | And then add on top of your root module: 116 | ```rust 117 | #[macro_use] extern crate command_macros; 118 | ``` 119 | 120 | ### Limitations 121 | 122 | This macro is a "lite" version of the `command!`. 123 | Differences: 124 | * Worse error messages. 125 | * It is whitespace-insensitive. 126 | * Creating arguments from arbitrary tokens (such as `-c:a`) is not supported (only idents). 127 | The workaround is to use Rust string as an expression: `("-c:a")`. 128 | * `((expr))` and `(expr)` always evaluate to full argument (no tricks like `file:(filename)`). 129 | * Expressions in `if`, `match` and `for` have to be surrounded by parens. 130 | * No support for `else if` (use `else { if ... }` instead). 131 | 132 | Besides, all other features should work. 133 | 134 | ### Examples 135 | 136 | Examples from `command!` section rewritten to match `cmd!` syntax: 137 | 138 | ```rust 139 | command!( 140 | ffmpeg ("-i") (file) 141 | ("-c:v") libx264 ("-preset") (preset) [moreargs] 142 | ("-c:a") copy 143 | (format!("file:{}", tmpname)) 144 | ).status().unwrap(); 145 | ``` 146 | 147 | ```rust 148 | command!(make 149 | if let Some(n) = (n_cores) { ("-j") ((n + 1)) } 150 | ).status().unwrap(); 151 | ``` 152 | 153 | ## [Changelog](CHANGELOG.md) 154 | -------------------------------------------------------------------------------- /command-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Macros for creating 2 | //! [`std::process::Command`](https://static.rust-lang.org/doc/master/std/process/struct.Command.html) 3 | //! with shell-like syntax. 4 | //! 5 | //! The `command!` macro is a syntax extension and requires nightly, 6 | //! the `cmd!` is simpler version built using `macro_rules!`. 7 | //! 8 | //! This page describes syntax used by both `command!` and `cmd!` macros. 9 | //! See the [github page](https://github.com/krdln/command-macros) for more general introduction. 10 | //! 11 | //! Features marked with \* are unavailable for `cmd!`. 12 | //! 13 | //! ## Naked idents 14 | //! 15 | //! First ident is treated as command name, 16 | //! the rest are parsed as arguments. This invocation: 17 | //! 18 | //! ``` 19 | //! # #[macro_use] extern crate command_macros; 20 | //! # fn main() { 21 | //! cmd!(echo foo bar).status().unwrap(); 22 | //! # } 23 | //! ``` 24 | //! 25 | //! expands to 26 | //! 27 | //! ```ignore 28 | //! { 29 | //! let cmd = ::std::process::Command::new("echo"); 30 | //! cmd.arg("foo"); 31 | //! cmd.arg("bar"); 32 | //! cmd 33 | //! }.status().unwrap() 34 | //! ``` 35 | //! 36 | //! ## `(expression)` (OsStr expression) 37 | //! 38 | //! Interior of `( )` is parsed as Rust expression 39 | //! which should evaluate to `T: AsRef`. 40 | //! This will be put in `cmd.arg(& $expr)`. 41 | //! The `&` is added automatically, like in `println!`, 42 | //! to prevent accidentally moving arguments. 43 | //! 44 | //! ```no_run 45 | //! # #[macro_use] extern crate command_macros; 46 | //! # fn main() { 47 | //! let filename = String::from("foo bar"); 48 | //! let get_command = || "touch"; 49 | //! cmd!( (get_command()) (filename) ).status().unwrap(); 50 | //! # } 51 | //! ``` 52 | //! 53 | //! ## `((expression))` (ToString expression) 54 | //! 55 | //! Interior of `(( ))` is parsed as Rust expression 56 | //! which should evaluate to `T: ToString`. 57 | //! Similar rules as with `( )` apply. 58 | //! 59 | //! The following should echo `4` 60 | //! 61 | //! ``` 62 | //! # #[macro_use] extern crate command_macros; 63 | //! # fn main() { 64 | //! cmd!( echo ((2+2)) ).status().unwrap(); 65 | //! # } 66 | //! ``` 67 | //! 68 | //! ## `[expression]` (args expression) 69 | //! 70 | //! Interior of `[ ]` is parsed as Rust expression 71 | //! which should evaluate to `impl IntoIterator>`, 72 | //! eg. a vector of strings. 73 | //! This expression will be put in `cmd.args($expr)` 74 | //! 75 | //! ```no_run 76 | //! # #[macro_use] extern crate command_macros; 77 | //! # fn main() { 78 | //! let args: Vec<_> = std::env::args_os().collect(); 79 | //! cmd!( (args[1]) [&args[2..]] ).status().unwrap(); 80 | //! # } 81 | //! ``` 82 | //! 83 | //! ## `{expression}` (Command expression) 84 | //! 85 | //! Interior of `{ }` is parsed as Rust expression 86 | //! which should evaluate to `Command` or `&mut Command` 87 | //! (or anything that has `arg` and `args` methods). 88 | //! It is allowed only at the beginning of macro. 89 | //! It is helpful when you want to append arguments to 90 | //! existing command: 91 | //! 92 | //! ```no_run 93 | //! # #[macro_use] extern crate command_macros; 94 | //! # fn main() { 95 | //! let mut cmd = ::std::process::Command::new("echo"); 96 | //! cmd!( {&mut cmd} bar baz ).status().unwrap(); 97 | //! # } 98 | //! ``` 99 | //! 100 | //! ## Strings\* 101 | //! 102 | //! String literals work like in shell – they expand to single argument or part of it. 103 | //! Character literals and raw string literals are also supported. 104 | //! Note that shell-style `"$variables"` won't work here. 105 | //! 106 | //! ```ignore 107 | //! command!("echo" "single argument" "***") 108 | //! ``` 109 | //! 110 | //! `cmd!` workaroud: 111 | //! 112 | //! ```ignore 113 | //! cmd!(echo ("single argument") ("***")) 114 | //! ``` 115 | //! 116 | //! ## Arbitrary tokens\* 117 | //! 118 | //! Everything that is not [block], {block}, (block) or string literal, 119 | //! will be stringified. This is mostly helpful for unix-like flags. 120 | //! Everything within a single whitespace-separated chunk will be treated 121 | //! as a single argument. In the following example we are passing 122 | //! three arguments to a `foo.2.5` command. 123 | //! 124 | //! ```ignore 125 | //! command!(foo.2.5 --flag -v:c -=:.:=-).status().unwrap(); 126 | //! ``` 127 | //! 128 | //! `cmd!` workaround: `("--flag")`. 129 | //! 130 | //! ## Multi-part arguments\* 131 | //! 132 | //! You can mix `((e))`, `(e)`, tokens and strings within a single 133 | //! arguments as long as they are not separated by whitespace. 134 | //! The following will touch the `foo.v5.special edition` file. 135 | //! 136 | //! ```ignore 137 | //! let p = Path::new("foo"); 138 | //! let version = 5; 139 | //! command!(touch (p).v((version))".special edition") 140 | //! ``` 141 | //! 142 | //! This is roughly equivalent to `(format!(...))`, but the macro version 143 | //! can handle `OsStr`s (such as `Path`). 144 | //! 145 | //! Please note that this is **not** supported by `cmd!`, which would evaluate 146 | //! every part as *separate* argument. 147 | //! 148 | //! ## `{}`\* 149 | //! 150 | //! Empty `{}` is treated as `"{}"`. This is handy when using commands like `find`. 151 | //! There has to be no space between braces. 152 | //! 153 | //! 154 | //! ## If 155 | //! 156 | //! The `if` token should be surrounded by whitespace. 157 | //! The expression (and pattern in if-let) is parsed as Rust, 158 | //! the inside of {block} is parsed using regular `commmand!` syntax 159 | //! and can evaluate to multiple arguments (or 0). 160 | //! The following should pass `--number` `5` to command `foo`. 161 | //! 162 | //! ```ignore 163 | //! let bar = 5; 164 | //! let option = Some(5); 165 | //! command!(foo 166 | //! if bar > 10 { zzz } 167 | //! else if let Some(a) = option { --number ((a)) } 168 | //! ).status().unwrap(); 169 | //! ``` 170 | //! 171 | //! `cmd!` limitations: `else if` is not supported, expression has to be in parens. 172 | //! 173 | //! ```ignore 174 | //! cmd!(foo 175 | //! if (bar > 10) { zzz } 176 | //! else { if let Some(a) = (option) { ("--number") ((a)) } } 177 | //! ).status().unwrap(); 178 | //! ``` 179 | //! 180 | //! ## Match 181 | //! 182 | //! The `match` token should be surrounded by whitespace. 183 | //! The expression and patterns are parsed as Rust. 184 | //! On the right side of `=>` there should be a {block}, 185 | //! which will be parsed (similarly to if blocks) 186 | //! using regular `command!` syntax. 187 | //! 188 | //! This example will pass a single argument `yes` to `foo` command. 189 | //! 190 | //! ```ignore 191 | //! let option = Some(5); 192 | //! command!(foo 193 | //! match option { 194 | //! Some(x) if x > 10 => {} 195 | //! _ => { yes } 196 | //! } 197 | //! ).status().unwrap() 198 | //! ``` 199 | //! 200 | //! `cmd!` limitation: expression after `match` has to be in parens. 201 | //! 202 | //! ## For 203 | //! 204 | //! The `for` token should be surrounded by whitespace. 205 | //! The expression and patterns are parsed as Rust. 206 | //! The interior of block is parsed using `command!` syntax, 207 | //! and will be evaluated in every iteration. 208 | //! 209 | //! This example will pass three arguments `1` `2` `3`. 210 | //! 211 | //! ```ignore 212 | //! command!(echo 213 | //! for x in 1..4 { 214 | //! ((x)) 215 | //! } 216 | //! ).status().unwrap() 217 | //! ``` 218 | 219 | #[cfg(feature = "nightly")] 220 | extern crate command_macros_plugin; 221 | 222 | #[cfg(feature = "nightly")] 223 | pub use command_macros_plugin::command; 224 | 225 | // Stub for displaying in documentation 226 | #[cfg(feature = "dox")] 227 | /// Full-featured macro for creating `Command` 228 | /// 229 | /// This macro is available only with the "nightly" feature enabled. 230 | /// 231 | /// Please read the syntax description in the crate's [documentation](index.html). 232 | /// 233 | /// # Examples 234 | /// 235 | /// ```ignore 236 | /// #![feature(proc_macro_hygiene)] 237 | /// 238 | /// extern crate command_macros; 239 | /// 240 | /// use command_macros::command; 241 | /// 242 | /// fn main() { 243 | /// command!(echo foo --bar ((2+2))=4).status().unwrap(); 244 | /// // should echo: foo --bar 4=4 245 | /// } 246 | /// ``` 247 | /// 248 | /// # Stability 249 | /// 250 | /// This is an experimental, nightly-only version of `cmd!` macro, 251 | /// so it might break with a nightly update from time to time. 252 | /// However, it uses a new `proc_macro` interface rather than 253 | /// compiler internals, so the breakage shouldn't occur too often. 254 | /// 255 | /// In future, when the `proc_macro` interface is stabilized, 256 | /// this macro should work on stable without significant changes. 257 | #[macro_export] 258 | macro_rules! command { 259 | ($($tt:tt)*) => { /* proc_macro */ } 260 | } 261 | 262 | /// Simple macro for creating `Command`. 263 | /// 264 | /// Please read the syntax description in the crate's [documentation](index.html). 265 | /// 266 | /// Please note that this macro is **not** whitespace-sensitive and the following 267 | /// will evaluate to four separate arguments (as opposed to one in `command!`): 268 | /// 269 | /// ```ignore 270 | /// cmd!(echo (foo)(bar)baz(qux)) // don't do it! 271 | /// ``` 272 | /// 273 | /// # Examples 274 | /// 275 | /// ``` 276 | /// #[macro_use] extern crate command_macros; 277 | /// 278 | /// fn main() { 279 | /// cmd!( echo ((2+2)) ).status().unwrap(); 280 | /// } 281 | /// ``` 282 | #[macro_export] 283 | macro_rules! cmd { 284 | ({$e:expr}) => ($e); 285 | 286 | // arg ToString splice 287 | ({$e:expr} (($a:expr)) $($tail:tt)*) => 288 | { 289 | { 290 | // Allowing unused mut in case `$e` is of type `&mut Command` 291 | #[allow(unused_mut)] 292 | let mut cmd = $e; 293 | cmd.arg((&$a).to_string()); 294 | cmd!( {cmd} $($tail)* ) 295 | } 296 | }; 297 | 298 | // arg splice 299 | ({$e:expr} ($a:expr) $($tail:tt)*) => 300 | { 301 | { 302 | #[allow(unused_mut)] 303 | let mut cmd = $e; 304 | cmd.arg(&$a); 305 | cmd!( {cmd} $($tail)* ) 306 | } 307 | }; 308 | 309 | // args splice 310 | ({$e:expr} [$aa:expr] $($tail:tt)*) => { 311 | { 312 | #[allow(unused_mut)] 313 | let mut cmd = $e; 314 | cmd.args($aa); 315 | cmd!( {cmd} $($tail)* ) 316 | } 317 | }; 318 | 319 | // match 320 | ({$e:expr} match ($m:expr) { $($($p:pat)|+ $(if $g:expr)* => {$($rr:tt)*} ),* } $($tail:tt)*) => { 321 | cmd!({$e} match ($m) { $($($p)|+ $(if $g)* => {$($rr)*})* } $($tail)*) 322 | }; 323 | ({$e:expr} match ($m:expr) { $($($p:pat)|+ $(if $g:expr)* => {$($rr:tt)*},)* } $($tail:tt)*) => { 324 | cmd!({$e} match ($m) { $($($p)|+ $(if $g)* => {$($rr)*})* } $($tail)*) 325 | }; 326 | ({$e:expr} match ($m:expr) { $($($p:pat)|+ $(if $g:expr)* => {$($rr:tt)*} )* } $($tail:tt)*) => { 327 | { 328 | let cmd = $e; 329 | cmd!( {match $m { $($($p)|+ $(if $g)* => cmd!({cmd} $($rr)*)),* }} $($tail)* ) 330 | } 331 | }; 332 | 333 | // if let 334 | ({$e:expr} if let $p:pat = ($m:expr) { $($then:tt)* } else { $($els:tt)* } $($tail:tt)*) => { 335 | { 336 | let cmd = $e; 337 | cmd!( { 338 | if let $p = $m { cmd!({cmd} $($then)*) } else { cmd!({cmd} $($els)*) } 339 | } $($tail)*) 340 | } 341 | }; 342 | ({$e:expr} if let $p:pat = ($m:expr) { $($then:tt)* } $($tail:tt)*) => { 343 | cmd!( {$e}if let $p = ($m) { $($then)* } else {} $($tail)* ) 344 | }; 345 | 346 | // if else 347 | ({$e:expr} if ($b:expr) { $($then:tt)* } else { $($els:tt)* } $($tail:tt)*) => { 348 | { 349 | let cmd = $e; 350 | cmd!( { 351 | if $b { cmd!({cmd} $($then)*) } else { cmd!({cmd} $($els)*) } 352 | } $($tail)*) 353 | } 354 | }; 355 | ({$e:expr} if ($b:expr) { $($then:tt)* } $($tail:tt)*) => { 356 | cmd!( {$e}if ($b) { $($then)* } else {} $($tail)* ) 357 | }; 358 | 359 | // for 360 | ({$e:expr} for $p:pat in ($i:expr) { $($body:tt)* } $($tail:tt)*) => { 361 | { 362 | #[allow(unused_mut)] 363 | let mut cmd = $e; 364 | for $p in $i { cmd = cmd!( {cmd} $($body)* ); } 365 | cmd 366 | } 367 | }; 368 | 369 | // naked ident 370 | ({$e:expr} $a:ident $($tail:tt)*) => (cmd!( {$e} (stringify!($a)) $($tail)* )); 371 | 372 | // Main entry points (command name) 373 | (($c:expr) $($tail:tt)*) => { 374 | cmd!( {::std::process::Command::new(&$c)} $($tail)* ) 375 | }; 376 | ($c:ident $($tail:tt)*) => (cmd!( (stringify!($c)) $($tail)* )); 377 | } 378 | 379 | #[cfg(test)] 380 | use ::std::process::Command; 381 | 382 | #[test] 383 | fn expr() { 384 | let mut base: Command = cmd!(echo a); 385 | base.env("FOO", "bar"); 386 | quicktest(cmd!({base} b), "a b"); 387 | } 388 | 389 | #[cfg(test)] 390 | fn quicktest(mut echocmd: Command, target: &str) { 391 | let out = echocmd.output().expect("quicktest: can't echo").stdout; 392 | assert_eq!(String::from_utf8_lossy(&out).trim(), target); 393 | } 394 | 395 | #[test] 396 | fn simple() { 397 | let output = cmd!(echo raz dwa trzy).output().expect("can't echo"); 398 | assert_eq!(output.stdout, &b"raz dwa trzy\n"[..]); 399 | } 400 | 401 | #[test] 402 | fn ffmpeg() { 403 | let moreargs = ["-pix_fmt", "yuv420p"]; 404 | let file = "file.mp4".to_string(); 405 | let preset = "slow"; 406 | let tmpname = "tmp.mkv"; 407 | let output = cmd!(echo 408 | ("-i") (file) 409 | ("-c:v") libx264 ("-preset") (preset) [&moreargs] 410 | ("-c:a") copy 411 | (tmpname)) 412 | .output() 413 | .expect("can't echo"); 414 | assert_eq!( 415 | String::from_utf8_lossy(&output.stdout), 416 | "-i file.mp4 -c:v libx264 -preset slow -pix_fmt yuv420p -c:a copy tmp.mkv\n" 417 | ); 418 | } 419 | 420 | #[test] 421 | fn match_test() { 422 | let option = Some(5); 423 | 424 | quicktest( 425 | cmd!(echo 426 | match (option) { 427 | Some(x) => {("--number") (x.to_string())} 428 | None => {} 429 | } 430 | tail), 431 | "--number 5 tail" 432 | ); 433 | 434 | for &(x, target) in &[ 435 | (Ok(1), ". 0101 ."), 436 | (Ok(5), ". small 5 ."), 437 | (Ok(10), ". 10 ."), 438 | (Err("bu"), ". err bu ."), 439 | ] { 440 | quicktest(cmd!( 441 | echo (".") match (x) { 442 | Ok(0) | Ok(1) => { ("0101") }, 443 | Ok(x) if x < 7 => { small (x.to_string()) }, 444 | Ok(x) => { (x.to_string()) }, 445 | Err(x) => { err (x) } 446 | } (".") 447 | ), 448 | target 449 | ); 450 | } 451 | } 452 | 453 | #[test] 454 | fn iflet() { 455 | let option = Some(5); 456 | quicktest( 457 | cmd!(echo 458 | if let Some(x) = (option) { ("--number") ((x)) } 459 | tail), 460 | "--number 5 tail" 461 | ); 462 | 463 | let option: Option<()> = None; 464 | quicktest( 465 | cmd!(echo 466 | if let Some(_) = (option) {} else { ok } 467 | tail), 468 | "ok tail" 469 | ); 470 | } 471 | 472 | 473 | #[test] 474 | fn ifelse() { 475 | quicktest( 476 | cmd!(echo 477 | if (true) { abc (1.to_string()) } 478 | tail), 479 | "abc 1 tail" 480 | ); 481 | 482 | let counter = ::std::cell::Cell::new(0); 483 | quicktest( 484 | cmd!(echo 485 | ({counter.set(counter.get() + 1); "blah"}) 486 | if (true) { a } else { b } 487 | if (false) { c } else { d } 488 | tail), 489 | "blah a d tail" 490 | ); 491 | assert_eq!(counter.get(), 1); 492 | } 493 | 494 | #[test] 495 | fn test_mutref() { 496 | let cmd = &mut Command::new("echo"); 497 | let cmd: &mut Command = cmd!({cmd} foo); 498 | assert_eq!(cmd.output().unwrap().stdout, &b"foo\n"[..]); 499 | } 500 | 501 | #[test] 502 | fn test_parenparen() { 503 | quicktest(cmd!( echo ((2+2)) ), "4"); 504 | let foo = || "a"; 505 | quicktest(cmd!( echo ((foo)()) ), "a"); 506 | } 507 | 508 | #[test] 509 | fn for_loop() { 510 | quicktest(cmd!( 511 | echo 512 | for x in (&["a", "b"]) { 513 | foo (x) 514 | } 515 | end 516 | ), 517 | "foo a foo b" 518 | ); 519 | } 520 | 521 | #[test] 522 | fn not_moving() { 523 | let s = String::new(); 524 | cmd!((s)); 525 | cmd!(((s))); 526 | cmd!((s)); 527 | } 528 | -------------------------------------------------------------------------------- /command-macros/tests/reexport.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "nightly", feature(proc_macro_hygiene))] 2 | 3 | #[cfg(feature = "nightly")] extern crate command_macros; 4 | 5 | #[cfg(feature = "nightly")] 6 | mod tests { 7 | #[test] 8 | fn reexport_via_use() { 9 | use command_macros::command; 10 | let _ = command!(foo --bar); 11 | } 12 | } 13 | --------------------------------------------------------------------------------