Bash, is subshell output implicitly quoted - linux

In the following example
for f in *
do
res=$(ls -la "$f")
echo "$res"
done
Is this the correct way to use quotes? I know I should always quote the variable, but is the subshell output implicitly quoted?

The output from command substitution ($()) is not implicitly quoted:
$ for i in $(echo "foo bar"); do echo $i; done
foo
bar
The loop above splits the unquoted output along words. We can prevent this behavior by quoting the result:
$ for i in "$(echo "foo bar")"; do echo $i; done
foo bar
However, when assigning a variable, as in your example, the result of the subshell is not split, even without quotes:
$ baz=$(echo "foo bar")
$ echo "$baz"
foo bar
Unlike StackOverflow's syntax highlighting, the shell understands quotes inside command substitution, so we don't need to escape nested quotes:
$ baz="foo"
$ echo "$(echo "$baz $(echo "bar")")"
foo bar

Related

Bash call variables in for loop variables

I am curious to know that whether it is possible in bash that we can run for loop on a bunch of variables and call those values within for loop. Example:
a="hello"
b="world"
c="this is bash"
for f in a b c; do {
echo $( $f )
OR
echo $ ( "$f" )
} done
I know this is not working but can we call the values saved in a, b and c variables in for loop with printing f. I tried multiple way but unable to resolve.
You need the ! like this:
for f in a b c; do
echo "${!f}"
done
You can also use a nameref:
#!/usr/bin/env bash
a="hello"
b="world"
c="this is bash"
declare -n f
for f in a b c; do
printf "%s\n" "$f"
done
From the documentation:
If the control variable in a for loop has the nameref attribute, the list of words can be a list of shell variables, and a name reference will be established for each word in the list, in turn, when the loop is executed.
Notes on the OP's code, (scroll to bottom for corrected version):
for f in a b c; do {
echo $( $f )
} done
Problems:
The purpose of { & } is usually to put the separate outputs of
separate unpiped commands into one stream. Example of separate
commands:
echo foo; echo bar | tac
Output:
foo
bar
The tac command puts lines of input in reverse order, but in the
code above it only gets one line, so there's nothing to reverse.
But with curly braces:
{ echo foo; echo bar; } | tac
Output:
bar
foo
A do ... done already acts just like curly braces.
So "do {" instead of a "do" is unnecessary and redundant; but it
won't harm anything, or have any effect.
If f=hello and we write:
echo $f
The output will be:
hello
But the code $( $f ) runs a subshell on $f which only works if $f is
a command. So:
echo $( $f )
...tries to run the command hello, but there probably is no such
command, so the subshell will output to standard error:
hello: command not found
...but no data is sent to standard output, so echo will
print nothing.
To fix:
a="hello"
b="world"
c="this is bash"
for f in "$a" "$b" "$c"; do
echo "$f"
done

Define bash variable to be evaluated every time it is used

