The feature you're looking for is called Serialisation (Serialization in American English).
Here a simple command is an array of one or more strings, so it boils down to serialising an array.
If the command is an external command, there's that further limitation that the strings cannot contain the NUL byte as they are passed as C-style strings to the execve()
system call. In most shells, you have that same limitation even for commands that don't involve the execve()
system call (like for builtins or functions), the only exception being the zsh shell.
So if you can make that assumption that command arguments won't contain NUL bytes, serialisation is easy: you just need to print them NUL-delimited:
print0() {
[ "$#" -eq 0 ] ||
printf '%s\0' "$@"
}
print0 cmd -o"value with space" > file
In bash
4.4 or newer, reading that back as a list of arguments is just:
readarray -td '' args < file
Where -d ''
sets the NUL byte as the delimiter, -t
strips the delimiter from the values, not strictly necessary in current versions of bash which can't store NULs in variables.
An then do:
"${args[@]}"
To execute the command.
Or even with GNU xargs
:
xargs -r0a file env
Beware however, that except in zsh, you can't store the result of that serialisation in a variable as in all other shells, you can't have a NUL byte in the value of a shell variable.
JSON, XML, YAML are common formats used for serialisation of complex data structure, but they have their own problem (for instance, JSON strings must be made of characters, while argument strings are arrays of arbitrary bytes), and more importantly, few shells have builtin support for parsing them (ksh93v- beta version had some experimental support for parsing JSON, but that was very buggy and dropped in newer versions).
A few languages have a builtin serialisation format. For instance, php
has a serialize()
and corresponding unserialize()
function, but php
doesn't have very good APIs to execute commands.
A common approach in interpreted languages is to serialise as code. That's what Data::Dumper
in perl
does for instance. If you have an array with cmd
and -ovalue with space
as arguments, you can just store it as @array = ("cmd", "-o value with space")
and then it's just a matter of evaluating that perl code to get the array back.
In Korn-like shells such as bash or zsh, it's very easily done as that's exactly what typeset -p
does. In zsh, you could do typeset -p argv
in your serialise
function, but you can't do typeset -p @
as @
is not a variable. In bash
, where the positional parameters are not mapped to the argv
variable, you can still use a temporary array.
serialise() {
local args
args=( "$@" )
typeset -p args
}
serialised=$(serialise cmd -o"value with space")
Then unserialising is just:
eval "$serialised"
Which will create the $args
array (beware that if run in a function, that array will be local to the function).
And then:
"${args[@]}"
again to run the command.
Beware that the unserialisation has to be done with the same shell version, on the same OS and in the same locale as where serialisation was done. See this answer for "Escape a variable for use as content of another script" for further details on how to serialise strings.
For completeness, in ksh93, which has more complex data structures than other shells, including multi-dimensional arrays, structures and objects, there is builtin serialisation and unserialisation support.
- serialise:
print -C var
- unserialise:
read -C var
For instance, you can copy a variable with:
print -C var | read -C var_copy