diff --git a/dev/compute_dir_coverage.py b/dev/compute_dir_coverage.py
new file mode 100755
index 0000000000000000000000000000000000000000..757e9d21423dc995f71d4be5f938113b5ec9ebdb
--- /dev/null
+++ b/dev/compute_dir_coverage.py
@@ -0,0 +1,214 @@
+##########################################################################
+#                                                                        #
+#  This file is part of Frama-C.                                         #
+#                                                                        #
+#  Copyright (C) 2007-2023                                               #
+#    CEA (Commissariat à l'énergie atomique et aux énergies              #
+#         alternatives)                                                  #
+#                                                                        #
+#  you can redistribute it and/or modify it under the terms of the GNU   #
+#  Lesser General Public License as published by the Free Software       #
+#  Foundation, version 2.1.                                              #
+#                                                                        #
+#  It is distributed in the hope that it will be useful,                 #
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of        #
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         #
+#  GNU Lesser General Public License for more details.                   #
+#                                                                        #
+#  See the GNU Lesser General Public License version 2.1                 #
+#  for more details (enclosed in the file licenses/LGPLv2.1).            #
+#                                                                        #
+##########################################################################
+
+# usage: compute_dir_coverage.py [-h] [-f] [-r] [-b] filepath
+#
+# positional arguments:
+#   filepath     path to frama-c/_coverage/index.html file
+#
+# options:
+#   -h, --help   show this help message and exit
+#   -f, --files  display files coverage
+#   -r, --ratio  display ratio (covered lines / total lines)
+#   -b, --bar    display % bars
+
+import re, sys, os.path
+from bs4 import BeautifulSoup
+import argparse
+
+FILE_MARKER = "<files>"
+DISPLAY_FILES = False
+DISPLAY_RATIO = False
+DISPLAY_BAR = False
+root = {FILE_MARKER: []}
+coverage = {}
+
+
+# Generate command line arguments and format
+def options():
+    argparser = argparse.ArgumentParser()
+    argparser.add_argument("filepath", help="path to frama-c/_coverage/index.html file")
+    argparser.add_argument("-f", "--files", action="store_true", help="display files coverage")
+    argparser.add_argument(
+        "-r", "--ratio", action="store_true", help="display ratio (covered lines / total lines)"
+    )
+    argparser.add_argument("-b", "--bar", action="store_true", help="display %% bars")
+    args = argparser.parse_args()
+    return args
+
+
+# Open the html file and create the structure to navigate inside
+def parse(filename):
+    if not os.path.exists(filename):
+        print("No such file or directory: " + filename)
+        exit()
+    with open(filename) as fp:
+        return BeautifulSoup(fp, "html.parser")
+
+
+# From our html structure, extract files informations
+# Return a list of nuples (file_path, covered_lines, total_lines)
+def extract(html_parser):
+    # This div contains all files data
+    all_files = html_parser.find(id="files")
+
+    file_list = list()
+    for file in all_files.find_all("div"):
+        # Access to covered/total lines data
+        data = file.contents[3].contents[1].text.split("/", 1)
+        covered_lines = int(re.sub(r"[^\d]+", "", data[0]))
+        total_lines = int(re.sub(r"[^\d]+", "", data[1]))
+        # Access to file path
+        path_to_file = file.contents[5].text.strip()
+        file_list.append((path_to_file, covered_lines, total_lines))
+    return file_list
+
+
+# Build file directory tree
+# Each folder is a dictionnary containing a list of files marked with FILE_MARKER
+# Other entries are folder
+# We store each files with it's coverage data
+def build_tree(path, covered_lines, total_lines, current_dir):
+    parts = path.split("/", 1)  # Perform only 1 split
+    if len(parts) == 1:  # path contains only a filename
+        current_dir[FILE_MARKER].append((parts[0], covered_lines, total_lines))
+    else:
+        directory, remaining_path = parts
+        if directory not in current_dir:
+            current_dir[directory] = {FILE_MARKER: []}
+        build_tree(remaining_path, covered_lines, total_lines, current_dir[directory])
+
+
+def concat_path(path, file):
+    if path == "":
+        return file
+    else:
+        return path + "/" + file
+
+
+def dir_coverage(d, path=""):
+    acc_coverage, acc_total = 0, 0
+    for key, value in d.items():
+        if key == FILE_MARKER:  # Files
+            # Add files stats to current dir
+            for file, covered_lines, total_lines in value:
+                acc_coverage = acc_coverage + covered_lines
+                acc_total = acc_total + total_lines
+        else:  # Directory
+            currpath = concat_path(path, key)
+            # Compute subdir stats
+            covered_lines, total_lines = dir_coverage(value, currpath)
+            coverage[currpath] = covered_lines, total_lines
+            # Add subdir stats to current dir
+            acc_coverage = acc_coverage + covered_lines
+            acc_total = acc_total + total_lines
+    return acc_coverage, acc_total
+
+
+def percentage(covered_lines, total_lines):
+    if total_lines != 0:
+        return covered_lines / total_lines * 100
+    else:
+        return 0.0
+
+
+###################
+# Print functions #
+###################
+
+
+def str_per(per):
+    return "{:>6.2f}".format(per)
+
+
+def str_bar(per):
+    if DISPLAY_BAR:
+        nb = round(per / 5)
+        return "|" + nb * "#" + (20 - nb) * "-" + "| "
+    else:
+        return ""
+
+
+def str_ratio(covered_lines, total_lines):
+    if DISPLAY_RATIO:
+        return " : (" + str(covered_lines) + " / " + str(total_lines) + ")"
+    else:
+        return ""
+
+
+def print_line(name, covered_lines, total_lines, indent):
+    per = percentage(covered_lines, total_lines)
+
+    bar = str_bar(per)
+    p = str_per(per)
+    ratio = str_ratio(covered_lines, total_lines)
+
+    if indent != "":
+        indent = indent + " "
+
+    print(bar + p + "% " + indent + name + ratio)
+
+
+def print_files(files, indent):
+    for file, covered_lines, total_lines in files:
+        print_line(file, covered_lines, total_lines, "---" + indent)
+
+
+def print_dir(directory, children, path, indent):
+    currpath = concat_path(path, directory)
+    covered_lines, total_lines = coverage[currpath]
+    print_line(directory, covered_lines, total_lines, indent)
+    print_tree(children, "---" + indent, currpath)
+
+
+# !!! dir_coverage must be called before !!!
+def print_tree(d, indent="", path=""):
+    for key, value in d.items():
+        if key != FILE_MARKER:  # Direcory
+            print_dir(key, value, path, indent)
+        elif DISPLAY_FILES:  # Files
+            print_files(value, indent)
+
+
+def main():
+    args = options()
+
+    # Set options booleans
+    global DISPLAY_FILES
+    DISPLAY_FILES = args.files
+    global DISPLAY_RATIO
+    DISPLAY_RATIO = args.ratio
+    global DISPLAY_BAR
+    DISPLAY_BAR = args.bar
+
+    html_parser = parse(args.filepath)
+    files = extract(html_parser)
+
+    for path_to_file, cov, tot in files:
+        build_tree(path_to_file, cov, tot, root)
+
+    dir_coverage(root)
+    print_tree(root)
+
+
+if __name__ == "__main__":
+    main()