@@ -122,7 +122,11 @@ fn parse_args() -> Result<(CliArgs, RunMode, Vec<String>), lexopt::Error> { |
122 | 122 | Short('B') => args.dont_write_bytecode = true, |
123 | 123 | Short('c') => { |
124 | 124 | let cmd = parser.value()?.string()?; |
125 | | - return Ok((args, RunMode::Command(cmd), argv("-c".to_owned(), parser)?)); |
| 125 | + return Ok(( |
| 126 | + args, |
| 127 | + RunMode::Command(dedent(&cmd)), |
| 128 | + argv("-c".to_owned(), parser)?, |
| 129 | + )); |
126 | 130 | } |
127 | 131 | Short('d') => args.debug += 1, |
128 | 132 | Short('E') => args.ignore_environment = true, |
@@ -465,3 +469,70 @@ pub(crate) fn split_paths<T: AsRef<std::ffi::OsStr> + ?Sized>( |
465 | 469 | .into() |
466 | 470 | }) |
467 | 471 | } |
| 472 | + |
| 473 | +/// Remove common whitespace prefix from all lines in a string. |
| 474 | +/// |
| 475 | +/// This is like textwrap.dedent, and is used to process -c's code |
| 476 | +/// argument. It's different from ruff's dedent, which does not |
| 477 | +/// distinguish between tab and space characters when dedenting. |
| 478 | +fn dedent(input: &str) -> String { |
| 479 | + let mut prefix: Option<String> = None; |
| 480 | + let isspace = |c| c == ' ' || c == '\t'; |
| 481 | + |
| 482 | + // All-whitespace lines become empty. |
| 483 | + let deblanked: Vec<&str> = input |
| 484 | + .lines() |
| 485 | + .map(|line| if line.chars().all(isspace) { "" } else { line }) |
| 486 | + .collect(); |
| 487 | + |
| 488 | + // Find maximum common whitespace prefix, if any. |
| 489 | + for line in deblanked.iter() { |
| 490 | + if line.is_empty() { |
| 491 | + continue; |
| 492 | + } |
| 493 | + if let Some(ref mut pstr) = prefix { |
| 494 | + for (i, (c, pc)) in line.chars().zip(pstr.chars()).enumerate() { |
| 495 | + // It's okay if `line` is shorter than `pstr`. At |
| 496 | + // least one char in `line` must be non-whitespace, |
| 497 | + // and if `line` is shorter than `pstr`, this |
| 498 | + // non-whitespace char will be compared to a |
| 499 | + // whitespace char, and loop will terminate. |
| 500 | + if c != pc { |
| 501 | + pstr.truncate(i); |
| 502 | + break; |
| 503 | + } |
| 504 | + } |
| 505 | + } else { |
| 506 | + let mut pstr = String::new(); |
| 507 | + for c in line.chars() { |
| 508 | + if isspace(c) { |
| 509 | + pstr.push(c); |
| 510 | + } else { |
| 511 | + break; |
| 512 | + } |
| 513 | + } |
| 514 | + prefix = Some(pstr); |
| 515 | + continue; |
| 516 | + } |
| 517 | + } |
| 518 | + |
| 519 | + if let Some(pstr) = prefix { |
| 520 | + // Strip common prefix. |
| 521 | + deblanked |
| 522 | + .iter() |
| 523 | + .map(|line| { |
| 524 | + if line.is_empty() { |
| 525 | + String::from("") |
| 526 | + } else { |
| 527 | + // All non-empty lines start with pstr, must be at |
| 528 | + // least pstr.len() long. |
| 529 | + String::from(line.get(pstr.len()..).unwrap()) |
| 530 | + } |
| 531 | + }) |
| 532 | + .collect::<Vec<_>>() |
| 533 | + .join("\n") |
| 534 | + } else { |
| 535 | + // No prefix found: all lines blank. |
| 536 | + deblanked.join("\n") |
| 537 | + } |
| 538 | +} |