.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.
Just add it in your project with this command:
dotnet add package dein.ToolBox
dotnet add package Newtonsoft.Json --version 11.0.2
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()
}
}
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()
}
}
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
Bash file for macOS:
#!/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
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);
}
}
}
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;
}
}
}
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>
Usage (magic comes true)
Term function always returns a Response class with code, stdout
and stderr
.
namespace ToolBox.Bridge
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);
}
Internal
You can run a command and see the result on the same terminal.
Term("java -version 2>&1", Output.Internal);
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”));
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 🖖
Top comments (2)
why the GetFileName in Shell class don't have a return statement?
Fixed! Thanks!