Bash: parse shell command string - bash

Given a shell command string (ls a\ b "c d" $f), can bash parse it into an array, preferrably as bash itself would see it after all expansions, right before execution (([0]=ls [1]='a b' [2]='c d' [3]=<value of $f>))? I.e. the read() part of a REPL.
I'd like to write a custom bind -x function, and I need to parse the current READLINE_LINE. I've tried read -a, but that only takes care of backslashes (e.g. "a b" gets split into <"a> and <b">)

I hope you can make use of the following code
#!/usr/bin/env bash
parse(){
declare -g cmd
local i
for i in $(seq 1 $#); do
cmd[$((i-1))]="${!i}"
done
declare -p cmd
}
touch "a b" "c d" "e"
f="e"
trap 'eval "parse $BASH_COMMAND"; trap -- DEBUG' DEBUG
ls a\ b "c d" $f
declare -p cmd

Related

Escape an array of parameters for use in `eval`

I need to insert an array of arguments into an eval string executed via bash -c.
In this particular case, it's not possible to pass them separately as proper arguments (it's for an flock invocation which doesn't accept arguments for a -c script).
Smth like this:
flock f.lock -c 'do_stuff '"${ARGS[#]}"'; do_other_stuff'
How do I quote them properly so that they are correctly parsed into a sequence of arguments, even if they contain spaces or Bash special syntax?
Don't! It is going to be error prone and give you pain. Just:
{
flock 9
do_stuff "${ARGS[#]}"
do_other_stuff
} 9>f.lock
Anyway, split the operation into two:
first, safely transfer the environment to the subshell
then execute what you want to execute in a normal way
And it's just:
bash -c "$(declare -p ARGS)"'; do_stuff "${ARGS[#]}"; do_other_stuff'
Use Parameter transformation (new in Bash 4.4) with the Q (quote) operator which is specifically designed for this:
Q The expansion is a string that is the value of parameter quoted
in a format that can be reused as input.
$ ARGS=("foo bar" 'baz\n'\' '$xyzzy')
$ echo 'do_stuff '"${ARGS[*]#Q}"'; do_other_stuff'
do_stuff 'foo bar' 'baz\n'\''' '$xyzzy'; do_other_stuff
Note the use of * instead of the usual # since the code needs to be a single argument. Using # leads to erroneous behavior in some cases:
$ bash -xc "echo ${ARGS[#]#Q}; do_other_stuff"
+ echo 'foo bar'
foo bar
$ bash -xc "echo ${ARGS[*]#Q}; do_other_stuff"
+ echo 'foo bar' 'baz\n'\''' '$xyzzy'
foo bar baz\n' $xyzzy
+ do_other_stuff
You can even use this syntax for $*:
'do_stuff '"${*#Q}"' other args; do other stuff'

Space separated argument to script

I'm trying to use a wrapper bash script to execute some command with one parameter separated by space 'A B'.
foo-wrapper.sh content:
#/!bin/bash
foo $1
When running foo-wrapper.sh:
$bash -x foo-wrapper.sh "'A B'"
+ Error: foo ''\''A' 'B'\'''
The expected call would be: foo 'A B'
Any ideas how to make that work?
In the wrapper replace $1 by "$1".
If your arguments/variables contain space characters, you need to quote them to prevent them from being split.
In the call instead of "'A B'" use one of "A B", 'A B' or A\ B.
These are the most usual ways of specifying strings in BASH. (Note that the whitespace only makes a difference in the 3rd case. You would have to use quotes in the first two cases no matter whether you have a space or not.)
If you want to play around a little to get a feeling, you can start with:
$ foo() { echo "count: $#"; echo "arg 1: $1"; echo "arg 2: $2"; }
$ foo A\ B C
count: 2
arg 1: A B
arg 2: C

Bash pass all arguments as one quoted argument to another command

I usually do this:
git commit -m "My hands are typing words!"
I am gettin' tired of that, so I made this batch:
#echo off
set var=%*
git commit -m "%var%"
Which works as:
commit.bat blah blah blah
So I can drop the -m and the quotes, but it adds .bat. When I remove file extension, git Bash tries to interpret the batch as bash. So I need to use bash syntax instead. That's fine, I tried this:
#!/usr/bin/bash
git commit -m "$#"
That doesn't work, it passes arguments as multiple arguments. It invokes this:
git commit -m My hands are typing words!
I tried to add even more quotes (git commit -m ""$#""), no effect.
So how do I convert all arguments to a string that can be passed in bash to another command?
You can use "$*" instead of "$#".
This sample script should explain it:
$ cat a.sh
#!/bin/bash
echo '$*:'
printf "%s\n" $*
echo
echo '"$*":'
printf "%s\n" "$*"
echo
echo '$#:'
printf "%s\n" $#
echo
echo '"$#":'
printf "%s\n" "$#"
$ ./a.sh a b "c d" e
$*:
a
b
c
d
e
"$*":
a b c d e
$#:
a
b
c
d
e
"$#":
a
b
c d
e
But correctly, you should use "$1" & quote your message string before passing to the script.
In above example, you can see that the spaces between c & d are retained, but those between "c d" & e are lost.

Pass parameters that contain whitespaces via shell variable

I've got a program that I want to call by passing parameters from a shell variable. Throughout this question, I am going to assume that it is given by
#!/bin/sh
echo $#
i.e. that it prints out the number of arguments that are passed to it. Let's call it count-args.
I call my program like this:
X="arg1 arg2"
count-args $X
This works quite well. But now one of my arguments has a whitespace in it and I can't find a way to escape it, e.g. the following things do not work:
X="Hello\ World"
X="Hello\\ World"
X="'Hello World'"
In all of the cases, my program count-args prints out 2. I want to find a way so I can pass the string Hello World and that it returns 1 instead. How?
Just for clarification: I do not want to pass all parameters as a single string, e.g.
X="Hello World"
count-args $X
should print out 2. I want a way to pass parameters that contain whitespaces.
Use an array to store multiple, space-containing arguments.
$ args=("first one" "second one")
$ count-args "${args[#]}"
2
This can be solved with xargs. By replacing
count-args $X
with
echo $X | xargs count-args
I can use backslashes to escape whitespaces in $X, e.g.
X="Hello\\ World"
echo $X | xargs count-args
prints out 1 and
X="Hello World"
echo $X | xargs count-args
prints out 2.
count-args "$X"
The quotes ensure in bash, that the whole content of variable X is passed as a single parameter.
Your Counting script:
$ cat ./params.sh
#!/bin/sh
echo $#
For completeness here is what happens with various arguments:
$ ./params.sh
0
$ ./params.sh 1 2
2
$ ./params.sh
0
$ ./params.sh 1
1
$ ./params.sh 1 2
2
$ ./params.sh "1 2"
1
And here is what you get with variables:
$ XYZ="1 2" sh -c './params.sh $XYZ'
2
$ XYZ="1 2" sh -c './params.sh "$XYZ"'
1
Taking this a bit further:
$ cat params-printer.sh
#!/bin/sh
echo "Count: $#"
echo "1 : '$1'"
echo "2 : '$2'"
We get:
$ XYZ="1 2" sh -c './params-printer.sh "$XYZ"'
Count: 1
1 : '1 2'
2 : ''
This looks like what you wanted to do.
Now: If you have a script you cannot control and neither can you control the way the script is invoked. Then there is very little you can do to prevent a variable with spaces turning into multiple arguments.
There are quite a few questions around this on StackOverflow which indicate that you need the ability to control how the command is invoked else there is little you can do.
Passing arguments with spaces between (bash) script
Passing a string with spaces as a function argument in bash
Passing arguments to a command in Bash script with spaces
And wow! this has been asked so many times before:
How to pass argument with spaces to a shell script function

Passing arguments to a command in Bash script with spaces

I'm trying to pass 2 arguments to a command and each argument contains spaces, I've tried escaping the spaces in the args, I've tried wrapping in single quotes, I've tried escaping \" but nothing will work.
Here's a simple example.
#!/bin/bash -xv
ARG="/tmp/a b/1.txt"
ARG2="/tmp/a b/2.txt"
ARG_BOTH="\"$ARG\" \"$ARG2\""
cat $ARG_BOTH
I'm getting the following when it runs:
ARG_BOTH="$ARG $ARG2"
+ ARG_BOTH='/tmp/a\ b/1.txt /tmp/a\ b/2.txt'
cat $ARG_BOTH
+ cat '/tmp/a\' b/1.txt '/tmp/a\' b/2.txt
cat: /tmp/a\: No such file or directory
cat: b/1.txt: No such file or directory
cat: /tmp/a\: No such file or directory
cat: b/2.txt: No such file or directory
See http://mywiki.wooledge.org/BashFAQ/050
TLDR
Put your args in an array and call your program as myutil "${arr[#]}"
#!/bin/bash -xv
file1="file with spaces 1"
file2="file with spaces 2"
echo "foo" > "$file1"
echo "bar" > "$file2"
arr=("$file1" "$file2")
cat "${arr[#]}"
Output
file1="file with spaces 1"
+ file1='file with spaces 1'
file2="file with spaces 2"
+ file2='file with spaces 2'
echo "foo" > "$file1"
+ echo foo
echo "bar" > "$file2"
+ echo bar
arr=("$file1" "$file2")
+ arr=("$file1" "$file2")
cat "${arr[#]}"
+ cat 'file with spaces 1' 'file with spaces 2'
foo
bar
This might be a good use-case for the generic "set" command, which sets the top-level shell parameters to a word list. That is, $1, $2, ... and so also $* and $# get reset.
This gives you some of the advantages of arrays while also staying all-Posix-shell-compatible.
So:
set "arg with spaces" "another thing with spaces"
cat "$#"
The most straightforward revision of your example shell script that will work correctly is:
#! /bin/sh
ARG="/tmp/a b/1.txt"
ARG2="/tmp/a b/2.txt"
cat "$ARG" "$ARG2"
However, if you need to wrap up a whole bunch of arguments in one shell variable, you're up a creek; there is no portable, reliable way to do it. (Arrays are Bash-specific; the only portable options are set and eval, both of which are asking for grief.) I would consider a need for this as an indication that it was time to rewrite in a more powerful scripting language, e.g. Perl or Python.

Resources