How can I escape an arbitrary string for use as a command line argument in Bash? - perl

I have a list of strings and I want to pass those strings as arguments in a single Bash command line call. For simple alphanumeric strings it suffices to just pass them verbatim:
> script.pl foo bar baz yes no
foo
bar
baz
yes
no
I understand that if an argument contains spaces or backslashes or double-quotes, I need to backslash-escape the double-quotes and backslashes, and then double-quote the argument.
> script.pl foo bar baz "\"yes\"\\\"no\""
foo
bar
baz
"yes"\"no"
But when an argument contains an exclamation mark, this happens:
> script.pl !foo
-bash: !foo: event not found
Double quoting doesn't work:
> script.pl "!foo"
-bash: !foo: event not found
Nor does backslash-escaping (notice how the literal backslash is present in the output):
> script.pl "\!foo"
\!foo
I don't know much about Bash yet but I know that there are other special characters which do similar things. What is the general procedure for safely escaping an arbitrary string for use as a command line argument in Bash? Let's assume the string can be of arbitrary length and contain arbitrary combinations of special characters. I would like an escape() subroutine that I can use as below (Perl example):
$cmd = join " ", map { escape($_); } #args;
Here are some more example strings which should be safely escaped by this function (I know some of these look Windows-like, that's deliberate):
yes
no
Hello, world [string with a comma and space in it]
C:\Program Files\ [path with backslashes and a space in it]
" [i.e. a double-quote]
\ [backslash]
\\ [two backslashes]
\\\ [three backslashes]
\\\\ [four backslashes]
\\\\\ [five backslashes]
"\ [double-quote, backslash]
"\T [double-quote, backslash, T]
"\\T [double-quote, backslash, backslash, T]
!1
!A
"!\/'" [double-quote, exclamation, backslash, forward slash, apostrophe, double quote]
"Jeff's!" [double-quote, J, e, f, f, apostrophe, s, exclamation, double quote]
$PATH
%PATH%
&
<>|&^
*#$$A$##?-_
EDIT:
Would this do the trick? Escape every unusual character with a backslash, and omit single or double quotes. (Example is in Perl but any language can do this)
sub escape {
$_[0] =~ s/([^a-zA-Z0-9_])/\\$1/g;
return $_[0];
}

If you want to securely quote anything for Bash, you can use its built-in printf %q formatting:
cat strings.txt:
yes
no
Hello, world
C:\Program Files\
"
\
\\
\\\
\\\\
\\\\\
"\
"\T
"\\T
!1
!A
"!\/'"
"Jeff's!"
$PATH
%PATH%
&
<>|&^
*#$$A$##?-_
cat quote.sh:
#!/bin/bash
while IFS= read -r string
do
printf '%q\n' "$string"
done < strings.txt
./quote.sh:
yes
no
Hello\,\ world
C:\\Program\ Files\\
\"
\\
\\\\
\\\\\\
\\\\\\\\
\\\\\\\\\\
\"\\
\"\\T
\"\\\\T
\!1
\!A
\"\!\\/\'\"
\"Jeff\'s\!\"
\$PATH
%PATH%
\&
\<\>\|\&\^
\*#\$\$A\$##\?-_
These strings can be copied verbatim to for example echo to output the original strings in strings.txt.

What is the general procedure for safely escaping an arbitrary string for use as a command line argument in Bash?
Replace every occurrence of ' with '\'', then put ' at the beginning and end.
Every character except for a single quote can be used verbatim in a single-quote-delimited string. There's no way to put a single quote inside a single-quote-delimited string, but that's easy enough to work around: end the string ('), then add a single quote by using a backslash to escape it (\'), then begin a new string (').
As far as I know, this will always work, with no exceptions.

You can use single quotes to escape strings for Bash. Note however this does not expand variables within quotes as double quotes do. In your example, the following should work:
script.pl '!foo'
From Perl, this depends on the function you are using to spawn the external process. For example, if you use the system function, you can pass arguments as parameters so there"s no need to escape them. Of course you"d still need to escape quotes for Perl:
system("/usr/bin/rm", "-fr", "/tmp/CGI_test", "/var/tmp/CGI");

sub text_to_shell_lit(_) {
return $_[0] if $_[0] =~ /^[a-zA-Z0-9_\-]+\z/;
my $s = $_[0];
$s =~ s/'/'\\''/g;
return "'$s'";
}
See this earlier post for an example.

Whenever you see you don't get the desired output, use the following method:
"""\special character"""
where special character may include ! " * ^ % $ # # ....
For instance, if you want to create a bash generating another bash file in which there is a string and you want to assign a value to that, you can have the following sample scenario:
Area="(1250,600),(1400,750)"
printf "SubArea="""\""""${Area}"""\""""\n" > test.sh
printf "echo """\$"""{SubArea}" >> test.sh
Then test.sh file will have the following code:
SubArea="(1250,600),(1400,750)"
echo ${SubArea}
As a reminder to have newline \n, we should use printf.

Bash interprets exclamation marks only in interactive mode.
You can prevent this by doing:
set +o histexpand
Inside double quotes you must escape dollar signs, double quotes, backslashes and I would say that's all.

This is not a complete answer, but I find it useful sometimes to combine two types of quote for a single string by concatenating them, for example echo "$HOME"'/foo!?.*' .

FWIW, I wrote this function that invokes a set of arguments using different credentials. The su command required serializing all the arguments, which required escaping them all, which I did with the printf idiom suggested above.
$ escape_args_then_call_as myname whoami
escape_args_then_call_as() {
local user=$1
shift
local -a args
for i in "$#"; do
args+=( $(printf %q "${i}") )
done
sudo su "${user}" -c "${args[*]}"
}

Related

Replace every " within string

I have lines in a text file which looks like this example:
"2009217",2015,3,"N","N","2","UPPER DARBY FIREFIGHTERS "PAC"","","","","7235 WEST CHESTER PIKE","","UPPER DARBY","PA","19082","","6106220269",4245.0100,650.0000,.0000
I want to replace every double quote in multiple partial strings similar to this "UPPER DARBY FIREFIGHTERS "PAC""across the whole file.
So the result should be as below for each instance of the recurring double quotes:
"2009217",2015,3,"N","N","2","UPPER DARBY FIREFIGHTERS PAC","","","","7235 WEST CHESTER PIKE","","UPPER DARBY","PA","19082","","6106220269",4245.0100,650.0000,.0000
I came to this sed line:
cat file.txt | sed "s/\([^,]*,[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,\)\([^,]*\),\(.*\)/\1\2\3/"
But now I don't know how to replace the double quote within \2.
Is that possible with sed?
I would personally use awk for that because it is more readable:
#!/usr/bin/env awk
BEGIN {
# Use ',' as the input and output field delimiter
FS=OFS=","
}
{
# Iterate through all fields. (NF is the number of fields.)
for(i=1;i<=NF;i++) {
# If the field starts and ends with a '"'
if($i ~ /^".*"$/) {
# Replace all '""
gsub(/"/,"",$i)
# Wrap in '"' again
$i = "\"" $i "\""
}
}
}
print
This might work for you (GNU sed):
sed -r ':a;s/^((([^",]*,)*("[^",]*",([^",]*,)*)*)"[^",]*)"([^,])/\1\6/;ta' file
This removes extra double quotes from strings surrounded by double quotes and delimited by ,'s.
It does this by eliminating properly constructed double quotes strings and non-quoted strings (in this example numbers) and then removes double quotes that are not followed by ,
[^",]*, # non double quoted strings
"[^",]*", # properly quoted strings
(([^",]*,)*("[^",]*",([^",]*,)*)*) # eliminate all properly constructed strings
"[^",]*"([^,]) # improper double quotes
^
|

terminal command: handle special characters in filename

I want to execute some commands in terminal. I create them in Swift 3.0 and write them to a command file. But some special characters make problems, e.g. single quote:
mv 'Don't do it.txt' 'Don_t do it.txt'
I use single quote to cover other special characters. But what's about single quotes itself. How can I convert them in a way every possible filename can be handled correctly?
You question is strange:
In this case we would be writing to shell script rather than a text file
You are replacing single quotes in the output file name, but not spaces,
which should be replaced
Here is a solution that gives proper escaping for the input files, and proper
replacing (read: spaces too) for the output files:
#!/usr/bin/awk -f
BEGIN {
mi = "\47"
no = "[^[:alnum:]%+,./:=#_-]"
print "#!/bin/sh"
while (++os < ARGC) {
pa = split(ARGV[os], qu, mi)
printf "mv "
for (ro in qu) {
printf "%s", match(qu[ro], no) ? mi qu[ro] mi : qu[ro]
if (ro < pa) printf "\\" mi
}
gsub(no, "_", ARGV[os])
print FS ARGV[os]
}
}
Result:
#!/bin/sh
mv 'dont do it!.txt' dont_do_it_.txt
mv Don\''t do it.txt' Don_t_do_it.txt
mv dont-do-it.txt dont-do-it.txt

sed substitute with quotes and wildcard

I need to replace if ($_SESSION['POST']['*']==1){ with if (isset($_SESSION['POST']['*']) && $_SESSION['POST']['*']==1){
(I'm using * as a wild card)
I've tried sed -i "s/if ($_SESSION['POST']['.*']/if (isset($_SESSION['POST']['.*']) && $_SESSION['POST']['.*']/g" *.php and a few other variations without success.
Here goes...
sed "s/\(if (\)\(\$_SESSION\['POST']\['\([^']*\)']\)==1/\1isset(\2) \&\& \$_SESSION['POST']['\3']==1/" file
Using double quotes means that the $ symbols must be escaped, otherwise they will be interpreted as shell variables. The square brackets need to be escaped, otherwise they will be interpreted as the beginning of a range. It's OK to leave the closing square brackets as they are.
In order to capture the key, I have used a character class [^']*. This means zero or more characters that are not a single quote.
In the replacement, the captured groups (the parts between parentheses in the match) are referred to using \1, \2, etc.
Testing it out:
$ cat file
if ($_SESSION['POST']['foo']==1){
// do something
}
if ($_SESSION['POST']['bar']==1){
// do something else
}
$ sed "s/\(if (\)\(\$_SESSION\['POST']\['\([^']*\)']\)==1/\1isset(\2) \&\& \$_SESSION['POST']['\3']==1/" file
if (isset($_SESSION['POST']['foo']) && $_SESSION['POST']['foo']==1){
// do something
}
if (isset($_SESSION['POST']['bar']) && $_SESSION['POST']['bar']==1){
// do something else
}
By the way it makes the command a few characters shorter if you use extended regexp mode (-r or -E). In extended mode, the parentheses enclosing capture groups don't have to be escaped but literal ones do, so your command would then be:
sed -r "s/(if \()(\$_SESSION\['POST']\['([^']*)'])==1/\1isset(\2) \&\& \$_SESSION['POST']['\3']==1/" file
This sed should work:
s="if (\$_SESSION['POST']['name']==1){"
sed -r 's/(if +)\((([^=]+)[^\)]+)/\1(isset(\3) \&\& \2/' <<< "$s"
if (isset($_SESSION['POST']['name']) && $_SESSION['POST']['name']==1){
PS: Use sed -E instead of sed -r on OSX.
Here's another.
This is what we need to produce:
Pattern: if (\$_SESSION\['POST'\]\['\([^']*\)'\]
Replacement: if (isset($_SESSION['POST']['\1']) \&\& $_SESSION['POST']['\1']
When quoted in shell level:
Pattern: "if (\\\$_SESSION\['POST'\]\['\([^']*\)'\]"
Replacement: "if (isset(\$_SESSION['POST']['\1']) \\&\\& \$_SESSION['POST']['\1']"
Putting it together:
sed -i "s|if (\\\$_SESSION\['POST'\]\['\([^']*\)'\]|if (isset(\$_SESSION['POST']['\1']) \\&\\& \$_SESSION['POST']['\1']|g" file
Test:
# sed "s|if (\\\$_SESSION\['POST'\]\['\([^']*\)'\]|if (isset(\$_SESSION['POST']['\1']) \\&\\& \$_SESSION['POST']['\1']|g" <<'EOF'
> if ($_SESSION['POST']['ABC']==1){
> EOF
if (isset($_SESSION['POST']['ABC']) && $_SESSION['POST']['ABC']==1){

Ignoring escape characters in perl

my %result = "\\path\tfolder\file.txt";
How can I ignore the \t escape sequence without prepending a '\'. Is there something like:
my %result = r"\\path\tfolder\file.txt";
The above doesn't work.
Single quotes process two escape sequences: \\ and \', so you would have to double the leading double-backslash but not the others:
my $result = '\\\\server\toppath\files';
To get what you want, you could use a here-document at the cost of some syntactic bulk.
chomp(my $result = <<'EOPath');
\\server\toppath\files
EOPath
Note the change of sigil from % to $ because a string is a scalar, and hashes are for associations.

CMD.exe quoting a string

What's the exact way to properly quote a single command line argument?
For example, I have some random text in a variable $X. I need to quote it in a way so that if I call
system("program.exe " + $X_QUOTED);
then argv[1] of my program.exe has to match original unquoted $X
imagine I have this program.exe:
int main(const char **argv, int){ puts(argv[1]); }
and the output of command: "program xxxx" is:
"test |test
what xxxx has to literally be? I tried to add quotes and all that trickery, but then I can always add some other type of output that would break my approach to quote cmd line arguments.
H:>args """test |test"
argv[0] = args
argv[1] = "test |test
Apparently:
Replace each quote by ""
Surround the argument with quotes