diff --git a/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj b/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj
index aaf566b4d4..1fae14160e 100644
--- a/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj
+++ b/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj
@@ -99,6 +99,7 @@
+
@@ -509,6 +510,45 @@
Always
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/ShimTargetSpecs.cs b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/ShimTargetSpecs.cs
new file mode 100644
index 0000000000..74cd026286
--- /dev/null
+++ b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/ShimTargetSpecs.cs
@@ -0,0 +1,441 @@
+// Copyright © 2017 - 2018 Chocolatey Software, Inc
+// Copyright © 2011 - 2017 RealDimensions Software, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+//
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace chocolatey.tests.integration.infrastructure.app.shimtarget
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Text;
+ using chocolatey.infrastructure.app.domain;
+ using NUnit.Framework;
+ using Should;
+
+ public class ShimTargetSpecs
+ {
+ public abstract class ShimTargetSpecsBase : TinySpec
+ {
+ protected string PackagePath;
+ protected string RootPath;
+ protected string IncludeFile;
+ protected string ErrorMessage;
+ protected IEnumerable Results;
+
+ public override void Context()
+ {
+ PackagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "infrastructure.app", "shimtarget", "pkg");
+ RootPath = Path.Combine(PackagePath, "tools");
+ IncludeFile = Path.Combine(RootPath, ".shiminclude");
+ ErrorMessage = string.Empty;
+ }
+
+ public override void Because()
+ {
+ var shimManager = new ShimTargetManager(PackagePath);
+ Results = shimManager.get_shim_targets();
+ }
+
+ protected bool check_results(string[] expected)
+ {
+ var targetsExist = true;
+ ErrorMessage = string.Empty;
+
+ foreach (var path in expected)
+ {
+ var target = resolve_from_root(path);
+ if (Results.Contains(target, StringComparer.OrdinalIgnoreCase)) continue;
+
+ targetsExist = false;
+ ErrorMessage = "Expected file missing: {0}".format_with(target);
+ break;
+ }
+
+ return targetsExist;
+ }
+
+ protected string resolve_from_root(string relativePath)
+ {
+ return Path.GetFullPath(Path.Combine(RootPath, relativePath));
+ }
+
+ protected void write_shim_include(string[] content)
+ {
+ File.WriteAllLines(IncludeFile, content, Encoding.UTF8);
+ }
+ }
+
+ [Category("Integration")]
+ public class when_shiminclude_is_empty : ShimTargetSpecsBase
+ {
+ public override void Context()
+ {
+ base.Context();
+ string[] lines = { };
+ write_shim_include(lines);
+ }
+
+ [Fact]
+ public void no_targets_should_be_returned()
+ {
+ Results.ShouldBeEmpty();
+ }
+ }
+
+ [Category("Integration")]
+ public class when_shiminclude_contains_blank_and_comment_lines : ShimTargetSpecsBase
+ {
+ public override void Context()
+ {
+ base.Context();
+ string[] lines = { "# comment", "", "", "#*.exe" };
+ write_shim_include(lines);
+ }
+
+ [Fact]
+ public void no_targets_should_be_returned()
+ {
+ Results.ShouldBeEmpty();
+ }
+ }
+
+ [Category("Integration")]
+ public class when_including_a_target_outside_the_package_directory : ShimTargetSpecsBase
+ {
+ private string _target;
+
+ public override void Context()
+ {
+ base.Context();
+ _target = @"..\..\outside.exe";
+ string[] lines = { _target };
+ write_shim_include(lines);
+ }
+
+ [Fact]
+ public void the_target_should_exist()
+ {
+ File.Exists(resolve_from_root(_target)).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void the_target_should_not_be_returned()
+ {
+ Results.ShouldBeEmpty();
+ }
+ }
+
+ [Category("Integration")]
+ public class when_including_a_target_in_the_package_directory : ShimTargetSpecsBase
+ {
+ private string _target;
+
+ public override void Context()
+ {
+ base.Context();
+ _target = @"..\pkg.exe";
+ string[] lines = { _target };
+ write_shim_include(lines);
+ }
+
+ [Fact]
+ public void there_should_be_one_target_returned()
+ {
+ Results.Count().ShouldEqual(1);
+ }
+
+ [Fact]
+ public void the_target_should_be_returned()
+ {
+ string[] expected = { _target };
+ check_results(expected).ShouldBeTrue(ErrorMessage);
+ }
+ }
+
+ [Category("Integration")]
+ public class when_including_a_target_with_an_absolute_path : ShimTargetSpecsBase
+ {
+ private string _target;
+
+ public override void Context()
+ {
+ base.Context();
+ _target = resolve_from_root(@"prog\sbin\prog.exe");
+ string[] lines = { _target };
+ write_shim_include(lines);
+ }
+
+ [Fact]
+ public void the_target_should_exist()
+ {
+ File.Exists(_target).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void the_target_should_not_be_returned()
+ {
+ Results.ShouldBeEmpty();
+ }
+ }
+
+ [Category("Integration")]
+ public class when_including_a_target_with_an_absolute_path_from_the_current_drive : ShimTargetSpecsBase
+ {
+ private string _target;
+
+ public override void Context()
+ {
+ base.Context();
+ _target = resolve_from_root(@"prog\sbin\prog.exe");
+ string[] lines = { _target.Substring(3) };
+ write_shim_include(lines);
+ }
+
+ [Fact]
+ public void the_target_should_exist()
+ {
+ File.Exists(_target).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void the_target_should_not_be_returned()
+ {
+ Results.ShouldBeEmpty();
+ }
+ }
+
+ [Category("Integration")]
+ public class when_including_a_target_with_an_unsupported_file_extension : ShimTargetSpecsBase
+ {
+ private string _target;
+
+ public override void Context()
+ {
+ base.Context();
+ _target = @"prog\opt\opt3.vbs";
+ string[] lines = { _target };
+ write_shim_include(lines);
+ }
+
+ [Fact]
+ public void the_target_should_exist()
+ {
+ File.Exists(resolve_from_root(_target)).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void the_target_should_not_be_returned()
+ {
+ Results.ShouldBeEmpty();
+ }
+ }
+
+ [Category("Integration")]
+ public class when_including_exe_targets_in_the_root_directory : ShimTargetSpecsBase
+ {
+ public override void Context()
+ {
+ base.Context();
+ string[] lines = { "." };
+ write_shim_include(lines);
+ }
+
+ [Fact]
+ public void there_should_be_two_targets_returned()
+ {
+ Results.Count().ShouldEqual(2);
+ }
+
+ [Fact]
+ public void the_exe_targets_should_be_returned()
+ {
+ string[] expected = { "!tools.exe", "#tools.exe" };
+ check_results(expected).ShouldBeTrue(ErrorMessage);
+ }
+ }
+
+ [Category("Integration")]
+ public class when_including_targets_starting_with_hash_and_exclamation_mark : ShimTargetSpecsBase
+ {
+ public override void Context()
+ {
+ base.Context();
+ // also test using an empty directory
+ string[] lines = { @"\#tools.exe", @"\!tools.exe" };
+ write_shim_include(lines);
+ }
+
+ [Fact]
+ public void there_should_be_two_targets_returned()
+ {
+ Results.Count().ShouldEqual(2);
+ }
+
+ [Fact]
+ public void the_targets_should_be_returned()
+ {
+ string[] expected = { "!tools.exe", "#tools.exe" };
+ check_results(expected).ShouldBeTrue(ErrorMessage);
+ }
+ }
+
+ [Category("Integration")]
+ public class when_excluding_a_target_starting_with_exclamation_mark : ShimTargetSpecsBase
+ {
+ public override void Context()
+ {
+ base.Context();
+ // also test using an empty directory
+ string[] lines = { "*.exe", "!!tools.exe" };
+ write_shim_include(lines);
+ }
+
+ [Fact]
+ public void there_should_be_one_target_returned()
+ {
+ Results.Count().ShouldEqual(1);
+ }
+
+ [Fact]
+ public void the_non_excluded_targets_should_be_returned()
+ {
+ string[] expected = { "#tools.exe" };
+ check_results(expected).ShouldBeTrue(ErrorMessage);
+ }
+ }
+
+ [Category("Integration")]
+ public class when_including_wildcard_folders : ShimTargetSpecsBase
+ {
+ public override void Context()
+ {
+ base.Context();
+ // also test forward slashes
+ string[] lines = { "prog/*" };
+ write_shim_include(lines);
+ }
+
+ [Fact]
+ public void there_should_be_three_targets_returned()
+ {
+ Results.Count().ShouldEqual(3);
+ }
+
+ [Fact]
+ public void the_targets_should_be_returned()
+ {
+ string[] expected = { @"prog\opt\opt.exe", @"prog\sbin\prog.exe", @"prog\sbin\prog2.exe" };
+ check_results(expected).ShouldBeTrue(ErrorMessage);
+ }
+ }
+
+ [Category("Integration")]
+ public class when_including_wildcard_folders_with_other_extensions : ShimTargetSpecsBase
+ {
+ public override void Context()
+ {
+ base.Context();
+ // also test forward slashes
+ string[] lines = { "prog/*/*.bat", "prog/*/*.cmd" };
+ write_shim_include(lines);
+ }
+
+ [Fact]
+ public void there_should_be_two_targets_returned()
+ {
+ Results.Count().ShouldEqual(2);
+ }
+
+ [Fact]
+ public void the_targets_should_be_returned()
+ {
+ string[] expected = { @"prog\opt\opt2.cmd", @"prog\sbin\prog3.bat" };
+ check_results(expected).ShouldBeTrue(ErrorMessage);
+ }
+ }
+
+ [Category("Integration")]
+ public class when_including_a_specifically_ignored_target : ShimTargetSpecsBase
+ {
+ private string _resolvedTarget;
+
+ public override void Context()
+ {
+ base.Context();
+ var target = @"prog\ignore-me.exe";
+ _resolvedTarget = resolve_from_root(target);
+ string[] lines = { target };
+ write_shim_include(lines);
+ }
+
+ [Fact]
+ public void the_target_should_exist()
+ {
+ File.Exists(_resolvedTarget).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void the_target_ignore_file_should_exist()
+ {
+ File.Exists(_resolvedTarget + ".ignore").ShouldBeTrue();
+ }
+
+ [Fact]
+ public void the_target_should_not_be_returned()
+ {
+ Results.ShouldBeEmpty();
+ }
+ }
+
+ [Category("Integration")]
+ public class when_no_shiminclude_is_found : ShimTargetSpecsBase
+ {
+ public override void Context()
+ {
+ base.Context();
+ File.Delete(IncludeFile);
+ }
+
+ [Fact]
+ public void the_shiminclude_file_should_not_exist()
+ {
+ File.Exists(IncludeFile).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void there_should_be_six_targets_returned()
+ {
+ Results.Count().ShouldEqual(6);
+ }
+
+ [Fact]
+ public void the_exe_targets_should_be_returned()
+ {
+ string[] expected =
+ {
+ @"..\pkg.exe",
+ @".\!tools.exe",
+ @".\#tools.exe",
+ @"prog\opt\opt.exe",
+ @"prog\sbin\prog.exe",
+ @"prog\sbin\prog2.exe"
+ };
+
+ check_results(expected).ShouldBeTrue(ErrorMessage);
+ }
+ }
+ }
+}
diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/outside.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/outside.exe
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/pkg.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/pkg.exe
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/!tools.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/!tools.exe
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/#tools.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/#tools.exe
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/.shiminclude b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/.shiminclude
new file mode 100644
index 0000000000..fa7c7139fd
--- /dev/null
+++ b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/.shiminclude
@@ -0,0 +1 @@
+# no shims here
diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/ignore-me.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/ignore-me.exe
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/ignore-me.exe.ignore b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/ignore-me.exe.ignore
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt.exe
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt2.cmd b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt2.cmd
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt3.vbs b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt3.vbs
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog.exe
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog2.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog2.exe
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog3.bat b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog3.bat
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj
index f1d4e6bc7e..a341017926 100644
--- a/src/chocolatey/chocolatey.csproj
+++ b/src/chocolatey/chocolatey.csproj
@@ -134,6 +134,10 @@
+
+
+
+
diff --git a/src/chocolatey/infrastructure.app/domain/ShimTargetFileFinder.cs b/src/chocolatey/infrastructure.app/domain/ShimTargetFileFinder.cs
new file mode 100644
index 0000000000..f26407d7c9
--- /dev/null
+++ b/src/chocolatey/infrastructure.app/domain/ShimTargetFileFinder.cs
@@ -0,0 +1,152 @@
+// Copyright © 2017 - 2018 Chocolatey Software, Inc
+// Copyright © 2011 - 2017 RealDimensions Software, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+//
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace chocolatey.infrastructure.app.domain
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.IO;
+
+ public class ShimTargetFileFinder
+ {
+ ///
+ /// Returns the last three characters of the file name.
+ ///
+ /// The filename
+ /// The extension
+ ///
+ /// Only specific three-character extensions are used.
+ ///
+ public string get_extension(string fileName)
+ {
+ return fileName.Substring(fileName.Length - 3).ToLower();
+ }
+
+ ///
+ /// Searches the package for all exe files.
+ ///
+ /// The package folder location.
+ /// The exe files to shim.
+ public IEnumerable get_targets(string path)
+ {
+ var result = find(path, "*.exe", SearchOption.AllDirectories);
+
+ return remove_ignored_files(result);
+ }
+
+ ///
+ /// Resolves executable file patterns from an include and exclude list.
+ ///
+ /// The include list.
+ /// The exclude list.
+ /// The executable files to shim.
+ public IEnumerable get_targets(ShimTargetList includes, ShimTargetList excludes)
+ {
+ var result = new List();
+
+ foreach (var path in includes.Items.Keys)
+ {
+ var includedFiles = get_files(path, includes.Items[path]);
+ var excludedFiles = get_files(excludes, path);
+
+ if (excludedFiles.Count == 0)
+ {
+ result.AddRange(includedFiles);
+ continue;
+ }
+
+ foreach (var file in includedFiles)
+ {
+ if (!excludedFiles.Contains(file))
+ {
+ result.Add(file);
+ }
+ }
+ }
+
+ return remove_ignored_files(result);
+ }
+
+ ///
+ /// Searches for files in a specified path, optionally searching subdirectories.
+ ///
+ /// The path.
+ /// The search pattern.
+ /// TopDirectoryOnly or AllDirectories.
+ /// The matched file names.
+ private IEnumerable find(string path, string pattern, SearchOption searchOption)
+ {
+ var extension = get_extension(pattern);
+
+ return Directory.EnumerateFiles(path, pattern, searchOption)
+ .Where(f => f.EndsWith(extension, StringComparison.OrdinalIgnoreCase));
+ }
+
+ ///
+ /// Searches for files from a list of search patterns.
+ ///
+ /// The path.
+ /// List of search patterns.
+ /// The matched file names.
+ private List get_files(string path, IEnumerable patterns)
+ {
+ var result = new List();
+
+ foreach (var pattern in patterns)
+ {
+ var files = find(path, pattern, SearchOption.TopDirectoryOnly);
+ result.AddRange(files);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Searches for files if a list contains a specific path.
+ ///
+ /// The list
+ /// The path.
+ /// The matched file names or an empty List.
+ private List get_files(ShimTargetList list, string path)
+ {
+ if (list.Items.ContainsKey(path))
+ {
+ return get_files(path, list.Items[path]);
+ }
+
+ return new List();
+ }
+
+ ///
+ /// Filters out files that should not be shimmed.
+ ///
+ /// The candidate files to shim.
+ /// The intended files to shim
+ private List remove_ignored_files(IEnumerable targetFiles)
+ {
+ var result = new List();
+
+ foreach (var file in targetFiles)
+ {
+ // ignore the file if there is a matching file suffixed '.ignore'
+ if (!File.Exists(file + ".ignore")) result.Add(file);
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/chocolatey/infrastructure.app/domain/ShimTargetList.cs b/src/chocolatey/infrastructure.app/domain/ShimTargetList.cs
new file mode 100644
index 0000000000..7cbe4824da
--- /dev/null
+++ b/src/chocolatey/infrastructure.app/domain/ShimTargetList.cs
@@ -0,0 +1,60 @@
+// Copyright © 2017 - 2018 Chocolatey Software, Inc
+// Copyright © 2011 - 2017 RealDimensions Software, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+//
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace chocolatey.infrastructure.app.domain
+{
+ using System;
+ using System.Collections.Generic;
+
+ public class ShimTargetList
+ {
+ public IDictionary> Items;
+
+ ///
+ /// Creates a ShimTargetList instance.
+ ///
+ public ShimTargetList()
+ {
+ Items = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Adds a path and file pattern to the items collection.
+ ///
+ /// The path.
+ /// The file pattern.
+ public void add_directive(string path, string filePattern)
+ {
+ List filePatterns;
+
+ // important - always lowercase the file pattern
+ filePattern = filePattern.ToLower();
+
+ if (Items.TryGetValue(path, out filePatterns))
+ {
+ if (!filePatterns.Contains(filePattern))
+ {
+ filePatterns.Add(filePattern);
+ }
+ }
+ else
+ {
+ filePatterns = new List { filePattern };
+ Items.Add(path, filePatterns);
+ }
+ }
+ }
+}
diff --git a/src/chocolatey/infrastructure.app/domain/ShimTargetManager.cs b/src/chocolatey/infrastructure.app/domain/ShimTargetManager.cs
new file mode 100644
index 0000000000..4e7e86ccb6
--- /dev/null
+++ b/src/chocolatey/infrastructure.app/domain/ShimTargetManager.cs
@@ -0,0 +1,296 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+//
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace chocolatey.infrastructure.app.domain
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Text.RegularExpressions;
+
+ public class ShimTargetManager
+ {
+ private readonly ShimTargetFileFinder _finder;
+ private readonly ShimTargetPathResolver _resolver;
+ private readonly string _packageDir;
+ private readonly string _includeFile;
+ private ShimTargetList _includes;
+ private ShimTargetList _excludes;
+
+ ///
+ /// Creates a ShimTargetManager instance.
+ ///
+ /// The package directory.
+ public ShimTargetManager(string packageDir)
+ {
+ var toolsDir = Path.Combine(packageDir, "tools");
+ _packageDir = packageDir;
+ _includeFile = Path.Combine(toolsDir, ".shiminclude");
+ _finder = new ShimTargetFileFinder();
+ _resolver = new ShimTargetPathResolver(toolsDir);
+ }
+
+ ///
+ /// Searches for executables to shim.
+ ///
+ /// Executables to shim or an empty list.
+ public IEnumerable get_shim_targets()
+ {
+ if (!File.Exists(_includeFile))
+ {
+ return _finder.get_targets(_packageDir);
+ }
+
+ _includes = new ShimTargetList();
+ _excludes = new ShimTargetList();
+
+ var directives = File.ReadAllLines(_includeFile, System.Text.Encoding.UTF8);
+ parse_include_file(directives);
+
+ return _finder.get_targets(_includes, _excludes);
+ }
+
+ ///
+ /// Evaluates the .shiminclude file directives.
+ ///
+ /// The list of directives.
+ /// Populates _includes and _excludes lists.
+ private void parse_include_file(string[] directives)
+ {
+ var includedEntries = new ShimTargetList();
+ var excludedEntries = new ShimTargetList();
+
+ foreach (string lineEntry in directives)
+ {
+ var line = lineEntry.Trim();
+
+ // skip blank or comment lines
+ if (string.IsNullOrEmpty(line) || line.StartsWith("#"))
+ {
+ continue;
+ }
+
+ var entryList = includedEntries;
+
+ if (line.StartsWith("\\#") || line.StartsWith("\\!"))
+ {
+ // backslash escapes
+ line = line.Substring(1);
+ }
+ else if (line.StartsWith("!"))
+ {
+ // make sure we have enough characters
+ if (line.Length == 1) continue;
+
+ line = line.Substring(1);
+ entryList = excludedEntries;
+ }
+
+ // transform to single backslashes, and remove duplicate asterisks
+ var path = Regex.Replace(line.Replace('/', '\\'), @"\\+", "\\");
+ path = Regex.Replace(path, @"\*+", "*");
+
+ // relative paths only - skip if rooted or drive path
+ if (path.StartsWith("\\") || Regex.IsMatch(path, @"^[A-Za-z]:")) continue;
+
+ // check the last segment to determine if we are a filename or a directory
+ var lastSegment = Path.GetFileName(path);
+ var filePattern = "*.exe";
+
+ if (Regex.IsMatch(lastSegment, @"\.(exe|bat|cmd)$", RegexOptions.IgnoreCase))
+ {
+ path = Path.GetDirectoryName(path);
+ filePattern = lastSegment;
+ }
+
+ // normalize the path
+ path = path.TrimEnd('\\');
+ if (string.IsNullOrEmpty(path))
+ {
+ path = ".";
+ }
+
+ entryList.add_directive(path, filePattern);
+ }
+
+ resolve_directives(includedEntries, _includes);
+ resolve_directives(excludedEntries, _excludes);
+ remove_excluded();
+ }
+
+ ///
+ /// Resolves directives and adds them to the list.
+ ///
+ /// The parsed directives from the file.
+ /// The resolved directives.
+ public void resolve_directives(ShimTargetList entryList, ShimTargetList pathList)
+ {
+ foreach (var path in entryList.Items.Keys)
+ {
+ var resolvedPaths = _resolver.resolve(path);
+
+ // check we have results and that they are in the package directory
+ if (resolvedPaths.Count == 0 || !resolvedPaths[0].StartsWith(_packageDir, StringComparison.CurrentCultureIgnoreCase))
+ {
+ continue;
+ }
+
+ var filePatterns = entryList.Items[path];
+
+ foreach (var filePattern in filePatterns)
+ {
+ foreach (var resolvedPath in resolvedPaths)
+ {
+ pathList.add_directive(resolvedPath, filePattern);
+ }
+ }
+ }
+
+ sanitize_directives(pathList);
+ }
+
+ ///
+ /// Removes redundant file patterns.
+ ///
+ /// List of resolved directives.
+ private void sanitize_directives(ShimTargetList list)
+ {
+ foreach (var filePatterns in list.Items.Values)
+ {
+ if (filePatterns.Count == 0) continue;
+
+ var anyPatterns = new List();
+ var specificPatterns = new List();
+
+ // split into anyPatterns (like *.exe) and specificPatterns (like file.exe)
+ foreach (var filePattern in filePatterns)
+ {
+ var match = Regex.Match(filePattern, @"^\*\.(exe|bat|cmd)$").Groups[1];
+ if (match.Success)
+ {
+ anyPatterns.Add(match.Value);
+ }
+ else
+ {
+ specificPatterns.Add(filePattern);
+ }
+ }
+
+ // remove specific patterns (file.exe) if there is a matching any pattern (*.exe)
+ foreach (var pattern in specificPatterns)
+ {
+ var extension = _finder.get_extension(pattern);
+
+ if (anyPatterns.Contains(extension))
+ {
+ filePatterns.Remove(pattern);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Removes matching path-patterns from the included and excluded lists.
+ ///
+ private void remove_excluded()
+ {
+ var redundantExcludes = new List();
+
+ foreach (var path in _excludes.Items.Keys)
+ {
+ var excludedPatterns = _excludes.Items[path];
+ List includedPatterns;
+
+ // add to redundantExcludes if not in includes
+ if (!_includes.Items.TryGetValue(path, out includedPatterns))
+ {
+ redundantExcludes.Add(path);
+ continue;
+ }
+
+ // remove matching patterns from includes list
+ var removedPatterns = exclude_patterns(excludedPatterns, includedPatterns);
+
+ // remove from includes list if no patterns left
+ if (includedPatterns.Count == 0)
+ {
+ _includes.Items.Remove(path);
+ }
+
+ // remove matched patterns from excluded patterns
+ foreach (var pattern in removedPatterns)
+ {
+ excludedPatterns.Remove(pattern);
+ }
+
+ // add to redundantExcludes if no patterns left
+ if (excludedPatterns.Count == 0)
+ {
+ redundantExcludes.Add(path);
+ }
+ }
+
+ // remove redundant excludes
+ foreach (var path in redundantExcludes)
+ {
+ _excludes.Items.Remove(path);
+ }
+ }
+
+ ///
+ /// Removes matching file patterns from a list of included patterns.
+ ///
+ /// The excluded file patterns.
+ /// The included file patterns.
+ /// The file patterns that have been removed.
+ private List exclude_patterns(List excludedPatterns, List includedPatterns)
+ {
+ var result = new List();
+
+ foreach (var exPattern in excludedPatterns)
+ {
+ var match = Regex.Match(exPattern, @"^\*\.(exe|bat|cmd)$").Groups[1];
+ if (match.Success)
+ {
+ // we match any pattern (like *.exe)
+ var extension = match.Value;
+ var removals = new List();
+
+ // add patterns with the same extension for removal
+ foreach (var incPattern in includedPatterns)
+ {
+ if (extension == _finder.get_extension(incPattern))
+ {
+ removals.Add(incPattern);
+ }
+ }
+
+ // remove the patterns
+ foreach (var pattern in removals)
+ {
+ includedPatterns.Remove(pattern);
+ }
+
+ result.Add(exPattern);
+ }
+ else if (includedPatterns.Contains(exPattern))
+ {
+ includedPatterns.Remove(exPattern);
+ result.Add(exPattern);
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/chocolatey/infrastructure.app/domain/ShimTargetPathResolver.cs b/src/chocolatey/infrastructure.app/domain/ShimTargetPathResolver.cs
new file mode 100644
index 0000000000..0aaf9a6d52
--- /dev/null
+++ b/src/chocolatey/infrastructure.app/domain/ShimTargetPathResolver.cs
@@ -0,0 +1,140 @@
+// Copyright © 2017 - 2018 Chocolatey Software, Inc
+// Copyright © 2011 - 2017 RealDimensions Software, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+//
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace chocolatey.infrastructure.app.domain
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Text.RegularExpressions;
+
+ public class ShimTargetPathResolver
+ {
+ private readonly string _rootPath;
+
+ ///
+ /// Creates a ShimTargetPathResolver instance.
+ ///
+ /// The directory on which relative paths are based.
+ public ShimTargetPathResolver(string rootPath)
+ {
+ _rootPath = rootPath;
+ }
+
+ ///
+ /// Resolves a path.
+ ///
+ /// The path relative to the root path.
+ /// The resolved paths or an empty list.
+ public List resolve(string path)
+ {
+ var resolvedPaths = new List { _rootPath };
+ var subDirs = new Queue(path.Split('\\'));
+
+ return resolve_path(resolvedPaths, subDirs);
+ }
+
+ ///
+ /// Recursively steps through the components of a directory path in order to resolve it.
+ ///
+ /// The list of already resolved directory paths.
+ /// The remaining subdirectories to resolve.
+ /// The resolved directory names as absolute paths.
+ private List resolve_path(List resolvedPaths, Queue subDirs)
+ {
+ var result = new List();
+
+ // safety, shouldn't happen
+ if (subDirs.Count == 0) return result;
+
+ // remove first directory
+ var folder = subDirs.Dequeue();
+ var wildcard = Regex.IsMatch(folder, @"[?*]");
+
+ foreach (var path in resolvedPaths)
+ {
+ var resolved = wildcard ? search(path, folder) : test(path, folder);
+
+ if (subDirs.Count > 0 && resolved.Count > 0)
+ {
+ resolved = resolve_path(resolved, subDirs);
+ }
+
+ result.AddRange(resolved);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Searches for directories in a specified path.
+ ///
+ /// The path.
+ /// The search pattern.
+ /// The matched directory names as absolute paths.
+ private List search(string path, string pattern)
+ {
+ var result = new List();
+ IEnumerable directories;
+
+ try
+ {
+ directories = Directory.GetDirectories(path, pattern, SearchOption.TopDirectoryOnly);
+ }
+ catch
+ {
+ return result;
+ }
+
+ // no need for try-catch as error conditions will have already been caught
+ foreach (var dir in directories)
+ {
+ var fullPath = Path.GetFullPath(dir);
+ result.Add(fullPath);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Tests for a directory in a specified path.
+ ///
+ /// The path.
+ /// The directory name to test.
+ /// A list containing the absolute path of any match.
+ private List test(string path, string folder)
+ {
+ var result = new List();
+ string fullPath;
+
+ try
+ {
+ fullPath = Path.GetFullPath(Path.Combine(path, folder));
+ }
+ catch
+ {
+ return result;
+ }
+
+ if (Directory.Exists(fullPath))
+ {
+ result.Add(fullPath);
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/chocolatey/infrastructure.app/services/ShimGenerationService.cs b/src/chocolatey/infrastructure.app/services/ShimGenerationService.cs
index 01d6991b24..afdb3c9fcb 100644
--- a/src/chocolatey/infrastructure.app/services/ShimGenerationService.cs
+++ b/src/chocolatey/infrastructure.app/services/ShimGenerationService.cs
@@ -22,6 +22,7 @@ namespace chocolatey.infrastructure.app.services
using configuration;
using filesystem;
using infrastructure.commands;
+ using infrastructure.app.domain;
using results;
public class ShimGenerationService : IShimGenerationService
@@ -87,8 +88,10 @@ public void install(ChocolateyConfiguration configuration, PackageResult package
return;
}
- //gather all .exes in the folder
- var exeFiles = _fileSystem.get_files(packageResult.InstallLocation, pattern: "*.exe", option: SearchOption.AllDirectories);
+ //gather all .exes in the folder
+ var shimManager = new ShimTargetManager(packageResult.InstallLocation);
+ var exeFiles = shimManager.get_shim_targets();
+
foreach (string file in exeFiles.or_empty_list_if_null())
{
if (_fileSystem.file_exists(file + ".ignore")) continue;