diff --git a/src/code/PSScriptContents.cs b/src/code/PSScriptContents.cs index c04f1dc49..63f1c4e80 100644 --- a/src/code/PSScriptContents.cs +++ b/src/code/PSScriptContents.cs @@ -15,7 +15,7 @@ public sealed class PSScriptContents /// /// End of file contents for the .ps1 file. /// - public string[] EndOfFileContents { get; private set; } = Utils.EmptyStrArray; + public string[] ScriptContents { get; private set; } = Utils.EmptyStrArray; /// /// End of file contents for the .ps1 file. @@ -38,7 +38,7 @@ public sealed class PSScriptContents /// public PSScriptContents(string[] endOfFileContents) { - EndOfFileContents = endOfFileContents; + ScriptContents = endOfFileContents; ContainsSignature = CheckForSignature(); } @@ -60,7 +60,7 @@ internal void ParseContent(string[] commentLines) { if (commentLines.Length != 0) { - EndOfFileContents = commentLines; + ScriptContents = commentLines; ContainsSignature = CheckForSignature(); } } @@ -74,7 +74,7 @@ internal void ParseContent(string[] commentLines) internal string[] EmitContent() { RemoveSignatureString(); - return EndOfFileContents; + return ScriptContents; } #endregion @@ -86,11 +86,12 @@ internal string[] EmitContent() /// private bool CheckForSignature() { - for (int i = 0; i < EndOfFileContents.Length; i++) + for (int i = 0; i < ScriptContents.Length; i++) { - if (String.Equals(EndOfFileContents[i], signatureStartString, StringComparison.InvariantCultureIgnoreCase)) + if (String.Equals(ScriptContents[i], signatureStartString, StringComparison.InvariantCultureIgnoreCase)) { _signatureStartIndex = i; + break; } } @@ -105,9 +106,13 @@ private void RemoveSignatureString() { if (ContainsSignature) { - string[] newEndOfFileContents = new string[EndOfFileContents.Length - _signatureStartIndex]; - Array.Copy(EndOfFileContents, newEndOfFileContents, _signatureStartIndex); - EndOfFileContents = newEndOfFileContents; + // The script signature comment block always appears at the end of the script file, + // so its start location becomes the end of the content section after the signature + // comment block is removed, and is also the length of the content section minus the + // signature block. + string[] contentsWithoutSignature = new string[_signatureStartIndex]; + Array.Copy(ScriptContents, contentsWithoutSignature, _signatureStartIndex); + ScriptContents = contentsWithoutSignature; ContainsSignature = false; } diff --git a/src/code/PSScriptFileInfo.cs b/src/code/PSScriptFileInfo.cs index f3c4f3b5b..dfeee2121 100644 --- a/src/code/PSScriptFileInfo.cs +++ b/src/code/PSScriptFileInfo.cs @@ -132,14 +132,15 @@ internal static bool TryParseScriptFileContents( while (j < fileContents.Length) { string blockLine = fileContents[j]; + psScriptInfoCommentContent.Add(blockLine); if (blockLine.StartsWith("#>")) { + reachedPSScriptInfoCommentEnd = true; i = j + 1; break; } - psScriptInfoCommentContent.Add(blockLine); j++; } @@ -159,6 +160,7 @@ internal static bool TryParseScriptFileContents( while (j < fileContents.Length) { string blockLine = fileContents[j]; + if (blockLine.StartsWith("#>")) { reachedHelpInfoCommentEnd = true; @@ -166,7 +168,7 @@ internal static bool TryParseScriptFileContents( endOfFileContentsStartIndex = i; break; } - + helpInfoCommentContent.Add(blockLine); j++; } diff --git a/src/code/PSScriptHelp.cs b/src/code/PSScriptHelp.cs index 9964d2159..d6cef24ba 100644 --- a/src/code/PSScriptHelp.cs +++ b/src/code/PSScriptHelp.cs @@ -18,52 +18,12 @@ public sealed class PSScriptHelp /// /// The description of the script. /// - public string Description { get; private set; } + public string Description { get; private set; } = String.Empty; /// - /// The synopsis of the script. + /// This contains all help content aside from Description /// - public string Synopsis { get; private set; } - - /// - /// The example(s) relating to the script's usage. - /// - public string[] Example { get; private set; } = Utils.EmptyStrArray; - - /// - /// The inputs to the script. - /// - public string[] Inputs { get; private set; } = Utils.EmptyStrArray; - - /// - /// The outputs to the script. - /// - public string[] Outputs { get; private set; } = Utils.EmptyStrArray; - - /// - /// The notes for the script. - /// - public string[] Notes { get; private set; } = Utils.EmptyStrArray; - - /// - /// The links for the script. - /// - public string[] Links { get; private set; } = Utils.EmptyStrArray; - - /// - /// The components for the script. - /// - public string[] Component { get; private set; } = Utils.EmptyStrArray; - - /// - /// The roles for the script. - /// - public string[] Role { get; private set; } = Utils.EmptyStrArray; - - /// - /// The functionality components for the script. - /// - public string[] Functionality { get; private set; } = Utils.EmptyStrArray; + public List HelpContent { get; private set; } = new List(); #endregion @@ -74,35 +34,7 @@ public sealed class PSScriptHelp /// public PSScriptHelp (string description) { - this.Description = description; - } - - /// - /// This constructor takes values for description as well as other properties and creates a new PSScriptHelp instance. - /// Currently, the New-PSScriptFileInfo and Update-PSScriptFileInfo cmdlets don't support the user providing these values. - /// - public PSScriptHelp ( - string description, - string synopsis, - string[] example, - string[] inputs, - string[] outputs, - string[] notes, - string[] links, - string[] component, - string[] role, - string[] functionality) - { - this.Description = description; - this.Synopsis = synopsis; - this.Example = example; - this.Inputs = inputs; - this.Outputs = outputs; - this.Notes = notes; - this.Links = links; - this.Component = component; - this.Role = role; - this.Functionality = functionality; + Description = description; } /// @@ -121,33 +53,91 @@ internal PSScriptHelp() {} /// internal bool ParseContentIntoObj(string[] commentLines, out ErrorRecord error) { - bool successfullyParsed = true; - string[] spaceDelimeter = new string[]{" "}; - string[] newlineDelimeter = new string[]{Environment.NewLine}; + error = null; - // parse content into a hashtable - Hashtable parsedHelpMetadata = Utils.ParseCommentBlockContent(commentLines); + // Parse content into a hashtable. + Hashtable parsedHelpMetadata = ParseHelpContentHelper(commentLines); - if (!ValidateParsedContent(parsedHelpMetadata, out error)) + if (!ValidateParsedContent(parsedHelpMetadata, out ErrorRecord validationError)) { + error = validationError; return false; } - // populate object - Description = (string) parsedHelpMetadata["DESCRIPTION"]; - Synopsis = (string) parsedHelpMetadata["SYNOPSIS"] ?? String.Empty; - Example = Utils.GetStringArrayFromString(newlineDelimeter, (string) parsedHelpMetadata["EXAMPLE"]); - Inputs = Utils.GetStringArrayFromString(spaceDelimeter, (string) parsedHelpMetadata["INPUT"]); - Outputs = Utils.GetStringArrayFromString(spaceDelimeter, (string) parsedHelpMetadata["OUTPUTS"]); - Notes = Utils.GetStringArrayFromString(spaceDelimeter, (string) parsedHelpMetadata["NOTES"]); - Links = Utils.GetStringArrayFromString(newlineDelimeter, (string) parsedHelpMetadata["LINKS"]); - Component = Utils.GetStringArrayFromString(spaceDelimeter, (string) parsedHelpMetadata["COMPONENT"]); - Role = Utils.GetStringArrayFromString(spaceDelimeter, (string) parsedHelpMetadata["ROLE"]); - Functionality = Utils.GetStringArrayFromString(spaceDelimeter, (string) parsedHelpMetadata["FUNCTIONALITY"]); - - return successfullyParsed; + // Populate object. + List descriptionValue = (List) parsedHelpMetadata["DESCRIPTION"]; + Description = String.Join(Environment.NewLine, descriptionValue); + if (parsedHelpMetadata.ContainsKey("HELPCONTENT")) + { + HelpContent = (List) parsedHelpMetadata["HELPCONTENT"]; + } + + return true; } + /// + /// Parses metadata out of PSScriptCommentInfo comment block's lines (which are passed in) into a hashtable. + /// + public static Hashtable ParseHelpContentHelper(string[] commentLines) + { + /** + Comment lines can look like this: + + .KEY1 value + + .KEY2 value + + .KEY2 value2 + + .KEY3 + value + + .KEY4 value + value continued + + */ + + // Parse out Description and everything else into a bucket list. + + List helpContent = new List(); + List descriptionValue = new List(); + bool parsingDescription = false; + + for(int i = 0; i < commentLines.Length; i++) + { + string line = commentLines[i]; + if (line.Trim().StartsWith(".DESCRIPTION")) + { + parsingDescription = true; + } + else if (line.Trim().StartsWith(".")) + { + parsingDescription = false; + helpContent.Add(line); + } + else if (!String.IsNullOrEmpty(line)) + { + if (parsingDescription) + { + descriptionValue.Add(line); + } + else + { + helpContent.Add(line); + } + } + } + + Hashtable parsedHelpMetadata = new Hashtable(); + parsedHelpMetadata.Add("DESCRIPTION", descriptionValue); + if (helpContent.Count != 0) + { + parsedHelpMetadata.Add("HELPCONTENT", helpContent); + } + + return parsedHelpMetadata; + } + /// /// Valides parsed help info content from the hashtable to ensure required help metadata (Description) is present /// and does not contain empty values. @@ -155,7 +145,7 @@ internal bool ParseContentIntoObj(string[] commentLines, out ErrorRecord error) internal bool ValidateParsedContent(Hashtable parsedHelpMetadata, out ErrorRecord error) { error = null; - if (!parsedHelpMetadata.ContainsKey("DESCRIPTION") || String.IsNullOrEmpty((string) parsedHelpMetadata["DESCRIPTION"]) || String.Equals(((string) parsedHelpMetadata["DESCRIPTION"]).Trim(), String.Empty)) + if (!parsedHelpMetadata.ContainsKey("DESCRIPTION")) { var exMessage = "PSScript file must contain value for Description. Ensure value for Description is passed in and try again."; var ex = new ArgumentException(exMessage); @@ -164,7 +154,18 @@ internal bool ValidateParsedContent(Hashtable parsedHelpMetadata, out ErrorRecor return false; } - if (StringContainsComment((string) parsedHelpMetadata["DESCRIPTION"])) + List descriptionValue = (List) parsedHelpMetadata["DESCRIPTION"]; + string descriptionString = String.Join("", descriptionValue); + if (descriptionValue.Count == 0 || (String.IsNullOrEmpty(descriptionString)) || String.IsNullOrWhiteSpace(descriptionString)) + { + var exMessage = "PSScript file value for Description cannot be null, empty or whitespace. Ensure value for Description meets these conditions and try again."; + var ex = new ArgumentException(exMessage); + var PSScriptInfoMissingDescriptionError = new ErrorRecord(ex, "PSScriptInfoMissingDescription", ErrorCategory.InvalidArgument, null); + error = PSScriptInfoMissingDescriptionError; + return false; + } + + if (StringContainsComment(descriptionString)) { var exMessage = "PSScript file's value for Description cannot contain '<#' or '#>'. Pass in a valid value for Description and try again."; var ex = new ArgumentException(exMessage); @@ -216,60 +217,11 @@ internal string[] EmitContent() psHelpInfoLines.Add($".DESCRIPTION"); psHelpInfoLines.Add($"{Description}{Environment.NewLine}"); - if (!String.IsNullOrEmpty(Synopsis)) - { - psHelpInfoLines.Add($".SYNOPSIS"); - psHelpInfoLines.Add($"{Synopsis}{Environment.NewLine}"); - } - - foreach (string currentExample in Example) - { - psHelpInfoLines.Add($".EXAMPLE"); - psHelpInfoLines.Add($"{currentExample}{Environment.NewLine}"); - } - - foreach (string input in Inputs) - { - psHelpInfoLines.Add($".INPUTS"); - psHelpInfoLines.Add($"{input}{Environment.NewLine}"); - } - - foreach (string output in Outputs) - { - psHelpInfoLines.Add($".OUTPUTS"); - psHelpInfoLines.Add($"{output}{Environment.NewLine}"); - } - - if (Notes.Length > 0) + if (HelpContent.Count != 0) { - psHelpInfoLines.Add($".NOTES"); - psHelpInfoLines.Add($"{String.Join(Environment.NewLine, Notes)}{Environment.NewLine}"); - } - - foreach (string link in Links) - { - psHelpInfoLines.Add($".LINK"); - psHelpInfoLines.Add($"{link}{Environment.NewLine}"); - } - - if (Component.Length > 0) - { - psHelpInfoLines.Add($".COMPONENT"); - psHelpInfoLines.Add($"{String.Join(Environment.NewLine, Component)}{Environment.NewLine}"); + psHelpInfoLines.AddRange(HelpContent); } - if (Role.Length > 0) - { - psHelpInfoLines.Add($".ROLE"); - psHelpInfoLines.Add($"{String.Join(Environment.NewLine, Role)}{Environment.NewLine}"); - } - - if (Functionality.Length > 0) - { - psHelpInfoLines.Add($".FUNCTIONALITY"); - psHelpInfoLines.Add($"{String.Join(Environment.NewLine, Functionality)}{Environment.NewLine}"); - } - psHelpInfoLines.Add("#>"); return psHelpInfoLines.ToArray(); diff --git a/src/code/PSScriptMetadata.cs b/src/code/PSScriptMetadata.cs index ded8088e7..f5cadb1ed 100644 --- a/src/code/PSScriptMetadata.cs +++ b/src/code/PSScriptMetadata.cs @@ -150,7 +150,12 @@ internal bool ParseContentIntoObj(string[] commentLines, out ErrorRecord[] error List msgsList = new List(); // parse content into a hashtable - Hashtable parsedMetadata = Utils.ParseCommentBlockContent(commentLines); + Hashtable parsedMetadata = ParseMetadataContentHelper(commentLines, out errors); + if (errors.Length != 0) + { + return false; + } + if (parsedMetadata.Count == 0) { @@ -220,6 +225,92 @@ internal bool ParseContentIntoObj(string[] commentLines, out ErrorRecord[] error return true; } + /// + /// Parses metadata out of PSScriptCommentInfo comment block's lines (which are passed in) into a hashtable. + /// This comment block cannot have duplicate keys. + /// + public static Hashtable ParseMetadataContentHelper(string[] commentLines, out ErrorRecord[] errors) + { + /** + Comment lines can look like this: + + .KEY1 value + + .KEY2 value + + .KEY3 + value + + .KEY4 value + value continued + + */ + + errors = Array.Empty(); + List errorsList = new List(); + + Hashtable parsedHelpMetadata = new Hashtable(); + char[] spaceDelimeter = new char[]{' '}; + string keyName = ""; + string value = ""; + + for (int i = 1; i < commentLines.Length; i++) + { + string line = commentLines[i]; + + // scenario where line is: .KEY VALUE + // this line contains a new metadata property. + if (line.Trim().StartsWith(".")) + { + // check if keyName was previously populated, if so add this key value pair to the metadata hashtable + if (!String.IsNullOrEmpty(keyName)) + { + if (parsedHelpMetadata.ContainsKey(keyName)) + { + var message = String.Format("PowerShell script '<#PSScriptInfo .. #>' comment block metadata cannot contain duplicate key i.e .KEY"); + var ex = new InvalidOperationException(message); + var psScriptInfoDuplicateKeyError = new ErrorRecord(ex, "psScriptInfoDuplicateKeyError", ErrorCategory.ParserError, null); + errorsList.Add(psScriptInfoDuplicateKeyError); + continue; + } + + parsedHelpMetadata.Add(keyName, value); + } + + // setting count to 2 will get 1st separated string (key) into part[0] and the rest (value) into part[1] if any + string[] parts = line.Trim().TrimStart('.').Split(separator: spaceDelimeter, count: 2); + keyName = parts[0]; + value = parts.Length == 2 ? parts[1] : String.Empty; + } + else if (line.Trim().StartsWith("#>")) + { + // This line signifies end of comment block, so add last recorded key value pair before the comment block ends. + if (!String.IsNullOrEmpty(keyName) && !parsedHelpMetadata.ContainsKey(keyName)) + { + // only add this key value if it hasn't already been added + parsedHelpMetadata.Add(keyName, value); + } + } + else if (!String.IsNullOrEmpty(line)) + { + // scenario where line contains text that is a continuation of value from previously recorded key + // this line does not starting with .KEY, and is also not an empty line. + if (value.Equals(String.Empty)) + { + value += line; + } + else + { + value += Environment.NewLine + line; + } + } + } + + errors = errorsList.ToArray(); + + return parsedHelpMetadata; + } + /// /// Valides parsed metadata content from the hashtable to ensure required metadata (Author, Version, Guid) is present /// and does not contain empty values. diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 977d0be8a..32292d87f 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -1037,74 +1037,6 @@ public static bool TryCreateModuleSpecification( return moduleSpecCreatedSuccessfully; } - /// - /// Parses metadata out of a comment block's lines (which are passed in) into a hashtable. - /// - public static Hashtable ParseCommentBlockContent(string[] commentLines) - { - /** - Comment lines can look like this: - - .KEY1 value - - .KEY2 value - - .KEY3 - value - - .KEY4 value - value continued - - */ - - Hashtable parsedHelpMetadata = new Hashtable(); - string keyName = ""; - string value = ""; - - for (int i = 1; i < commentLines.Count(); i++) - { - string line = commentLines[i]; - - // scenario where line is: .KEY VALUE - // this line contains a new metadata property. - if (line.Trim().StartsWith(".")) - { - // check if keyName was previously populated, if so add this key value pair to the metadata hashtable - if (!String.IsNullOrEmpty(keyName)) - { - parsedHelpMetadata.Add(keyName, value); - } - - string[] parts = line.Trim().TrimStart('.').Split(); - keyName = parts[0]; - value = parts.Count() > 1 ? String.Join(" ", parts.Skip(1)) : String.Empty; - } - else if (!String.IsNullOrEmpty(line)) - { - // scenario where line contains text that is a continuation of value from previously recorded key - // this line does not starting with .KEY, and is also not an empty line. - if (value.Equals(String.Empty)) - { - value += line; - } - else - { - value += Environment.NewLine + line; - } - } - } - - // this is the case where last key value had multi-line value. - // and we've captured it, but still need to add it to hashtable. - if (!String.IsNullOrEmpty(keyName) && !parsedHelpMetadata.ContainsKey(keyName)) - { - // only add this key value if it hasn't already been added - parsedHelpMetadata.Add(keyName, value); - } - - return parsedHelpMetadata; - } - #endregion #region Directory and File