Skip to content

Commit d058819

Browse files
committed
refactor: Extract DirectoryManager from ShellExecutor
- Create new DirectoryManager class - Move directory validation logic - Centralize path resolution - Update tests and maintain coverage
1 parent 2def658 commit d058819

File tree

2 files changed

+67
-29
lines changed

2 files changed

+67
-29
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import os
2+
from typing import Optional
3+
4+
5+
class DirectoryManager:
6+
"""
7+
Manages directory validation and path operations for shell command execution.
8+
"""
9+
10+
def validate_directory(self, directory: Optional[str]) -> None:
11+
"""
12+
Validate if the directory exists and is accessible.
13+
14+
Args:
15+
directory (Optional[str]): Directory path to validate
16+
17+
Raises:
18+
ValueError: If the directory doesn't exist, not absolute or is not accessible
19+
"""
20+
# make directory required
21+
if directory is None:
22+
raise ValueError("Directory is required")
23+
24+
# verify directory is absolute path
25+
if not os.path.isabs(directory):
26+
raise ValueError(f"Directory must be an absolute path: {directory}")
27+
28+
if not os.path.exists(directory):
29+
raise ValueError(f"Directory does not exist: {directory}")
30+
31+
if not os.path.isdir(directory):
32+
raise ValueError(f"Not a directory: {directory}")
33+
34+
if not os.access(directory, os.R_OK | os.X_OK):
35+
raise ValueError(f"Directory is not accessible: {directory}")
36+
37+
def get_absolute_path(self, path: str, base_directory: Optional[str] = None) -> str:
38+
"""
39+
Get absolute path by joining base directory with path if path is relative.
40+
41+
Args:
42+
path (str): The path to make absolute
43+
base_directory (Optional[str]): Base directory to join with relative paths
44+
45+
Returns:
46+
str: Absolute path
47+
"""
48+
if os.path.isabs(path):
49+
return path
50+
if not base_directory:
51+
return os.path.abspath(path)
52+
return os.path.join(base_directory, path)

src/mcp_shell_server/shell_executor.py

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import IO, Any, Dict, List, Optional, Tuple, Union
77

88
from mcp_shell_server.command_validator import CommandValidator
9+
from mcp_shell_server.directory_manager import DirectoryManager
910

1011

1112
class ShellExecutor:
@@ -15,9 +16,10 @@ class ShellExecutor:
1516

1617
def __init__(self):
1718
"""
18-
Initialize the executor with a command validator.
19+
Initialize the executor with a command validator and directory manager.
1920
"""
2021
self.validator = CommandValidator()
22+
self.directory_manager = DirectoryManager()
2123

2224
def _validate_redirection_syntax(self, command: List[str]) -> None:
2325
"""
@@ -221,18 +223,7 @@ def _validate_directory(self, directory: Optional[str]) -> None:
221223
Raises:
222224
ValueError: If the directory doesn't exist, not absolute or is not accessible
223225
"""
224-
# make directory required
225-
if directory is None:
226-
raise ValueError("Directory is required")
227-
# verify directory is absolute path
228-
if not os.path.isabs(directory):
229-
raise ValueError(f"Directory must be an absolute path: {directory}")
230-
if not os.path.exists(directory):
231-
raise ValueError(f"Directory does not exist: {directory}")
232-
if not os.path.isdir(directory):
233-
raise ValueError(f"Not a directory: {directory}")
234-
if not os.access(directory, os.R_OK | os.X_OK):
235-
raise ValueError(f"Directory is not accessible: {directory}")
226+
self.directory_manager.validate_directory(directory)
236227

237228
def _validate_no_shell_operators(self, cmd: str) -> None:
238229
"""Validate that the command does not contain shell operators"""
@@ -465,10 +456,8 @@ async def execute(
465456

466457
# Update stdin from redirects if present
467458
if redirects["stdin"]:
468-
stdin_path = (
469-
os.path.join(directory, str(redirects["stdin"]))
470-
if directory and redirects["stdin"]
471-
else str(redirects["stdin"])
459+
stdin_path = self.directory_manager.get_absolute_path(
460+
str(redirects["stdin"]), directory
472461
)
473462
try:
474463
with open(stdin_path, "r") as f:
@@ -479,10 +468,8 @@ async def execute(
479468
# Setup output redirection
480469
stdout_handle: Union[int, IO[Any]] = asyncio.subprocess.PIPE
481470
if redirects["stdout"]:
482-
stdout_path = (
483-
os.path.join(directory, str(redirects["stdout"]))
484-
if directory and redirects["stdout"]
485-
else str(redirects["stdout"])
471+
stdout_path = self.directory_manager.get_absolute_path(
472+
str(redirects["stdout"]), directory
486473
)
487474
mode = "a" if redirects["stdout_append"] else "w"
488475
try:
@@ -593,19 +580,18 @@ async def _execute_pipeline(
593580
parsed_commands.append(parsed_cmd)
594581

595582
if commands.index(cmd) == 0 and redirects["stdin"]:
596-
stdin_path = (
597-
os.path.join(directory, str(redirects["stdin"]))
598-
if directory
599-
else str(redirects["stdin"])
583+
stdin_path = self.directory_manager.get_absolute_path(
584+
str(redirects["stdin"]), directory
600585
)
601586
with open(stdin_path, "r") as f:
602587
first_stdin = f.read().encode()
603588

604589
if commands.index(cmd) == len(commands) - 1 and redirects["stdout"]:
605-
stdout_path = (
606-
os.path.join(directory, str(redirects["stdout"]))
607-
if directory
608-
else str(redirects["stdout"])
590+
stdout_path = self.directory_manager.get_absolute_path(
591+
str(redirects["stdout"]), directory
592+
)
593+
last_stdout = open(
594+
stdout_path, "a" if redirects["stdout_append"] else "w"
609595
)
610596
last_stdout = open(
611597
stdout_path, "a" if redirects["stdout_append"] else "w"

0 commit comments

Comments
 (0)