19
\$\begingroup\$

I just wrote this short little program to increment up the build number for my projects every time I build them.

After compiling this exe, I just call it in the pre-build command line.

I did take out the filename string because I used it thrice, and I figured I'd get some flack if I didn't.

static void Main()
{
    int num;
    string file = "AssemblyInfo.cs";
    if (File.Exists(file))
        File.WriteAllLines(file, File.ReadAllLines(file).Select(s => !s.Trim().StartsWith("[assembly: AssemblyFileVersion(") ? s : string.Join(".", s.Trim(']').Trim(')').Trim('"').Split('.').Select((n, i) => i != 3 ? n : !int.TryParse(n, out num) ? n : (num + 1).ToString())) + "\")]"));
}

I realize this is complex, and the average person wont be able to read it. I use magic numbers, and I do some things specifically to keep the glory on 1 line. Feel free to bask in the presence of psychedelic code (which just wants to be your friend).

\$\endgroup\$
8
  • \$\begingroup\$ How about just having [assembly: AssemblyVersion("1.0.*")] in your AssemblyInfo.cs file? \$\endgroup\$ Apr 22, 2014 at 17:16
  • 1
    \$\begingroup\$ @Mat'sMug Last I checked(years ago), that didn't work as I'd liked. The numbers weren't incrementing, but instead being eradic and random, I probably should have checked again before writing this :D \$\endgroup\$
    – BenVlodgi
    Apr 22, 2014 at 17:22
  • \$\begingroup\$ You're right, the build number does look somewhat random, but a new build will always have a higher build number than the previous. What need is there to have it exactly incremental? \$\endgroup\$ Apr 22, 2014 at 17:24
  • 2
    \$\begingroup\$ Is it possible that the revision number went up when the build number went down? \$\endgroup\$ Apr 22, 2014 at 18:04
  • 1
    \$\begingroup\$ I suggest using the System.Version class (Version.Parse, then create an incremented object, then call Version.ToString) to reduce the string gymnastics. \$\endgroup\$
    – Ani
    Apr 23, 2014 at 3:30

6 Answers 6

16
\$\begingroup\$

Why not use Regex.Match instead of using those confusing joins/splits? Unless you are purposely trying to code golf this, doing it using regular expression is much easier to follow logically.

After taking into account delegates used with Regex.Replace (as explained in svick's comment), here is a functionally superior solution to the previous answer I posted. This eliminates much of the unnecessary lambda pieces. The original solution is arguably not a good use of LINQ because of the changes being made to the collection (ie. not a very strong use of queries, despite lambdas having that functionality).

static void Main()
{
    string file = "AssemblyInfo.cs";
    if (File.Exists(file))
        File.WriteAllText(file,
            Regex.Replace(
                File.ReadAllText(file),
                @"(?<=\[assembly: AssemblyFileVersion\(""[0-9]*.[0-9]*.)[0-9]*(?=.[0-9]*""\)\])",
                m => (Convert.ToInt16(m.Value) + 1).ToString()
            )
        );
    }
}

Still only one semi-colon, but at least it's possible to look at it and recognize each piece for what it does. Note I split the parentheses onto separate lines here for parameter readability, but some programmers disagree with this style of line-breaking.

It basically works similar to your original solution, but it basically finds and replaces the Regex.Match with an incremented value.

\$\endgroup\$
5
  • \$\begingroup\$ Wouldn't this be even better by using Regex.Replace? That way, you wouldn't have to do anything with parts that don't change. \$\endgroup\$
    – svick
    Apr 22, 2014 at 20:10
  • \$\begingroup\$ @svick: I'm not sure the Regex engine is able to perform number increments like that within Regex.Replace. Maybe you can accomplish this by using a delegate. \$\endgroup\$
    – grovesNL
    Apr 22, 2014 at 20:19
  • \$\begingroup\$ @CodesInChaos: foreach uses an enumerator. How would you modify the value of the line in this way? \$\endgroup\$
    – grovesNL
    Apr 22, 2014 at 21:33
  • \$\begingroup\$ Yeah, delegate, along with look-ahead and look-behind: string expr = @"(?<=\[assembly: AssemblyFileVersion\(""[0-9]*.[0-9]*.)[0-9]*(?=.[0-9]*""\)\])"; file[i] = Regex.Replace(file[i], expr, m => (Convert.ToInt16(m.Value) + 1).ToString());. \$\endgroup\$
    – svick
    Apr 22, 2014 at 22:30
  • \$\begingroup\$ @svick: Nice, I like that. Feel free to edit it directly into my post if you'd like (I don't have access to a compiler right now to test). \$\endgroup\$
    – grovesNL
    Apr 23, 2014 at 1:04
13
\$\begingroup\$

