{"id":26798186,"url":"https://github.com/krzys9876/command-line-reader","last_synced_at":"2025-03-29T19:17:21.878Z","repository":{"id":40586406,"uuid":"507398716","full_name":"krzys9876/command-line-reader","owner":"krzys9876","description":"Command-line arguments reader to a class with respective fields","archived":false,"fork":false,"pushed_at":"2023-03-06T10:18:59.000Z","size":61,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2023-03-10T09:36:45.296Z","etag":null,"topics":["command-line","functional-programming","implicits","scala"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/krzys9876.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-06-25T19:21:19.000Z","updated_at":"2023-03-04T18:04:49.000Z","dependencies_parsed_at":"2022-08-27T20:30:53.520Z","dependency_job_id":null,"html_url":"https://github.com/krzys9876/command-line-reader","commit_stats":null,"previous_names":[],"tags_count":null,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krzys9876%2Fcommand-line-reader","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krzys9876%2Fcommand-line-reader/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krzys9876%2Fcommand-line-reader/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krzys9876%2Fcommand-line-reader/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/krzys9876","download_url":"https://codeload.github.com/krzys9876/command-line-reader/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246230504,"owners_count":20744349,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["command-line","functional-programming","implicits","scala"],"created_at":"2025-03-29T19:17:21.352Z","updated_at":"2025-03-29T19:17:21.870Z","avatar_url":"https://github.com/krzys9876.png","language":"Scala","readme":"# Scala command-line reader\n## Convert arguments to fields within a simple class\n\nThis short project is an exercise of scala implicit type conversions with a bit of java reflection.\n\nIf you're using your code in a way that the key configurations are passed as program arguments \n(this is what I do with my spark applications) you've probably already came across libraries that \nhide all the complexity of this (not that simple) task.\n\nI usually try to understand the mechanics of the task before I decide if I should use an external library \nor write some code on my own. This is a result of a study of scala implicits (which I find extremely useful) \nand java reflection, which I already knew from other projects.\n\nThe idea is to allow a developer to create the simplest possible class which contains fields corresponding to every argument. \nThese fields should be easily definable (e.g. required/optional) and accessible (without too much boilerplate). \nBoth scala files are close to 150 lines, which I guess qualifies them as _simple_. There would be even\nless without parsing additional types or printing instructions \n\n### Example ###\n\nConsider an example argument list:\n\n    --input-file=\"/tmp/some_folder/input.bin\" --algorithm=gz --iterations=10 --output-file=\"/tmp/some_other_folder/output.bin\"\n  \nYou may define a class like this:\n\n    class SampleArgs(args:Array[String]) extends ArgsAsClass(args) {\n      val inputFile:Argument[String]=Argument.required\n      val algorithm:Argument[String]=Argument.required\n      val iterations:Argument[Int]=Argument.optional(5)\n      val outputFile:Argument[String]=Argument.required\n      val verbose:Argument[Boolean]=Argument.optional(false)\n\n      parse()\n    }\n\nInvocation of the method \u003ccode\u003eparse()\u003c/code\u003e at the end of the class body is actually the only boilerplate \n(apart from type declaration). It fills all the required values just after instantiation of the class. See side note below.\n\nInstantiate the class at the very top of your main class:\n\n    object Main extends App {\n      val arguments=new SampleArgs(args)\n      ...\n    }\n\n_NOTE:_ this will throw \u003ccode\u003eMissingArgumentException\u003c/code\u003e if a required argument is missing!\n\n_NOTE2:_ if a user provides \u003ccode\u003e--help\u003c/code\u003e as arguments (this is a reserved name!), this will throw \u003ccode\u003ePrintHelpAndExit\u003c/code\u003e exception \nwith message containing usage instructions. You might leave it as it is, but if you prefer not to print \na standard stack trace you should catch it and print instructions in a more friendly manner. Yes, this could\nbe done by packing \u003ccode\u003eparse\u003c/code\u003e method result in e.g. \u003ccode\u003eEither\u003c/code\u003e, so feel free to modify it \nif you find exceptions less elegant.\n\nNow you can access values by:\n\n    // assignment to a typed val - scala will do implicit type conversion for you:\n    val inFile:String=arguments.inputFile\n\n    // assignment to a val using explicit type conversion:\n    val numOfIterations=arguments.iterations.value\n    // or using apply():\n    val numOfIterations=arguments.iterations()\n\n\n    // assignent to a optional val with implicit type conversion\n    val outFile:Option[String]=arguments.outputFile\n    // or explicitly\n    val outFile:String=arguments.outputFile.optValue.getOrElse(\"/tmp/any_folder/out.bin\")\n    \nThis is safe (i.e. no error will be thrown) but you have to deal with options yourself.\n\nIt was a bit complicated to accept Boolean parameters without values. This will not work if you mix\nboolean parameters with positional ones, particularly when a boolean parameter is provided without value \nand right before named ones. This will confuse parser and cause incorrect behaviour.\n\nI tend not to mix parameter types (named/positional) if possible. \n\n### Side note 1 on immutability ###\n\nYou may have noticed that I use some private _vars_ to keep a name, position and actual argument.\nThe problem is this: I need to set contents of a field on the basis of its actual name. In order\nto use reflection the field must be instantiated, i.e. I cannot set _vals_ afterwards. \n\nNote that all this happens during, or rather just after, instantiation of the class. It means that\nthere is no access to _vars_ at runtime since they are set only once. This makes them a bit like _lazy vals_.\n\n### Side note 2 on testability ###\n\nAs you use runtime arguments, you may also want to be using similar construct for testing.\nYou may create separate test class with arguments defined not as parser for array of strings\nbut even more verbose.\n\nSay you define a trait with application-wide arguments:\n\n    trait SampleArgsBase {\n      val inputFile:Argument[String]\n      val algorithm:Argument[String]\n      val iterations:Argument[Int]\n      val outputFile:Argument[String]\n      val verbose:Argument[Boolean]\n    }\n\nNote: there's no \u003ccode\u003eparse\u003c/code\u003e method.\n\nIn your code you pass around a trait not the concrete type. You may make it implicit \nas well as other dependencies that you have to inject. \n\nAt runtime, you may use the above class:\n\n    class SampleArgsRuntime(args:Array[String]) extends ArgsAsClass(args) with SampleArgsBase {\n        //same body as above\n\n        parse()\n    }\n\nNote: You have to use \u003ccode\u003eparse()\u003c/code\u003e here.\n\nFor testing, you may define a separate class in a different way:\n\n    class SampleArgsTest extends SampleArgsBase {\n      val inputFile:Argument[String]=Argument.static(\"/test/files/in.bin\")\n      val algorithm:Argument[String]=Argument.static(\"fast\")\n      val iterations:Argument[Int]=Argument.static(2)\n      val outputFile:Argument[String]=Argument.static(\"/test/files/in.bin\")\n      val verbose:Argument[Boolean]=Argument.ignored\n    }\n\nThis is just a different convention. By using \u003ccode\u003estatic\u003c/code\u003e you explicitly set configuration values\nand more importantly by using \u003ccode\u003eignored\u003c/code\u003e you show that this parameter is not \nused during testing. Since there is nothing to be parsed, you don't need the \u003ccode\u003eparse\u003c/code\u003e method.\n\nI find this more verbose.\n\n### Side note 3 on adding new types ###\n\nDepending on personal preferences you may find useful arguments of very specific types, e.g. Double from scientific notation. \nTo add new type all you have to do is:\n1. Define a new parsing method in \u003ccode\u003eRawArgument\u003c/code\u003e class, e.g. \u003ccode\u003easYourType\u003c/code\u003e. It should convert the textual \u003ccode\u003evalue\u003c/code\u003e into option of the type you're adding. \n2. Add another implicit object to \u003ccode\u003eRawArgumentConverter\u003c/code\u003e, which overrides \u003ccode\u003etoValue\u003c/code\u003e method. It should invoke _asYourType_ method (you need _flatMap_ here since you must map option to option). \n\nThis would look like:\n\n    case class RawArgument(key:Either[String,Int], value:String) {\n        ...\n      def asDouble:Option[Double]=\n        Double.toDoubleOption\n    }\n\n    object RawArgumentConverter {\n        ...\n      implicit object RawArgumentToDouble extends RawArgumentConverter[Double] {\n        override def toValue(rawArgument: Option[RawArgument]): Option[Double] = rawArgument.flatMap(_.asDouble)\n      }\n    }\n\nYou could argue that value conversion and exposing implicit object could \nbe combined \u003ccode\u003eRawArgumentConverter\u003c/code\u003e. Still I prefer to separate these two responsibilities, even \nif it generates some more boilerplate.\n\nNow you can define an argument as double:\n\n    class SampleArgs2(args:Array[String]) extends ArgsAsClass(args) {\n      val doubleValue:Argument[Double]=Argument.required\n      parse()\n    }\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkrzys9876%2Fcommand-line-reader","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkrzys9876%2Fcommand-line-reader","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkrzys9876%2Fcommand-line-reader/lists"}