Run a command in an external terminal with .Net Core
Tags: dotnetcore, productivity, howto, csharp
Published: 2018-09-28 12:24:37

---
title: Run a command in an external terminal with .Net Core
published: true
description: How can use ToolBox library to extend .Net Core functionalities.
tags: dotnetcore, productivity, howto, csharp
cover_image: https://thepracticaldev.s3.amazonaws.com/i/2db95akukzqqb3w0ij32.jpg
---

.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 ]](https://github.com/deinsoftware/toolbox)

Just add it in your project with this command:

```bash
dotnet add package dein.ToolBox
dotnet add package Newtonsoft.Json --version 11.0.2
```

Implements Bridge System and Shell Configurator:

```csharp
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()
    }
}
```
<figcaption>toolbox-bridge-factory.cs</figcaption>

Uses a NotificationSystem by [Default](https://github.com/deinsoftware/toolbox/blob/master/ToolBox/Notification/NotificationSystemDefault.cs), but you can customize it implementing the `INotificationSystem` interface and send it as a parameter on `ShellConfigurator`.

```csharp
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()
    }
}

```
<figcaption>toolbox-bridge-notification.cs</figcaption>

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:

```bash
echo off
set cmd=%1
set dir=%2
if defined dir (
 cd /d %dir%\
)
start cmd.exe /K %cmd%
cls
exit 0
```
<figcaption>cmd.bat</figcaption>

Bash file for macOS:

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

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

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

```csharp
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);
        }
    }
}

```
<figcaption>Platform.cs</figcaption>

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).

```csharp
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;
                }
            }
            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;
        }
    }
}
```
<figcaption>Platform.cs</figcaption>

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

```xml
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.0'">
    <None Update="cmd.sh" CopyToOutputDirectory="PreserveNewest" />
    <None Update="cmd.bat" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
```
<figcaption>shell.csproj</figcaption>

# Usage (magic comes true)

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

```csharp
namespace ToolBox.Bridge
```
<figcaption>toolbox-namespace.cs</figcaption>

## 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.

```csharp
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);
}
```
<figcaption>toolbox-hidden-example.cs</figcaption>

## Internal

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

```csharp
Term("java -version 2>&1", Output.Internal);
```
<figcaption>toolbox-internal-example.cs</figcaption>

## 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`.

```csharp
Term($”gulp browserSync”, Output.External, Environment.GetEnvironmentVariable(“GULP_PROJECT”));
```
<figcaption>toolbox-external-example.cs</figcaption>

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:

- [dein ToolBox - C# NETCore Library with utilities like: command line, files, log, platform, system, transform and validation [ for Win+Mac ]](https://www.nuget.org/packages/dein.ToolBox/)

---

**That’s All Folks!**
**Happy Coding** 🖖

[![beer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mkgsx4aw6qn9i9ax6as9.png)](https://github.com/sponsors/deinsoftware)