Python · 3727 bytes Raw Blame History
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)