I need to write a CLI for the find command in scala3 for school. The syntax is: run [path] [--filterName filterParameter]
. I currently have it working for one filter at a time, but once it try the command with two or more filters, only the last one takes effect.
So for example, if I run: run src/ --name main
I get the output src/main
and src/main/scala/find/cli/main.scala
, which is correct.
And if I run: run src/ --type scala
I get all the scala files correctly.
But if I run: run src/ --name main --type scala
, instead of getting src/main/scala/find/cli/main.scala
I get the same output as running run src/ --type scala
. And if I invert the filters, so run: run src/ --type scala --name main
I get the same output as running: run src/ --name main
.
cli.scala :
package find
package cli
import scala.collection.mutable
import scala.util.matching.Regex
val usageMap: mutable.LinkedHashMap[String, String] = mutable.LinkedHashMap(
"name" -> "[--name X] (where X is a string)",
"type" -> "[--type X] (where X is a string)",
"size" -> "[--size -Xc|--size Xc|--size +Xc] (where X is a number)",
"count" -> "[--count X] (where X is an integer)",
"minDepth" -> "[--minDepth X] (where X is an integer)",
"maxDepth" -> "[--maxDepth X] (where X is an integer)",
"mode" -> "[--mode X] (where X is an element of: (and, or, xor). If the mode filter is not given, \"--mode and\" will be used by default)",
"contains" -> "[--contains X] (where X is a string)",
"not" -> "[--not F V] (where F is a filter name and V is it's value)",
"help" -> "[--help X] (where X is optional and a filter name)"
)
val SIZE_RE = "size ([+-]?)([0-9]+)c".r
val INT_RE = "([0-9]+)".r
val DEPTH_RE = "(min|max)Depth (.*)".r
var path = ""
var file: cs214.Entry = cs214.MockFile("zef", None, 2)
var failMsg = ""
var minDepth = -1
var maxDepth = -1
var fileCount = -1
var findMode = "and"
/** Command-line interface for the `find` program.
*
* Note: this function is provided to you and you do not need to understand how
* it works yet.
*
* @param args
* the command-line arguments.
*/
@main def cs214find(args: String*) =
entryPoint(args)(cs214.open)
def entryPoint(args: Seq[String])(open: String => cs214.Entry): Boolean =
if args.length < 1 then
return fail("No path argument given.")
path = args.head
val expr = args.tail
path match
case "--help" =>
if expr.isEmpty then
println("Usages:\n * run [path] " + usageMap("name"))
usageMap.foreach((filter, usage) => if filter != "name" then println(" " * 14 + usage))
println(
usageMap.filter((filter, _) => filter != "help")
.map((filter, usage) => usage.split("\\(where")(0))
.toList.foldLeft(" * run [path] ")((res, u) => res + u)
)
else if expr.length > 1 then fail(createFailMsg(error = "help takes at most 1 parameter", filter = "help"))
else
if !usageMap.contains(expr.head) then fail(createFailMsg(error = f"${expr.head} is not a valid filter"))
if expr.head.nonEmpty then println("Usage: run [path] " + usageMap(expr.head))
false
case _ =>
file =
try
open(path)
catch
case _: java.nio.file.NoSuchFileException =>
return fail(createFailMsg(error = f"Path '$path' does not exist."))
case e: Exception =>
return fail(createFailMsg(error = f"Open raised an exception: ${e.getMessage}."))
var results: List[List[String]] = List()
var filters = args.mkString(" ").split("--")
filters(0) = filters(0).replaceAll(path + " ", "")
// Check for duplicate filters
filters = filters.filter(f => f.nonEmpty)
filters
.flatMap(filter => List(filter.trim.split(" ")(0)))
.groupBy(identity).map((f, c) => (f, c.length))
.filter(_._2 > 1)
.foreach(duplicate => failMsg += f"Cannot have duplicate filter: ${duplicate._1}\n")
if failMsg.nonEmpty then fail(failMsg + "To check the proper usage, use: run --help")
else
if filters.isEmpty then showResults(find(file, _ => true))
else
filters.filterNot(f => f.matches("-?(name|type|size)\\s?.*")).foreach(filter => getFilterData(filter))
for filter <- filters.filter(f => f.matches("-?(name|type|size)\\s?.*")) yield
getFilterCondition(filter) match
case Nil => None
case l => results = l :: results
if failMsg.nonEmpty then fail(failMsg)
else if results.length == 1 then showResults(results.head)
else
showResults(
findMode match
case "and" => results.flatten.distinct.filter(elem => results.forall(_.contains(elem)))
//case "or" => conditionValues.flatten.distinct
//case "xor" => conditionValues.reduce(_^_)
case _ => List()
)
def showResults(res: List[String]): Boolean =
for f <- res yield println(f)
res.nonEmpty
def getFilterData(filter: String) =
filter match
case s"count $count" =>
INT_RE.findFirstIn(count) match
case None => failMsg += createFailMsg(filter = "count")
case Some(value) => fileCount = value.toInt
case DEPTH_RE(minOrMax, depth) =>
INT_RE.findFirstIn(depth) match
case None => failMsg += createFailMsg(filter = s"${minOrMax}Depth")
case Some(value) => if minOrMax == "min" then minDepth = value.toInt else maxDepth = value.toInt
case s"mode $mode" =>
if !List("and", "or", "xor").contains(mode) then fail(createFailMsg(filter = "mode"))
else findMode = mode
case s"not$p" => ???
case filter =>
if filter.startsWith("-") then failMsg += f"Error: $filter is not a valid filter\nUsage:\n * Use run --help to check proper usage\n * Try running this instead: run $path -$filter\n"
else if usageMap.contains(filter.split(" ")(0)) then failMsg += createFailMsg(error = f"$filter is missing it's required parameter", filter = filter)
else failMsg += createFailMsg(error = f"$filter is not a valid filter")
def getFilterCondition(filter: String): List[String] =
filter match
case s"name $name" => find(file, (entry: cs214.Entry) => isValidName(entry, name))
case s"type $extension" => find(file, (entry: cs214.Entry) => !entry.isDirectory() && entry.name().split("\\.")(1) == `extension`)
case s"size $size" =>
SIZE_RE.findFirstMatchIn(size) match
case None =>
failMsg += createFailMsg(filter = "size")
List()
case Some(matched) =>
val value = matched.group(2).toLong
matched.group(1) match
case "+" => find(file, (entry: cs214.Entry) => !entry.isDirectory() && entry.size() >= value)
case "-" => find(file, (entry: cs214.Entry) => !entry.isDirectory() && entry.size() <= value)
case "" => find(file, (entry: cs214.Entry) => !entry.isDirectory() && entry.size() == value)
case filter =>
if filter.startsWith("-") then failMsg += f"Error: $filter is not a valid filter\nUsage:\n * Use run --help to check proper usage\n * Try running this instead: run $path -$filter\n"
else if usageMap.contains(filter.split(" ")(0)) then failMsg += createFailMsg(error = f"$filter is missing it's required parameter", filter = filter)
else failMsg += createFailMsg(error = f"$filter is not a valid filter")
List()
def isValidName(entry: cs214.Entry, name: String): Boolean =
def wildcard(wildName: List[Char], wildEntry: List[Char]): Boolean =
(wildName, wildEntry) match
case (Nil, Nil) => true
case (Nil, _) => false
case ('?' :: ntail, _ :: etail) => wildcard(ntail, etail)
case ('*' :: ntail, Nil) => wildcard(ntail, wildEntry)
case ('*' :: ntail, _ :: etail) => wildcard(ntail, wildEntry) || wildcard(wildName, etail)
case (_ :: ntail, Nil) => false
case (n :: ntail, e :: etail) => n == e && wildcard(ntail, etail)
if name.contains('*') || name.contains('?') then
(entry.isDirectory() && wildcard(name.toList, entry.name().toList)) ||
(!entry.isDirectory() && wildcard(name.toList, entry.name().split("\\.").head.toList))
else
(entry.isDirectory() && entry.name() == name) || (!entry.isDirectory() && entry.name().split("\\.").head == name)
def createFailMsg(error: String = "", filter: String = ""): String =
if filter.nonEmpty then
if error.nonEmpty then f"Error: $error\nTo check the proper usage, use: run --help $filter"
else f"Error: Incorrect Usage of $filter\nUsage: run $path ${usageMap(filter).replace("[", "").replace("]", "")}"
else if filter.isEmpty && error.nonEmpty then f"Error: $error\nTo check the proper usage, use: run --help"
else ""
def fail(msg: String): Boolean =
System.err.println(msg)
false
find.scala :
package find
def find(entry: cs214.Entry, f: cs214.Entry => Boolean): List[String] =
var res: List[String] = List()
if f(entry) then res = List(entry.path())
if entry.isDirectory() && entry.hasChildren() then res = find(entry.firstChild(), f) ::: res
if entry.hasNextSibling() then res = find(entry.nextSibling(), f) ::: res
res.reverse
The results seem to be lost around here in cli.scala:
for filter <- filters.filter(f => f.matches("-?(name|type|size)\\s?.*")) yield
getFilterCondition(filter) match <-- results lost
case Nil => None
case l => results = l :: results
And I do not understand how and why.
Any help would be greatly appreciated.
P.S: Some parts are not finished such as the implementation of the not filter and the depth search, this is normal.