Skip to content

Script file duplicate keys and parsing help comment block #726

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 26, 2022
Merged
23 changes: 14 additions & 9 deletions src/code/PSScriptContents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public sealed class PSScriptContents
/// <summary>
/// End of file contents for the .ps1 file.
/// </summary>
public string[] EndOfFileContents { get; private set; } = Utils.EmptyStrArray;
public string[] ScriptContents { get; private set; } = Utils.EmptyStrArray;

/// <summary>
/// End of file contents for the .ps1 file.
Expand All @@ -38,7 +38,7 @@ public sealed class PSScriptContents
/// </summary>
public PSScriptContents(string[] endOfFileContents)
{
EndOfFileContents = endOfFileContents;
ScriptContents = endOfFileContents;
ContainsSignature = CheckForSignature();
}

Expand All @@ -60,7 +60,7 @@ internal void ParseContent(string[] commentLines)
{
if (commentLines.Length != 0)
{
EndOfFileContents = commentLines;
ScriptContents = commentLines;
ContainsSignature = CheckForSignature();
}
}
Expand All @@ -74,7 +74,7 @@ internal void ParseContent(string[] commentLines)
internal string[] EmitContent()
{
RemoveSignatureString();
return EndOfFileContents;
return ScriptContents;
}

#endregion
Expand All @@ -86,11 +86,12 @@ internal string[] EmitContent()
/// </summary>
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;
}
}

Expand All @@ -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;
}
Expand Down
6 changes: 4 additions & 2 deletions src/code/PSScriptFileInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
}

Expand All @@ -159,14 +160,15 @@ internal static bool TryParseScriptFileContents(
while (j < fileContents.Length)
{
string blockLine = fileContents[j];

if (blockLine.StartsWith("#>"))
{
reachedHelpInfoCommentEnd = true;
i = j + 1;
endOfFileContentsStartIndex = i;
break;
}

helpInfoCommentContent.Add(blockLine);
j++;
}
Expand Down
240 changes: 96 additions & 144 deletions src/code/PSScriptHelp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,52 +18,12 @@ public sealed class PSScriptHelp
/// <summary>
/// The description of the script.
/// </summary>
public string Description { get; private set; }
public string Description { get; private set; } = String.Empty;

/// <summary>
/// The synopsis of the script.
/// This contains all help content aside from Description
/// </summary>
public string Synopsis { get; private set; }

/// <summary>
/// The example(s) relating to the script's usage.
/// </summary>
public string[] Example { get; private set; } = Utils.EmptyStrArray;

/// <summary>
/// The inputs to the script.
/// </summary>
public string[] Inputs { get; private set; } = Utils.EmptyStrArray;

/// <summary>
/// The outputs to the script.
/// </summary>
public string[] Outputs { get; private set; } = Utils.EmptyStrArray;

/// <summary>
/// The notes for the script.
/// </summary>
public string[] Notes { get; private set; } = Utils.EmptyStrArray;

/// <summary>
/// The links for the script.
/// </summary>
public string[] Links { get; private set; } = Utils.EmptyStrArray;

/// <summary>
/// The components for the script.
/// </summary>
public string[] Component { get; private set; } = Utils.EmptyStrArray;

/// <summary>
/// The roles for the script.
/// </summary>
public string[] Role { get; private set; } = Utils.EmptyStrArray;

/// <summary>
/// The functionality components for the script.
/// </summary>
public string[] Functionality { get; private set; } = Utils.EmptyStrArray;
public List<string> HelpContent { get; private set; } = new List<string>();

#endregion

Expand All @@ -74,35 +34,7 @@ public sealed class PSScriptHelp
/// </summary>
public PSScriptHelp (string description)
{
this.Description = description;
}

/// <summary>
/// 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.
/// </summary>
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;
}

/// <summary>
Expand All @@ -121,41 +53,99 @@ internal PSScriptHelp() {}
/// </summary>
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<string> descriptionValue = (List<string>) parsedHelpMetadata["DESCRIPTION"];
Description = String.Join(Environment.NewLine, descriptionValue);
if (parsedHelpMetadata.ContainsKey("HELPCONTENT"))
{
HelpContent = (List<string>) parsedHelpMetadata["HELPCONTENT"];
}

return true;
}

/// <summary>
/// Parses metadata out of PSScriptCommentInfo comment block's lines (which are passed in) into a hashtable.
/// </summary>
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<string> helpContent = new List<string>();
List<string> descriptionValue = new List<string>();
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;
}

/// <summary>
/// Valides parsed help info content from the hashtable to ensure required help metadata (Description) is present
/// and does not contain empty values.
/// </summary>
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);
Expand All @@ -164,7 +154,18 @@ internal bool ValidateParsedContent(Hashtable parsedHelpMetadata, out ErrorRecor
return false;
}

if (StringContainsComment((string) parsedHelpMetadata["DESCRIPTION"]))
List<string> descriptionValue = (List<string>) 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);
Expand Down Expand Up @@ -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();
Expand Down
Loading