DEV Community

Cover image for Run a command in an external terminal with .Net Core
Camilo Martinez
Camilo Martinez

Posted on • Edited on

Run a command in an external terminal with .Net Core

.Net Core is a powerful tool, but have some limitations, one of them is that ProcessStartInfo over macOS workingDirectory not work and over Windows required credentials in order to create a new terminal.

But you can save the day, using the good parts of ProcessStartInfo as a bridge and call external scripts that can do the job (even without credentials).

Easy Way (Recommended)

I have created ToolBox library, that includes this functionality.

dein ToolBox - C# NETCore Library with utilities like: command line, files, log, platform, system, transform and validation [ for Win+Mac ]

Just add it in your project with this command:

dotnet add package dein.ToolBox
dotnet add package Newtonsoft.Json --version 11.0.2
Enter fullscreen mode Exit fullscreen mode

Implements Bridge System and Shell Configurator:

using ToolBox.Bridge;

class Program
{
    public static IBridgeSystem _bridgeSystem { get; set; }
    public static ShellConfigurator _shell { get; set; }

    static void Main(string[] args)
    {
        switch (OS.GetCurrent())
        {
            case "win":
                _bridgeSystem = BridgeSystem.Bat;
                break;
            case "mac":
            case "gnu":
                _bridgeSystem = BridgeSystem.Bash;
                break;
        }
        _shell = new ShellConfigurator(_bridgeSystem);
        //Foo()
        //Bar()
    }
}
Enter fullscreen mode Exit fullscreen mode
toolbox-bridge-factory.cs

Uses a NotificationSystem by Default, but you can customize it implementing the INotificationSystem interface and send it as a parameter on ShellConfigurator.

using static ToolBox.Notification;

class Program
{
    public static INotificationSystem _notificationSystem { get; set; }
    public static IBridgeSystem _bridgeSystem { get; set; }
    public static ShellConfigurator _shell { get; set; }

    static void Main(string[] args)
    {
        _notificationSystem = new MyConsoleNotificationSystem();
        switch (OS.GetCurrent())
        {
            case "win":
                _bridgeSystem = BridgeSystem.Bat;
                break;
            case "mac":
            case "gnu":
                _bridgeSystem = BridgeSystem.Bash;
                break;
        }
        _shell = new ShellConfigurator(_bridgeSystem, _notificationSystem);
        //Foo()
        //Bar()
    }
}

Enter fullscreen mode Exit fullscreen mode
toolbox-bridge-notification.cs

To understand how this library works, take a look inside the Sample folder. Better easy to use a guide than words.

Hard Way

In the root project folder, create two scripts (Batch and Bash) that can receive a command and a directory (optional). Internally they will do the job to create the external terminal in a new window:

Bat file for Windows:

echo off
set cmd=%1
set dir=%2
if defined dir (
 cd /d %dir%\
)
start cmd.exe /K %cmd%
cls
exit 0
Enter fullscreen mode Exit fullscreen mode
cmd.bat

Bash file for macOS:

#!/bin/bash
cmd=$1";
dir=”$2";
if [ -n$dir]; then
osascript <<EOF
 tell application “Terminal” to do script “cd $dir; $cmdEOF
else
osascript <<EOF
 tell application “Terminal” to do script “$cmdEOF
fi
clear
exit 0
Enter fullscreen mode Exit fullscreen mode
cmd.sh

Remember assign execution permissions to this file: chmod +x cmd.mac.sh

Add a better .Net Core OS detection with this class:

using System;
using System.Runtime.InteropServices;

namespace ToolBox.Platform
{
    public static class OS
    {
        public static bool IsWin() =>
            RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

        public static bool IsMac() =>
            RuntimeInformation.IsOSPlatform(OSPlatform.OSX);

        public static bool IsGnu() =>
            RuntimeInformation.IsOSPlatform(OSPlatform.Linux);

