bash - Why does the "-e" at the beginning of my variable not get output with echo? - string

At the command prompt ($) I execute the commands:
$ stupid="-a hello"
$ echo $stupid
Echo produces:
-a hello
At the command prompt ($) I execute the commands:
$ stupid="-e hello"
$ echo $stupid
Echo produces:
hello
Why did the "-e" disappear?

Since $stupid is unquoted, it gets processed as flag of echo and enables interpretation of backslash escapes.
If you did:
$ stupid="-e hello"
$ echo "$stupid"
You would see value of $stupid echoed in its entirety:
-e hello
Because the resulting command after variable expansion would be
echo "-e hello"
In your case however, $stupid is first expanded and then then the command is executed as:
echo -e hello
It may become even more obvious if your variable value actually included an escaped character such as: foo="-e \ttext", try both echo $foo and echo "$foo" and see what happens.
Bottom line: double quoting your strings and or variable is usually the prudent thing to do.

Related

Bash script in bash variable

I have a bash script that arrives like:
SCRIPT=$(curl .... | parsing...)
echo $SCRIPT > myfile
But when I try to echo it in a file, some parts get evaluated. (Variables are substituted if any are defined, the * character is replaced by all files in the working directory, etc...)
Can I prevent bash from evaluating any content of a variable, while still echoing?
Yes, use double quotes for that. I'll demonstrate:
$ x='*'
$ echo $x
..list of files..
$ echo '$x'
$x
$ echo "$x"
*

Bash discards command line arguments when passing to another bash shell

I have a big script (call it test) that, after stripping out the unrelated parts, comes down to just this using which I can explain my question:
#!/bin/bash
bash -c "$#"
This doesn't work as expected. E.g. ./test echo hi executes the only the echo and the argument disappears!
Testing with various inputs I can see only $1 is passed to bash -c ... and rest are discarded.
But if I use a variable like:
#!/bin/bash
cmd="$#"
bash -c "$cmd"
it works as expected for all inputs.
Questions:
1) I would like to understand why the double quotes don't "pass" the entire command line arguments to bash -c .... What am I missing here (that it works perfectly fine when using an intermediate variable)?
2) Why does bash discard the rest of the arguments (except $1) without any error messages?
For example:
bash -c "ls" -l -a hi hello blah
simply runs echo and hi hello blah doesn't result in any errors at all?
(If possible, please refer to the bash grammar where this behaviour is documented).
1) I would like to understand why the double quotes don't "pass" the entire command line arguments to bash -c .... What am I missing here (that it works perfectly fine when using an intermediate variable)?
From info bash #:
#
($#) Expands to the positional parameters, starting from one. When the expansion occurs within double quotes, each parameter expands
to a separate word. That is, "$#" is equivalent to "$1" "$2" ....
Thus, bash -c "$#" is equivalent to bash -c "$1" "$2" .... In the case of ./test echo hi invocation, the expression is expanded to
bash -c "echo" "hi"
2) Why does bash discard the rest of the arguments (except $1) without any error messages?
Bash actually doesn't discard anything. From man bash:
If the -c option is present, then commands are read from the first non-option argument command_string. If there are arguments after the command_string, they are assigned to the positional parameters, starting with $0.
Thus, for the command bash -c "echo" "hi", Bash passes "hi" as $0 for the "echo" script.
bash -c "ls" -l -a hi hello blah
simply runs echo and hi hello blah doesn't result in any errors at all?
According to the rules mentioned above, Bash executes "ls" script and passes the following positional parameters to this script:
$0: "-l"
$1: "-a"
$2: "hi"
$3: "hello"
$4: "blah"
Thus, the command actually executes ls, and the positional parameters are unused in the script. You can use them by referencing to the positional parameters, e.g.:
$ set -x
$ bash -c "ls \$0 \$1 \$3" -l -a hi hello blah
+ bash -c 'ls $0 $1 $3' -l -a hi hello blah
ls: cannot access hello: No such file or directory
You should be using $* instead of $# to pass command line as string. "$#" expands to multiple quoted arguments and "$*" combines multiple arguments into a single argument.
#!/bin/bash
bash -c "$*"
Problem is with your $# it executes:
bash -c echo hi
But with $* it executes:
bash -c 'echo hi'
When you use:
cmd="$#"
and use: bash -c "$cmd" it does the same thing for you.
Read: What is the difference between “$#” and “$*” in Bash?

