Skip to content

Commit f7e9625

Browse files
committed
Add shared functions for quoting shell words
1 parent 3602c70 commit f7e9625

File tree

3 files changed

+73
-14
lines changed

3 files changed

+73
-14
lines changed

internal/patroni/config.go

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/crunchydata/postgres-operator/internal/config"
1616
"github.com/crunchydata/postgres-operator/internal/naming"
1717
"github.com/crunchydata/postgres-operator/internal/postgres"
18+
"github.com/crunchydata/postgres-operator/internal/shell"
1819
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
1920
)
2021

@@ -34,12 +35,6 @@ const (
3435
"# Your changes will not be saved.\n"
3536
)
3637

37-
// quoteShellWord ensures that s is interpreted by a shell as single word.
38-
func quoteShellWord(s string) string {
39-
// https://www.gnu.org/software/bash/manual/html_node/Quoting.html
40-
return `'` + strings.ReplaceAll(s, `'`, `'"'"'`) + `'`
41-
}
42-
4338
// clusterYAML returns Patroni settings that apply to the entire cluster.
4439
func clusterYAML(
4540
cluster *v1beta1.PostgresCluster,
@@ -581,15 +576,11 @@ func instanceYAML(
581576
"-",
582577
}, command...)
583578

584-
quoted := make([]string, len(command))
585-
for i := range command {
586-
quoted[i] = quoteShellWord(command[i])
587-
}
588579
postgresql[pgBackRestCreateReplicaMethod] = map[string]any{
589-
"command": strings.Join(quoted, " "),
590-
"keep_data": true,
591-
"no_leader": true,
592-
"no_params": true,
580+
"command": strings.Join(shell.QuoteWords(command...), " "),
581+
"keep_data": true, // Use the data directory from a prior method.
582+
"no_leader": true, // Works without a replication connection.
583+
"no_params": true, // Patroni should not add "--scope", "--role", etc.
593584
}
594585
methods = append([]string{pgBackRestCreateReplicaMethod}, methods...)
595586
}

internal/shell/quote.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2024 - 2025 Crunchy Data Solutions, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package shell
6+
7+
import "strings"
8+
9+
// escapeSingleQuoted is used by [QuoteWord].
10+
var escapeSingleQuoted = strings.NewReplacer(
11+
// slightly shorter results for the unlikely pair of quotes.
12+
`''`, `'"''"'`,
13+
14+
// first, close the single-quote U+0027,
15+
// add one between double-quotes U+0022,
16+
// then reopen the single-quote U+0027.
17+
`'`, `'"'"'`,
18+
).Replace
19+
20+
// QuoteWord ensures that v is interpreted by a shell as a single word.
21+
func QuoteWord(v string) string {
22+
// https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html
23+
// https://www.gnu.org/software/bash/manual/html_node/Quoting.html
24+
return `'` + escapeSingleQuoted(v) + `'`
25+
}
26+
27+
// QuoteWords ensures that s is interpreted by a shell as individual words.
28+
func QuoteWords(s ...string) []string {
29+
quoted := make([]string, len(s))
30+
for i := range s {
31+
quoted[i] = QuoteWord(s[i])
32+
}
33+
return quoted
34+
}

internal/shell/quote_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2024 - 2025 Crunchy Data Solutions, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package shell
6+
7+
import (
8+
"testing"
9+
10+
"gotest.tools/v3/assert"
11+
)
12+
13+
func TestQuoteWord(t *testing.T) {
14+
assert.Equal(t, QuoteWord(""), `''`,
15+
"expected empty and single-quoted")
16+
17+
assert.Equal(t, QuoteWord("abc"), `'abc'`,
18+
"expected single-quoted")
19+
20+
assert.Equal(t, QuoteWord(`a" b"c`), `'a" b"c'`,
21+
"expected easy double-quotes")
22+
23+
assert.Equal(t, QuoteWord(`a' b'c`),
24+
`'a'`+`"'"`+`' b'`+`"'"`+`'c'`,
25+
"expected close-quote-open twice")
26+
27+
assert.Equal(t, QuoteWord(`a''b`),
28+
`'a'`+`"''"`+`'b'`,
29+
"expected close-quotes-open once")
30+
31+
assert.Equal(t, QuoteWord(`x''''y`),
32+
`'x'`+`"''"`+`''`+`"''"`+`'y'`,
33+
"expected close-quotes-open twice")
34+
}

0 commit comments

Comments
 (0)