davenverse / shellfish

Shell Scripting for Cats-Effect
MIT License
4 stars 4 forks source link

GSoC Rough plan #62

Open TonioGela opened 1 month ago

TonioGela commented 1 month ago

As discussed with Gabriel, Rishad, Micheal and Than, we will probably organise this work by roughly splitting it into three big chunks:

The idea is to begin importing what's present in the fs2-io's Files type class first, then in the Process one, and re-expose the stricter version of their methods.

I'll begin opening a specific issue to track the progress and ideas about Files, and I'll link it here.

TonioGela commented 1 month ago

Something that might end up in that interoperability ticket is mimicking this:

// Find and concatenate all .txt files directly in the working directory using `cat`
os.proc("cat", os.list(wd).filter(_.ext == "txt")).call(stdout = wd / "all.txt")

os.read(wd / "all.txt") ==>
  """I am cowI am cow
    |Hear me moo
    |I weigh twice as much as you
    |And I look good on the barbecue""".stripMargin

more or less in this way:

for {
 textFiles <- wd.listAll.filter(_.ext == "txt")
 stdout <- Process.run("cat", textFiles).stdout
 _ <- (wd / "all.txt").writeUtf8(stdout)
 output <- (wd / "all.txt").readUtf8
} yield output
TonioGela commented 1 month ago

Adding another (completely insane) suggestion here. That's what I currently use locally to auto-commit my dotfiles each time a file gets changed (the "watching" functionality is implemented calling this via a launchd macos daemon btw).

Ideally (IMHO), with this lib, it should be possible to rewrite it all in 10/15 lines top.

//> using scala 3.4.0
//> using platform native
//> using nativeGc none
//> using nativeMode release-full
//> using toolkit typelevel::latest
//> using dep org.typelevel::cats-time::0.5.1
//> using packaging.output dotfiles-watcher

import cats.effect.*
import cats.syntax.all.*
import org.typelevel.cats.time._
import fs2.io.file.*
import fs2.io.process.*

object Main extends IOApp:
    def run(args: List[String]): IO[ExitCode] =
      git(args.head)("status","--porcelain")
        .map(_.isEmpty).ifM(IO.unit, logic(args.head)).as(ExitCode.Success)
        .recoverWith {
            case ExitException(code, message) => reportToDesktop(code, message)
            case t: Throwable => reportToDesktop(1, t.getMessage)
        }

def logic(folder: String): IO[Unit] = for {
    prevProfile <- ghProfile("show").map(_.trim)
    _ <- ghProfile("switch", "personal")
    _ <- git(folder)("add", ".")
    date <- IO.realTimeInstant.map(_.show)
    _ <- git(folder)("-c", "commit.gpgsign=false", "commit", "-m", date)
    _ <- git(folder)("push")
    _ <- ghProfile("switch", prevProfile)
  } yield ()

def ghProfile(command: String*) = 
    run("/opt/homebrew/bin/gh", ("profile" +: command)*)

def git(folder: String)(command: String*) = 
    run("/usr/bin/git", ("-C" +: folder +: command)*)

def reportToDesktop(exitCode:Int, message: String): IO[ExitCode] =
    fs2.Stream.emit(message).through(
      Files[IO].writeUtf8(Path("/Users/toniogela/Desktop/dotfiles_watcher_log"))
    ).compile.drain.as(ExitCode(exitCode))

def run(command: String, args:String*): IO[String] =
    ProcessBuilder(command, args.toList).spawn[IO].use(p =>
        (
          p.exitValue,
          p.stdout.through(fs2.text.utf8.decode).compile.string,
          p.stderr.through(fs2.text.utf8.decode).compile.string
        ).parFlatMapN {
          case (0, stdout, stdErr) => IO(stdout)
          case (exitCode, stdout, stdErr) =>
            val errorMessage: String = List(
              Option(stdout).filter(_.nonEmpty).map(s => s"[STDOUT]: $s"),
              Option(stdErr).filter(_.nonEmpty).map(s => s"[STDERR]: $s")
            ).foldLeft(
              s"Non zero exit code ($exitCode) for `$command ${args.mkString(" ")}`"
            ) {
              case (summary, Some(err)) => s"$summary\n$err"
              case (summary, None)      => summary
            }

            IO.raiseError(new ExitException(exitCode, errorMessage))
        }
    )

case class ExitException(exitCode:Int, message:String) extends Throwable
Hombre-x commented 4 weeks ago

Should we implement the library abstract on F[_] on use plain IO? I think most of the people using tagless in their projects would benefit more with the former, also it is not much more difficult doing it that way as the majority of shellfish is already written like that 🤔

TonioGela commented 3 weeks ago

Should we implement the library abstract on F[_] on use plain IO? I think most of the people using tagless in their projects would benefit more with the former, also it is not much more difficult doing it that way as the majority of shellfish is already written like that 🤔

I just wanted to let you know I haven't answered you here because I did in person. This library aims to make the learning curve less steep and to make life easier for people who want to manipulate files and run external processes, w/o having to worry about tagless final or Stream. That's why we will write it using concrete IO and "strict" methods.