        public static string GetCurrent()
        {
            return
            (IsWin() ? "win" : null) ??
            (IsMac() ? "mac" : null) ??
            (IsGnu() ? "gnu" : null);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode
Platform.cs

Use ProcessStartInfo but understand his limitations. This Class can run command Hidden (run command in the background), Internal (run commands and show stdout and stderr in the same terminal) and External (run commands in external terminal).

using System.Runtime.InteropServices;
using System;
using System.Diagnostics;
using System.Text;

using System.Reflection;
using System.IO;

namespace ToolBox.Bridge
{
    public class Response {
        public int code { get; set; }
        public string stdout { get; set; }
        public string stderr { get; set; }
    }

    public enum Output {
        Hidden,
        Internal,
        External
    }

    public static class Shell
    {
        private static string GetFileName()
        {
            string fileName = "";
            try
            {
                switch (OS.WhatIs())
                {
                    case "win":
                        fileName = "cmd.exe";
                        break;
                    case "mac":
                    case "gnu":
                        fileName = "/bin/bash";
                        break;
                }
                return fileName;
            }
            catch (Exception Ex)
            {
                Console.WriteLine(Ex.Message);
            }
        }


        private static string CommandConstructor (string cmd, Output? output = Output.Hidden, string dir = "")
        {
            try
            {
                if (!String.IsNullOrEmpty(dir))
                {
                    dir.Exists("");
                }
                switch (OS.WhatIs())
                {
                    case "win":
                        if (!String.IsNullOrEmpty(dir))
                        {
                            dir = $" \"{dir}\"";
                        }
                        if (output == Output.External)
                        {
                            cmd = $"{Directory.GetCurrentDirectory()}/cmd.win.bat \"{cmd}\"{dir}";
                        }
                        cmd = $"/c \"{cmd}\"";
                        break;
                    case "mac":
                    case "gnu":
                        if (!String.IsNullOrEmpty(dir))
                        {
                            dir = $" '{dir}'";
                        }
                        if (output == Output.External)
                        {
                            cmd = $"sh {Directory.GetCurrentDirectory()}/cmd.mac.sh '{cmd}'{dir}";
                        }
                        cmd = $"-c \"{cmd}\"";
                        break;
                }
                return cmd;
            }
            catch (Exception Ex)
            {
                Console.WriteLine(Ex.Message);
            }
        }

        public static Response Term (string cmd, Output? output = Output.Hidden, string dir = ""){
            var result = new Response();
            var stderr = new StringBuilder();
            var stdout = new StringBuilder();
            try
            {
                ProcessStartInfo startInfo = new ProcessStartInfo();
                startInfo.FileName = GetFileName();
                startInfo.Arguments = CommandConstructor(cmd, output, dir);
                startInfo.RedirectStandardOutput = !(output == Output.External);
                startInfo.RedirectStandardError = !(output == Output.External);
                startInfo.UseShellExecute = false;
                startInfo.CreateNoWindow = !(output == Output.External);
                if (!String.IsNullOrEmpty(dir) && output != Output.External){
                    startInfo.WorkingDirectory = dir;
                }

                using (Process process = Process.Start(startInfo))
                {
                    switch (output)
                    {
                        case Output.Internal:
                            $"".fmNewLine();

                            while (!process.StandardOutput.EndOfStream) {
                                string line = process.StandardOutput.ReadLine();
                                stdout.AppendLine(line);
                                Console.WriteLine(line);
                            }

                            while (!process.StandardError.EndOfStream) {
                                string line = process.StandardError.ReadLine();
                                stderr.AppendLine(line);
                                Console.WriteLine(line);
                            }
                            break;
                        case Output.Hidden:
                            stdout.AppendLine(process.StandardOutput.ReadToEnd());
                            stderr.AppendLine(process.StandardError.ReadToEnd());
                            break;
                    }
                    process.WaitForExit();
                    result.stdout = stdout.ToString();
                    result.stderr = stderr.ToString();
                    result.code = process.ExitCode;
                }
            }
            catch (Exception Ex)
            {
                Console.WriteLine(Ex.Message);
            }
            return result;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
Platform.cs

Remember add this tags on .csproj file, to copy script file when Publish your project:

<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.0'">
    <None Update="cmd.sh" CopyToOutputDirectory="PreserveNewest" />
    <None Update="cmd.bat" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode
shell.csproj

Usage (magic comes true)

Term function always returns a Response class with code, stdout and stderr.

namespace ToolBox.Bridge
Enter fullscreen mode Exit fullscreen mode
toolbox-namespace.cs

Hidden

You can run a command silently. Don’t show anything on console, if you want to print the result, use values on Response return.

Response result = Term("dotnet --version", Output.Hidden);
Result(result.stdout, "Not Installed");
Console.WriteLine(result.code.ToString());
if (result.code == 0)
{
    Console.WriteLine($"Command Works :D");
}
else
{
    Console.WriteLine(result.stderr);
}
Enter fullscreen mode Exit fullscreen mode
toolbox-hidden-example.cs

Internal

You can run a command and see the result on the same terminal.

Term("java -version 2>&1", Output.Internal);
Enter fullscreen mode Exit fullscreen mode
toolbox-internal-example.cs

External

gulp browserSync needs to stay open in order to have the “web Server” running. This is the main reason to run it in an external window.

Gulp project path is different between Windows and macOS, then I think the better approach is to create an Environment Variable called GULP_PROJECT.

Term($”gulp browserSync, Output.External, Environment.GetEnvironmentVariable(GULP_PROJECT));
Enter fullscreen mode Exit fullscreen mode
toolbox-external-example.cs

Another use is if you need to run a simultaneous process at the same time, open each one in a new terminal and will don’t need to wait until finish one to run the next one.

Conclusions

Why is better the Easy Way?

Because you don’t need to deal with files, script permission, and project build configuration; NuGet package does it all for you. Plus you can create another shell implementing IBridge (like Zsh, PowerShell, etc) interface and use it on ShellConfigurator as a parameter and… ToolBox library comes with utilities like: command line, files, log, platform, system, transform and validation.

Hard way is good as a study case or if you want a better understanding of how it works, but you will deal with manually with file permission and believe you don’t want and end-user running additional commands. Finally… was not develop using polymorphism, then you will have to deal manually with new additions.

Bonus Track

Take a look inside this library, it's not only to launch external commands:


That’s All Folks!
Happy Coding 🖖

beer

Top comments (2)

Collapse
 
jason_yan_4b6fab2427c4840 profile image
Jason Yan

why the GetFileName in Shell class don't have a return statement?

Collapse
 
equiman profile image
Camilo Martinez

Fixed! Thanks!