ANN: cabal-fmt-0.1.4 - --no-cabal-file flag and fragments

Posted on 2020-08-30 by Oleg Grenrus packages

I spent this Sunday writing two small patches to cabal-fmt.

#--no-cabal-file flag

cabal-fmt reasonably assumes that the file it is formatting is a cabal package definition file. So it parses it as such. That is needed to correctly pretty print the fields, as some syntax, for example leading comma requires somewhat recent cabal-version: 2.2 (see Package Description Format Specification History for details).

However, there are other files using the same markup format, for example cabal.project files or cabal.haskell-ci configuration files used by haskell-ci tool. Wouldn't it be nice if cabal-fmt could format these as well. In cabal-fmt-0.1.4 you can pass -n or --no-cabal-fmt flag, to prevent cabal-fmt from parsing these files as cabal package file.

The downside is that the latest known cabal specification will be used. That shouldn't break cabal.haskell-ci files, but it might break cabal.project files if you are not careful. (Their parsing code is somewhat antique).

An example of reformatting the cabal.project of this blog:

--- a/cabal.project
+++ b/cabal.project
@@ -1,9 +1,7 @@
 index-state:   2020-05-10T17:53:22Z
 with-compiler: ghc-8.6.5
-
 packages:
-    "."
-    pkg/gists-runnable.cabal
+  "."
+  pkg/gists-runnable.cabal
 
-constraints:
-  hakyll +previewServer
+constraints:   hakyll +previewServer

So satisfying.

#Fragments

Another addition are fragments. They are best illustrated by an example. Imagine you have a multi-package project, and you use haskell-ci to generate your .travis.yml. Each .cabal package file must have the same

...

tested-with: GHC ==8.4.4 || ==8.6.5 || ==8.8.3 || ==8.10.1

library
  ...

Then you find out that GHC 8.8.4 and GHC-8.10.2 were recently released, and you want to update your CI configuration. Editing multiple files, with the same change. Busy work.

With cabal-fmt-0.1.4 you can create a fragment file, lets call it tested-with.fragment:

tested-with: GHC ==8.4.4 || ==8.6.5 || ==8.8.4 || ==8.10.2

And then edit your package files with a cabal-fmt pragma ( the fragment is probably in the root directory of project, but .cabal files are inside a directory per package):

 ...

+-- cabal-fmt: fragment ../tested-with.fragment
 tested-with: GHC ==8.4.4 || ==8.6.5 || ==8.8.3 || ==8.10.1

 library
  ...

Then when you next time run

cabal-fmt --inplace */*.cabal

you'll see the diff

 ...

 -- cabal-fmt: fragment ../tested-with.fragment
-tested-with: GHC ==8.4.4 || ==8.6.5 || ==8.8.3 || ==8.10.1
+tested-with: GHC ==8.4.4 || ==8.6.5 || ==8.8.4 || ==8.10.2


 library
  ...

for all libraries. Handy!

Some design comments:

  • Fragment is only a single field or a single section (e.g. common stanzas). Never multiple single fields. (Easier to implement, least surprising behavior: pragma is attached to a single field or section).
  • Field name or section header in the .cabal file and the fragment have to match. (To avoid mistakes).
  • Substitution is not recursive. (Guaranteed termination).
  • Other pragmas in fragments are not executed. Neither are comments in fragments preserved. (Not sure whether that would be valuable).

Finally, you can use cabal-fmt --no-cabal-fmt to format fragment files too, even they are reformatted when spliced.

#Conclusion

cabal-fmt-0.1.4 is a small release. I made --no-cabal-file to scratch my itch, and fragments partly to highlight that not every feature can exist in Cabal, but is very fine for preprocessors. I do think that fragments could be very useful in bigger projects. Let me know!


Site proudly generated by Hakyll