Hello Tommi!
07 Oct 25 09:07, you wrote to me:
Do you have the "FIDOCONFIG" environment set?
Yes, it works also without any paramaters. But see my previous
message. :)
Fixed...
The script now:
  1. Actually does something with the "FIDOCONFIG" environment.
  1. Extracts the basename from the configured Outbound path
     (e.g., "fred" from /home/vk3heg/stats/fred/)
  2. Uses that basename instead of hardcoded "outbound" (doh.. I though I'd removed them... )
Try this version.
Stephen
=== Cut ===
#!/usr/bin/env python3
"""
Display outbound summary for every link
for which there is anything in the outbound
Created by Pavel Gulchouck 2:463/68@fidonet
Fixed by Stas Degteff 2:5080/102@fidonet
Modified by Michael Dukelsky 2:5020/1042@fidonet
Modified by Stephen Walsh 3:633/280@fidonet
Python version by Stephen Walsh 3:633/280@fidonet
"""
import os
import sys
import glob
import re
import time
from pathlib import Path
from typing import Dict, List, Tuple, Optional
VERSION = "3.1
# Size constants
MB = 1024 * 1024
GB = MB * 1024
def usage():
    """Print usage information"""
    print("""
    The script showold.py prints out to STDOUT how much netmail,
    echomail and files are stored for every link in the outbound
    and fileboxes and for how long they are stored.
    If FIDOCONFIG environment variable is defined, you may use the
    script without arguments, otherwise you have to supply the path
    to fidoconfig as an argument.
    Usage:
        python3 showold.py
        python3 showold.py <path to fidoconfig>
    Example:
        python3 showold.py /home/husky/etc/config
    """)
    sys.exit(1)
def parse_fido_address(addr: str) -> Tuple[int, int, int, int]:
    """Parse FidoNet address and return sortable tuple.
    Returns: (zone, net, node, point)
    """
    # Format: zone:net/node[.point][@domain]
    addr = addr.split('@')[0]  # Remove domain
    zone, net, node, point = 0, 0, 0, 0
    if ':' in addr:
        zone_part, rest = addr.split(':', 1)
        zone = int(zone_part) if zone_part.isdigit() else 0
    else:
        rest = addr
    if '/' in rest:
        net_part, node_part = rest.split('/', 1)
        net = int(net_part) if net_part.isdigit() else 0
        if '.' in node_part:
            node_str, point_str = node_part.split('.', 1)
            node = int(node_str) if node_str.isdigit() else 0
            point = int(point_str) if point_str.isdigit() else 0
        else:
            node = int(node_part) if node_part.isdigit() else 0
    return (zone, net, node, point)
def node_sort_key(addr: str) -> Tuple[int, int, int, int]:
    """Return sortable key for FidoNet address"""
    return parse_fido_address(addr)
def unbso(filename: str, directory: str, def_zone: int, outbound_basename: str = 'outbound') -> str:
    """Parse BSO filename and directory to get FidoNet address"""
    # Check if we're in a point directory
    dir_name = os.path.basename(directory)
    is_point_dir = False
    net = None
    node = None
    # Match point directory: NNNNPPPP.pnt
    pnt_match = re.match(r'^([0-9a-f]{4})([0-9a-f]{4})\.pnt$',
                        dir_name, re.I)
    if pnt_match:
        net = int(pnt_match.group(1), 16)
        node = int(pnt_match.group(2), 16)
        is_point_dir = True
        directory = os.path.dirname(directory)
    # Determine zone from directory name
    dir_name = os.path.basename(directory)
    zone_match = re.match(rf'^{re.escape(outbound_basename)}\.([0-9a-f]{{3}})$', dir_name, re.I)
    if zone_match:
        zone = int(zone_match.group(1), 16)
    elif dir_name.lower() == outbound_basename.lower():
        zone = def_zone
    else:
        zone = def_zone
    # Parse filename
    if is_point_dir:
        # In point directory: filename is 8 hex digits (point number)
        file_match = re.match(r'^([0-9a-f]{8})', filename, re.I)
        if file_match and net is not None and node is not None:
            point = int(file_match.group(1), 16)
            return f"{zone}:{net}/{node}.{point}"
    else:
        # Not in point directory: NNNNPPPP format
        file_match = re.match(r'^([0-9a-f]{4})([0-9a-f]{4})', filename,
                             re.I)
        if file_match:
            net = int(file_match.group(1), 16)
            node = int(file_match.group(2), 16)
            return f"{zone}:{net}/{node}"
    return ""
def unaso(filename: str) -> str:
    """Parse ASO filename to get FidoNet address"""
    match = re.match(r'(\d+)\.(\d+)\.(\d+)\.(\d+)', filename)
    if match:
        zone, net, node, point = match.groups()
        if point == '0':
            return f"{zone}:{net}/{node}"
        else:
            return f"{zone}:{net}/{node}.{point}"
    return ""
def unbox(directory: str) -> str:
    """Parse filebox directory name to get FidoNet address"""
    dir_name = os.path.basename(directory.rstrip('/'))
    match = re.match(r'(\d+)\.(\d+)\.(\d+)\.(\d+)(?:\.h)?$',
                    dir_name, re.I)
    if match:
        zone, net, node, point = match.groups()
        if point == '0':
            return f"{zone}:{net}/{node}"
        else:
            return f"{zone}:{net}/{node}.{point}"
    return ""
def nice_number(num: int) -> float:
    """Convert number to nice format (MB or GB)"""
    if num < MB:
        return num
    elif num >= MB and num < GB:
        return num / MB
    else:
        return num / GB
def nice_number_format(num: int) -> str:
    """Return format string for nice number"""
    if num < MB:
        return f"{num:9d} "
    elif num < GB:
        return f"{num/MB:9.4f}M"
    else:
        return f"{num/GB:9.4f}G"
def find_outbounds(base_dir: str, outbound_basename: str = 'outbound') -> List[str]:
    """Find all outbound directories"""
    outbounds = []
    for root, dirs, files in os.walk(base_dir):
        for d in dirs:
            if re.match(rf'^{re.escape(outbound_basename)}(?:\.[0-9a-f]{{3}})?$', d, re.I):
                outbounds.append(os.path.join(root, d))
    return outbounds
def find_fileboxes(base_dir: str) -> List[str]:
    """Find all filebox directories"""
    boxes = []
    for root, dirs, files in os.walk(base_dir):
        for d in dirs:
            if re.match(r'\d+\.\d+\.\d+\.\d+(?:\.h)?$', d, re.I):
                boxes.append(os.path.join(root, d))
    return boxes
def read_fidoconfig(config_path: str) -> Dict[str, str]:
    """Read fidoconfig file and extract needed values"""
    config = {}
    with open(config_path, 'r', encoding='utf-8',
             errors='replace') as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith('#'):
                continue
            # Simple tokenization
            parts = line.split(None, 1)
            if len(parts) < 2:
                continue
            keyword = parts[0].lower()
            value = parts[1]
            if keyword == 'address' and 'address' not in config:
                config['address'] = value
            elif keyword == 'outbound':
                config['outbound'] = value.rstrip('/')
            elif keyword == 'fileboxesdir':
                config['fileboxesdir'] = value.rstrip('/')
            elif keyword == 'passfileareadir':
                config['passfileareadir'] = value.rstrip('/')
    return config
def process_bso_outbound(outbound_dir: str, def_zone: int,
                        pass_file_area_dir: str,
                        minmtime: Dict, netmail: Dict,
                        echomail: Dict, files: Dict,
                        outbound_basename: str = 'outbound'):
    """Process BSO outbound directory"""
    # Find all control files in outbound
    control_patterns = ['*.[IiCcDdFfHh][Ll][Oo]', '*.[IiCcDdOoHh][Uu][Tt]']
    control_files = []
    for pattern in control_patterns:
        control_files.extend(
            glob.glob(os.path.join(outbound_dir, pattern)))
    # Find point directories
    pnt_dirs = glob.glob(os.path.join(outbound_dir, '*.[Pp][Nn][Tt]'))
    for pnt_dir in pnt_dirs:
        if os.path.isdir(pnt_dir):
            for pattern in control_patterns:
                control_files.extend(
                    glob.glob(os.path.join(pnt_dir, pattern)))
    for ctrl_file in control_files:
        directory = os.path.dirname(ctrl_file)
        filename = os.path.basename(ctrl_file)
        node = unbso(filename, directory, def_zone, outbound_basename)
        if not node:
            continue
        stat_info = os.stat(ctrl_file)
        size = stat_info.st_size
        mtime = stat_info.st_mtime
        if size == 0:
            continue
        # Update min mtime
        if node not in minmtime or mtime < minmtime[node]:
            minmtime[node] = mtime
        # Check if it's netmail
        if ctrl_file.lower().endswith('ut'):
            netmail[node] = netmail.get(node, 0) + size
            continue
        # Process control file contents
        is_echomail_ctrl = bool(re.search(r'\.(c|h|f)lo$', ctrl_file,
                                         re.I))
        is_file_ctrl = bool(re.search(r'\.(c|i|d)lo$', ctrl_file, re.I))
        try:
            with open(ctrl_file, 'r', encoding='utf-8',
                     errors='replace') as f:
                for line in f:
                    line = line.strip()
                    line = re.sub(r'^[#~^]', '', line)
                    if not line:
                        continue
                    bundle_path = None
                    # Check if absolute path
                    if line.startswith('/'):
                        bundle_path = line
                    # Check if it's in passFileAreaDir
                    elif (pass_file_area_dir and
                          line.startswith(pass_file_area_dir)):
                        bundle_path = line
                    # Check for bundle patterns
                    elif re.match(
                        r'^[0-9a-f]{8}\.(su|mo|tu|we|th|fr|sa)[0-9a-z]$',
                        line, re.I):
                        bundle_path = os.path.join(directory, line)
                    elif re.match(r'\.tic$', line, re.I):
                        bundle_path = os.path.join(directory, line)
                    elif re.match(r'^[0-9a-f]{8}\.pkt$', line, re.I):
                        bundle_path = os.path.join(directory, line)
                    if bundle_path and os.path.exists(bundle_path):
                        b_stat = os.stat(bundle_path)
                        b_size = b_stat.st_size
                        b_mtime = b_stat.st_mtime
                        if (node not in minmtime or
                            b_mtime < minmtime[node]):
                            minmtime[node] = b_mtime
                        # Categorize
                        if re.search(
                            r'\.(su|mo|tu|we|th|fr|sa)[0-9a-z]$',
                            line, re.I):
                            echomail[node] = echomail.get(node, 0) + b_size
                        elif (bundle_path.endswith('.pkt') and
                              is_echomail_ctrl):
                            echomail[node] = echomail.get(node, 0) + b_size
                        elif bundle_path.endswith('.tic'):
                            files[node] = files.get(node, 0) + b_size
                        elif (pass_file_area_dir and
                              bundle_path.startswith(pass_file_area_dir)):
                            files[node] = files.get(node, 0) + b_size
                        elif line.startswith('/') and is_file_ctrl:
                            files[node] = files.get(node, 0) + b_size
                        elif line.startswith('/'):
                            files[node] = files.get(node, 0) + b_size
        except Exception as e:
            print(f"WARN: Could not process {ctrl_file}: {e}",
                 file=sys.stderr)
def process_fileboxes(box_dir: str, minmtime: Dict, netmail: Dict,
                     echomail: Dict, files: Dict):
    """Process filebox directory"""
    node = unbox(box_dir)
    if not node:
        return
    # Find all files in the filebox
    patterns = ['*.[IiCcDdOoHh][Uu][Tt]',
               '*.[Ss][Uu][0-9a-zA-Z]', '*.[Mm][Oo][0-9a-zA-Z]',
               '*.[Tt][Uu][0-9a-zA-Z]', '*.[Ww][Ee][0-9a-zA-Z]',
               '*.[Tt][Hh][0-9a-zA-Z]', '*.[Ff][Rr][0-9a-zA-Z]',
               '*.[Ss][Aa][0-9a-zA-Z]']
    file_list = []
    for pattern in patterns:
        file_list.extend(glob.glob(os.path.join(box_dir, pattern)))
    for fpath in file_list:
        if not os.path.isfile(fpath):
            continue
        stat_info = os.stat(fpath)
        size = stat_info.st_size
        mtime = stat_info.st_mtime
        if size == 0:
            continue
        if node not in minmtime or mtime < minmtime[node]:
            minmtime[node] = mtime
        filename = os.path.basename(fpath)
        if re.search(r'ut$', filename, re.I):
            netmail[node] = netmail.get(node, 0) + size
        elif re.search(r'\.(su|mo|tu|we|th|fr|sa)[0-9a-z]$',
                      filename, re.I):
            echomail[node] = echomail.get(node, 0) + size
        else:
            files[node] = files.get(node, 0) + size
def main():
    # Get fidoconfig path
    fidoconfig = os.environ.get('FIDOCONFIG')
    if len(sys.argv) == 2:
        if sys.argv[1] in ['-h', '--help', '-?', '/?', '/h']:
            usage()
        fidoconfig = sys.argv[1]
    elif not fidoconfig:
        usage()
    if not os.path.isfile(fidoconfig):
        print(f"\n'{fidoconfig}' is not a fidoconfig file\n")
        usage()
    # Read config
    print(f"Showold.py version '{VERSION}'")
    config = read_fidoconfig(fidoconfig)
    if 'address' not in config:
        print("\nYour FTN address is not defined\n", file=sys.stderr)
        sys.exit(1)
    # Parse default zone
    addr_match = re.match(r'^(\d+):\d+/\d+', config['address'])
    if not addr_match:
        print("\nYour FTN address has a syntax error\n",
             file=sys.stderr)
        sys.exit(1)
    def_zone = int(addr_match.group(1))
    if 'outbound' not in config:
        print("\nOutbound is not defined\n", file=sys.stderr)
        sys.exit(1)
    outbound = config['outbound']
    if not os.path.isdir(outbound):
        print(f"\nOutbound '{outbound}' is not a directory\n",
             file=sys.stderr)
        sys.exit(1)
    # Get parent directory for finding all outbounds
    husky_base_dir = os.path.dirname(outbound)
    outbound_basename = os.path.basename(outbound)
    print(f"Searching for outbounds in: '{husky_base_dir}'")
    # Find directories
    outbound_dirs = find_outbounds(husky_base_dir, outbound_basename)
    fileboxes_dir = config.get('fileboxesdir', '')
    filebox_dirs = []
    if fileboxes_dir and os.path.isdir(fileboxes_dir):
        filebox_dirs = find_fileboxes(fileboxes_dir)
    pass_file_area_dir = config.get('passfileareadir', '')
    # Process all outbounds
    minmtime = {}
    netmail = {}
    echomail = {}
    files_dict = {}
    for ob_dir in outbound_dirs:
        process_bso_outbound(ob_dir, def_zone, pass_file_area_dir,
                            minmtime, netmail, echomail, files_dict,
                            outbound_basename)
    for fb_dir in filebox_dirs:
        process_fileboxes(fb_dir, minmtime, netmail, echomail,
                         files_dict)
    # Print results
    print("+------------------+--------+-----------+-----------+"
         "-----------+")
    print("|       Node       |  Days  |  NetMail  |  EchoMail "
         "|   Files   |")
    print("+------------------+--------+-----------+-----------+"
         "-----------+")
    for node in sorted(minmtime.keys(), key=node_sort_key):
        nm = netmail.get(node, 0)
        em = echomail.get(node, 0)
        fl = files_dict.get(node, 0)
        days = (time.time() - minmtime[node]) / (24 * 60 * 60)
        print(f"| {node:16s} |{days:7.0f} |"
             f"{nice_number_format(nm)} |"
             f"{nice_number_format(em)} |"
             f"{nice_number_format(fl)} |")
    print("+------------------+--------+-----------+-----------+"
         "-----------+")
if __name__ == '__main__':
    main()
=== Cut ===
--- GoldED+/LNX 1.1.5-b20250409
 * Origin: Dragon's Lair ---:- dragon.vk3heg.net -:--- Prt: 6800 (3:633/280)