What's the point of eval/bash -c as opposed to just evaluating a variable?

Suppose you have the following command stored in a variable:
COMMAND='echo hello'
What's the difference between
$ eval "$COMMAND"
hello
$ bash -c "$COMMAND"
hello
$ $COMMAND
hello
? Why is the last version almost never used if it is shorter and (as far as I can see) does exactly the same thing?
The third form is not at all like the other two -- but to understand why, we need to go into the order of operations when bash in interpreting a command, and look at which of those are followed when each method is in use.
Bash Parsing Stages
Quote Processing
Splitting Into Commands
Special Operator Parsing
Expansions
Word Splitting
Globbing
Execution
Using eval "$string"
eval "$string" follows all the above steps starting from #1. Thus:
Literal quotes within the string become syntactic quotes
Special operators such as >() are processed
Expansions such as $foo are honored
Results of those expansions are split on characters into whitespace into separate words
Those words are expanded as globs if they parse as same and have available matches, and finally the command is executed.
Using sh -c "$string"
...performs the same as eval does, but in a new shell launched as a separate process; thus, changes to variable state, current directory, etc. will expire when this new process exits. (Note, too, that that new shell may be a different interpreter supporting a different language; ie. sh -c "foo" will not support the same syntax that bash, ksh, zsh, etc. do).
Using $string
...starts at step 5, "Word Splitting".
What does this mean?
Quotes are not honored.
printf '%s\n' "two words" will thus parse as printf %s\n "two words", as opposed to the usual/expected behavior of printf %s\n two words (with the quotes being consumed by the shell).
Splitting into multiple commands (on ;s, &s, or similar) does not take place.
Thus:
s='echo foo && echo bar'
$s
...will emit the following output:
foo && echo bar
...instead of the following, which would otherwise be expected:
foo
bar
Special operators and expansions are not honored.
No $(foo), no $foo, no <(foo), etc.
Redirections are not honored.
>foo or 2>&1 is just another word created by string-splitting, rather than a shell directive.
$ bash -c "$COMMAND"
This version starts up a new bash interpreter, runs the command, and then exits, returning control to the original shell. You don't need to be running bash at all in the first place to do this, you can start a bash interpreter from tcsh, for example. You might also do this from a bash script to start with a fresh environment or to keep from polluting your current environment.
EDIT:
As #CharlesDuffy points out starting a new bash shell in this way will clear shell variables but environment variables will be inherited by the spawned shell process.
Using eval causes the shell to parse your command twice. In the example you gave, executing $COMMAND directly or doing an eval are equivalent, but have a look at the answer here to get a more thorough idea of what eval is good (or bad) for.
There are at least times when they are different. Consider the following:
$ cmd="echo \$var"
$ var=hello
$ $cmd
$var
$ eval $cmd
hello
$ bash -c "$cmd"
$ var=world bash -c "$cmd"
world
which shows the different points at which variable expansion is performed. It's even more clear if we do set -x first
$ set -x
$ $cmd
+ echo '$var'
$var
$ eval $cmd
+ eval echo '$var'
++ echo hello
hello
$ bash -c "$cmd"
+ bash -c 'echo $var'
$ var=world bash -c "$cmd"
+ var=world
+ bash -c 'echo $var'
world
We can see here much of what Charles Duffy talks about in his excellent answer. For example, attempting to execute the variable directly prints $var because parameter expansion and those earlier steps had already been done, and so we don't get the value of var, as we do with eval.
The bash -c option only inherits exported variables from the parent shell, and since I didn't export var it's not available to the new shell.

Positional parameters for shell not working as expected

