UNPKG

@catladder/pipeline

Version:

Panter workflow for cloud CI/CD and DevOps

165 lines (151 loc) 5.17 kB
import { registerGlobalScriptFunction } from "../globalScriptFunctions"; import type { VariableValue } from "../variables/VariableValue"; import { VariableValueContainingReferences } from "../variables/VariableValueContainingReferences"; import { BashExpression } from "./BashExpression"; export type QuotesEscapeMode = "single" | "double"; export type EscapeOptions = { quotes?: QuotesEscapeMode | false; }; /** * escapes a string or bash expression for bash * it either can escape single or double quotes (double is default) */ export const bashEscape = ( value: VariableValue | any, options: EscapeOptions = { quotes: "double", }, ) => { if (value instanceof BashExpression) { // no need to escape return escapeBashExpression(value, options); } if (value instanceof VariableValueContainingReferences) { return value.toString(options); } return escapeString(value, options); }; export const escapeString = ( value: string | null | undefined, { quotes }: EscapeOptions = { quotes: "double", }, ) => { const quoteEscaped = quotes ? quotes === "single" ? escapeSingleQuotes(value) : escapeDoubleQuotes(value) : value; return quoteEscaped; }; export const escapeBashExpression = ( value: BashExpression, options: EscapeOptions, ) => { // no need to escape, we just return the string return value; }; export const escapeDoubleQuotes = (value: string | null | undefined) => value?.toString().replace(/"/g, '\\"'); export const escapeSingleQuotes = (value: string | null | undefined) => value?.toString().replace(/'/g, "\\'"); export type EscapeForDotEnvOptions = { quoteMode: "auto" | "always"; }; /** * * escape env vars for .env files. * unfortunatly, the format has many limitations. In order to be very forgiving, we need to do some magic here: * * - when the value contains no newlines, we are fine * - if the value contains newlines, we need to wrap it in quotes. And thats where the problem begins: * - you can't escape quotes. this is a limitation of dotenv and node * - you can have inner quotes, but they break in node.js (not in dotenv though), see https://github.com/nodejs/node/issues/54134 * - so we need to quote cleverly * - to make things worse, we need to check whether we have a simple stirng or a bash expression, that needs to be evalulated first... * * what an absolute nightmare. * * - other languages are currently only partially supported, since most .env implementations are slightly different */ export const escapeForDotEnv = ( value: VariableValue | undefined | null, options: EscapeForDotEnvOptions = { quoteMode: "auto", }, ): string => { if (value === undefined || value === null) { return ""; } if (typeof value === "string") { // if string contains newlines, we need to wrap it in quotes // we additionaly escape newlines, that give best compatibility if (options.quoteMode === "always" || value.includes("\n")) { const newlinesReplaces = value.replace(/\n/g, "\\n"); // default to ", but if this is not possible, we try to use ' or ` const quote = value.includes(`"`) ? value.includes(`'`) ? value.includes("`") ? // If all quote types are present, default to double quotes. This works in dotenv, but not in node.js because of the bug mentioned abouve '"' : "`" : "'" : '"'; // if we found a quote, we can wrap the string in it return `${quote}${newlinesReplaces}${quote}`; } else { // otherwise we can return as is return value; } } else if (value instanceof BashExpression) { return escapeBashExpressionForDotEnv(value); } else if (value instanceof VariableValueContainingReferences) { // instead of doing it part-wise, we just do it all at once const containsAnyBashExpression = value.parts.some( (part) => part instanceof BashExpression, ); if (!containsAnyBashExpression) { return escapeForDotEnv( value.toString({ quotes: "double", }), ); } else { const result = escapeBashExpressionForDotEnv( new BashExpression( value.toString({ quotes: "double", }), ), ); return result; } } else { return value; } }; // basically the same thing as above for bash // thx chatgpt for this const escapeForDotEnvScript = registerGlobalScriptFunction( "escapeForDotEnv", ` input="\${1:-$(cat)}" input="\${input//$'\\n'/\\\\n}" if [[ "$input" == *\\\\n* ]]; then if [[ "$input" == *\\"* && "$input" == *\\'* && "$input" == *\\\`* ]]; then printf "\\"%s\\"\\n" "$input" ${/* fallback to double quotes */ ""} elif [[ "$input" == *\\"* && "$input" == *\\'* ]]; then printf "\`%s\`\\n" "$input" elif [[ "$input" == *\\"* ]]; then printf "'%s'\\n" "$input" else printf "\\"%s\\"\\n" "$input" fi else printf "%s\\n" "$input" fi `, ); const escapeBashExpressionForDotEnv = (value: BashExpression) => { return value.transformWithCommand(escapeForDotEnvScript.name).toString(); };