subshell 1.23.1

Substrate shell
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

use crate::colors;
use crate::config_file::ConfigFile;
use crate::flags::Flags;
use crate::flags::TaskFlags;
use crate::fs_util;
use crate::proc_state::ProcState;
use deno_core::anyhow::bail;
use deno_core::anyhow::Context;
use deno_core::error::AnyError;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;

fn get_tasks_config(
  maybe_config_file: Option<&ConfigFile>,
) -> Result<BTreeMap<String, String>, AnyError> {
  if let Some(config_file) = maybe_config_file {
    let maybe_tasks_config = config_file.to_tasks_config()?;
    if let Some(tasks_config) = maybe_tasks_config {
      for key in tasks_config.keys() {
        if key.is_empty() {
          bail!("Configuration file task names cannot be empty");
        } else if !key
          .chars()
          .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | ':'))
        {
          bail!("Configuration file task names must only contain alpha-numeric characters, colons (:), underscores (_), or dashes (-). Task: {}", key);
        } else if !key.chars().next().unwrap().is_ascii_alphabetic() {
          bail!("Configuration file task names must start with an alphabetic character. Task: {}", key);
        }
      }
      Ok(tasks_config)
    } else {
      bail!("No tasks found in configuration file")
    }
  } else {
    bail!("No config file found")
  }
}

fn print_available_tasks(tasks_config: BTreeMap<String, String>) {
  eprintln!("{}", colors::green("Available tasks:"));

  for name in tasks_config.keys() {
    eprintln!("- {}", colors::cyan(name));
    eprintln!("    {}", tasks_config[name])
  }
}

pub async fn execute_script(
  flags: Flags,
  task_flags: TaskFlags,
) -> Result<i32, AnyError> {
  log::warn!(
    "{} deno task is unstable and may drastically change in the future",
    crate::colors::yellow("Warning"),
  );
  let flags = Arc::new(flags);
  let ps = ProcState::build(flags.clone()).await?;
  let tasks_config = get_tasks_config(ps.maybe_config_file.as_ref())?;
  let config_file_url = &ps.maybe_config_file.as_ref().unwrap().specifier;
  let config_file_path = if config_file_url.scheme() == "file" {
    config_file_url.to_file_path().unwrap()
  } else {
    bail!("Only local configuration files are supported")
  };

  if task_flags.task.is_empty() {
    print_available_tasks(tasks_config);
    return Ok(1);
  }

  let cwd = match task_flags.cwd {
    Some(path) => fs_util::canonicalize_path(&PathBuf::from(path))?,
    None => config_file_path.parent().unwrap().to_owned(),
  };
  let task_name = task_flags.task;
  let maybe_script = tasks_config.get(&task_name);

  if let Some(script) = maybe_script {
    let additional_args = flags
      .argv
      .iter()
      // surround all the additional arguments in double quotes
      // and santize any command substition
      .map(|a| format!("\"{}\"", a.replace('"', "\\\"").replace('$', "\\$")))
      .collect::<Vec<_>>()
      .join(" ");
    let script = format!("{} {}", script, additional_args);
    let script = script.trim();
    log::info!(
      "{} {} {}",
      colors::green("Task"),
      colors::cyan(&task_name),
      script,
    );
    let seq_list = deno_task_shell::parser::parse(script)
      .with_context(|| format!("Error parsing script '{}'.", task_name))?;
    let env_vars = std::env::vars().collect::<HashMap<String, String>>();
    let exit_code = deno_task_shell::execute(seq_list, env_vars, &cwd).await;
    Ok(exit_code)
  } else {
    eprintln!("Task not found: {}", task_name);
    print_available_tasks(tasks_config);
    Ok(1)
  }
}

#[cfg(test)]
mod test {
  use deno_ast::ModuleSpecifier;
  use pretty_assertions::assert_eq;

  use super::*;

  #[test]
  fn tasks_no_tasks() {
    run_task_error_test(r#"{}"#, "No tasks found in configuration file");
  }

  #[test]
  fn task_name_invalid_chars() {
    run_task_error_test(
      r#"{
        "tasks": {
          "build": "deno test",
          "some%test": "deno bundle mod.ts"
        }
      }"#,
      concat!(
        "Configuration file task names must only contain alpha-numeric ",
        "characters, colons (:), underscores (_), or dashes (-). Task: some%test",
      ),
    );
  }

  #[test]
  fn task_name_non_alpha_starting_char() {
    run_task_error_test(
      r#"{
        "tasks": {
          "build": "deno test",
          "1test": "deno bundle mod.ts"
        }
      }"#,
      concat!(
        "Configuration file task names must start with an ",
        "alphabetic character. Task: 1test",
      ),
    );
  }

  #[test]
  fn task_name_empty() {
    run_task_error_test(
      r#"{
        "tasks": {
          "build": "deno test",
          "": "deno bundle mod.ts"
        }
      }"#,
      "Configuration file task names cannot be empty",
    );
  }

  fn run_task_error_test(config_text: &str, expected_error: &str) {
    let config_dir = ModuleSpecifier::parse("file:///deno/").unwrap();
    let config_specifier = config_dir.join("tsconfig.json").unwrap();
    let config_file = ConfigFile::new(config_text, &config_specifier).unwrap();
    assert_eq!(
      get_tasks_config(Some(&config_file))
        .err()
        .unwrap()
        .to_string(),
      expected_error,
    );
  }
}