less command for Windows
Introduction
It is common to develop console applications that have long outputs that, because of the cmd buffer, are not entirely visible to the user. Recently I had the opportunity to install a Linux distro into an old computer and I discovered the "less" command. Basically this little program allows the user to scroll an application output and because Windows does not implement anything like it, I decided to develop something by myself.
Here is an example of less usage:
myprogram.exe -variousarguments | less
Background
In the usage, you have seen that less is executed with |
("pipe"); this tells cmd that it has to pass the output to another program.
How do we read output passed with another program with a pipe in .NET? We simply have to read the entire Console.In
like this:
string output = Console.In.ReadToEnd();
Now we just have to tell our program that it has to scroll up/down whenever the Down/Up arrow is pressed. So, let's try Console.ReadKey()
and see what happens.
var key = Console.ReadKey().Key;
This will cause an exception in our program, like the one below:
Cannot read keys when either application does not have a console
or when console input has been redirected from a file. Try Console.Read.
Yes, because actually our less program is running "inside another application" and it does not have its own console from which to read user input. To solve this problem, we have to look at some P/Invoke methods that we will insert in the ConsoleHelper
class.
internal static class ConsoleHelper
{
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool FreeConsole();
[DllImport("kernel32", SetLastError = true)]
static extern bool AttachConsole(int dwProcessId);
[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll", SetLastError = true)]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
/// <summary>
/// Checks if current program is running inside cmd.
/// </summary>
/// <param name="processId"></param>
/// <returns></returns>
internal static bool CheckCmd(out int processId)
{
IntPtr ptr = GetForegroundWindow();
GetWindowThreadProcessId(ptr, out processId);
return Process.GetProcessById(processId).ProcessName == "cmd";
}
/// <summary>
/// Kill current process and attach this console in order to use Console.ReadKey().
/// </summary>
/// <param name="processId"></param>
internal static void CreateNew(int processId)
{
FreeConsole();
AttachConsole(processId);
}
}
In our main program, we will check if we are running inside cmd, and we will first get the output and then will kill the main process, attaching the new one inside the existing console:
int procId;
if (!ConsoleHelper.CheckCmd(out procId))
{
return;
}
string output = ReplaceNewLine(Environment.NewLine + Console.In.ReadToEnd());
ConsoleHelper.CreateNew(procId);
Console.Clear();
Now we can really implement the logic of the program (bear in mind the ReplaceNewLine
function).
less implementation
To see how many chars cmd can actually display, we make use of Console.WindowHeight
and Console.WindowWidth
.
int maxVisibleChars = Console.WindowWidth * Console.WindowHeight;
We have also to convert Environment.NewLine
to whitespace characters in order to see the effective length of the output string.
static string ReplaceNewLine(string input)
{
var lines = InternalReplace(input.Split(
new string[] { Environment.NewLine }, StringSplitOptions.None));
return string.Join("", lines);
}
static IEnumerable<string> InternalReplace(IEnumerable<string> lines)
{
foreach (var line in lines)
{
string current = line;
while (current.Length > Console.BufferWidth)
{
current = current.Substring(Console.BufferWidth,
current.Length - Console.BufferWidth);
}
string whitespaces = string.Join("", Enumerable.Repeat(" ",
Console.BufferWidth - current.Length));
yield return line + whitespaces;
}
}
Now, we just handle the pressed keys like this:
int line = 0;
while (true)
{
int index = line * Console.BufferWidth;
if (index + maxVisibleChars > output.Length)
{
line--;
index = line * Console.BufferWidth;
}
string available = output.Substring(index, maxVisibleChars);
Console.Clear();
Console.Write(available);
var key = Console.ReadKey().Key;
if (key == ConsoleKey.Q)
{
return;
}
else if (key == ConsoleKey.DownArrow)
{
line++;
}
else if (key == ConsoleKey.UpArrow)
{
line = line != 0 ? line - 1 : 0;
}
}
That's all! Now he have a fully functional less program for Windows!
All together
Let's see what we have written so far:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace less
{
static class Program
{
static void Main(string[] args)
{
int procId;
if (!ConsoleHelper.CheckCmd(out procId))
{
return;
}
string output =
ReplaceNewLine(Environment.NewLine + Console.In.ReadToEnd());
ConsoleHelper.CreateNew(procId);
Console.Clear();
int maxVisibleChars = Console.BufferWidth * Console.WindowHeight;
if (output.Length <= maxVisibleChars)
{
Console.Write(output);
return;
}
int line = 0;
while (true)
{
int index = line * Console.BufferWidth;
if (index + maxVisibleChars > output.Length)
{
line--;
index = line * Console.BufferWidth;
}
string available = output.Substring(index, maxVisibleChars);
Console.Clear();
Console.Write(available);
var key = Console.ReadKey().Key;
if (key == ConsoleKey.Q)
{
return;
}
else if (key == ConsoleKey.DownArrow)
{
line++;
}
else if (key == ConsoleKey.UpArrow)
{
line = line != 0 ? line - 1 : 0;
}
}
}
static string ReplaceNewLine(string input)
{
var lines = InternalReplace(input.Split(
new string[] { Environment.NewLine }, StringSplitOptions.None));
return string.Join("", lines);
}
static IEnumerable<string> InternalReplace(IEnumerable<string> lines)
{
foreach (var line in lines)
{
string current = line;
while (current.Length > Console.BufferWidth)
{
current = current.Substring(Console.BufferWidth,
current.Length - Console.BufferWidth);
}
string whitespaces = new string(' ', Console.BufferWidth - current.Length);
yield return line + whitespaces;
}
}
}
}
Conclusion
I hope this article can be useful (also to learn how to attach a console to an existing cmd or to read input from another program passed with a pipe).
Enjoy!
History
- 02/05/2011 - First version!
- 05/05/2011 - Updated source code!
发表评论
And every few For and Yeast other had is be cause sperm the or treat. However, is apple of are a levels.