Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Commands/CdCommand.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

using System.IO;
using NShell.Shell;
using NShell.Shell.Commands;
using Spectre.Console;

namespace NShell.Commands;
Expand Down
2 changes: 1 addition & 1 deletion Commands/SetThemeCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

using NShell.Shell;
using NShell.Shell.Commands;
using Spectre.Console;

namespace NShell.Commands
Expand All @@ -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}");}
Expand Down
1 change: 1 addition & 0 deletions NShell.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.50.0" />
<PackageReference Include="System.Memory" Version="4.6.3" />
</ItemGroup>

</Project>
165 changes: 58 additions & 107 deletions Shell/Commands/CommandParser.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

using Spectre.Console;
using System.Diagnostics;

Expand All @@ -10,7 +9,6 @@ namespace NShell.Shell.Commands;
/// </summary>
public class CommandParser
{

public static readonly Dictionary<string, ICustomCommand> CustomCommands = new();
public static readonly HashSet<string> SystemCommands = new();
private static readonly HashSet<string> InteractiveCommands = new()
Expand All @@ -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}[/]");
}

/// <summary>
Expand All @@ -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);
}
}
}
}
Expand All @@ -85,73 +75,32 @@ private void LoadSystemCommands()
/// <returns>Returns true if the command was successfully executed, false otherwise.</returns>
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;
}

Expand All @@ -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;
}
}
Expand All @@ -174,65 +125,78 @@ public bool TryExecute(string commandLine, ShellContext context)
}

/// <summary>
/// Resolves the full path of a system command by searching in common system directories.
/// Executes a local shell file.
/// </summary>
/// <param name="cmdName">The name of the system command.</param>
/// <returns>The full path to the command if found, otherwise null.</returns>
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;
}
}

/// <summary>
/// Resolves the full path of a system command by searching in common system directories.
/// </summary>
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));
}

/// <summary>
/// Checks if a file is executable.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <returns>True if the file is executable, otherwise false.</returns>
private static bool IsExecutable(string path)
{
return (new FileInfo(path).Exists && (new FileInfo(path).Attributes & FileAttributes.Directory) == 0);
return new FileInfo(path).Exists;
}

/// <summary>
/// Runs a system command, optionally using `sudo`, and handles interactive vs non-interactive command behavior.
/// </summary>
/// <param name="path">The full path to the system command.</param>
/// <param name="args">Arguments to pass to the command.</param>
/// <param name="useSudo">Whether to use `sudo` to run the command.</param>
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,
Arguments = useSudo ? $"{path} {string.Join(' ', args)}" : string.Join(' ', args),
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))
{
Expand All @@ -252,27 +216,14 @@ private static bool RunSystemCommand(string path, string[] args, bool useSudo)
}

process.WaitForExit();

return process.ExitCode == 0;
}

/// <summary>
/// Checks if the current user is root.
/// </summary>
/// <returns>True if the current user is root, otherwise false.</returns>
private static bool IsRootUser()
{
return Environment.UserName == "root" || Environment.GetEnvironmentVariable("USER") == "root";
}

/// <summary>
/// Escapes markup characters in a string.
/// </summary>
/// <param name="input">The input string to escape.</param>
/// <returns>The escaped string.</returns>
private static string EscapeMarkup(string input)
{
return input.Replace("[", "[[").Replace("]", "]]");
}
}

20 changes: 20 additions & 0 deletions Shell/Commands/CommandRegistry.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

using Spectre.Console;
using NShell.Commands;

namespace NShell.Shell.Commands;
Expand All @@ -8,6 +9,9 @@ namespace NShell.Shell.Commands;
/// </summary>
public static class CommandRegistry
{

private static readonly Dictionary<string, ICustomCommand> _commands = new();

/// <summary>
/// Retrieves all registered custom commands implemented in the shell.
/// </summary>
Expand All @@ -20,4 +24,20 @@ public static IEnumerable<ICustomCommand> GetAll()
new SetThemeCommand(),
};
}

/// <summary>
/// Register a new plugin command into the shell.
/// This method adds the command to the command list and prints a confirmation.
/// </summary>
/// <param name="command">The plugin command implementing ICustomCommand to be registered.</param>
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}[/]");
}
}


}
2 changes: 1 addition & 1 deletion Shell/Commands/ICustomCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

namespace NShell.Shell;
namespace NShell.Shell.Commands;

/// <summary>
/// The <c>ICustomCommand</c> interface defines a contract for implementing
Expand Down
Loading