Awesome
Chell
Write scripts with the power of C# and .NET.
Chell is a library and execution tool for providing a shell script-like (bash, cmd, ...) experience to .NET applications.
var branch = await Run($"git branch --show-current");
await Run($"git archive {branch} -o {branch}.zip");
.NET applications are great for complex tasks, but executing processes can be boring. Chell brings the experience closer to shell scripting. This library is heavily influenced by google/zx.
When should I use Chell?
- Write better shell scripts: Write a complex script and use the power of .NET and C#
- Write multi-platform shell scripts: As an alternative to scripts that work on multiple platforms
- Run a process quickly in your app: As .NET library for easy handling of process launch and output
- All developers in the project are .NET developers: 🙃
Of course, if the shell script is already working fine and you don't have any problems, then there is no need to use Chell.
Chell at a glance
Using Chell makes the code feel more like a script by taking advantage of C# 9's top-level statements and C# 6's using static
.
// Chell.Exports exposes a variety of functions and properties
using Chell;
using static Chell.Exports;
// Move the current directory with Cd method
Cd("/tmp");
// Dispose the return value of Cd method to return to the previous directory
using (Cd("/usr/local/bin"))
{
// The current directory is "/usr/local/bin".
}
// The current directory is "/" again.
// You can run the process by passing a string to Run method
await Run($"ls -lFa");
// An interpolated string passed to Run method will be escaped and expanded if it is an array
var newDirs = new [] { "foo", "bar", "my app", "your;app" };
await Run($"mkdir {newDirs}"); // $ mkdir foo bar "my app" "your;app"
// Run method returns the result object of the command (ProcessOutput class)
var result = await Run($"ls -lFa");
// You can read stdout & stderr line by line
foreach (var line in result)
{
Echo(line);
}
// Allows to get stdout & stderr with implicit conversion to `string`
string output = result;
// You can also get stdout as bytes (ReadOnlyMemory<byte>)
var binary = result.OutputBinary;
// Provides convenient extension methods for parsing JSON.
var images = await Run($"docker image ls --format {"{{json .}}"}").SuppressConsoleOutputs();
foreach (var image in images.AsJsonLines(new { Repository = "", ID = "", Tag = ""}))
{
Echo(image);
}
// $ docker image ls --format "{{json .}}"
// { Repository = mcr.microsoft.com/dotnet/sdk, ID = b160c8f3dbd6, Tag = 5.0 }
// { Repository = <none>, ID = 3ee645b4a3bd, Tag = <none> }
// Standard input/output of process tasks can be connected by pipes
await (Run($"ls -lFa") | Run($"grep dotnet"));
// The difference with `await (Run($"ls -lFa | grep dotnet"));` is that the shell can pipe or not.
// You can also specify a Stream as input or output
// Write ffmpeg output to a Stream.
await (Run($"ffmpeg ...") | destinationStream);
// Write a Stream to ffmpeg process.
await (srcStream | Run($"ffmpeg ..."));
Just want to make it easy for your app to handle processes? If you don't use Chell.Exports
, you won't get any unnecessary methods or properties, and you'll get the same functions by new Run(...)
.
using Chell;
var result = await new Run($"ls -lF");
Want to run it like a scripting language? Install Chell.Run, and you can run it like a script.
% dotnet tool install -g Chell.Run
% chell -e "Echo(DateTime.Now)"
9/1/2021 0:00:00 PM
% cat <<__EOF__ > MyScript.cs
var dirs = new [] { "foo bar", "baz" };
await Run($"mkdir {dirs}");
await Run($"ls -l");
__EOF__
% chell MyScript.cs
$ mkdir "foo bar" "baz"
$ ls -l
total 8
drwxr-xr-x 2 mayuki mayuki 4096 Sep 1 00:00 baz/
drwxr-xr-x 2 mayuki mayuki 4096 Sep 1 00:00 'foo bar'/
Features
- Automatic shell character escaping and array expansion
- Stream and Process Pipes
- Provide utilities and shortcuts useful for scripting.
- Simple shell script-like execution tools
- Multi-platform (Windows, Linux, macOS)
- LINQPad friendly
Install
dotnet package add Chell
Requirements
.NET Standard 2.1, .NET 5 or higher
Chell.Exports
Chell.Exports class exposes a variety of utilities and shortcuts to make writing feel like shell scripting. It is recommended to include this class in your scripts with static using
.
Methods (Functions)
Run
Starts a process using the specified command-line and returns a ProcessTask
.
await Run($"ls -lF");
// The followings are equivalent to calling Run method
await (Run)$"ls -lF";
await new Run($"ls -lF");
The process will be launched asynchronously and can wait for completion by await
. And you can await
to get a ProcessOutput
object with its output.
If the exit code of the process returns non-zero, it will throw an exception. To suppress this exception, see NoThrow
.
An interpolated string passed to Run method will be escaped and expanded if it is an array.
var newDirs = new [] { "foo", "bar", "my app", "your;app" };
await Run($"mkdir {newDirs}"); // equivalent to `mkdir foo bar "my app" "your;app"`
You can also pass an execution options (ProcessTaskOptions
) to Run method.
await Run($"ping -t localhost", new ProcessTaskOptions(
workingDirectory: @"C:\Windows",
timeout: TimeSpan.FromSeconds(1)
));
Cd(string)
Cd("/usr/local/bin"); // equivalent to `Environment.CurrentDirectory = "/usr/local/bin";`
Dispose the return value of Cd
method to return to the previous directory.
Cd("/"); // The current directory is "/".
using (Cd("/usr/local/bin"))
{
// The current directory is "/usr/local/bin".
}
// The current directory is "/" again.
Mkdirp(string path)
Same as mkdir -p
. Creates a new directory and any necessary sub-directories in the specified path.
Dump<T>(T value)
Formats the object and write it to the console.
Dump(new { Foo = 123, Bar = "Baz" }); // => "{ Foo = 123, Bar = "Baz" }"
Which(string name)
, TryWhich(string name, out string path)
Returns a path of the specified command.
var dotnetPath = Which("dotnet");
await Run($"{dotnetPath} run");
Echo(object message = default)
Echo
method is equivalent to Console.WriteLine.
Echo("Hello World!"); // equivalent to Console.WriteLine("Hello World!");
Sleep(int duration)
, Sleep(TimeSpan duration)
Returns a Task that waits for the specified duration.
await Sleep(10); // Sleep for 10 seconds.
Exit(int exitCode)
Terminates the application with an exit code.
Exit(1);
Properties
Env.Vars
Exposes the environment variables as IDictionary<string, string>
.
Env.Vars["PATH"] = Env.Vars["PATH"] + ":/path/to/";
Env.IsWindows
Returns whether the running operating system is Windows or not. If it returns false
, the operating system is Linux or macOS.
if (Env.IsWindows) { /* Something to do for Windows */ }
Env.Shell
Specify explicitly which shell to use, or set to not use a shell.
Env.Shell.UseBash();
Env.Shell.NoUseShell();
Env.Shell.UseCmd();
Env.Verbosity
Sets or gets the output level when executing a command/process.
Verbosity.All
: Displays both the command line and the output of the commandVerbosity.CommandLine
: Displays the command lineVerbosity.Output
: Displays the output of the commandVerbosity.Silent
: No display
Env.ProcessTimeout
Sets the timeout for running the process. The default value is 0
(disabled).
Env.ProcessTimeout = TimeSpan.FromSecond(1);
// OperationCanceledException will be thrown after 1s.
await Run($"ping -t localhost");
Arguments
Gets the arguments passed to the current application.
// $ myapp foo bar baz => new [] { "foo", "bar", "baz" };
foreach (var arg in Arguments) { /* ... */ }
CurrentDirectory
, ExecutableDirectory
, ExecutableName
, ExecutablePath
Gets the current directory and the application directory or name or path.
// C:\> cd C:\Users\Alice
// C:\Users\Alice> Downloads\MyApp.exe
Echo(CurrentDirectory); // C:\Users\Alice
Echo(ExecutableDirectory); // C:\Users\Alice\Downloads
Echo(ExecutableName); // MyApp.exe
Echo(ExecutablePath); // C:\Users\Alice\Downloads\MyApp.exe
HomeDirectory
Gets the path of the current user's home directory.
// Windows: C:/Users/<UserName>
// Linux: /home/<UserName>
// macOS: /Users/<UserName>
Echo(HomeDirectory);
StdIn
, StdOut
, StdErr
Provides the wrapper with methods useful for reading and writing to the standard input/output/error streams.
// Reads data from standard input.
await StdIn.ReadToEndAsync();
// Writes data to standard output or error.
StdOut.WriteLine("FooBar");
StdErr.WriteLine("Oops!");
ProcessTask class
Represents the execution task of the process started by Run
.
Pipe
Connects the standard output of the process to another ProcessTask
or Stream
.
await (Run($"ls -lF") | Run($"grep .dll"));
// The followings are equivalent to using '|'.
var procTask1 = Run($"ls -lF");
var procTask2 = Run($"grep .dll");
procTask1.Pipe(procTask2);
A Stream
can also be passed to Pipe. If the ProcessTask has connected to the Stream
, it will not write to ProcessOutput
.
var memStream = new MemoryStream();
await Run($"ls -lF").Pipe(memStream);
ConnectStreamToStandardInput
Connects the Stream to the standard input of the process. The method can be called only once before the process starts.
await (myStream | Run($"grep .dll"));
// The followings are equivalent to using '|'.
var procTask = Run($"grep .dll");
procTask.ConnectStreamToStandardInput(myStream);
NoThrow
Suppresses exception throwing when the exit code is non-zero.
await Run($"AppReturnsExitCodeNonZero").NoThrow();
SuppressConsoleOutputs
Suppresses the writing of command execution results to the standard output.
// equivalent to "Env.Verbosity = Verbosity.Silent" or pipe to null.
await Run($"ls -lF").SuppressConsoleOutputs();
ExitCode
Returns a Task
to get the exit code of the process. This is equivalent to waiting for a ProcessTask
with NoThrow
.
var proc = Run($"ls -lF");
if (await proc.ExitCode != 0)
{
...
}
// equivalent to `(await Run($"ls -lF").NoThrow()).ExitCode`
ProcessOutput class
Provides the results of the process execution.
Combined
, CombinedBinary
Gets the combined standard output and error as a string or byte array.
Output
, OutputBinary
Gets the standard output as a string or byte array.
Error
, ErrorBinary
Gets the standard error as a string or byte array.
AsLines(bool trimEnd = false)
, GetEnumerator()
Gets the combined standard output and error as a per-line IEnumerable<string>
.
// equivalent to `foreach (var line in procOutput.AsLines())`
foreach (var line in procOutput) { ... }
ToString()
The method equivalent to Combined
property.
ExitCode
Gets the exit code of the process.
Utilities and shortcuts
Chell.Exports class also exposes a variety of useful utilities and shortcuts to libraries.
Prompt
Prompts the user for input and gets it.
var name = await Prompt("What's your name? ");
Chalk
: Kokuban: Terminal string styling
Provides a shortcut to mayuki/Kokuban. You can easily style the text on the terminal.
// "Error: " will be colored.
Echo((Chalk.Red + "Error: ") + "Something went wrong.");
Glob
Provides a shortcut to Microsoft.Extensions.FileSystemGlobbing
.
Glob(params string[] patterns)
Glob(string baseDir, string[] patterns)
// Glob patterns starting with '!' will be treated as excludes.
foreach (var path in Glob("**/*.cs", "!**/*.vb"))
{
...
}
JSON serialize/deserialize (System.Text.Json)
Provides shortcuts to System.Text.Json
.
ToJson<T>(T obj)
var obj = new { Name = "Alice", Age = 18 };
var json = ToJson(obj);
Echo(json); // {"Name":"Alice","Age":18}
FromJson<T>(string json)
FromJson<T>(string json, T shape)
var json = "{ \"foo\": 123 }";
var obj = FromJson(json, new { Foo = 0 });
Dump(obj); // { Foo = 123 }
AsJson
AsJsonLines
using Chell;
var output = await Run($"docker image ls --format {"{{json .}}"}");
foreach (var image in output.AsJsonLines(new { Repository = "", ID = "", Tag = ""}))
{
// ...
}
using Chell;
var output = await Run($"kubectl version --client -o json");
var obj = output.AsJson(new { clientVersion = new { major = "", minor = "", gitVersion = "" } });
Echo(obj); // { clientVersion = { major = 1, minor = 21, gitVersion = v1.21.2 } }
HTTP acccess (System.Net.Http)
Provides shortcuts to System.Net.Http.HttpClient
.
FetchAsync
FetchByteArrayAsync
FetchStreamAsync
FetchStringAsync
Chell as a Library
Chell can also be used as a utility library to run processes.
If you don't use Chell.Exports
, you won't get any unnecessary methods or properties, and you can use Run
and ChellEnvironment
, Exports
class.
using Chell;
var results = await new Run($"ls -lF");
// ChellEnvironment.Current is equivalent to `Env` on `Chell.Exports`.
Console.WriteLine(ChellEnvironment.Current.ExecutablePath);
Console.WriteLine(ChellEnvironment.Current.ExecutableName);
Console.WriteLine(ChellEnvironment.Current.Arguments);
Console.WriteLine(ChellEnvironment.Current.Vars["PATH"]);
Chell.Run
Chell.Run executes the input source code in an environment where Chell and some libraries are available.
It does not perform any NuGet package resolution, so we recommend creating a typical C# project if you need to handle such complexities.
$ dotnet tool install -g Chell.Run
$ chell -e "Echo(123);"
$ chell <<EOF
var result = await Run($"ls -lF");
Echo("Hello World!");
EOF
Chell.Run implicitly has some namespace imports and library references and can be used out-of-the-box.
Implicitly specified using
s:
- System
- System.Collections
- System.Collections.Generic
- System.Diagnostics
- System.IO
- System.Text
- System.Text.RegularExpressions
- System.Linq
- System.Threading
- System.Threading.Tasks
- Chell
- Chell.Extensions
- Chell.Exports (using static)
Additional referenced libraries:
- Mono.Options
- Sharprompt
- Cocona.Lite
Chell ❤ LINQPad
LINQPad is often used to do tasks in place of shell scripts. Chell can help you in this case as well.
Chell knows about LINQPad, so Dump
method is replaced by LINQPad's Dump
, and standard output and errors are displayed in the results without problems.
License
MIT License
Copyright © Mayuki Sawatari <mayuki@misuzilla.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.