I am trying to learn bash commands, and some very basic commands are not working as I expect...http://www.tutorialspoint.com/unix/unix-special-variables.htm
http://i.stack.imgur.com/F5VGK.png
Script:
#!/bin/bash
name="john"
other="shawn"
echo $name
echo $other
echo $1
echo $2
echo $#
echo $#
Output:
$ new
john
shawn
0
$
$1, $2, etc and $# have special meaning in bash scripts. They refer to the arguments passed to the bash script, so if you have a script in a file called foo.sh like:
#!/bin/bash
echo "Number of arguments: $#";
echo "First argument: $1";
echo "Second argument: $2";
If you chmod +x foo.sh and then run:
./foo.sh first second
You will see:
Number of arguments: 2
First argument: first
Second argument: second
$1 refers to the first command line argument passed to the script. The script is foo.sh, so anything after the script name will become a command line argument.
The default command line argument separator is the "space", so when you type ./foo.sh first second, bash stores first into $1 and second into $2.
If you typed:
./foo.sh first second third FOURTH fifth
bash would store third in the variable $3, FOURTH in the variable $4, and so on.
Is your script named 'new' ? In that case run it as follows one by one and you will get an idea how this works:
./new
./new a
./new a b
when you ran your script you did not pass any arguments. the number of arguments passed to the scripts are show by echoing "echo $#". and your output clearly shows that the "echo $#" command returned "0" count. pass the argument when you call your script like below
./new argument1 argument2

How do I echo "-e"?

I want to echo a string that might contain the same parameters as echo. How can I do it without modifying the string?
For instance:
$ var="-e something"
$ echo $var
something
... didn't print -e
A surprisingly deep question. Since you tagged bash, I'll assume you mean bash's internal echo command, though the GNU coreutils' standalone echo command probably works similarly enough.
The gist of it is: if you really need to use echo (which would be surprising, but that's the way the question is written by now), it all depends on what exactly your string can contain.
The easy case: -e plus non-empty string
In that case, all you need to do is quote the variable before passing it to echo.
$ var="-e something"
$ echo "$var"
-e something
If the string isn't eaxctly an echo option or combination, which includes any non-option suffix, it won't be recognized as such by echo and will be printed out.
Harder: string can be -e only
If your case can reduce to just "-e", it gets trickier. One way to do it would be:
$ echo -e '\055e'
-e
(escaping the dash so it doesn't get interpreted as an option but as on octal sequence)
That's rewriting the string. It can be done automatically and non-destructively, so it feels acceptable:
$ var="-e something"
$ echo -e ${var/#-/\\055}
-e something
You noticed I'm actually using the -e option to interpret an octal sequence, so it won't work if you intended to echo -E. It will work for other options, though.
The right way
Seriously, you're not restricted to echo, are you?
printf '%s\n' "$var"
The proper bash way is to use printf:
printf "%s\n" "$var"
By the way, your echo didn't work because when you run:
var="-e something"
echo $var
(without quoting $var), echo will see two arguments: -e and something. Because when echo meets -e as its first argument, it considers it's an option (this is also true for -n and -E), and so processes it as such. If you had quoted var, as shown in other answers, it would have worked.
Quote it:
$ var="-e something"
$ echo "$var"
-e something
If what you want is to get echo -e's behaviour (enable interpretation of backslash escapes), then you have to leave the $var reference without quotes:
$ var="hi\nho"
$ echo $var
hi
ho
Or use eval:
$ var="hi\nho"
$ eval echo \${var}
hi\nho
$ var="-e hi\nho"
$ eval echo \${var}
hi
ho
Since we're using bash, another alternative to echo is to simply cat a "here string":
$ var="-e something"
$ cat <<< "$var"
-e something
$ var="-e"
$ cat <<< "$var"
-e
$
printf-based solutions will almost certainly be more portable though.
Try the following:
$ env POSIXLY_CORRECT=1 echo -e
-e
Due to shell aliases and built-in echo command, using an unadorned
echo interactively or in a script may get you different functionality
than that described here. Invoke it via env (i.e., env echo ...)
to avoid interference from the shell.
The environment variable POSIXLY_CORRECT was introduced to allow the user to force the standards-compliant behaviour. See: POSIX at Wikipedia.
Or use printf:
$ printf '%s\n' "$var"
Source: Why is bash swallowing -e in the front of an array at stackoverflow SE
Use printf instead:
var="-e bla"
printf "%s\n" "$var"
Using just echo "$var" will still fail if var contains just a -e or similar. If you need to be able to print that as well, use printf.

Resources