Reading, writing, and appending

Catscript comes with three different functions for reading and writing operations, read, write and append, each with four different variants: Standalone, Bytes, Lines and As, with these variants you will be able to work with the file and/or its contents as a UTF-8 string or with a custom java.nio.charset.Charset, as bytes, line by line, and with a custom codec respectively.

Reading

One of the most common operations in scripting is reading a file, that is why you can read a file in different ways:

read

This function loads the whole file as a string in memory using UTF-8 encoding.

import cats.syntax.all.*

import catscript.syntax.path.*

val path = Path("link/to/your/file.txt")

path.read >>= IO.println
import cats.syntax.all.* 

import catscript.Catscript

val path = Path("link/to/your/file.txt")

Catscript.read(path) >>= IO.println
import fs2.io.file.Files

val path = Path("link/to/your/file.txt")

Files[IO].readUtf8(path).evalMap(IO.println).compile.drain

If you need to handle non UTF-8 files, you can also use a custom java.nio.charset.Charset:

import java.nio.charset.StandardCharsets

path.read(StandardCharsets.UTF_16)
import java.nio.charset.StandardCharsets

Catscript.read(path, StandardCharsets.UTF_16)
import fs2.text.decodeWithCharset
import java.nio.charset.StandardCharsets

Files[IO].readAll(path)
  .through(decodeWithCharset(StandardCharsets.UTF_16))
  .compile
  .string

readBytes

Reads the file as a ByteVector, useful when working with binary data:

path.readBytes.map(_.rotateLeft(2))
Catscript.readBytes(path).map(_.rotateLeft(2))
import scodec.bits.ByteVector

Files[IO].readAll(path).compile.to(ByteVector).map(_.rotateLeft(2))

If you are not used to working with bytes and bits in general, rotateLeft(N) will shift bits N number of places to the left! For example, if we rotate to the left 11100101 three times, we will get 00101111.

readLines

Similar to read as it reads the file as a UTF-8 string, but stores each line of the file in a List:

for
  lines <- path.readLines
  _     <- lines.traverse_(IO.println)  
yield ()
for
  lines <- Catscript.readLines(path)
  _     <- IO(lines.foreach(println))
yield ()
Files[IO].readUtf8Lines(path).evalMap(IO.println).compile.drain

readAs

This method reads the contents of the file given a Codec[A]. Useful when you want to convert a binary file into a custom type A:

import scodec.Codec

case class Coordinates(x: Double, y: Double) derives Codec

path.readAs[Coordinates].map(coord => coord.x + coord.y)
import scodec.Codec

case class Coordinates(x: Double, y: Double) derives Codec

Catscript.readAs[Coordinates](path).map(coord => coord.x + coord.y)
import scodec.Codec
import fs2.interop.scodec.StreamDecoder

case class Coordinates(x: Double, y: Double) derives Codec

Files[IO].readAll(path)
  .through(StreamDecoder.many(summon[Codec[Coordinates]]).toPipeByte)
  .map(coord => coord.x + coord.y)
  .compile
  .drain

Writing

You can also overwrite the contents of a file using the write method and any of its variants. If the the file does not exist, it will be created automatically:

write

Writes to the desired file in the path. The contents of the file will be written as a UTF-8 string:

val path = Path("path/to/write.txt")

val poem = 
  """Music, thou queen of heaven, care-charming spell,
    |That strik'st a stillness into hell;
    |Thou that tam'st tigers, and fierce storms, that rise,
    |With thy soul-melting lullabies;
    |Fall down, down, down, from those thy chiming spheres
    |To charm our souls, as thou enchant'st our ears.
    |
    |— Robert Herrick""".stripMargin

path.write(poem)
val path = Path("path/to/write.txt")

val poem = 
  """Music, thou queen of heaven, care-charming spell,
    |That strik'st a stillness into hell;
    |Thou that tam'st tigers, and fierce storms, that rise,
    |With thy soul-melting lullabies;
    |Fall down, down, down, from those thy chiming spheres
    |To charm our souls, as thou enchant'st our ears.
    |
    |— Robert Herrick""".stripMargin

Catscript.write(path, poem)
val path = Path("path/to/write.txt")

