diff --git a/Commands/CdCommand.cs b/Commands/CdCommand.cs
index 6769ff7..d689019 100644
--- a/Commands/CdCommand.cs
+++ b/Commands/CdCommand.cs
@@ -1,6 +1,7 @@
using System.IO;
using NShell.Shell;
+using NShell.Shell.Commands;
using Spectre.Console;
namespace NShell.Commands;
diff --git a/Commands/SetThemeCommand.cs b/Commands/SetThemeCommand.cs
index 3deca51..34e7051 100644
--- a/Commands/SetThemeCommand.cs
+++ b/Commands/SetThemeCommand.cs
@@ -1,5 +1,6 @@
using NShell.Shell;
+using NShell.Shell.Commands;
using Spectre.Console;
namespace NShell.Commands
@@ -20,7 +21,6 @@ public void Execute(ShellContext context, string[] args)
}
string themeName = args[0];
-
bool res = context.SetTheme(themeName);
if(res){AnsiConsole.MarkupLine($"[[[green]+[/]]] - Theme set to: {themeName}");}
diff --git a/NShell.csproj b/NShell.csproj
index f4447f6..d7439bc 100644
--- a/NShell.csproj
+++ b/NShell.csproj
@@ -9,6 +9,7 @@
+
diff --git a/Shell/Commands/CommandParser.cs b/Shell/Commands/CommandParser.cs
index f1d1a3a..9462412 100644
--- a/Shell/Commands/CommandParser.cs
+++ b/Shell/Commands/CommandParser.cs
@@ -1,4 +1,3 @@
-
using Spectre.Console;
using System.Diagnostics;
@@ -10,7 +9,6 @@ namespace NShell.Shell.Commands;
///
public class CommandParser
{
-
public static readonly Dictionary CustomCommands = new();
public static readonly HashSet SystemCommands = new();
private static readonly HashSet InteractiveCommands = new()
@@ -37,19 +35,13 @@ private void LoadCommands()
AnsiConsole.MarkupLine($"\t[[[green]+[/]]] - Loaded custom command: [yellow]{command.Name}[/]");
}
- var TotalCommands = CustomCommands.Count + SystemCommands.Count;
-
LoadSystemCommands();
- if (TotalCommands > 0)
- {
- AnsiConsole.MarkupLine($"[bold grey]→ Total commands loaded:[/] [bold green]{TotalCommands}[/]");
- }
- else
- {
- AnsiConsole.MarkupLine($"[bold grey]→ Total commands loaded:[/] [yellow]{TotalCommands}[/]");
- }
+ var total = CustomCommands.Count + SystemCommands.Count;
+ AnsiConsole.MarkupLine(total > 0
+ ? $"[bold grey]→ Total commands loaded:[/] [bold green]{total}[/]"
+ : $"[bold grey]→ Total commands loaded:[/] [yellow]{total}[/]");
}
///
@@ -63,15 +55,13 @@ private void LoadSystemCommands()
{
if (!Directory.Exists(dir)) continue;
- var commands = Directory.GetFiles(dir)
- .Select(Path.GetFileName)
- .Where(f => !string.IsNullOrWhiteSpace(f));
-
- foreach (var cmd in commands)
+ foreach (var file in Directory.GetFiles(dir))
{
- SystemCommands.Add(cmd);
- var safeCmd = EscapeMarkup(cmd);
- //AnsiConsole.MarkupLine($"\t[[[green]+[/]]] Loaded system command: [yellow]{safeCmd}[/]");
+ var cmd = Path.GetFileName(file);
+ if (!string.IsNullOrWhiteSpace(cmd))
+ {
+ SystemCommands.Add(cmd);
+ }
}
}
}
@@ -85,73 +75,32 @@ private void LoadSystemCommands()
/// Returns true if the command was successfully executed, false otherwise.
public bool TryExecute(string commandLine, ShellContext context)
{
- string expanded = context.ExpandVariables(commandLine);
+ var expanded = context.ExpandVariables(commandLine);
var parts = expanded.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0) return false;
- bool usedSudo = false;
- if (parts[0] == "sudo")
- {
- usedSudo = true;
- parts = parts.Skip(1).ToArray();
- }
+ var usedSudo = parts[0] == "sudo";
+ if (usedSudo) parts = parts.Skip(1).ToArray();
if (parts.Length == 0) return false;
var cmdName = parts[0];
var args = parts.Skip(1).ToArray();
-
- if (cmdName.StartsWith("./") || cmdName.StartsWith("/") || cmdName.Contains("/"))
- {
- try
- {
- var process = new Process
- {
- StartInfo = new ProcessStartInfo
- {
- FileName = "/bin/bash",
- Arguments = $"-c \"{commandLine}\"",
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- RedirectStandardInput = true,
- UseShellExecute = false,
- CreateNoWindow = true
- }
- };
-
- process.Start();
- string output = process.StandardOutput.ReadToEnd();
- string error = process.StandardError.ReadToEnd();
- process.WaitForExit();
-
- if (!string.IsNullOrWhiteSpace(output))
- Console.WriteLine(output);
-
- if (!string.IsNullOrWhiteSpace(error))
- {
- Console.ForegroundColor = ConsoleColor.Red;
- Console.WriteLine(error);
- Console.ResetColor();
- }
- return true;
- }
- catch (Exception ex)
- {
- Console.WriteLine($"[[[red]-[/]]] - Error executing file: {ex.Message}");
- return true;
- }
+ if (cmdName.StartsWith("./"))
+ {
+ return ExecuteLocalFile(commandLine);
}
- if (CustomCommands.TryGetValue(cmdName, out var command))
+ if (CustomCommands.TryGetValue(cmdName, out var customCmd))
{
- if (command is IMetadataCommand meta && meta.RequiresRoot && !(usedSudo || IsRootUser()))
+ if (customCmd is IMetadataCommand meta && meta.RequiresRoot && !(usedSudo || IsRootUser()))
{
AnsiConsole.MarkupLine("[red][[-]] - This command requires root privileges. Prefix with [bold yellow]sudo[/] or run as root.[/]");
return true;
}
- command.Execute(context, args);
+ customCmd.Execute(context, args);
return true;
}
@@ -160,11 +109,13 @@ public bool TryExecute(string commandLine, ShellContext context)
var fullPath = ResolveSystemCommandPath(cmdName);
if (fullPath != null)
{
- bool success = RunSystemCommand(fullPath, args, usedSudo);
+ RunSystemCommand(fullPath, args, usedSudo);
+
if ((cmdName == "apt" || cmdName == "apt-get") && args.Length > 0 && args[0] == "install")
{
CommandLoader.RefreshCommands();
}
+
return true;
}
}
@@ -174,44 +125,59 @@ public bool TryExecute(string commandLine, ShellContext context)
}
///
- /// Resolves the full path of a system command by searching in common system directories.
+ /// Executes a local shell file.
///
- /// The name of the system command.
- /// The full path to the command if found, otherwise null.
- private static string? ResolveSystemCommandPath(string cmdName)
+ private static bool ExecuteLocalFile(string commandLine)
{
- var paths = new[] { "/usr/bin", "/usr/local/bin", "/usr/games", "/bin", "/sbin", "/usr/sbin" };
+ try
+ {
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = "/bin/bash",
+ Arguments = $"-c \"{commandLine}\"",
+ UseShellExecute = false
+ }
+ };
- foreach (var path in paths)
+ process.Start();
+ process.WaitForExit();
+
+ return true;
+ }
+ catch (Exception ex)
{
- var fullPath = Path.Combine(path, cmdName);
- if (File.Exists(fullPath) && IsExecutable(fullPath))
- return fullPath;
+ AnsiConsole.MarkupLine($"[[[red]-[/]]] - Error executing file: {ex.Message}");
+ return true;
}
+ }
+
+ ///
+ /// Resolves the full path of a system command by searching in common system directories.
+ ///
+ private static string? ResolveSystemCommandPath(string cmdName)
+ {
+ var paths = new[] { "/usr/bin", "/usr/local/bin", "/usr/games", "/bin", "/sbin", "/usr/sbin" };
- return null;
+ return paths.Select(path => Path.Combine(path, cmdName))
+ .FirstOrDefault(fullPath => File.Exists(fullPath) && IsExecutable(fullPath));
}
///
/// Checks if a file is executable.
///
- /// The path to the file.
- /// True if the file is executable, otherwise false.
private static bool IsExecutable(string path)
{
- return (new FileInfo(path).Exists && (new FileInfo(path).Attributes & FileAttributes.Directory) == 0);
+ return new FileInfo(path).Exists;
}
///
/// Runs a system command, optionally using `sudo`, and handles interactive vs non-interactive command behavior.
///
- /// The full path to the system command.
- /// Arguments to pass to the command.
- /// Whether to use `sudo` to run the command.
private static bool RunSystemCommand(string path, string[] args, bool useSudo)
{
- bool isInteractive = InteractiveCommands.Contains(Path.GetFileName(path));
-
+ var isInteractive = InteractiveCommands.Contains(Path.GetFileName(path));
var startInfo = new ProcessStartInfo
{
FileName = useSudo ? "/usr/bin/sudo" : path,
@@ -219,20 +185,18 @@ private static bool RunSystemCommand(string path, string[] args, bool useSudo)
UseShellExecute = isInteractive,
RedirectStandardOutput = !isInteractive,
RedirectStandardError = !isInteractive,
- RedirectStandardInput = false,
CreateNoWindow = false
};
- var process = new Process { StartInfo = startInfo };
+ using var process = new Process { StartInfo = startInfo };
if (!isInteractive)
{
- process.OutputDataReceived += (s, e) =>
+ process.OutputDataReceived += (_, e) =>
{
if (!string.IsNullOrWhiteSpace(e.Data)) Console.WriteLine(e.Data);
};
-
- process.ErrorDataReceived += (s, e) =>
+ process.ErrorDataReceived += (_, e) =>
{
if (!string.IsNullOrWhiteSpace(e.Data))
{
@@ -252,27 +216,14 @@ private static bool RunSystemCommand(string path, string[] args, bool useSudo)
}
process.WaitForExit();
-
return process.ExitCode == 0;
}
///
/// Checks if the current user is root.
///
- /// True if the current user is root, otherwise false.
private static bool IsRootUser()
{
return Environment.UserName == "root" || Environment.GetEnvironmentVariable("USER") == "root";
}
-
- ///
- /// Escapes markup characters in a string.
- ///
- /// The input string to escape.
- /// The escaped string.
- private static string EscapeMarkup(string input)
- {
- return input.Replace("[", "[[").Replace("]", "]]");
- }
}
-
diff --git a/Shell/Commands/CommandRegistry.cs b/Shell/Commands/CommandRegistry.cs
index 4c0dd8c..a2c5d5f 100644
--- a/Shell/Commands/CommandRegistry.cs
+++ b/Shell/Commands/CommandRegistry.cs
@@ -1,4 +1,5 @@
+using Spectre.Console;
using NShell.Commands;
namespace NShell.Shell.Commands;
@@ -8,6 +9,9 @@ namespace NShell.Shell.Commands;
///
public static class CommandRegistry
{
+
+ private static readonly Dictionary _commands = new();
+
///
/// Retrieves all registered custom commands implemented in the shell.
///
@@ -20,4 +24,20 @@ public static IEnumerable GetAll()
new SetThemeCommand(),
};
}
+
+ ///
+ /// Register a new plugin command into the shell.
+ /// This method adds the command to the command list and prints a confirmation.
+ ///
+ /// The plugin command implementing ICustomCommand to be registered.
+ public static void Register(ICustomCommand command)
+ {
+ if (!_commands.ContainsKey(command.Name))
+ {
+ _commands.Add(command.Name, command);
+ AnsiConsole.MarkupLine($"\t[[[green]+[/]]] - Registered plugin command: [yellow]{command.Name}[/]");
+ }
+ }
+
+
}
\ No newline at end of file
diff --git a/Shell/Commands/ICustomCommand.cs b/Shell/Commands/ICustomCommand.cs
index a4fb5c0..0ffbf13 100644
--- a/Shell/Commands/ICustomCommand.cs
+++ b/Shell/Commands/ICustomCommand.cs
@@ -1,5 +1,5 @@
-namespace NShell.Shell;
+namespace NShell.Shell.Commands;
///
/// The ICustomCommand interface defines a contract for implementing
diff --git a/Shell/Plugin/PluginLoadContext.cs b/Shell/Plugin/PluginLoadContext.cs
new file mode 100644
index 0000000..4258853
--- /dev/null
+++ b/Shell/Plugin/PluginLoadContext.cs
@@ -0,0 +1,44 @@
+using System.Reflection;
+using System.Runtime.Loader;
+
+public class PluginLoadContext : AssemblyLoadContext
+{
+ private readonly AssemblyDependencyResolver _resolver;
+
+ public PluginLoadContext(string pluginPath) : base(isCollectible: false)
+ {
+ _resolver = new AssemblyDependencyResolver(pluginPath);
+ }
+
+ protected override Assembly? Load(AssemblyName assemblyName)
+ {
+ if (assemblyName.Name == "System.Runtime"
+ || assemblyName.Name == "System.Private.CoreLib"
+ || assemblyName.Name.StartsWith("System.")
+ || assemblyName.Name.StartsWith("Microsoft."))
+ {
+ return null;
+ }
+
+ var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
+ if (assemblyPath != null)
+ {
+ Console.WriteLine($"[PluginLoadContext] Loading {assemblyName} from {assemblyPath}");
+ return LoadFromAssemblyPath(assemblyPath);
+ }
+
+ return null;
+ }
+
+
+ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
+ {
+ string? libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
+ if (libraryPath != null)
+ {
+ return LoadUnmanagedDllFromPath(libraryPath);
+ }
+
+ return IntPtr.Zero;
+ }
+}
\ No newline at end of file
diff --git a/Shell/Plugin/PluginLoader.cs b/Shell/Plugin/PluginLoader.cs
index 4ae2d3d..6db9af8 100644
--- a/Shell/Plugin/PluginLoader.cs
+++ b/Shell/Plugin/PluginLoader.cs
@@ -1,57 +1,97 @@
-
using Spectre.Console;
+using System.Reflection;
+using NShell.Shell.Commands;
+
+namespace NShell.Shell.Plugins;
-namespace NShell.Shell.Plugins
+///
+/// Manages loading and registration of plugins implementing ICustomCommand.
+///
+public class PluginLoader
{
+ private readonly string PluginFolderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nshell", "plugins");
+ private readonly List _loadedPlugins = new();
+
+ public string[] PluginList { get; private set; } = [];
+ public int NumberOfPlugins { get; private set; }
+
+ public static PluginLoader Instance { get; private set; } = new();
+
+ public void LoadPlugins()
+ {
+ if (!Directory.Exists(PluginFolderPath))
+ {
+ AnsiConsole.MarkupLine("\t[[[red]-[/]]] - Plugin directory doesn't exist.");
+ AnsiConsole.MarkupLine("\t[[[yellow]*[/]]] - Creating plugins directory.");
+ Directory.CreateDirectory(PluginFolderPath);
+ }
+
+ PluginList = Directory.GetFiles(PluginFolderPath, "*.dll", SearchOption.AllDirectories);
+
+ if (PluginList.Length > 0)
+ {
+ foreach (var pluginPath in PluginList)
+ {
+ try
+ {
+ var context = new PluginLoadContext(pluginPath);
+ var assembly = context.LoadFromAssemblyPath(pluginPath);
+
+ _loadedPlugins.Add(assembly);
+ AnsiConsole.MarkupLine($"\t[[[green]+[/]]] - Loading plugin: [yellow]{Path.GetFileName(pluginPath)}[/]");
+ }
+ catch (Exception ex)
+ {
+ AnsiConsole.MarkupLine($"\t[[[red]-[/]]] - Failed to load plugin: {Path.GetFileName(pluginPath)}");
+ AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks);
+ }
+ }
+
+ NumberOfPlugins = _loadedPlugins.Count;
+ AnsiConsole.MarkupLine($"[bold grey]→ Total plugins loaded:[/] [bold green]{NumberOfPlugins}[/]");
+
+ LoadPluginCommands();
+ }
+ else
+ {
+ NumberOfPlugins = 0;
+ AnsiConsole.MarkupLine("\t[[[yellow]*[/]]] - No plugins found.");
+ AnsiConsole.MarkupLine($"[bold grey]→ Total plugins loaded:[/] [bold yellow]0[/]");
+ }
+ }
///
- /// PluginLoader manage all about loading, parse, execute plugins.
+ /// Extracts and registers all valid types implementing ICustomCommand.
///
- public class PluginLoader
+ private void LoadPluginCommands()
{
- private string PluginFolderPath { get; set; } = $"/home/{Environment.UserName}/.nshell/plugins";
- public string[] PluginList { get; set; }
- public int NumberOfPlugins { get; set; }
-
- public static PluginLoader Instance { get; private set; } = null!;
-
- ///
- /// Load all plugins into ~/.nshell/plugins folder.
- ///
- public void LoadPlugins()
+ foreach (var plugin in _loadedPlugins)
{
- if (Path.Exists(PluginFolderPath))
+ try
{
- var PluginList = Directory.GetFiles(
- PluginFolderPath,
- "*.dll",
- SearchOption.AllDirectories);
+ var commandTypes = plugin.GetTypes()
+ .Where(t => typeof(ICustomCommand).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
- if (PluginList.Length > 0)
+ foreach (var type in commandTypes)
{
- NumberOfPlugins = PluginList.Length;
- foreach (var plugin in PluginList)
+ if (Activator.CreateInstance(type) is ICustomCommand command)
{
- AnsiConsole.MarkupLine($"\t[[[green]+[/]]] - Loading plugin : [yellow]{plugin}[/].");
+ CommandRegistry.Register(command);
+ AnsiConsole.MarkupLine($"[[[green]+[/]]] - Loaded plugin command: [yellow]{command.Name}[/]");
}
- AnsiConsole.MarkupLine($"[bold grey]→ Total plugins loaded:[/] [bold green]{NumberOfPlugins}[/]");
}
- else
+ }
+ catch (ReflectionTypeLoadException rtle)
+ {
+ foreach (var loaderEx in rtle.LoaderExceptions)
{
- NumberOfPlugins = 0;
- AnsiConsole.MarkupLine($"\t[[[yellow]*[/]]] - No plugins found.");
- AnsiConsole.MarkupLine($"[bold grey]→ Total plugins loaded:[/] [bold yellow]{NumberOfPlugins}[/]");
+ AnsiConsole.MarkupLine($"[[[red]-[/]]] - Error loading plugin type: [red]{loaderEx?.Message}[/]");
}
}
- else
+ catch (Exception ex)
{
- PluginList = [];
- AnsiConsole.MarkupLine($"\t[[[red]-[/]]] - Plugin directory doesn't exist.");
- AnsiConsole.MarkupLine($"\t[[[yellow]*[/]]] - Creating plugins directory.");
- Directory.CreateDirectory(PluginFolderPath);
+ AnsiConsole.MarkupLine($"[[[red]-[/]]] - Error processing plugin: [red]{ex.Message}[/]");
}
}
-
}
-
}
diff --git a/Shell/ShellContext.cs b/Shell/ShellContext.cs
index 1eaabe1..28c24c2 100644
--- a/Shell/ShellContext.cs
+++ b/Shell/ShellContext.cs
@@ -3,6 +3,7 @@
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
+using NShell.Shell.Commands;
using NShell.Shell.Themes;
using Spectre.Console;
@@ -14,6 +15,7 @@ public class ShellContext
public string CurrentTheme { get; set; } = "default";
public string Prompt { get; private set; }
public string LSColors { get; private set; }
+ public Dictionary CustomCommands => CommandParser.CustomCommands;
public ShellContext()
{