[c#] Visual Studio 솔루션 파일 구문 분석

.NET에서 SLN (Visual Studio 솔루션) 파일을 어떻게 구문 분석 할 수 있습니까? 상대적 빌드 순서를 저장하면서 여러 솔루션을 하나로 병합하는 앱을 작성하고 싶습니다.



답변

Microsoft.Build 어셈블리의 .NET 4.0 버전에는 Visual Studio 솔루션 파일을 구문 분석하는 Microsoft.Build.Construction 네임 스페이스에 SolutionParser 클래스가 포함되어 있습니다.

안타깝게도이 클래스는 내부 클래스이지만 유용 할 수있는 몇 가지 공통 속성을 얻기 위해 리플렉션을 사용하는 클래스에 해당 기능 중 일부를 래핑했습니다.

public class Solution
{
    //internal class SolutionParser
    //Name: Microsoft.Build.Construction.SolutionParser
    //Assembly: Microsoft.Build, Version=4.0.0.0

    static readonly Type s_SolutionParser;
    static readonly PropertyInfo s_SolutionParser_solutionReader;
    static readonly MethodInfo s_SolutionParser_parseSolution;
    static readonly PropertyInfo s_SolutionParser_projects;

    static Solution()
    {
        s_SolutionParser = Type.GetType("Microsoft.Build.Construction.SolutionParser, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false);
        if (s_SolutionParser != null)
        {
            s_SolutionParser_solutionReader = s_SolutionParser.GetProperty("SolutionReader", BindingFlags.NonPublic | BindingFlags.Instance);
            s_SolutionParser_projects = s_SolutionParser.GetProperty("Projects", BindingFlags.NonPublic | BindingFlags.Instance);
            s_SolutionParser_parseSolution = s_SolutionParser.GetMethod("ParseSolution", BindingFlags.NonPublic | BindingFlags.Instance);
        }
    }

    public List<SolutionProject> Projects { get; private set; }

    public Solution(string solutionFileName)
    {
        if (s_SolutionParser == null)
        {
            throw new InvalidOperationException("Can not find type 'Microsoft.Build.Construction.SolutionParser' are you missing a assembly reference to 'Microsoft.Build.dll'?");
        }
        var solutionParser = s_SolutionParser.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).First().Invoke(null);
        using (var streamReader = new StreamReader(solutionFileName))
        {
            s_SolutionParser_solutionReader.SetValue(solutionParser, streamReader, null);
            s_SolutionParser_parseSolution.Invoke(solutionParser, null);
        }
        var projects = new List<SolutionProject>();
        var array = (Array)s_SolutionParser_projects.GetValue(solutionParser, null);
        for (int i = 0; i < array.Length; i++)
        {
            projects.Add(new SolutionProject(array.GetValue(i)));
        }
        this.Projects = projects;
    }
}

[DebuggerDisplay("{ProjectName}, {RelativePath}, {ProjectGuid}")]
public class SolutionProject
{
    static readonly Type s_ProjectInSolution;
    static readonly PropertyInfo s_ProjectInSolution_ProjectName;
    static readonly PropertyInfo s_ProjectInSolution_RelativePath;
    static readonly PropertyInfo s_ProjectInSolution_ProjectGuid;
    static readonly PropertyInfo s_ProjectInSolution_ProjectType;

    static SolutionProject()
    {
        s_ProjectInSolution = Type.GetType("Microsoft.Build.Construction.ProjectInSolution, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false);
        if (s_ProjectInSolution != null)
        {
            s_ProjectInSolution_ProjectName = s_ProjectInSolution.GetProperty("ProjectName", BindingFlags.NonPublic | BindingFlags.Instance);
            s_ProjectInSolution_RelativePath = s_ProjectInSolution.GetProperty("RelativePath", BindingFlags.NonPublic | BindingFlags.Instance);
            s_ProjectInSolution_ProjectGuid = s_ProjectInSolution.GetProperty("ProjectGuid", BindingFlags.NonPublic | BindingFlags.Instance);
            s_ProjectInSolution_ProjectType = s_ProjectInSolution.GetProperty("ProjectType", BindingFlags.NonPublic | BindingFlags.Instance);
        }
    }

    public string ProjectName { get; private set; }
    public string RelativePath { get; private set; }
    public string ProjectGuid { get; private set; }
    public string ProjectType { get; private set; }

    public SolutionProject(object solutionProject)
    {
        this.ProjectName = s_ProjectInSolution_ProjectName.GetValue(solutionProject, null) as string;
        this.RelativePath = s_ProjectInSolution_RelativePath.GetValue(solutionProject, null) as string;
        this.ProjectGuid = s_ProjectInSolution_ProjectGuid.GetValue(solutionProject, null) as string;
        this.ProjectType = s_ProjectInSolution_ProjectType.GetValue(solutionProject, null).ToString();
    }
}

Microsoft.Build 참조를 프로젝트에 추가하려면 대상 프레임 워크를 “.NET Framework 4″(클라이언트 프로필 아님)로 변경해야합니다.


답변

Visual Studio 2015에는 이제 SolutionFile솔루션 파일을 구문 분석하는 데 사용할 수 있는 공개적으로 액세스 가능한 클래스가 있습니다.

using Microsoft.Build.Construction;
var _solutionFile = SolutionFile.Parse(path);

이 클래스는 Microsoft.Build.dll 14.0.0.0 어셈블리에 있습니다. 제 경우에는 다음 위치에 있습니다.

C:\Program Files (x86)\Reference Assemblies\Microsoft\MSBuild\v14.0\Microsoft.Build.dll

이것을 지적한 Phil 에게 감사합니다 !


답변

아직이 문제에 대한 해결책을 찾고있는 사람이 있는지는 모르겠지만 필요한 작업을 수행하는 것으로 보이는 프로젝트를 실행했습니다.
https://slntools.codeplex.com/
이 도구의 기능 중 하나는 여러 솔루션을 함께 병합하는 것입니다.


답변

JetBrains (Resharper의 제작자)는 어셈블리에서 공개 sln 구문 분석 기능을 가지고 있습니다 (반영이 필요하지 않음). 아마도 여기에서 제안 된 기존 오픈 소스 솔루션 (ReGex 해킹은 제외)보다 더 강력 할 것입니다. 당신이해야 할 일은 :

  • 다운로드 ReSharper에서 명령 행 도구를 (무료).
  • 프로젝트에 대한 참조로 다음을 추가하십시오.
    • JetBrains.Platform.ProjectModel
    • JetBrains.Platform.Util
    • JetBrains.Platform.Interop.WinApi

라이브러리는 문서화되어 있지 않지만 Reflector (또는 실제로 dotPeek)는 당신의 친구입니다. 예를 들면 :

public static void PrintProjects(string solutionPath)
{
    var slnFile = SolutionFileParser.ParseFile(FileSystemPath.Parse(solutionPath));
    foreach (var project in slnFile.Projects)
    {
        Console.WriteLine(project.ProjectName);
        Console.WriteLine(project.ProjectGuid);
        Console.WriteLine(project.ProjectTypeGuid);
        foreach (var kvp in project.ProjectSections)
        {
            Console.WriteLine(kvp.Key);
            foreach (var projectSection in kvp.Value) 
            {
                Console.WriteLine(projectSection.SectionName);
                Console.WriteLine(projectSection.SectionValue);
                foreach (var kvpp in projectSection.Properties)
                {
                    Console.WriteLine(kvpp.Key); 
                    Console.WriteLine(string.Join(",", kvpp.Value));
                }
            }
        }
    }
}


답변

나는 정말로 당신에게 도서관을 제공 할 수 없으며 거기에 존재하는 도서관이 없다고 생각합니다. 그러나 일괄 편집 시나리오에서 .sln 파일을 엉망으로 만드는 데 많은 시간을 보냈으며 Powershell이이 작업에 매우 유용한 도구라는 것을 알았습니다. .SLN 형식은 매우 간단하며 몇 가지 빠르고 더러운 식으로 거의 완전히 구문 분석 할 수 있습니다. 예를 들어

포함 된 프로젝트 파일.

gc ConsoleApplication30.sln | 
  ? { $_ -match "^Project" } | 
  %{ $_ -match ".*=(.*)$" | out-null ; $matches[1] } | 
  %{ $_.Split(",")[1].Trim().Trim('"') }

항상 예쁘지는 않지만 일괄 처리를 수행하는 효과적인 방법입니다.


답변

새 솔루션을 만든 다음 * .sln 파일을 검색하고 다음을 사용하여 새 솔루션으로 가져 오는 Visual Studio 플러그인을 작성하여 솔루션을 자동으로 병합하는 유사한 문제를 해결했습니다.

dte2.Solution.AddFromFile(solutionPath, false);

우리의 문제는 VS가 빌드 순서를 정렬하기를 원한다는 점에서 약간 달랐으므로 가능한 경우 모든 dll 참조를 프로젝트 참조로 변환했습니다.

그런 다음 COM 자동화를 통해 VS를 실행하여이를 빌드 프로세스로 자동화했습니다.

이 솔루션은 약간의 Heath Robinson 이었지만 VS가 편집을 수행한다는 이점이 있었으므로 코드가 sln 파일의 형식에 의존하지 않았습니다. VS 2005에서 2008로, 그리고 다시 2010으로 이동할 때 도움이되었습니다.


답변

모든 것이 훌륭하지만 sln 생성 기능도 얻고 싶었습니다. 위의 코드 스냅 샷에서는 .sln 파일 만 구문 분석하고 있습니다. .sln 파일을 약간 수정하여 sln을 다시 생성 할 수 있다는 점을 제외하면 비슷한 것을 만들고 싶었습니다. . 이러한 경우는 예를 들어 다른 .NET 플랫폼에 대해 동일한 프로젝트를 포팅 할 수 있습니다. 지금은 sln 재생 일 뿐이지 만 나중에 프로젝트로 확장 할 것입니다.

정규 표현식과 네이티브 인터페이스의 힘을 보여주고 싶었습니다. (더 많은 기능을 가진 적은 양의 코드)

업데이트 4.1.2017
.sln 솔루션을 구문 분석하기 위해 별도의 svn 저장소를 만들었습니다 :
https://sourceforge.net/p/syncproj/code/HEAD/tree/

아래는 내 자신의 코드 샘플 스 니펫 (전임자)입니다. 당신은 그들 중 하나를 자유롭게 사용할 수 있습니다.

향후 svn 기반 솔루션 구문 분석 코드가 생성 기능으로 업데이트 될 수도 있습니다.

업데이트 4.2.2017
SVN의 소스 코드는 .sln 생성도 지원합니다.

using System;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Text;


public class Program
{
    [DebuggerDisplay("{ProjectName}, {RelativePath}, {ProjectGuid}")]
    public class SolutionProject
    {
        public string ParentProjectGuid;
        public string ProjectName;
        public string RelativePath;
        public string ProjectGuid;

        public string AsSlnString()
        {
            return "Project(\"" + ParentProjectGuid + "\") = \"" + ProjectName + "\", \"" + RelativePath + "\", \"" + ProjectGuid + "\"";
        }
    }

/// <summary>
/// .sln loaded into class.
/// </summary>
public class Solution
{
    public List<object> slnLines;       // List of either String (line format is not intresting to us), or SolutionProject.

    /// <summary>
    /// Loads visual studio .sln solution
    /// </summary>
    /// <param name="solutionFileName"></param>
    /// <exception cref="System.IO.FileNotFoundException">The file specified in path was not found.</exception>
    public Solution( string solutionFileName )
    {
        slnLines = new List<object>();
        String slnTxt = File.ReadAllText(solutionFileName);
        string[] lines = slnTxt.Split('\n');
        //Match string like: Project("{66666666-7777-8888-9999-AAAAAAAAAAAA}") = "ProjectName", "projectpath.csproj", "{11111111-2222-3333-4444-555555555555}"
        Regex projMatcher = new Regex("Project\\(\"(?<ParentProjectGuid>{[A-F0-9-]+})\"\\) = \"(?<ProjectName>.*?)\", \"(?<RelativePath>.*?)\", \"(?<ProjectGuid>{[A-F0-9-]+})");

        Regex.Replace(slnTxt, "^(.*?)[\n\r]*$", new MatchEvaluator(m =>
            {
                String line = m.Groups[1].Value;

                Match m2 = projMatcher.Match(line);
                if (m2.Groups.Count < 2)
                {
                    slnLines.Add(line);
                    return "";
                }

                SolutionProject s = new SolutionProject();
                foreach (String g in projMatcher.GetGroupNames().Where(x => x != "0")) /* "0" - RegEx special kind of group */
                    s.GetType().GetField(g).SetValue(s, m2.Groups[g].ToString());

                slnLines.Add(s);
                return "";
            }),
            RegexOptions.Multiline
        );
    }

    /// <summary>
    /// Gets list of sub-projects in solution.
    /// </summary>
    /// <param name="bGetAlsoFolders">true if get also sub-folders.</param>
    public List<SolutionProject> GetProjects( bool bGetAlsoFolders = false )
    {
        var q = slnLines.Where( x => x is SolutionProject ).Select( i => i as SolutionProject );

        if( !bGetAlsoFolders )  // Filter away folder names in solution.
            q = q.Where( x => x.RelativePath != x.ProjectName );

        return q.ToList();
    }

    /// <summary>
    /// Saves solution as file.
    /// </summary>
    public void SaveAs( String asFilename )
    {
        StringBuilder s = new StringBuilder();

        for( int i = 0; i < slnLines.Count; i++ )
        {
            if( slnLines[i] is String )
                s.Append(slnLines[i]);
            else
                s.Append((slnLines[i] as SolutionProject).AsSlnString() );

            if( i != slnLines.Count )
                s.AppendLine();
        }

        File.WriteAllText(asFilename, s.ToString());
    }
}


    static void Main()
    {
        String projectFile = @"yourown.sln";

        try
        {
            String outProjectFile = Path.Combine(Path.GetDirectoryName(projectFile), Path.GetFileNameWithoutExtension(projectFile) + "_2.sln");
            Solution s = new Solution(projectFile);
            foreach( var proj in s.GetProjects() )
            {
                Console.WriteLine( proj.RelativePath );
            }

            SolutionProject p = s.GetProjects().Where( x => x.ProjectName.Contains("Plugin") ).First();
            p.RelativePath = Path.Combine( Path.GetDirectoryName(p.RelativePath) , Path.GetFileNameWithoutExtension(p.RelativePath) + "_Variation" + ".csproj");

            s.SaveAs(outProjectFile);

        }
        catch (Exception ex)
        {
            Console.WriteLine("Error: " + ex.Message);
        }
    }
}