# How to organize all the tags in Hledger (including sub-tags)?

**URL:** https://forum.plaintextaccounting.org/t/how-to-organize-all-the-tags-in-hledger-including-sub-tags/854
**Category:** General
**Tags:** hledger
**Created:** [July 21, 2026, 2:03am UTC](https://forum.plaintextaccounting.org/t/how-to-organize-all-the-tags-in-hledger-including-sub-tags/854 "2026-07-21T02:03:46Z")
**Posts on this page:** 5
**Page:** 1

<div class="post-metadata">

### Author: ![joviwu](https://avatars.discourse-cdn.com/v4/letter/j/b5ac83/32.png) [@joviwu](https://forum.plaintextaccounting.org/u/joviwu)
#### Post date: [July 21, 2026, 2:03am UTC](https://forum.plaintextaccounting.org/t/how-to-organize-all-the-tags-in-hledger-including-sub-tags/854/1 "2026-07-21T02:03:46Z")

</div>

Hi everyone,

Using `hledger tags` only shows the top-level tags, and doesn't display any sub-tags.  
For example, if I have tags like `vehicle : A`, `vehicle : B`, `vendor : AA`, `vendor : BB`,  
running `hledger tags` only shows `vehicle` and `vendor`.

Is there a command in hledger that lists all tags along with their sub-tags?

OR  
Do you have any other recommendations for managing tags?  
Thanks!

---

<div class="post-metadata">

### Author: ![simonmic](https://yyz1.discourse-cdn.com/flex003/user_avatar/forum.plaintextaccounting.org/simonmic/32/1_2.png) [@simonmic](https://forum.plaintextaccounting.org/u/simonmic)
#### Post date: [July 21, 2026, 2:12am UTC](https://forum.plaintextaccounting.org/t/how-to-organize-all-the-tags-in-hledger-including-sub-tags/854/2 "2026-07-21T02:12:36Z")

</div>

We call those tag values, and `tags --values` will show them. But, there's no builtin report that will show tag names and values together, except `print`, I think.

So you could extract them like this, using ripgrep:

```auto
$ hledger print tag:. | rg '; *(\w+:.*)' -or '$1'
vehicle:A
vehicle:AA
vendor:B
vendor:BB

```

---

<div class="post-metadata">

### Author: ![joviwu](https://avatars.discourse-cdn.com/v4/letter/j/b5ac83/32.png) [@joviwu](https://forum.plaintextaccounting.org/u/joviwu)
#### Post date: [July 21, 2026, 2:28am UTC](https://forum.plaintextaccounting.org/t/how-to-organize-all-the-tags-in-hledger-including-sub-tags/854/3 "2026-07-21T02:28:28Z")

</div>

But tags --value won't be displayed simultaneously with tags.

Is there a command like `accounts -tree` that can display the hierarchical relationship between tags and values?

Example

vehicle

A

B

vendor

AA

BB

---

<div class="post-metadata">

### Author: ![simonmic](https://yyz1.discourse-cdn.com/flex003/user_avatar/forum.plaintextaccounting.org/simonmic/32/1_2.png) [@simonmic](https://forum.plaintextaccounting.org/u/simonmic)
#### Post date: [July 21, 2026, 2:51am UTC](https://forum.plaintextaccounting.org/t/how-to-organize-all-the-tags-in-hledger-including-sub-tags/854/4 "2026-07-21T02:51:13Z")

</div>

Just for an example, I went through the process of making a custom report. It went like this:

I copied `bin/hledger-example-read.hs` to `bin/hledger-tagvals.hs`:

```haskell
#!/usr/bin/env stack
-- stack runghc --package hledger --package text
-- (to see setup progress output, add --verbosity info)

-- hledger-example-read - a short hledger stack script example

{-# LANGUAGE OverloadedStrings #-}

import Hledger.Cli.Script
import Data.Text qualified as T
import Data.Text.IO qualified as T

cmdmode = hledgerCommandMode (unlines
    ---------------------------standard terminal width-----------------------------
  ["example-hello"
  ,"Usage: hledger-example-hello [OPTS] [ARGS]"
  ,"or: hledger example-hello -- [OPTS] [ARGS]"
  ,"Examples:"
  ,"$ hledger-example-hello # do the thing"
  ,"$ hledger-example-hello --help # print help"
  ])
  [] [generalflagsgroup1] [] ([], Just $ argsFlag "[ARGS]")

main = do
  opts@CliOpts{reportspec_=rspec} <- getHledgerCliOpts cmdmode
  withJournal opts $ \j -> do
    putStrLn $ "successfully read journal: " <> journalFilePath j

```

And checked that it runs:

```auto
~/src/hledger$ hledger tagvals
successfully read journal: /Users/simon/adm/2026/2026.journal

```

Then I found the builtin tags command source (`hledger/Hledger/Cli/Commands/Tags.hs`)  
and copied its `tags` function (and the non-Hledger imports),  
and hid the `tags` import because I wanted to use the same name for my version,  
and updated the help text:

```haskell
#!/usr/bin/env stack
-- stack runghc --package hledger --package text --verbosity info
-- (to see setup progress output, add --verbosity info)

{-# LANGUAGE OverloadedStrings #-}

import Control.Monad.Fail qualified as Fail
import Data.Function ((&))
import Data.List (find)
import Data.List.Extra (nubSort)
import Data.Maybe (fromMaybe)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Safe
import System.Console.CmdArgs.Explicit

import Hledger.Cli.Script hiding (tags)

cmdmode = hledgerCommandMode (unlines
    ---------------------------standard terminal width-----------------------------
  ["tagvals"
  ,"Usage: hledger-tagvals [OPTS] [ARGS]"
  ,"or: hledger tagvals -- [OPTS] [ARGS]"
  ,":"
  ,"Print the unique tag name & value pairs."
  ])
  [] [generalflagsgroup1] [] ([], Just $ argsFlag "[ARGS]")

main = do
  opts@CliOpts{reportspec_=rspec} <- getHledgerCliOpts cmdmode
  withJournal opts $ \j -> tags opts j
    
tags :: CliOpts -> Journal -> IO ()
tags opts@CliOpts{rawopts_=rawopts, reportspec_=rspec@ReportSpec{_rsQuery=_q, _rsReportOpts=ropts}} j = do
  printTitle ropts
  let today = _rsDay rspec
      args = listofstringopt "args" rawopts
  -- For convenience/power, the first argument is a tag name regex, 
  -- separate from the main query arguments: hledger tags [TAGREGEX [QUERYARGS..]]
  -- So we have to re-parse the query here. Overcomplicated ?
  mtagpat <- mapM (either Fail.fail pure . toRegexCI . T.pack) $ headMay args
  let
    values = boolopt "values" rawopts
    parsed = boolopt "parsed" rawopts
    empty = empty_ ropts
    querystr = map T.pack $ drop 1 args
  query <- either usageError (return . fst) $ parseQueryList today querystr
  let
    q = simplifyQuery $ And [queryFromFlags ropts, query]
    txns = filter (q `matchesTransaction`) $ jtxns $ journalApplyValuationFromOpts rspec j
    accts =
      -- also search for tags in matched account declarations,
      -- unless there is a query for something transaction-specific, like date: or amt:.
      if dbg5 "queryIsTransactionRelated" $ queryIsTransactionRelated $ dbg4 "q" q
      then []
      else filter (matchesAccountExtra (journalAccountType j) (journalInheritedAccountTags j) q) $
           map fst $ jdeclaredaccounts j
    -- bit of a mess.
    used = dbg5 "used" $ concatMap (journalAccountTags j) accts ++ concatMap transactionAllTags txns
    declared' = dbg5 "declared'" $ map (,"") $ journalTagsDeclared j
    filtereddeclared = dbg5 "filtereddeclared'" $ filter (q `matchesTag`) declared'
    (usednames, declarednames) = (map fst used, map fst filtereddeclared)
    unused = dbg5 "unused" $ filter (not . (`elem` usednames) . fst) filtereddeclared
    undeclared = dbg5 "undeclared" $ filter (not . (`elem` declarednames) . fst) used
    all' = dbg5 "all''" $ filtereddeclared <> used
    found = dbg5 "found" $ foundtag
      where
        -- First find the name, then the first occurrence of that tag.
        -- So that --values and --parsed still work with --find (in some reasonably stable way).
        alltags = declared' <> used
        allnames = dbg5 "allnames" $ nubSort $ map fst alltags
        foundname = dbg5 "foundname" $ findMatchedByArgument rawopts "tag name" allnames
        foundtag = find ((==foundname).fst) alltags
          & fromMaybe (error' "tags: could not find a tag's first occurrence") -- PARTIAL: should not happen because allnames and alltags correspond

    tags' =
      case declarablesSelectorFromOpts opts of
        Nothing -> all'
        Just Used -> used
        Just Declared -> declared'
        Just Undeclared -> undeclared
        Just Unused -> unused
        Just Find -> [found]

    results =
      (if parsed then id else nubSort)
      [ r
      | (t,v) <- tags'
      , maybe True (`regexMatchText` t) mtagpat
      , let r = if values then v else t
      , not (values && T.null v && not empty)
      ]

  mapM_ T.putStrLn results

```

Now I had my own version of the tags command:

```auto
$ cat a.j
2026-07-12
    (assets) 1 ; vehicle:A

2026-07-12
    (assets) 1 ; vehicle:AA

2026-07-12
    (assets) 1 ; vendor:B

2026-07-12
    (assets) 1 ; vendor:B

```

```auto
$ hledger -f a.j tagval
vehicle
vendor

```

And could replace line 87:

```haskell
      [ r

```

with:

```haskell
      [ t <> ": " <> v

```

```auto
$ hledger -f a.j tagvals
vehicle: A
vehicle: AA
vendor: B
vendor: BB

```

And compile it for speed (using the hledger project's installed packages):

```auto
~/src/hledger$ stack exec -- ghc bin/hledger-tagvals.hs 
[1 of 2] Compiling Main ( bin/hledger-tagvals.hs, bin/hledger-tagvals.o ) [Source file changed]
[2 of 2] Linking bin/hledger-tagvals [Objects changed]
~/src/hledger$ hledger tagvals --help
tagvals [OPTIONS] [ARGS]
  or: hledger tagvals -- [OPTS] [ARGS]
  Examples:
  $ hledger-tagvals # do the thing
  $ hledger-tagvals --help # print help

  See also hledger -h for general hledger options.

```

Solutions using just command-line tools, like [the one above](https://forum.plaintextaccounting.org/t/how-to-organize-all-the-tags-in-hledger-including-sub-tags/854/2), are usually quicker to make and more terse. But the custom haskell script can be more powerful and more robust.

---

<div class="post-metadata">

### Author: ![simonmic](https://yyz1.discourse-cdn.com/flex003/user_avatar/forum.plaintextaccounting.org/simonmic/32/1_2.png) [@simonmic](https://forum.plaintextaccounting.org/u/simonmic)
#### Post date: [July 21, 2026, 3:14am UTC](https://forum.plaintextaccounting.org/t/how-to-organize-all-the-tags-in-hledger-including-sub-tags/854/5 "2026-07-21T03:14:17Z")

</div>

For example, I added a tag on the same line as another (`; vendor:B, verb:walk`). Then I got your desired tree report by making account directives and using `hledger accounts`.

Using command line only:

```auto
~/src/hledger$ hledger -f a.j print tag:. | rg '; *(\w+:.*)' -or '$1' | sed 's/^/account /' | hledger -f- accounts -t
vehicle
  A
  AA
vendor
  B
  B, verb
    walk

```

Using a combination of haskell script and command line, it works properly:

```auto
~/src/hledger$ hledger -f a.j tagvals | sed 's/^/account /' | hledger -f- accounts -t
vehicle
   A
   AA
vendor
   B
verb
   walk

```

Or of course you could do it all in the script, at the cost of more programming.
