feat: sap-cli skill v2.1.0 — self-contained distributable package

- assets/: sap-cli source (v2.1.0, 26 commands, 16 object types)
- references/: tool constraints + error handling (self-contained)
- scripts/setup.py: one-click install/config/verify
- SKILL.md: full command reference + dual-platform install guide
- VERSION: 2.1.0

Built from D:/Codespace/sap-cli via scripts/pack_skill.py
This commit is contained in:
2026-06-13 20:52:24 +08:00
commit 1e39b6da88
49 changed files with 7172 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
"""sapcli 命令模块包。
将原 commands.py 按功能拆分为多个子模块,保持统一的导出接口。
"""
from sapcli.commands.crud import (
cmd_create,
cmd_delete,
cmd_download,
cmd_info,
cmd_sync,
)
from sapcli.commands.batch import (
cmd_init,
cmd_refresh,
cmd_sync_all,
)
from sapcli.commands.config_cmd import (
cmd_config,
)
from sapcli.commands.search import (
cmd_list,
cmd_whereused,
cmd_search,
)
from sapcli.commands.transport import (
cmd_transport,
)
from sapcli.commands.quality import (
cmd_check,
cmd_format,
)
from sapcli.commands.diff_cmd import (
cmd_diff,
)
from sapcli.commands.package_cmd import (
cmd_package,
)
from sapcli.commands.cds import (
cmd_cds,
)
from sapcli.commands.analyze import (
cmd_analyze,
)
from sapcli.commands.scaffold import (
cmd_scaffold,
)
from sapcli.commands.ddl_query import (
cmd_show_table,
cmd_read_table,
)
from sapcli.commands.program_run import (
cmd_run_program,
)
from sapcli.commands.activate import (
cmd_activate,
)
__all__ = [
"cmd_create",
"cmd_delete",
"cmd_download",
"cmd_info",
"cmd_init",
"cmd_refresh",
"cmd_sync",
"cmd_sync_all",
"cmd_config",
"cmd_list",
"cmd_whereused",
"cmd_search",
"cmd_transport",
"cmd_check",
"cmd_format",
"cmd_diff",
"cmd_package",
"cmd_cds",
"cmd_analyze",
"cmd_scaffold",
"cmd_show_table",
"cmd_read_table",
"cmd_run_program",
"cmd_activate",
]
+122
View File
@@ -0,0 +1,122 @@
"""activate 命令 — 单独激活 SAP 对象(无需重新 sync)。"""
from __future__ import annotations
import argparse
import logging
from sapcli.client import ADTClient
from sapcli.types import get_type_config, parse_object_name
logger = logging.getLogger("sapcli.commands.activate")
def cmd_activate(args: argparse.Namespace, client: ADTClient) -> None:
"""单独激活一个 SAP 对象。
支持 --name / --type 激活单个对象,
也支持 --names / --types 批量激活多个对象。
可选 --corr_nr 传入传输请求号。
"""
corr_nr: str | None = getattr(args, "corr_nr", None)
# 批量模式:--names ZCLS1,ZCLS2 --types class,class
names_raw: str | None = getattr(args, "names", None)
types_raw: str | None = getattr(args, "types", None)
if names_raw and types_raw:
names = [n.strip() for n in names_raw.split(",") if n.strip()]
types = [t.strip() for t in types_raw.split(",") if t.strip()]
if len(names) != len(types):
print(" ✗ --names 和 --types 的数量不匹配")
return
_activate_batch(client, names, types, corr_nr)
return
# 单对象模式:--name / --type
name: str = args.name
obj_type: str = args.type
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
print("=" * 60)
print(" SAP ADT 对象激活")
print("=" * 60)
print(f" 对象名称: {parsed.display_name}")
print(f" 对象类型: {type_label}")
if corr_nr:
print(f" 传输请求: {corr_nr}")
_activate_single(client, name, parsed.obj_uri, corr_nr, type_label)
def _activate_single(
client: ADTClient,
name: str,
obj_uri: str,
corr_nr: str | None,
type_label: str = "",
) -> bool:
"""激活单个对象,返回是否成功。"""
label = type_label or name
print(f"\n → 正在激活 {name}...")
try:
success, messages = client.activate(name, obj_uri, corr_nr)
except Exception as e:
print(f" ✗ 激活失败: {e}")
return False
errors = [m for m in messages if m["type"] == "E"]
warnings = [m for m in messages if m["type"] == "W"]
infos = [m for m in messages if m["type"] == "I"]
if success:
print(f"{name} 激活成功!")
else:
print(f"{name} 激活失败! {len(errors)} 个错误, {len(warnings)} 个警告")
for msg in messages:
icon = {"E": "", "W": "", "I": "", "S": ""}.get(msg["type"], "?")
line = msg.get("line", "?")
text = msg.get("text", "")
print(f" {icon} [{msg['type']}] 行 {line}: {text}")
return success
def _activate_batch(
client: ADTClient,
names: list[str],
types: list[str],
corr_nr: str | None,
) -> None:
"""批量激活多个对象。"""
print("=" * 60)
print(" SAP ADT 批量激活")
print("=" * 60)
print(f" 对象数量: {len(names)}")
if corr_nr:
print(f" 传输请求: {corr_nr}")
results: list[tuple[str, bool]] = []
for name, obj_type in zip(names, types):
parsed = parse_object_name(name, obj_type)
ok = _activate_single(client, name, parsed.obj_uri, corr_nr)
results.append((name, ok))
# 汇总
print()
print("" * 60)
print(" 激活结果汇总:")
print("" * 60)
ok_count = sum(1 for _, ok in results if ok)
fail_count = len(results) - ok_count
for name, ok in results:
icon = "" if ok else ""
print(f" {icon} {name}")
print()
if fail_count == 0:
print(f" ✓ 全部激活成功 ({ok_count}/{len(results)})")
else:
print(f" ⚠ 成功 {ok_count}, 失败 {fail_count} (共 {len(results)})")
+126
View File
@@ -0,0 +1,126 @@
"""依赖分析命令:analyze。"""
from __future__ import annotations
import argparse
import logging
import os
import re
from sapcli.client import ADTClient
logger = logging.getLogger("sapcli.commands.analyze")
# ABAP 依赖模式
_DEPENDENCY_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
("TYPE REF TO", re.compile(r"TYPE\s+REF\s+TO\s+(\w+)", re.IGNORECASE)),
("CALL METHOD", re.compile(r"CALL\s+METHOD\s+(\w+)", re.IGNORECASE)),
("CALL METHOD (static)", re.compile(r"(\w+)=>(\w+)", re.IGNORECASE)),
("CREATE OBJECT", re.compile(r"CREATE\s+OBJECT\s+\w+\s+TYPE\s+(\w+)", re.IGNORECASE)),
("PERFORM", re.compile(r"PERFORM\s+(\w+)", re.IGNORECASE)),
("CALL FUNCTION", re.compile(r"CALL\s+FUNCTION\s+\'(\w+)\'", re.IGNORECASE)),
("SELECT ... FROM", re.compile(r"FROM\s+(\w+)", re.IGNORECASE)),
("LIKE", re.compile(r"LIKE\s+(\w+)", re.IGNORECASE)),
("TYPE", re.compile(r"TYPE\s+(\w+)", re.IGNORECASE)),
]
# 已知的 ABAP 内置类型(不需要追踪)
_BUILTIN_TYPES = frozenset({
"STRING", "CHAR", "NUMC", "INT1", "INT2", "INT4", "INT8",
"FLOAT", "DECFLOAT16", "DECFLOAT34", "DEC", "CURR", "QUAN",
"RAW", "LRAW", "RAWSTRING", "DATS", "TIMS", "SSTRING",
"XSTRING", "X", "C", "N", "D", "T", "I", "F", "P",
"ANY", "DATA", "REF", "OBJECT", "STRUCTURE", "TABLE",
"STANDARD", "SORTED", "HASHED", "INDEX", "SY",
"ABAP_BOOL", "ABAP_TRUE", "ABAP_FALSE", "BOOLEAN",
"XSDBOOLEAN", "FLAG", "CHAR1", "CHAR10",
})
def cmd_analyze(args: argparse.Namespace, client: ADTClient) -> None:
"""依赖自动分析。
解析本地 .abap 文件中的 TYPE REF TO / CALL METHOD / PERFORM 等,
自动生成 depends_on 列表。
"""
project_path: str = getattr(args, "path", ".")
print("=" * 60)
print(" sap-cli 依赖分析")
print("=" * 60)
print(f" 项目路径: {project_path}")
if not os.path.isdir(project_path):
print(f"\n ✗ 目录不存在: {project_path}")
return
# 扫描所有 .abap 文件
abap_files: list[str] = []
for root, dirs, files in os.walk(project_path):
for fname in files:
if fname.endswith(".abap"):
abap_files.append(os.path.join(root, fname))
if not abap_files:
print("\n 未找到 .abap 文件")
return
print(f"\n → 扫描到 {len(abap_files)} 个 .abap 文件\n")
# 分析每个文件
all_deps: dict[str, dict[str, list[str]]] = {}
for filepath in abap_files:
rel_path = os.path.relpath(filepath, project_path)
file_base = os.path.splitext(os.path.basename(filepath))[0].upper()
with open(filepath, "r", encoding="utf-8") as f:
source = f.read()
deps = _analyze_dependencies(source)
all_deps[file_base] = {"file": rel_path, "deps": deps}
if deps:
print(f" 📄 {rel_path}")
print(f"{', '.join(deps)}")
else:
print(f" 📄 {rel_path} (无外部依赖)")
# 汇总
print(f"\n {'' * 50}")
total_deps = sum(len(v["deps"]) for v in all_deps.values())
files_with_deps = sum(1 for v in all_deps.values() if v["deps"])
print(f" 总计: {len(abap_files)} 个文件, {files_with_deps} 个有外部依赖, {total_deps} 个依赖关系")
# 输出 depends_on 配置建议
if total_deps > 0:
print(f"\n 建议的 manifest.json depends_on 配置:")
print(f" {'' * 50}")
for name, info in sorted(all_deps.items()):
if info["deps"]:
deps_str = ", ".join(f'"{d}"' for d in sorted(info["deps"]))
print(f' "{name}": [{deps_str}]')
def _analyze_dependencies(source: str) -> list[str]:
"""分析 ABAP 源码中的依赖关系。
Args:
source: ABAP 源码字符串。
Returns:
依赖对象名称列表(去重)。
"""
deps: set[str] = set()
for pattern_name, pattern in _DEPENDENCY_PATTERNS:
for match in pattern.finditer(source):
name = match.group(1).upper()
# 过滤内置类型和短名称
if name in _BUILTIN_TYPES:
continue
if len(name) < 3:
continue
# 只追踪 Z/Y 开头的自定义对象(更精确)
if name.startswith("Z") or name.startswith("Y"):
deps.add(name)
return sorted(deps)
+328
View File
@@ -0,0 +1,328 @@
"""批量操作命令:init / refresh / sync-all。"""
from __future__ import annotations
import argparse
import logging
import os
from sapcli.client import ADTClient
from sapcli.commands.crud import _sync_single
from sapcli.exceptions import ConfigError, CyclicDependencyError
from sapcli.manifest import Manifest, ManifestEntry, init_manifest
from sapcli.scanner import scan_project
from sapcli.sorter import topological_sort
from sapcli.types import parse_object_name
logger = logging.getLogger("sapcli.commands.batch")
def cmd_init(args: argparse.Namespace, client: ADTClient) -> None:
"""项目初始化:扫描本地文件 → 查询 SAP → 生成 manifest.json。"""
from datetime import datetime, timezone
from sapcli.types import parse_object_name
project_path = os.path.abspath(args.path)
if not os.path.isdir(project_path):
raise ConfigError(f"项目目录不存在: {project_path}")
print("=" * 60)
print(" sap-cli 项目初始化")
print("=" * 60)
print(f" 项目路径: {project_path}")
# ── Step 1: 扫描本地文件 ──
print(f"\n → 正在扫描本地文件...")
scanned = scan_project(project_path)
if not scanned:
print(" 未扫描到 .abap 文件")
print(" 请确认项目目录结构是否符合约定:")
print(" reports/, classes/, interfaces/, functions/, domains/, ...")
return
print(f" ✓ 扫描到 {len(scanned)} 个本地文件\n")
# ── Step 2: 逐个查询 SAP 状态 ──
print(f" → 正在查询 SAP 系统状态...")
manifest = init_manifest(project_path)
counts = {"active": 0, "inactive": 0, "not_exists": 0}
for obj in scanned:
parsed = parse_object_name(obj.name, obj.type)
try:
status_info = client.get_object_status(parsed.obj_uri)
except Exception as e:
logger.warning("查询 %s 状态失败: %s", obj.name, e)
status_info = {"exists": False, "status": "not_exists", "corr_nr": None}
system_status = status_info["status"]
corr_nr = status_info["corr_nr"]
counts[system_status] = counts.get(system_status, 0) + 1
# 状态图标
if system_status == "active":
icon = ""
corr_display = f"corr: {corr_nr or '本地对象'}"
elif system_status == "inactive":
icon = ""
corr_display = f"corr: {corr_nr or '本地对象'}"
else:
icon = ""
corr_display = "不存在"
print(f" {icon} {obj.name:<20s} ({obj.type:<10s}) {system_status:<12s} {corr_display}")
entry = ManifestEntry(
name=obj.name,
type=obj.type,
file=obj.file,
system_status=system_status,
corr_nr=corr_nr,
depends_on=[],
last_sync=None,
last_sync_result="pending",
)
manifest.upsert(entry)
# ── Step 3: 保存清单 ──
manifest.save()
total = sum(counts.values())
print(f"\n ✓ 清单已生成: {os.path.join(project_path, 'manifest.json')}")
print(f" {total} 个对象: "
f"{counts.get('active', 0)} 已激活, "
f"{counts.get('inactive', 0)} 未激活, "
f"{counts.get('not_exists', 0)} 不存在")
def cmd_refresh(args: argparse.Namespace, client: ADTClient) -> None:
"""刷新清单:重新查询 SAP 状态,更新 system_status 和 corr_nr。"""
from datetime import datetime, timezone
from sapcli.types import parse_object_name
project_path = os.path.abspath(args.path)
manifest = Manifest.load(project_path)
print("=" * 60)
print(" sap-cli 清单刷新")
print("=" * 60)
print(f" 项目路径: {project_path}")
print(f" 清单对象: {len(manifest.objects)}\n")
print(f" → 正在查询 SAP 系统状态...")
changed = 0
unchanged = 0
for name, entry in manifest.objects.items():
parsed = parse_object_name(name, entry.type)
old_status = entry.system_status
old_corr = entry.corr_nr
try:
status_info = client.get_object_status(parsed.obj_uri)
except Exception as e:
logger.warning("查询 %s 状态失败: %s", name, e)
print(f"{name:<20s} ({entry.type:<10s}) 查询失败: {e}")
unchanged += 1
continue
new_status = status_info["status"]
new_corr = status_info["corr_nr"]
# 更新
entry.system_status = new_status
entry.corr_nr = new_corr
# 对比变化
status_changed = old_status != new_status
corr_changed = old_corr != new_corr
is_changed = status_changed or corr_changed
if is_changed:
changed += 1
changes = []
if status_changed:
changes.append(f"状态变更: {old_status}{new_status}")
if corr_changed:
changes.append(f"传输请求: {old_corr or ''}{new_corr or ''}")
change_str = ", ".join(changes)
print(f" ~ {name:<20s} ({entry.type:<10s}) {new_status:<12s} corr: {new_corr or 'null':<15s} ({change_str})")
else:
unchanged += 1
print(f"{name:<20s} ({entry.type:<10s}) {new_status:<12s} corr: {new_corr or 'null':<15s} (未变化)")
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
manifest.last_refresh = now
manifest.save()
print(f"\n ✓ 刷新完成")
print(f" 变更: {changed} 个 | 未变化: {unchanged}")
def cmd_sync_all(args: argparse.Namespace, client: ADTClient) -> None:
"""批量同步:读取清单 → 拓扑排序 → 逐个同步 → 更新清单。"""
from datetime import datetime, timezone
from sapcli.types import parse_object_name
project_path = os.path.abspath(args.path)
manifest = Manifest.load(project_path)
dry_run = getattr(args, "dry_run", False)
fail_fast = getattr(args, "fail_fast", False)
all_entries = list(manifest.objects.values())
total = len(all_entries)
print("=" * 60)
if dry_run:
print(" sap-cli 批量同步 (dry-run)")
else:
print(" sap-cli 批量同步")
print("=" * 60)
print(f" 项目路径: {project_path}")
print(f" 清单对象: {total}")
# ── Step 1: 扫描本地文件 vs 清单差异 ──
scanned = scan_project(project_path)
scanned_names = {s.name for s in scanned}
manifest_names = set(manifest.objects.keys())
new_files = scanned_names - manifest_names
missing_files = manifest_names - scanned_names
if new_files:
print(f"\n{len(new_files)} 个本地文件未纳入清单,建议先 create:")
for name in sorted(new_files):
print(f" - {name}")
# ── Step 2: 过滤待处理对象 ──
pending = []
skipped_up_to_date = []
for entry in all_entries:
# 检查本地文件是否存在
abs_file = manifest.file_path(entry)
if entry.name in missing_files or not os.path.isfile(abs_file):
print(f"\n{entry.name} 本地文件缺失,将跳过")
skipped_up_to_date.append(entry)
continue
# 跳过已同步且激活的对象
if entry.system_status == "active" and entry.last_sync_result == "success":
skipped_up_to_date.append(entry)
continue
pending.append(entry)
# ── Step 3: 拓扑排序 ──
try:
sorted_entries = topological_sort(all_entries)
except CyclicDependencyError as e:
print(f"\n{e}")
raise
# ── Step 4: dry-run 模式 ──
if dry_run:
print(f"\n 执行计划(按依赖排序):")
pending_set = {e.name for e in pending}
idx = 0
for entry in sorted_entries:
idx += 1
if entry in skipped_up_to_date and entry.name not in pending_set:
print(f" {idx}. {entry.name:<20s} ({entry.type:<10s}) → 跳过 [已是最新]")
elif entry.name in missing_files:
print(f" {idx}. {entry.name:<20s} ({entry.type:<10s}) → 跳过 [文件缺失]")
else:
status_info = f"{entry.system_status} → active"
deps = entry.depends_on
dep_str = f" [依赖: {', '.join(deps)}]" if deps else ""
print(f" {idx}. {entry.name:<20s} ({entry.type:<10s}) → sync [{status_info}]{dep_str}")
print(f"\n 待处理: {len(pending)} 个 | 跳过: {total - len(pending)}")
return
# ── Step 5: 执行同步 ──
print(f" 待处理: {len(pending)} 个对象\n")
succeeded: list[str] = []
failed: list[tuple[str, str]] = [] # (name, reason)
skipped_deps: list[str] = [] # 因依赖失败而跳过的
failed_set: set[str] = set() # 失败的对象名集合(用于级联跳过)
# 建立排序后的顺序映射
sorted_order = {entry.name: i for i, entry in enumerate(sorted_entries)}
pending_sorted = sorted(pending, key=lambda e: sorted_order.get(e.name, 999))
for i, entry in enumerate(pending_sorted, 1):
# 检查依赖是否全部成功
dep_failed = [d for d in entry.depends_on if d in failed_set]
if dep_failed:
skipped_deps.append(entry.name)
failed_set.add(entry.name)
entry.last_sync_result = "skipped"
print(f" ⊘ [{i}/{len(pending_sorted)}] {entry.name} — 跳过(依赖失败: {', '.join(dep_failed)}")
continue
abs_file = manifest.file_path(entry)
print(f"\n ── [{i}/{len(pending_sorted)}] {entry.name} ({entry.type}) ──")
# 如果对象不存在,先创建
if entry.system_status == "not_exists":
parsed = parse_object_name(entry.name, entry.type)
print(f" ℹ 对象不存在,自动创建空对象...")
try:
if entry.type == "function":
group_name = entry.name.split("/", 1)[0]
if not client.function_group_exists(group_name):
print(f" → 函数组 {group_name} 不存在,自动创建...")
client.create_function_group(group_name)
print(f" ✓ 函数组 {group_name} 创建成功")
client.create_object(entry.type, entry.name, entry.name, source=None)
print(f" ✓ 空对象创建成功")
except Exception as e:
error_msg = f"创建失败: {e}"
print(f"{error_msg}")
failed.append((entry.name, error_msg))
failed_set.add(entry.name)
entry.last_sync_result = "failed"
if fail_fast:
break
continue
# 执行同步
success, error_msg, actual_corr_nr = _sync_single(
entry.name, entry.type, abs_file, client, quiet=False,
)
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
if success:
succeeded.append(entry.name)
entry.system_status = "active"
entry.corr_nr = actual_corr_nr
entry.last_sync = now
entry.last_sync_result = "success"
else:
failed.append((entry.name, error_msg or "未知错误"))
failed_set.add(entry.name)
entry.last_sync = now
entry.last_sync_result = "failed"
print(f" ⊘ 跳过(标记为 failed")
if fail_fast:
break
# ── Step 6: 保存并输出汇总 ──
manifest.save()
print(f"\n{'=' * 60}")
print(f" 批量同步完成")
print(f"{'=' * 60}")
if succeeded:
print(f" ✓ 成功: {', '.join(succeeded)}")
if failed:
for name, reason in failed:
print(f" ✗ 失败: {name} ({reason})")
if skipped_deps:
print(f" ⊘ 跳过: {', '.join(skipped_deps)}")
if not failed and not skipped_deps:
print(f" ⊘ 跳过: (无)")
print(f" 清单已更新: {os.path.join(project_path, 'manifest.json')}")
+186
View File
@@ -0,0 +1,186 @@
"""CDS View 命令:cds (download / sync / create)。"""
from __future__ import annotations
import argparse
import logging
import os
from sapcli.client import ADTClient
logger = logging.getLogger("sapcli.commands.cds")
def cmd_cds(args: argparse.Namespace, client: ADTClient) -> None:
"""CDS View 操作。"""
action = getattr(args, "cds_action", None)
if action == "download":
_cds_download(args, client)
elif action == "sync":
_cds_sync(args, client)
elif action == "create":
_cds_create(args, client)
else:
print(" 用法: sap-cli cds [download|sync|create]")
print(" download — 下载 CDS View DDL 源码")
print(" sync — 同步本地 DDL 到 SAP")
print(" create — 创建新的 CDS View")
def _cds_download(args: argparse.Namespace, client: ADTClient) -> None:
"""下载 CDS View DDL 源码。"""
name: str = args.name
save_path: str = getattr(args, "path", ".")
print("=" * 60)
print(" SAP CDS View DDL 源码下载")
print("=" * 60)
print(f" CDS 名称: {name}")
print(f" 保存路径: {save_path}")
print(f"\n → 正在下载 DDL 源码...")
try:
source = client.get_cds_source(name)
except Exception as e:
print(f" ✗ 下载失败: {e}")
return
source = source.replace("\r\n", "\n").replace("\r", "\n")
line_count = len(source.splitlines())
print(f" ✓ DDL 源码下载成功! {len(source)} 字符, {line_count}")
if not os.path.isdir(save_path):
os.makedirs(save_path, exist_ok=True)
filename = f"{name.lower()}.ddl"
filepath = os.path.join(save_path, filename)
with open(filepath, "w", encoding="utf-8") as f:
f.write(source)
print(f"\n ✓ 文件已保存: {filepath}")
def _cds_sync(args: argparse.Namespace, client: ADTClient) -> None:
"""同步本地 DDL 到 SAP。"""
name: str = args.name
ddl_path: str = args.path
print("=" * 60)
print(" SAP CDS View DDL 同步")
print("=" * 60)
print(f" CDS 名称: {name}")
print(f" DDL 文件: {ddl_path}")
if not os.path.isfile(ddl_path):
print(f"\n ✗ DDL 文件不存在: {ddl_path}")
return
with open(ddl_path, "r", encoding="utf-8") as f:
ddl_source = f.read()
obj_uri = f"/sap/bc/adt/ddic/ddlsources/{name.lower()}"
src_uri = f"/sap/bc/adt/ddic/ddlsources/{name.lower()}/source/main"
# 检查对象是否存在
print(f"\n → 检查 CDS View 是否存在...")
exists = client.object_exists(obj_uri)
if not exists:
print(f" ℹ CDS View 不存在,需要先创建")
try:
client.create_cds(name, name, ddl_source)
print(f" ✓ CDS View 创建并同步成功!")
except Exception as e:
print(f" ✗ 创建失败: {e}")
return
print(f" ✓ CDS View 存在")
# 锁定 → 写入 → 解锁 → 激活
print(f"\n → 正在同步 DDL 源码...")
try:
lock_handle, corr_nr = client.lock(obj_uri)
try:
client.set_source(src_uri, ddl_source, lock_handle, corr_nr)
print(f" ✓ DDL 源码写入成功")
finally:
client.unlock(obj_uri, lock_handle)
except Exception as e:
print(f" ✗ 写入失败: {e}")
return
# 激活
print(f"\n → 正在激活...")
try:
success, messages = client.activate(name.upper(), obj_uri)
if success:
print(f" ✓ 激活成功")
else:
errors = [m for m in messages if m["type"] == "E"]
print(f" ✗ 激活失败: {len(errors)} 个错误")
for e in errors:
print(f" [错误] 行 {e['line']}: {e['text']}")
except Exception as e:
print(f" ⚠ 激活失败: {e}")
print(f"\n ✓ CDS View 同步完成!")
def _cds_create(args: argparse.Namespace, client: ADTClient) -> None:
"""创建新的 CDS View。"""
name: str = args.name
description: str = getattr(args, "description", None) or name
ddl_path: str | None = getattr(args, "ddl_path", None)
print("=" * 60)
print(" SAP 创建 CDS View")
print("=" * 60)
print(f" CDS 名称: {name}")
print(f" 描述: {description}")
# 读取 DDL 源码
ddl_source = ""
if ddl_path and os.path.isfile(ddl_path):
with open(ddl_path, "r", encoding="utf-8") as f:
ddl_source = f.read()
print(f" DDL 文件: {ddl_path}")
else:
# 生成默认模板
ddl_source = _default_cds_template(name, description)
print(f" DDL: 使用默认模板")
print(f"\n → 正在创建 CDS View...")
try:
obj_uri, src_uri = client.create_cds(name, description, ddl_source)
except Exception as e:
print(f" ✗ 创建失败: {e}")
return
print(f" ✓ CDS View 创建成功!")
print(f" ✓ URI: {obj_uri}")
# 保存本地文件
save_dir = getattr(args, "path", ".")
if save_dir:
if not os.path.isdir(save_dir):
os.makedirs(save_dir, exist_ok=True)
filepath = os.path.join(save_dir, f"{name.lower()}.ddl")
with open(filepath, "w", encoding="utf-8") as f:
f.write(ddl_source)
print(f" ✓ DDL 已保存: {filepath}")
def _default_cds_template(name: str, description: str) -> str:
"""生成默认 CDS View DDL 模板。"""
view_name = name[:16].upper()
return (
f"@AbapCatalog.sqlViewName: \'{view_name}\'\n"
f"@EndUserText.label: \'{description}\'\n"
f"define view {name.lower()}\n"
f" as select from sflight\n"
f" {{\n"
f" carrid,\n"
f" connid,\n"
f" fldate\n"
f" }}\n"
)
+106
View File
@@ -0,0 +1,106 @@
"""config 子命令:显示配置、列出 profile、设置配置项。"""
from __future__ import annotations
import argparse
import configparser
import os
def cmd_config(args: argparse.Namespace, client=None) -> None:
"""配置管理命令。"""
action = getattr(args, "config_action", None)
if action == "show":
_config_show(args)
elif action == "list-profiles":
_config_list_profiles(args)
elif action == "set":
_config_set(args)
else:
print(" 用法: sap-cli config [show|list-profiles|set]")
def _config_show(args: argparse.Namespace) -> None:
"""显示当前配置。"""
from sapcli.config import load_config
config_path = getattr(args, "config", None)
try:
cfg, loaded_from = load_config(config_path)
except Exception as e:
print(f" ✗ 加载配置失败: {e}")
return
print("=" * 60)
print(" sap-cli 当前配置")
print("=" * 60)
if loaded_from:
print(f" 配置文件: {loaded_from}")
print(f" 主机: {cfg.host}")
print(f" Client: {cfg.client}")
print(f" 用户: {cfg.user}")
print(f" 密码: {'***' if cfg.password else '(未设置)'}")
def _config_list_profiles(args: argparse.Namespace) -> None:
"""列出所有可用的 profile。"""
config_path = getattr(args, "config", None)
if config_path is None:
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "config.ini")
print("=" * 60)
print(" sap-cli 可用 profile")
print("=" * 60)
if not os.path.isfile(config_path):
print(" (无配置文件)")
return
parser = configparser.ConfigParser()
parser.read(config_path, encoding="utf-8")
sections = parser.sections()
if not sections:
print(" (无 profile)")
for name in sections:
active = " (默认)" if name == "SAP" else ""
print(f" - {name}{active}")
print(f"\n 配置文件: {config_path}")
def _config_set(args: argparse.Namespace) -> None:
"""设置配置项到 config.ini。"""
key = getattr(args, "key", None)
value = getattr(args, "value", None)
profile = getattr(args, "profile", None) or "SAP"
if not key or not value:
print(" ✗ 用法: sap-cli config set <key> <value>")
print(" 可设置的 key: host, client, user, password")
return
valid_keys = {"host", "client", "user", "password"}
if key not in valid_keys:
print(f" ✗ 不支持的配置项: {key}")
print(f" 可设置的 key: {', '.join(sorted(valid_keys))}")
return
# 找到或创建 config.ini
config_path = getattr(args, "config", None)
if not config_path:
config_path = os.path.join(os.getcwd(), "config.ini")
parser = configparser.ConfigParser()
if os.path.isfile(config_path):
parser.read(config_path, encoding="utf-8")
if not parser.has_section(profile):
parser.add_section(profile)
parser.set(profile, key, value)
with open(config_path, "w", encoding="utf-8") as f:
parser.write(f)
print(f" ✓ 已设置 [{profile}] {key} = {'***' if key == 'password' else value}")
print(f" 配置文件: {config_path}")
+890
View File
@@ -0,0 +1,890 @@
"""CRUD 命令:download / sync / info / delete / create。"""
from __future__ import annotations
import argparse
import logging
import os
import xml.etree.ElementTree as ET
from sapcli.client import ADTClient
from sapcli.exceptions import (
ConfigError,
CreateError,
DeleteError,
InvalidNameError,
LockError,
ObjectAlreadyExistsError,
ObjectNotFoundError,
SapCliError,
)
from sapcli.manifest import Manifest, ManifestEntry
from sapcli.types import get_type_config, parse_object_name
DDIC_TYPES = {"domain", "dataelement", "table", "structure", "tabletype"}
logger = logging.getLogger("sapcli.commands.crud")
_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
LOG_DIR = os.path.join(_PROJECT_ROOT, "log")
LOG_FILE = os.path.join(LOG_DIR, "adt_tools.log")
DEFAULT_TEMPLATES: dict[str, str] = {
"report": 'REPORT {name}.\nWRITE: / \'Hello from {name}\'.\n',
"class": (
'CLASS {name} DEFINITION\n'
' PUBLIC\n'
' FINAL\n'
' CREATE PUBLIC.\n'
' PUBLIC SECTION.\n'
' METHODS: hello.\n'
'ENDCLASS.\n'
'CLASS {name} IMPLEMENTATION.\n'
' METHOD hello.\n'
' ENDMETHOD.\n'
'ENDCLASS.\n'
),
"function": (
'FUNCTION {name}\n'
' EXPORTING\n'
' VALUE(EV_RESULT) TYPE STRING.\n'
' ev_result = \'hello\'.\n'
'ENDFUNCTION.\n'
),
"interface": (
'INTERFACE {name}\n'
' PUBLIC.\n'
' METHODS: hello.\n'
'ENDINTERFACE.\n'
),
}
def print_source_preview(source: str, max_lines: int = 20) -> None:
"""打印源代码预览。"""
lines = source.splitlines()
print(f" ┌─── 源代码 (前 {max_lines} 行) ──────────────────────")
for i, line in enumerate(lines[:max_lines], 1):
print(f"{i:4d} | {line}")
if len(lines) > max_lines:
print(f" │ ... 省略剩余 {len(lines) - max_lines} 行 ...")
print(f" └────────────────────────────────────────────")
def cmd_download(args: argparse.Namespace, client: ADTClient) -> None:
"""下载 SAP 对象源代码到本地文件。"""
obj_type: str = args.type
name: str = args.name
save_dir: str = args.path
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
if parsed.src_uri is None:
print(f"\n{type_label}没有源代码,不支持 download 操作")
print(f" functiongroup 是函数模块的容器,不包含可编辑的源代码文件")
raise InvalidNameError(f"{type_label}没有源代码,不支持 download 操作")
print("=" * 60)
print(" SAP ADT 源代码下载")
print("=" * 60)
print(f" 对象类型: {type_label}")
print(f" 对象名称: {parsed.display_name}")
print(f" 保存路径: {save_dir}")
print(f"\n → 检查对象是否存在...")
if not client.object_exists(parsed.obj_uri):
print(f" ✗ 对象不存在: {parsed.display_name}")
print(f" 请确认名称和类型是否正确")
raise ObjectNotFoundError(parsed.display_name, obj_type)
print(" ✓ 对象存在")
print(f"\n → 正在下载源代码...")
source = client.get_source(parsed.src_uri)
source = source.replace("\r\n", "\n").replace("\r", "\n")
line_count = len(source.splitlines())
print(f" ✓ 源代码下载成功! {len(source)} 字符, {line_count}")
if not os.path.isdir(save_dir):
os.makedirs(save_dir, exist_ok=True)
filename = f"{parsed.file_base}.abap"
filepath = os.path.join(save_dir, filename)
with open(filepath, "w", encoding="utf-8") as f:
f.write(source)
print(f"\n ✓ 文件已保存: {filepath}")
print()
print_source_preview(source)
print(f"\n ✓ 下载完成!")
print(f" 文件: {filepath}")
print(f" 大小: {len(source)} 字符, {line_count}")
def cmd_sync(args: argparse.Namespace, client: ADTClient) -> None:
"""同步单个对象源代码到 SAP 并激活。"""
obj_type: str = args.type
name: str = args.name
file_path: str = args.path
# 向后兼容:检测项目根目录(从文件路径向上查找 manifest.json
if not hasattr(args, "project_path") or not args.project_path:
parent = os.path.dirname(os.path.abspath(file_path))
if os.path.isfile(os.path.join(parent, "manifest.json")):
args.project_path = parent
else:
args.project_path = None
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
if parsed.src_uri is None:
print(f"\n{type_label}没有源代码,不支持 sync 操作")
print(f" functiongroup 是函数模块的容器,不包含可编辑的源代码文件")
raise InvalidNameError(f"{type_label}没有源代码,不支持 sync 操作")
if not os.path.isfile(file_path):
print(f"\n ✗ 文件不存在: {file_path}")
raise ConfigError(f"文件不存在: {file_path}")
with open(file_path, "r", encoding="utf-8") as f:
source = f.read()
print("=" * 60)
print(" SAP ADT 源代码同步激活")
print("=" * 60)
print(f" 对象类型: {type_label}")
print(f" 对象名称: {parsed.display_name}")
print(f" 本地文件: {file_path}")
print(f" 源代码: {len(source)} 字符, {len(source.splitlines())}")
print(f" 流程: 检查/创建 → 写入 → 语法检查 → 激活")
corr_nr_arg = getattr(args, "corr_nr", None)
success, error_msg, actual_corr_nr = _sync_single(
name, obj_type, file_path, client, corr_nr=corr_nr_arg,
)
if success:
# 更新清单(如果适用)
_update_manifest_after_sync(args, name, obj_type, file_path, actual_corr_nr)
return
# 失败时也更新清单
_update_manifest_after_sync(args, name, obj_type, file_path, actual_corr_nr, failed=True)
raise SapCliError(error_msg or "同步失败")
def _sync_single(
name: str,
obj_type: str,
file_path: str,
client: ADTClient,
corr_nr: str | None = None,
quiet: bool = False,
) -> tuple[bool, str | None, str | None]:
"""单个对象的同步核心逻辑。
Args:
name: 对象名称(function 类型含 /)
obj_type: 对象类型
file_path: 本地源代码文件路径
client: ADT 客户端
corr_nr: 传输请求号(可选)
quiet: 是否静默模式(批量同步时减少输出)
Returns:
(success, error_msg, actual_corr_nr)
"""
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
if parsed.src_uri is None:
return False, f"{type_label}没有源代码,不支持 sync 操作", None
if not os.path.isfile(file_path):
return False, f"文件不存在: {file_path}", None
with open(file_path, "r", encoding="utf-8") as f:
source = f.read()
if not quiet:
print(f"\n → 检查对象是否存在...")
if not client.object_exists(parsed.obj_uri):
if not quiet:
print(f" ℹ 对象不存在,自动创建空对象...")
try:
client.create_object(obj_type, name, name, source=None)
if not quiet:
print(f" ✓ 空对象创建成功,进入同步流程")
except Exception as e:
return False, f"创建失败: {e}", None
else:
if not quiet:
print(" ✓ 对象存在")
# ── Step 1: 锁定 → 写入 → 解锁 ──
if not quiet:
print("\n ── 锁定 → 写入 → 解锁 ──")
try:
if corr_nr:
lock_handle, _ = client.lock(parsed.obj_uri, corr_nr)
if not quiet:
print(f" ✓ 锁定成功(传输请求: {corr_nr}")
else:
try:
lock_handle, detected_corr_nr = client.lock(parsed.obj_uri)
if detected_corr_nr:
corr_nr = detected_corr_nr
if not quiet:
print(f" ✓ 锁定成功(对象已绑定传输请求: {corr_nr}")
else:
if not quiet:
print(f" ✓ 锁定成功(本地对象,无需传输请求)")
except LockError:
if not quiet:
print(" 对象需要传输请求号")
corr_nr = _select_transport_request(client)
if corr_nr:
lock_handle, _ = client.lock(parsed.obj_uri, corr_nr)
if not quiet:
print(f" ✓ 锁定成功(传输请求: {corr_nr}")
else:
return False, "锁定失败: 无法获取传输请求号", None
except LockError as e:
return False, str(e), None
try:
client.set_source(parsed.src_uri, source, lock_handle, corr_nr)
if not quiet:
print(f" ✓ 源代码写入成功")
finally:
client.unlock(parsed.obj_uri, lock_handle)
if not quiet:
print(f" ✓ 解锁成功")
# ── Step 2: 语法检查 ──
if not quiet:
print("\n ── 语法检查 ──")
try:
check_ok, check_msgs = client.syntax_check(name, parsed.obj_uri)
except Exception as e:
if not quiet:
print(f" ℹ 语法检查异常(跳过,直接激活): {e}")
check_ok = True
check_msgs = []
if not check_ok:
errors = [m for m in check_msgs if m["type"] == "E"]
warnings = [m for m in check_msgs if m["type"] == "W"]
if not quiet:
print(f" ✗ 语法检查未通过! {len(errors)} 个错误, {len(warnings)} 个警告")
for e in errors:
print(f" [错误] 行 {e['line']}: {e['text']}")
for w in warnings:
print(f" [警告] 行 {w['line']}: {w['text']}")
error_summary = "; ".join(f"{e['line']}: {e['text']}" for e in errors)
return False, f"语法检查未通过: {error_summary}", corr_nr
if not quiet:
print(" ✓ 语法检查通过")
# ── Step 3: 激活 ──
if not quiet:
print("\n ── 激活对象 ──")
success, messages = client.activate(name, parsed.obj_uri, corr_nr)
if success:
if not quiet:
print(f" ✓ 激活成功")
print(f"\n {'=' * 60}")
print(f"{parsed.display_name} 已成功同步并激活")
print(f" 文件: {file_path}")
print(f" 源代码: {len(source)} 字符, {len(source.splitlines())}")
print(f" 日志: {LOG_FILE}")
print(f" {'=' * 60}")
return True, None, corr_nr
errors = [m for m in messages if m["type"] == "E"]
if not quiet:
warnings = [m for m in messages if m["type"] == "W"]
print(f" ✗ 激活失败! {len(errors)} 个错误, {len(warnings)} 个警告")
for e in errors:
print(f" [错误] 行 {e['line']}: {e['text']}")
for w in warnings:
print(f" [警告] 行 {w['line']}: {w['text']}")
error_summary = "; ".join(f"{e['line']}: {e['text']}" for e in errors)
return False, f"激活失败: {error_summary}", corr_nr
def _update_manifest_after_sync(
args: argparse.Namespace,
name: str,
obj_type: str,
file_path: str,
corr_nr: str | None,
failed: bool = False,
) -> None:
"""sync 成功/失败后尝试更新清单(如果适用)。"""
from datetime import datetime, timezone
project_path = getattr(args, "project_path", None)
if not project_path:
return
manifest_path = os.path.join(project_path, "manifest.json")
if not os.path.isfile(manifest_path):
return
try:
manifest = Manifest.load(project_path)
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
# 确定相对文件路径
try:
rel_file = os.path.relpath(file_path, project_path).replace("\\", "/")
except ValueError:
rel_file = file_path
entry = manifest.get(name)
if entry:
entry.corr_nr = corr_nr
entry.system_status = "active" if not failed else entry.system_status
entry.last_sync = now
entry.last_sync_result = "failed" if failed else "success"
else:
manifest.upsert(ManifestEntry(
name=name,
type=obj_type,
file=rel_file,
system_status="active" if not failed else "inactive",
corr_nr=corr_nr,
depends_on=[],
last_sync=now,
last_sync_result="failed" if failed else "success",
))
manifest.save()
except Exception as e:
logger.warning("更新清单失败: %s", e)
def cmd_info(args: argparse.Namespace, client: ADTClient) -> None:
"""查询对象元数据信息。"""
obj_type: str = args.type
name: str = args.name
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
INFO_ACCEPT: dict[str, str] = {
"report": "application/vnd.sap.adt.programs.programs.v2+xml",
"class": "application/vnd.sap.adt.oo.classes.v2+xml",
"interface": "application/vnd.sap.adt.oo.interfaces.v2+xml",
"function": "application/vnd.sap.adt.functions.fmodules.v2+xml",
"functiongroup": "application/vnd.sap.adt.functions.groups.v2+xml",
"domain": "*/*",
"dataelement": "*/*",
"table": "*/*",
"tabletype": "*/*",
}
print("=" * 60)
print(" SAP ADT 对象信息查询")
print("=" * 60)
print(f" 对象类型: {type_label}")
print(f" 对象名称: {parsed.display_name}")
print(f"\n → 正在查询对象信息...")
accept = INFO_ACCEPT.get(obj_type, "application/xml")
url = client.host + parsed.obj_uri
hdrs = client._headers("application/xml")
hdrs["Accept"] = accept
logger.info("INFO: GET %s", url)
resp = client.session.get(url, headers=hdrs)
logger.info("INFO RESPONSE: HTTP %s", resp.status_code)
if resp.status_code == 404:
print(f" ✗ 对象不存在: {parsed.display_name}")
raise ObjectNotFoundError(parsed.display_name, obj_type)
if resp.status_code == 406:
hdrs["Accept"] = "application/xml"
resp = client.session.get(url, headers=hdrs)
if resp.status_code != 200:
print(f" ✗ 查询失败: HTTP {resp.status_code}")
print(f" {resp.text[:200]}")
raise SapCliError(f"查询失败: HTTP {resp.status_code}")
root = ET.fromstring(resp.content)
ns = {"adtcore": "http://www.sap.com/adt/core"}
info_name = root.attrib.get(f"{{{ns['adtcore']}}}name", "")
info_type = root.attrib.get(f"{{{ns['adtcore']}}}type", "")
info_desc = root.attrib.get(f"{{{ns['adtcore']}}}description", "")
info_version = root.attrib.get(f"{{{ns['adtcore']}}}version", "")
info_changed_at = root.attrib.get(f"{{{ns['adtcore']}}}changedAt", "")
info_changed_by = root.attrib.get(f"{{{ns['adtcore']}}}changedBy", "")
info_created_by = root.attrib.get(f"{{{ns['adtcore']}}}createdBy", "")
info_responsible = root.attrib.get(f"{{{ns['adtcore']}}}responsible", "")
info_language = root.attrib.get(f"{{{ns['adtcore']}}}masterLanguage", "")
if info_version == "active":
status_icon = "✓ 已激活"
elif info_version == "inactive":
status_icon = "⚠️ 未激活"
else:
status_icon = f"{info_version}"
print(f" ✓ 查询成功\n")
print(f" {'' * 50}")
print(f" 名称: {info_name}")
print(f" 类型: {info_type}")
print(f" 描述: {info_desc}")
print(f" 状态: {status_icon}")
print(f" 负责人: {info_responsible}")
print(f" 语言: {info_language}")
print(f" 创建者: {info_created_by}")
print(f" 修改者: {info_changed_by}")
print(f" 修改时间: {info_changed_at}")
print(f" URI: {parsed.obj_uri}")
print(f" {'' * 50}")
expected_name = parsed.display_name.upper().split("/")[-1]
returned_name = info_name.upper()
if returned_name != expected_name:
print(f"\n ✗ 名称不匹配! 请求: {expected_name}, 返回: {returned_name}")
print(f" 可能查询到了错误的对象")
raise SapCliError(f"名称不匹配! 请求: {expected_name}, 返回: {returned_name}")
def cmd_delete(args: argparse.Namespace, client: ADTClient) -> None:
"""从 SAP 系统删除对象。"""
obj_type: str = args.type
name: str = args.name
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
print("=" * 60)
print(" SAP ADT 源代码删除")
print("=" * 60)
print(f" 对象类型: {type_label}")
print(f" 对象名称: {parsed.display_name}")
print(f"\n → 检查对象是否存在...")
if not client.object_exists(parsed.obj_uri):
print(f" ✗ 对象不存在: {parsed.display_name}")
print(f" 请确认名称和类型是否正确")
raise ObjectNotFoundError(parsed.display_name, obj_type)
print(" ✓ 对象存在")
print(f"\n ⚠️ 即将从 SAP 系统中删除: {parsed.display_name}")
print(f" 类型: {type_label}")
print(f" URI: {parsed.obj_uri}")
confirm = input("\n 确认删除? (输入 yes 确认): ").strip()
if confirm.lower() != "yes":
print(" 已取消删除操作")
return
corr_nr = client.get_transport_request()
if corr_nr:
print(f" ✓ 传输请求: {corr_nr}")
else:
print(" ℹ 未找到可修改的传输请求,将尝试无传输号删除")
print(f"\n → 正在删除对象...")
success, error = client.delete_object(parsed.obj_uri, corr_nr)
if success:
print(f" ✓ 删除成功!")
print(f"\n{parsed.display_name} 已从 SAP 系统中删除")
# 更新清单(如果适用)
_update_manifest_after_delete(args, name)
else:
print(f" ✗ 删除失败: {error}")
print(f"\n 可能原因:")
print(f" - 对象被其他用户锁定")
print(f" - 缺少删除权限")
print(f" - 需要传输请求号")
raise DeleteError(f"删除失败: {error}")
def _update_manifest_after_delete(
args: argparse.Namespace,
name: str,
) -> None:
"""delete 成功后尝试从清单移除(如果适用)。"""
project_path = getattr(args, "project_path", None)
if not project_path:
return
manifest_path = os.path.join(project_path, "manifest.json")
if not os.path.isfile(manifest_path):
return
try:
manifest = Manifest.load(project_path)
if manifest.remove(name):
manifest.save()
logger.info("清单已更新: 移除 %s", name)
except Exception as e:
logger.warning("更新清单失败: %s", e)
def _select_transport_request(client: ADTClient) -> str | None:
"""交互式传输请求选择:列出已有请求或新建。
Returns:
选中的传输请求编号,或 None 表示无传输号创建。
"""
print(f"\n → 查询可用的传输请求...")
try:
requests_list = client.list_transport_requests()
except Exception as e:
logger.warning("查询传输请求失败: %s", e)
print(f" ℹ 查询传输请求失败,将尝试无传输号创建")
return None
if requests_list:
print(f" 找到 {len(requests_list)} 个可修改的传输请求:\n")
for i, req in enumerate(requests_list, 1):
desc = req.get("description", "")
owner = req.get("owner", "")
print(f" {i}. {req['number']} {desc} (所有者: {owner})")
print(f" {len(requests_list) + 1}. 新建传输请求")
print(f" 0. 不使用传输请求(本地对象)")
print()
while True:
choice = input(" 请选择 [0-{}]: ".format(len(requests_list) + 1)).strip()
if not choice:
continue
try:
idx = int(choice)
except ValueError:
print(" ✗ 请输入数字")
continue
if idx == 0:
print(" ℹ 将尝试无传输号创建(本地对象 $TMP)")
return None
elif 1 <= idx <= len(requests_list):
selected = requests_list[idx - 1]["number"]
print(f" ✓ 已选择传输请求: {selected}")
return selected
elif idx == len(requests_list) + 1:
# 新建传输请求
tr_desc = input(" 请输入新传输请求描述: ").strip()
if not tr_desc:
print(" ✗ 描述不能为空,请重新选择")
continue
try:
new_nr = client.create_transport_request(tr_desc)
if new_nr:
print(f" ✓ 传输请求已创建: {new_nr}")
return new_nr
else:
print(" ✗ 创建传输请求失败(未返回编号),将尝试无传输号创建")
return None
except Exception as e:
print(f" ✗ 创建传输请求失败: {e}")
return None
else:
print(f" ✗ 请输入 0-{len(requests_list) + 1} 之间的数字")
else:
print(" 未找到可修改的传输请求")
print()
choice = input(" 是否新建传输请求? (y/n): ").strip().lower()
if choice in ("y", "yes"):
tr_desc = input(" 请输入新传输请求描述: ").strip()
if tr_desc:
try:
new_nr = client.create_transport_request(tr_desc)
if new_nr:
print(f" ✓ 传输请求已创建: {new_nr}")
return new_nr
except Exception as e:
print(f" ✗ 创建传输请求失败: {e}")
else:
print(" ✗ 描述不能为空")
print(" 将尝试无传输号创建")
return None
def cmd_create(args: argparse.Namespace, client: ADTClient) -> None:
"""在 SAP 系统创建开发对象。"""
obj_type: str = args.type
name: str = args.name
description: str = getattr(args, "description", None) or name
source_file: str | None = getattr(args, "source", None)
definition_file: str | None = getattr(args, "definition", None)
type_label = get_type_config(obj_type).label
if obj_type == "function":
if "/" not in name:
print(f" ✗ function 类型需要'函数组名/函数模块名' 格式,例如: ZGROUP/Z_MY_FUNC")
raise InvalidNameError(
"function 类型需要'函数组名/函数模块名' 格式,例如: ZGROUP/Z_MY_FUNC"
)
display_name = name
print("=" * 60)
print(" SAP ADT 创建开发对象")
print("=" * 60)
print(f" 对象类型: {type_label}")
print(f" 对象名称: {display_name}")
print(f" 描述: {description}")
print(f"\n → 检查对象是否存在...")
if obj_type != "functiongroup":
parsed = parse_object_name(name, obj_type)
if client.object_exists(parsed.obj_uri):
print(f" ✗ 对象已存在: {display_name}")
raise ObjectAlreadyExistsError(display_name, type_label)
else:
if client.function_group_exists(name):
print(f" ✗ 函数组已存在: {name}")
raise ObjectAlreadyExistsError(name, type_label)
print(" ✓ 对象不存在,可以创建")
# ── 传输请求选择 ──
corr_nr = getattr(args, "corr_nr", None)
if corr_nr:
# 非交互模式:用户通过 --corr_nr 指定
print(f" ✓ 传输请求(指定): {corr_nr}")
else:
corr_nr = _select_transport_request(client)
if obj_type == "function":
group_name = name.split("/", 1)[0]
if not client.function_group_exists(group_name):
print(f"\n → 函数组 {group_name} 不存在,自动创建...")
try:
client.create_function_group(group_name, corr_nr=corr_nr)
print(f" ✓ 函数组 {group_name} 创建成功")
except Exception as e:
print(f" ✗ 函数组创建失败: {e}")
raise CreateError(f"函数组创建失败: {e}") from e
else:
print(f" ✓ 函数组 {group_name} 已存在")
if obj_type == "functiongroup":
print(f"\n → 正在创建函数组...")
try:
created_uri = client.create_function_group(name, description, corr_nr)
except Exception as e:
print(f" ✗ 创建失败: {e}")
raise CreateError(str(e)) from e
print(f" ✓ 函数组创建成功")
print(f" ✓ URI: {created_uri}")
print(f"\n{name} 创建完成!")
print(f" 类型: 函数组(FUNCTION GROUP)")
print(f" 描述: {description}")
return
if obj_type in DDIC_TYPES:
_create_ddic(args, client, obj_type, name, description, corr_nr, definition_file)
return
if source_file:
with open(source_file, "r", encoding="utf-8") as f:
source = f.read()
else:
template_name = name.split("/", 1)[-1].upper() if obj_type == "function" else name.upper()
source = DEFAULT_TEMPLATES[obj_type].format(name=template_name)
print(f"\n → 正在创建对象...")
try:
created_uri, created_src_uri = client.create_object(
obj_type, name, description, corr_nr, source
)
except Exception as e:
print(f" ✗ 创建失败: {e}")
raise CreateError(str(e)) from e
print(f" ✓ 对象创建成功!")
print(f" ✓ URI: {created_uri}")
if source:
print(f" ✓ 源代码已写入并激活 ({len(source)} 字符)")
print(f"\n{display_name} 创建完成!")
print(f" 类型: {type_label}")
print(f" 描述: {description}")
# 更新清单(如果适用)
_update_manifest_after_create(args, name, obj_type, corr_nr)
def _update_manifest_after_create(
args: argparse.Namespace,
name: str,
obj_type: str,
corr_nr: str | None,
) -> None:
"""create 成功后尝试写入清单(如果适用)。"""
from datetime import datetime, timezone
project_path = getattr(args, "project_path", None)
if not project_path:
return
manifest_path = os.path.join(project_path, "manifest.json")
if not os.path.isfile(manifest_path):
return
try:
manifest = Manifest.load(project_path)
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
# 根据类型推断文件路径
from sapcli.scanner import DIRECTORY_TYPE_MAP
type_to_dir = {v: k for k, v in DIRECTORY_TYPE_MAP.items()}
dir_name = type_to_dir.get(obj_type, "")
if obj_type == "function" and "/" in name:
group, func = name.split("/", 1)
rel_file = f"functions/{group.lower()}/{func.lower()}.abap"
elif dir_name:
file_base = name.split("/", 1)[-1].lower()
rel_file = f"{dir_name}/{file_base}.abap"
else:
rel_file = ""
manifest.upsert(ManifestEntry(
name=name,
type=obj_type,
file=rel_file,
system_status="active",
corr_nr=corr_nr,
depends_on=[],
last_sync=now,
last_sync_result="success",
))
manifest.save()
logger.info("清单已更新: 新增 %s", name)
except Exception as e:
logger.warning("更新清单失败: %s", e)
def _create_ddic(
args: argparse.Namespace,
client: ADTClient,
obj_type: str,
name: str,
description: str,
corr_nr: str | None,
definition_file: str | None,
) -> None:
"""创建 DDIC 对象(domain/dataelement/table/structure/tabletype)。"""
from sapcli.ddic import (
DomainDefinition,
DataElementDefinition,
TableDefinition,
StructureDefinition,
TableTypeDefinition,
TableField,
)
type_label = get_type_config(obj_type).label
definition_body: str | None = None
if definition_file:
import json
with open(definition_file, "r", encoding="utf-8") as f:
data = json.load(f)
if obj_type == "domain":
defn = DomainDefinition(
datatype=data.get("datatype", "CHAR"),
length=data.get("length", 10),
decimals=data.get("decimals", 0),
lowercase=data.get("lowercase", False),
fix_values=data.get("fix_values", []),
)
definition_body = defn.to_xml(name.upper(), description)
elif obj_type == "dataelement":
defn = DataElementDefinition(
datatype=data.get("datatype", "CHAR"),
length=data.get("length", 10),
decimals=data.get("decimals", 0),
domain_name=data.get("domain_name", ""),
)
definition_body = defn.to_xml(name.upper(), description)
elif obj_type == "table":
fields = [
TableField(
name=f.get("name", ""),
type_name=f.get("type", ""),
is_key=f.get("key", False),
not_null=f.get("not_null", False),
)
for f in data.get("fields", [])
]
defn = TableDefinition(
fields=fields,
enhancement_category=data.get("enhancement_category", "#NOT_CLASSIFIED"),
delivery_class=data.get("delivery_class", "#A"),
data_maintenance=data.get("data_maintenance", "#LIMITED"),
table_category=data.get("table_category", "#TRANSPARENT"),
)
definition_body = defn.to_ddl(name.lower(), description)
elif obj_type == "structure":
fields = [
TableField(
name=f.get("name", ""),
type_name=f.get("type", ""),
is_key=f.get("key", False),
not_null=f.get("not_null", False),
)
for f in data.get("fields", [])
]
defn = StructureDefinition(
fields=fields,
enhancement_category=data.get("enhancement_category", "#NOT_CLASSIFIED"),
)
definition_body = defn.to_ddl(name.lower(), description)
elif obj_type == "tabletype":
defn = TableTypeDefinition(
line_type=data.get("line_type", ""),
key_type=data.get("key_type", "#USER_DEFINED"),
access_mode=data.get("access_mode", "#STANDARD"),
)
definition_body = defn.to_xml(name.upper(), description)
if definition_body is None:
if obj_type == "domain":
defn = DomainDefinition()
definition_body = defn.to_xml(name.upper(), description)
elif obj_type == "dataelement":
defn = DataElementDefinition()
definition_body = defn.to_xml(name.upper(), description)
elif obj_type == "table":
defn = TableDefinition(fields=[
TableField(name="key_field", type_name="char10", not_null=True),
])
definition_body = defn.to_ddl(name.lower(), description)
elif obj_type == "structure":
defn = StructureDefinition(fields=[
TableField(name="field1", type_name="char10"),
])
definition_body = defn.to_ddl(name.lower(), description)
elif obj_type == "tabletype":
print(f" ✗ tabletype 需要通过 --definition 指定 line_type")
raise CreateError("tabletype 需要通过 --definition 指定 line_type")
print(f"\n → 正在创建 DDIC 对象...")
try:
created_uri, created_src_uri = client.create_ddic_object(
obj_type, name, definition_body, corr_nr
)
except Exception as e:
print(f" ✗ 创建失败: {e}")
raise CreateError(str(e)) from e
print(f" ✓ DDIC 对象创建并激活成功!")
print(f" ✓ URI: {created_uri}")
print(f"\n{name} 创建完成!")
print(f" 类型: {type_label}")
print(f" 描述: {description}")
+100
View File
@@ -0,0 +1,100 @@
"""DDL query commands — show table fields, read table data."""
from __future__ import annotations
from sapcli.cli.output import print_error, print_info
def cmd_show_table(args, client) -> None:
"""查看 DDIC 表字段结构。"""
table_name = args.name.upper()
print_info(f"查询表 {table_name} 的字段结构...")
fields = client.get_table_fields(table_name)
if not fields:
print_error(f"{table_name} 无字段信息")
return
print()
print("=" * 72)
print(f"{table_name} — 字段结构")
print("=" * 72)
print(f" {'字段名':<20} {'类型':<6} {'长度':<6} {'Key':<5} {'描述'}")
print("-" * 72)
for f in fields:
print(f" {f['name']:<20} {f['type']:<6} {f['length']:<6} {f['key_attribute']:<5} {f['description']}")
print("-" * 72)
print(f"{len(fields)} 个字段")
print()
def cmd_read_table(args, client) -> None:
"""查询表数据(ADT freestyle SQL)。"""
table_name = args.name.upper()
max_rows = getattr(args, "max_rows", 200)
where = getattr(args, "where", None)
fields = getattr(args, "fields", None)
select = fields if fields else "*"
sql = f"SELECT {select} FROM {table_name}"
if where:
sql += f" WHERE {where}"
sql += f" UP TO {max_rows} ROWS"
print_info(f"执行 SQL: {sql}")
result = client.query_table_data(sql, max_rows=max_rows)
columns = result["columns"]
rows = result["rows"]
total = result["total_rows"]
if not rows:
print()
print_error(f"{table_name} 无数据({total} 行)")
print()
return
# 计算列宽
col_widths = []
for i, col in enumerate(columns):
max_w = len(col)
for row in rows:
if i < len(row):
max_w = max(max_w, min(len(str(row[i])), 40))
col_widths.append(max_w + 2)
# 限制总宽度
total_width = sum(col_widths) + len(columns) + 1
if total_width > 200:
# 截断过宽的列
scale = 200 / total_width
col_widths = [max(int(w * scale), 6) for w in col_widths]
sep = "+" + "+".join("-" * w for w in col_widths) + "+"
print()
print(sep)
# 表头
header = "|"
for i, col in enumerate(columns):
w = col_widths[i] if i < len(col_widths) else 10
header += f" {col:<{w-1}}|"
print(header)
print(sep)
# 数据行
for row in rows:
line = "|"
for i, val in enumerate(row):
w = col_widths[i] if i < len(col_widths) else 10
s = str(val)[:w-1]
line += f" {s:<{w-1}}|"
print(line)
print(sep)
exec_time = result.get("execution_time", "")
time_info = f" ({exec_time}ms)" if exec_time else ""
print(f" {len(rows)}{time_info}")
print()
+89
View File
@@ -0,0 +1,89 @@
"""代码差异对比命令:diff。"""
from __future__ import annotations
import argparse
import difflib
import logging
import os
from sapcli.client import ADTClient
from sapcli.types import get_type_config, parse_object_name
logger = logging.getLogger("sapcli.commands.diff_cmd")
def cmd_diff(args: argparse.Namespace, client: ADTClient) -> None:
"""本地 vs SAP 代码差异对比。"""
name: str = args.name
obj_type: str = args.type
local_path: str | None = getattr(args, "path", None)
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
if parsed.src_uri is None:
print(f"\n{type_label}没有源代码,不支持 diff 操作")
return
print("=" * 60)
print(" SAP ADT 代码差异对比")
print("=" * 60)
print(f" 对象名称: {parsed.display_name}")
print(f" 对象类型: {type_label}")
# 读取本地文件
if local_path is None:
# 尝试自动推断文件路径
local_path = f"{parsed.file_base}.abap"
if not os.path.isfile(local_path):
print(f"\n ✗ 本地文件不存在: {local_path}")
return
with open(local_path, "r", encoding="utf-8") as f:
local_source = f.read()
local_lines = local_source.splitlines(keepends=True)
print(f" 本地文件: {local_path} ({len(local_lines)} 行)")
# 读取 SAP 端源码
print(f"\n → 正在读取 SAP 端源码...")
try:
sap_source = client.read_source_for_diff(name, obj_type)
except Exception as e:
print(f" ✗ 读取 SAP 源码失败: {e}")
return
sap_source = sap_source.replace("\r\n", "\n").replace("\r", "\n")
sap_lines = sap_source.splitlines(keepends=True)
print(f" ✓ SAP 源码读取成功 ({len(sap_lines)} 行)")
# 生成 unified diff
diff_lines = list(difflib.unified_diff(
sap_lines,
local_lines,
fromfile=f"SAP:{parsed.display_name}",
tofile=f"本地:{os.path.basename(local_path)}",
lineterm="",
))
if not diff_lines:
print(f"\n ✓ 本地文件与 SAP 端完全一致,无差异")
return
print(f"\n {'' * 60}")
print(f" 差异摘要:")
added = sum(1 for l in diff_lines if l.startswith("+") and not l.startswith("+++"))
removed = sum(1 for l in diff_lines if l.startswith("-") and not l.startswith("---"))
print(f" 新增行: {added} | 删除行: {removed}")
print(f" {'' * 60}\n")
# 输出 diff
for line in diff_lines:
if line.startswith("+") and not line.startswith("+++"):
print(f" \033[32m{line}\033[0m")
elif line.startswith("-") and not line.startswith("---"):
print(f" \033[31m{line}\033[0m")
elif line.startswith("@@"):
print(f" \033[36m{line}\033[0m")
else:
print(f" {line}")
+107
View File
@@ -0,0 +1,107 @@
"""包管理命令:package (create / info / list)。"""
from __future__ import annotations
import argparse
import logging
from sapcli.client import ADTClient
logger = logging.getLogger("sapcli.commands.package_cmd")
def cmd_package(args: argparse.Namespace, client: ADTClient) -> None:
"""ABAP 包操作。"""
action = getattr(args, "package_action", None)
if action == "create":
_package_create(args, client)
elif action == "info":
_package_info(args, client)
elif action == "list":
_package_list(args, client)
else:
print(" 用法: sap-cli package [create|info|list]")
print(" create — 创建 ABAP 包")
print(" info — 查看包详情")
print(" list — 列出对象(通过 list 命令按包过滤)")
def _package_create(args: argparse.Namespace, client: ADTClient) -> None:
"""创建 ABAP 包。"""
name: str = args.name
description: str = getattr(args, "description", None) or name
superpackage: str | None = getattr(args, "superpackage", None)
print("=" * 60)
print(" SAP 创建 ABAP 包")
print("=" * 60)
print(f" 包名: {name}")
print(f" 描述: {description}")
if superpackage:
print(f" 上级包: {superpackage}")
print(f"\n → 正在创建包...")
try:
success = client.create_package(name, description, superpackage)
except Exception as e:
print(f" ✗ 创建失败: {e}")
return
if success:
print(f" ✓ 包 {name} 创建成功!")
else:
print(f" ✗ 包创建失败")
def _package_info(args: argparse.Namespace, client: ADTClient) -> None:
"""查看包详情。"""
name: str = args.name
print("=" * 60)
print(" SAP 包信息")
print("=" * 60)
print(f" 包名: {name}")
print(f"\n → 正在查询...")
try:
info = client.get_package_info(name)
except Exception as e:
print(f" ✗ 查询失败: {e}")
return
print(f"\n {'' * 50}")
print(f" 名称: {info.get('name', '')}")
print(f" 描述: {info.get('description', '')}")
print(f" 所有者: {info.get('owner', '')}")
print(f" 上级包: {info.get('superpackage', '(无)')}")
print(f" {'' * 50}")
def _package_list(args: argparse.Namespace, client: ADTClient) -> None:
"""列出包中的对象。"""
name: str = args.name
print("=" * 60)
print(" SAP 包对象列表")
print("=" * 60)
print(f" 包名: {name}")
print(f"\n → 正在查询...")
try:
results = client.list_objects(package=name)
except Exception as e:
print(f" ✗ 查询失败: {e}")
return
if not results:
print(" 包中没有找到对象")
return
print(f" ✓ 找到 {len(results)} 个对象\n")
print(f" {'名称':<30s} {'类型':<12s} {'描述':<30s}")
print(f" {'' * 30} {'' * 12} {'' * 30}")
for obj in results:
obj_name = obj.get("name", "")[:30]
obj_type = obj.get("type", "")[:12]
desc = obj.get("description", "")[:30]
print(f" {obj_name:<30s} {obj_type:<12s} {desc:<30s}")
+20
View File
@@ -0,0 +1,20 @@
"""Program execution command — remotely run ABAP programs via ADT."""
from __future__ import annotations
from sapcli.cli.output import print_info, print_success, print_error
def cmd_run_program(args, client) -> None:
"""远程执行 ABAP 程序。"""
program_name = args.name.upper()
print_info(f"远程执行程序 {program_name}...")
output = client.run_program(program_name)
if output.strip():
print()
print(output.rstrip())
else:
print()
print_info("程序执行完成,无输出。")
+131
View File
@@ -0,0 +1,131 @@
"""代码质量命令:check (ATC) / format (Pretty Printer)。"""
from __future__ import annotations
import argparse
import logging
from sapcli.client import ADTClient
from sapcli.types import get_type_config, parse_object_name
logger = logging.getLogger("sapcli.commands.quality")
def cmd_check(args: argparse.Namespace, client: ADTClient) -> None:
"""ATC 代码检查。"""
name: str = args.name
obj_type: str = args.type
variant: str | None = getattr(args, "variant", None)
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
print("=" * 60)
print(" SAP ATC 代码检查")
print("=" * 60)
print(f" 对象名称: {parsed.display_name}")
print(f" 对象类型: {type_label}")
if variant:
print(f" 检查变体: {variant}")
print(f"\n → 正在执行 ATC 检查...")
try:
success, findings = client.atc_check(name, parsed.obj_uri, variant)
except Exception as e:
print(f" ✗ ATC 检查失败: {e}")
return
if success and not findings:
print(" ✓ ATC 检查通过,无发现项")
return
errors = [f for f in findings if f["type"] in ("E", "1")]
warnings = [f for f in findings if f["type"] in ("W", "2")]
infos = [f for f in findings if f["type"] in ("I", "3")]
print(f"\n {'' * 50}")
if success:
print(f" ✓ ATC 检查完成(无严重错误)")
else:
print(f" ✗ ATC 检查发现 {len(errors)} 个错误")
print(f" 错误: {len(errors)} | 警告: {len(warnings)} | 信息: {len(infos)}")
print(f" {'' * 50}")
for finding in findings:
severity_icon = {"E": "", "1": "", "W": "", "2": "", "I": "", "3": ""}.get(
finding["type"], "?"
)
line = finding.get("line", "?")
text = finding.get("text", "")
print(f" {severity_icon} [{finding['type']}] 行 {line}: {text}")
def cmd_format(args: argparse.Namespace, client: ADTClient) -> None:
"""代码格式化(ABAP Pretty Printer)。"""
name: str = args.name
obj_type: str = args.type
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
if parsed.src_uri is None:
print(f"\n{type_label}没有源代码,不支持格式化操作")
return
print("=" * 60)
print(" SAP ABAP 代码格式化")
print("=" * 60)
print(f" 对象名称: {parsed.display_name}")
print(f" 对象类型: {type_label}")
# 读取当前源码
print(f"\n → 正在读取源代码...")
try:
source = client.get_source(parsed.src_uri)
except Exception as e:
print(f" ✗ 读取源代码失败: {e}")
return
print(f" ✓ 源代码读取成功 ({len(source)} 字符)")
# 调用 Pretty Printer
print(f"\n → 正在调用 Pretty Printer...")
try:
formatted = client.pretty_print(source)
except Exception as e:
print(f" ✗ 格式化失败: {e}")
return
if formatted == source:
print(" 代码已经是格式化的,无需修改")
return
print(f" ✓ 格式化完成")
# 写回 SAP
print(f"\n → 正在写回 SAP...")
try:
lock_handle, corr_nr = client.lock(parsed.obj_uri)
try:
client.set_source(parsed.src_uri, formatted, lock_handle, corr_nr)
print(f" ✓ 源代码已写回")
finally:
client.unlock(parsed.obj_uri, lock_handle)
except Exception as e:
print(f" ✗ 写回失败: {e}")
return
# 激活
try:
success, messages = client.activate(name, parsed.obj_uri)
if success:
print(f" ✓ 激活成功")
else:
errors = [m for m in messages if m["type"] == "E"]
print(f" ⚠ 激活返回 {len(errors)} 个错误")
for e in errors:
print(f" [错误] 行 {e['line']}: {e['text']}")
except Exception as e:
print(f" ⚠ 激活失败: {e}")
print(f"\n ✓ 格式化操作完成!")
+251
View File
@@ -0,0 +1,251 @@
"""项目模板命令:scaffold。"""
from __future__ import annotations
import argparse
import logging
import os
from sapcli.client import ADTClient
logger = logging.getLogger("sapcli.commands.scaffold")
def _alv_report_template(name: str, package: str) -> str:
"""生成 ALV 报表模板。"""
return (
f"REPORT {name.upper()}.\n"
f"\n"
f"TYPES: BEGIN OF ty_data,\n"
f" field1 TYPE char10,\n"
f" field2 TYPE char20,\n"
f" END OF ty_data.\n"
f"\n"
f"DATA: gt_data TYPE STANDARD TABLE OF ty_data,\n"
f" gs_data TYPE ty_data,\n"
f" go_alv TYPE REF TO cl_salv_table,\n"
f" go_msg TYPE REF TO cx_salv_msg.\n"
f"\n"
f"START-OF-SELECTION.\n"
f" PERFORM get_data.\n"
f" PERFORM display_alv.\n"
f"\n"
f"FORM get_data.\n"
f" \" TODO: 填充数据\n"
f" gs_data-field1 = '示例'.\n"
f" gs_data-field2 = '数据'.\n"
f" APPEND gs_data TO gt_data.\n"
f"ENDFORM.\n"
f"\n"
f"FORM display_alv.\n"
f" TRY.\n"
f" cl_salv_table=>factory(\n"
f" IMPORTING\n"
f" r_salv_table = go_alv\n"
f" CHANGING\n"
f" t_table = gt_data\n"
f" ).\n"
f" go_alv->display( ).\n"
f" CATCH cx_salv_msg INTO go_msg.\n"
f" MESSAGE go_msg->get_text( ) TYPE 'I'.\n"
f" ENDTRY.\n"
f"ENDFORM.\n"
)
def _bapi_wrapper_template(name: str, package: str) -> str:
"""生成 BAPI 包装类模板。"""
class_name = name.upper()
return (
f"CLASS {class_name} DEFINITION\n"
f" PUBLIC\n"
f" FINAL\n"
f" CREATE PUBLIC.\n"
f"\n"
f" PUBLIC SECTION.\n"
f" TYPES: BEGIN OF ty_result,\n"
f" success TYPE abap_bool,\n"
f" message TYPE string,\n"
f" END OF ty_result.\n"
f"\n"
f" CLASS-METHODS:\n"
f" call_bapi\n"
f" IMPORTING\n"
f" iv_param1 TYPE string OPTIONAL\n"
f" RETURNING\n"
f" VALUE(rs_result) TYPE ty_result.\n"
f"\n"
f" PRIVATE SECTION.\n"
f" CLASS-METHODS:\n"
f" _call_remote_bapi\n"
f" IMPORTING\n"
f" iv_param1 TYPE string\n"
f" EXPORTING\n"
f" ev_success TYPE abap_bool\n"
f" ev_message TYPE string.\n"
f"ENDCLASS.\n"
f"\n"
f"\n"
f"CLASS {class_name} IMPLEMENTATION.\n"
f"\n"
f" METHOD call_bapi.\n"
f" _call_remote_bapi(\n"
f" EXPORTING\n"
f" iv_param1 = iv_param1\n"
f" IMPORTING\n"
f" ev_success = rs_result-success\n"
f" ev_message = rs_result-message\n"
f" ).\n"
f" ENDMETHOD.\n"
f"\n"
f" METHOD _call_remote_bapi.\n"
f" \" TODO: 调用 BAPI 函数\n"
f" ev_success = abap_true.\n"
f" ev_message = '未实现'.\n"
f" ENDMETHOD.\n"
f"\n"
f"ENDCLASS.\n"
)
def _interface_class_template(name: str, package: str) -> str:
"""生成接口 + 实现类模板。"""
name_upper = name.upper()
if name_upper.startswith("Z"):
if_name = f"ZIF_{name_upper[2:]}"
else:
if_name = f"ZIF_{name_upper}"
cls_name = name_upper
return (
f"INTERFACE {if_name}\n"
f" PUBLIC.\n"
f" METHODS:\n"
f" execute\n"
f" RETURNING VALUE(rv_result) TYPE string.\n"
f"ENDINTERFACE.\n"
f"\n"
f"\n"
f"CLASS {cls_name} DEFINITION\n"
f" PUBLIC\n"
f" FINAL\n"
f" CREATE PUBLIC.\n"
f"\n"
f" PUBLIC SECTION.\n"
f" INTERFACES: {if_name}.\n"
f"\n"
f" PRIVATE SECTION.\n"
f" DATA: mv_state TYPE string.\n"
f"ENDCLASS.\n"
f"\n"
f"\n"
f"CLASS {cls_name} IMPLEMENTATION.\n"
f"\n"
f" METHOD {if_name}~execute.\n"
f" rv_result = 'Hello from {cls_name}'.\n"
f" ENDMETHOD.\n"
f"\n"
f"ENDCLASS.\n"
)
def _data_model_template(name: str, package: str) -> str:
"""生成数据模型模板(DDIC DDL)。"""
name_lower = name.lower()
return (
f"@EndUserText.label: '{name.upper()} 数据模型'\n"
f"@AbapCatalog.enhancementCategory: #NOT_CLASSIFIED\n"
f"@AbapCatalog.tableCategory: #TRANSPARENT\n"
f"@AbapCatalog.deliveryClass: #A\n"
f"@AbapCatalog.dataMaintenance: #LIMITED\n"
f"define table {name_lower} {{\n"
f" key_client : abap.clnt not null;\n"
f" key_id : abap.char10 not null;\n"
f" name : abap.char40;\n"
f" created_at : timestampl;\n"
f" changed_at : timestampl;\n"
f"}}\n"
)
# 模板注册表
_TEMPLATES: dict[str, dict] = {
"alv-report": {
"desc": "ALV 报表模板",
"file_name": "{name_lower}.abap",
"dir": "reports",
"generator": _alv_report_template,
},
"bapi-wrapper": {
"desc": "BAPI 包装类模板",
"file_name": "{name_lower}.abap",
"dir": "classes",
"generator": _bapi_wrapper_template,
},
"interface-class": {
"desc": "接口 + 实现类模板",
"file_name": "{name_lower}.abap",
"dir": "classes",
"generator": _interface_class_template,
},
"data-model": {
"desc": "数据模型模板 (DDIC DDL)",
"file_name": "{name_lower}.ddl",
"dir": "tables",
"generator": _data_model_template,
},
}
def cmd_scaffold(args: argparse.Namespace, client: ADTClient | None = None) -> None:
"""项目模板创建。"""
# 不指定模板时,列出可用模板
template = getattr(args, "template", None)
if not template:
print("=" * 60)
print(" sap-cli 可用模板")
print("=" * 60)
for key, info in _TEMPLATES.items():
desc = info.get("description", "")
print(f" {key:20s} {desc}")
print()
print(" 用法: sap-cli scaffold --name ZXXX --template <模板名>")
return
name: str = args.name
package: str = getattr(args, "package", "$TMP") or "$TMP"
output_dir: str = getattr(args, "path", ".") or "."
template_info = _TEMPLATES.get(template)
if not template_info:
print(f" ✗ 不支持的模板类型: {template}")
print(f" 可用模板: {', '.join(_TEMPLATES.keys())}")
return
print("=" * 60)
print(" sap-cli 项目模板创建")
print("=" * 60)
print(f" 对象名称: {name}")
print(f" 模板: {template} ({template_info['desc']})")
print(f" 包: {package}")
print(f" 输出目录: {output_dir}")
# 生成源码
name_lower = name.lower()
source = template_info["generator"](name, package)
# 确定输出路径
target_dir = os.path.join(output_dir, template_info["dir"])
if not os.path.isdir(target_dir):
os.makedirs(target_dir, exist_ok=True)
filename = template_info["file_name"].format(name_lower=name_lower)
filepath = os.path.join(target_dir, filename)
with open(filepath, "w", encoding="utf-8") as f:
f.write(source)
print(f"\n ✓ 模板已生成!")
print(f" 文件: {filepath}")
print(f" 大小: {len(source)} 字符")
print(f"\n 下一步:")
print(f" 1. 编辑 {filepath} 完善代码")
print(f" 2. 运行 sap-cli sync --name {name} --type report --path {filepath}")
+147
View File
@@ -0,0 +1,147 @@
"""搜索与浏览命令:list / whereused / search。"""
from __future__ import annotations
import argparse
import logging
from sapcli.client import ADTClient
from sapcli.types import get_type_config, parse_object_name
logger = logging.getLogger("sapcli.commands.search")
# 对象类型关键字 → ADT 类型代码映射
_TYPE_FILTER_MAP: dict[str, str] = {
"report": "PROG/P",
"class": "CLAS/OC",
"interface": "INTF/OI",
"function": "FUNC/F",
"functiongroup": "FUGR/F",
"domain": "DOMA/DD",
"dataelement": "DTEL/DE",
"table": "TABL/TT",
"structure": "TABL/ST",
"tabletype": "TTYP",
"include": "PROG/I",
"cdsview": "DDLS/DF",
"messageclass": "MSAG",
"view": "VIEW",
}
def cmd_list(args: argparse.Namespace, client: ADTClient) -> None:
"""列出 SAP 对象。"""
obj_type = getattr(args, "type", None)
package = getattr(args, "package", None)
prefix = getattr(args, "prefix", None)
# 将友好类型名转换为 ADT 类型代码
adt_type = None
if obj_type:
adt_type = _TYPE_FILTER_MAP.get(obj_type, obj_type)
print("=" * 60)
print(" SAP ADT 对象列表")
print("=" * 60)
if obj_type:
print(f" 类型过滤: {obj_type}")
if package:
print(f" 包过滤: {package}")
if prefix:
print(f" 名称前缀: {prefix}")
print(f"\n → 正在搜索对象...")
try:
results = client.list_objects(obj_type=adt_type, package=package, prefix=prefix)
except Exception as e:
print(f" ✗ 搜索失败: {e}")
return
if not results:
print(" 未找到匹配的对象")
return
print(f" ✓ 找到 {len(results)} 个对象\n")
print(f" {'名称':<30s} {'类型':<12s} {'':<15s} {'描述':<30s}")
print(f" {'' * 30} {'' * 12} {'' * 15} {'' * 30}")
for obj in results:
name = obj.get("name", "")[:30]
otype = obj.get("type", "")[:12]
pkg = obj.get("package", "")[:15]
desc = obj.get("description", "")[:30]
print(f" {name:<30s} {otype:<12s} {pkg:<15s} {desc:<30s}")
def cmd_whereused(args: argparse.Namespace, client: ADTClient) -> None:
"""Where-Used 引用查询。"""
name: str = args.name
obj_type: str | None = getattr(args, "type", None)
parsed = parse_object_name(name, obj_type or "report")
type_label = get_type_config(obj_type or "report").label
print("=" * 60)
print(" SAP ADT Where-Used 查询")
print("=" * 60)
print(f" 对象名称: {parsed.display_name}")
if obj_type:
print(f" 对象类型: {type_label}")
print(f"\n → 正在查询引用关系...")
adt_type = _TYPE_FILTER_MAP.get(obj_type, obj_type) if obj_type else None
try:
results = client.where_used(name, parsed.obj_uri, adt_type)
except Exception as e:
print(f" ✗ 查询失败: {e}")
return
if not results:
print(" 未找到引用关系")
return
print(f" ✓ 找到 {len(results)} 个引用\n")
print(f" {'名称':<30s} {'类型':<12s} {'':<15s} {'URI':<40s}")
print(f" {'' * 30} {'' * 12} {'' * 15} {'' * 40}")
for ref in results:
ref_name = ref.get("name", "")[:30]
ref_type = ref.get("type", "")[:12]
ref_pkg = ref.get("package", "")[:15]
ref_uri = ref.get("uri", "")[:40]
print(f" {ref_name:<30s} {ref_type:<12s} {ref_pkg:<15s} {ref_uri:<40s}")
def cmd_search(args: argparse.Namespace, client: ADTClient) -> None:
"""源代码搜索。"""
query: str = args.query
obj_type: str | None = getattr(args, "type", None)
# 将友好类型名转换为 ADT 类型代码
adt_type = None
if obj_type:
adt_type = _TYPE_FILTER_MAP.get(obj_type, obj_type)
print("=" * 60)
print(" SAP ADT 源代码搜索")
print("=" * 60)
print(f" 搜索关键词: {query}")
if obj_type:
print(f" 类型过滤: {obj_type}")
print(f"\n → 正在搜索...")
try:
results = client.search_code(query, obj_type=adt_type)
except Exception as e:
print(f" ✗ 搜索失败: {e}")
return
if not results:
print(" 未找到匹配的代码")
return
print(f" ✓ 找到 {len(results)} 个结果\n")
print(f" {'名称':<30s} {'类型':<12s} {'描述':<40s}")
print(f" {'' * 30} {'' * 12} {'' * 40}")
for obj in results:
obj_name = obj.get("name", "")[:30]
obj_type_val = obj.get("type", "")[:12]
desc = obj.get("description", "")[:40]
print(f" {obj_name:<30s} {obj_type_val:<12s} {desc:<40s}")
+136
View File
@@ -0,0 +1,136 @@
"""传输管理命令:transport (list / info / release / objects)。"""
from __future__ import annotations
import argparse
import logging
from sapcli.client import ADTClient
logger = logging.getLogger("sapcli.commands.transport")
def cmd_transport(args: argparse.Namespace, client: ADTClient) -> None:
"""传输请求管理。"""
action = getattr(args, "transport_action", None)
if action == "list":
_transport_list(args, client)
elif action == "info":
_transport_info(args, client)
elif action == "release":
_transport_release(args, client)
elif action == "objects":
_transport_objects(args, client)
else:
print(" 用法: sap-cli transport [list|info|release|objects]")
print(" list — 列出可修改的传输请求")
print(" info — 查看传输请求详情")
print(" release — 释放传输请求")
print(" objects — 列出传输请求中的对象")
def _transport_list(args: argparse.Namespace, client: ADTClient) -> None:
"""列出可修改的传输请求。"""
print("=" * 60)
print(" SAP 传输请求列表")
print("=" * 60)
print(f"\n → 正在查询传输请求...")
try:
requests_list = client.list_transport_requests()
except Exception as e:
print(f" ✗ 查询失败: {e}")
return
if not requests_list:
print(" 未找到可修改的传输请求")
return
print(f" ✓ 找到 {len(requests_list)} 个传输请求\n")
print(f" {'编号':<15s} {'描述':<40s} {'所有者':<15s}")
print(f" {'' * 15} {'' * 40} {'' * 15}")
for req in requests_list:
num = req.get("number", "")[:15]
desc = req.get("description", "")[:40]
owner = req.get("owner", "")[:15]
print(f" {num:<15s} {desc:<40s} {owner:<15s}")
def _transport_info(args: argparse.Namespace, client: ADTClient) -> None:
"""查看传输请求详情。"""
corr_nr: str = args.corr_nr
print("=" * 60)
print(" SAP 传输请求详情")
print("=" * 60)
print(f" 传输请求: {corr_nr}")
print(f"\n → 正在查询...")
try:
info = client.transport_info(corr_nr)
except Exception as e:
print(f" ✗ 查询失败: {e}")
return
print(f"\n {'' * 50}")
print(f" 描述: {info.get('description', '')}")
print(f" 状态: {info.get('status', '')}")
print(f" 所有者: {info.get('owner', '')}")
print(f" {'' * 50}")
def _transport_release(args: argparse.Namespace, client: ADTClient) -> None:
"""释放传输请求。"""
corr_nr: str = args.corr_nr
print("=" * 60)
print(" SAP 释放传输请求")
print("=" * 60)
print(f" 传输请求: {corr_nr}")
print(f"\n ⚠️ 即将释放传输请求: {corr_nr}")
confirm = input(" 确认释放? (输入 yes 确认): ").strip()
if confirm.lower() != "yes":
print(" 已取消释放操作")
return
print(f"\n → 正在释放...")
try:
success = client.transport_release(corr_nr)
except Exception as e:
print(f" ✗ 释放失败: {e}")
return
if success:
print(f" ✓ 传输请求 {corr_nr} 已成功释放!")
else:
print(f" ✗ 释放失败,请检查传输请求状态")
def _transport_objects(args: argparse.Namespace, client: ADTClient) -> None:
"""列出传输请求中的对象。"""
corr_nr: str = args.corr_nr
print("=" * 60)
print(" SAP 传输请求对象列表")
print("=" * 60)
print(f" 传输请求: {corr_nr}")
print(f"\n → 正在查询...")
try:
objects = client.transport_objects(corr_nr)
except Exception as e:
print(f" ✗ 查询失败: {e}")
return
if not objects:
print(" 传输请求中无对象")
return
print(f" ✓ 找到 {len(objects)} 个对象\n")
print(f" {'名称':<30s} {'类型':<15s}")
print(f" {'' * 30} {'' * 15}")
for obj in objects:
obj_name = obj.get("name", "")[:30]
obj_type = obj.get("type", "")[:15]
print(f" {obj_name:<30s} {obj_type:<15s}")