Don't try to be a badass cowboy. ☺

Stuffing everything onto one line of code is trying to be badass. You could improve clarity simply by adding whitespace.

Producing an incremented version number by careless string processing is trying to be a cowboy. I suggest introducing some kind of abstraction, such as this SemVer class. If you don't want to introduce an external dependency, it wouldn't be that hard to incorporate just the routines you need into your own code.

A regular expression substitution with a callback would be the way to go, I think.

static string IncrVersion(Match m)
{
    SemVersion version = SemVersion.Parse(match.Groups[1].Value);
    return version.Change(patch: 1 + version.Patch).ToString();
}

static void Main()
{
    string file = "AssemblyInfo.cs";
    var versionRx = new Regex(@"\[assembly: AssemblyFileVersion\((.*)\)\]");
    if (File.Exists(file)) 
    {
        File.WriteAllLines(file,
            File.ReadAllLines(file).Select(s =>
                versionRx.Replace(s, new MatchEvaluator(IncrVersion))
            )   
        );  
    }
}   
\$\endgroup\$
1
  • \$\begingroup\$ I'm no C# programmer. Please feel free to correct the code above. \$\endgroup\$ Apr 22, 2014 at 23:19
10
\$\begingroup\$

There is way too much code in that last line. I'm not a stickler for wrapping at 80 character, but 280 characters seems like a bit much. Just because this is a build step script doesn't mean you code doesn't have to be readable. Write a real function to do this operation so I don't have to trust that you are writing the line back the correct way.

\$\endgroup\$
10
\$\begingroup\$

I think it's not completely terrible to cram a lot of stuff in a single chain of instructions. But, for the love of god, you have to break that line. Perhaps something like this:

if (File.Exists(file)) {
    File.WriteAllLines(file,
            File.ReadAllLines(file)
                    .Select(s => !s.Trim().StartsWith("[assembly: AssemblyFileVersion(")
                    ? s
                    : string.Join(".", s.Trim(']').Trim(')').Trim('"').Split('.')
                    .Select((n, i) => i != 3
                    ? n
                    : !int.TryParse(n, out num)
                    ? n
                    : (num + 1).ToString())) + "\")]"));
}

I don't have a Visual Studio with me. I used the auto-format function of IntelliJ to get this one (sort of). I'm sure Visual Studio too has something to reformat this nicely. Use it!


In addition to the above, to put it simply, you want to change this:

[assembly: AssemblyVersion("x.y.z")]

to this:

[assembly: AssemblyVersion("x.y.z+1")]

In addition to expanding the long chain to multiple lines, I would point out that this part is ugly:

string.Join(".", s.Trim(']').Trim(')').Trim('"').Split('.')

It would be better to use an approach like this, sorry for the Perl implementation:

sub increment_version {
    my ($before, $minor, $after) = @_;
    $before . ($minor + 1) . $after;
}
s/(\[assembly: AssemblyFileVersion\("\d+\.\d+\.)(\d+)("\)\])/&increment_version($1, $2, $3)/e;

The idea here is to use a regular expression to extract the part you want to change, the part before and after it, and replace the whole thing with the middle part changed as you wish.

Finally, but perhaps most importantly, you definitely want to unit test the middle part that replaces the text. When you do that, you will have to extract the entire first Select into its own method, which will do you a lot of good, and make your implementation more readable.

\$\endgroup\$
0
5
\$\begingroup\$

Well, you could also use a custom build target that increments the build number automatically.

Using MSBuild.Community.Tasks, this is pretty easy:

<Version VersionFile="Version.txt" BuildType="Incremental">
  <Output TaskParameter="Major" PropertyName="Major" />
  <Output TaskParameter="Minor" PropertyName="Minor" />
  <Output TaskParameter="Build" PropertyName="Build" />
  <Output TaskParameter="Revision" PropertyName="Revision" />
</Version>
<AssemblyInfo 
   AssemblyVersion="$(Major).$(Minor).$(Build).$(Revision)"
   AssemblyFileVersion="$(Major).$(Minor).$(Build).$(Revision)" 
   CodeLanguage="CS" ComVisible="False" />
\$\endgroup\$
1
  • 1
    \$\begingroup\$ While it doesn't review the code in the OP itself, this certainly strikes towards the OP's goal, using a different part of the same technology. +1 \$\endgroup\$
    – Brian S
    Apr 23, 2014 at 20:17
3
\$\begingroup\$
i != 3 ? n : !int.TryParse(n, out num) ? n : code

Could be

i != 3 || !int.TryParse(n, out num) ? n : code

...saves 3 characters, and makes more logical sense.

Also, instead of checking that index, it could be re-written to store the result, then only modify the 3rd indexed location and TryParse that.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.