Skip to content

VSL Escaping

VSL (the Value Shell Language) is how you fetch and run values with the get-object, get-asset, and run-object commands you write in dk.u, in distribution scripts (dist-*/run.u), and in the precommands of .values.jsonc files. Often arguments to value commands like get-object have spaces, quotes, backticks, and other special characters.

This guide will help you write or understand the value commands that have special characters in their arguments.

The general rules for special characters are:

  • bare words may contain literal backticks
  • inside double quotes, backtick escapes a literal double quote or a literal backtick
  • variables and subshells stay expandable unless you deliberately quote a literal

The examples below show how to apply those general rules in specific situations.

Helpers

A few helpers render the example results as Markdown tables so the output is easy to read.

let ctx =
MlFront_Thunk.ThunkExecutionContext.create_literal
~created_for:"ESCAPING.md" ()
val ctx : MlFront_Thunk.ThunkExecutionContext.t = <abstr>
let roundtrip s =
MlFront_Thunk.ThunkCommand.parse_evalableterm ~on_warning:ignore ctx s
|> Result.map MlFront_Thunk.ThunkCommand.evalable_term_to_valueshell
val roundtrip : string -> (string, string) result = <fun>
let quote_expandable =
MlFront_Thunk.ThunkLexers.ValueShellLexer.Token.quote_bareword_if_needed
val quote_expandable : String.t -> String.t = <fun>
let quote_literal =
MlFront_Thunk.ThunkLexers.ValueShellLexer.Token.quote_literal_if_needed
val quote_literal : String.t -> String.t = <fun>
let code s =
let max_backticks =
let max_run = ref 0 in
let current_run = ref 0 in
String.iter
(fun c ->
if c = '`' then (
incr current_run;
max_run := max !max_run !current_run)
else current_run := 0)
s;
!max_run
in
let fence = String.make (max_backticks + 1) '`' in
fence ^ s ^ fence
val code : string -> string = <fun>
let print_table rows =
Format.printf "%s@." {|\markdown\;|};
Format.printf "| Field | Value |@.";
Format.printf "| --- | --- |@.";
List.iter (fun (field, value) ->
Format.printf "| %s | %s |@." field value) rows
val print_table : (string * string) list -> unit = <fun>
let show_quote ~scenario ~input ~helper ~notes quote =
let output = quote input in
print_table
[
("**Scenario**", scenario);
("**Helper**", code helper);
("**Raw Text**", code input);
("**Escaped VSL**", code output);
("**How To Escape**", notes);
]
val show_quote :
scenario:string ->
input:string -> helper:string -> notes:string -> (string -> string) -> unit =
<fun>
let show_roundtrip ~scenario ~input ~notes =
match roundtrip input with
| Ok output ->
print_table
[
("**Scenario**", scenario);
("**Helper**", code "parse_evalableterm -> evalable_term_to_valueshell");
("**Raw Text**", code input);
("**Escaped VSL**", code output);
("**How To Escape**", notes);
]
| Error err ->
print_table
[
("**Scenario**", scenario);
("**Helper**", code "parse_evalableterm -> evalable_term_to_valueshell");
("**Raw Text**", code input);
("**Error**", code err);
("**How To Escape**", notes);
]
val show_roundtrip : scenario:string -> input:string -> notes:string -> unit =
<fun>
show_quote
~scenario:"Simple bare words stay bare"
~input:{|clang.exe|}
~helper:"quote_bareword_if_needed"
~notes:"Escaping not needed."
quote_expandable
FieldValue
ScenarioSimple bare words stay bare
Helperquote_bareword_if_needed
Raw Textclang.exe
Escaped VSLclang.exe
How To EscapeEscaping not needed.

2. Windows paths with backslashes do not need escaping by themselves

Section titled “2. Windows paths with backslashes do not need escaping by themselves”
show_quote
~scenario:"Windows path without spaces"
~input:{|C:\temp\a.exe|}
~helper:"quote_bareword_if_needed"
~notes:"Escaping not needed; bare backslashes are literal."
quote_expandable
FieldValue
ScenarioWindows path without spaces
Helperquote_bareword_if_needed
Raw TextC:\temp\a.exe
Escaped VSLC:\temp\a.exe
How To EscapeEscaping not needed; bare backslashes are literal.
show_quote
~scenario:"Whitespace forces quoting"
~input:{|with space|}
~helper:"quote_bareword_if_needed"
~notes:"Wrap the whole text in single quotes."
quote_expandable
FieldValue
ScenarioWhitespace forces quoting
Helperquote_bareword_if_needed
Raw Textwith space
Escaped VSL'with space'
How To EscapeWrap the whole text in single quotes.

