/// <summary> /// given a JSON string and the user code, generate a bash file /// </summary> /// <param name="json"></param> /// <param name="userCode"></param> /// <returns></returns> public static ScriptData FromJson(string json, string userCode) { ScriptData scriptData = new ScriptData(); bool oldGenerateBashScript = scriptData.GenerateBashScript; try { scriptData = JsonConvert.DeserializeObject <ScriptData>(json); // // Serialize is OptIn, deserialize is not. so these will be reset // scriptData.JSON = json; scriptData.UserCode = userCode; scriptData.GenerateBashScript = true; scriptData.UpdateOnPropertyChanged = false; scriptData.ToBash(); //this will set the JSON return(scriptData); } catch (Exception e) { // // the user should expect to have the JSON still there and not lose their code // when they type incorrect JSON - setting these here allows them to fix the error // and get back to the state they were in. scriptData.JSON = json; scriptData.UserCode = userCode; scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Fatal, "Exception caught when parsing JSON.")); scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Fatal, $"ExceptionInfo: {e.Message}")); return(scriptData); } finally { scriptData.GenerateBashScript = oldGenerateBashScript; scriptData.UpdateOnPropertyChanged = true; } }
/// <summary> /// Given a bash file, create a ScriptData object. This is the "parse a bash script" function /// </summary> /// <param name="bash"></param> public static ScriptData FromBash(string input) { ScriptData scriptData = new ScriptData(); bool oldGenerateBashScript = scriptData.GenerateBashScript; try { scriptData.ClearParseErrors(); scriptData.UpdateOnPropertyChanged = false; // this flag stops the NotifyPropertyChanged events from firing scriptData.GenerateBashScript = false; // this flag tells everything that we are in the process of parsing scriptData.BashScript = input; // // make sure that we deal with the case of getting a file with EOL == \n\r. we only want \n // I've also had scenarios where I get only \r...fix those too. if (input.IndexOf("\n") != -1) { // // we have some new lines, just kill the \r if (input.IndexOf("\r") != -1) { input = input.Replace("\r", string.Empty); } } else if (input.IndexOf("\r") != -1) { // no \n, but we have \r input = input.Replace("\r", "\n"); } else { // no \r and no \n scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Fatal, noNewLines)); return(scriptData); } // // make sure the file doesn't have GIT merge conflicts if (input.IndexOf("<<<<<<< HEAD") != -1) { scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Fatal, unMergedGitFile)); return(scriptData); } /* * The general layout of the file is * #!/bin/bash # bashWizard version <version> # <BashWizard Functions> # --- BEGIN USER CODE --- # # --- END USER CODE --- # <optional BashWizard code> # # the general parse strategy is to separate the user code and then to parse the Bash Wizard Functions to find all the parameter information # we *never* want to touch the user code # */ string[] userComments = new string[] { "# --- BEGIN USER CODE ---", "# --- END USER CODE ---" }; string[] sections = input.Split(userComments, StringSplitOptions.RemoveEmptyEntries); string bashWizardCode = ""; switch (sections.Length) { case 0: // // this means we couldn't find any of the comments -- treat this as a non-BW file scriptData.UserCode = input.Trim(); // make it all user code scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Warning, missingComments)); scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Warning, addingComments)); return(scriptData); case 1: scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Fatal, missingOneUserComment)); scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Fatal, pleaseFix)); return(scriptData); case 2: case 3: bashWizardCode = sections[0]; scriptData.UserCode = sections[1].Trim(); // ignore section[2], it is code after the "# --- END USER CODE ---" that will be regenerated break; default: scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Fatal, tooManyUserComments)); return(scriptData); } // // first look for the bash version string versionLine = "# bashWizard version "; int index = bashWizardCode.IndexOf(versionLine); double userBashVersion = 0.1; string[] lines = null; string line = ""; bool foundDescription = false; if (index > 0) { bool ret = double.TryParse(bashWizardCode.Substring(index + versionLine.Length, 5), out userBashVersion); if (!ret) { ret = double.TryParse(bashWizardCode.Substring(index + versionLine.Length, 3), out userBashVersion); // 0.9 is a version i have out there... if (!ret) { scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Warning, missingVersionInfo)); } } } scriptData.EchoInput = (bashWizardCode.IndexOf(" echoInput") > 0); // // find the usage() function and parse it out - this gives us the 4 properties in the ParameterItem below if (scriptData.GetStringBetween(bashWizardCode, "usage() {", "}", out string bashFragment) == false) { scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Fatal, bashFragment)); } else { bashFragment = bashFragment.Replace("echoWarning", "echo"); bashFragment = bashFragment.Replace("\n", ""); lines = bashFragment.Split(new string[] { "echo ", "\"" }, StringSplitOptions.RemoveEmptyEntries); line = ""; foreach (string l in lines) { line = l.Trim(); if (line == "") { continue; } if (line == "exit 1") { break; } if (!foundDescription) { /* * it look like: * * function usage() { * echoWarning * echo "<description>" * ... * * } * * but the echoWarning isn't always there -- only if the --input-file option was specified. * */ if (line.StartsWith("Parameters can be passed in the command line or in the input file.")) { continue; } // // if the description is black, the next line echo's the usage -- so if we do NOT find the Usage statement // we have found the description. and in any case, if the Description isn't there by now, it isn't there // so always set the flag saying we found it. if (!line.StartsWith("Usage: $0")) { scriptData.Description = line.TrimEnd(); } foundDescription = true; continue; } if (line.Substring(0, 1) == "-") // we have a parameter! { string[] paramTokens = line.Split(new string[] { " ", "|" }, StringSplitOptions.RemoveEmptyEntries); string description = ""; for (int i = 3; i < paramTokens.Length; i++) { description += paramTokens[i] + " "; } description = description.Trim(); ParameterItem parameterItem = new ParameterItem() { ShortParameter = paramTokens[0].Trim(), LongParameter = paramTokens[1].Trim(), RequiredParameter = (paramTokens[2].Trim() == "Required") ? true : false, Description = description }; scriptData.Parameters.Add(parameterItem); } } } // // parse the echoInput() function to get script name - don't fail parsing on this one bashFragment = ""; if (scriptData.GetStringBetween(bashWizardCode, "echoInput() {", "parseInput()", out bashFragment)) { lines = bashFragment.Split('\n'); foreach (string l in lines) { line = l.Trim(); if (line == "") { continue; } // // the line is in the form of: "echo "<scriptName>:" if (scriptData.GetStringBetween(line, "echo \"", ":", out string name)) { scriptData.ScriptName = name; } break; } } // // next parse out the "parseInput" function to get "valueWhenSet" and the "VariableName" bashFragment = ""; if (scriptData.GetStringBetween(bashWizardCode, "eval set -- \"$PARSED\"", "--)", out bashFragment) == false) { scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Warning, bashFragment)); } else { lines = bashFragment.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); for (index = 0; index < lines.Length; index++) { line = lines[index].Trim(); if (line == "") { continue; } if (line.Substring(0, 1) == "-") // we have a parameter! { string[] paramTokens = lines[index + 1].Trim().Split(new char[] { '=' }); if (paramTokens.Length != 2) { scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Warning, $"When parsing the parseInput() function to get the variable names, encountered the line {lines[index + 1].Trim()} which doesn't parse. It should look like varName=$2 or the like.")); } string[] nameTokens = line.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries); if (nameTokens.Length != 2) // the first is the short param, second long param, and third is empty { scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Warning, $"When parsing the parseInput() function to get the variable names, encountered the line {lines[index].Trim()} which doesn't parse. It should look like \"-l | --long-name)\" or the like.")); } // nameTokens[1] looks like "--long-param) string longParam = nameTokens[1].Substring(3, nameTokens[1].Length - 4); ParameterItem param = scriptData.FindParameterByLongName(longParam); if (param == null) { scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Warning, $"When parsing the parseInput() function to get the variable names, found a long parameter named {longParam} which was not found in the usage() function")); } else { param.VariableName = paramTokens[0].Trim(); param.ValueIfSet = paramTokens[1].Trim(); if (lines[index + 2].Trim() == "shift 1") { param.RequiresInputString = false; } else if (lines[index + 2].Trim() == "shift 2") { param.RequiresInputString = true; } else { scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Warning, $"When parsing the parseInput() function to see if {param.VariableName} requires input, found this line: {lines[index + 1]} which does not parse. it should either be \"shift 1\" or \"shift 2\"")); } } index += 2; } } } // the last bit of info to figure out is the default value -- find these with a comment if (scriptData.GetStringBetween(bashWizardCode, "# input variables", "parseInput", out bashFragment) == false) { scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Fatal, bashFragment)); } else { // throw away the "declare " bashFragment = bashFragment.Replace("declare ", ""); lines = bashFragment.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (string l in lines) { line = l.Trim(); if (line == "") { continue; } if (line.StartsWith("#")) { continue; } string[] varTokens = line.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries); if (varTokens.Length == 0 || varTokens.Length > 2) { scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Warning, $"When parsing the variable declarations between the \"# input variables\" comment and the \"parseInput\" calls, the line {line} was encountered that didn't parse. it should be in the form of varName=Default")); } string varName = varTokens[0].Trim(); ParameterItem param = scriptData.FindParameterByVarName(varName); if (param == null) { scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Warning, $"When parsing the variable declarations between the \"# input variables\" comment and the \"parseInput\" calls, found a variable named {varName} which was not found in the usage() function")); scriptData.ParseErrors.Add(new ParseErrorInfo(ErrorLevel.Information, $"\"{line}\" will be removed from the script. If you want to declare it, put the declaration inside the user code comments")); } else { param.Default = varTokens.Length == 2 ? varTokens[1].Trim() : ""; // in bash "varName=" is a valid declaration } } } return(scriptData); } finally { // // need to update everything that might have been changed by the parse scriptData.UpdateOnPropertyChanged = true; // force events to fire scriptData.NotifyPropertyChanged("Description"); scriptData.NotifyPropertyChanged("ScriptName"); // "BashScript" also updates the ToggleButtons scriptData.GenerateBashScript = oldGenerateBashScript; scriptData.NotifyPropertyChanged("BashScript"); // // now go from Parameters back to bash script, unless there are fatal errors // if there are, it will stay as the input set at the top of the FromBash() function if (!scriptData.HasFatalErrors) { scriptData.ToBash(); } } }