From ca2471a1a5a6ff47106eb33b50eae22d427ae3ed Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Thu, 18 Apr 2024 13:12:22 +0530 Subject: [PATCH 01/16] support for file based config --- build.sbt | 5 +- .../src/main/scala/zio/cli/CliApp.scala | 89 ++++++++++++++++--- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/build.sbt b/build.sbt index 41d08b74..98dc3f9d 100644 --- a/build.sbt +++ b/build.sbt @@ -62,11 +62,12 @@ lazy val zioCli = crossProject(JSPlatform, JVMPlatform, NativePlatform) "dev.zio" %%% "zio-json" % "0.6.2", "dev.zio" %%% "zio-streams" % zioVersion, "dev.zio" %%% "zio-test" % zioVersion % Test, - "dev.zio" %%% "zio-test-sbt" % zioVersion % Test + "dev.zio" %%% "zio-test-sbt" % zioVersion % Test, + "dev.zio" %% "zio-nio" % "2.0.0" ) ) .jvmSettings( - libraryDependencies += "dev.zio" %% "zio-process" % "0.7.1" + libraryDependencies += "dev.zio" %% "zio-process" % "0.7.1", ) .nativeSettings(Test / fork := false) .nativeSettings( diff --git a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala index de346c2d..e1728f84 100644 --- a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala +++ b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala @@ -10,6 +10,8 @@ import zio.cli.completion.{Completion, CompletionScript} import zio.cli.figlet.FigFont import scala.annotation.tailrec +import zio.nio.file.{Files, Path} +import java.io.IOException /** * A `CliApp[R, E]` is a complete description of a command-line application, which requires environment `R`, and may @@ -66,6 +68,59 @@ object CliApp { def printDocs(helpDoc: HelpDoc): UIO[Unit] = printLine(helpDoc.toPlaintext(80)).! + def checkAndGetOptionsFilePaths(topLevelCommand: String): Task[List[String]] = { + val filename = s".$topLevelCommand" + val cwd = java.lang.System.getProperty("user.dir") + val homeDirOpt = java.lang.System.getProperty("user.home") + + def parentPaths(path: String): List[String] = { + val parts = path.split(java.io.File.separatorChar).filterNot(_.isEmpty) + (0 to parts.length) + .map(i => s"${java.io.File.separatorChar}${parts.take(i).mkString(java.io.File.separator)}") + .toList + } + + val paths = parentPaths(cwd) + val pathsToCheck = homeDirOpt :: paths + + // Use ZIO to filter the paths + ZIO + .foreach(pathsToCheck) { path => + Files.exists(Path(path, filename)) + } + .map(_.zip(pathsToCheck).collect { case (exists, path) if exists => path }) + } + + // Merges a list of options, removing any duplicate keys. + // If there are options with the same keys but different values, it will use the value from the last option in the + // list. + def mergeOptionsBasedOnPriority(options: List[String]): List[String] = { + val mergedOptions = options.flatMap { opt => + opt.split('=') match { + case Array(key) => Some(key -> None) + case Array(key, value) => Some(key -> value) + case _ => + None // handles the case when there isn't exactly one '=' in the string + } + }.toMap.toList.map { + case (key, None) => key + case (key, value) => s"$key=$value" + } + + mergedOptions + } + + def loadOptionsFromFile(topLevelCommand: String): ZIO[Any, IOException, List[String]] = + checkAndGetOptionsFilePaths(topLevelCommand).flatMap { filePaths => + ZIO.foreach(filePaths) { filePath => + readFileAsString(Path(filePath, s".$topLevelCommand")) + } + }.map(_.flatten).refineToOrDie[IOException] + + def readFileAsString(path: zio.nio.file.Path): Task[List[String]] = + Files + .readAllLines(path) + def run(args: List[String]): ZIO[R, CliError[E], Option[A]] = { def executeBuiltIn(builtInOption: BuiltInOption): ZIO[R, CliError[E], Option[A]] = builtInOption match { @@ -128,19 +183,27 @@ object CliApp { case Command.Subcommands(parent, _) => prefix(parent) } - self.command - .parse(prefix(self.command) ++ args, self.config) - .foldZIO( - e => printDocs(e.error) *> ZIO.fail(CliError.Parsing(e)), - { - case CommandDirective.UserDefined(_, value) => - self.execute(value).map(Some(_)).mapError(CliError.Execution(_)) - case CommandDirective.BuiltIn(x) => - executeBuiltIn(x).catchSome { case err @ CliError.Parsing(e) => - printDocs(e.error) *> ZIO.fail(err) - } - } - ) + // Reading args from config files and combining with provided args + val combinedArgs: ZIO[R, CliError[E], List[String]] = + loadOptionsFromFile(self.command.names.head).flatMap { configArgs => + ZIO.succeed(configArgs ++ args) + }.mapError(e => CliError.IO(e)) // Convert any IO errors into CliError.IO + + combinedArgs.flatMap { allArgs => + self.command + .parse(prefix(self.command) ++ allArgs, self.config) + .foldZIO( + e => printDocs(e.error) *> ZIO.fail(CliError.Parsing(e)), + { + case CommandDirective.UserDefined(_, value) => + self.execute(value).map(Some(_)).mapError(CliError.Execution(_)) + case CommandDirective.BuiltIn(x) => + executeBuiltIn(x).catchSome { case err @ CliError.Parsing(e) => + printDocs(e.error) *> ZIO.fail(err) + } + } + ) + } } override def flatMap[R1 <: R, E1 >: E, B](f: A => ZIO[R1, E1, B]): CliApp[R1, E1, B] = From bb2461be73e041e58a10ff031dc990c52e4222ad Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Thu, 18 Apr 2024 15:46:20 +0530 Subject: [PATCH 02/16] scalafmt fixed --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 98dc3f9d..c96be346 100644 --- a/build.sbt +++ b/build.sbt @@ -67,7 +67,7 @@ lazy val zioCli = crossProject(JSPlatform, JVMPlatform, NativePlatform) ) ) .jvmSettings( - libraryDependencies += "dev.zio" %% "zio-process" % "0.7.1", + libraryDependencies += "dev.zio" %% "zio-process" % "0.7.1" ) .nativeSettings(Test / fork := false) .nativeSettings( From ea35be533575fc5e66cf1669929cab3a07b83fc3 Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Fri, 19 Apr 2024 17:42:41 +0530 Subject: [PATCH 03/16] build fix set collection-compat to fixed 2.12.0 --- build.sbt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/build.sbt b/build.sbt index c96be346..e0ad5981 100644 --- a/build.sbt +++ b/build.sbt @@ -21,7 +21,8 @@ inThisBuild( pgpSecretRing := file("/tmp/secret.asc"), scmInfo := Some( ScmInfo(url("https://github.com/zio/zio-cli/"), "scm:git:git@github.com:zio/zio-cli.git") - ) + ), + dependencyOverrides += "org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0" ) ) @@ -58,12 +59,12 @@ lazy val zioCli = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings(buildInfoSettings("zio.cli")) .settings( libraryDependencies ++= Seq( - "dev.zio" %%% "zio" % zioVersion, - "dev.zio" %%% "zio-json" % "0.6.2", - "dev.zio" %%% "zio-streams" % zioVersion, - "dev.zio" %%% "zio-test" % zioVersion % Test, - "dev.zio" %%% "zio-test-sbt" % zioVersion % Test, - "dev.zio" %% "zio-nio" % "2.0.0" + "dev.zio" %%% "zio" % zioVersion, + "dev.zio" %%% "zio-json" % "0.6.2", + "dev.zio" %%% "zio-streams" % zioVersion, + "dev.zio" %%% "zio-test" % zioVersion % Test, + "dev.zio" %%% "zio-test-sbt" % zioVersion % Test, + "dev.zio" %% "zio-nio" % "2.0.0" ) ) .jvmSettings( From f8cba2ae34936452200303a0de3caad07fc545a5 Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Fri, 19 Apr 2024 17:43:50 +0530 Subject: [PATCH 04/16] formatting fixed --- build.sbt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build.sbt b/build.sbt index e0ad5981..a8c53a3b 100644 --- a/build.sbt +++ b/build.sbt @@ -22,7 +22,7 @@ inThisBuild( scmInfo := Some( ScmInfo(url("https://github.com/zio/zio-cli/"), "scm:git:git@github.com:zio/zio-cli.git") ), - dependencyOverrides += "org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0" + dependencyOverrides += "org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0" ) ) @@ -59,12 +59,12 @@ lazy val zioCli = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings(buildInfoSettings("zio.cli")) .settings( libraryDependencies ++= Seq( - "dev.zio" %%% "zio" % zioVersion, - "dev.zio" %%% "zio-json" % "0.6.2", - "dev.zio" %%% "zio-streams" % zioVersion, - "dev.zio" %%% "zio-test" % zioVersion % Test, - "dev.zio" %%% "zio-test-sbt" % zioVersion % Test, - "dev.zio" %% "zio-nio" % "2.0.0" + "dev.zio" %%% "zio" % zioVersion, + "dev.zio" %%% "zio-json" % "0.6.2", + "dev.zio" %%% "zio-streams" % zioVersion, + "dev.zio" %%% "zio-test" % zioVersion % Test, + "dev.zio" %%% "zio-test-sbt" % zioVersion % Test, + "dev.zio" %% "zio-nio" % "2.0.0" ) ) .jvmSettings( From 9fbc76d75b0ff08a2e33b97c97d119aa0bb498ce Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Fri, 19 Apr 2024 18:50:39 +0530 Subject: [PATCH 05/16] Removing the nio as it doesnt support scalaJS yet As of 19th April zio-nio doesnt support scalaJS so using java to read file and zio core for resource management --- build.sbt | 3 +- .../src/main/scala/zio/cli/CliApp.scala | 34 +++++++++++++------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/build.sbt b/build.sbt index a8c53a3b..f2042f50 100644 --- a/build.sbt +++ b/build.sbt @@ -63,8 +63,7 @@ lazy val zioCli = crossProject(JSPlatform, JVMPlatform, NativePlatform) "dev.zio" %%% "zio-json" % "0.6.2", "dev.zio" %%% "zio-streams" % zioVersion, "dev.zio" %%% "zio-test" % zioVersion % Test, - "dev.zio" %%% "zio-test-sbt" % zioVersion % Test, - "dev.zio" %% "zio-nio" % "2.0.0" + "dev.zio" %%% "zio-test-sbt" % zioVersion % Test ) ) .jvmSettings( diff --git a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala index e1728f84..bd926b00 100644 --- a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala +++ b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala @@ -10,8 +10,11 @@ import zio.cli.completion.{Completion, CompletionScript} import zio.cli.figlet.FigFont import scala.annotation.tailrec -import zio.nio.file.{Files, Path} import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import scala.io.Source +import java.nio.file.Paths /** * A `CliApp[R, E]` is a complete description of a command-line application, which requires environment `R`, and may @@ -86,7 +89,7 @@ object CliApp { // Use ZIO to filter the paths ZIO .foreach(pathsToCheck) { path => - Files.exists(Path(path, filename)) + ZIO.succeed(Files.exists(Path.of(path, filename))) } .map(_.zip(pathsToCheck).collect { case (exists, path) if exists => path }) } @@ -111,15 +114,24 @@ object CliApp { } def loadOptionsFromFile(topLevelCommand: String): ZIO[Any, IOException, List[String]] = - checkAndGetOptionsFilePaths(topLevelCommand).flatMap { filePaths => - ZIO.foreach(filePaths) { filePath => - readFileAsString(Path(filePath, s".$topLevelCommand")) - } - }.map(_.flatten).refineToOrDie[IOException] - - def readFileAsString(path: zio.nio.file.Path): Task[List[String]] = - Files - .readAllLines(path) + for { + filePaths <- self + .checkAndGetOptionsFilePaths(topLevelCommand) + .refineToOrDie[IOException] + lines <- ZIO + .foreach(filePaths) { filePath => + ZIO.acquireReleaseWith( + ZIO.attempt( + Source.fromFile(Paths.get(filePath, "." + topLevelCommand).toFile) + ) + )(source => ZIO.attempt(source.close()).orDie + ) { source => + ZIO.attempt(source.getLines().toList.filter(_.trim.nonEmpty)) + } + } + .map(_.flatten) + .refineToOrDie[IOException] + } yield lines def run(args: List[String]): ZIO[R, CliError[E], Option[A]] = { def executeBuiltIn(builtInOption: BuiltInOption): ZIO[R, CliError[E], Option[A]] = From 79ea5a5d2cad843d92cc862caab8f869a3265e0a Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Fri, 19 Apr 2024 19:33:29 +0530 Subject: [PATCH 06/16] formatting fixed --- zio-cli/shared/src/main/scala/zio/cli/CliApp.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala index bd926b00..45cc4dab 100644 --- a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala +++ b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala @@ -124,8 +124,7 @@ object CliApp { ZIO.attempt( Source.fromFile(Paths.get(filePath, "." + topLevelCommand).toFile) ) - )(source => ZIO.attempt(source.close()).orDie - ) { source => + )(source => ZIO.attempt(source.close()).orDie) { source => ZIO.attempt(source.getLines().toList.filter(_.trim.nonEmpty)) } } From 3fd02785d10ce0c85ca87691930ffb8811b81ed4 Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Fri, 19 Apr 2024 23:57:34 +0530 Subject: [PATCH 07/16] adding tests init --- .../test/scala/zio/cli/FileBasedArgs.scala | 60 +++++++++ .../src/main/scala/zio/cli/CliApp.scala | 125 +++++++++--------- 2 files changed, 121 insertions(+), 64 deletions(-) create mode 100644 zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala diff --git a/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala b/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala new file mode 100644 index 00000000..d72415ff --- /dev/null +++ b/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala @@ -0,0 +1,60 @@ +import zio._ +import zio.test._ +import zio.test.Assertion._ +import java.nio.file.{Files, Paths, Path} +import zio.cli.CliApp +import java.io.IOException + +object FileBasedArgs extends ZIOSpecDefault { + def spec = suite("Vivasvan")( + test("should load options from files and merge them appropriatly") { + for { + // Create Sample config files + cwd <- ZIO.succeed(Paths.get(java.lang.System.getProperty("user.dir"))) + homeDir <- ZIO.succeed(Paths.get(java.lang.System.getProperty("user.home"))) + _ <- createSampleConfigFiles(cwd, homeDir) + + // Check if the func checkAndGetOptionsFilePaths can + config_args <- CliApp.loadOptionsFromConfigFiles("testApp") + + _ <- cleanUpSampleConfigFiles(cwd: Path, homeDir: Path) + + } yield assert(config_args)(hasSameElements(List("home=true", "dir=true", "home=false"))) + }, + test("should merge duplicate options") { + val options = List("option1=value1", "option2=value2", "option1=value3"); + val mergedOptions = CliApp.mergeOptionsBasedOnPriority(options); + assert(mergedOptions)(equalTo(List("option1=value3", "option2=value2"))); + }, + test("should return directory ~/home and ./ which have .testApp config file for loading the args") { + for { + // Create Sample config files + cwd <- ZIO.succeed(Paths.get(java.lang.System.getProperty("user.dir"))) + homeDir <- ZIO.succeed(Paths.get(java.lang.System.getProperty("user.home"))) + _ <- createSampleConfigFiles(cwd, homeDir) + + // Check if the func checkAndGetOptionsFilePaths can + paths <- CliApp.findPathsOfCliConfigFiles("testApp") + + _ <- cleanUpSampleConfigFiles(cwd: Path, homeDir: Path) + + } yield assert(paths)(hasSameElements(List(homeDir.toString(), cwd.toString()))) + } + ) + + def createSampleConfigFiles(cwd: Path, homeDir: Path): IO[IOException, Unit] = + ZIO.attempt { + Files.write(Paths.get(homeDir.toString(), ".testApp"), java.util.Arrays.asList("home=true")); + Files.write(Paths.get(cwd.toString(), ".testApp"), java.util.Arrays.asList("dir=true\nhome=false")); + + () + }.refineToOrDie[IOException] + + def cleanUpSampleConfigFiles(cwd: Path, homeDir: Path): IO[IOException, Unit] = + ZIO.attempt { + Files.delete(Paths.get(homeDir.toString(), ".testApp")); + Files.delete(Paths.get(cwd.toString(), ".testApp")); + + () + }.refineToOrDie[IOException] +} diff --git a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala index 45cc4dab..8f874776 100644 --- a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala +++ b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala @@ -53,6 +53,64 @@ object CliApp { )(execute: Model => ZIO[R, E, A]): CliApp[R, E, A] = CliAppImpl(name, version, summary, command, execute, footer, config, figFont) + def findPathsOfCliConfigFiles(topLevelCommand: String): Task[List[String]] = { + val filename = s".$topLevelCommand" + val cwd = java.lang.System.getProperty("user.dir") + val homeDirOpt = java.lang.System.getProperty("user.home") + + def parentPaths(path: String): List[String] = { + val parts = path.split(java.io.File.separatorChar).filterNot(_.isEmpty) + (0 to parts.length) + .map(i => s"${java.io.File.separatorChar}${parts.take(i).mkString(java.io.File.separator)}") + .toList + } + + val paths = parentPaths(cwd) + val pathsToCheck = homeDirOpt :: paths + + // Use ZIO to filter the paths + ZIO + .foreach(pathsToCheck)(path => ZIO.succeed(Files.exists(Path.of(path, filename)))) + .map(_.zip(pathsToCheck).collect { case (exists, path) if exists => path }) + } + + // Merges a list of options, removing any duplicate keys. + // If there are options with the same keys but different values, it will use the value from the last option in the + // list. + def mergeOptionsBasedOnPriority(options: List[String]): List[String] = { + val mergedOptions = options.flatMap { opt => + opt.split('=') match { + case Array(key) => Some(key -> None) + case Array(key, value) => Some(key -> value) + case _ => + None // handles the case when there isn't exactly one '=' in the string + } + }.toMap.toList.map { + case (key, None) => key + case (key, value) => s"$key=$value" + } + + mergedOptions + } + + def loadOptionsFromConfigFiles(topLevelCommand: String): ZIO[Any, IOException, List[String]] = + for { + filePaths <- findPathsOfCliConfigFiles(topLevelCommand) + .refineToOrDie[IOException] + lines <- ZIO + .foreach(filePaths) { filePath => + ZIO.acquireReleaseWith( + ZIO.attempt( + Source.fromFile(Paths.get(filePath, "." + topLevelCommand).toFile) + ) + )(source => ZIO.attempt(source.close()).orDie) { source => + ZIO.attempt(source.getLines().toList.filter(_.trim.nonEmpty)) + } + } + .map(_.flatten) + .refineToOrDie[IOException] + } yield lines + private[cli] case class CliAppImpl[-R, +E, Model, +A]( name: String, version: String, @@ -71,67 +129,6 @@ object CliApp { def printDocs(helpDoc: HelpDoc): UIO[Unit] = printLine(helpDoc.toPlaintext(80)).! - def checkAndGetOptionsFilePaths(topLevelCommand: String): Task[List[String]] = { - val filename = s".$topLevelCommand" - val cwd = java.lang.System.getProperty("user.dir") - val homeDirOpt = java.lang.System.getProperty("user.home") - - def parentPaths(path: String): List[String] = { - val parts = path.split(java.io.File.separatorChar).filterNot(_.isEmpty) - (0 to parts.length) - .map(i => s"${java.io.File.separatorChar}${parts.take(i).mkString(java.io.File.separator)}") - .toList - } - - val paths = parentPaths(cwd) - val pathsToCheck = homeDirOpt :: paths - - // Use ZIO to filter the paths - ZIO - .foreach(pathsToCheck) { path => - ZIO.succeed(Files.exists(Path.of(path, filename))) - } - .map(_.zip(pathsToCheck).collect { case (exists, path) if exists => path }) - } - - // Merges a list of options, removing any duplicate keys. - // If there are options with the same keys but different values, it will use the value from the last option in the - // list. - def mergeOptionsBasedOnPriority(options: List[String]): List[String] = { - val mergedOptions = options.flatMap { opt => - opt.split('=') match { - case Array(key) => Some(key -> None) - case Array(key, value) => Some(key -> value) - case _ => - None // handles the case when there isn't exactly one '=' in the string - } - }.toMap.toList.map { - case (key, None) => key - case (key, value) => s"$key=$value" - } - - mergedOptions - } - - def loadOptionsFromFile(topLevelCommand: String): ZIO[Any, IOException, List[String]] = - for { - filePaths <- self - .checkAndGetOptionsFilePaths(topLevelCommand) - .refineToOrDie[IOException] - lines <- ZIO - .foreach(filePaths) { filePath => - ZIO.acquireReleaseWith( - ZIO.attempt( - Source.fromFile(Paths.get(filePath, "." + topLevelCommand).toFile) - ) - )(source => ZIO.attempt(source.close()).orDie) { source => - ZIO.attempt(source.getLines().toList.filter(_.trim.nonEmpty)) - } - } - .map(_.flatten) - .refineToOrDie[IOException] - } yield lines - def run(args: List[String]): ZIO[R, CliError[E], Option[A]] = { def executeBuiltIn(builtInOption: BuiltInOption): ZIO[R, CliError[E], Option[A]] = builtInOption match { @@ -196,9 +193,9 @@ object CliApp { // Reading args from config files and combining with provided args val combinedArgs: ZIO[R, CliError[E], List[String]] = - loadOptionsFromFile(self.command.names.head).flatMap { configArgs => - ZIO.succeed(configArgs ++ args) - }.mapError(e => CliError.IO(e)) // Convert any IO errors into CliError.IO + loadOptionsFromConfigFiles(self.command.names.head).flatMap { configArgs => + ZIO.succeed(mergeOptionsBasedOnPriority(configArgs ++ args)) + }.mapError(e => CliError.IO(e)) combinedArgs.flatMap { allArgs => self.command From dea5f4657d7e7a9dba29e8947b1957d7ca768577 Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Sat, 20 Apr 2024 00:08:49 +0530 Subject: [PATCH 08/16] remove the duplicates --- zio-cli/shared/src/main/scala/zio/cli/CliApp.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala index 8f874776..73b4e990 100644 --- a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala +++ b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala @@ -72,6 +72,8 @@ object CliApp { ZIO .foreach(pathsToCheck)(path => ZIO.succeed(Files.exists(Path.of(path, filename)))) .map(_.zip(pathsToCheck).collect { case (exists, path) if exists => path }) + .map(_.distinct) // Remove them duplicates + } // Merges a list of options, removing any duplicate keys. From 07d182371ecb7a6830fb55816b03078cc5f9286a Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Sat, 20 Apr 2024 00:15:20 +0530 Subject: [PATCH 09/16] bug-fix: if homedir==workdir so removing dups --- zio-cli/shared/src/main/scala/zio/cli/CliApp.scala | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala index 73b4e990..9bb949c6 100644 --- a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala +++ b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala @@ -69,11 +69,10 @@ object CliApp { val pathsToCheck = homeDirOpt :: paths // Use ZIO to filter the paths - ZIO - .foreach(pathsToCheck)(path => ZIO.succeed(Files.exists(Path.of(path, filename)))) - .map(_.zip(pathsToCheck).collect { case (exists, path) if exists => path }) - .map(_.distinct) // Remove them duplicates - + for { + do_path_exist <- ZIO.foreach(pathsToCheck)(path => ZIO.succeed(Files.exists(Path.of(path, filename)))) + existing_paths = do_path_exist.zip(pathsToCheck).collect { case (exists, path) if exists => path } + } yield existing_paths.distinct // Use distinct to remove duplicates at the end } // Merges a list of options, removing any duplicate keys. From 952fde7741794a3444e771db55d8813aad778fb0 Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Sun, 21 Apr 2024 22:21:43 +0530 Subject: [PATCH 10/16] removing support of configfile arg in JS --- .../cli/ConfigFileArgsPlatformSpecific.scala | 16 ++++ .../cli/ConfigFileArgsPlatformSpecific.scala | 64 ++++++++++++++++ .../test/scala/zio/cli/FileBasedArgs.scala | 36 +++++---- .../cli/ConfigFileArgsPlatformSpecific.scala | 64 ++++++++++++++++ .../src/main/scala/zio/cli/CliApp.scala | 73 ++----------------- .../main/scala/zio/cli/ConfigFileArgs.scala | 10 +++ 6 files changed, 181 insertions(+), 82 deletions(-) create mode 100644 zio-cli/js/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala create mode 100644 zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala create mode 100644 zio-cli/native/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala create mode 100644 zio-cli/shared/src/main/scala/zio/cli/ConfigFileArgs.scala diff --git a/zio-cli/js/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala b/zio-cli/js/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala new file mode 100644 index 00000000..17779701 --- /dev/null +++ b/zio-cli/js/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala @@ -0,0 +1,16 @@ +package zio.cli + +import java.io.IOException +import zio._ + +object ConfigFileArgsPlatformSpecific extends ConfigFilePlatformSpecific { + def findPathsOfCliConfigFiles(topLevelCommand: String): Task[List[String]] = + ZIO.succeed(Nil) // Always return empty for JS + + def mergeOptionsBasedOnPriority(options: List[String]): List[String] = + Nil // Always return empty for JS + + def loadOptionsFromConfigFiles(topLevelCommand: String): ZIO[Any, IOException, List[String]] = + ZIO.succeed(Nil) // Always return empty for JS +} + diff --git a/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala b/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala new file mode 100644 index 00000000..0f892fb1 --- /dev/null +++ b/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala @@ -0,0 +1,64 @@ +package zio.cli + +import java.io.IOException +import zio._ +import java.nio.file.{Files, Path, Paths} +import scala.io.Source + +object ConfigFileArgsPlatformSpecific extends ConfigFilePlatformSpecific { + def findPathsOfCliConfigFiles(topLevelCommand: String): Task[List[String]] = { + val filename = s".$topLevelCommand" + val cwd = java.lang.System.getProperty("user.dir") + val homeDirOpt = java.lang.System.getProperty("user.home") + + def parentPaths(path: String): List[String] = { + val parts = path.split(java.io.File.separatorChar).filterNot(_.isEmpty) + (0 to parts.length) + .map(i => s"${java.io.File.separatorChar}${parts.take(i).mkString(java.io.File.separator)}") + .toList + } + + val paths = parentPaths(cwd) + val pathsToCheck = homeDirOpt :: homeDirOpt :: paths + + // Use ZIO to filter the paths + for { + do_path_exist <- ZIO.foreach(pathsToCheck)(path => ZIO.succeed(Files.exists(Path.of(path, filename)))) + existing_paths = do_path_exist.zip(pathsToCheck).collect { case (exists, path) if exists => path } + } yield existing_paths.distinct // Use distinct to remove duplicates at the end + } + + def mergeOptionsBasedOnPriority(options: List[String]): List[String] = { + val mergedOptions = options.flatMap { opt => + opt.split('=') match { + case Array(key) => Some(key -> None) + case Array(key, value) => Some(key -> value) + case _ => + None // handles the case when there isn't exactly one '=' in the string + } + }.toMap.toList.map { + case (key, None) => key + case (key, value) => s"$key=$value" + } + + mergedOptions + } + + def loadOptionsFromConfigFiles(topLevelCommand: String): ZIO[Any, IOException, List[String]] = + for { + filePaths <- findPathsOfCliConfigFiles(topLevelCommand) + .refineToOrDie[IOException] + lines <- ZIO + .foreach(filePaths) { filePath => + ZIO.acquireReleaseWith( + ZIO.attempt( + Source.fromFile(Paths.get(filePath, "." + topLevelCommand).toFile) + ) + )(source => ZIO.attempt(source.close()).orDie) { source => + ZIO.attempt(source.getLines().toList.filter(_.trim.nonEmpty)) + } + } + .map(_.flatten) + .refineToOrDie[IOException] + } yield lines +} diff --git a/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala b/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala index d72415ff..b569e315 100644 --- a/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala +++ b/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala @@ -2,28 +2,33 @@ import zio._ import zio.test._ import zio.test.Assertion._ import java.nio.file.{Files, Paths, Path} -import zio.cli.CliApp import java.io.IOException +import zio.cli.ConfigFileArgsPlatformSpecific +import zio.cli.ConfigFilePlatformSpecific object FileBasedArgs extends ZIOSpecDefault { - def spec = suite("Vivasvan")( + + val configFileOps: ConfigFilePlatformSpecific = ConfigFileArgsPlatformSpecific + + def spec = suite("FileBasedArgs")( test("should load options from files and merge them appropriatly") { for { // Create Sample config files cwd <- ZIO.succeed(Paths.get(java.lang.System.getProperty("user.dir"))) homeDir <- ZIO.succeed(Paths.get(java.lang.System.getProperty("user.home"))) - _ <- createSampleConfigFiles(cwd, homeDir) + command <- ZIO.succeed("testApp") + _ <- createSampleConfigFiles(cwd, homeDir, command) // Check if the func checkAndGetOptionsFilePaths can - config_args <- CliApp.loadOptionsFromConfigFiles("testApp") + config_args <- configFileOps.loadOptionsFromConfigFiles(command) - _ <- cleanUpSampleConfigFiles(cwd: Path, homeDir: Path) + _ <- cleanUpSampleConfigFiles(cwd: Path, homeDir: Path, command) } yield assert(config_args)(hasSameElements(List("home=true", "dir=true", "home=false"))) }, test("should merge duplicate options") { val options = List("option1=value1", "option2=value2", "option1=value3"); - val mergedOptions = CliApp.mergeOptionsBasedOnPriority(options); + val mergedOptions = configFileOps.mergeOptionsBasedOnPriority(options); assert(mergedOptions)(equalTo(List("option1=value3", "option2=value2"))); }, test("should return directory ~/home and ./ which have .testApp config file for loading the args") { @@ -31,29 +36,30 @@ object FileBasedArgs extends ZIOSpecDefault { // Create Sample config files cwd <- ZIO.succeed(Paths.get(java.lang.System.getProperty("user.dir"))) homeDir <- ZIO.succeed(Paths.get(java.lang.System.getProperty("user.home"))) - _ <- createSampleConfigFiles(cwd, homeDir) + command <- ZIO.succeed("testApp1") + _ <- createSampleConfigFiles(cwd, homeDir, command) // Check if the func checkAndGetOptionsFilePaths can - paths <- CliApp.findPathsOfCliConfigFiles("testApp") + paths <- configFileOps.findPathsOfCliConfigFiles(command) - _ <- cleanUpSampleConfigFiles(cwd: Path, homeDir: Path) + _ <- cleanUpSampleConfigFiles(cwd: Path, homeDir: Path, command) } yield assert(paths)(hasSameElements(List(homeDir.toString(), cwd.toString()))) } ) - def createSampleConfigFiles(cwd: Path, homeDir: Path): IO[IOException, Unit] = + def createSampleConfigFiles(cwd: Path, homeDir: Path, command: String = "testApp"): IO[IOException, Unit] = ZIO.attempt { - Files.write(Paths.get(homeDir.toString(), ".testApp"), java.util.Arrays.asList("home=true")); - Files.write(Paths.get(cwd.toString(), ".testApp"), java.util.Arrays.asList("dir=true\nhome=false")); + Files.write(Paths.get(homeDir.toString(), s".$command"), java.util.Arrays.asList("home=true")); + Files.write(Paths.get(cwd.toString(), s".$command"), java.util.Arrays.asList("dir=true\nhome=false")); () }.refineToOrDie[IOException] - def cleanUpSampleConfigFiles(cwd: Path, homeDir: Path): IO[IOException, Unit] = + def cleanUpSampleConfigFiles(cwd: Path, homeDir: Path, command: String = "testApp"): IO[IOException, Unit] = ZIO.attempt { - Files.delete(Paths.get(homeDir.toString(), ".testApp")); - Files.delete(Paths.get(cwd.toString(), ".testApp")); + Files.delete(Paths.get(homeDir.toString(), s".$command")); + Files.delete(Paths.get(cwd.toString(), s".$command")); () }.refineToOrDie[IOException] diff --git a/zio-cli/native/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala b/zio-cli/native/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala new file mode 100644 index 00000000..0f892fb1 --- /dev/null +++ b/zio-cli/native/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala @@ -0,0 +1,64 @@ +package zio.cli + +import java.io.IOException +import zio._ +import java.nio.file.{Files, Path, Paths} +import scala.io.Source + +object ConfigFileArgsPlatformSpecific extends ConfigFilePlatformSpecific { + def findPathsOfCliConfigFiles(topLevelCommand: String): Task[List[String]] = { + val filename = s".$topLevelCommand" + val cwd = java.lang.System.getProperty("user.dir") + val homeDirOpt = java.lang.System.getProperty("user.home") + + def parentPaths(path: String): List[String] = { + val parts = path.split(java.io.File.separatorChar).filterNot(_.isEmpty) + (0 to parts.length) + .map(i => s"${java.io.File.separatorChar}${parts.take(i).mkString(java.io.File.separator)}") + .toList + } + + val paths = parentPaths(cwd) + val pathsToCheck = homeDirOpt :: homeDirOpt :: paths + + // Use ZIO to filter the paths + for { + do_path_exist <- ZIO.foreach(pathsToCheck)(path => ZIO.succeed(Files.exists(Path.of(path, filename)))) + existing_paths = do_path_exist.zip(pathsToCheck).collect { case (exists, path) if exists => path } + } yield existing_paths.distinct // Use distinct to remove duplicates at the end + } + + def mergeOptionsBasedOnPriority(options: List[String]): List[String] = { + val mergedOptions = options.flatMap { opt => + opt.split('=') match { + case Array(key) => Some(key -> None) + case Array(key, value) => Some(key -> value) + case _ => + None // handles the case when there isn't exactly one '=' in the string + } + }.toMap.toList.map { + case (key, None) => key + case (key, value) => s"$key=$value" + } + + mergedOptions + } + + def loadOptionsFromConfigFiles(topLevelCommand: String): ZIO[Any, IOException, List[String]] = + for { + filePaths <- findPathsOfCliConfigFiles(topLevelCommand) + .refineToOrDie[IOException] + lines <- ZIO + .foreach(filePaths) { filePath => + ZIO.acquireReleaseWith( + ZIO.attempt( + Source.fromFile(Paths.get(filePath, "." + topLevelCommand).toFile) + ) + )(source => ZIO.attempt(source.close()).orDie) { source => + ZIO.attempt(source.getLines().toList.filter(_.trim.nonEmpty)) + } + } + .map(_.flatten) + .refineToOrDie[IOException] + } yield lines +} diff --git a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala index 9bb949c6..d9d21bce 100644 --- a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala +++ b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala @@ -10,11 +10,6 @@ import zio.cli.completion.{Completion, CompletionScript} import zio.cli.figlet.FigFont import scala.annotation.tailrec -import java.io.IOException -import java.nio.file.Files -import java.nio.file.Path -import scala.io.Source -import java.nio.file.Paths /** * A `CliApp[R, E]` is a complete description of a command-line application, which requires environment `R`, and may @@ -53,65 +48,6 @@ object CliApp { )(execute: Model => ZIO[R, E, A]): CliApp[R, E, A] = CliAppImpl(name, version, summary, command, execute, footer, config, figFont) - def findPathsOfCliConfigFiles(topLevelCommand: String): Task[List[String]] = { - val filename = s".$topLevelCommand" - val cwd = java.lang.System.getProperty("user.dir") - val homeDirOpt = java.lang.System.getProperty("user.home") - - def parentPaths(path: String): List[String] = { - val parts = path.split(java.io.File.separatorChar).filterNot(_.isEmpty) - (0 to parts.length) - .map(i => s"${java.io.File.separatorChar}${parts.take(i).mkString(java.io.File.separator)}") - .toList - } - - val paths = parentPaths(cwd) - val pathsToCheck = homeDirOpt :: paths - - // Use ZIO to filter the paths - for { - do_path_exist <- ZIO.foreach(pathsToCheck)(path => ZIO.succeed(Files.exists(Path.of(path, filename)))) - existing_paths = do_path_exist.zip(pathsToCheck).collect { case (exists, path) if exists => path } - } yield existing_paths.distinct // Use distinct to remove duplicates at the end - } - - // Merges a list of options, removing any duplicate keys. - // If there are options with the same keys but different values, it will use the value from the last option in the - // list. - def mergeOptionsBasedOnPriority(options: List[String]): List[String] = { - val mergedOptions = options.flatMap { opt => - opt.split('=') match { - case Array(key) => Some(key -> None) - case Array(key, value) => Some(key -> value) - case _ => - None // handles the case when there isn't exactly one '=' in the string - } - }.toMap.toList.map { - case (key, None) => key - case (key, value) => s"$key=$value" - } - - mergedOptions - } - - def loadOptionsFromConfigFiles(topLevelCommand: String): ZIO[Any, IOException, List[String]] = - for { - filePaths <- findPathsOfCliConfigFiles(topLevelCommand) - .refineToOrDie[IOException] - lines <- ZIO - .foreach(filePaths) { filePath => - ZIO.acquireReleaseWith( - ZIO.attempt( - Source.fromFile(Paths.get(filePath, "." + topLevelCommand).toFile) - ) - )(source => ZIO.attempt(source.close()).orDie) { source => - ZIO.attempt(source.getLines().toList.filter(_.trim.nonEmpty)) - } - } - .map(_.flatten) - .refineToOrDie[IOException] - } yield lines - private[cli] case class CliAppImpl[-R, +E, Model, +A]( name: String, version: String, @@ -194,9 +130,12 @@ object CliApp { // Reading args from config files and combining with provided args val combinedArgs: ZIO[R, CliError[E], List[String]] = - loadOptionsFromConfigFiles(self.command.names.head).flatMap { configArgs => - ZIO.succeed(mergeOptionsBasedOnPriority(configArgs ++ args)) - }.mapError(e => CliError.IO(e)) + ConfigFileArgsPlatformSpecific + .loadOptionsFromConfigFiles(self.command.names.head) + .flatMap { configArgs => + ZIO.succeed(ConfigFileArgsPlatformSpecific.mergeOptionsBasedOnPriority(configArgs ++ args)) + } + .mapError(e => CliError.IO(e)) combinedArgs.flatMap { allArgs => self.command diff --git a/zio-cli/shared/src/main/scala/zio/cli/ConfigFileArgs.scala b/zio-cli/shared/src/main/scala/zio/cli/ConfigFileArgs.scala new file mode 100644 index 00000000..54b705c2 --- /dev/null +++ b/zio-cli/shared/src/main/scala/zio/cli/ConfigFileArgs.scala @@ -0,0 +1,10 @@ +package zio.cli + +import zio._ +import java.io.IOException + +trait ConfigFilePlatformSpecific { + def findPathsOfCliConfigFiles(topLevelCommand: String): Task[List[String]] + def mergeOptionsBasedOnPriority(options: List[String]): List[String] + def loadOptionsFromConfigFiles(topLevelCommand: String): ZIO[Any, IOException, List[String]] +} From 83a8f6f69ad78a86534065e7c3a9d2902f9aca48 Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Mon, 22 Apr 2024 00:33:42 +0530 Subject: [PATCH 11/16] simplify: removing merging args feature --- .../zio/cli/ConfigFileArgsPlatformSpecific.scala | 4 ---- .../zio/cli/ConfigFileArgsPlatformSpecific.scala | 16 ---------------- .../zio/cli/ConfigFileArgsPlatformSpecific.scala | 16 ---------------- .../shared/src/main/scala/zio/cli/CliApp.scala | 2 +- .../src/main/scala/zio/cli/ConfigFileArgs.scala | 1 - 5 files changed, 1 insertion(+), 38 deletions(-) diff --git a/zio-cli/js/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala b/zio-cli/js/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala index 17779701..e4938007 100644 --- a/zio-cli/js/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala +++ b/zio-cli/js/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala @@ -7,10 +7,6 @@ object ConfigFileArgsPlatformSpecific extends ConfigFilePlatformSpecific { def findPathsOfCliConfigFiles(topLevelCommand: String): Task[List[String]] = ZIO.succeed(Nil) // Always return empty for JS - def mergeOptionsBasedOnPriority(options: List[String]): List[String] = - Nil // Always return empty for JS - def loadOptionsFromConfigFiles(topLevelCommand: String): ZIO[Any, IOException, List[String]] = ZIO.succeed(Nil) // Always return empty for JS } - diff --git a/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala b/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala index 0f892fb1..f8de8c3d 100644 --- a/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala +++ b/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala @@ -28,22 +28,6 @@ object ConfigFileArgsPlatformSpecific extends ConfigFilePlatformSpecific { } yield existing_paths.distinct // Use distinct to remove duplicates at the end } - def mergeOptionsBasedOnPriority(options: List[String]): List[String] = { - val mergedOptions = options.flatMap { opt => - opt.split('=') match { - case Array(key) => Some(key -> None) - case Array(key, value) => Some(key -> value) - case _ => - None // handles the case when there isn't exactly one '=' in the string - } - }.toMap.toList.map { - case (key, None) => key - case (key, value) => s"$key=$value" - } - - mergedOptions - } - def loadOptionsFromConfigFiles(topLevelCommand: String): ZIO[Any, IOException, List[String]] = for { filePaths <- findPathsOfCliConfigFiles(topLevelCommand) diff --git a/zio-cli/native/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala b/zio-cli/native/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala index 0f892fb1..f8de8c3d 100644 --- a/zio-cli/native/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala +++ b/zio-cli/native/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala @@ -28,22 +28,6 @@ object ConfigFileArgsPlatformSpecific extends ConfigFilePlatformSpecific { } yield existing_paths.distinct // Use distinct to remove duplicates at the end } - def mergeOptionsBasedOnPriority(options: List[String]): List[String] = { - val mergedOptions = options.flatMap { opt => - opt.split('=') match { - case Array(key) => Some(key -> None) - case Array(key, value) => Some(key -> value) - case _ => - None // handles the case when there isn't exactly one '=' in the string - } - }.toMap.toList.map { - case (key, None) => key - case (key, value) => s"$key=$value" - } - - mergedOptions - } - def loadOptionsFromConfigFiles(topLevelCommand: String): ZIO[Any, IOException, List[String]] = for { filePaths <- findPathsOfCliConfigFiles(topLevelCommand) diff --git a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala index d9d21bce..346c9f0f 100644 --- a/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala +++ b/zio-cli/shared/src/main/scala/zio/cli/CliApp.scala @@ -133,7 +133,7 @@ object CliApp { ConfigFileArgsPlatformSpecific .loadOptionsFromConfigFiles(self.command.names.head) .flatMap { configArgs => - ZIO.succeed(ConfigFileArgsPlatformSpecific.mergeOptionsBasedOnPriority(configArgs ++ args)) + ZIO.succeed(configArgs ++ args) } .mapError(e => CliError.IO(e)) diff --git a/zio-cli/shared/src/main/scala/zio/cli/ConfigFileArgs.scala b/zio-cli/shared/src/main/scala/zio/cli/ConfigFileArgs.scala index 54b705c2..5e5c2b6a 100644 --- a/zio-cli/shared/src/main/scala/zio/cli/ConfigFileArgs.scala +++ b/zio-cli/shared/src/main/scala/zio/cli/ConfigFileArgs.scala @@ -5,6 +5,5 @@ import java.io.IOException trait ConfigFilePlatformSpecific { def findPathsOfCliConfigFiles(topLevelCommand: String): Task[List[String]] - def mergeOptionsBasedOnPriority(options: List[String]): List[String] def loadOptionsFromConfigFiles(topLevelCommand: String): ZIO[Any, IOException, List[String]] } From d655dee363304c60268231714631f763bb83457c Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Mon, 22 Apr 2024 00:37:27 +0530 Subject: [PATCH 12/16] removing the merge test as its no longer needed --- zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala | 5 ----- 1 file changed, 5 deletions(-) diff --git a/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala b/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala index b569e315..301c2d25 100644 --- a/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala +++ b/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala @@ -26,11 +26,6 @@ object FileBasedArgs extends ZIOSpecDefault { } yield assert(config_args)(hasSameElements(List("home=true", "dir=true", "home=false"))) }, - test("should merge duplicate options") { - val options = List("option1=value1", "option2=value2", "option1=value3"); - val mergedOptions = configFileOps.mergeOptionsBasedOnPriority(options); - assert(mergedOptions)(equalTo(List("option1=value3", "option2=value2"))); - }, test("should return directory ~/home and ./ which have .testApp config file for loading the args") { for { // Create Sample config files From 7a6d2cc7435607a314cd4b10210e5f987d0a3466 Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Wed, 24 Apr 2024 14:00:55 +0530 Subject: [PATCH 13/16] camelCase done --- .../main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala | 6 +++--- zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala | 4 ++-- .../main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala b/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala index f8de8c3d..3304b95e 100644 --- a/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala +++ b/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala @@ -23,9 +23,9 @@ object ConfigFileArgsPlatformSpecific extends ConfigFilePlatformSpecific { // Use ZIO to filter the paths for { - do_path_exist <- ZIO.foreach(pathsToCheck)(path => ZIO.succeed(Files.exists(Path.of(path, filename)))) - existing_paths = do_path_exist.zip(pathsToCheck).collect { case (exists, path) if exists => path } - } yield existing_paths.distinct // Use distinct to remove duplicates at the end + doPathExist <- ZIO.foreach(pathsToCheck)(path => ZIO.succeed(Files.exists(Path.of(path, filename)))) + existingPaths = doPathExist.zip(pathsToCheck).collect { case (exists, path) if exists => path } + } yield existingPaths.distinct // Use distinct to remove duplicates at the end } def loadOptionsFromConfigFiles(topLevelCommand: String): ZIO[Any, IOException, List[String]] = diff --git a/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala b/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala index 301c2d25..073f9d51 100644 --- a/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala +++ b/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala @@ -20,11 +20,11 @@ object FileBasedArgs extends ZIOSpecDefault { _ <- createSampleConfigFiles(cwd, homeDir, command) // Check if the func checkAndGetOptionsFilePaths can - config_args <- configFileOps.loadOptionsFromConfigFiles(command) + configArgs <- configFileOps.loadOptionsFromConfigFiles(command) _ <- cleanUpSampleConfigFiles(cwd: Path, homeDir: Path, command) - } yield assert(config_args)(hasSameElements(List("home=true", "dir=true", "home=false"))) + } yield assert(configArgs)(hasSameElements(List("home=true", "dir=true", "home=false"))) }, test("should return directory ~/home and ./ which have .testApp config file for loading the args") { for { diff --git a/zio-cli/native/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala b/zio-cli/native/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala index f8de8c3d..03efdf09 100644 --- a/zio-cli/native/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala +++ b/zio-cli/native/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala @@ -23,9 +23,9 @@ object ConfigFileArgsPlatformSpecific extends ConfigFilePlatformSpecific { // Use ZIO to filter the paths for { - do_path_exist <- ZIO.foreach(pathsToCheck)(path => ZIO.succeed(Files.exists(Path.of(path, filename)))) - existing_paths = do_path_exist.zip(pathsToCheck).collect { case (exists, path) if exists => path } - } yield existing_paths.distinct // Use distinct to remove duplicates at the end + doPathExist <- ZIO.foreach(pathsToCheck)(path => ZIO.succeed(Files.exists(Path.of(path, filename)))) + existingPaths = doPathExist.zip(pathsToCheck).collect { case (exists, path) if exists => path } + } yield existingPaths.distinct // Use distinct to remove duplicates at the end } def loadOptionsFromConfigFiles(topLevelCommand: String): ZIO[Any, IOException, List[String]] = From 40d0dcf3947073bef4bd2683959733b12805a8c0 Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Wed, 24 Apr 2024 14:03:37 +0530 Subject: [PATCH 14/16] fmt fix and removing unneccesary deps --- build.sbt | 3 +-- .../main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index f2042f50..41d08b74 100644 --- a/build.sbt +++ b/build.sbt @@ -21,8 +21,7 @@ inThisBuild( pgpSecretRing := file("/tmp/secret.asc"), scmInfo := Some( ScmInfo(url("https://github.com/zio/zio-cli/"), "scm:git:git@github.com:zio/zio-cli.git") - ), - dependencyOverrides += "org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0" + ) ) ) diff --git a/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala b/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala index 3304b95e..03efdf09 100644 --- a/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala +++ b/zio-cli/jvm/src/main/scala/zio/cli/ConfigFileArgsPlatformSpecific.scala @@ -23,7 +23,7 @@ object ConfigFileArgsPlatformSpecific extends ConfigFilePlatformSpecific { // Use ZIO to filter the paths for { - doPathExist <- ZIO.foreach(pathsToCheck)(path => ZIO.succeed(Files.exists(Path.of(path, filename)))) + doPathExist <- ZIO.foreach(pathsToCheck)(path => ZIO.succeed(Files.exists(Path.of(path, filename)))) existingPaths = doPathExist.zip(pathsToCheck).collect { case (exists, path) if exists => path } } yield existingPaths.distinct // Use distinct to remove duplicates at the end } From 88489a6feb16e0c1fc2c79ecdd71870a14824112 Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Tue, 4 Jun 2024 13:16:54 +0530 Subject: [PATCH 15/16] full app test case --- .../test/scala/zio/cli/FileBasedArgs.scala | 144 +++++++++++++++++- 1 file changed, 140 insertions(+), 4 deletions(-) diff --git a/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala b/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala index 073f9d51..93c8e01f 100644 --- a/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala +++ b/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala @@ -5,12 +5,41 @@ import java.nio.file.{Files, Paths, Path} import java.io.IOException import zio.cli.ConfigFileArgsPlatformSpecific import zio.cli.ConfigFilePlatformSpecific +import zio.cli.Options +import zio.cli.Args +import zio.cli.Exists +import zio.cli.Command +import zio.Console.printLine +import zio.stream.ZSink +import zio.stream.ZPipeline +import zio.stream.ZStream +import zio.cli.CliApp +import zio.cli.HelpDoc.Span.text object FileBasedArgs extends ZIOSpecDefault { val configFileOps: ConfigFilePlatformSpecific = ConfigFileArgsPlatformSpecific def spec = suite("FileBasedArgs")( + test("") { + for { + // Create Sample config files + cwd <- ZIO.succeed(Paths.get(java.lang.System.getProperty("user.dir"))) + homeDir <- ZIO.succeed(Paths.get(java.lang.System.getProperty("user.home"))) + command <- ZIO.succeed("wc") + + _ <- createLineCountTestFile(cwd, "sample_file") + _ <- createSampleConfigFiles(cwd, homeDir, command, content_home = "-w", content_cwd = "-l") + + _ <- cliApp.run(args = List("sample_file")).either + output <- TestConsole.output + + correctOutput <- ZIO.succeed(" 3 4 20 sample_file\n") + + _ <- cleanupLineCountTestFile(cwd, "sample_file") + _ <- cleanUpSampleConfigFiles(cwd: Path, homeDir: Path, command) + } yield assert(output.last)(equalTo(correctOutput)) + }, test("should load options from files and merge them appropriatly") { for { // Create Sample config files @@ -43,11 +72,32 @@ object FileBasedArgs extends ZIOSpecDefault { } ) - def createSampleConfigFiles(cwd: Path, homeDir: Path, command: String = "testApp"): IO[IOException, Unit] = + def createLineCountTestFile( + cwd: Path, + file_name: String = "sample_file", + content: String = "asdf\nqweqwer\njdsafn" + ): IO[IOException, Unit] = ZIO.attempt { - Files.write(Paths.get(homeDir.toString(), s".$command"), java.util.Arrays.asList("home=true")); - Files.write(Paths.get(cwd.toString(), s".$command"), java.util.Arrays.asList("dir=true\nhome=false")); + Files.write(Paths.get(cwd.toString(), s"$file_name"), java.util.Arrays.asList(content)); + () + }.refineToOrDie[IOException] + + def cleanupLineCountTestFile(cwd: Path, file_name: String = "sample_file"): IO[IOException, Unit] = + ZIO.attempt { + Files.delete(Paths.get(cwd.toString(), s"$file_name")); + () + }.refineToOrDie[IOException] + def createSampleConfigFiles( + cwd: Path, + homeDir: Path, + command: String = "testApp", + content_home: String = "home=true", + content_cwd: String = "dir=true\nhome=false" + ): IO[IOException, Unit] = + ZIO.attempt { + Files.write(Paths.get(homeDir.toString(), s".$command"), java.util.Arrays.asList(content_home)); + Files.write(Paths.get(cwd.toString(), s".$command"), java.util.Arrays.asList(content_cwd)); () }.refineToOrDie[IOException] @@ -55,7 +105,93 @@ object FileBasedArgs extends ZIOSpecDefault { ZIO.attempt { Files.delete(Paths.get(homeDir.toString(), s".$command")); Files.delete(Paths.get(cwd.toString(), s".$command")); - () }.refineToOrDie[IOException] + + val bytesFlag: Options[Boolean] = Options.boolean("c") + val linesFlag: Options[Boolean] = Options.boolean("l") + val wordsFlag: Options[Boolean] = Options.boolean("w") + val charFlag: Options[Boolean] = Options.boolean("m", false) + + case class WcOptions(bytes: Boolean, lines: Boolean, words: Boolean, char: Boolean) + case class WcResult( + fileName: String, + countBytes: Option[Long], + countLines: Option[Long], + countWords: Option[Long], + countChar: Option[Long] + ) + + val options = (bytesFlag ++ linesFlag ++ wordsFlag ++ charFlag).as(WcOptions.apply _) + + val args = Args.file("files", Exists.Yes).repeat1 + + val wc: Command[(WcOptions, ::[Path])] = Command("wc", options, args) + + val execute: (WcOptions, ::[Path]) => UIO[Unit] = { + def printResult(res: List[WcResult]): UIO[Unit] = { + def wcTotal(results: List[WcResult]) = { + def optSum(acc: WcResult, elem: WcResult, extract: WcResult => Option[Long]): Option[Long] = + extract(acc).flatMap(a => extract(elem).map(_ + a)) + + results.fold(WcResult("total", Some(0), Some(0), Some(0), Some(0))) { (acc, wcRes) => + acc.copy( + countBytes = optSum(acc, wcRes, _.countBytes), + countChar = optSum(acc, wcRes, _.countChar), + countWords = optSum(acc, wcRes, _.countWords), + countLines = optSum(acc, wcRes, _.countLines) + ) + } + } + + def format(res: WcResult) = { + def opt(option: Option[Long]): String = option.map(l => f"$l%9d").getOrElse("") + + s"${opt(res.countLines)} ${opt(res.countWords)} ${opt(res.countChar)} ${opt(res.countBytes)} ${res.fileName}" + } + + ZIO.foreachDiscard(res)(r => printLine(format(r)).!) *> ZIO + .when(res.length > 1)(printLine(format(wcTotal(res))).!) + .ignore + } + + (opts, paths) => { + zio.Console.printLine(s"executing wc with args: $opts $paths").! *> + ZIO + .foreachPar[Any, Throwable, Path, WcResult, List](paths)({ path => + def option(bool: Boolean, sink: ZSink[Any, Throwable, Byte, Byte, Long]) + : ZSink[Any, Throwable, Byte, Byte, Option[Long]] = + if (bool) sink.map(Some(_)) else ZSink.succeed(None) + + val byteCount = option(opts.bytes, ZSink.count) + val lineCount = option(opts.lines, ZPipeline.utfDecode >>> ZPipeline.splitLines >>> ZSink.count) + val wordCount = + option( + opts.words, + ZPipeline.utfDecode >>> ZPipeline.mapChunks((_: Chunk[String]).flatMap(_.split("\\s+"))) >>> ZSink.count + ) + val charCount = + option(opts.char, ZPipeline.utfDecode >>> ZSink.foldLeft[String, Long](0L)((s, e) => s + e.length)) + + val zippedSinks + : ZSink[Any, Throwable, Byte, Byte, (Option[Long], Option[Long], Option[Long], Option[Long])] = + (byteCount <&> lineCount <&> wordCount <&> charCount) + + ZStream + .fromFile(path.toFile) + .run(zippedSinks) + .map(t => WcResult(path.getFileName.toString, t._1, t._2, t._3, t._4)) + }) + .withParallelism(4) + .orDie + .flatMap(res => printResult(res)) + } + } + + val cliApp = CliApp.make( + "ZIO Word Count", + "0.1.2", + text("counts words in the file"), + wc + )(execute.tupled) } From ba5aa9d4e1be525273452a5d1cc517e7ee0f59b3 Mon Sep 17 00:00:00 2001 From: Vivasvan Patel Date: Tue, 4 Jun 2024 13:29:51 +0530 Subject: [PATCH 16/16] fmt fix + type fix --- zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala b/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala index 93c8e01f..4b55ee83 100644 --- a/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala +++ b/zio-cli/jvm/src/test/scala/zio/cli/FileBasedArgs.scala @@ -21,7 +21,7 @@ object FileBasedArgs extends ZIOSpecDefault { val configFileOps: ConfigFilePlatformSpecific = ConfigFileArgsPlatformSpecific def spec = suite("FileBasedArgs")( - test("") { + test("Full CLI App output test") { for { // Create Sample config files cwd <- ZIO.succeed(Paths.get(java.lang.System.getProperty("user.dir"))) @@ -81,7 +81,7 @@ object FileBasedArgs extends ZIOSpecDefault { Files.write(Paths.get(cwd.toString(), s"$file_name"), java.util.Arrays.asList(content)); () }.refineToOrDie[IOException] - + def cleanupLineCountTestFile(cwd: Path, file_name: String = "sample_file"): IO[IOException, Unit] = ZIO.attempt { Files.delete(Paths.get(cwd.toString(), s"$file_name")); @@ -188,7 +188,7 @@ object FileBasedArgs extends ZIOSpecDefault { } } - val cliApp = CliApp.make( + val cliApp = CliApp.make[Any, Throwable, (WcOptions, ::[Path]), Unit]( "ZIO Word Count", "0.1.2", text("counts words in the file"),