4. Single quotes are a readable wrapper when the word is fully literal

Section titled “4. Single quotes are a readable wrapper when the word is fully literal”
show_quote
~scenario:"Literal Windows path with spaces"
~input:{|C:\My Documents|}
~helper:"quote_bareword_if_needed"
~notes:"Wrap the whole literal path in single quotes."
quote_expandable
FieldValue
ScenarioLiteral Windows path with spaces
Helperquote_bareword_if_needed
Raw TextC:\My Documents
Escaped VSL'C:\My Documents'
How To EscapeWrap the whole literal path in single quotes.

5. Double quotes keep variables expandable while preserving spaces

Section titled “5. Double quotes keep variables expandable while preserving spaces”
show_roundtrip
~scenario:"Keep ${CONFIG} expandable inside spaced path"
~input:{|"${CONFIG}\Floor Plans\Master Bedroom.rvt"|}
~notes:"Use double quotes so spaces stay intact and ${CONFIG} still expands."
FieldValue
ScenarioKeep ${CONFIG} expandable inside spaced path
Helperparse_evalableterm -> evalable_term_to_valueshell
Raw Text"${CONFIG}\Floor Plans\Master Bedroom.rvt"
Escaped VSL"${CONFIG}\Floor Plans\Master Bedroom.rvt"
How To EscapeUse double quotes so spaces stay intact and ${CONFIG} still expands.

6. Inside double quotes, escape a literal double quote with backtick

