Skip to content

Commit 63191ed

Browse files
committed
Add support for environment variable expansion in SSH config
This commit adds support for environment variable substitutions for select string-valued SSH config options. Any option which previously supported percent expansion can now also support environment variable expansion using "${varname}". Thanks go to Aleksandr Ilin for pointing out that OpenSSH supports more than just a boolean value for the ForwardAgent config option and that a string value here supports percent and environment variable expansion.
1 parent 1d9e5b8 commit 63191ed

File tree

2 files changed

+47
-28
lines changed

2 files changed

+47
-28
lines changed

asyncssh/config.py

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
ConfigPaths = Union[None, FilePath, Sequence[FilePath]]
4242

4343

44+
_token_pattern = re.compile(r'%(.)')
45+
_env_pattern = re.compile(r'\${(.*)}')
46+
47+
4448
def _exec(cmd: str) -> bool:
4549
"""Execute a command and return if exit status is 0"""
4650

@@ -93,36 +97,38 @@ def _set_tokens(self) -> None:
9397

9498
raise NotImplementedError
9599

96-
def _expand_val(self, value: str) -> str:
97-
"""Perform percent token expansion on a string"""
100+
def _expand_token(self, match):
101+
"""Expand a percent token reference"""
102+
103+
try:
104+
token = match.group(1)
105+
return self._tokens[token]
106+
except KeyError:
107+
if token == 'd':
108+
raise ConfigParseError('Home directory is '
109+
'not available') from None
110+
elif token == 'i':
111+
raise ConfigParseError('User id not available') from None
112+
else:
113+
raise ConfigParseError('Invalid token expansion: ' +
114+
token) from None
98115

99-
last_idx = 0
100-
result: List[str] = []
116+
@staticmethod
117+
def _expand_env(match):
118+
"""Expand an environment variable reference"""
101119

102-
for match in re.finditer(r'%', value):
103-
idx = match.start()
120+
try:
121+
var = match.group(1)
122+
return os.environ[var]
123+
except KeyError:
124+
raise ConfigParseError('Invalid environment expansion: ' +
125+
var) from None
104126

105-
if idx < last_idx:
106-
continue
127+
def _expand_val(self, value: str) -> str:
128+
"""Perform percent token and environment expansion on a string"""
107129

108-
try:
109-
token = value[idx+1]
110-
result.extend([value[last_idx:idx], self._tokens[token]])
111-
last_idx = idx + 2
112-
except IndexError:
113-
raise ConfigParseError('Invalid token substitution') from None
114-
except KeyError:
115-
if token == 'd':
116-
raise ConfigParseError('Home directory is '
117-
'not available') from None
118-
elif token == 'i':
119-
raise ConfigParseError('User id not available') from None
120-
else:
121-
raise ConfigParseError('Invalid token substitution: ' +
122-
value[idx+1]) from None
123-
124-
result.append(value[last_idx:])
125-
return ''.join(result)
130+
return _env_pattern.sub(self._expand_env,
131+
_token_pattern.sub(self._expand_token, value))
126132

127133
def _include(self, option: str, args: List[str]) -> None:
128134
"""Read config from a list of other config files"""

tests/test_config.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,12 +514,25 @@ def test_invalid_percent_expansion(self):
514514

515515
for desc, config_data in (
516516
('Bad token in hostname', 'Hostname %p'),
517-
('Invalid token', 'IdentityFile %x'),
518-
('Percent at end', 'IdentityFile %')):
517+
('Invalid token', 'IdentityFile %x')):
519518
with self.subTest(desc):
520519
with self.assertRaises(asyncssh.ConfigParseError):
521520
self._parse_config(config_data)
522521

522+
def test_env_expansion(self):
523+
"""Test environment variable expansion"""
524+
525+
config = self._parse_config('RemoteCommand ${HOME}/.ssh')
526+
527+
self.assertEqual(config.get('RemoteCommand'), './.ssh')
528+
529+
def test_invalid_env_expansion(self):
530+
"""Test invalid environment variable expansion"""
531+
532+
with self.assertRaises(asyncssh.ConfigParseError):
533+
self._parse_config('RemoteCommand ${XXX}')
534+
535+
523536
class _TestServerConfig(_TestConfig):
524537
"""Unit tests for server config objects"""
525538

0 commit comments

Comments
 (0)