mirror of
https://github.com/mRemoteNG/mRemoteNG.git
synced 2026-02-26 03:58:45 +08:00
Merge branch 'v1.78.2-dev' of https://github.com/mRemoteNG/mRemoteNG into v1.78.2-dev
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
<PackageVersion Include="Gherkin" Version="38.0.0" />
|
||||
<PackageVersion Include="Google.Protobuf" Version="3.33.5" />
|
||||
<PackageVersion Include="LiteDB" Version="5.0.21" />
|
||||
<PackageVersion Include="log4net" Version="3.2.0" />
|
||||
<PackageVersion Include="log4net" Version="3.3.0" />
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient.SNI" Version="6.0.2" />
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient.SNI.runtime" Version="6.0.2" />
|
||||
@@ -62,6 +62,7 @@
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="10.0.3" />
|
||||
<PackageVersion Include="System.Console" Version="4.3.1" />
|
||||
<PackageVersion Include="System.Data.Common" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Data.Odbc" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.3" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="10.0.3" />
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="10.0.3" />
|
||||
@@ -95,7 +96,7 @@
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.3" />
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<PackageVersion Include="System.ValueTuple" Version="4.6.1" />
|
||||
<PackageVersion Include="System.ValueTuple" Version="4.6.2" />
|
||||
<PackageVersion Include="System.Windows.Extensions" Version="10.0.3" />
|
||||
<PackageVersion Include="System.Xml.ReaderWriter" Version="4.3.1" />
|
||||
<PackageVersion Include="VaultSharp" Version="1.17.5.1" />
|
||||
|
||||
@@ -1,149 +1,38 @@
|
||||
using System;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using System;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using System.Runtime.InteropServices;
|
||||
using LiteDB;
|
||||
using mRemoteNG.Resources.Language;
|
||||
|
||||
namespace mRemoteNG.Config.DatabaseConnectors
|
||||
{
|
||||
//[SupportedOSPlatform("windows")]
|
||||
/// <summary>
|
||||
/// A helper class for testing database connectivity
|
||||
/// A helper class for testing database connectivity.
|
||||
/// </summary>
|
||||
///
|
||||
using System;
|
||||
using System.Data.SqlClient;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public class DatabaseConnectionTester
|
||||
{
|
||||
public async Task<ConnectionTestResult> TestConnectivity(string type, string server, string database, string username, string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Build the connection string based on the provided parameters
|
||||
string connectionString = $"Data Source={server};Initial Catalog={database};User ID={username};Password={password}";
|
||||
|
||||
// Attempt to open a connection to the database
|
||||
using (SqlConnection connection = new SqlConnection(connectionString))
|
||||
{
|
||||
await connection.OpenAsync();
|
||||
}
|
||||
|
||||
using IDatabaseConnector dbConnector = DatabaseConnectorFactory.DatabaseConnector(type, server, database, username, password);
|
||||
await dbConnector.ConnectAsync();
|
||||
return ConnectionTestResult.ConnectionSucceded;
|
||||
}
|
||||
catch (SqlException ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle specific SQL exceptions
|
||||
switch (ex.Number)
|
||||
{
|
||||
case 4060: // Invalid Database
|
||||
return ConnectionTestResult.UnknownDatabase;
|
||||
case 18456: // Login Failed
|
||||
return ConnectionTestResult.CredentialsRejected;
|
||||
case -1: // Server not accessible
|
||||
return ConnectionTestResult.ServerNotAccessible;
|
||||
default:
|
||||
return ConnectionTestResult.UnknownError;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Handle any other exceptions
|
||||
string message = ex.Message;
|
||||
if (message.Contains("server was not found", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("network-related", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("instance-specific", StringComparison.OrdinalIgnoreCase))
|
||||
return ConnectionTestResult.ServerNotAccessible;
|
||||
if (message.Contains("Cannot open database", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("Unknown database", StringComparison.OrdinalIgnoreCase))
|
||||
return ConnectionTestResult.UnknownDatabase;
|
||||
if (message.Contains("Login failed", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("Access denied", StringComparison.OrdinalIgnoreCase))
|
||||
return ConnectionTestResult.CredentialsRejected;
|
||||
return ConnectionTestResult.UnknownError;
|
||||
}
|
||||
}
|
||||
}
|
||||
//public class DatabaseConnectionTester
|
||||
//{
|
||||
//public async Task<ConnectionTestResult> TestConnectivity(string type, string server, string database, string username, string password)
|
||||
//{
|
||||
//using IDatabaseConnector dbConnector = DatabaseConnectorFactory.DatabaseConnector(type, server, database, username, password);
|
||||
//try
|
||||
//{
|
||||
// Validate architecture compatibility
|
||||
//if (!Environment.Is64BitProcess)
|
||||
//{
|
||||
// throw new PlatformNotSupportedException("The application must run in a 64-bit process to use this database connector.");
|
||||
// }
|
||||
|
||||
// Attempt to connect
|
||||
|
||||
//using (SqlConnection connection = new SqlConnection("Data Source=172.22.155.100,1433;Initial Catalog=Demo;Integrated Security=False;User ID=sa;Password=London123;Multiple Active Result Sets=True;Connect Timeout=30;Encrypt=True;Trust Server Certificate=True;Application Name=mRemoteNG;Application Intent=ReadOnly"))
|
||||
//{
|
||||
// connection.Open();
|
||||
// Console.WriteLine("Connection successful!");
|
||||
//}
|
||||
//Console.WriteLine($"{RuntimeInformation.OSArchitecture}");
|
||||
//Console.WriteLine($"{RuntimeInformation.ProcessArchitecture}");
|
||||
//try
|
||||
//{
|
||||
// using (SqlConnection connection = new SqlConnection("Data Source=172.22.155.100,1433;Initial Catalog=Demo;Integrated Security=False;User ID=sa;Password=London123;Multiple Active Result Sets=True;Connect Timeout=30;Encrypt=True;Trust Server Certificate=True;Application Name=mRemoteNG;Application Intent=ReadOnly"))
|
||||
// {
|
||||
// connection.Open();
|
||||
// Console.WriteLine("Connection successful!");
|
||||
// }
|
||||
//}
|
||||
//catch (Exception ex)
|
||||
//{
|
||||
// Console.WriteLine($"Connection failed: {ex.Message}");
|
||||
//}
|
||||
//}
|
||||
/*
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
using (SqlConnection connection = new SqlConnection("Data Source=172.22.155.100,1433;Initial Catalog=Demo;Integrated Security=False;User ID=sa;Password=London123;Multiple Active Result Sets=True;Connect Timeout=30;Encrypt=True;Trust Server Certificate=True;Application Name=mRemoteNG;Application Intent=ReadOnly"))
|
||||
{
|
||||
connection.Open();
|
||||
}
|
||||
}
|
||||
catch (TypeInitializationException ex)
|
||||
{
|
||||
Console.WriteLine($"Type initialization error: {ex.InnerException?.Message}");
|
||||
}
|
||||
|
||||
|
||||
//await dbConnector.ConnectAsync();
|
||||
return ConnectionTestResult.ConnectionSucceded;
|
||||
}
|
||||
catch (PlatformNotSupportedException ex)
|
||||
{
|
||||
// Log or handle architecture mismatch
|
||||
Console.WriteLine(string.Format(Language.ErrorPlatformNotSupported, ex.Message));
|
||||
return ConnectionTestResult.UnknownError;
|
||||
}
|
||||
catch (DllNotFoundException ex)
|
||||
{
|
||||
// Handle missing native dependencies
|
||||
Console.WriteLine(string.Format(Language.ErrorMissingDependency, ex.Message));
|
||||
return ConnectionTestResult.UnknownError;
|
||||
}
|
||||
catch (BadImageFormatException ex)
|
||||
{
|
||||
// Handle architecture mismatch in native libraries
|
||||
Console.WriteLine(string.Format(Language.ErrorArchitectureMismatch, ex.Message));
|
||||
return ConnectionTestResult.UnknownError;
|
||||
}
|
||||
catch (SqlException sqlException)
|
||||
{
|
||||
if (sqlException.Message.Contains("The server was not found"))
|
||||
return ConnectionTestResult.ServerNotAccessible;
|
||||
if (sqlException.Message.Contains("Cannot open database"))
|
||||
return ConnectionTestResult.UnknownDatabase;
|
||||
if (sqlException.Message.Contains("Login failed for user"))
|
||||
return ConnectionTestResult.CredentialsRejected;
|
||||
return ConnectionTestResult.UnknownError;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log unexpected errors
|
||||
Console.WriteLine($"Unexpected error: {ex.Message}");
|
||||
return ConnectionTestResult.UnknownError;
|
||||
}
|
||||
*/
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace mRemoteNG.Config.DatabaseConnectors
|
||||
{
|
||||
case "mysql":
|
||||
return new MySqlDatabaseConnector(server, database, username, password);
|
||||
case "odbc":
|
||||
throw new NotSupportedException("ODBC database connections are not supported for schema initialization. Please use a supported database backend.");
|
||||
case "mssql":
|
||||
default:
|
||||
return new MSSqlDatabaseConnector(server, database, username, password);
|
||||
|
||||
106
mRemoteNG/Config/DatabaseConnectors/OdbcDatabaseConnector.cs
Normal file
106
mRemoteNG/Config/DatabaseConnectors/OdbcDatabaseConnector.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Data.Odbc;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
// ReSharper disable ArrangeAccessorOwnerBody
|
||||
|
||||
namespace mRemoteNG.Config.DatabaseConnectors
|
||||
{
|
||||
public class OdbcDatabaseConnector : IDatabaseConnector
|
||||
{
|
||||
private DbConnection _dbConnection { get; set; } = default(OdbcConnection);
|
||||
private string _dbConnectionString = "";
|
||||
private readonly string _connectionString;
|
||||
private readonly string _dbUsername;
|
||||
private readonly string _dbPassword;
|
||||
|
||||
public DbConnection DbConnection()
|
||||
{
|
||||
return _dbConnection;
|
||||
}
|
||||
|
||||
public DbCommand DbCommand(string dbCommand)
|
||||
{
|
||||
return new OdbcCommand(dbCommand, (OdbcConnection) _dbConnection);
|
||||
}
|
||||
|
||||
public bool IsConnected => (_dbConnection.State == ConnectionState.Open);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ODBC database connector.
|
||||
/// </summary>
|
||||
/// <param name="connectionString">
|
||||
/// An ODBC connection string or DSN name. If a plain DSN name is provided (no '=' character),
|
||||
/// it is automatically wrapped as "DSN=<name>".
|
||||
/// </param>
|
||||
/// <param name="username">Optional user name appended to the connection string as UID.</param>
|
||||
/// <param name="password">Optional password appended to the connection string as PWD.</param>
|
||||
public OdbcDatabaseConnector(string connectionString, string username, string password)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
_dbUsername = username;
|
||||
_dbPassword = password;
|
||||
Initialize();
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
BuildConnectionString();
|
||||
_dbConnection = new OdbcConnection(_dbConnectionString);
|
||||
}
|
||||
|
||||
private void BuildConnectionString()
|
||||
{
|
||||
// If no '=' present in the provided string it is a plain DSN name.
|
||||
string baseString = _connectionString.Contains('=')
|
||||
? _connectionString
|
||||
: $"DSN={_connectionString}";
|
||||
|
||||
OdbcConnectionStringBuilder builder = new()
|
||||
{
|
||||
ConnectionString = baseString
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(_dbUsername))
|
||||
builder["UID"] = _dbUsername;
|
||||
|
||||
if (!string.IsNullOrEmpty(_dbPassword))
|
||||
builder["PWD"] = _dbPassword;
|
||||
|
||||
_dbConnectionString = builder.ConnectionString;
|
||||
}
|
||||
|
||||
public void Connect()
|
||||
{
|
||||
_dbConnection.Open();
|
||||
}
|
||||
|
||||
public async Task ConnectAsync()
|
||||
{
|
||||
await _dbConnection.OpenAsync();
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
_dbConnection.Close();
|
||||
}
|
||||
|
||||
public void AssociateItemToThisConnector(DbCommand dbCommand)
|
||||
{
|
||||
dbCommand.Connection = (OdbcConnection) _dbConnection;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool itIsSafeToFreeManagedObjects)
|
||||
{
|
||||
if (!itIsSafeToFreeManagedObjects) return;
|
||||
_dbConnection.Close();
|
||||
_dbConnection.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,7 @@
|
||||
<PackageReference Include="Microsoft.Web.WebView2" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
|
||||
<PackageReference Include="MySql.Data" />
|
||||
<PackageReference Include="System.Data.Odbc" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" />
|
||||
<PackageReference Include="OpenCover" />
|
||||
|
||||
@@ -5,6 +5,10 @@ Common External Tool Configurations
|
||||
The list below of various examples is by no means a full list of ways to use
|
||||
**External Tools** but gives you a idea of how it can be used in different ways.
|
||||
|
||||
.. note::
|
||||
These examples use variables like %HOSTNAME%, %USERNAME%, etc.
|
||||
For a complete list of available variables, see the :ref:`variables_reference` page.
|
||||
|
||||
Ping
|
||||
====
|
||||
Ping a server via cmdline.
|
||||
|
||||
@@ -23,6 +23,8 @@ What we are going to use is the following for our entry:
|
||||
- Protocol - sftp://
|
||||
- Input Parameters (variables) - %HOSTNAME%, %USERNAME%,%PASSWORD% and %PORT%
|
||||
|
||||
For a complete list of available variables, see the :ref:`variables_reference` page.
|
||||
|
||||
All of the variables are parsed from mRemoteNG connection item to the filezilla command line.
|
||||
So lets build this entry up in **External Tools** where we add all these items.
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ Welcome to mRemoteNG's documentation!
|
||||
:maxdepth: 2
|
||||
:caption: Miscellaneous
|
||||
|
||||
variables_reference.rst
|
||||
external_tools_cheat_sheet.rst
|
||||
migrate.rst
|
||||
Contribute <https://github.com/mRemoteNG/mRemoteNG/wiki>
|
||||
|
||||
@@ -100,7 +100,9 @@ Variables
|
||||
|
||||
Variables and arguments can be used to tell the external tool what to do.
|
||||
|
||||
This is the list of variables supported by mRemoteNG:
|
||||
For a complete list of available variables and their usage, see the :ref:`variables_reference` page.
|
||||
|
||||
Quick Reference - Available Variables:
|
||||
|
||||
- %NAME%
|
||||
- %HOSTNAME%
|
||||
@@ -112,53 +114,9 @@ This is the list of variables supported by mRemoteNG:
|
||||
- %MACADDRESS%
|
||||
- %USERFIELD%
|
||||
|
||||
mRemoteNG will also expand environment variables such as %PATH% and %USERPROFILE%. If you need to use an environment
|
||||
variable with the same name as an mRemoteNG variable, use \\% instead of %. The most common use of this is for the
|
||||
USERNAME environment variable. %USERNAME% will be expanded to the username set in the currently selected connection.
|
||||
\\%USERNAME\\% will be expanded to the value set in the USERNAME environment variable.
|
||||
See the :ref:`variables_reference` page for detailed information about:
|
||||
|
||||
If you need to send a variable name to a program without mRemoteNG expanding it, use ^% instead of %.
|
||||
mRemoteNG will remove the caret (^) and leave the rest unchanged.
|
||||
For example, ^%USERNAME^% will be sent to the program as %USERNAME% and will not be expanded.
|
||||
|
||||
Rules for variables
|
||||
-------------------
|
||||
- Variables always refer to the currently selected connection.
|
||||
- Variable names are case-insensitive.
|
||||
- Variables can be used in both the Filename and Arguments fields.
|
||||
|
||||
|
||||
Special Character Escaping
|
||||
==========================
|
||||
Expanded variables will be escaped using the rules below. There are two levels of escaping that are done.
|
||||
|
||||
1. Is escaping for standard argument splitting (C/C++ argv, CommandLineToArgvW, etc)
|
||||
2. Is escaping shell metacharacters for ShellExecute.
|
||||
|
||||
Argument splitting escaping
|
||||
---------------------------
|
||||
|
||||
- Each quotation mark will be escaped by a backslash
|
||||
- One or more backslashes (\\) followed by a quotation mark ("):
|
||||
- Each backslash will be escaped by another backslash
|
||||
- The quotation mark will be escaped by a backslash
|
||||
- If the connection's user field contains ``"This"`` is a ``\"test\"``
|
||||
- Then %USERFIELD% is replaced with ``\"This\"`` is a ``\\\"test\\\"``
|
||||
- A variable name followed by a quotation mark (for example, %USERFIELD%") with a value ending in one or more backslashes:
|
||||
- Each backslash will be escaped by another backslash
|
||||
- Example:
|
||||
- If the connection's user field contains ``c:\Example\``
|
||||
- Then "%USERFIELD%" is replaced with ``"c:\Example\\"``
|
||||
|
||||
To disable argument splitting escaping for a variable, precede its name with a minus (-) sign. For example: %-USERFIELD%
|
||||
|
||||
Shell metacharacter escaping
|
||||
----------------------------
|
||||
|
||||
- The shell metacharacters are ( ) % ! ^ " < > & |
|
||||
- Each shell metacharacter will be escaped by a caret (^)
|
||||
|
||||
To disable both argument splitting and shell metacharacter escaping for a variable, precede its name with an exclamation point (!).
|
||||
For example, %!USERFIELD%. This is not recommended and may cause unexpected results.
|
||||
|
||||
Only variables that have been expanded will be escaped. It is up to you to escape the rest of the arguments.
|
||||
- How to use environment variables
|
||||
- How to prevent variable expansion
|
||||
- Rules for using variables
|
||||
- Special character escaping
|
||||
|
||||
102
mRemoteNGDocumentation/variables_reference.rst
Normal file
102
mRemoteNGDocumentation/variables_reference.rst
Normal file
@@ -0,0 +1,102 @@
|
||||
.. _variables_reference:
|
||||
|
||||
*******************
|
||||
Variables Reference
|
||||
*******************
|
||||
|
||||
Variables (also called parameters) can be used with External Tools to dynamically insert connection properties into commands and arguments.
|
||||
|
||||
Available Variables
|
||||
===================
|
||||
|
||||
mRemoteNG supports the following variables:
|
||||
|
||||
%NAME%
|
||||
The display name of the connection.
|
||||
|
||||
%HOSTNAME%
|
||||
The hostname or IP address of the connection.
|
||||
|
||||
%PORT%
|
||||
The port number for the connection.
|
||||
|
||||
%USERNAME%
|
||||
The username configured for the connection.
|
||||
|
||||
%PASSWORD%
|
||||
The password configured for the connection.
|
||||
|
||||
%DOMAIN%
|
||||
The domain configured for the connection.
|
||||
|
||||
%DESCRIPTION%
|
||||
The description text of the connection.
|
||||
|
||||
%MACADDRESS%
|
||||
The MAC address configured for the connection.
|
||||
|
||||
%USERFIELD%
|
||||
The custom user field configured for the connection.
|
||||
|
||||
Environment Variables
|
||||
======================
|
||||
|
||||
mRemoteNG will also expand environment variables such as %PATH% and %USERPROFILE%. If you need to use an environment
|
||||
variable with the same name as an mRemoteNG variable, use \\% instead of %. The most common use of this is for the
|
||||
USERNAME environment variable. %USERNAME% will be expanded to the username set in the currently selected connection.
|
||||
\\%USERNAME\\% will be expanded to the value set in the USERNAME environment variable.
|
||||
|
||||
Preventing Variable Expansion
|
||||
==============================
|
||||
|
||||
If you need to send a variable name to a program without mRemoteNG expanding it, use ^% instead of %.
|
||||
mRemoteNG will remove the caret (^) and leave the rest unchanged.
|
||||
For example, ^%USERNAME^% will be sent to the program as %USERNAME% and will not be expanded.
|
||||
|
||||
Rules for Variables
|
||||
===================
|
||||
|
||||
- Variables always refer to the currently selected connection.
|
||||
- Variable names are case-insensitive.
|
||||
- Variables can be used in both the Filename and Arguments fields in External Tools.
|
||||
|
||||
Special Character Escaping
|
||||
===========================
|
||||
|
||||
Expanded variables will be escaped using the rules below. There are two levels of escaping that are done:
|
||||
|
||||
1. Escaping for standard argument splitting (C/C++ argv, CommandLineToArgvW, etc)
|
||||
2. Escaping shell metacharacters for ShellExecute.
|
||||
|
||||
Argument Splitting Escaping
|
||||
----------------------------
|
||||
|
||||
- Each quotation mark will be escaped by a backslash
|
||||
- One or more backslashes (\\) followed by a quotation mark ("):
|
||||
- Each backslash will be escaped by another backslash
|
||||
- The quotation mark will be escaped by a backslash
|
||||
- If the connection's user field contains ``"This"`` is a ``\"test\"``
|
||||
- Then %USERFIELD% is replaced with ``\"This\"`` is a ``\\\"test\\\"``
|
||||
- A variable name followed by a quotation mark (for example, %USERFIELD%") with a value ending in one or more backslashes:
|
||||
- Each backslash will be escaped by another backslash
|
||||
- Example:
|
||||
- If the connection's user field contains ``c:\Example\``
|
||||
- Then "%USERFIELD%" is replaced with ``"c:\Example\\"``
|
||||
|
||||
To disable argument splitting escaping for a variable, precede its name with a minus (-) sign. For example: %-USERFIELD%
|
||||
|
||||
Shell Metacharacter Escaping
|
||||
-----------------------------
|
||||
|
||||
- The shell metacharacters are ( ) % ! ^ " < > & |
|
||||
- Each shell metacharacter will be escaped by a caret (^)
|
||||
|
||||
To disable both argument splitting and shell metacharacter escaping for a variable, precede its name with an exclamation point (!).
|
||||
For example, %!USERFIELD%. This is not recommended and may cause unexpected results.
|
||||
|
||||
Only variables that have been expanded will be escaped. It is up to you to escape the rest of the arguments.
|
||||
|
||||
See Also
|
||||
========
|
||||
|
||||
- :ref:`external_tools` - For information on using variables with External Tools
|
||||
Reference in New Issue
Block a user