summaryrefslogblamecommitdiff
path: root/core/src/mill/main/ParseArgs.scala
blob: bafcd907e8b308fb8b1ffbf1153a03159b58f603 (plain) (tree)
1
2
3
4
5
6
7
8
9



                          
                                      



                                    
                                                                           














































































































                                                                               
                                                                                             





                                                                             




                                                                                 





                                                                           




                      
package mill.main

import mill.util.EitherOps
import fastparse.all._
import mill.define.{Segment, Segments}

object ParseArgs {

  def apply(scriptArgs: Seq[String])
    : Either[String, (List[(Option[Segments], Segments)], Seq[String])] = {
    val (selectors, args, isMultiSelectors) = extractSelsAndArgs(scriptArgs)
    for {
      _ <- validateSelectors(selectors)
      expandedSelectors <- EitherOps
        .sequence(selectors.map(expandBraces))
        .map(_.flatten)
      _ <- validateExpanded(expandedSelectors, isMultiSelectors)
      selectors <- EitherOps.sequence(expandedSelectors.map(extractSegments))
    } yield (selectors.toList, args)
  }

  def extractSelsAndArgs(
      scriptArgs: Seq[String]): (Seq[String], Seq[String], Boolean) = {
    val multiFlags = Seq("--all", "--seq")
    val isMultiSelectors = scriptArgs.headOption.exists(multiFlags.contains)

    if (isMultiSelectors) {
      val dd = scriptArgs.indexOf("--")
      val selectors = (if (dd == -1) scriptArgs
                       else scriptArgs.take(dd)).filterNot(multiFlags.contains)
      val args = if (dd == -1) Seq.empty else scriptArgs.drop(dd + 1)

      (selectors, args, isMultiSelectors)
    } else {
      (scriptArgs.take(1), scriptArgs.drop(1), isMultiSelectors)
    }
  }

  private def validateSelectors(
      selectors: Seq[String]): Either[String, Unit] = {
    if (selectors.isEmpty || selectors.exists(_.isEmpty))
      Left("Selector cannot be empty")
    else Right(())
  }

  private def validateExpanded(expanded: Seq[String],
                               isMulti: Boolean): Either[String, Unit] = {
    if (!isMulti && expanded.length > 1)
      Left("Please use --all flag to run multiple tasks")
    else Right(())
  }

  def expandBraces(selectorString: String): Either[String, List[String]] = {
    parseBraceExpansion(selectorString) match {
      case f: Parsed.Failure           => Left(s"Parsing exception ${f.msg}")
      case Parsed.Success(expanded, _) => Right(expanded.toList)
    }
  }

  private sealed trait Fragment
  private object Fragment {
    case class Keep(value: String) extends Fragment
    case class Expand(values: List[List[Fragment]]) extends Fragment

    def unfold(fragments: List[Fragment]): Seq[String] = {
      fragments match {
        case head :: rest =>
          val prefixes = head match {
            case Keep(v)          => Seq(v)
            case Expand(Nil)      => Seq("{}")
            case Expand(List(vs)) => unfold(vs).map("{" + _ + "}")
            case Expand(vss)      => vss.flatMap(unfold)
          }
          for {
            prefix <- prefixes
            suffix <- unfold(rest)
          } yield prefix + suffix

        case Nil => Seq("")
      }
    }
  }

  private object BraceExpansionParser {
    val plainChars =
      P(CharsWhile(c => c != ',' && c != '{' && c != '}')).!.map(Fragment.Keep)

    val toExpand: P[Fragment] =
      P("{" ~ braceParser.rep(1).rep(sep = ",") ~ "}").map(
        x => Fragment.Expand(x.toList.map(_.toList))
      )

    val braceParser = P(toExpand | plainChars)

    val parser = P(braceParser.rep(1).rep(sep = ",") ~ End)
  }

  private def parseBraceExpansion(input: String) = {
    def unfold(vss: List[Seq[String]]): Seq[String] = {
      vss match {
        case Nil => Seq("")
        case head :: rest =>
          for {
            str <- head
            r <- unfold(rest)
          } yield
            r match {
              case "" => str
              case _  => str + "," + r
            }
      }
    }

    BraceExpansionParser.parser
      .map { vss =>
        val stringss = vss.map(x => Fragment.unfold(x.toList)).toList
        unfold(stringss)
      }
      .parse(input)
  }

  def extractSegments(selectorString: String): Either[String, (Option[Segments], Segments)] =
    parseSelector(selectorString) match {
      case f: Parsed.Failure           => Left(s"Parsing exception ${f.msg}")
      case Parsed.Success(selector, _) => Right(selector)
    }

  private def parseSelector(input: String) = {
    val identChars = ('a' to 'z') ++ ('A' to 'Z') ++ ('0' to '9') ++ "_"
    val ident = P( CharsWhileIn(identChars) ).!
    val ident2 = P( CharsWhileIn(identChars ++ ".") ).!
    val segment = P( ident ).map( Segment.Label)
    val crossSegment = P("[" ~ ident2.rep(1, sep = ",") ~ "]").map(Segment.Cross)
    val simpleQuery = P(segment ~ ("." ~ segment | crossSegment).rep).map {
      case (h, rest) => Segments(h :: rest.toList:_*)
    }
    val query = P( simpleQuery ~ ("/" ~/ simpleQuery).?).map{
      case (q, None) => (None, q)
      case (q, Some(q2)) => (Some(q), q2)
    }
    query.parse(input)
  }

}