Files

1636 lines
65 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using SAP.Middleware.Connector;
namespace SapProbe
{
internal static class Program
{
private static int Main(string[] args)
{
Console.OutputEncoding = Encoding.UTF8;
try
{
var options = CliOptions.Parse(args);
if (options.ShowHelp)
{
PrintHelp();
return 0;
}
PrintBanner(options);
if (Environment.Is64BitProcess)
{
Console.Error.WriteLine("ERROR: This tool must run as x86 because SAP NCo is installed in the 32-bit GAC.");
return 2;
}
if (options.Command == SapCommand.LoadOnly)
{
Console.WriteLine("Connector load : OK");
return 0;
}
var destination = CreateDestination(options);
destination.Ping();
if (!options.Quiet)
{
Console.WriteLine("Target : " + options.AppServerHost + " / SYSNR " + options.SystemNumber + " / CLIENT " + options.Client + " / USER " + options.User);
Console.WriteLine("Ping : OK");
Console.WriteLine();
}
switch (options.Command)
{
case SapCommand.SystemInfo:
RunSystemInfo(destination);
break;
case SapCommand.TableRead:
RunTableRead(destination, options);
break;
case SapCommand.TableFields:
RunTableFields(destination, options);
break;
case SapCommand.FieldExists:
RunFieldExists(destination, options);
break;
case SapCommand.FunctionInfo:
RunFunctionInfo(destination, options.FunctionName);
break;
case SapCommand.FunctionSearch:
RunFunctionSearch(destination, options.SearchPattern, options);
break;
case SapCommand.RfcCall:
RunRfcCall(destination, options);
break;
case SapCommand.AbapRead:
RunAbapRead(destination, options);
break;
case SapCommand.AbapCheck:
RunAbapCheck(destination, options);
break;
case SapCommand.AbapWrite:
RunAbapWrite(destination, options);
break;
case SapCommand.AbapActivate:
RunAbapActivate(destination, options);
break;
default:
throw new ArgumentOutOfRangeException("Command", "Unsupported command: " + options.Command);
}
return 0;
}
catch (RfcLogonException ex)
{
return Fail("SAP logon failed", ex);
}
catch (RfcCommunicationException ex)
{
return Fail("SAP communication failed", ex);
}
catch (RfcAbapRuntimeException ex)
{
return Fail("ABAP runtime error", ex);
}
catch (RfcAbapException ex)
{
return Fail("ABAP exception", ex);
}
catch (Exception ex)
{
return Fail("SAP CLI failed", ex);
}
}
private static RfcDestination CreateDestination(CliOptions options)
{
var password = Environment.GetEnvironmentVariable("SAP_NCO_PASSWORD");
if (string.IsNullOrEmpty(password))
{
password = Environment.GetEnvironmentVariable("SAP_T76_PASSWORD");
}
if (string.IsNullOrEmpty(password))
{
if (options.NoPasswordPrompt)
{
throw new InvalidOperationException("No SAP password was provided. Set SAP_NCO_PASSWORD/SAP_T76_PASSWORD or allow the masked password prompt.");
}
password = ReadPassword("Password for " + options.User + "@" + options.Client + ": ");
}
var parameters = new RfcConfigParameters
{
{ RfcConfigParameters.Name, options.Name },
{ RfcConfigParameters.AppServerHost, options.AppServerHost },
{ RfcConfigParameters.SystemNumber, options.SystemNumber },
{ RfcConfigParameters.Client, options.Client },
{ RfcConfigParameters.User, options.User },
{ RfcConfigParameters.Password, password },
{ RfcConfigParameters.Language, options.Language },
{ RfcConfigParameters.PoolSize, "1" },
{ RfcConfigParameters.PeakConnectionsLimit, "1" },
{ RfcConfigParameters.ConnectionIdleTimeout, "600" }
};
if (!string.IsNullOrWhiteSpace(options.SapRouter))
{
parameters.Add(RfcConfigParameters.SAPRouter, options.SapRouter);
}
if (!string.IsNullOrWhiteSpace(options.Trace))
{
parameters.Add(RfcConfigParameters.Trace, options.Trace);
}
return RfcDestinationManager.GetDestination(parameters);
}
private static void RunSystemInfo(RfcDestination destination)
{
var function = destination.Repository.CreateFunction("RFC_SYSTEM_INFO");
function.Invoke(destination);
var info = function.GetStructure("RFCSI_EXPORT");
Console.WriteLine("RFC_SYSTEM_INFO");
PrintField(info, "RFCSYSID", "SAP System ID");
PrintField(info, "RFCHOST", "RFC Host");
PrintField(info, "RFCHOST2", "Host 2");
PrintField(info, "RFCIPADDR", "IP Address");
PrintField(info, "RFCOPSYS", "OS");
PrintField(info, "RFCMACH", "Machine");
PrintField(info, "RFCDBSYS", "DB System");
PrintField(info, "RFCDBHOST", "DB Host");
PrintField(info, "RFCDATABS", "DB Name");
PrintField(info, "RFCKERNRL", "Kernel Release");
}
private static void RunTableRead(RfcDestination destination, CliOptions options)
{
if (string.IsNullOrWhiteSpace(options.TableName))
{
throw new ArgumentException("Missing table name. Example: SapProbe.exe table-read T000 --fields MANDT,MTEXT --rowcount 5");
}
var function = destination.Repository.CreateFunction("RFC_READ_TABLE");
function.SetValue("QUERY_TABLE", options.TableName.ToUpperInvariant());
function.SetValue("DELIMITER", options.Delimiter);
function.SetValue("ROWSKIPS", options.RowSkip);
function.SetValue("ROWCOUNT", options.RowCount);
var fields = function.GetTable("FIELDS");
foreach (var field in options.Fields)
{
fields.Append();
fields.SetValue("FIELDNAME", field.ToUpperInvariant());
}
var whereOptions = function.GetTable("OPTIONS");
foreach (var where in options.WhereClauses)
{
foreach (var line in SplitAbapOptionLine(where, 72))
{
whereOptions.Append();
whereOptions.SetValue("TEXT", line);
}
}
function.Invoke(destination);
var resolvedFields = ReadReadTableFieldNames(fields);
var data = function.GetTable("DATA");
var rows = new List<List<string>>();
for (var i = 0; i < data.RowCount; i++)
{
data.CurrentIndex = i;
rows.Add(SplitDataRow(data.GetString("WA"), options.Delimiter, resolvedFields.Count));
}
RenderRows(resolvedFields, rows, options.OutputFormat);
}
private static void RunTableFields(RfcDestination destination, CliOptions options)
{
if (string.IsNullOrWhiteSpace(options.TableName))
{
throw new ArgumentException("Missing table name. Example: SapProbe.exe table-fields MARC");
}
var fields = GetTableFieldRows(destination, options.TableName, options.FieldName);
var headers = new[] { "FIELDNAME", "KEYFLAG", "DATATYPE", "LENG", "DECIMALS", "ROLLNAME", "FIELDTEXT" };
var visibleFields = fields.Take(options.MaxTableRows).ToList();
RenderRows(headers, visibleFields, options.OutputFormat);
if (fields.Count > visibleFields.Count)
{
Console.WriteLine("... " + (fields.Count - visibleFields.Count) + " more fields not printed. Increase --max-table-rows if needed.");
}
}
private static void RunFieldExists(RfcDestination destination, CliOptions options)
{
if (string.IsNullOrWhiteSpace(options.TableName) || string.IsNullOrWhiteSpace(options.FieldName))
{
throw new ArgumentException("Missing table or field. Example: SapProbe.exe field-exists MARC MMSTA");
}
var fields = GetTableFieldRows(destination, options.TableName, options.FieldName);
Console.WriteLine("Table : " + options.TableName.ToUpperInvariant());
Console.WriteLine("Field : " + options.FieldName.ToUpperInvariant());
Console.WriteLine("Exists : " + (fields.Count > 0 ? "YES" : "NO"));
if (fields.Count > 0)
{
Console.WriteLine();
RenderRows(new[] { "FIELDNAME", "KEYFLAG", "DATATYPE", "LENG", "DECIMALS", "ROLLNAME", "FIELDTEXT" }, fields, options.OutputFormat);
}
}
private static List<List<string>> GetTableFieldRows(RfcDestination destination, string tableName, string fieldName)
{
var function = destination.Repository.CreateFunction("DDIF_FIELDINFO_GET");
function.SetValue("TABNAME", tableName.ToUpperInvariant());
function.SetValue("LANGU", "D");
function.SetValue("DO_NOT_WRITE", "X");
if (!string.IsNullOrWhiteSpace(fieldName))
{
function.SetValue("FIELDNAME", fieldName.ToUpperInvariant());
}
function.Invoke(destination);
var table = function.GetTable("DFIES_TAB");
var rows = new List<List<string>>();
for (var i = 0; i < table.RowCount; i++)
{
table.CurrentIndex = i;
rows.Add(new List<string>
{
SafeGetString(table.CurrentRow, "FIELDNAME"),
SafeGetString(table.CurrentRow, "KEYFLAG"),
SafeGetString(table.CurrentRow, "DATATYPE"),
SafeGetString(table.CurrentRow, "LENG"),
SafeGetString(table.CurrentRow, "DECIMALS"),
SafeGetString(table.CurrentRow, "ROLLNAME"),
SafeGetString(table.CurrentRow, "FIELDTEXT")
});
}
return rows;
}
private static void RunFunctionInfo(RfcDestination destination, string functionName)
{
if (string.IsNullOrWhiteSpace(functionName))
{
throw new ArgumentException("Missing function name. Example: SapProbe.exe function-info RFC_SYSTEM_INFO");
}
var metadata = destination.Repository.GetFunctionMetadata(functionName.ToUpperInvariant());
Console.WriteLine("Function: " + functionName.ToUpperInvariant());
Console.WriteLine();
Console.WriteLine("Direction Name Type Length Decimals Default");
Console.WriteLine("--------- -------------------------------- --------- ------ -------- -------");
for (var i = 0; i < metadata.ParameterCount; i++)
{
var parameter = metadata[i];
Console.WriteLine(
parameter.Direction.ToString().PadRight(9) + " " +
parameter.Name.PadRight(32) + " " +
parameter.DataType.ToString().PadRight(9) + " " +
parameter.NucLength.ToString().PadLeft(6) + " " +
parameter.Decimals.ToString().PadLeft(8) + " " +
(parameter.DefaultValue ?? string.Empty));
}
}
private static void RunFunctionSearch(RfcDestination destination, string pattern, CliOptions options)
{
if (string.IsNullOrWhiteSpace(pattern))
{
throw new ArgumentException("Missing search pattern. Example: SapProbe.exe function-search RPY*PROGRAM*");
}
var function = destination.Repository.CreateFunction("RFC_FUNCTION_SEARCH");
function.SetValue("FUNCNAME", pattern.ToUpperInvariant());
function.Invoke(destination);
Console.WriteLine("Function search: " + pattern.ToUpperInvariant());
Console.WriteLine();
DumpTable(function.GetTable("FUNCTIONS"), options.MaxTableRows, options.OutputFormat);
}
private static void RunRfcCall(RfcDestination destination, CliOptions options)
{
if (string.IsNullOrWhiteSpace(options.FunctionName))
{
throw new ArgumentException("Missing function name. Example: SapProbe.exe rfc-call STFC_CONNECTION --set REQUTEXT=hello");
}
var functionName = options.FunctionName.ToUpperInvariant();
var metadata = destination.Repository.GetFunctionMetadata(functionName);
var function = metadata.CreateFunction();
foreach (var pair in options.SetValues)
{
function.SetValue(pair.Key.ToUpperInvariant(), pair.Value);
}
function.Invoke(destination);
Console.WriteLine("Function: " + functionName);
Console.WriteLine();
DumpFunctionResult(function, metadata, options);
}
private static void RunAbapRead(RfcDestination destination, CliOptions options)
{
var programName = RequireProgramName(options);
var lines = ReadAbapProgram(destination, programName, options);
if (!string.IsNullOrWhiteSpace(options.OutputPath))
{
var fullPath = Path.GetFullPath(options.OutputPath);
File.WriteAllLines(fullPath, lines, Encoding.UTF8);
Console.WriteLine("Program : " + programName);
Console.WriteLine("Lines : " + lines.Count);
Console.WriteLine("Output : " + fullPath);
return;
}
foreach (var line in lines)
{
Console.WriteLine(line);
}
}
private static void RunAbapCheck(RfcDestination destination, CliOptions options)
{
var programName = RequireProgramName(options);
AbapSyntaxResult result;
var lineCount = 0;
if (string.IsNullOrWhiteSpace(options.SourceFile))
{
result = CheckExistingAbapProgram(destination, programName, options);
}
else
{
var lines = LoadSourceForAbapCommand(destination, programName, options);
lineCount = lines.Count;
result = CheckAbapSyntax(destination, programName, lines);
}
Console.WriteLine("Program : " + programName);
if (lineCount > 0)
{
Console.WriteLine("Lines : " + lineCount);
}
Console.WriteLine("Syntax status : " + (result.ErrorSubrc == 0 ? "OK" : "ERROR"));
Console.WriteLine("Error subrc : " + result.ErrorSubrc);
if (result.ErrorSubrc != 0)
{
Console.WriteLine("Error include : " + result.ErrorInclude);
Console.WriteLine("Error line : " + result.ErrorLine);
Console.WriteLine("Error offset : " + result.ErrorOffset);
Console.WriteLine("Error word : " + result.ErrorWord);
Console.WriteLine("Error message : " + result.ErrorMessage);
}
if (result.Warnings.Count > 0)
{
Console.WriteLine();
Console.WriteLine("Warnings:");
foreach (var warning in result.Warnings)
{
Console.WriteLine(" " + warning);
}
}
}
private static void RunAbapWrite(RfcDestination destination, CliOptions options)
{
var programName = RequireProgramName(options);
if (string.IsNullOrWhiteSpace(options.SourceFile))
{
throw new ArgumentException("abap-write requires --source-file <path>.");
}
var lines = LoadSourceForAbapCommand(destination, programName, options);
Console.WriteLine("Program : " + programName);
Console.WriteLine("Source file : " + Path.GetFullPath(options.SourceFile));
Console.WriteLine("Lines : " + lines.Count);
Console.WriteLine("Pre-check : skipped for local source; repository syntax can be checked after write.");
if (options.DryRun)
{
Console.WriteLine("Dry run : no SAP repository changes were written.");
return;
}
if (!options.ConfirmWrite)
{
throw new InvalidOperationException("abap-write is blocked unless --confirm-write is provided.");
}
WriteAbapProgram(destination, options, programName, lines);
Console.WriteLine("Lines written : " + lines.Count);
Console.WriteLine("Result : RPY_PROGRAM_INSERT returned without RFC exception.");
var syntax = CheckExistingAbapProgram(destination, programName, options);
Console.WriteLine("Post-check : " + (syntax.ErrorSubrc == 0 ? "OK" : "ERROR"));
if (syntax.ErrorSubrc != 0)
{
Console.WriteLine("Error line : " + syntax.ErrorLine);
Console.WriteLine("Error message : " + syntax.ErrorMessage);
}
}
private static void RunAbapActivate(RfcDestination destination, CliOptions options)
{
var programName = RequireProgramName(options);
var lines = LoadSourceForAbapCommand(destination, programName, options);
Console.WriteLine("Program : " + programName);
Console.WriteLine("Lines : " + lines.Count);
Console.WriteLine("Activation : RPY_PROGRAM_INSERT with SAVE_INACTIVE blank");
if (options.DryRun)
{
Console.WriteLine("Dry run : no SAP repository changes were written.");
return;
}
if (!options.ConfirmWrite)
{
throw new InvalidOperationException("abap-activate is blocked unless --confirm-write is provided.");
}
var activateOptions = options.CloneForActiveWrite();
WriteAbapProgram(destination, activateOptions, programName, lines);
Console.WriteLine("Result : activation write returned without RFC exception.");
var syntax = CheckExistingAbapProgram(destination, programName, options);
Console.WriteLine("Post-check : " + (syntax.ErrorSubrc == 0 ? "OK" : "ERROR"));
if (syntax.ErrorSubrc != 0)
{
Console.WriteLine("Error line : " + syntax.ErrorLine);
Console.WriteLine("Error message : " + syntax.ErrorMessage);
}
}
private static void WriteAbapProgram(RfcDestination destination, CliOptions options, string programName, IList<string> lines)
{
var function = destination.Repository.CreateFunction("RPY_PROGRAM_INSERT");
function.SetValue("PROGRAM_NAME", programName);
function.SetValue("TITLE_STRING", string.IsNullOrWhiteSpace(options.Title) ? programName : options.Title);
function.SetValue("SUPPRESS_DIALOG", "X");
function.SetValue("UCCHECK", "X");
if (!string.IsNullOrWhiteSpace(options.DevelopmentClass))
{
function.SetValue("DEVELOPMENT_CLASS", options.DevelopmentClass);
}
if (!string.IsNullOrWhiteSpace(options.TransportNumber))
{
function.SetValue("TRANSPORT_NUMBER", options.TransportNumber);
}
if (options.SaveInactive)
{
function.SetValue("SAVE_INACTIVE", "X");
}
if (options.Temporary)
{
function.SetValue("TEMPORARY", "X");
}
FillSingleColumnTable(function.GetTable("SOURCE_EXTENDED"), lines);
function.Invoke(destination);
}
private static void DumpFunctionResult(IRfcFunction function, RfcFunctionMetadata metadata, CliOptions options)
{
for (var i = 0; i < metadata.ParameterCount; i++)
{
var parameter = metadata[i];
if (parameter.Direction == RfcDirection.IMPORT && !options.DumpImports)
{
continue;
}
Console.WriteLine(parameter.Direction + " " + parameter.Name + " (" + parameter.DataType + ")");
if (parameter.DataType == RfcDataType.TABLE)
{
DumpTable(function.GetTable(parameter.Name), options.MaxTableRows, options.OutputFormat);
}
else if (parameter.DataType == RfcDataType.STRUCTURE)
{
DumpStructure(function.GetStructure(parameter.Name));
}
else
{
Console.WriteLine(" " + SafeGetString(function, parameter.Name));
}
Console.WriteLine();
}
}
private static string RequireProgramName(CliOptions options)
{
if (string.IsNullOrWhiteSpace(options.ProgramName))
{
throw new ArgumentException("Missing ABAP program name.");
}
return options.ProgramName.ToUpperInvariant();
}
private static List<string> LoadSourceForAbapCommand(RfcDestination destination, string programName, CliOptions options)
{
if (!string.IsNullOrWhiteSpace(options.SourceFile))
{
var fullPath = Path.GetFullPath(options.SourceFile);
if (!File.Exists(fullPath))
{
throw new FileNotFoundException("ABAP source file was not found.", fullPath);
}
return File.ReadAllLines(fullPath, Encoding.UTF8).ToList();
}
return ReadAbapProgram(destination, programName, options);
}
private static List<string> ReadAbapProgram(RfcDestination destination, string programName, CliOptions options)
{
var function = destination.Repository.CreateFunction("RPY_PROGRAM_READ");
function.SetValue("PROGRAM_NAME", programName);
function.SetValue("LANGUAGE", options.Language);
function.SetValue("ONLY_SOURCE", "X");
function.SetValue("ONLY_TEXTS", " ");
function.SetValue("WITH_INCLUDELIST", options.WithIncludeList ? "X" : " ");
function.SetValue("WITH_LOWERCASE", "X");
function.SetValue("READ_LATEST_VERSION", options.ReadLatestVersion ? "X" : " ");
function.Invoke(destination);
var extended = ReadSingleColumnTable(function.GetTable("SOURCE_EXTENDED"));
if (extended.Count > 0)
{
return extended;
}
return ReadSingleColumnTable(function.GetTable("SOURCE"));
}
private static AbapSyntaxResult CheckExistingAbapProgram(RfcDestination destination, string programName, CliOptions options)
{
var function = destination.Repository.CreateFunction("RS_ABAP_SYNTAX_CHECK_E");
function.SetValue("P_PROGRAM", programName);
function.SetValue("P_LANGU", options.Language);
function.SetValue("P_NO_PACKAGE_CHECK", "X");
function.Invoke(destination);
var result = new AbapSyntaxResult
{
ErrorSubrc = SafeGetInt(function, "P_SUBRC"),
ErrorInclude = string.Empty,
ErrorWord = string.Empty,
ErrorMessage = string.Empty
};
var errors = function.GetTable("P_ERRORS");
for (var i = 0; i < errors.RowCount; i++)
{
errors.CurrentIndex = i;
var kind = SafeGetString(errors.CurrentRow, "KIND");
var message = SafeGetString(errors.CurrentRow, "MESSAGE");
var line = SafeGetInt(errors.CurrentRow, "LINE");
if (result.ErrorSubrc != 0 && string.IsNullOrWhiteSpace(result.ErrorMessage))
{
result.ErrorLine = line;
result.ErrorMessage = message;
result.ErrorInclude = SafeGetString(errors.CurrentRow, "INCNAME");
result.ErrorWord = SafeGetString(errors.CurrentRow, "KEYWORD");
continue;
}
if (!string.IsNullOrWhiteSpace(message))
{
result.Warnings.Add((string.IsNullOrWhiteSpace(kind) ? "INFO" : kind) + " line " + line + ": " + message);
}
}
var warnings = function.GetTable("P_WARNINGS");
for (var i = 0; i < warnings.RowCount; i++)
{
warnings.CurrentIndex = i;
var message = SafeGetString(warnings.CurrentRow, "MESSAGE");
if (!string.IsNullOrWhiteSpace(message))
{
result.Warnings.Add("WARN line " + SafeGetInt(warnings.CurrentRow, "LINE") + ": " + message);
}
}
return result;
}
private static AbapSyntaxResult CheckAbapSyntax(RfcDestination destination, string programName, IList<string> lines)
{
var function = destination.Repository.CreateFunction("RFC_PROGRAM_CHECK_SYNTAX");
function.SetValue("SOURCE_NAME", programName);
function.SetValue("GLOBAL_PROGRAM", programName);
function.SetValue("REPLACING", "X");
FillSingleColumnTable(function.GetTable("SOURCE"), lines);
FillSingleColumnTable(function.GetTable("REPLACING_SOURCE"), lines);
function.Invoke(destination);
var result = new AbapSyntaxResult
{
ErrorSubrc = SafeGetInt(function, "ERROR_SUBRC"),
ErrorInclude = SafeGetString(function, "ERROR_INCLUDE"),
ErrorLine = SafeGetInt(function, "ERROR_LINE"),
ErrorOffset = SafeGetInt(function, "ERROR_OFFSET"),
ErrorWord = SafeGetString(function, "ERROR_WORD"),
ErrorMessage = SafeGetString(function, "ERROR_MESSAGE")
};
result.Warnings.AddRange(ReadSingleColumnTable(function.GetTable("WARNINGS_TABLE")));
if (result.ErrorSubrc != 0 && string.IsNullOrWhiteSpace(result.ErrorMessage))
{
var fallback = CheckAbapSyntaxWithErrorSyntaxCheck(destination, programName, lines);
if (fallback != null)
{
return fallback;
}
}
return result;
}
private static AbapSyntaxResult CheckAbapSyntaxWithErrorSyntaxCheck(RfcDestination destination, string programName, IList<string> lines)
{
try
{
var function = destination.Repository.CreateFunction("RS_ABAP_ERROR_SYNTAX_CHECK");
function.SetValue("P_PROGRAM", programName);
function.SetValue("P_MODE", 0);
FillSingleColumnTable(function.GetTable("P_REPTAB"), lines);
function.Invoke(destination);
var kind = SafeGetString(function, "P_KIND");
var message = SafeGetString(function, "P_MESSAGE");
var result = new AbapSyntaxResult
{
ErrorSubrc = string.Equals(kind, "E", StringComparison.OrdinalIgnoreCase) || !string.IsNullOrWhiteSpace(message) ? 4 : 0,
ErrorMessage = message,
ErrorInclude = string.Empty,
ErrorWord = string.Empty
};
result.Warnings.AddRange(ReadSingleColumnTable(function.GetTable("P_TRCTAB")));
var error = function.GetStructure("P_ERROR");
result.ErrorLine = SafeGetFirstInt(error, "LINE", "LINENO", "ROW");
result.ErrorOffset = SafeGetFirstInt(error, "OFFSET", "COL", "COLUMN");
result.ErrorWord = SafeGetFirstString(error, "WORD", "TOKEN");
return result;
}
catch (Exception)
{
return null;
}
}
private static List<string> ReadSingleColumnTable(IRfcTable table)
{
var lines = new List<string>();
var columnIndex = PickTextColumnIndex(table.Metadata.LineType);
for (var i = 0; i < table.RowCount; i++)
{
table.CurrentIndex = i;
lines.Add(table.CurrentRow.GetString(columnIndex).TrimEnd());
}
return lines;
}
private static void FillSingleColumnTable(IRfcTable table, IEnumerable<string> lines)
{
var columnIndex = PickTextColumnIndex(table.Metadata.LineType);
foreach (var line in lines)
{
table.Append();
table.CurrentRow.SetValue(columnIndex, line ?? string.Empty);
}
}
private static int PickTextColumnIndex(RfcStructureMetadata metadata)
{
var preferred = new[] { "LINE", "TEXT", "SOURCE", "WA" };
foreach (var name in preferred)
{
var index = metadata.TryNameToIndex(name);
if (index >= 0)
{
return index;
}
}
return 0;
}
private static void DumpTable(IRfcTable table, int maxRows, OutputFormat format)
{
var headers = GetContainerFieldNames(table.Metadata.LineType);
var rows = new List<List<string>>();
var rowLimit = Math.Min(table.RowCount, Math.Max(0, maxRows));
for (var i = 0; i < rowLimit; i++)
{
table.CurrentIndex = i;
rows.Add(ReadStructureValues(table.CurrentRow, headers));
}
RenderRows(headers, rows, format);
if (table.RowCount > rowLimit)
{
Console.WriteLine("... " + (table.RowCount - rowLimit) + " more rows not printed. Increase --max-table-rows if needed.");
}
}
private static void DumpStructure(IRfcStructure structure)
{
var headers = GetContainerFieldNames(structure.Metadata);
foreach (var header in headers)
{
Console.WriteLine(" " + header.PadRight(32) + ": " + SafeGetString(structure, header));
}
}
private static List<string> GetContainerFieldNames(RfcStructureMetadata metadata)
{
var names = new List<string>();
for (var i = 0; i < metadata.FieldCount; i++)
{
names.Add(metadata[i].Name);
}
return names;
}
private static List<string> ReadStructureValues(IRfcStructure structure, IList<string> headers)
{
var values = new List<string>();
foreach (var header in headers)
{
values.Add(SafeGetString(structure, header));
}
return values;
}
private static List<string> ReadReadTableFieldNames(IRfcTable fields)
{
var names = new List<string>();
for (var i = 0; i < fields.RowCount; i++)
{
fields.CurrentIndex = i;
var name = SafeGetString(fields.CurrentRow, "FIELDNAME").Trim();
if (!string.IsNullOrEmpty(name))
{
names.Add(name);
}
}
return names;
}
private static List<string> SplitDataRow(string row, string delimiter, int expectedColumns)
{
var parts = row.Split(new[] { delimiter }, StringSplitOptions.None)
.Select(part => part.TrimEnd())
.ToList();
while (parts.Count < expectedColumns)
{
parts.Add(string.Empty);
}
if (expectedColumns > 0 && parts.Count > expectedColumns)
{
parts = parts.Take(expectedColumns).ToList();
}
return parts;
}
private static IEnumerable<string> SplitAbapOptionLine(string text, int maxLength)
{
if (string.IsNullOrWhiteSpace(text))
{
yield break;
}
var remaining = text.Trim();
while (remaining.Length > maxLength)
{
var splitAt = remaining.LastIndexOf(' ', maxLength - 1, maxLength);
if (splitAt < 1)
{
splitAt = maxLength;
}
yield return remaining.Substring(0, splitAt).TrimEnd();
remaining = remaining.Substring(splitAt).TrimStart();
}
if (remaining.Length > 0)
{
yield return remaining;
}
}
private static void RenderRows(IList<string> headers, IList<List<string>> rows, OutputFormat format)
{
if (format == OutputFormat.Json)
{
RenderJson(headers, rows);
return;
}
if (format == OutputFormat.Csv)
{
Console.WriteLine(string.Join(",", headers.Select(EscapeCsv)));
foreach (var row in rows)
{
Console.WriteLine(string.Join(",", row.Select(EscapeCsv)));
}
return;
}
Console.WriteLine(string.Join(" | ", headers));
Console.WriteLine(string.Join("-+-", headers.Select(header => new string('-', Math.Max(3, header.Length)))));
foreach (var row in rows)
{
Console.WriteLine(string.Join(" | ", row));
}
if (rows.Count == 0)
{
Console.WriteLine("(no rows)");
}
}
private static void RenderJson(IList<string> headers, IList<List<string>> rows)
{
Console.WriteLine("[");
for (var rowIndex = 0; rowIndex < rows.Count; rowIndex++)
{
var row = rows[rowIndex];
Console.Write(" {");
for (var columnIndex = 0; columnIndex < headers.Count; columnIndex++)
{
if (columnIndex > 0)
{
Console.Write(", ");
}
var value = columnIndex < row.Count ? row[columnIndex] : string.Empty;
Console.Write("\"" + EscapeJson(headers[columnIndex]) + "\": \"" + EscapeJson(value) + "\"");
}
Console.Write(rowIndex + 1 == rows.Count ? "}" : "},");
Console.WriteLine();
}
Console.WriteLine("]");
}
private static string EscapeCsv(string value)
{
value = value ?? string.Empty;
if (value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) < 0)
{
return value;
}
return "\"" + value.Replace("\"", "\"\"") + "\"";
}
private static string EscapeJson(string value)
{
if (value == null)
{
return string.Empty;
}
var builder = new StringBuilder();
foreach (var ch in value)
{
switch (ch)
{
case '\\':
builder.Append("\\\\");
break;
case '"':
builder.Append("\\\"");
break;
case '\r':
builder.Append("\\r");
break;
case '\n':
builder.Append("\\n");
break;
case '\t':
builder.Append("\\t");
break;
default:
if (char.IsControl(ch))
{
builder.Append("\\u" + ((int)ch).ToString("x4"));
}
else
{
builder.Append(ch);
}
break;
}
}
return builder.ToString();
}
private static void PrintBanner(CliOptions options)
{
if (options.Quiet)
{
return;
}
Console.WriteLine("SAP NCo CLI");
Console.WriteLine("Architecture : " + (Environment.Is64BitProcess ? "x64" : "x86"));
Console.WriteLine("NCo Assembly : " + typeof(RfcDestinationManager).Assembly.FullName);
}
private static int Fail(string title, Exception ex)
{
Console.Error.WriteLine();
Console.Error.WriteLine("ERROR: " + title);
Console.Error.WriteLine(ex.GetType().FullName + ": " + ex.Message);
return 1;
}
private static string ReadPassword(string prompt)
{
Console.Write(prompt);
var password = new StringBuilder();
while (true)
{
var key = Console.ReadKey(intercept: true);
if (key.Key == ConsoleKey.Enter)
{
Console.WriteLine();
return password.ToString();
}
if (key.Key == ConsoleKey.Backspace)
{
if (password.Length > 0)
{
password.Length--;
Console.Write("\b \b");
}
continue;
}
if (key.Key == ConsoleKey.Escape)
{
Console.WriteLine();
throw new OperationCanceledException("Password entry cancelled.");
}
if (!char.IsControl(key.KeyChar))
{
password.Append(key.KeyChar);
Console.Write('*');
}
}
}
private static void PrintField(IRfcStructure structure, string fieldName, string label)
{
Console.WriteLine(label.PadRight(16) + ": " + SafeGetString(structure, fieldName));
}
private static string SafeGetString(IRfcDataContainer container, string fieldName)
{
try
{
return container.GetString(fieldName).Trim();
}
catch (Exception)
{
return "<not available>";
}
}
private static int SafeGetInt(IRfcDataContainer container, string fieldName)
{
try
{
return container.GetInt(fieldName);
}
catch (Exception)
{
return 0;
}
}
private static string SafeGetFirstString(IRfcDataContainer container, params string[] fieldNames)
{
foreach (var fieldName in fieldNames)
{
try
{
var value = container.GetString(fieldName).Trim();
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
catch (Exception)
{
// Try the next known field name.
}
}
return string.Empty;
}
private static int SafeGetFirstInt(IRfcDataContainer container, params string[] fieldNames)
{
foreach (var fieldName in fieldNames)
{
try
{
var value = container.GetInt(fieldName);
if (value != 0)
{
return value;
}
}
catch (Exception)
{
// Try the next known field name.
}
}
return 0;
}
private static void PrintHelp()
{
Console.WriteLine("SAP NCo CLI for T76");
Console.WriteLine();
Console.WriteLine("Usage:");
Console.WriteLine(" SapProbe.exe [global options] <command> [command options]");
Console.WriteLine();
Console.WriteLine("Commands:");
Console.WriteLine(" system-info Ping SAP and call RFC_SYSTEM_INFO. Default command.");
Console.WriteLine(" table-read <table> Read table data via RFC_READ_TABLE.");
Console.WriteLine(" table-fields <table> [field] Show DDIC field metadata via DDIF_FIELDINFO_GET.");
Console.WriteLine(" field-exists <table> <field> Check whether a DDIC table field exists.");
Console.WriteLine(" function-info <function> Show RFC function interface metadata.");
Console.WriteLine(" function-search <pattern> Search RFC functions via RFC_FUNCTION_SEARCH.");
Console.WriteLine(" rfc-call <function> Call an RFC-enabled function module.");
Console.WriteLine(" abap-read <program> Read ABAP report/source via RPY_PROGRAM_READ.");
Console.WriteLine(" abap-check <program> Syntax-check an existing repository program.");
Console.WriteLine(" abap-write <program> Write ABAP source via RPY_PROGRAM_INSERT; requires --confirm-write.");
Console.WriteLine(" abap-activate <program> Activation attempt through active RPY_PROGRAM_INSERT write.");
Console.WriteLine(" load-only Only load the 32-bit NCo assembly; do not connect.");
Console.WriteLine();
Console.WriteLine("Global options:");
Console.WriteLine(" --ashost <host> ABAP application server. Default: travt762.sap.trafag.com");
Console.WriteLine(" --sysnr <nr> SAP system number. Default: 00");
Console.WriteLine(" --client <mandt> SAP client. Default: 100");
Console.WriteLine(" --user <user> SAP user. Default: KOI");
Console.WriteLine(" --lang <lang> SAP logon language. Default: DE");
Console.WriteLine(" --router <route> Optional SAProuter string.");
Console.WriteLine(" --trace <level> Optional NCo trace level.");
Console.WriteLine(" --quiet Suppress connection banner.");
Console.WriteLine(" --no-password-prompt Fail if no password env var is set.");
Console.WriteLine(" --help Show this help.");
Console.WriteLine();
Console.WriteLine("table-read options:");
Console.WriteLine(" --fields A,B,C Comma-separated fields. Recommended to avoid RFC_READ_TABLE row limits.");
Console.WriteLine(" --field FIELD Single field filter for table-fields.");
Console.WriteLine(" --where \"FIELD = 'VALUE'\" WHERE fragment; can be repeated.");
Console.WriteLine(" --rowcount <n> Max rows. Default: 10.");
Console.WriteLine(" --rowskip <n> Rows to skip. Default: 0.");
Console.WriteLine(" --format table|csv|json Output format. Default: table.");
Console.WriteLine();
Console.WriteLine("rfc-call options:");
Console.WriteLine(" --set NAME=VALUE Scalar import/changing value; can be repeated.");
Console.WriteLine(" --dump-imports Include import parameters in output.");
Console.WriteLine(" --max-table-rows <n> Max rows printed for table parameters. Default: 20.");
Console.WriteLine(" --format table|csv|json Format for table outputs. Default: table.");
Console.WriteLine();
Console.WriteLine("ABAP source options:");
Console.WriteLine(" --out <path> Write abap-read output to a local file.");
Console.WriteLine(" --source-file <path> Use a local source file for abap-check/abap-write.");
Console.WriteLine(" --latest Read latest version in abap-read/abap-check.");
Console.WriteLine(" --with-includes Request include list while reading.");
Console.WriteLine(" --title <text> Title used by abap-write.");
Console.WriteLine(" --devclass <package> Package/development class for abap-write, e.g. $TMP.");
Console.WriteLine(" --transport <request> Transport request for abap-write if required.");
Console.WriteLine(" --temporary Set TEMPORARY=X in abap-write.");
Console.WriteLine(" --save-inactive Set SAVE_INACTIVE=X in abap-write.");
Console.WriteLine(" --dry-run Syntax-check only; do not write.");
Console.WriteLine(" --confirm-write Required for any repository write.");
Console.WriteLine();
Console.WriteLine("Examples:");
Console.WriteLine(" SapProbe.exe system-info");
Console.WriteLine(" SapProbe.exe table-read T000 --fields MANDT,MTEXT --where \"MANDT = '100'\" --rowcount 5");
Console.WriteLine(" SapProbe.exe table-fields MARC MMSTA");
Console.WriteLine(" SapProbe.exe field-exists MARC MMSTA");
Console.WriteLine(" SapProbe.exe function-info RFC_SYSTEM_INFO");
Console.WriteLine(" SapProbe.exe function-search RPY*PROGRAM*");
Console.WriteLine(" SapProbe.exe rfc-call STFC_CONNECTION --set REQUTEXT=hello");
Console.WriteLine(" SapProbe.exe abap-read Z_TEST3 --out C:\\Temp\\Z_TEST3.abap");
Console.WriteLine(" SapProbe.exe abap-check Z_TEST3 --source-file C:\\Temp\\Z_TEST3.abap");
Console.WriteLine(" SapProbe.exe abap-write Z_TEST3 --source-file C:\\Temp\\Z_TEST3.abap --confirm-write");
Console.WriteLine(" SapProbe.exe abap-activate Z_TEST3 --dry-run");
Console.WriteLine();
Console.WriteLine("Password input:");
Console.WriteLine(" This tool never accepts a password as a command-line argument.");
Console.WriteLine(" It reads SAP_NCO_PASSWORD or SAP_T76_PASSWORD if set; otherwise it prompts with masking.");
}
}
internal enum SapCommand
{
SystemInfo,
TableRead,
TableFields,
FieldExists,
FunctionInfo,
FunctionSearch,
RfcCall,
AbapRead,
AbapCheck,
AbapWrite,
AbapActivate,
LoadOnly
}
internal enum OutputFormat
{
Table,
Csv,
Json
}
internal sealed class AbapSyntaxResult
{
public int ErrorSubrc { get; set; }
public string ErrorInclude { get; set; }
public int ErrorLine { get; set; }
public int ErrorOffset { get; set; }
public string ErrorWord { get; set; }
public string ErrorMessage { get; set; }
public List<string> Warnings { get; } = new List<string>();
}
internal sealed class CliOptions
{
private readonly List<string> _positionals = new List<string>();
public string Name { get; private set; } = "T76";
public string AppServerHost { get; private set; } = "travt762.sap.trafag.com";
public string SystemNumber { get; private set; } = "00";
public string Client { get; private set; } = "100";
public string User { get; private set; } = "KOI";
public string Language { get; private set; } = "DE";
public string SapRouter { get; private set; }
public string Trace { get; private set; }
public bool Quiet { get; private set; }
public bool NoPasswordPrompt { get; private set; }
public bool ShowHelp { get; private set; }
public SapCommand Command { get; private set; } = SapCommand.SystemInfo;
public string TableName { get; private set; }
public string FieldName { get; private set; }
public List<string> Fields { get; private set; } = new List<string>();
public List<string> WhereClauses { get; private set; } = new List<string>();
public int RowCount { get; private set; } = 10;
public int RowSkip { get; private set; }
public string Delimiter { get; private set; } = "|";
public OutputFormat OutputFormat { get; private set; } = OutputFormat.Table;
public string FunctionName { get; private set; }
public string SearchPattern { get; private set; }
public Dictionary<string, string> SetValues { get; private set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public bool DumpImports { get; private set; }
public int MaxTableRows { get; private set; } = 20;
public string ProgramName { get; private set; }
public string OutputPath { get; private set; }
public string SourceFile { get; private set; }
public string Title { get; private set; }
public string DevelopmentClass { get; private set; }
public string TransportNumber { get; private set; }
public bool ConfirmWrite { get; private set; }
public bool DryRun { get; private set; }
public bool SaveInactive { get; private set; }
public bool Temporary { get; private set; }
public bool WithIncludeList { get; private set; }
public bool ReadLatestVersion { get; private set; }
public static CliOptions Parse(string[] args)
{
var options = new CliOptions();
var commandExplicitlySet = false;
for (var i = 0; i < (args ?? Array.Empty<string>()).Length; i++)
{
var arg = args[i];
var lower = arg.ToLowerInvariant();
switch (lower)
{
case "--help":
case "-h":
case "/?":
options.ShowHelp = true;
break;
case "--ashost":
options.AppServerHost = RequireValue(args, ref i, arg);
break;
case "--sysnr":
options.SystemNumber = RequireValue(args, ref i, arg);
break;
case "--client":
options.Client = RequireValue(args, ref i, arg);
break;
case "--user":
options.User = RequireValue(args, ref i, arg);
break;
case "--lang":
options.Language = RequireValue(args, ref i, arg).ToUpperInvariant();
break;
case "--router":
options.SapRouter = RequireValue(args, ref i, arg);
break;
case "--trace":
options.Trace = RequireValue(args, ref i, arg);
break;
case "--quiet":
options.Quiet = true;
break;
case "--no-password-prompt":
options.NoPasswordPrompt = true;
break;
case "--load-only":
options.Command = SapCommand.LoadOnly;
commandExplicitlySet = true;
break;
case "--fields":
options.Fields.AddRange(SplitCsvList(RequireValue(args, ref i, arg)).Select(field => field.ToUpperInvariant()));
break;
case "--field":
options.FieldName = RequireValue(args, ref i, arg).ToUpperInvariant();
break;
case "--where":
options.WhereClauses.Add(RequireValue(args, ref i, arg));
break;
case "--rowcount":
options.RowCount = ParseNonNegativeInt(RequireValue(args, ref i, arg), arg);
break;
case "--rowskip":
case "--rowskips":
options.RowSkip = ParseNonNegativeInt(RequireValue(args, ref i, arg), arg);
break;
case "--delimiter":
options.Delimiter = RequireValue(args, ref i, arg);
if (options.Delimiter.Length != 1)
{
throw new ArgumentException("--delimiter must be exactly one character for RFC_READ_TABLE.");
}
break;
case "--format":
options.OutputFormat = ParseOutputFormat(RequireValue(args, ref i, arg));
break;
case "--set":
case "--param":
AddSetValue(options, RequireValue(args, ref i, arg));
break;
case "--dump-imports":
options.DumpImports = true;
break;
case "--max-table-rows":
options.MaxTableRows = ParseNonNegativeInt(RequireValue(args, ref i, arg), arg);
break;
case "--out":
case "--output":
options.OutputPath = RequireValue(args, ref i, arg);
break;
case "--source-file":
case "--file":
options.SourceFile = RequireValue(args, ref i, arg);
break;
case "--title":
options.Title = RequireValue(args, ref i, arg);
break;
case "--devclass":
case "--development-class":
options.DevelopmentClass = RequireValue(args, ref i, arg);
break;
case "--transport":
case "--transport-number":
options.TransportNumber = RequireValue(args, ref i, arg);
break;
case "--confirm-write":
options.ConfirmWrite = true;
break;
case "--dry-run":
options.DryRun = true;
break;
case "--save-inactive":
options.SaveInactive = true;
break;
case "--temporary":
options.Temporary = true;
break;
case "--with-includes":
options.WithIncludeList = true;
break;
case "--latest":
options.ReadLatestVersion = true;
break;
default:
if (arg.StartsWith("--", StringComparison.Ordinal))
{
throw new ArgumentException("Unknown option: " + arg);
}
if (!commandExplicitlySet && TryParseCommand(arg, out var command))
{
options.Command = command;
commandExplicitlySet = true;
}
else
{
options._positionals.Add(arg);
}
break;
}
}
options.ApplyPositionals();
return options;
}
private void ApplyPositionals()
{
switch (Command)
{
case SapCommand.TableRead:
TableName = TakeRequiredPositional(0, "table name");
break;
case SapCommand.TableFields:
TableName = TakeRequiredPositional(0, "table name");
if (_positionals.Count > 1)
{
FieldName = _positionals[1].ToUpperInvariant();
}
break;
case SapCommand.FieldExists:
TableName = TakeRequiredPositional(0, "table name");
FieldName = TakeRequiredPositional(1, "field name").ToUpperInvariant();
break;
case SapCommand.FunctionInfo:
case SapCommand.RfcCall:
FunctionName = TakeRequiredPositional(0, "function name");
break;
case SapCommand.FunctionSearch:
SearchPattern = TakeRequiredPositional(0, "function search pattern");
break;
case SapCommand.AbapRead:
case SapCommand.AbapCheck:
case SapCommand.AbapWrite:
case SapCommand.AbapActivate:
ProgramName = TakeRequiredPositional(0, "ABAP program name");
break;
case SapCommand.SystemInfo:
case SapCommand.LoadOnly:
if (_positionals.Count > 0)
{
throw new ArgumentException("Unexpected positional argument: " + _positionals[0]);
}
break;
default:
throw new ArgumentOutOfRangeException();
}
var allowedPositionals = Command == SapCommand.TableFields || Command == SapCommand.FieldExists ? 2 : 1;
if (_positionals.Count > allowedPositionals)
{
throw new ArgumentException("Unexpected positional argument: " + _positionals[allowedPositionals]);
}
}
private string TakeRequiredPositional(int index, string description)
{
if (_positionals.Count <= index || string.IsNullOrWhiteSpace(_positionals[index]))
{
throw new ArgumentException("Missing " + description + ".");
}
return _positionals[index];
}
private static bool TryParseCommand(string text, out SapCommand command)
{
switch (text.ToLowerInvariant())
{
case "system-info":
case "info":
case "ping":
command = SapCommand.SystemInfo;
return true;
case "table-read":
case "read-table":
case "table":
command = SapCommand.TableRead;
return true;
case "table-fields":
case "fields":
case "ddic-fields":
command = SapCommand.TableFields;
return true;
case "field-exists":
case "has-field":
command = SapCommand.FieldExists;
return true;
case "function-info":
case "func-info":
case "interface":
command = SapCommand.FunctionInfo;
return true;
case "function-search":
case "func-search":
case "search-functions":
command = SapCommand.FunctionSearch;
return true;
case "rfc-call":
case "call":
case "rfc":
command = SapCommand.RfcCall;
return true;
case "abap-read":
case "read-program":
case "program-read":
command = SapCommand.AbapRead;
return true;
case "abap-check":
case "syntax-check":
case "program-check":
command = SapCommand.AbapCheck;
return true;
case "abap-write":
case "write-program":
case "program-write":
command = SapCommand.AbapWrite;
return true;
case "abap-activate":
case "activate-program":
case "program-activate":
command = SapCommand.AbapActivate;
return true;
case "load-only":
command = SapCommand.LoadOnly;
return true;
default:
command = SapCommand.SystemInfo;
return false;
}
}
private static string RequireValue(string[] args, ref int index, string option)
{
if (index + 1 >= args.Length)
{
throw new ArgumentException("Missing value for " + option);
}
index++;
return args[index];
}
private static int ParseNonNegativeInt(string value, string option)
{
if (!int.TryParse(value, out var number) || number < 0)
{
throw new ArgumentException(option + " expects a non-negative integer.");
}
return number;
}
private static OutputFormat ParseOutputFormat(string value)
{
switch (value.ToLowerInvariant())
{
case "table":
return OutputFormat.Table;
case "csv":
return OutputFormat.Csv;
case "json":
return OutputFormat.Json;
default:
throw new ArgumentException("Unsupported output format: " + value);
}
}
private static IEnumerable<string> SplitCsvList(string value)
{
return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(item => item.Trim())
.Where(item => item.Length > 0);
}
private static void AddSetValue(CliOptions options, string expression)
{
var separator = expression.IndexOf('=');
if (separator < 1)
{
throw new ArgumentException("--set expects NAME=VALUE.");
}
var key = expression.Substring(0, separator).Trim();
var value = expression.Substring(separator + 1);
if (key.Length == 0)
{
throw new ArgumentException("--set expects NAME=VALUE.");
}
options.SetValues[key] = value;
}
public CliOptions CloneForActiveWrite()
{
return new CliOptions
{
Name = Name,
AppServerHost = AppServerHost,
SystemNumber = SystemNumber,
Client = Client,
User = User,
Language = Language,
SapRouter = SapRouter,
Trace = Trace,
Quiet = Quiet,
NoPasswordPrompt = NoPasswordPrompt,
Command = Command,
ProgramName = ProgramName,
SourceFile = SourceFile,
Title = Title,
DevelopmentClass = DevelopmentClass,
TransportNumber = TransportNumber,
ConfirmWrite = ConfirmWrite,
DryRun = DryRun,
SaveInactive = false,
Temporary = Temporary
};
}
}
}