blob: 24a8bba5d6a3516f683fac20b380e3d7fd6281a9 [file] [log] [blame]
// Copyright 2022 The LUCI Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
// Package shell contains functions for quoting arguments to commands.
package shell
// QuoteUnix takes an array of strings representing individual arguments of a
// command and returns a space-delimited sequence of double-quoted string literals
// quoted correctly for any POSIX-compatible shell.
// It does not produce pretty output. QuoteUnix("a", "cool", "bear") becomes `"a" "cool" "bear"`, not
// `a cool bear`. This behavior simplifies the implementaiton, but also bypasses aliases if we are
// handing our string to a login shell.
// QuoteUnix(s) is a valid UTF-8 string if and only if s is a valid UTF-8 string.
// Proof of this fact is left as an exercise to the reader.
// When given a single argument s, QuoteUnix(s) quotes that argument alone.
// When given multiple arguments, QuoteUnix(...) produces a space delimited sequence of quoted
// strings, which is interpreted as a command with arguments by every POSIX-compatible shell.
// See for more information.
func QuoteUnix(args ...string) string {
out := []byte{}
for i, s := range args {
if i != 0 {
out = append(out, ' ')
out = append(out, '"')
// UTF-8 is self-synchronizing and furthermore, single-byte characters like a, $, and ` never
// appear within multibyte characters. Therefore, iterating one byte at a time and inserting
// backslashes precisely when we have to is correct.
for _, ch := range []byte(s) {
switch ch {
// According to the documentation, inside a POSIX shell only five characters
// may be quoted inside a double-quoted string. The extra character is the newline.
// However, quoting or failing to quote the newline does not affect the semantics of
// the string literal, so we do not do it to reduce the length of the output string.
case '"', '\\', '$', '`':
out = append(out, '\\', ch)
out = append(out, ch)
out = append(out, '"')
return string(out)