elm-script icon indicating copy to clipboard operation
elm-script copied to clipboard

Experimental command-line scripting for Elm

ianmackenzie/elm-script

EXPERIMENTAL - expect breaking changes, missing functionality, incomplete documentation etc.

This package allows you define command-line scripts in Elm that can

  • Read and write files
  • Accept command-line arguments
  • Read environment variables
  • Make HTTP requests
  • Run subprocesses

Here's a complete "Hello World" program (examples/HelloWorld.elm):

module HelloWorld exposing (main)

import Script exposing (Script)


script : Script.Init -> Script String ()
script {} =
    Script.printLine "Hello World!"


main : Script.Program
main =
    Script.program script

And here's a slightly more realistic/useful script that counts the number of lines in files given at the command line (examples/LineCounts.elm):

module LineCounts exposing (main)

import Script exposing (Script)
import Script.File as File exposing (File, ReadOnly)


getLineCount : File ReadOnly -> Script String Int
getLineCount file =
    File.read file
        |> Script.map (String.trimRight >> String.lines >> List.length)


script : Script.Init -> Script String ()
script { arguments, userPrivileges } =
    List.map (File.readOnly userPrivileges) arguments
        |> Script.collect getLineCount
        |> Script.map (List.map2 Tuple.pair arguments)
        |> Script.thenWith
            (Script.each
                (\( fileName, lineCount ) ->
                    Script.printLine
                        (fileName
                            ++ ": "
                            ++ String.fromInt lineCount
                            ++ " lines"
                        )
                )
            )


main : Script.Program
main =
    Script.program script

One of they key features of this package is very explicit control over permissions. The top-level script has full access to the file system, environment variables etc. but it can choose exactly how much access to give to helper scripts. A Script cannot by default do anything other than a few harmless things like getting the current time and printing to the console; in order to do anything more, it must explicitly be given read access to a particular directory, write access to a particular file, network access etc. In the above example, you can know just from the type signature of getLineCount that the returned script can read the file that you pass it, but it can't read any other files, it can't write to any files at all, and it can't access the network (to, say, send the contents of passwords.txt to an evil server somewhere).

My hope is that this will make it possible to share scripting functionality via the Elm package system without worrying that some script written by a stranger is going to format your hard drive. Even when just writing your own scripts, the type system helps keep track of which parts of your code are doing file I/O (to what files, in what directories), which parts are performing network requests, which parts are running potentially dangerous subprocesses, etc.

Getting started

ianmackenzie/elm-script has not yet been published, so right now if you want to play around with it you'll have to check out this repository. You can then either just experiment with the files in the examples directory, or add the src directory of this package to the source-directories field of your elm.json.

To actually run scripts, you'll need to first install Deno. You should then be able to install the elm-script command by running

deno install -A -n elm-script https://elm-script.github.io/latest

This will create a small executable file named elm-script that calls Deno to execute runner/main.js:

  • -A tells Deno to give full permissions (file system, network etc.) to elm-script; elm-script has its own permissions system (described above) to ensure that untrusted code cannot do anything malicious.
  • -n elm-script tells Deno to name the resulting file elm-script instead of latest.
  • You will need to also add -f to force Deno to overwrite an existing elm-script file if you are updating to a new version.

If you need to use the bleeding-edge version of the runner script (either because the published version is out of date, or you've made some local modifications) you can instead run

deno install -A -n elm-script path/to/elm-script/runner/main.js

to use the locally checked-out version. However, this will also introduce a small delay when running scripts, since Deno seems to then 'recompile' main.js every time.

Where exactly elm-script gets installed depends on your operating system and Deno configuration, but you will need to make sure that directory gets added to your PATH; see the deno install docs for details. Once that is all done, you should be able to run Elm scripts using

elm-script run Main.elm

Refer to the API documentation for more details, or check out some more examples.