escape: add --stdin input mode

systemd-escape currently only processes strings passed as
command line arguments. This is awkward for callers that already
have a generated list of strings, because they need to loop around
the tool or use xargs and carefully preserve whitespace and other
special characters.

Add --stdin to read one string per line from standard input and
write one escaped result per output line. Keep command line strings
mutually exclusive with --stdin so the input source remains
unambiguous.

Use an explicit option instead of treating '-' specially, since '-'
is itself a valid string to escape. The existing escape, unescape,
mangle, path, suffix, and template rules are reused unchanged.
This commit is contained in:
dongshengyuan
2026-07-26 13:20:55 +09:00
committed by Yu Watanabe
parent 5ef2510988
commit 47536f5aac
3 changed files with 193 additions and 99 deletions
+18 -1
View File
@@ -38,7 +38,9 @@
<para>The command takes any number of strings on the command line,
and will process them individually, one after another. It will
output them separated by spaces to stdout.</para>
output them separated by spaces to stdout. If <option>--stdin</option> is specified, strings are
instead read from standard input, one per line, and the result for each input line is written on a
separate output line.</para>
<para>By default, this command will escape the strings passed,
unless <option>--unescape</option> is passed which results in the
@@ -86,6 +88,16 @@
<xi:include href="version-info.xml" xpointer="v216"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--stdin</option></term>
<listitem><para>Read strings from standard input, one per line, instead of from the command line.
The line ending is not included in the string to escape or unescape, and the result for each line
is written on a separate output line. This option may not be combined with command line strings.</para>
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--path</option></term>
<term><option>-p</option></term>
@@ -170,6 +182,11 @@ tmp-waldi-foobar.mount</programlisting>
<programlisting>$ systemd-escape --template=systemd-nspawn@.service 'My Container 1' 'containerb' 'container/III'
systemd-nspawn@My\x20Container\x201.service systemd-nspawn@containerb.service systemd-nspawn@container-III.service</programlisting>
<para>To generate instance names from standard input:</para>
<programlisting>$ printf '%s\n' 'My Container 1' 'container/III' | systemd-escape --stdin --template=systemd-nspawn@.service
systemd-nspawn@My\x20Container\x201.service
systemd-nspawn@container-III.service</programlisting>
<para>To extract the instance part of an instantiated unit:</para>
<programlisting>$ systemd-escape -u --instance 'systemd-nspawn@My\x20Container\x201.service'
My Container 1</programlisting>
+143 -98
View File
@@ -4,6 +4,7 @@
#include "alloc-util.h"
#include "build.h"
#include "fileio.h"
#include "format-table.h"
#include "log.h"
#include "main-func.h"
@@ -24,6 +25,7 @@ static const char *arg_suffix = NULL;
static const char *arg_template = NULL;
static bool arg_path = false;
static bool arg_instance = false;
static bool arg_stdin = false;
static int help(void) {
_cleanup_free_ char *link = NULL;
@@ -84,6 +86,10 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
arg_template = opts.arg;
break;
OPTION_LONG("stdin", NULL, "Read strings from standard input"):
arg_stdin = true;
break;
OPTION_LONG("instance", NULL, "With --unescape, show just the instance part"):
arg_instance = true;
break;
@@ -102,7 +108,11 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
break;
}
if (option_parser_get_n_args(&opts) == 0)
if (arg_stdin && option_parser_get_n_args(&opts) > 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"--stdin may not be combined with arguments.");
if (!arg_stdin && option_parser_get_n_args(&opts) == 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Not enough arguments.");
@@ -134,6 +144,115 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
return 1;
}
static int process_one(const char *input, char **ret) {
_cleanup_free_ char *e = NULL;
int r;
assert(input);
assert(ret);
switch (arg_action) {
case ACTION_ESCAPE:
if (arg_path) {
r = unit_name_path_escape(input, &e);
if (r == -EINVAL) {
/* If escaping failed because the string was invalid, let's print a
* friendly message about it. Catch these specific error cases
* explicitly. */
if (!path_is_valid(input))
return log_error_errno(r, "Input '%s' is not a valid file system path, failed to escape.", input);
if (!path_is_absolute(input))
return log_error_errno(r, "Input '%s' is not an absolute file system path, failed to escape.", input);
if (!path_is_normalized(input))
return log_error_errno(r, "Input '%s' is not a normalized file system path, failed to escape.", input);
}
if (r < 0)
/* All other error cases. */
return log_error_errno(r, "Failed to escape string: %m");
/* If the escaping worked, then still warn if the path is not like we'd like
* it. Because that means escaping is not necessarily reversible. */
if (!path_is_valid(input))
log_warning("Input '%s' is not a valid file system path, escaping is likely not going to be reversible.", input);
else if (!path_is_absolute(input))
log_warning("Input '%s' is not an absolute file system path, escaping is likely not going to be reversible.", input);
/* Note that we don't complain about paths not being normalized here, because
* some forms of non-normalization is actually OK, such as a series // and
* unit_name_path_escape() will clean those up silently, and the reversal is
* "close enough" to be OK. */
} else {
e = unit_name_escape(input);
if (!e)
return log_oom();
}
if (arg_template) {
char *x;
r = unit_name_replace_instance(arg_template, e, &x);
if (r < 0)
return log_error_errno(r, "Failed to replace instance: %m");
free_and_replace(e, x);
} else if (arg_suffix) {
if (!strextend(&e, ".", arg_suffix))
return log_oom();
}
break;
case ACTION_UNESCAPE: {
_cleanup_free_ char *name = NULL;
if (arg_template || arg_instance) {
_cleanup_free_ char *template = NULL;
r = unit_name_to_instance(input, &name);
if (r < 0)
return log_error_errno(r, "Failed to extract instance: %m");
if (isempty(name))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Unit %s is missing the instance name.", input);
r = unit_name_template(input, &template);
if (r < 0)
return log_error_errno(r, "Failed to extract template: %m");
if (arg_template && !streq(arg_template, template))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Unit %s template %s does not match specified template %s.",
input, template, arg_template);
} else {
name = strdup(input);
if (!name)
return log_oom();
}
if (arg_path)
r = unit_name_path_unescape(name, &e);
else
r = unit_name_unescape(name, &e);
if (r < 0)
return log_error_errno(r, "Failed to unescape string: %m");
break;
}
case ACTION_MANGLE:
r = unit_name_mangle(input, 0, &e);
if (r < 0)
return log_error_errno(r, "Failed to mangle name: %m");
break;
}
*ret = TAKE_PTR(e);
return 0;
}
static int run(int argc, char *argv[]) {
int r;
@@ -144,106 +263,32 @@ static int run(int argc, char *argv[]) {
if (r <= 0)
return r;
if (arg_stdin) {
for (;;) {
_cleanup_free_ char *e = NULL, *line = NULL;
r = read_line(stdin, LONG_LINE_MAX, &line);
if (r == -ENOBUFS)
return log_error_errno(r, "Input line from standard input is too long, refusing.");
if (r < 0)
return log_error_errno(r, "Failed to read from standard input: %m");
if (r == 0)
return 0;
r = process_one(line, &e);
if (r < 0)
return r;
puts(e);
}
}
STRV_FOREACH(i, args) {
_cleanup_free_ char *e = NULL;
switch (arg_action) {
case ACTION_ESCAPE:
if (arg_path) {
r = unit_name_path_escape(*i, &e);
if (r == -EINVAL) {
/* If escaping failed because the string was invalid, let's print a
* friendly message about it. Catch these specific error cases
* explicitly. */
if (!path_is_valid(*i))
return log_error_errno(r, "Input '%s' is not a valid file system path, failed to escape.", *i);
if (!path_is_absolute(*i))
return log_error_errno(r, "Input '%s' is not an absolute file system path, failed to escape.", *i);
if (!path_is_normalized(*i))
return log_error_errno(r, "Input '%s' is not a normalized file system path, failed to escape.", *i);
}
if (r < 0)
/* All other error cases. */
return log_error_errno(r, "Failed to escape string: %m");
/* If the escaping worked, then still warn if the path is not like we'd like
* it. Because that means escaping is not necessarily reversible. */
if (!path_is_valid(*i))
log_warning("Input '%s' is not a valid file system path, escaping is likely not going to be reversible.", *i);
else if (!path_is_absolute(*i))
log_warning("Input '%s' is not an absolute file system path, escaping is likely not going to be reversible.", *i);
/* Note that we don't complain about paths not being normalized here, because
* some forms of non-normalization is actually OK, such as a series // and
* unit_name_path_escape() will clean those up silently, and the reversal is
* "close enough" to be OK. */
} else {
e = unit_name_escape(*i);
if (!e)
return log_oom();
}
if (arg_template) {
char *x;
r = unit_name_replace_instance(arg_template, e, &x);
if (r < 0)
return log_error_errno(r, "Failed to replace instance: %m");
free_and_replace(e, x);
} else if (arg_suffix) {
if (!strextend(&e, ".", arg_suffix))
return log_oom();
}
break;
case ACTION_UNESCAPE: {
_cleanup_free_ char *name = NULL;
if (arg_template || arg_instance) {
_cleanup_free_ char *template = NULL;
r = unit_name_to_instance(*i, &name);
if (r < 0)
return log_error_errno(r, "Failed to extract instance: %m");
if (isempty(name))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Unit %s is missing the instance name.", *i);
r = unit_name_template(*i, &template);
if (r < 0)
return log_error_errno(r, "Failed to extract template: %m");
if (arg_template && !streq(arg_template, template))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Unit %s template %s does not match specified template %s.",
*i, template, arg_template);
} else {
name = strdup(*i);
if (!name)
return log_oom();
}
if (arg_path)
r = unit_name_path_unescape(name, &e);
else
r = unit_name_unescape(name, &e);
if (r < 0)
return log_error_errno(r, "Failed to unescape string: %m");
break;
}
case ACTION_MANGLE:
r = unit_name_mangle(*i, 0, &e);
if (r < 0)
return log_error_errno(r, "Failed to mangle name: %m");
break;
}
r = process_one(*i, &e);
if (r < 0)
return r;
if (i != args)
fputc(' ', stdout);
+32
View File
@@ -35,6 +35,31 @@ assert_eq "$(systemd-escape 'hello-world' '/dev/loop1' 'template@🐍')" \
assert_eq "$(systemd-escape --unescape -- 'hello\x2dworld' '-dev-loop1' 'template\x40\xf0\x9f\x90\x8d')" \
'hello-world /dev/loop1 template@🐍'
# Multiple strings from stdin to escape/unescape
assert_eq "$(systemd-escape --stdin <<EOF
hello-world
/dev/loop1
template@🐍
EOF
)" $'hello\\x2dworld\n-dev-loop1\ntemplate\\x40\\xf0\\x9f\\x90\\x8d'
assert_eq "$(systemd-escape --stdin <<EOF
hello
world
EOF
)" $'hello\n\nworld'
assert_eq "$(systemd-escape --stdin --path --suffix=mount <<EOF
/var/lib/machines
/srv/my data
EOF
)" $'var-lib-machines.mount\nsrv-my\\x20data.mount'
assert_eq "$(systemd-escape --stdin --unescape <<EOF
hello\x2dworld
-dev-loop1
template\x40\xf0\x9f\x90\x8d
EOF
)" $'hello-world\n/dev/loop1\ntemplate@🐍'
# --suffix= is not compatible with --unescape
assert_eq "$(systemd-escape --suffix=mount -- '-+ěščřž---🤔')" \
'\x2d\x2b\xc4\x9b\xc5\xa1\xc4\x8d\xc5\x99\xc5\xbe\x2d\x2d\x2d\xf0\x9f\xa4\x94.mount'
@@ -57,6 +82,12 @@ check_escape '/this/is/where/my/stuff/is/ with spaces though ' \
'mount-my-stuff@this-is-where-my-stuff-is-\x20with\x20spaces\x20though\x20.service' \
--template=mount-my-stuff@.service --path
assert_eq "$(systemd-escape --stdin --template=hello@.service <<EOF
hello
this has spaces
EOF
)" $'hello@hello.service\nhello@this\\x20has\\x20spaces.service'
# --instance (must be used with --unescape)
assert_eq "$(systemd-escape --unescape --instance 'hello@\x20\x20what\x20\x2d\x20is\x20_\x20love\x3f\x20\xf0\x9f\xa4\x94\x20\xc2\xaf\x5c_\x28\xe3\x83\x84\x29_-\xc2\xaf.service')" \
' what - is _ love? 🤔 ¯\_(ツ)_/¯'
@@ -87,6 +118,7 @@ assert_eq "$(systemd-escape --mangle 'daily-existential-crisis .timer')" 'daily-
assert_eq "$(systemd-escape --mangle 'trailing-whitespace.mount ')" 'trailing-whitespace.mount\x20.service'
(! systemd-escape)
(! systemd-escape --stdin hello)
(! systemd-escape --suffix='' hello)
(! systemd-escape --suffix=invalid hello)
(! systemd-escape --suffix=mount --template=hello@.service hello)