# Issue #979: fact.Crontab cannot handle variables resulting in crash

GitHub Issue: https://github.com/pyinfra-dev/pyinfra/issues/979

## Issue Summary

The Crontab fact was unable to parse crontab files containing environment variable definitions, causing a crash with `ValueError: not enough values to unpack (expected 6, got 1)`.

### Reproduction

A crontab file like:
```
MAILTO=me@example.com
CRON_TZ=Europe/Amsterdam

@reboot echo "rebooted"
```

Would crash with:
```
...venv/lib/python3.10/site-packages/pyinfra/facts/server.py", line 387, in process
    minute, hour, day_of_month, month, day_of_week, command = line.split(None, 5)
ValueError: not enough values to unpack (expected 6, got 1)
```

### Background

According to `man 5 crontab`, crontab files can contain two types of active lines:
1. **Environment settings**: Lines of the form `name = value` (spaces around `=` are optional)
2. **Cron commands**: Standard cron entries with 5 time fields + command, or special times like `@daily`

The old code (in `server.py`) did not account for environment variable lines, attempting to parse every non-comment line as a cron command.

## Root Cause Analysis

### Original Problematic Code

The original `Crontab` fact was located in `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/facts/server.py` and contained code that assumed every non-comment, non-empty line was a cron entry:

```python
def process(self, output):
    crons = {}
    current_comments = []

    for line in output:
        line = line.strip()
        if not line or line.startswith("#"):
            current_comments.append(line)
            continue

        if line.startswith("@"):
            special_time, command = line.split(None, 1)
            crons[command] = {
                "special_time": special_time,
                "comments": current_comments,
            }
        else:
            # BUG: This line crashes on environment variables like "MAILTO=me@example.com"
            minute, hour, day_of_month, month, day_of_week, command = line.split(None, 5)
            ...
```

When encountering a line like `MAILTO=me@example.com`, the `.split(None, 5)` call would only produce 1 element (the entire line), causing the `ValueError`.

## Resolution Status: FIXED

This issue has been **resolved** in PR #1193, merged in commit `612c59f6` and released in version **v3.2**.

### Fix Details

The fix involved:

1. **Moving the Crontab fact to a dedicated module**: `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/facts/crontab.py`

2. **Adding regex-based detection of environment variables**:
   ```python
   _crontab_env_re = re.compile(r"^\s*([A-Z_]+)=(.*)$")
   ```

3. **Properly handling environment variable lines in the `process()` method**:
   ```python
   elif _crontab_env_re.match(line):
       # handle environment variables
       item = CrontabDict(
           env=line,
           comments=current_comments,
       )
       crons.add_item(item)
   ```

4. **Creating a new `CrontabFile` class** that can hold both command entries and environment variable entries, with proper type definitions in `CrontabDict`.

### Current Code Structure

The current implementation in `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/facts/crontab.py`:

- **Lines 103**: Regex pattern for environment variables: `_crontab_env_re = re.compile(r"^\s*([A-Z_]+)=(.*)$")`
- **Lines 155-195**: The `process()` method now properly handles:
  - Empty lines and comments (lines 161-163)
  - Special time entries like `@daily` (lines 165-172)
  - Environment variable lines (lines 174-180)
  - Standard cron entries (lines 181-194)

### Backwards Compatibility

The fix maintains backwards compatibility:
- `server.Crontab` still works (re-exported from `crontab` module at line 513 in `server.py`)
- `CrontabDict` includes new optional keys: `env`, `command` (was previously the dict key)
- `CrontabFile` class provides both old-style dict access (`.items()`) and new list access (`.commands`)

## Files Modified in the Fix

1. `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/facts/crontab.py` - New dedicated module (created)
2. `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/facts/server.py` - Now re-exports from crontab module
3. `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/operations/crontab.py` - New dedicated operations module
4. `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/operations/server.py` - Now re-exports from crontab module

## Recommendation for Issue Reporter

The issue reporter was using pyinfra v2.6.2. The fix is available in v3.2 and later. Upgrading to the latest version (v3.6 as of this writing) will resolve the issue.

## Test Coverage Gap

While the fix is in place, there is currently **no dedicated test case** for environment variables in crontab entries. A test case should be added at:
`/Users/nick/Dev/Webroot/pyinfra/src/tests/facts/crontab.Crontab/env_variables.json`

Suggested test case:
```json
{
    "command": "crontab -l || true",
    "requires_command": "crontab",
    "output": [
        "MAILTO=me@example.com",
        "CRON_TZ=Europe/Amsterdam",
        "",
        "@reboot echo \"rebooted\"",
        "* * * * * /usr/bin/command"
    ],
    "fact": [
        {
            "env": "MAILTO=me@example.com",
            "comments": []
        },
        {
            "env": "CRON_TZ=Europe/Amsterdam",
            "comments": [""]
        },
        {
            "command": "echo \"rebooted\"",
            "special_time": "@reboot",
            "comments": []
        },
        {
            "command": "/usr/bin/command",
            "minute": "*",
            "hour": "*",
            "month": "*",
            "day_of_month": "*",
            "day_of_week": "*",
            "comments": []
        }
    ]
}
```

## Related Issues

- Issue #1532: "Odd error on creating a cron job" - Related to `KeyError: 'command'` when environment variables are present with pyinfra name comments. Analysis in `/Users/nick/Dev/Webroot/pyinfra/src/.issues/1532.md`.
