Click here to Skip to main content
15,891,864 members
Articles / Programming Languages / C#

less command for Windows

Rate me:
Please Sign up or sign in to vote.
2.47/5 (8 votes)
5 May 2011CPOL2 min read 31.1K   373   8   13
Implementation of less command in C#.

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:

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

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

C#
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:

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

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

C#
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:

C#
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:

C#
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!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



Comments and Discussions

 
GeneralMy vote of 1 Pin
ElektroStudios7-May-15 7:20
ElektroStudios7-May-15 7:20 
GeneralMy vote of 5 Pin
Mario Majčica5-May-11 21:46
professionalMario Majčica5-May-11 21:46 
GeneralMore thoughts Pin
PIEBALDconsult3-May-11 15:23
mvePIEBALDconsult3-May-11 15:23 
GeneralMy vote of 3 Pin
John Brett3-May-11 2:41
John Brett3-May-11 2:41 
GeneralWhat about MORE Pin
DaveAuld2-May-11 20:04
professionalDaveAuld2-May-11 20:04 
GeneralRe: What about MORE Pin
User 43245233-May-11 2:10
User 43245233-May-11 2:10 
GeneralRe: What about MORE Pin
PIEBALDconsult3-May-11 3:12
mvePIEBALDconsult3-May-11 3:12 
GeneralRe: What about MORE Pin
User 43245233-May-11 3:39
User 43245233-May-11 3:39 
GeneralRe: What about MORE Pin
PIEBALDconsult3-May-11 15:11
mvePIEBALDconsult3-May-11 15:11 
GeneralThoughts Pin
PIEBALDconsult2-May-11 18:32
mvePIEBALDconsult2-May-11 18:32 
GeneralJust FYI Pin
Oleg Shilo2-May-11 14:01
Oleg Shilo2-May-11 14:01 
GeneralRe: Just FYI Pin
DaveAuld2-May-11 20:08
professionalDaveAuld2-May-11 20:08 
JokeRe: Just FYI Pin
cjb1103-May-11 2:02
cjb1103-May-11 2:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.