await Cli . Wrap ( "docker" )
. WithArguments ( "run --detach -e POSTGRES_PASSWORD=password postgres" )
. ExecuteAsync ( ) ;
Show contents of command result
Show how to get process ID
var result = await Cli . Wrap ( "docker" )
. WithArguments ( "run --detach -e POSTGRES_PASSWORD=password postgres" )
. ExecuteBufferedAsync ( ) ;
Show contents of buffered command result
await Cli . Wrap ( "docker" )
. WithArguments ( "run --detach -e POSTGRES_PASSWORD=password postgresss" )
. ExecuteAsync ( ) ;
Show how ExecuteBufferedAsync()
affects the error message
Passing arguments in different ways
var password = "hello world" ;
var result = await Cli . Wrap ( "docker" )
. WithArguments ( $ "run --detach -e POSTGRES_PASSWORD={ password } postgres")
. ExecuteBufferedAsync ( ) ;
Show that the password is not escaped and the arguments end up malformed
Instead of password, it can be anything, e.g. file path
var password = "hello world" ;
var result = await Cli . Wrap ( "docker" )
. WithArguments ( new [ ]
{
"run" ,
"--detach" ,
"-e" , $ "POSTGRES_PASSWORD={ password } ",
"postgres"
} )
. ExecuteBufferedAsync ( ) ;
Show that arguments are escaped correctly
var password = "hello world" ;
var result = await Cli . Wrap ( "docker" )
. WithArguments ( args => args
. Add ( "run" )
. Add ( "--detach" )
. Add ( "-e" ) . Add ( $ "POSTGRES_PASSWORD={ password } ")
. Add ( "postgres" )
)
. ExecuteBufferedAsync ( ) ;
Show usage with IFormattable
Custom arguments builder extensions
public static class CliExtensions
{
public static ArgumentsBuilder AddOption (
this ArgumentsBuilder args ,
string name ,
string ? value )
{
if ( string . IsNullOrWhiteSpace ( value ) )
return args ;
return args . Add ( name ) . Add ( value ) ;
}
}
var network = "mynet" ;
var result = await Cli . Wrap ( "docker" )
. WithArguments ( args => args
. Add ( "run" )
. Add ( "--detach" )
. Add ( "-e" ) . Add ( "POSTGRES_PASSWORD=hello world" )
. AddOption ( "--network" , network )
. Add ( "postgres" )
)
. ExecuteBufferedAsync ( ) ;
Show how changing network
to empty string does not pass the option
Event stream execution models
var cmd = Cli . Wrap ( "docker" )
. WithArguments ( args => args
. Add ( "run" )
. Add ( "-e" ) . Add ( "POSTGRES_PASSWORD=hello world" )
. Add ( "postgres" )
) ;
await foreach ( var cmdEvent in cmd . ListenAsync ( ) )
{
switch ( cmdEvent )
{
case StartedCommandEvent started :
{
Console . WriteLine ( "Process started. PID: " + started . ProcessId ) ;
break ;
}
case StandardOutputCommandEvent stdOut :
{
Console . ForegroundColor = ConsoleColor . White ;
Console . WriteLine ( "OUT> " + stdOut . Text ) ;
Console . ResetColor ( ) ;
break ;
}
case StandardErrorCommandEvent stdErr :
{
Console . ForegroundColor = ConsoleColor . DarkRed ;
Console . WriteLine ( "ERR> " + stdErr . Text ) ;
Console . ResetColor ( ) ;
break ;
}
case ExitedCommandEvent exited :
{
Console . WriteLine ( "Process exited. Code: " + exited . ExitCode ) ;
break ;
}
}
}
Show how the events are handled as the process is running
Explain backpressure and single-threaded event flow
Show observable stream execution model
Mention that you can easily create your own execution models
var stdOutBuffer = new StringBuilder ( ) ;
var stdErrBuffer = new StringBuilder ( ) ;
await Cli . Wrap ( "docker" )
. WithArguments ( args => args
. Add ( "run" )
. Add ( "--detach" )
. Add ( "-e" ) . Add ( "POSTGRES_PASSWORD=hello world" )
. Add ( "postgres" )
)
. WithStandardOutputPipe ( PipeTarget . ToStringBuilder ( stdOutBuffer ) )
. WithStandardErrorPipe ( PipeTarget . ToStringBuilder ( stdErrBuffer ) )
. ExecuteAsync ( ) ;
Console . WriteLine ( stdOutBuffer ) ;
Console . WriteLine ( stdErrBuffer ) ;
Explain that this is functionally equivalent to ExecuteBufferedAsync()
(exception non-zero exit code exception message)
Show PipeTarget
contract
await Cli . Wrap ( "docker" )
. WithArguments ( args => args
. Add ( "run" )
. Add ( "--detach" )
. Add ( "-e" ) . Add ( "POSTGRES_PASSWORD=hello world" )
. Add ( "postgres" )
)
. WithStandardOutputPipe ( PipeTarget . ToDelegate ( Console . WriteLine ) )
. WithStandardErrorPipe ( PipeTarget . ToDelegate ( Console . WriteLine ) )
. ExecuteAsync ( ) ;
Explain that ListenAsync()
and ObserveAsync()
are using a simlar setup under the hood
Show that this supports async delegates too (Func<string, Task>
)
await Cli . Wrap ( "docker" )
. WithArguments ( args => args
. Add ( "run" )
. Add ( "--detach" )
. Add ( "-e" ) . Add ( "POSTGRES_PASSWORD=hello world" )
. Add ( "postgres" )
)
. WithStandardOutputPipe ( PipeTarget . ToFile ( "stdout.txt" ) )
. WithStandardErrorPipe ( PipeTarget . ToFile ( "stderr.txt" ) )
. ExecuteAsync ( ) ;
var result = await Cli . Wrap ( "docker" )
. WithArguments ( args => args
. Add ( "run" )
. Add ( "--detach" )
. Add ( "-e" ) . Add ( "POSTGRES_PASSWORD=hello world" )
. Add ( "postgres" )
)
. WithStandardOutputPipe ( PipeTarget . ToDelegate ( Console . WriteLine ) )
. WithStandardErrorPipe ( PipeTarget . ToDelegate ( Console . WriteLine ) )
. ExecuteBufferedAsync ( ) ;
Console . WriteLine ( result . StandardOutput ) ;
Explain how execution models work without overriding pipes
var cmd = Cli . Wrap ( "docker" )
. WithArguments ( args => args
. Add ( "run" )
. Add ( "-e" ) . Add ( "POSTGRES_PASSWORD=hello world" )
. Add ( "postgres" )
) | ( Console . WriteLine , Console . Error . WriteLine ) ;
await cmd . ExecuteAsync ( ) ;
Explain pipe operators
Explain how pipe operators work with tuples
using var cts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 6 ) ) ;
try
{
await Cli . Wrap ( "ping" )
. WithArguments ( "-t google.com" )
. WithStandardOutputPipe ( PipeTarget . ToDelegate ( Console . WriteLine ) )
. ExecuteAsync ( cts . Token ) ;
}
catch ( OperationCanceledException )
{
Console . WriteLine ( "Cancelled!" ) ;
}
Explain how CliWrap kills the process on cancellation
var cmd = PipeSource . FromFile ( "video.mp4" ) | Cli . Wrap ( "ffmpeg" )
. WithArguments ( args => args
. Add ( "-i" ) . Add ( "-" )
. Add ( "video_out.webm" )
) | ( Console . WriteLine , Console . Error . WriteLine ) ;
await cmd . ExecuteAsync ( ) ;
Mention other sources
Remind that the same can be done using WithStandardInputPipe(...)
instead
var youtube = new YoutubeClient ( ) ;
var streamManifest = await youtube . Videos . Streams . GetManifestAsync ( "https://www.youtube.com/watch?v=-Sf9NPQeZOQ" ) ;
var streamInfo = streamManifest . GetVideoStreams ( ) . GetWithHighestVideoQuality ( ) ;
var stream = await youtube . Videos . Streams . GetAsync ( streamInfo ) ;
var cmd = stream | Cli . Wrap ( "ffmpeg" )
. WithArguments ( args => args
. Add ( "-i" ) . Add ( "-" )
. Add ( "-preset" ) . Add ( "ultrafast" )
. Add ( "youtube_video.mp4" )
) | ( Console . WriteLine , Console . Error . WriteLine ) ;
await cmd . ExecuteAsync ( ) ;
Process-to-process piping
var cmd =
// Take first 5 seconds of the video and convert to webm
Cli . Wrap ( "ffmpeg" )
. WithArguments ( args => args
. Add ( "-i" ) . Add ( "video.mp4" )
. Add ( "-t" ) . Add ( 5 )
. Add ( "-f" ) . Add ( "webm" )
. Add ( "-" ) ) |
// Reverse the stream and write to file
Cli . Wrap ( "ffmpeg" )
. WithArguments ( args => args
. Add ( "-i" ) . Add ( "-" )
. Add ( "-vf" ) . Add ( "reverse" )
. Add ( "video_altered.webm" ) ) |
( Console . WriteLine , Console . Error . WriteLine ) ;
await cmd . ExecuteAsync ( ) ;