I want to define bash a variable which will be evaluated every time it is used.
My goal is to define two variables:
A=/home/userA
B=$A/my_file
So whenever I update A, B will be updated with the new value of A
I know how to do it in prompt variables, but, is there a way to do it for regular variables?
If you have Bash 4.4 or newer, you could (ab)use the ${parameter#P} parameter expansion, which expands parameter as if it were a prompt string:
$ A='/home/userA'
$ B='$A/my_file' # Single quotes to suppress expansion
$ echo "${B#P}"
/home/userA/my_file
$ A='/other/path'
$ echo "${B#P}"
/other/path/my_file
However, as pointed out in the comments, it's much simpler and more portable to use a function instead:
$ appendfile() { printf '%s/%s\n' "$1" 'my_file'; }
$ A='/home/user'
$ B=$(appendfile "$A")
$ echo "$B"
/home/user/my_file
$ A='/other/path'
$ B=$(appendfile "$A")
$ echo "$B"
/other/path/my_file
No. Use a simple and robust function instead:
b() {
echo "$a/my_file"
}
a="/home/userA"
echo "b outputs $(b)"
a="/foo/bar"
echo "b outputs $(b)"
Result:
b outputs /home/userA/my_file
b outputs /foo/bar/my_file
That said, here's one ugly way of fighting the system accomplish your goal verbatim:
# Trigger a re-assignment after every single command
trap 'b="$a/my_file"' DEBUG
a="/home/userA"
echo "b is $b"
a="/foo/bar"
echo "b is $b"
Result:
b is /home/userA/my_file
b is /foo/bar/my_file

Bash: highlight command before execution (set -x)

I have a bash script which executes about 20 commands and for debugging purposes I find myself scrolling through the output a lot.
Unfortunately bash doesn't tell me which part of the output is part of what command.
When I use "set -x" in the script it at least prints some information on what it just executed, but I don't really like the output it generates.
For instance, if I have this script:
#!/bin/bash
set -x
echo "foo"
if [ "$ASD" == "QWE" ] ; then
echo "bar"
fi
I would like the output to be something like this:
echo "foo"
foo
echo "bar"
bar
Or maybe:
echo "foo"
foo
if [ "value_of_ASD" == "QWE" ] ; then
echo "bar"
bar
fi
Instead of printing the commands in bold, highlighting with a color would also be okay. But I don't just want to have "+" characters in front of the commands and I also don't like the if statements showing up like '[' value_of_ASD == QWE ']'.
How can I accomplish that with bash?
At the moment the output looks like this btw:
+ echo foo
foo
+ '[' value_of_ASD == QWE ']'
+ echo bar
bar
Edit:
One idea I had was to write a script that I would source in the very beginning of the main script and then let the sourced script parse the main one. Something like this:
source_me.sh
#!/bin/bash
SCRIPT_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/$(basename $0)"
FORMAT_SET_BOLD='\033[0;1m'
FORMAT_RESET='\033[0m'
cat $SCRIPT_PATH | tail -n "+$START_LINE" | while read line; do
printf "${FORMAT_SET_BOLD}${line}${FORMAT_RESET}\n"
eval "${line}"
done
exit 0;
main.sh
#!/bin/bash
START_LINE=$((LINENO+1)) source ./source_me.sh
echo "Foo"
echo "Bar"
echo "+Hello"
The output in that case is:
echo "Foo"
Foo
echo "Bar"
Bar
echo "+Hello"
+Hello
But this method will fail if I use more complex code that goes over multiple lines (if statements, loops etc):
#!/bin/bash
START_LINE=$((LINENO+1)) source ./source_me.sh
echo "Foo"
if [ "$FOOBAR" == "" ] ; then
echo "Bar"
fi
echo "+Hello"
In this case I get:
echo "Foo"
Foo
if [ "$FOOBAR" == "" ] ; then
./source_me.sh: eval: line 9: syntax error: unexpected end of file
echo "Bar"
Bar
fi
./source_me.sh: eval: line 8: syntax error near unexpected token ´fi'
./source_me.sh: eval: line 8: ´fi'
echo "+Hello"
+Hello
I would like to extend rubo77's answer with a few examples that I think deserve a separate answer:
Plain text prefix
So the basic example is to set PS4 to some plain text, e.g.:
PS4="# "; set -x
Which will result in:
Color & extra line text prefix
But because you can use special characters and ANSI escape codes you can for example add a new line before each new command and print the prefix in a color, e.g.:
PS4="\n\033[1;33m>>>\033[0m "; set -x
Result:
Dynamic color prefix
Finally you can make the command prefix call other programs with each use, which you can use to add a timestamp, e.g.:
# yes, there have to be single quotes below, not double!
PS4='\033[1;34m$(date +%H:%M:%S)\033[0m '; set -x
Result:
You can change the + to a string of your desire by setting PS4, e.g.:
PS4="# "; set -x
Note that set -x has no innate knowledge of your terminal, which would be required to send the correct characters to create bold or coloured text.
If you can capture all output through a single file handle, you can probably do this with a pipe:
$ /path/to/your/script 2>&1 | sed "/^+ /s/.*/$(tput bold)&$(tput sgr0)/"
You might want to man tput for more information about the tool we're using to get bold type.
The viability of this depends on many factors I don't know about your environment. YMMV, may contain nuts.
Note:
You might think it was better to capture stderr separately with a line like this:
$ /path/to/your/script 2> >(sed "/^+ /s/.*/$(tput bold)&$(tput sgr0)/")
Alas, this doesn't behave consistently, because stderr passing through the pipe may not be submitted to the terminal before subsequent stdout from the script itself. Note also that this solution, because it uses process substitution, is a bashism and is not portable to POSIX.
What you are looking for is set -v.
-v prints out the line the interpreter just read.
-x prints out the post-parsing results of the line that the interpreter just read.
file x:
set -vx
foo=1
echo $foo
set +vx
Executing:
$: . x
foo=1
++ foo=1
echo $foo
++ echo 1
1
set +vx
++ set +vx

what does echo ${1+"$#"} mean

From /usr/local/bin/erl
ROOTDIR=/usr/local/lib/erlang
BINDIR=$ROOTDIR/erts-5.9.1/bin
EMU=beam
PROGNAME=`echo $0 | sed 's/.*\///'`
export EMU
export ROOTDIR
export BINDIR
export PROGNAME
exec $BINDIR/erlexec ${1+"$#"}
I know "$#" meams arguments. But {1+"$#"} means what?
From IEEE Std 1003.1 (POSIX 2013), Shell command language:
${parameter:+[word]}
Use Alternative Value. If parameter is unset or null, null shall be substituted; otherwise, the expansion of word (or an empty string if word is omitted) shall be substituted.
I.e., ${1+"$#"} expands to the value of "$#", the command line arguments, except when $1 is not set, i.e. there are no command line arguments, in which case the expression expands to nothing. A simpler script that shows how this works is
echo '"' ${1+"$#"} '"'
If you store this in a file test.sh and run it, you get:
/tmp$ sh test.sh
" "
/tmp$ sh test.sh 1
" 1 "
/tmp$ sh test.sh 1 2
" 1 2 "
(The spaces at the begin and end come from echo.)
larsmans provides an explanation of the semantics, but doesn't clarify why ${1+"$#"} is different than simply "${#}". In a properly behaving shell, "${#}" expands to nothing. That is: foo "$#" should call foo with no arguments if "$#" is empty. In an incorrectly behaving shell, foo "$#" will be invoked with one argument (the empty string). Many historical shells expanded "$#" to the empty string rather than to nothing, but ${1+"$#"} correctly expands to nothing.

turn off bash variable substitution

function ctrace {
echo "+ $#"
"$#"
}
ctrace echo "hi"
How would I get this function to output (with quotes):
echo "hi"
In this version the quotes are lost echo hi... Here is another example:
a=b
ctrace echo $a
This should output echo $a instead of echo b
The problem is not the function, but the caller.
In the first case the quotes are stripped out before the function gets the parameters. In the second, $a substitution is done before it gets to the function.
Try:
ctrace 'echo "hi"'
ctrace 'echo $a'
You need to enclose the string within single quotes
ctrace 'echo "hi"'
ctrace 'echo $a'

Resources