2026-03-07 05:43:51 +00:00
#!/usr/bin/env python3
"""
2026-03-10 07:30:42 +08:00
Linux 命令沙盒模拟器 ( 增强版 )
2026-03-07 05:43:51 +00:00
- 零风险 : 不真实执行系统命令
2026-03-10 07:30:42 +08:00
- 可变虚拟文件系统 : 支持创建 / 删除 / 移动 / 改权限
- 覆盖更多 Linux 常见命令 , 尽量让课程任务可跑通
2026-03-07 05:43:51 +00:00
"""
2026-03-10 07:30:42 +08:00
from __future__ import annotations
from copy import deepcopy
from fnmatch import fnmatch
from datetime import datetime
2026-03-07 05:43:51 +00:00
import re
2026-03-10 07:30:42 +08:00
BASE_FS = {
" / " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " users " , " projects " , " logs " , " sandbox_tips " , " tmp " , " etc " , " var " , " home " , " bin " , " usr " ] } ,
" /home " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " sandbox_user " ] } ,
" /home/sandbox_user " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " notes.txt " , " .bashrc " ] } ,
" /home/sandbox_user/notes.txt " : { " type " : " file " , " perm " : " 644 " , " content " : " Practice Linux every day. \n " } ,
" /home/sandbox_user/.bashrc " : { " type " : " file " , " perm " : " 644 " , " content " : " alias ll= ' ls -l ' \n export PATH=/usr/local/bin:/usr/bin:/bin \n " } ,
" /tmp " : { " type " : " dir " , " perm " : " 777 " , " children " : [ ] } ,
" /users " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " alice.txt " , " bob.txt " , " charlie " ] } ,
" /users/alice.txt " : { " type " : " file " , " perm " : " 644 " , " content " : " Hi, I ' m Alice. I love Linux! \n " } ,
" /users/bob.txt " : { " type " : " file " , " perm " : " 644 " , " content " : " Hello from Bob. Learning Linux daily. \n " } ,
" /users/charlie " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " profile.md " , " skills.txt " ] } ,
" /users/charlie/profile.md " : { " type " : " file " , " perm " : " 644 " , " content " : " # Charlie \n Loves open source and Linux commands. \n " } ,
" /users/charlie/skills.txt " : { " type " : " file " , " perm " : " 644 " , " content " : " Linux \n Python \n Git \n Docker \n " } ,
" /projects " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " web " , " backend " ] } ,
" /projects/web " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " index.html " , " style.css " ] } ,
" /projects/web/index.html " : { " type " : " file " , " perm " : " 644 " , " content " : " <html><body>Hello Linux Sandbox</body></html> \n " } ,
" /projects/web/style.css " : { " type " : " file " , " perm " : " 644 " , " content " : " body { font-family: sans-serif; } \n " } ,
" /projects/backend " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " app.py " , " model.py " , " utils.py " ] } ,
" /projects/backend/app.py " : { " type " : " file " , " perm " : " 644 " , " content " : " def main(): \n print( ' Hello, Linux! ' ) \n \n if __name__ == ' __main__ ' : \n main() \n " } ,
" /projects/backend/model.py " : { " type " : " file " , " perm " : " 644 " , " content " : " class User: \n def __init__(self, name): \n self.name = name \n " } ,
" /projects/backend/utils.py " : { " type " : " file " , " perm " : " 644 " , " content " : " def log(msg): \n print(f ' [LOG] {msg} ' ) \n " } ,
" /logs " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " access.log " , " error.log " ] } ,
" /logs/access.log " : { " type " : " file " , " perm " : " 644 " , " content " : " 2024-01-01 10:00 GET /index.html 200 \n 2024-01-01 10:01 POST /api/login 200 \n 2024-01-01 10:02 GET /about 404 \n " } ,
" /logs/error.log " : { " type " : " file " , " perm " : " 644 " , " content " : " 2024-01-01 10:02 ERROR Page not found \n 2024-01-01 10:03 WARNING High memory usage \n " } ,
" /sandbox_tips " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " welcome.txt " ] } ,
" /sandbox_tips/welcome.txt " : { " type " : " file " , " perm " : " 644 " , " content " : " Welcome to Linux Sandbox! \n Try ls, cat, grep, find, ps, df and more. \n " } ,
" /etc " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " passwd " , " group " , " hosts " , " nginx.conf " , " ssh.conf " , " app.conf " , " skel " ] } ,
" /etc/passwd " : { " type " : " file " , " perm " : " 644 " , " content " : " root:x:0:0:root:/root:/bin/bash \n daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin \n sandbox_user:x:1000:1000:Sandbox User:/home/sandbox_user:/bin/bash \n " } ,
" /etc/group " : { " type " : " file " , " perm " : " 644 " , " content " : " root:x:0: \n users:x:100:alice,bob \n docker:x:999:sandbox_user \n " } ,
" /etc/hosts " : { " type " : " file " , " perm " : " 644 " , " content " : " 127.0.0.1 localhost \n 127.0.1.1 sandbox \n " } ,
" /etc/nginx.conf " : { " type " : " file " , " perm " : " 644 " , " content " : " user nginx; \n worker_processes auto; \n " } ,
" /etc/ssh.conf " : { " type " : " file " , " perm " : " 644 " , " content " : " Port 22 \n PermitRootLogin no \n " } ,
" /etc/app.conf " : { " type " : " file " , " perm " : " 644 " , " content " : " APP_ENV=prod \n APP_DEBUG=false \n " } ,
" /etc/skel " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " .bash_logout " , " .bashrc " ] } ,
" /etc/skel/.bash_logout " : { " type " : " file " , " perm " : " 644 " , " content " : " # logout \n " } ,
" /etc/skel/.bashrc " : { " type " : " file " , " perm " : " 644 " , " content " : " # skeleton bashrc \n " } ,
" /var " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " log " ] } ,
" /var/log " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " syslog " , " auth.log " , " nginx " ] } ,
" /var/log/syslog " : { " type " : " file " , " perm " : " 644 " , " content " : " Mar 06 10:00 kernel: system boot complete \n Mar 06 10:02 app: error database connection timeout \n Mar 06 10:04 sshd: Accepted password for sandbox_user \n Mar 06 10:05 nginx: worker started \n Mar 06 10:06 app: error failed to load config \n " } ,
" /var/log/auth.log " : { " type " : " file " , " perm " : " 640 " , " content " : " Mar 06 10:01 sshd[123]: Failed password \n Mar 06 10:04 sshd[124]: Accepted password for sandbox_user \n " } ,
" /var/log/nginx " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " access.log " , " error.log " ] } ,
" /var/log/nginx/access.log " : { " type " : " file " , " perm " : " 644 " , " content " : " 127.0.0.1 - - [06/Mar/2026] \" GET / HTTP/1.1 \" 200 612 \n " } ,
" /var/log/nginx/error.log " : { " type " : " file " , " perm " : " 644 " , " content " : " 2026/03/06 [error] connect() failed \n " } ,
" /bin " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " ls " , " cat " , " grep " , " find " , " bash " ] } ,
" /usr " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " bin " ] } ,
" /usr/bin " : { " type " : " dir " , " perm " : " 755 " , " children " : [ " vim " , " curl " , " wget " , " python3 " ] } ,
2026-03-07 05:43:51 +00:00
}
2026-03-10 07:30:42 +08:00
COMMAND_INDEX = {
" ls " : " /bin/ls " ,
" cat " : " /bin/cat " ,
" grep " : " /bin/grep " ,
" find " : " /bin/find " ,
" bash " : " /bin/bash " ,
" vim " : " /usr/bin/vim " ,
" curl " : " /usr/bin/curl " ,
" wget " : " /usr/bin/wget " ,
" python3 " : " /usr/bin/python3 " ,
2026-03-07 05:43:51 +00:00
}
2026-03-10 07:30:42 +08:00
2026-03-07 05:43:51 +00:00
class LinuxSandbox :
def __init__ ( self ) :
2026-03-10 07:30:42 +08:00
self . reset ( )
def reset ( self ) :
self . fs = deepcopy ( BASE_FS )
self . cwd = " / "
self . home = " /home/sandbox_user "
self . user = " sandbox_user "
self . history : list [ str ] = [ ]
self . env = { " HOME " : self . home , " USER " : self . user , " PATH " : " /usr/local/bin:/usr/bin:/bin " , " SHELL " : " /bin/bash " }
self . aliases = { " ll " : " ls -l " }
# ---------- FS helpers ----------
def resolve_path ( self , path : str | None ) - > str :
if not path or path == " . " :
2026-03-07 05:43:51 +00:00
return self . cwd
2026-03-10 07:30:42 +08:00
if path == " ~ " :
return self . home
if path . startswith ( " ~/ " ) :
return self . home + path [ 1 : ]
if not path . startswith ( " / " ) :
base = self . cwd . rstrip ( " / " ) or " / "
path = f " { base } / { path } " if base != " / " else f " / { path } "
parts = [ ]
for part in path . split ( " / " ) :
if part in ( " " , " . " ) :
continue
if part == " .. " :
if parts :
parts . pop ( )
else :
parts . append ( part )
return " / " + " / " . join ( parts )
def get_node ( self , path : str ) :
return self . fs . get ( path )
def exists ( self , path : str ) - > bool :
return self . resolve_path ( path ) in self . fs
def is_executable ( self , path : str ) - > bool :
node = self . get_node ( self . resolve_path ( path ) )
return bool ( node and len ( node . get ( " perm " , " " ) ) > = 3 and node [ " perm " ] [ 2 ] == " 1 " or node and " x " in node . get ( " perm_human " , " " ) )
def _perm_human ( self , perm : str , is_dir : bool ) - > str :
mapping = {
" 0 " : " --- " , " 1 " : " --x " , " 2 " : " -w- " , " 3 " : " -wx " ,
" 4 " : " r-- " , " 5 " : " r-x " , " 6 " : " rw- " , " 7 " : " rwx " ,
}
if perm . isdigit ( ) and len ( perm ) == 3 :
return ( " d " if is_dir else " - " ) + " " . join ( mapping . get ( x , " --- " ) for x in perm )
return ( " d " if is_dir else " - " ) + perm
def _ensure_dir ( self , path : str ) :
if path in self . fs :
return
parent = self . parent_dir ( path )
if parent != path and parent not in self . fs :
self . _ensure_dir ( parent )
self . fs [ path ] = { " type " : " dir " , " perm " : " 755 " , " children " : [ ] }
self . _add_child ( parent , path . split ( " / " ) [ - 1 ] )
def _add_child ( self , parent : str , name : str ) :
node = self . fs . get ( parent )
if node and node [ " type " ] == " dir " :
node . setdefault ( " children " , [ ] )
if name not in node [ " children " ] :
node [ " children " ] . append ( name )
def _remove_child ( self , parent : str , name : str ) :
node = self . fs . get ( parent )
if node and node [ " type " ] == " dir " and name in node . get ( " children " , [ ] ) :
node [ " children " ] . remove ( name )
def parent_dir ( self , path : str ) - > str :
if path == " / " :
return " / "
parts = path . rstrip ( " / " ) . split ( " / " )
if len ( parts ) < = 2 :
return " / "
return " / " + " / " . join ( parts [ 1 : - 1 ] )
def _copy_node ( self , src : str , dst : str ) :
node = deepcopy ( self . fs [ src ] )
self . fs [ dst ] = node
self . _add_child ( self . parent_dir ( dst ) , dst . split ( " / " ) [ - 1 ] )
if node [ " type " ] == " dir " :
old_children = list ( node . get ( " children " , [ ] ) )
node [ " children " ] = [ ]
for child in old_children :
self . _copy_node ( f " { src . rstrip ( ' / ' ) } / { child } " , f " { dst . rstrip ( ' / ' ) } / { child } " )
def _delete_node ( self , path : str ) :
node = self . fs . get ( path )
2026-03-07 05:43:51 +00:00
if not node :
2026-03-10 07:30:42 +08:00
return
if node [ " type " ] == " dir " :
for child in list ( node . get ( " children " , [ ] ) ) :
self . _delete_node ( f " { path . rstrip ( ' / ' ) } / { child } " )
parent = self . parent_dir ( path )
self . _remove_child ( parent , path . split ( " / " ) [ - 1 ] )
self . fs . pop ( path , None )
def _safe_shell_split ( self , cmd : str ) - > list [ str ] :
matches = re . findall ( r " ' [^ ' ]* ' | \" [^ \" ]* \" | \ S+ " , cmd )
return [ m [ 1 : - 1 ] if ( m . startswith ( " ' " ) and m . endswith ( " ' " ) ) or ( m . startswith ( ' " ' ) and m . endswith ( ' " ' ) ) else m for m in matches ]
def _expand_alias ( self , cmd : str ) - > str :
if not cmd :
return cmd
head = cmd . split ( ) [ 0 ]
if head in self . aliases :
rest = cmd [ len ( head ) : ] . lstrip ( )
return f " { self . aliases [ head ] } { rest } " . strip ( )
return cmd
def _expand_env ( self , text : str ) - > str :
for key , value in self . env . items ( ) :
text = text . replace ( f " $ { key } " , value )
return text
# ---------- Commands ----------
def _simulate_ls ( self , args : list [ str ] ) - > str :
flags = { a for a in args if a . startswith ( " - " ) }
targets = [ a for a in args if not a . startswith ( " - " ) ] or [ self . cwd ]
lines = [ ]
for target in targets :
path = self . resolve_path ( target )
node = self . get_node ( path )
if not node :
lines . append ( f " ls: cannot access ' { target } ' : No such file or directory " )
continue
if node [ " type " ] == " file " :
if " -l " in flags :
lines . append ( f " { self . _perm_human ( node . get ( ' perm ' , ' 644 ' ) , False ) } 1 sandbox sandbox { len ( node . get ( ' content ' , ' ' ) ) : >4 } Mar 6 10:00 { path . split ( ' / ' ) [ - 1 ] } " )
else :
lines . append ( path . split ( " / " ) [ - 1 ] )
continue
children = sorted ( node . get ( " children " , [ ] ) )
if " -a " in flags :
children = [ " . " , " .. " ] + children
if " -l " in flags :
lines . append ( " total " + str ( max ( len ( children ) , 1 ) ) )
for child in children :
if child in ( " . " , " .. " ) :
display_path = path if child == " . " else self . parent_dir ( path )
cnode = self . get_node ( display_path ) or { " type " : " dir " , " perm " : " 755 " , " children " : [ ] }
name = child
else :
display_path = f " { path . rstrip ( ' / ' ) } / { child } " if path != " / " else f " / { child } "
cnode = self . get_node ( display_path )
name = child
if not cnode :
continue
is_dir = cnode [ " type " ] == " dir "
size = len ( cnode . get ( " content " , " " ) ) if not is_dir else max ( 4 , len ( cnode . get ( " children " , [ ] ) ) * 4 )
lines . append ( f " { self . _perm_human ( cnode . get ( ' perm ' , ' 755 ' if is_dir else ' 644 ' ) , is_dir ) } 1 sandbox sandbox { size : >4 } Mar 6 10:00 { name } " )
else :
lines . append ( " " . join ( children ) )
return " \n " . join ( line for line in lines if line != " " )
2026-03-07 05:43:51 +00:00
def _simulate_pwd ( self ) - > str :
return self . cwd
2026-03-10 07:30:42 +08:00
def _simulate_cd ( self , args : list [ str ] ) - > str :
target = args [ 0 ] if args else self . home
if target == " - " :
target = self . home
path = self . resolve_path ( target )
node = self . get_node ( path )
if not node or node [ " type " ] != " dir " :
return f " cd: { target } : No such file or directory "
self . cwd = path
return self . cwd
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
def _simulate_echo ( self , args : list [ str ] ) - > str :
text = " " . join ( args )
return self . _expand_env ( text )
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
def _simulate_cat ( self , args : list [ str ] ) - > str :
if not args :
return " cat: missing operand "
number = " -n " in args
files = [ a for a in args if not a . startswith ( " - " ) ]
out = [ ]
for path_arg in files :
path = self . resolve_path ( path_arg )
node = self . get_node ( path )
if not node :
out . append ( f " cat: { path_arg } : No such file or directory " )
continue
if node [ " type " ] != " file " :
out . append ( f " cat: { path_arg } : Is a directory " )
continue
content = node . get ( " content " , " " )
if number :
content = " \n " . join ( f " { i + 1 : >6 } { line } " for i , line in enumerate ( content . rstrip ( " \n " ) . split ( " \n " ) ) )
out . append ( content . rstrip ( " \n " ) )
return " \n " . join ( out )
def _simulate_head_tail ( self , cmd_name : str , args : list [ str ] ) - > str :
count = 10
files = [ ]
2026-03-07 05:43:51 +00:00
i = 0
while i < len ( args ) :
2026-03-10 07:30:42 +08:00
if args [ i ] in ( " -n " , ) :
count = int ( args [ i + 1 ] )
i + = 2
elif args [ i ] . startswith ( " - " ) and args [ i ] [ 1 : ] . isdigit ( ) :
count = int ( args [ i ] [ 1 : ] )
i + = 1
elif args [ i ] == " -f " :
files . append ( " -f " )
2026-03-07 05:43:51 +00:00
i + = 1
else :
2026-03-10 07:30:42 +08:00
files . append ( args [ i ] )
2026-03-07 05:43:51 +00:00
i + = 1
2026-03-10 07:30:42 +08:00
if " -f " in files :
target = files [ - 1 ] if len ( files ) > 1 else " /var/log/syslog "
return f " ==> { target } <== \n (log streaming simulated) \n Press Ctrl+C to exit "
if not files :
return f " { cmd_name } : missing file operand "
node = self . get_node ( self . resolve_path ( files [ - 1 ] ) )
if not node or node [ " type " ] != " file " :
return f " { cmd_name } : cannot open ' { files [ - 1 ] } ' "
lines = node . get ( " content " , " " ) . rstrip ( " \n " ) . split ( " \n " )
picked = lines [ : count ] if cmd_name == " head " else lines [ - count : ]
return " \n " . join ( picked )
def _simulate_grep ( self , args : list [ str ] ) - > str :
flags = { a for a in args if a . startswith ( " - " ) }
items = [ a for a in args if not a . startswith ( " - " ) ]
if len ( items ) < 2 :
2026-03-07 05:43:51 +00:00
return " grep: pattern or file missing "
2026-03-10 07:30:42 +08:00
pattern , * targets = items
insensitive = " -i " in flags
recursive = " -r " in flags
show_num = " -n " in flags
invert = " -v " in flags
2026-03-07 05:43:51 +00:00
results = [ ]
2026-03-10 07:30:42 +08:00
for target in targets :
resolved = self . resolve_path ( target )
candidate_paths = [ ]
if " * " in target :
regex = target . replace ( " * " , " .* " )
candidate_paths = [ p for p in self . fs if re . fullmatch ( self . resolve_path ( regex ) . replace ( " . " , r " \ . " ) . replace ( " .* " , " .* " ) , p ) ]
elif recursive and self . get_node ( resolved ) and self . get_node ( resolved ) [ " type " ] == " dir " :
candidate_paths = [ p for p in self . fs if p . startswith ( resolved . rstrip ( " / " ) + " / " ) and self . fs [ p ] [ " type " ] == " file " ]
2026-03-07 05:43:51 +00:00
else :
2026-03-10 07:30:42 +08:00
candidate_paths = [ resolved ]
for path in candidate_paths :
node = self . get_node ( path )
if not node or node [ " type " ] != " file " :
continue
for idx , line in enumerate ( node . get ( " content " , " " ) . splitlines ( ) , 1 ) :
hay = line . lower ( ) if insensitive else line
needle = pattern . lower ( ) if insensitive else pattern
matched = needle in hay
if invert :
matched = not matched
if matched :
prefix = [ ]
if recursive or len ( candidate_paths ) > 1 :
prefix . append ( path )
if show_num :
prefix . append ( str ( idx ) )
results . append ( ( " : " . join ( prefix ) + ( " : " if prefix else " " ) ) + line )
return " \n " . join ( results )
def _simulate_find ( self , args : list [ str ] ) - > str :
2026-03-07 05:43:51 +00:00
path = self . cwd
file_type = None
2026-03-10 07:30:42 +08:00
name_pattern = " * "
has_exec = False
2026-03-07 05:43:51 +00:00
i = 0
while i < len ( args ) :
arg = args [ i ]
2026-03-10 07:30:42 +08:00
if arg == " -type " and i + 1 < len ( args ) :
file_type = args [ i + 1 ]
i + = 2
elif arg == " -name " and i + 1 < len ( args ) :
name_pattern = args [ i + 1 ] . strip ( " ' \" " )
i + = 2
elif arg == " -exec " :
has_exec = True
break
elif arg . startswith ( " - " ) :
i + = 1
2026-03-07 05:43:51 +00:00
else :
path = self . resolve_path ( arg )
i + = 1
results = [ ]
2026-03-10 07:30:42 +08:00
for key , node in self . fs . items ( ) :
if not key . startswith ( path . rstrip ( " / " ) if path != " / " else " / " ) :
continue
if key == path :
continue
if file_type == " f " and node [ " type " ] != " file " :
continue
if file_type == " d " and node [ " type " ] != " dir " :
continue
if fnmatch ( key . split ( " / " ) [ - 1 ] , name_pattern ) :
results . append ( key )
if has_exec and " rm " in " " . join ( args ) :
for result in list ( results ) :
self . _delete_node ( result )
return " \n " . join ( results )
return " \n " . join ( results )
def _simulate_du ( self , args : list [ str ] ) - > str :
target = next ( ( a for a in args if not a . startswith ( " - " ) ) , self . cwd )
target = self . resolve_path ( target )
node = self . get_node ( target )
if not node :
return f " du: cannot access ' { target } ' "
def calc_size ( path : str ) - > int :
item = self . fs [ path ]
if item [ " type " ] == " file " :
return max ( 1 , len ( item . get ( " content " , " " ) ) / / 16 )
return 4 + sum ( calc_size ( f " { path . rstrip ( ' / ' ) } / { child } " ) for child in item . get ( " children " , [ ] ) )
size = calc_size ( target )
suffix = " K "
if " -h " in args or " -sh " in args :
return f " { size } { suffix } \t { target } "
return f " { size } \t { target } "
def _simulate_wc ( self , args : list [ str ] ) - > str :
flag = next ( ( a for a in args if a . startswith ( " - " ) ) , " " )
target = next ( ( a for a in args if not a . startswith ( " - " ) ) , None )
content = " "
if target :
node = self . get_node ( self . resolve_path ( target ) )
2026-03-07 05:43:51 +00:00
if node and node [ " type " ] == " file " :
content = node . get ( " content " , " " )
2026-03-10 07:30:42 +08:00
lines = len ( content . splitlines ( ) )
words = len ( content . split ( ) )
chars = len ( content )
2026-03-07 05:43:51 +00:00
if flag == " -l " :
return str ( lines )
2026-03-10 07:30:42 +08:00
if flag == " -w " :
2026-03-07 05:43:51 +00:00
return str ( words )
2026-03-10 07:30:42 +08:00
if flag == " -c " :
2026-03-07 05:43:51 +00:00
return str ( chars )
2026-03-10 07:30:42 +08:00
return f " { lines : >4 } { words : >4 } { chars : >4 } "
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
def _simulate_mkdir ( self , args : list [ str ] ) - > str :
2026-03-07 05:43:51 +00:00
if not args :
return " mkdir: missing operand "
2026-03-10 07:30:42 +08:00
recursive = " -p " in args
targets = [ a for a in args if not a . startswith ( " - " ) ]
for target in targets :
path = self . resolve_path ( target )
parent = self . parent_dir ( path )
if self . exists ( path ) :
continue
if not recursive and not self . exists ( parent ) :
return f " mkdir: cannot create directory ' { target } ' : No such file or directory "
if recursive :
self . _ensure_dir ( parent )
self . fs [ path ] = { " type " : " dir " , " perm " : " 755 " , " children " : [ ] }
self . _add_child ( parent , path . split ( " / " ) [ - 1 ] )
return " "
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
def _simulate_touch ( self , args : list [ str ] ) - > str :
2026-03-07 05:43:51 +00:00
if not args :
return " touch: missing file operand "
2026-03-10 07:30:42 +08:00
for target in args :
path = self . resolve_path ( target )
if not self . exists ( path ) :
parent = self . parent_dir ( path )
if not self . exists ( parent ) :
return f " touch: cannot touch ' { target } ' : No such file or directory "
self . fs [ path ] = { " type " : " file " , " perm " : " 644 " , " content " : " " }
self . _add_child ( parent , path . split ( " / " ) [ - 1 ] )
return " "
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
def _simulate_cp ( self , args : list [ str ] ) - > str :
opts = { a for a in args if a . startswith ( " - " ) }
items = [ a for a in args if not a . startswith ( " - " ) ]
if len ( items ) < 2 :
2026-03-07 05:43:51 +00:00
return " cp: missing file operand "
2026-03-10 07:30:42 +08:00
src , dst = self . resolve_path ( items [ 0 ] ) , self . resolve_path ( items [ 1 ] )
src_node = self . get_node ( src )
if not src_node :
return f " cp: cannot stat ' { items [ 0 ] } ' "
if src_node [ " type " ] == " dir " and " -r " not in opts :
return f " cp: -r not specified; omitting directory ' { items [ 0 ] } ' "
if self . get_node ( dst ) and self . get_node ( dst ) [ " type " ] == " dir " :
dst = f " { dst . rstrip ( ' / ' ) } / { src . split ( ' / ' ) [ - 1 ] } "
self . _copy_node ( src , dst )
return " "
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
def _simulate_mv ( self , args : list [ str ] ) - > str :
if len ( args ) < 2 :
return " mv: missing file operand "
src , dst = self . resolve_path ( args [ 0 ] ) , self . resolve_path ( args [ 1 ] )
node = self . get_node ( src )
if not node :
return f " mv: cannot stat ' { args [ 0 ] } ' "
if self . get_node ( dst ) and self . get_node ( dst ) [ " type " ] == " dir " :
dst = f " { dst . rstrip ( ' / ' ) } / { src . split ( ' / ' ) [ - 1 ] } "
self . _copy_node ( src , dst )
self . _delete_node ( src )
return " "
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
def _simulate_rm ( self , args : list [ str ] ) - > str :
opts = { a for a in args if a . startswith ( " - " ) }
targets = [ a for a in args if not a . startswith ( " - " ) ]
if not targets :
return " rm: missing operand "
for target in targets :
path = self . resolve_path ( target )
node = self . get_node ( path )
if not node :
continue
if node [ " type " ] == " dir " and not ( { " -r " , " -rf " , " -fr " } & opts ) :
return f " rm: cannot remove ' { target } ' : Is a directory "
self . _delete_node ( path )
return " "
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
def _simulate_chmod ( self , args : list [ str ] ) - > str :
2026-03-07 05:43:51 +00:00
if len ( args ) < 2 :
2026-03-10 07:30:42 +08:00
return " chmod: missing operand "
mode , target = args [ 0 ] , self . resolve_path ( args [ 1 ] )
node = self . get_node ( target )
if not node :
return f " chmod: cannot access ' { args [ 1 ] } ' "
perm = node . get ( " perm " , " 644 " )
if mode . isdigit ( ) and len ( mode ) == 3 :
node [ " perm " ] = mode
elif mode == " +x " :
node [ " perm " ] = perm [ : 2 ] + " 1 "
elif mode == " -r " :
node [ " perm " ] = " 244 "
return " "
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
def _simulate_chown_group ( self , cmd_name : str , args : list [ str ] ) - > str :
if not args :
return f " { cmd_name } : missing operand "
recursive = args [ 0 ] == " -R "
target = self . resolve_path ( args [ - 1 ] )
if not self . exists ( target ) :
return f " { cmd_name } : cannot access ' { args [ - 1 ] } ' "
return " "
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
def _simulate_stat ( self , args : list [ str ] ) - > str :
2026-03-07 05:43:51 +00:00
if not args :
return " stat: missing operand "
path = self . resolve_path ( args [ 0 ] )
2026-03-10 07:30:42 +08:00
node = self . get_node ( path )
2026-03-07 05:43:51 +00:00
if not node :
2026-03-10 07:30:42 +08:00
return f " stat: cannot stat ' { args [ 0 ] } ' "
is_dir = node [ " type " ] == " dir "
size = len ( node . get ( " content " , " " ) ) if not is_dir else max ( 4 , len ( node . get ( " children " , [ ] ) ) * 4 )
return f " File: { path } \n Size: { size } IO Block: 4096 { ' directory ' if is_dir else ' regular file ' } \n Access: ( { node . get ( ' perm ' , ' 755 ' ) } / { self . _perm_human ( node . get ( ' perm ' , ' 755 ' ) , is_dir ) } ) \n Modify: 2026-03-06 10:00:00 "
def _simulate_history ( self , args : list [ str ] ) - > str :
n = 10
if args and args [ 0 ] == " -n " and len ( args ) > 1 and args [ 1 ] . isdigit ( ) :
n = int ( args [ 1 ] )
return " \n " . join ( f " { i + 1 } { cmd } " for i , cmd in enumerate ( self . history [ - n : ] ) )
def _simulate_ps ( self , args : list [ str ] ) - > str :
if args == [ " aux " ] or args == [ " -ef " ] :
return " USER PID % CPU % MEM VSZ RSS TTY STAT START TIME COMMAND \n root 1 0.0 0.1 10000 2048 ? Ss 10:00 0:01 /sbin/init \n sandbox 1234 0.1 0.3 20480 4096 pts/0 S+ 10:05 0:00 python3 server.py \n nginx 1400 0.0 0.2 18900 3500 ? S 10:06 0:00 nginx: worker "
return " PID TTY TIME CMD \n 1234 pts/0 00:00:00 bash "
def _simulate_system_text ( self , cmd_name : str , args : list [ str ] ) - > str :
canned = {
" clear " : " [screen cleared] " ,
" id " : " uid=1000(sandbox_user) gid=1000(sandbox_user) groups=1000(sandbox_user),999(docker) " ,
" w " : " 10:00:00 up 5 days, 2 users, load average: 0.10, 0.12, 0.08 \n USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT \n sandbox pts/0 localhost 10:00 1:20 0.01s 0.00s bash " ,
" last " : " sandbox_user pts/0 localhost Mon Mar 6 10:04 still logged in \n reboot system boot 5.15.0 Mon Mar 6 10:00 " ,
" passwd " : " Changing password for sandbox_user... (simulated) " ,
" su " : " Switched to root (simulated) " ,
" df " : " Filesystem Size Used Avail Use % Mounted on \n /dev/vda1 40G 12G 26G 32 % / \n tmpfs 512M 8.0M 504M 2 % /run " ,
" free " : " total used free shared buff/cache available \n Mem: 2.0Gi 1.1Gi 256Mi 48Mi 700Mi 750Mi " ,
" mount " : " /dev/vda1 on / type ext4 (rw,relatime) \n tmpfs on /run type tmpfs (rw,nosuid,nodev) " ,
" fdisk " : " Disk /dev/vda: 40 GiB, 42949672960 bytes, 83886080 sectors " ,
" top " : " top - 10:00:00 up 5 days, 1 user, load average: 0.10, 0.12, 0.08 \n Tasks: 132 total, 1 running, 131 sleeping " ,
" kill " : " signal sent (simulated) " ,
" pkill " : " matched processes terminated (simulated) " ,
" nohup " : " appending output to nohup.out " ,
2026-03-10 09:23:23 +08:00
" systemctl " : " ● nginx.service - A high performance web server \n Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled) \n Active: active (running) since Mon 2026-03-06 10:00:00 CST; 5 days ago \n Main PID: 1042 (nginx) \n Tasks: 3 \n Memory: 12.5M " ,
" service " : " nginx is running " ,
" journalctl " : " Mar 06 10:00:00 systemd[1]: Started nginx.service \n Mar 06 10:00:01 nginx[1042]: worker process started \n Mar 06 10:04:12 nginx[1042]: connect() failed (111: Connection refused) " ,
2026-03-10 07:30:42 +08:00
" ifconfig " : " eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 \n inet 192.168.1.20 netmask 255.255.255.0 \n lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536 \n inet 127.0.0.1 netmask 255.0.0.0 " ,
" ip " : " 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 \n inet 127.0.0.1/8 scope host lo \n 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 \n inet 192.168.1.20/24 scope global eth0 " ,
" ping " : " PING 127.0.0.1 (127.0.0.1): 56 data bytes \n 64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.025 ms \n --- 127.0.0.1 ping statistics --- \n 4 packets transmitted, 4 received, 0 % packet loss " ,
" netstat " : " Active Internet connections (only servers) \n Proto Local Address Foreign Address State PID/Program name \n tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 102/sshd " ,
" ss " : " State Recv-Q Send-Q Local Address:Port Peer Address:Port Process \n LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(( \" sshd \" ,pid=102,fd=3)) " ,
" curl " : " <!doctype html><html><body>hello localhost</body></html> " ,
" wget " : " Saving to: ' /tmp/test.html ' \n 2026-03-06 10:00:00 (1.2 MB/s) - ' /tmp/test.html ' saved [612/612] " ,
" traceroute " : " traceroute to 8.8.8.8, 30 hops max \n 1 192.168.1.1 1.012 ms \n 2 10.0.0.1 4.221 ms " ,
" which " : COMMAND_INDEX . get ( args [ 0 ] , " " ) if args else " " ,
" whereis " : f " { args [ 0 ] } : { COMMAND_INDEX . get ( args [ 0 ] , ' ' ) } /usr/share/man/man1/ { args [ 0 ] } .1.gz " if args else " " ,
" yum " : " Loaded plugins: fastestmirror \n Installed Packages \n nginx.x86_64 1.24.0-1 @base " ,
" rpm " : " bash-5.1.8-6.el9.x86_64 " ,
" apt " : " Hit:1 http://archive.ubuntu.com/ubuntu jammy InRelease \n Reading package lists... Done " ,
" dpkg " : " ii bash 5.1-6ubuntu1 amd64 GNU Bourne Again SHell " ,
" vim " : " [Opened in vim simulator] " ,
" vi " : " [Opened in vim simulator] " ,
" env " : " \n " . join ( f " { k } = { v } " for k , v in self . env . items ( ) ) ,
" export " : " environment updated " ,
" alias " : " alias ll= ' ls -l ' " if not args else " alias set " ,
" date " : datetime . now ( ) . strftime ( " %a % b %d % H: % M: % S CST % Y " ) ,
" cal " : " March 2026 \n Su Mo Tu We Th Fr Sa \n 1 2 3 4 5 6 7 \n 8 9 10 11 12 13 14 " ,
" bc " : " 2 " ,
" tar " : " tar operation simulated " ,
" crontab " : " no crontab for sandbox_user " ,
" more " : " --More-- (simulated pagination) " ,
" less " : " /etc/passwd (simulated pager) " ,
}
if cmd_name == " ip " and args [ : 1 ] == [ " addr " ] :
return canned [ " ip " ]
2026-03-10 09:23:23 +08:00
if cmd_name == " systemctl " :
if args [ : 1 ] == [ " status " ] :
target = args [ 1 ] if len ( args ) > 1 else ' nginx '
return canned [ " systemctl " ] . replace ( ' nginx ' , target )
if args [ : 1 ] in ( [ " restart " ] , [ " start " ] , [ " stop " ] , [ " reload " ] , [ " enable " ] ) :
target = args [ 1 ] if len ( args ) > 1 else ' service '
return f " { args [ 0 ] } operation completed for { target } (simulated) "
return canned [ " systemctl " ]
if cmd_name == " service " :
return canned [ " service " ]
if cmd_name == " journalctl " :
return canned [ " journalctl " ]
2026-03-10 07:30:42 +08:00
if cmd_name == " tail " or cmd_name == " head " :
return self . _simulate_head_tail ( cmd_name , args )
if cmd_name == " which " :
return canned [ " which " ]
if cmd_name == " whereis " :
return canned [ " whereis " ]
2026-03-10 09:23:23 +08:00
if cmd_name == " dig " :
return canned . get ( " dig " , " ;; ANSWER SECTION: \n example.com. 300 IN A 93.184.216.34 " )
2026-03-10 07:30:42 +08:00
if cmd_name == " export " and args and " = " in args [ 0 ] :
key , value = args [ 0 ] . split ( " = " , 1 )
self . env [ key ] = value
return f " exported { key } = { value } "
if cmd_name == " alias " and args and " = " in args [ 0 ] :
key , value = args [ 0 ] . split ( " = " , 1 )
self . aliases [ key ] = value . strip ( " ' \" " )
return f " alias { key } = ' { self . aliases [ key ] } ' "
if cmd_name == " bc " :
expr = " " . join ( args )
if " | " in expr :
match = re . search ( r " ' ([^ ' ]+) ' " , expr )
if match :
try :
return str ( eval ( match . group ( 1 ) , { " __builtins__ " : { } } , { } ) )
except Exception :
return " 2 "
return " 2 "
if cmd_name == " tar " :
if " -czf " in args and len ( args ) > = 3 :
archive = self . resolve_path ( args [ args . index ( " -czf " ) + 1 ] )
self . fs [ archive ] = { " type " : " file " , " perm " : " 644 " , " content " : " tarball " }
self . _add_child ( self . parent_dir ( archive ) , archive . split ( " / " ) [ - 1 ] )
if " -xzf " in args and " -C " in args :
dest = self . resolve_path ( args [ args . index ( " -C " ) + 1 ] )
self . _ensure_dir ( dest )
return canned [ " tar " ]
return canned . get ( cmd_name , f " { cmd_name } : simulated " )
# ---------- Main ----------
def execute ( self , raw_cmd : str ) - > dict :
cmd = raw_cmd . strip ( )
2026-03-07 05:43:51 +00:00
if not cmd :
return { " output " : " " , " success " : True , " message " : " " }
2026-03-10 07:30:42 +08:00
cmd = self . _expand_alias ( cmd )
2026-03-07 05:43:51 +00:00
self . history . append ( cmd )
2026-03-10 07:30:42 +08:00
parts = self . _safe_shell_split ( cmd )
if not parts :
return { " output " : " " , " success " : True , " message " : " " }
cmd_name , args = parts [ 0 ] , parts [ 1 : ]
if any ( x in cmd for x in [ " sudo rm -rf / " , " :() { :|:&};: " ] ) :
return { " output " : " " , " success " : False , " message " : " ❌ 危险命令已拦截,沙盒环境禁止执行。 " }
2026-03-07 05:43:51 +00:00
try :
if cmd_name == " ls " :
output = self . _simulate_ls ( args )
elif cmd_name == " pwd " :
output = self . _simulate_pwd ( )
2026-03-10 07:30:42 +08:00
elif cmd_name == " cd " :
output = self . _simulate_cd ( args )
2026-03-07 05:43:51 +00:00
elif cmd_name == " echo " :
output = self . _simulate_echo ( args )
elif cmd_name == " cat " :
output = self . _simulate_cat ( args )
2026-03-10 07:30:42 +08:00
elif cmd_name in { " head " , " tail " } :
output = self . _simulate_head_tail ( cmd_name , args )
2026-03-07 05:43:51 +00:00
elif cmd_name == " grep " :
output = self . _simulate_grep ( args )
elif cmd_name == " find " :
output = self . _simulate_find ( args )
elif cmd_name == " du " :
output = self . _simulate_du ( args )
elif cmd_name == " wc " :
output = self . _simulate_wc ( args )
elif cmd_name == " mkdir " :
output = self . _simulate_mkdir ( args )
elif cmd_name == " touch " :
output = self . _simulate_touch ( args )
elif cmd_name == " cp " :
output = self . _simulate_cp ( args )
elif cmd_name == " mv " :
output = self . _simulate_mv ( args )
2026-03-10 07:30:42 +08:00
elif cmd_name == " rm " :
output = self . _simulate_rm ( args )
elif cmd_name == " chmod " :
output = self . _simulate_chmod ( args )
elif cmd_name in { " chown " , " chgrp " } :
output = self . _simulate_chown_group ( cmd_name , args )
2026-03-07 05:43:51 +00:00
elif cmd_name == " stat " :
output = self . _simulate_stat ( args )
2026-03-10 07:30:42 +08:00
elif cmd_name == " whoami " :
output = self . user
elif cmd_name == " history " :
output = self . _simulate_history ( args )
2026-03-10 09:23:23 +08:00
elif cmd_name in { " systemctl " , " service " , " journalctl " , " dig " } :
output = self . _simulate_system_text ( cmd_name , args )
2026-03-07 05:43:51 +00:00
else :
2026-03-10 07:30:42 +08:00
output = self . _simulate_system_text ( cmd_name , args )
2026-03-07 05:43:51 +00:00
except Exception as e :
2026-03-10 07:30:42 +08:00
return { " output " : f " Error: { e } " , " success " : False , " message " : " ❌ 执行失败 " }
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
return { " output " : output , " success " : True , " message " : " ✅ 命令执行成功 " , " cwd " : self . cwd }
2026-03-07 05:43:51 +00:00
if __name__ == " __main__ " :
sb = LinuxSandbox ( )
print ( sb . execute ( " pwd " ) )
2026-03-10 07:30:42 +08:00
print ( sb . execute ( " mkdir -p /tmp/a/b " ) )
print ( sb . execute ( " find /etc -name ' *.conf ' " ) )