tenseleyflow/bensch / d92057f

Browse files

add shell profile system

framework/profile.py — loads YAML profiles, auto-detects profile
from shell binary name, provides capability queries and rc-disable
env generation.

6 profiles: bash, zsh, dash, ksh, fortsh, generic.

Each profile describes: prompt_pattern, prompt_set_command,
mode_reset_command, rc_disable (flags + env), test_mode_env,
history_disable, capabilities, and suite skip/extra lists.
Authored by espadonne
SHA
d92057f7c33168282e32a25bd25b896390548a2b
Parents
ccd7ddb
Tree
c255f46

8 changed files

StatusFile+-
A framework/profile.py 134 0
D profiles/.gitkeep 0 0
A profiles/bash.yaml 26 0
A profiles/dash.yaml 30 0
A profiles/fortsh.yaml 28 0
A profiles/generic.yaml 27 0
A profiles/ksh.yaml 25 0
A profiles/zsh.yaml 26 0
framework/profile.pyadded
@@ -0,0 +1,134 @@
1
+"""
2
+Shell profile loader for bensch.
3
+
4
+Profiles describe a shell's capabilities, prompt format, rc-disable
5
+mechanism, and test mode configuration. The runner uses this to
6
+adapt its behavior for any shell.
7
+"""
8
+
9
+import os
10
+import yaml
11
+from pathlib import Path
12
+from typing import Dict, Any, Optional
13
+
14
+
15
+DEFAULT_PROFILE = {
16
+    "name": "generic",
17
+    "display_name": "Unknown Shell",
18
+    "prompt_pattern": r"\$ ",
19
+    "prompt_set_command": "PS1='$ '",
20
+    "mode_reset_command": "",
21
+    "rc_disable": {"flags": [], "env": {}},
22
+    "test_mode_env": {},
23
+    "history_disable": {"env": {"HISTFILE": "/dev/null"}},
24
+    "capabilities": {
25
+        "readline": False,
26
+        "vi_mode": False,
27
+        "job_control": True,
28
+        "command_completion": False,
29
+        "programmable_completion": False,
30
+        "coproc": False,
31
+        "arrays": False,
32
+        "associative_arrays": False,
33
+    },
34
+    "suites": {"skip": [], "extra": []},
35
+}
36
+
37
+
38
+def find_profiles_dir() -> Path:
39
+    """Find the profiles/ directory relative to this file."""
40
+    return Path(__file__).parent.parent / "profiles"
41
+
42
+
43
+def load_profile(name: str) -> Dict[str, Any]:
44
+    """
45
+    Load a shell profile by name.
46
+
47
+    Args:
48
+        name: Profile name (e.g., 'bash', 'fortsh', 'generic')
49
+
50
+    Returns:
51
+        Profile dict with all fields populated (defaults merged)
52
+    """
53
+    profiles_dir = find_profiles_dir()
54
+    profile_path = profiles_dir / f"{name}.yaml"
55
+
56
+    if not profile_path.exists():
57
+        # Fall back to generic
58
+        profile_path = profiles_dir / "generic.yaml"
59
+        if not profile_path.exists():
60
+            return dict(DEFAULT_PROFILE)
61
+
62
+    with open(profile_path) as f:
63
+        profile = yaml.safe_load(f) or {}
64
+
65
+    # Merge with defaults (profile overrides defaults)
66
+    merged = dict(DEFAULT_PROFILE)
67
+    for key, value in profile.items():
68
+        if isinstance(value, dict) and key in merged and isinstance(merged[key], dict):
69
+            merged[key] = {**merged[key], **value}
70
+        else:
71
+            merged[key] = value
72
+
73
+    return merged
74
+
75
+
76
+def detect_profile(shell_path: str) -> str:
77
+    """
78
+    Auto-detect profile name from shell binary path.
79
+
80
+    Args:
81
+        shell_path: Path to the shell binary
82
+
83
+    Returns:
84
+        Profile name (e.g., 'bash', 'zsh', 'generic')
85
+    """
86
+    basename = os.path.basename(shell_path).lower()
87
+
88
+    profiles_dir = find_profiles_dir()
89
+    available = [p.stem for p in profiles_dir.glob("*.yaml")]
90
+
91
+    # Direct match
92
+    for name in available:
93
+        if basename.startswith(name):
94
+            return name
95
+
96
+    # Common aliases
97
+    aliases = {
98
+        "sh": "dash",
99
+        "ksh93": "ksh",
100
+        "mksh": "ksh",
101
+        "pdksh": "ksh",
102
+    }
103
+    if basename in aliases:
104
+        return aliases[basename]
105
+
106
+    return "generic"
107
+
108
+
109
+def get_rc_disable_env(profile: Dict[str, Any]) -> str:
110
+    """
111
+    Build the RC_DISABLE_ENV string for shell script tests.
112
+
113
+    Returns a string like 'BASH_ENV= ' or 'FORTSH_RC_FILE=/dev/null '
114
+    suitable for prefixing shell commands.
115
+    """
116
+    env_vars = profile.get("rc_disable", {}).get("env", {})
117
+    parts = [f"{k}={v}" for k, v in env_vars.items()]
118
+    return " ".join(parts)
119
+
120
+
121
+def get_rc_disable_flags(profile: Dict[str, Any]) -> list:
122
+    """Get CLI flags to disable rc file loading."""
123
+    return profile.get("rc_disable", {}).get("flags", [])
124
+
125
+
126
+def should_skip_suite(profile: Dict[str, Any], suite_name: str) -> bool:
127
+    """Check if a suite should be skipped for this shell."""
128
+    skip_list = profile.get("suites", {}).get("skip", [])
129
+    return suite_name in skip_list
130
+
131
+
132
+def has_capability(profile: Dict[str, Any], capability: str) -> bool:
133
+    """Check if the shell has a specific capability."""
134
+    return profile.get("capabilities", {}).get(capability, False)
profiles/.gitkeepdeleted
profiles/bash.yamladded
@@ -0,0 +1,26 @@
1
+name: bash
2
+display_name: "GNU Bash"
3
+prompt_pattern: '\$ '
4
+prompt_set_command: "PS1='$ '"
5
+mode_reset_command: "set -o emacs"
6
+rc_disable:
7
+  flags: ["--norc", "--noprofile"]
8
+  env:
9
+    BASH_ENV: ""
10
+test_mode_env: {}
11
+history_disable:
12
+  env:
13
+    HISTFILE: /dev/null
14
+    HISTSIZE: "0"
15
+capabilities:
16
+  readline: true
17
+  vi_mode: true
18
+  job_control: true
19
+  command_completion: true
20
+  programmable_completion: true
21
+  coproc: true
22
+  arrays: true
23
+  associative_arrays: true
24
+suites:
25
+  skip: []
26
+  extra: []
profiles/dash.yamladded
@@ -0,0 +1,30 @@
1
+name: dash
2
+display_name: "Debian Almquist Shell"
3
+prompt_pattern: '\$ '
4
+prompt_set_command: "PS1='$ '"
5
+mode_reset_command: ""
6
+rc_disable:
7
+  flags: []
8
+  env:
9
+    ENV: ""
10
+test_mode_env: {}
11
+history_disable:
12
+  env:
13
+    HISTFILE: /dev/null
14
+capabilities:
15
+  readline: false
16
+  vi_mode: false
17
+  job_control: true
18
+  command_completion: false
19
+  programmable_completion: false
20
+  coproc: false
21
+  arrays: false
22
+  associative_arrays: false
23
+suites:
24
+  skip:
25
+    - "interactive/editing"
26
+    - "interactive/completion"
27
+    - "interactive/history"
28
+    - "interactive/signals"
29
+    - "interactive/stress"
30
+  extra: []
profiles/fortsh.yamladded
@@ -0,0 +1,28 @@
1
+name: fortsh
2
+display_name: "Fortran Shell"
3
+prompt_pattern: '> '
4
+prompt_set_command: "PS1='> '"
5
+mode_reset_command: "set -o emacs"
6
+rc_disable:
7
+  flags: []
8
+  env:
9
+    FORTSH_RC_FILE: /dev/null
10
+test_mode_env:
11
+  FORTSH_MINIMAL_ECHO: "1"
12
+  FORTSH_TEST_MODE: "1"
13
+history_disable:
14
+  env:
15
+    HISTFILE: /dev/null
16
+capabilities:
17
+  readline: true
18
+  vi_mode: true
19
+  job_control: true
20
+  command_completion: true
21
+  programmable_completion: true
22
+  coproc: true
23
+  arrays: true
24
+  associative_arrays: false
25
+suites:
26
+  skip: []
27
+  extra:
28
+    - "builtins/extended/fortsh"
profiles/generic.yamladded
@@ -0,0 +1,27 @@
1
+name: generic
2
+display_name: "POSIX Shell"
3
+prompt_pattern: '\$ '
4
+prompt_set_command: "PS1='$ '"
5
+mode_reset_command: ""
6
+rc_disable:
7
+  flags: []
8
+  env: {}
9
+test_mode_env: {}
10
+history_disable:
11
+  env:
12
+    HISTFILE: /dev/null
13
+capabilities:
14
+  readline: false
15
+  vi_mode: false
16
+  job_control: true
17
+  command_completion: false
18
+  programmable_completion: false
19
+  coproc: false
20
+  arrays: false
21
+  associative_arrays: false
22
+suites:
23
+  skip:
24
+    - "interactive/editing"
25
+    - "interactive/completion"
26
+    - "interactive/history"
27
+  extra: []
profiles/ksh.yamladded
@@ -0,0 +1,25 @@
1
+name: ksh
2
+display_name: "Korn Shell"
3
+prompt_pattern: '\$ '
4
+prompt_set_command: "PS1='$ '"
5
+mode_reset_command: "set -o emacs"
6
+rc_disable:
7
+  flags: []
8
+  env:
9
+    ENV: ""
10
+test_mode_env: {}
11
+history_disable:
12
+  env:
13
+    HISTFILE: /dev/null
14
+capabilities:
15
+  readline: true
16
+  vi_mode: true
17
+  job_control: true
18
+  command_completion: true
19
+  programmable_completion: false
20
+  coproc: true
21
+  arrays: true
22
+  associative_arrays: true
23
+suites:
24
+  skip: []
25
+  extra: []
profiles/zsh.yamladded
@@ -0,0 +1,26 @@
1
+name: zsh
2
+display_name: "Z Shell"
3
+prompt_pattern: '% '
4
+prompt_set_command: "PS1='%% '"
5
+mode_reset_command: "bindkey -e"
6
+rc_disable:
7
+  flags: ["--no-rcs"]
8
+  env:
9
+    ZDOTDIR: /nonexistent
10
+test_mode_env: {}
11
+history_disable:
12
+  env:
13
+    HISTFILE: /dev/null
14
+    HISTSIZE: "0"
15
+capabilities:
16
+  readline: true
17
+  vi_mode: true
18
+  job_control: true
19
+  command_completion: true
20
+  programmable_completion: true
21
+  coproc: true
22
+  arrays: true
23
+  associative_arrays: true
24
+suites:
25
+  skip: []
26
+  extra: []