Section titled “6. Inside double quotes, escape a literal double quote with backtick”
show_roundtrip
~scenario:"Literal double quote inside double-quoted text"
~input:{|"say `"hello`" loudly"|}
~notes:"Inside VSL double quotes, escape each literal double quote with backtick."
FieldValue
ScenarioLiteral double quote inside double-quoted text
Helperparse_evalableterm -> evalable_term_to_valueshell
Raw Text"say `"hello`" loudly"
Escaped VSL'say "hello" loudly'
How To EscapeInside VSL double quotes, escape each literal double quote with backtick.

7. Inside double quotes, escape a literal backtick with backtick

Section titled “7. Inside double quotes, escape a literal backtick with backtick”
show_roundtrip
~scenario:"Literal backtick inside double quotes"
~input:{|"two `` backticks"|}
~notes:"Inside VSL double quotes, write two backticks for one literal backtick."
FieldValue
ScenarioLiteral backtick inside double quotes
Helperparse_evalableterm -> evalable_term_to_valueshell
Raw Text"two `` backticks"
Escaped VSL'two ` backticks'
How To EscapeInside VSL double quotes, write two backticks for one literal backtick.

8. In a bare word, backtick is just a literal character

Section titled “8. In a bare word, backtick is just a literal character”
show_roundtrip
~scenario:"Literal backtick in a bare Windows path"
~input:{|C:\temp\`cache|}
~notes:"Wrap the whole word in single quotes to keep the bare backtick literal."
FieldValue
ScenarioLiteral backtick in a bare Windows path
Helperparse_evalableterm -> evalable_term_to_valueshell
Raw TextC:\temp\`cache
Escaped VSL'C:\temp\`cache'
How To EscapeWrap the whole word in single quotes to keep the bare backtick literal.

9. To suppress ${...} expansion, quote the literal form

Section titled “9. To suppress ${...} expansion, quote the literal form”
show_quote
~scenario:"Literal ${...} text"
~input:{|${SLOT.Release.Agnostic}|}
~helper:"quote_literal_if_needed"
~notes:"Wrap the whole text in single quotes so ${...} stays literal."
quote_literal
FieldValue
ScenarioLiteral ${...} text
Helperquote_literal_if_needed
Raw Text${SLOT.Release.Agnostic}
Escaped VSL'${SLOT.Release.Agnostic}'
How To EscapeWrap the whole text in single quotes so ${...} stays literal.

10. Variables and literals can be concatenated into one bare word

Section titled “10. Variables and literals can be concatenated into one bare word”
show_roundtrip
~scenario:"Literal text around an expandable variable"
~input:{|foo${SLOT.Baz}bar|}
~notes:"Escaping not needed; a whitespace-free variable-plus-literal word can stay bare."
FieldValue
ScenarioLiteral text around an expandable variable
Helperparse_evalableterm -> evalable_term_to_valueshell
Raw Textfoo${SLOT.Baz}bar
Escaped VSLfoo${SLOT.Baz}bar
How To EscapeEscaping not needed; a whitespace-free variable-plus-literal word can stay bare.

11. Expandable terms without spaces stay bare

Section titled “11. Expandable terms without spaces stay bare”
show_roundtrip
~scenario:"The `-f ${SLOT.File.Darwin_arm64}/dk` output path stays bare"
~input:{|${SLOT.File.Darwin_arm64}/dk|}
~notes:"Escaping not needed; keep `${SLOT.File.Darwin_arm64}/dk` bare after `-f`."
FieldValue
ScenarioThe -f ${SLOT.File.Darwin_arm64}/dk output path stays bare
Helperparse_evalableterm -> evalable_term_to_valueshell
Raw Text${SLOT.File.Darwin_arm64}/dk
Escaped VSL${SLOT.File.Darwin_arm64}/dk
How To EscapeEscaping not needed; keep ${SLOT.File.Darwin_arm64}/dk bare after -f.

12. Subshells can be concatenated with literals in one bare word

Section titled “12. Subshells can be concatenated with literals in one bare word”
show_roundtrip
~scenario:"Subshell plus literal suffix"
~input:{|$(get-object SomeLib_Std.Thing@1.0.0 -s Release.Agnostic -d :)/filename|}
~notes:"Use double quotes so the subshell expands and the suffix stays in the same word."
FieldValue
ScenarioSubshell plus literal suffix
Helperparse_evalableterm -> evalable_term_to_valueshell
Raw Text$(get-object SomeLib_Std.Thing@1.0.0 -s Release.Agnostic -d :)/filename
Escaped VSL"$(get-object SomeLib_Std.Thing@1.0.0 -s Release.Agnostic -d :)/filename"
How To EscapeUse double quotes so the subshell expands and the suffix stays in the same word.

13. A literal backtick can appear immediately before an expandable variable

Section titled “13. A literal backtick can appear immediately before an expandable variable”
show_roundtrip
~scenario:"Literal backtick before ${SLOT.Speedy}"
~input:{|C:\`${SLOT.Speedy}|}
~notes:"Use double quotes, and inside them write two backticks before ${SLOT.Speedy}."
FieldValue
ScenarioLiteral backtick before ${SLOT.Speedy}
Helperparse_evalableterm -> evalable_term_to_valueshell
Raw TextC:\`${SLOT.Speedy}
Escaped VSL"C:\``${SLOT.Speedy}"
How To EscapeUse double quotes, and inside them write two backticks before ${SLOT.Speedy}.

14. Single quotes inside double quotes are ordinary characters

Section titled “14. Single quotes inside double quotes are ordinary characters”
show_roundtrip
~scenario:"Single quote inside double quotes"
~input:{|"it's `${SLOT.Speedy}"|}
~notes:"Use double quotes; no extra escaping is needed for the single quote."
FieldValue
ScenarioSingle quote inside double quotes
Helperparse_evalableterm -> evalable_term_to_valueshell
Raw Text"it's `${SLOT.Speedy}"
Escaped VSL"it's ${SLOT.Speedy}"
How To EscapeUse double quotes; no extra escaping is needed for the single quote.

15. For cmd /c, keep the inner Windows quoting and then escape the whole payload as one literal

Section titled “15. For cmd /c, keep the inner Windows quoting and then escape the whole payload as one literal”

When building a single cmd /c argument that itself contains quoted paths, prefer the Windows cmd.exe convention of doubled outer double-quotes around the inner command string. A concise explanation is in this Stack Overflow answer.

If you are writing function.commands in a values file, prefer the ["--cmd.exe", "/c", "<string>"] special form documented in the SPECIFICATION; this section is only for places where you must still hand-author one literal cmd /c payload string.

show_quote
~scenario:"Literal cmd /c payload with quoted paths"
~input:{|cmd /c ""c:\Program Files\demo1.cmd" & "c:\Program Files\demo2.cmd""|}
~helper:"quote_literal_if_needed"
~notes:"Keep cmd.exe's doubled double-quotes, then wrap the whole payload in single quotes for VSL."
quote_literal
FieldValue
ScenarioLiteral cmd /c payload with quoted paths
Helperquote_literal_if_needed
Raw Textcmd /c ""c:\Program Files\demo1.cmd" & "c:\Program Files\demo2.cmd""
Escaped VSL'cmd /c ""c:\Program Files\demo1.cmd" & "c:\Program Files\demo2.cmd""'
How To EscapeKeep cmd.exe's doubled double-quotes, then wrap the whole payload in single quotes for VSL.