ANN: cabal-fmt

Posted on 2019-08-11 by Oleg Grenrus

As Cabal-3.0.0.0 is now released, I uploaded the cabal-fmt tool to Hackage. I have been using cabal-fmt for over a half year now for my own Haskell projects, and have been happy with this minimal, yet useful tool. cabal-fmt formats .cabal file preserving the field ordering and comments.

cabal-fmt is based on Distribution.Fields functionality. cabal-fmt is a thin addition on top. Same Distribution.Fields (and related Distribution.FieldsGrammar) is also used in haskell-ci to parse and print .cabal-like files. I also use it in other tools to implement configuration files. In my opinion the lexical structure of .cabal files is more flexible and human-writing-friendly than YAML or JSON. YMMV. For example the header for this post is written as

title: "ANN: cabal-fmt"
author: Oleg Grenrus

with quotes needed to disambiguate YAML. That's silly :) Cabal-like syntax would be

title: ANN: cabal-fmt
author: Oleg Grenrus

However, enough bashing YAML.

cabal-fmt is opinionated tool, it does format few fields to my liking. Let us see how.

#build-depends

build-depends modules are formatted comma first (with cabal-version: 2.2 also with a leading comma), tabulated, sorted, and ^>= preferred when it can be used. For example:

build-depends:
  , base        ^>=4.11.1.0 || ^>=4.12.0.0
  , bytestring  ^>=0.10.8.2
  , Cabal       ^>=3.0.0.0
  , containers  ^>=0.5.11.0 || ^>=0.6.0.1
  , filepath    ^>=1.4.2
  , mtl         ^>=2.2.2
  , parsec      ^>=3.1.13.0
  , pretty      ^>=1.1.3.6

or (for older cabal-version):

build-depends:
    base     >=4.3     && <4.14
  , deepseq  >=1.3.0.0 && <1.5
  , time     >=1.2.0.3 && <1.10

Single build-depends are formatted as a single line, like

build-depends: random >=1.0 && <1.2

#nub & sort

exposed-modules, other-modules, default-extensions and other-extensions are sorted and duplicates removed. For example.

exposed-modules:
  CabalFmt
  CabalFmt.Comments
  CabalFmt.Error
  CabalFmt.Fields
  CabalFmt.Fields.BuildDepends
  CabalFmt.Fields.Extensions
  CabalFmt.Fields.Modules
  CabalFmt.Fields.TestedWith
  CabalFmt.Monad
  CabalFmt.Options
  CabalFmt.Parser
  CabalFmt.Refactoring

Sometimes, you'll prefer some module to be the first, for cabal repl. In that case I would use two exposed-modules fields.

#tested-with

tested-with is one more field where I don't like the default formatting either. This field drives the job selection in haskell-ci. cabal-fmt combines version ranges for compilers, and prints GHC and GHCJS in upper case.

tested-with:
  GHC ==8.8.1 || ==8.6.5 || ==8.4.4 || ==8.2.2 || ==8.0.2 || ==7.10.3
  GHCJS ==8.4

The line generated is long, especially for packages supporting a lot of GHC versions. Something I don't have a clear preference yet how to handle.

#Extra: expand exposed-modules and other-modules

The recent addition is an ability to (re)write field contents, while formatting. There's an old, ongoing discussion of allowing wildcard specification of exposed-modules in .cabal format. I'm against that change. Instead, rather cabal-fmt (or an imaginary IDE), would regenerate parts of .cabal file given some commands.

cabal-fmt: expand <directory> is a one (the only at the moment) such command.

cabal-fmt will look into directory for files, turn filenames into module names and append to the contents of exposed-modules. As the field is then nubbed and sorted, expanding is idempotent. For example cabal-fmt itself has:

-- cabal-fmt: expand src
--
exposed-modules:
  CabalFmt
  ...

The functionality is simple. There is no removal of other-modules or main-is. I think that using different directory for these is good enough workaround, and may make things clearer: directory for public modules and a directory for private ones.

#Conclusion

And that's all that cabal-fmt does. Formatting of other fields comes directly from Cabal. I have few ideas, what else can be done, e.g.

  • cabal-fmt: expand for extra-source-files
  • formatting of reexported-modules
  • sorting of fields, e.g. putting type and default-language to the top of the component stanzas.

But these don't bother me enough yet, so they are not there.

The implicit goal of a project is to iterate independently of cabal-install, Find out what could be useful, and how it can be done, and later merge into cabal-install's cabal format functionality. Yet then providing enough configuration knobs to not be so opinionated.


Site proudly generated by Hakyll