views:

553

answers:

4

What would be the minimal boilerplate code for a C# console application entry-point that would make it a well-behaved citizen?

When anyone goes out to create a Console Application project using Visual Studio (up to 2008 at the time of writing), you are presented with a boilerplate Program.cs that looks like this:

class Program
{
    static void Main(string[] args)
    {
    }
}

There are, however, a few things that everyone needs to do to make a console application a minimally good citizen. For example, if an exception occurs, then write out a clean message to standard error (Console.Error) and not standard output (Console.Out). Likewise, set the error code to a non-zero value in the event of some error so that calling processes can detect failures.

What would be the minimal boilerplate code for a C# console application entry-point that would make it a well-behaved citizen? What would you add or change to the following?

using System;
using System.Diagnostics;
using System.Linq;

static class Program
{
    static void Run(string[] args)
    {
        // TODO Replace line-echoing sample with actual application code

        string line;
        while ((line = Console.ReadLine()) != null)
            Console.WriteLine(line);
    }

    static int Main(string[] args)
    {
        // TODO Use a more robust arguments parser
        if (args.Any(arg => arg.Equals("/v") || arg.Equals("-v"))) // verbose?
            Trace.Listeners.Add(new ConsoleTraceListener(true));

        try
        {
            Run(args);
            return Environment.ExitCode;
        }
        catch (Exception e)
        {
            Console.Error.WriteLine(e.Message);
            Trace.TraceError(e.ToString());

            return Environment.ExitCode != 0
                 ? Environment.ExitCode : 100;
        }
    }
}

What this boilerplate achieves:

  • If an exception is thrown:
    • a clean message is printed to standard error
    • the full details are traced
    • the exit code is set to an arbitrary non-zero value (100) unless already set
  • Tracing can be turned on dynamically via a verbose switch
  • Tracing is sent to standard error to not interfere the real output
  • On successful completion, the exit code reflects the last Environment.ExitCode value, which is usually zero but can be changed by downstream code
  • Program class is static

Non-goals of this question:

  • Identify command-line arguments handling code or library
  • Identify an external logging or tracing library
+1  A: 

To me I would want to see Run(args) replaced with the instantiation of a class. Something like:

Main Try:

try
{
    // Process args to get parameters for AClass
    AClass program = new AClass(a, b);
    return program.Run();
}

AClass:

public class AClass {
    AClass(string a, string b) { ... }
    public int Run() {
        ...
        return Environment.ExitCode;
    }
}

Something like this is going to discourage procedural code and encourage creating an object oriented approach.

In regards to the AClass() constructor, I think the arguments should be processed prior to being passed into AClass() instead of AClass() having to know that it's created via a Console app.

Gavin Miller
+4  A: 

I think all depends on actual requirements for that app. If there is no requirement to do custom error handling - don't do that. If no program needs to check your app's exit code - no need to return it; and I believe there are cases where those requirements do not apply.

In other words, minimal is minimal; do the simplest thing that could possibly work. And if yor app meets requirements, I guess we can call it well-behaved.

driushkin
+1 for recommending against overengineering on robustness.
dsimcha
A: 

I use emacs, and a thing called defaultcontent.el . If you aren't familiar, Emacs is a text editor, and is very extensible. defaultcontent.el is an extension that inserts (surprise) default content into new files, when they are first created by emacs.

So if I try to open a .cs file that does not exist, emacs creates it, and then puts a bunch of default content into it. I decide what content to include in new files. For my C# files, this content includes:

  • simple documentation with the timestamp, copyright, other boilerplate.
  • a bunch of assembly-scope attributes (FileVersion, AssemblyDescription, etc)
  • a single class
  • a contructor that accepts an array of string arguments, and parses them; the parse logic is a switch statement, with some boilerplate for parsing integers, strings, and booleans.
  • a Usage method, that prints the usage information.
  • a Main method, which instantiates the class and calls "Run()"; this is surrounded in a try..catch and in the catch, the Usage() method is called. If any error occurs anywhere in the program, the usage message is displayed.

The defaultcontent.el also lets me position the cursor where I want. In my case, that's in the middle of the empty Run() method.

This is my default content:

// default.cs
// ------------------------------------------------------------------
//
// Description goes here....
// 
// Author: MyUser
// built on host: MyMachine
// Created Tue Oct 27 15:01:18 2009
//
// last saved: 
// Time-stamp: <2009-October-20 00:18:52>
// ------------------------------------------------------------------
//
// Copyright Notice here
// All rights reserved!
//
// ------------------------------------------------------------------

using System;
using System.Reflection;


// to allow fast ngen
[assembly: AssemblyTitle("default.cs")]
[assembly: AssemblyDescription("insert purpose here")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Dino Chiesa")]
[assembly: AssemblyProduct("Tools")]
[assembly: AssemblyCopyright("Copyright notice again")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: AssemblyVersion("1.1.1.1")]


namespace Cheeso.ToolsAndTests
{

  public class default
  {
    // ctor
    public default () {}

    string _positionalParam1;
    string _param1;
    int _intParam = DefaultIntParamValue;
    bool _flag1;

    private readonly int DefaultIntParamValue = -99;

    // ctor
    public default (string[] args)
    {
        for (int i=0; i < args.Length; i++)
        {
            switch (args[i])
            {
            case "-stringArg":
                i++;
                if (args.Length <= i) throw new ArgumentException(args[i]);
                _param1 = args[i];
                break;

            case "-intArg":
                i++;
                if (args.Length <= i) throw new ArgumentException(args[i]);
                if (_intParam != DefaultIntParamValue)
                    throw new ArgumentException(args[i]);
                if (args[i].StartsWith("0x"))
                    _intParam = System.Int32.Parse(args[i].Substring(2), System.Globalization.NumberStyles.AllowHexSpecifier );
                else
                    _intParam = System.Int32.Parse(args[i]);
                break;


            case "-boolarg":
                _flag1 = true;
                break;

            case "-?":
                throw new ArgumentException(args[i]);

            default:
                if (_positionalParam1 != null)
                    throw new ArgumentException(args[i]);

                _positionalParam1 = args[i];
                break;
            }
        }

        // default values
        if (_positionalParam1 == null)
            _positionalParam1 = "default.value.for.positional.param";

        if (_param1 == null)
            _param1 = "default.value.for.param1";

        if (_param2 == 0)
            _param2 = DEFAULT_value_for_param2;

    }

    public void Run()
    {



    }

    public static void Usage()
    {
        Console.WriteLine("\ndefault: <usage statement here>.\n");
        Console.WriteLine("Usage:\n  default [-arg1 <value>] [-arg2]");
    }


    public static void Main(string[] args)
    {
      try 
      {
        new default(args)
            .Run();
      }
      catch (System.Exception exc1)
      {
        Console.WriteLine("Exception: {0}", exc1.ToString());
        Usage();
      }
    }

  }
}

I also have defaultcontent set up for .c, .cpp, .vb, .csproj, .xml, .xsd, .wsdl, makefile, and many other file types.

Cheeso
A: 

On this subject, I have found this article to be the most in depth and informative:

Console Appplications in .NET, or Teaching a New Dog Old Tricks

Michael Brook

MSDN Magazine Feb 2004

http://msdn.microsoft.com/en-us/magazine/cc164014.aspx

Martin Aatmaa