val poem = 
  """Music, thou queen of heaven, care-charming spell,
    |That strik'st a stillness into hell;
    |Thou that tam'st tigers, and fierce storms, that rise,
    |With thy soul-melting lullabies;
    |Fall down, down, down, from those thy chiming spheres
    |To charm our souls, as thou enchant'st our ears.
    |
    |— Robert Herrick""".stripMargin

Stream.emit(poem)
  .through(Files[IO].writeUtf8(path))
  .compile
  .drain

The default charset can also be changed for encoding when writing strings:

import java.nio.charset.StandardCharsets

val message = "Java? No thanks, I'm allergic to coffee λ"

path.write(message, StandardCharsets.US_ASCII)
import java.nio.charset.StandardCharsets

val message = "Java? No thanks, I'm allergic to coffee λ"

Catscript.write(path, message, StandardCharsets.US_ASCII)
import fs2.text.encode
import java.nio.charset.StandardCharsets

val message = "Java? No thanks, I'm allergic to coffee λ"

Stream.emit(message)
  .through(encode(StandardCharsets.US_ASCII))
  .through(Files[IO].writeAll(path))
  .compile
  .drain

writeBytes

With this method you can write bytes directly to a binary file. The contents of the file require to be in form of a scodec.bits.ByteVector:

import scodec.bits.*
val niceBytes = hex"DeadC0de"

path.writeBytes(niceBytes)
import scodec.bits.*
val niceBytes = hex"DeadC0de"

Catscript.writeBytes(path, niceBytes)
import scodec.bits.*
val niceBytes = hex"DeadC0de"

Stream.chunk(fs2.Chunk.byteVector(niceBytes))
  .covary[IO]
  .through(Files[IO].writeAll(path))
  .compile
  .drain

writeLines

If you want to write many lines to a file, you can provide the contents as a collection of strings, and this function will write each element as a new line:

val todoList = List(
  "Buy Milk",
  "Finish the pending work",
  "Close the bank account",
  "Mow the yard",
  "Take the dog for a walk"
)

path.writeLines(todoList)
val todoList = List(
  "Buy Milk",
  "Finish the pending work",
  "Close the bank account",
  "Mow the yard",
  "Take the dog for a walk"
)

Catscript.writeLines(path, todoList)
val todoList = List(
  "Buy Milk",
  "Finish the pending work",
  "Close the bank account",
  "Mow the lawn",
  "Take the dog for a walk"
)

Stream.emits(todoList)
  .through(Files[IO].writeUtf8Lines(path))
  .compile
  .drain

writeAs

This method allows you to write a custom type A to a file, given a Codec[A] in scope:

import scodec.Codec

case class Rectangle(base: Float, height: Float) derives Codec

path.writeAs[Rectangle]( Rectangle(2.4, 6.0) )
import scodec.Codec

case class Rectangle(base: Float, height: Float) derives Codec

Catscript.writeAs[Rectangle](path, Rectangle(2.4, 6.0))
import scodec.Codec
import fs2.interop.scodec.StreamEncoder

case class Rectangle(base: Float, height: Float) derives Codec

Stream.emit( Rectangle(2.4, 6.0) )
  .covary[IO]
  .through(StreamEncoder.many(summon[Codec[Rectangle]]).toPipeByte)
  .through(Files[IO].writeAll(path))
  .compile
  .drain

Appending

Appending is very similar to writing, except that, instead of overwriting the contents of the file, it writes them to the very end of the existing file without deleting the previous contents.

append

Similar to write, but adds the content to the end of the file instead of overwriting it. The function will append the contents to the last line of the file if you want to append the contents as a new line, see appendLine.

val path = Path("path/to/append.txt")

val secretFormula = "And the dish's final secret ingredient is "

for 
  _ <- path.write(secretFormula)
  _ <- path.append("Rustacean legs! 🦀")
yield ()
val path = Path("path/to/append.txt")

val secretFormula = "And the dish's final secret ingredient is "

for 
  _ <- Catscript.write(path, secretFormula)
  _ <- Catscript.append(path, "Rustacean legs! 🦀")
yield ()
import fs2.io.file.Flags

val path = Path("path/to/append.txt")

