Bash pass all arguments as one quoted argument to another command - bash

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.

Related

Issue understanding a parameter expansion in a bash script

I am trying to understand what a parameter expansion does inside a bash script.
third_party_bash_script
#!/bin/sh
files="${*:--}"
# For my understanding I tried to print the contents of files
echo $files
pkill bb_stream
if [ "x$VERBOSE" != "" ]; then
ARGS=-v1
fi
while [ 1 ]; do cat $files; done | bb_stream $ARGS
When I run ./third_party_bash_script, all it prints is a hyphen - and nothing else. Since it did not make sense to me, I also tried to experiment with it in the terminal
$ set one="1" two="2" three="3"
$ files="${*:--}"
$ echo $files
one="1" two="2" three="3"
$ set four="4"
$ files="${*:--}"
four="4"
I can't seem to understand what it's doing. Could someone kindly help me with the interpretation of ${*:--} by the sh?
"$#" is an array of the arguments passed to your script, "$*" is a string of all of those arguments concatenated with blanks in between.
"${*:--}" is the string of arguments if any were provided (:-), or - otherwise which means "take input from stdin" otherwise.
"${#:--}" is the array of arguments if any were provided (:-), or - otherwise which means "take input from stdin" otherwise.
$ cat file
foo
bar
$ cat tst.sh
#!/usr/bin/env bash
awk '{ print FILENAME, $0 }' "${#:--}"
When an arg is provided to the script, "$#" contains "file" so that is the arg that awk is called with:
$ ./tst.sh file
file foo
file bar
When no arg is provided to the script, "$#" is empty so awk is called with - (meaning read from stdin) as it's arg:
$ cat file | ./tst.sh
- foo
- bar
You almost always want to use "${#:--}" rather than "${*:--}" in this context, see https://unix.stackexchange.com/questions/41571/what-is-the-difference-between-and for more info on "$#" vs "$*".
${param:-default} expands to the value of $param if $param is set and not empty, otherwise it expands to default.
$* is all the command-line arguments to the script.
In ${*:--}, param is * and default is -. If $* is not an empty string, it expands to the script arguments. If it's empty, it expands to the default value -.
This can be used in a script to implement the common behavior that a program reads from the files listed in its arguments, and reads from standard input if no filename arguments are given. Many commands treat an input filename argument - as standing for the standard input.
NOTE: addressing OP's original, pre-edited post ...
See shell parameter expansion for a brief review of different options.
While the other answers reference the use of ${*:--} (and ${#:--}) as a alternate means of reading from stdin, OP's sample script is a bit simpler ... if the variable $* (ie, script's command line args) is empty then replace with the literal string -.
We can see this with a few examples:
$ third_party_bash_script
-
$ third_party_bash_script a b c
a b c
$ echo 'a b c' | third_party_bash_script
-
If we replace ${*:--} with ${*:-REPLACEMENT}:
$ third_party_bash_script
REPLACEMENT
$ third_party_bash_script a b c
a b c
$ echo 'a b c' | third_party_bash_script
REPLACEMENT
I'm guessing in OP's actual script there's more going on with the $files variable so in order to know for sure how the ${*:--} is being processed we'd need to see the actual script and how it's referencing the $files variable.
As for OP's set|files=|echo code snippets:
$ set one="1" two="2" three="3"
$ files="${*:--}"
$ echo $files
one=1 two=2 three=3
We can see the same behavior from the script with:
$ third_party_bash_script one="1" two="2" three="3"
one=1 two=2 three=3

Bash removing duplicate in a string list of argument $#

In an bash shell script I am receiving a list of argument in $# which are : a b c d a c e
Depending of the argument I need to do something specific with a case structure.
But I only want to do it for a b c d e
I should only use bash and not any other language...I can use awk for example
for argument in "$#"
do
case $argument in
a)
.....
Have tried many things but without success
Any help much appreciated
Use an associative array to track arguments you've already seen. Note that this requires bash 4.0 or later; the 3.2.x release that Apple ships is too old, as it only supports numerically-indexed arrays (declare -a, but not declare -A).
#!/usr/bin/env bash
case $BASH_VERSION in ''|[0-3].*) echo "ERROR: Bash 4.0+ required" >&2; exit 1;; esac
declare -A seen=( )
declare -a deduped=( )
for arg in "$#"; do # iterate over our argument list
[[ ${seen[$arg]} ]] && continue # already seen this? skip it
seen[$arg]=1 # mark as seen going forward...
deduped+=( "$arg" ) # ...and add to our new/future argv
done
set -- "${deduped[#]}" # replace "$#" with contents of deduped array
You could print, then sort unique, then print quoted the list and re-eval into arguments. With GNU tools:
set -- a b c d a c e
tmp=$(printf "%s\0" "$#" | sort -uz | xargs -0 printf "%q ")
eval set -- "$tmp"

Bash: parse shell command string

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

How should I properly loop over a list on a gitconfig alias?

I created an alias on .gitconfig:
[alias]
test = "!f() { \
for l in "$#"; do \
echo "$l"; \
done; \
}; f"
Considering the command :
git test "foo bar" last
I was expecting to get:
foo bar
last
However, I got:
foo
bar
last
It is a different behavior from a similar 'for' command on bash. Why did it happen? And how should I make this command to work properly?
The problem is that Git strips double quotes from alias expansions. Your alias definition, shrunken to a single line, is:
"!f() { for l in "$#"; do echo "$l"; done; }; f"
Git strips off all the unescaped double quotes (and of course the leading !), producing:
f() { for l in $#; do echo $l; done; }; f
You want the shell to see:
f() { for l in "$#"; do echo "$l"; done; }; f
which means you must give this to Git:
"!f() { for l in \"$#\"; do echo \"$l\"; done; }; f"
Now that each of the internal double quotes are escaped, Git will leave them there for the shell to see.
(If your script gets complex, I recommend not trying to write it as an alias. If you have an executable script named git-abc anywhere on your $PATH and you run git abc, Git will run your git-abc script. Note that when Git runs your script, Git has augmented its $PATH to include the git-core directory, so that you can use . git-sh-setup to obtain various useful subroutines.)
Before post, I found out a way to achieve what I need. I rewrote the alias to:
[alias]
test = "!f() { IFS='\n'; \
for l in "$#"; do \
echo "$l"; \
done; \
}; f"
The IFS redefinition did the trick. I am just not sure if this is the best way to do it. I would appreciate different takes on the subject.

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