Click here to Skip to main content
Click here to Skip to main content

less command for Windows

, 5 May 2011
Rate this:
Please Sign up or sign in to vote.
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:

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!

License

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

Share

About the Author

as-cii

Italy Italy
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 PinmemberMario Majcica5-May-11 21:46 
GeneralMore thoughts PinmemberPIEBALDconsult3-May-11 15:23 
GeneralMy vote of 3 PinmemberJohn Brett3-May-11 2:41 
GeneralWhat about MORE PinmemberDaveAuld2-May-11 20:04 
GeneralRe: What about MORE PinmemberHazel243-May-11 2:10 
GeneralRe: What about MORE PinmemberPIEBALDconsult3-May-11 3:12 
GeneralRe: What about MORE PinmemberHazel243-May-11 3:39 
GeneralRe: What about MORE PinmemberPIEBALDconsult3-May-11 15:11 
GeneralThoughts PinmemberPIEBALDconsult2-May-11 18:32 
GeneralJust FYI PinmemberOleg Shilo2-May-11 14:01 
GeneralRe: Just FYI PinmemberDaveAuld2-May-11 20:08 
JokeRe: Just FYI Pinmembercjb1103-May-11 2:02 

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

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

| Advertise | Privacy | Mobile
Web04 | 2.8.140827.1 | Last Updated 5 May 2011
Article Copyright 2011 by as-cii
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid