diff --git a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/ScriptBuilder.java b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/ScriptBuilder.java index a92a4708f8..d34bc84bc3 100644 --- a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/ScriptBuilder.java +++ b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/ScriptBuilder.java @@ -50,7 +50,10 @@ public class ScriptBuilder { Map> switchExec = Maps.newHashMap(); @VisibleForTesting - Map variables = Maps.newHashMap(); + List variableScopes = Lists.newArrayList(); + + @VisibleForTesting + Map functions = Maps.newHashMap(); @VisibleForTesting List variablesToUnset = Lists.newArrayList("path", "javaHome", "libraryPath"); @@ -91,8 +94,10 @@ public class ScriptBuilder { /** * Exports a variable inside the script */ - public ScriptBuilder export(String name, String value) { - variables.put(checkNotNull(name, "name"), checkNotNull(value, "value")); + public ScriptBuilder addEnvironmentVariableScope(String scopeName, Map variables) { + variableScopes.add(checkNotNull(scopeName, "scopeName")); + functions.put(scopeName, Utils.writeFunction(scopeName, Utils + .writeVariableExporters(checkNotNull(variables, "variables")))); return this; } @@ -108,22 +113,30 @@ public class ScriptBuilder { * whether to write a cmd or bash script. */ public String build(final OsFamily osFamily) { + final Map tokenValueMap = ShellToken.tokenValueMap(osFamily); StringBuilder builder = new StringBuilder(); builder.append(ShellToken.SHEBANG.to(osFamily)); + builder.append(Utils.writeScriptInit(osFamily)); builder.append(Utils.writeUnsetVariables(Lists.newArrayList(Iterables.transform( variablesToUnset, new Function() { - @Override public String apply(String from) { - if (ShellToken.tokenValueMap(osFamily).containsKey(from + "Variable")) - return Utils.FUNCTION_UPPER_UNDERSCORE_TO_LOWER_CAMEL.apply(ShellToken - .tokenValueMap(osFamily).get(from + "Variable")); + if (tokenValueMap.containsKey(from + "Variable")) + return Utils.FUNCTION_UPPER_UNDERSCORE_TO_LOWER_CAMEL.apply(tokenValueMap + .get(from + "Variable")); return from; } })), osFamily)); + if (functions.size() > 0) { + builder.append(ShellToken.BEGIN_FUNCTIONS.to(osFamily)); + builder.append(Utils.writeFunctionFromResource("abort", osFamily)); + for (String function : functions.values()) { + builder.append(Utils.replaceTokens(function, tokenValueMap)); + } + builder.append(ShellToken.END_FUNCTIONS.to(osFamily)); + } builder.append(Utils.writeZeroPath(osFamily)); - builder.append(Utils.writeVariableExporters(variables, osFamily)); for (Entry> entry : switchExec.entrySet()) { builder.append(Utils.writeSwitch(entry.getKey(), entry.getValue(), osFamily)); } diff --git a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/domain/ShellToken.java b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/domain/ShellToken.java index 88488b9efe..5aa1f555ca 100644 --- a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/domain/ShellToken.java +++ b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/domain/ShellToken.java @@ -39,7 +39,26 @@ import com.google.common.collect.Maps; */ public enum ShellToken { - FS, PS, LF, SH, SOURCE, REM, RETURN, ARGS, VARSTART, VAREND, SHEBANG, LIBRARY_PATH_VARIABLE; + FS, PS, + + /** + * If variable values need to be quoted when they include spaces, this will contain quotation + * mark + */ + VQ, + /** + * Left hand side of the function declaration directly before the name of the function. + */ + FNCL, + /** + * Right hand side of the function declaration directly after the name of the function. opens the + * code block + */ + FNCR, + /** + * End the function. exits successfully and closes the code block. + */ + FNCE, BEGIN_FUNCTIONS, END_FUNCTIONS, EXPORT, LF, SH, SOURCE, REM, RETURN, ARGS, VARSTART, VAREND, SHEBANG, LIBRARY_PATH_VARIABLE; private static final Map> familyToTokenValueMap = new MapMaker() .makeComputingMap(new Function>() { @@ -70,6 +89,28 @@ public enum ShellToken { case UNIX: return "/"; } + case FNCL: + switch (family) { + case WINDOWS: + return ":"; + case UNIX: + return "function "; + } + + case FNCR: + switch (family) { + case WINDOWS: + return "\r\n"; + case UNIX: + return " {\n"; + } + case FNCE: + switch (family) { + case WINDOWS: + return " exit /b 0\r\n"; + case UNIX: + return " return 0\n}\n"; + } case PS: switch (family) { case WINDOWS: @@ -77,6 +118,34 @@ public enum ShellToken { case UNIX: return ":"; } + case VQ: + switch (family) { + case WINDOWS: + return ""; + case UNIX: + return "\""; + } + case BEGIN_FUNCTIONS: + switch (family) { + case WINDOWS: + return "GOTO FUNCTION_END\r\n"; + case UNIX: + return ""; + } + case END_FUNCTIONS: + switch (family) { + case WINDOWS: + return ":FUNCTION_END\r\n"; + case UNIX: + return ""; + } + case EXPORT: + switch (family) { + case WINDOWS: + return "set"; + case UNIX: + return "export"; + } case RETURN: switch (family) { case WINDOWS: diff --git a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/util/Utils.java b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/util/Utils.java index 5400c02238..451426c692 100644 --- a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/util/Utils.java +++ b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/util/Utils.java @@ -23,6 +23,7 @@ */ package org.jclouds.scriptbuilder.util; +import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -33,10 +34,13 @@ import org.jclouds.scriptbuilder.domain.OsFamily; import org.jclouds.scriptbuilder.domain.ShellToken; import com.google.common.base.CaseFormat; +import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.google.common.io.CharStreams; +import com.google.common.io.Resources; /** * Utilities used to build init scripts. @@ -95,9 +99,6 @@ public class Utils { return builder.toString(); } - public static final Map OS_TO_EXPORTER_PATTERN = ImmutableMap.of( - OsFamily.UNIX, "export {key}=\"{value}\"\n", OsFamily.WINDOWS, "set {key}={value}\r\n"); - /** * converts a map into variable exports relevant to the specified platform. *

@@ -112,15 +113,43 @@ public class Utils { */ public static String writeVariableExporters(Map variablesInLowerCamelCase, OsFamily family) { + return replaceTokens(writeVariableExporters(variablesInLowerCamelCase), ShellToken + .tokenValueMap(family)); + } + + /** + * converts a map into variable exporters in shell intermediate language. + * + * @param variablesInLowerCamelCase + * lower camel keys to values + */ + public static String writeVariableExporters(Map variablesInLowerCamelCase) { StringBuilder initializers = new StringBuilder(); for (Entry entry : variablesInLowerCamelCase.entrySet()) { String key = CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, entry.getKey()); - initializers.append(replaceTokens(OS_TO_EXPORTER_PATTERN.get(family), ImmutableMap.of( - "key", key, "value", entry.getValue()))); + initializers.append(String.format("{export} %s={vq}%s{vq}{lf}", key, entry.getValue())); } return initializers.toString(); } + public static String writeFunction(String function, String source, OsFamily family) { + return replaceTokens(writeFunction(function, source), ShellToken.tokenValueMap(family)); + } + + public static String writeFunctionFromResource(String function, OsFamily family) { + try { + return CharStreams.toString(Resources.newReaderSupplier(Resources.getResource(String + .format("functions/%s.%s", function, ShellToken.SH.to(family))), Charsets.UTF_8)); + } catch (IOException e) { + // TODO + throw new RuntimeException(e); + } + } + + public static String writeFunction(String function, String source) { + return String.format("{fncl}%s{fncr}%s{fnce}", function, source.replaceAll("^", " ")); + } + public static final Map OS_TO_POSITIONAL_VAR_PATTERN = ImmutableMap.of( OsFamily.UNIX, "set {key}=$1\nshift\n", OsFamily.WINDOWS, "set {key}=%1\r\nshift\r\n"); @@ -185,6 +214,16 @@ public class Utils { return OS_TO_ZERO_PATH.get(family); } + public static final Map OS_TO_SCRIPT_INIT = ImmutableMap.of(OsFamily.UNIX, + "set +u\nshopt -s xpg_echo\nshopt -s expand_aliases\n", OsFamily.WINDOWS, ""); + + /** + * sets up shell options needed for script execution + */ + public static String writeScriptInit(OsFamily family) { + return OS_TO_SCRIPT_INIT.get(family); + } + public static final Map OS_TO_SWITCH_PATTERN = ImmutableMap.of(OsFamily.UNIX, "case ${variable} in\n", OsFamily.WINDOWS, "goto CASE%{variable}\r\n"); diff --git a/scriptbuilder/src/main/resources/functions/abort.bash b/scriptbuilder/src/main/resources/functions/abort.bash new file mode 100644 index 0000000000..148d83eda7 --- /dev/null +++ b/scriptbuilder/src/main/resources/functions/abort.bash @@ -0,0 +1,4 @@ +function abort { + echo "aborting: $@" 1>&2 + set -u +} diff --git a/scriptbuilder/src/main/resources/functions/abort.cmd b/scriptbuilder/src/main/resources/functions/abort.cmd new file mode 100644 index 0000000000..9ca47282a3 --- /dev/null +++ b/scriptbuilder/src/main/resources/functions/abort.cmd @@ -0,0 +1,3 @@ +:abort + echo aborting: %EXCEPTION% + exit /b 1 diff --git a/scriptbuilder/src/main/resources/functions/sourceEnvFile.bash b/scriptbuilder/src/main/resources/functions/sourceEnvFile.bash new file mode 100644 index 0000000000..412f88128a --- /dev/null +++ b/scriptbuilder/src/main/resources/functions/sourceEnvFile.bash @@ -0,0 +1,12 @@ +function sourceEnvFile { + [ $# -eq 1 ] || { + abort "sourceEnvFile requires a parameter of the file to source" + return 1 + } + local ENV_FILE="$1"; shift + . "$ENV_FILE" || { + abort "Please append 'return 0' to the end of '$ENV_FILE'" + return 1 + } + return 0 +} \ No newline at end of file diff --git a/scriptbuilder/src/main/resources/functions/sourceEnvFile.cmd b/scriptbuilder/src/main/resources/functions/sourceEnvFile.cmd new file mode 100644 index 0000000000..5d286a969b --- /dev/null +++ b/scriptbuilder/src/main/resources/functions/sourceEnvFile.cmd @@ -0,0 +1,13 @@ +:sourceEnvFile + set ENV_FILE=%1 + shift + if not defined ENV_FILE ( + set EXCEPTION=sourceEnvFile requires a parameter of the file to source + exit /b 1 + ) + call %ENV_FILE% + if errorlevel 1 ( + set EXCEPTION=Please append 'exit /b 0' to the end of '%ENV_FILE%' + exit /b 1 + ) + exit /b 0 diff --git a/scriptbuilder/src/main/resources/functions/validateEnvFile.bash b/scriptbuilder/src/main/resources/functions/validateEnvFile.bash new file mode 100644 index 0000000000..d2c26bec7c --- /dev/null +++ b/scriptbuilder/src/main/resources/functions/validateEnvFile.bash @@ -0,0 +1,24 @@ +function validateEnvFile { + [ $# -eq 1 ] || { + abort "validateEnvFile requires a parameter of the file to source" + return 1 + } + local ENV_FILE="$1"; shift + [ -f "$ENV_FILE" ] || { + abort "env file '$ENV_FILE' does not exist" + return 1 + } + [ -r "$ENV_FILE" ] || { + abort "env file '$ENV_FILE' is not readable" + return 1 + } + grep '\' "$ENV_FILE" > /dev/null && { + abort "please remove the 'exit' statement from env file '$ENV_FILE'" + return 1 + } + [ -x "$ENV_FILE" ] && { + abort "please remove the execute permission from env file '$ENV_FILE'" + return 1 + } + return 0 +} \ No newline at end of file diff --git a/scriptbuilder/src/main/resources/functions/validateEnvFile.cmd b/scriptbuilder/src/main/resources/functions/validateEnvFile.cmd new file mode 100644 index 0000000000..25b5903792 --- /dev/null +++ b/scriptbuilder/src/main/resources/functions/validateEnvFile.cmd @@ -0,0 +1,12 @@ +:validateEnvFile + set ENV_FILE=%1 + shift + if not defined ENV_FILE ( + set EXCEPTION=validateEnvFile requires a parameter of the file to source + exit /b 1 + ) + if not exist "%ENV_FILE%" ( + set EXCEPTION=env file '%ENV_FILE%' does not exist + exit /b 1 + ) + exit /b 0 diff --git a/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/ScriptBuilderTest.java b/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/ScriptBuilderTest.java index ed2c29137b..4ac371c966 100644 --- a/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/ScriptBuilderTest.java +++ b/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/ScriptBuilderTest.java @@ -23,8 +23,8 @@ import com.google.common.io.Resources; public class ScriptBuilderTest { ScriptBuilder testScriptBuilder = new ScriptBuilder().switchOn("1", - ImmutableMap.of("start", "echo started", "stop", "echo stopped")).export("javaHome", - "/apps/jdk1.6"); + ImmutableMap.of("start", "echo started", "stop", "echo stopped")) + .addEnvironmentVariableScope("default", ImmutableMap.of("javaHome", "/apps/jdk1.6")); @Test public void testBuildSimpleWindows() throws MalformedURLException, IOException { @@ -57,20 +57,19 @@ public class ScriptBuilderTest { @Test public void testExport() { ScriptBuilder builder = new ScriptBuilder(); - builder.export("javaHome", "/apps/jdk1.6"); - assertEquals(builder.variables, ImmutableMap.of("javaHome", "/apps/jdk1.6")); - + builder.addEnvironmentVariableScope("default", ImmutableMap.of("javaHome", "/apps/jdk1.6")); + assertEquals(builder.functions, ImmutableMap.of("default", "{fncl}default{fncr} {export} JAVA_HOME={vq}/apps/jdk1.6{vq}{lf}{fnce}")); } @Test public void testNoExport() { ScriptBuilder builder = new ScriptBuilder(); - assertEquals(builder.variables.size(), 0); + assertEquals(builder.functions.size(), 0); } @Test(expectedExceptions = NullPointerException.class) public void testExportNPE() { - new ScriptBuilder().export(null, null); + new ScriptBuilder().addEnvironmentVariableScope(null, null); } } diff --git a/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/domain/ShellTokenTest.java b/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/domain/ShellTokenTest.java index 8d0c5b2fda..feef4e6de1 100644 --- a/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/domain/ShellTokenTest.java +++ b/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/domain/ShellTokenTest.java @@ -43,7 +43,9 @@ public class ShellTokenTest { Map expected = new ImmutableMap.Builder().put("fs", "/").put( "ps", ":").put("lf", "\n").put("sh", "bash").put("source", ".").put("rem", "#").put( "args", "$@").put("varstart", "$").put("return", "return").put("varend", "").put( - "libraryPathVariable", "LD_LIBRARY_PATH").put("shebang", "#!/bin/bash\n").build(); + "libraryPathVariable", "LD_LIBRARY_PATH").put("shebang", "#!/bin/bash\n").put("vq", + "\"").put("beginFunctions", "").put("endFunctions", "").put("fncl", "function ") + .put("fncr", " {\n").put("fnce", " return 0\n}\n").put("export", "export").build(); assertEquals(ShellToken.tokenValueMap(OsFamily.UNIX), expected); } @@ -53,7 +55,10 @@ public class ShellTokenTest { .put("ps", ";").put("lf", "\r\n").put("sh", "cmd").put("source", "@call").put("rem", "@rem").put("args", "%*").put("varstart", "%").put("varend", "%").put( "libraryPathVariable", "PATH").put("return", "exit /b").put("shebang", - "@echo off\r\n").build(); + "@echo off\r\n").put("vq", "").put("beginFunctions", + "GOTO FUNCTION_END\r\n").put("endFunctions", ":FUNCTION_END\r\n").put( + "fncl", ":").put("fncr", "\r\n").put("fnce", " exit /b 0\r\n").put( + "export", "set").build(); assertEquals(ShellToken.tokenValueMap(OsFamily.WINDOWS), expected); } diff --git a/scriptbuilder/src/test/resources/test_script.bash b/scriptbuilder/src/test/resources/test_script.bash index e7917baf4c..63ae6f79cd 100644 --- a/scriptbuilder/src/test/resources/test_script.bash +++ b/scriptbuilder/src/test/resources/test_script.bash @@ -1,7 +1,17 @@ #!/bin/bash +set +u +shopt -s xpg_echo +shopt -s expand_aliases unset PATH JAVA_HOME LD_LIBRARY_PATH +function abort { + echo "aborting: $@" 1>&2 + set -u +} +function default { + export JAVA_HOME="/apps/jdk1.6" + return 0 +} export PATH=/usr/ucb/bin:/bin:/usr/bin:/usr/sbin -export JAVA_HOME="/apps/jdk1.6" case $1 in start) echo started diff --git a/scriptbuilder/src/test/resources/test_script.cmd b/scriptbuilder/src/test/resources/test_script.cmd index 95a7598e77..ac6b081772 100644 --- a/scriptbuilder/src/test/resources/test_script.cmd +++ b/scriptbuilder/src/test/resources/test_script.cmd @@ -2,8 +2,15 @@ set PATH= set JAVA_HOME= set PATH= +GOTO FUNCTION_END +:abort + echo aborting: %EXCEPTION% + exit /b 1 +:default + set JAVA_HOME=/apps/jdk1.6 + exit /b 0 +:FUNCTION_END set PATH=c:\windows\;C:\windows\system32 -set JAVA_HOME=/apps/jdk1.6 goto CASE%1 :CASE_start echo started diff --git a/scriptbuilder/src/test/resources/test_script_funcs.cmd b/scriptbuilder/src/test/resources/test_script_funcs.cmd deleted file mode 100644 index ddae1f268b..0000000000 --- a/scriptbuilder/src/test/resources/test_script_funcs.cmd +++ /dev/null @@ -1,27 +0,0 @@ -@echo off - -goto END_FUNCTIONS -:abortFunction - echo Aborting: %EXCEPTION%. - exit /b 1 - -:sourceEnv - set ENV_FILE=%1 - shift - if not defined ENV_FILE ( - set EXCEPTION=Internal error. Called sourceEnv with no file param - exit /b 1 - ) - call %ENV_FILE% - if errorlevel 1 ( - set EXCEPTION=Please end your '%ENV_FILE%' file with the command 'exit /b 0' to enable this script to detect syntax errors. - exit /b 1 - ) - exit /b 0 - -:END_FUNCTIONS - -if exist "%APPENV_SETTINGS_FILE%" ( - call :sourceEnv "%APPENV_SETTINGS_FILE%" - if errorlevel 1 goto abortFunction -)