val secretFormula = "And the dish's final secret ingredient is "

Stream.emit(secretFormula)
  .covary[IO]
  .through(Files[IO].writeUtf8(path))
  .map( _ => "Rustacean legs! 🦀")
  .through(Files[IO].writeUtf8(path, Flags.Append))
  .compile
  .drain

Like in the other variants, you can also use a custom charset to encode strings:

import java.nio.charset.StandardCharsets

path.append("Which encoding I'm I?", StandardCharsets.ISO_8859_1)
import java.nio.charset.StandardCharsets

Catscript.append(path, "Which encoding I'm I?", StandardCharsets.ISO_8859_1)
import fs2.text.encode

import java.nio.charset.StandardCharsets

Stream.emit("Which encoding I'm I?")
  .through(encode(StandardCharsets.ISO_8859_1))
  .through(Files[IO].writeAll(path))
  .compile
  .drain

appendLine

Very similar to append, with the difference that appends the contents as a new line (equivalent to prepending a \n to the contents):

for 
  _ <- path.write("I'm at the top!")
  _ <- path.appendLine("I'm at the bottom 😞")
yield ()
for 
  _ <- Catscript.write(path, "I'm at the top!")
  _ <- Catscript.appendLine(path, "I'm at the bottom 😞")
yield ()
import fs2.io.file.Flags

Stream.emit("I'm at the top!")
  .through(Files[IO].writeUtf8(path))
  .map(_ => "\nI'm at the bottom 😞")
  .through(Files[IO].writeUtf8(path, Flags.Append))
  .compile
  .drain

appendBytes

This function can be used to append bytes to the end of a binary file:

import scodec.bits.ByteVector

for 
  document  <- path.read
  signature <- IO(document.hashCode().toByte)
  _ <- path.appendBytes(ByteVector(signature))
yield ()
import scodec.bits.ByteVector

for 
  document  <- Catscript.read(path)
  signature <- IO(document.hashCode().toByte)
  _ <- Catscript.appendBytes(path, ByteVector(signature))
yield ()
import fs2.io.file.Flags

Files[IO].readUtf8(path)
  .evalMap(document => IO(document.hashCode().toByte))
  .through(Files[IO].writeAll(path, Flags.Append))
  .compile
  .drain

appendLines

You can also append multiple lines at the end of the file in the following way:

val missingIngredients = Vector(
  "40 ladyfingers.",
  "6 egg yolks",
  "3/4 cup granulated sugar.",
  "500 ml mascarpone, cold."
)

path.appendLines(missingIngredients)
val missingIngredients = Vector(
  "40 ladyfingers.",
  "6 egg yolks",
  "3/4 cup granulated sugar.",
  "500 ml mascarpone, cold."
)

Catscript.appendLines(path, missingIngredients)
import fs2.io.file.Flags

val missingIngredients = Vector(
  "40 ladyfingers.",
  "6 egg yolks",
  "3/4 cup granulated sugar.",
  "500 ml mascarpone, cold."
)

Stream.emits(missingIngredients)
 .through(Files[IO].writeUtf8Lines(path, Flags.Append))
 .compile
 .drain

appendAs

Finally, given a Codec[A] in the scope, this method will append a custom type A to the end of a file:

import scodec.Codec
import scodec.codecs.*

opaque type Ranking = (Int, Long)
given rankingCodec: Codec[Ranking] = uint8 :: int64

path.appendAs[Ranking]( (2, 3120948123123L) )
import scodec.Codec
import scodec.codecs.*

opaque type Ranking = (Int, Long)
given rankingCodec: Codec[Ranking] = uint8 :: int64

Catscript.appendAs[Ranking](path, (2, 3120948123123L))
import scodec.Codec
import scodec.codecs.*
import fs2.interop.scodec.*
import fs2.io.file.Flags

opaque type Ranking = (Int, Long)
given rankingCodec: Codec[Ranking] = uint8 :: int64

Stream.emit( (2, 3120948123123L) )
  .covary[IO]
  .through(StreamEncoder.many(summon[Codec[Ranking]]).toPipeByte)
  .through(Files[IO].writeAll(path, Flags.Append))
  .compile
  .drain