Bash Automatically replacing [0:100] with 1 - bash

I'm writing a simple graphing script that uses gnuplot, and I use a helper function to construct a .gscript file.
add_gscript() {
echo "->>"
echo $1
echo $1 >> plot.gscript
cat plot.gscript
}
However after passing the following argument into the function
echo "--"
echo 'set xrange [0:$RANGE]'
echo "--"
add_gscript "set xrange [0:100]"
where $RANGE has been defined beforehand, I get the following output
--
set xrange [0:$RANGE]
--
->>
set xrange 1
set datafile separator ","
set term qt size 800,640
set size ratio .618
set xrange 1
Is bash evaluating [0:100] to 1 somehow?

A fix was accurately described in comments on the question:
Always double-quote variable references to have their values treated as literals; without quoting, the values are (in most contexts) subject to shell expansions, including word splitting and pathname expansion. [0:100] happens to be a valid globbing pattern that matches any file in the current dir. named either 0 or : or 1. – mklement0
so echo "$1"; echo "$1" >> plot.gscript and any other unquoted vars. Good luck. – shellter
Double quoting the variables did indeed fix my issue. Thanks!

Related

Conditional on non-instantiated variable

I am new to Bash scripting, having a lot more experience with C-type languages. I have written a few scripts with a conditional that checks the value of a non-instantiated variable and if it doesn't exist or match a value sets the variable. On top of that the whole thing is in a for loop. Something like this:
for i in ${!my_array[#]}; do
if [ "${my_array[i]}" = true ]
then
#do something
else
my_array[i]=true;
fi
done
This would fail through a null pointer in Java since my_array[i] is not instantiated until after it is checked. Is this good practice in Bash? My script is working the way I designed, but I have learned that just because a kluge works now doesn't mean it will work in the future.
Thanks!
You will find this page on parameter expansion helpful, as well as this one on conditionals.
An easy way to test a variable is to check it for nonzero length.
if [[ -n "$var" ]]
then : do stuff ...
I also like to make it fatal to access a nonexisting variable; this means extra work, but better safety.
set -u # unset vars are fatal to access without exception handling
if [[ -n "${var:-}" ]] # handles unset during check
then : do stuff ...
By default, referencing undefined (or "unset") variable names in shell scripts just gives the empty string. But is an exception: if the shell is run with the -u option or set -u has been run in it, expansions of unset variables are treated as errors and (if the shell is not interactive) cause the shell to exit. Bash applies this principle to array elements as well:
$ array=(zero one two)
$ echo "${array[3]}"
$ echo "array[3] = '${array[3]}'"
array[3] = ''
$ set -u
$ echo "array[3] = '${array[3]}'"
-bash: array[3]: unbound variable
There are also modifiers you can use to control what expansions do if a variable (or array element) is undefined and/or empty (defined as the empty string):
$ array=(zero one '')
$ echo "array[2] is ${array[2]-unset}, array[3] is ${array[3]-unset}"
array[2] is , array[3] is unset
$ echo "array[2] is ${array[2]:-unset or empty}, array[3] is ${array[3]:-unset or empty}"
array[2] is unset or empty, array[3] is unset or empty
There are a bunch of other variants, see the POSIX shell syntax standard, section 2.6.2 (Parameter Expansion).
BTW, you do need to use curly braces (as I did above) around anything other than a plain variable reference. $name[2] is a reference to the plain variable name (or element 0 if it's an array), followed by the string "[2]"; ${name[2]}, on the other hand, is a reference to element 2 of the array name. Also, you pretty much always want to wrap variable references in double-quotes (or include them in double-quoted strings), to prevent the shell from "helpfully" splitting them into words and/or expanding them into lists of matching files. For example, this test:
if [ $my_array[i] = true ]
is (mostly) equivalent to:
if [ ${my_array[0]}[i] = true ]
...which isn't what you want at all. But this one:
if [ ${my_array[i]} = true ]
still doesn't work, because if my_array[i] is unset (or empty) it'll expand to the equivalent of:
if [ = true ]
...which is bad test expression syntax. You want this:
if [ "${my_array[i]}" = true ]

Bash multiple parameter substitution

I'm looking to set the value of a variable to one thing if it was already set or another if it was not. Here's an example of what I mean
export RESULT=${VALID:+Yes:-No}
Where the value of ${VALID:+Yes:-No} would be Yes if the variable was set or No if it was not.
One way I can do it now:
if [ -n "${VALID}" ]; then
export RESULT=Yes
else
export RESULT=No
fi
I could do it like this, but it would be nice to have a "one-liner".
Is it possible to do this in one line?
There's no way to do multiple parameter expansions within one variable assignement.
Without using an if statement, you can just use a couple of parameter expansions.
$ VALID="aaa"
$ RESULT="${VALID:+Yes}"; RESULT="${RESULT:-No}"; echo $RESULT
Yes
$ VALID=""
$ RESULT="${VALID:+Yes}"; RESULT="${RESULT:-No}"; echo $RESULT
No
It's not a single statement, but it's short enough that it fits on one line without the complexity of a subshell and if.
You can use an array with 2 elements (no yes) and based on variable's length decide which value to be returned:
# variable set case
>>> VALID=foo
>>> arr=(no yes) && echo "${arr[$((${#VALID} > 0))]}"
yes
# variable not set
>>> unset VALID
>>> arr=(no yes) && echo "${arr[$((${#VALID} > 0))]}"
no
The -v VAR printf option is a bash extension. Without it, you could use command substitution. (Variable names deliberately down-cased.)
printf -v result %.3s ${valid:+YES}NO
One line, requirement met:
export RESULT=$([ -n "$VALID" ] && echo Yes || echo No)

Substituting argument value in bash

I'm unable to substitute the argument value(s) in the bash command as below:
# echo $int1
{"id":"74953939-fd20-4472-8aaa-067e6f4c4106"}
# echo $int2
{"id":"5ef4664d-3600-4df9-a6a9-01ffb0f49422"}
# echo $int3
{"id":"6dc95c01-742e-4225-8298-e5750fe67f27"}
# set -x
# data set net-agent interfaces '["$int1", "$int2", "$int3"]'
+ data set net-agent interfaces '["$int1", "$int2", "$int3"]'
Any idea on why the values are not being substituted?
Thanks!
I'm guessing that the argument to the command should be valid JSON, in which case you should remove the double quotes from around each variable and wrap the entire string in double quotes so variables are expanded:
data set net-agent interfaces "[$int1, $int2, $int3]"
Using set -x, this produces:
$ data set net-agent interfaces "[$int1, $int2, $int3]"
+ data set net-agent interfaces '[{"id":"74953939-fd20-4472-8aaa-067e6f4c4106"}, {"id":"5ef4664d-3600-4df9-a6a9-01ffb0f49422"}, {"id":"6dc95c01-742e-4225-8298-e5750fe67f27"}]'

Detect empty command

Consider this PS1
PS1='\n${_:+$? }$ '
Here is the result of a few commands
$ [ 2 = 2 ]
0 $ [ 2 = 3 ]
1 $
1 $
The first line shows no status as expected, and the next two lines show the
correct exit code. However on line 3 only Enter was pressed, so I would like the
status to go away, like line 1. How can I do this?
Here's a funny, very simple possibility: it uses the \# escape sequence of PS1 together with parameter expansions (and the way Bash expands its prompt).
The escape sequence \# expands to the command number of the command to be executed. This is incremented each time a command has actually been executed. Try it:
$ PS1='\# $ '
2 $ echo hello
hello
3 $ # this is a comment
3 $
3 $ echo hello
hello
4 $
Now, each time a prompt is to be displayed, Bash first expands the escape sequences found in PS1, then (provided the shell option promptvars is set, which is the default), this string is expanded via parameter expansion, command substitution, arithmetic expansion, and quote removal.
The trick is then to have an array that will have the k-th field set (to the empty string) whenever the (k-1)-th command is executed. Then, using appropriate parameter expansions, we'll be able to detect when these fields are set and to display the return code of the previous command if the field isn't set. If you want to call this array __cmdnbary, just do:
PS1='\n${__cmdnbary[\#]-$? }${__cmdnbary[\#]=}\$ '
Look:
$ PS1='\n${__cmdnbary[\#]-$? }${__cmdnbary[\#]=}\$ '
0 $ [ 2 = 3 ]
1 $
$ # it seems that it works
$ echo "it works"
it works
0 $
To qualify for the shortest answer challenge:
PS1='\n${a[\#]-$? }${a[\#]=}$ '
that's 31 characters.
Don't use this, of course, as a is a too trivial name; also, \$ might be better than $.
Seems you don't like that the initial prompt is 0 $; you can very easily modify this by initializing the array __cmdnbary appropriately: you'll put this somewhere in your configuration file:
__cmdnbary=( '' '' ) # Initialize the field 1!
PS1='\n${__cmdnbary[\#]-$? }${__cmdnbary[\#]=}\$ '
Got some time to play around this weekend. Looking at my earlier answer (not-good) and other answers I think this may be probably the smallest answer.
Place these lines at the end of your ~/.bash_profile:
PS1='$_ret$ '
trapDbg() {
local c="$BASH_COMMAND"
[[ "$c" != "pc" ]] && export _cmd="$c"
}
pc() {
local r=$?
trap "" DEBUG
[[ -n "$_cmd" ]] && _ret="$r " || _ret=""
export _ret
export _cmd=
trap 'trapDbg' DEBUG
}
export PROMPT_COMMAND=pc
trap 'trapDbg' DEBUG
Then open a new terminal and note this desired behavior on BASH prompt:
$ uname
Darwin
0 $
$
$
$ date
Sun Dec 14 05:59:03 EST 2014
0 $
$
$ [ 1 = 2 ]
1 $
$
$ ls 123
ls: cannot access 123: No such file or directory
2 $
$
Explanation:
This is based on trap 'handler' DEBUG and PROMPT_COMMAND hooks.
PS1 is using a variable _ret i.e. PS1='$_ret$ '.
trap command runs only when a command is executed but PROMPT_COMMAND is run even when an empty enter is pressed.
trap command sets a variable _cmd to the actually executed command using BASH internal var BASH_COMMAND.
PROMPT_COMMAND hook sets _ret to "$? " if _cmd is non-empty otherwise sets _ret to "". Finally it resets _cmd var to empty state.
The variable HISTCMD is updated every time a new command is executed. Unfortunately, the value is masked during the execution of PROMPT_COMMAND (I suppose for reasons related to not having history messed up with things which happen in the prompt command). The workaround I came up with is kind of messy, but it seems to work in my limited testing.
# This only works if the prompt has a prefix
# which is displayed before the status code field.
# Fortunately, in this case, there is one.
# Maybe use a no-op prefix in the worst case (!)
PS1_base=$'\n'
# Functions for PROMPT_COMMAND
PS1_update_HISTCMD () {
# If HISTCONTROL contains "ignoredups" or "ignoreboth", this breaks.
# We should not change it programmatically
# (think principle of least astonishment etc)
# but we can always gripe.
case :$HISTCONTROL: in
*:ignoredups:* | *:ignoreboth:* )
echo "PS1_update_HISTCMD(): HISTCONTROL contains 'ignoredups' or 'ignoreboth'" >&2
echo "PS1_update_HISTCMD(): Warning: Please remove this setting." >&2 ;;
esac
# PS1_HISTCMD needs to contain the old value of PS1_HISTCMD2 (a copy of HISTCMD)
PS1_HISTCMD=${PS1_HISTCMD2:-$PS1_HISTCMD}
# PS1_HISTCMD2 needs to be unset for the next prompt to trigger properly
unset PS1_HISTCMD2
}
PROMPT_COMMAND=PS1_update_HISTCMD
# Finally, the actual prompt:
PS1='${PS1_base#foo${PS1_HISTCMD2:=${HISTCMD%$PS1_HISTCMD}}}${_:+${PS1_HISTCMD2:+$? }}$ '
The logic in the prompt is roughly as follows:
${PS1_base#foo...}
This displays the prefix. The stuff in #... is useful only for its side effects. We want to do some variable manipulation without having the values of the variables display, so we hide them in a string substitution. (This will display odd and possibly spectacular things if the value of PS1_base ever happens to begin with foo followed by the current command history index.)
${PS1_HISTCMD2:=...}
This assigns a value to PS1_HISTCMD2 (if it is unset, which we have made sure it is). The substitution would nominally also expand to the new value, but we have hidden it in a ${var#subst} as explained above.
${HISTCMD%$PS1_HISTCMD}
We assign either the value of HISTCMD (when a new entry in the command history is being made, i.e. we are executing a new command) or an empty string (when the command is empty) to PS1_HISTCMD2. This works by trimming off the value HISTCMD any match on PS1_HISTCMD (using the ${var%subst} suffix replacement syntax).
${_:+...}
This is from the question. It will expand to ... something if the value of $_ is set and nonempty (which it is when a command is being executed, but not e.g. if we are performing a variable assignment). The "something" should be the status code (and a space, for legibility) if PS1_HISTCMD2 is nonempty.
${PS1_HISTCMD2:+$? }
There.
'$ '
This is just the actual prompt suffix, as in the original question.
So the key parts are the variables PS1_HISTCMD which remembers the previous value of HISTCMD, and the variable PS1_HISTCMD2 which captures the value of HISTCMD so it can be accessed from within PROMPT_COMMAND, but needs to be unset in the PROMPT_COMMAND so that the ${PS1_HISTCMD2:=...} assignment will fire again the next time the prompt is displayed.
I fiddled for a bit with trying to hide the output from ${PS1_HISTCMD2:=...} but then realized that there is in fact something we want to display anyhow, so just piggyback on that. You can't have a completely empty PS1_base because the shell apparently notices, and does not even attempt to perform a substitution when there is no value; but perhaps you can come up with a dummy value (a no-op escape sequence, perhaps?) if you have nothing else you want to display. Or maybe this could be refactored to run with a suffix instead; but that is probably going to be trickier still.
In response to Anubhava's "smallest answer" challenge, here is the code without comments or error checking.
PS1_base=$'\n'
PS1_update_HISTCMD () { PS1_HISTCMD=${PS1_HISTCMD2:-$PS1_HISTCMD}; unset PS1_HISTCMD2; }
PROMPT_COMMAND=PS1_update_HISTCMD
PS1='${PS1_base#foo${PS1_HISTCMD2:=${HISTCMD%$PS1_HISTCMD}}}${_:+${PS1_HISTCMD2:+$? }}$ '
This is probably not the best way to do this, but it seems to be working
function pc {
foo=$_
fc -l > /tmp/new
if cmp -s /tmp/{new,old} || test -z "$foo"
then
PS1='\n$ '
else
PS1='\n$? $ '
fi
cp /tmp/{new,old}
}
PROMPT_COMMAND=pc
Result
$ [ 2 = 2 ]
0 $ [ 2 = 3 ]
1 $
$
I need to use great script bash-preexec.sh.
Although I don't like external dependencies, this was the only thing to help me avoid to have 1 in $? after just pressing enter without running any command.
This goes to your ~/.bashrc:
__prompt_command() {
local exit="$?"
PS1='\u#\h: \w \$ '
[ -n "$LASTCMD" -a "$exit" != "0" ] && PS1='['${red}$exit$clear"] $PS1"
}
PROMPT_COMMAND=__prompt_command
[-f ~/.bash-preexec.sh ] && . ~/.bash-preexec.sh
preexec() { LASTCMD="$1"; }
UPDATE: later I was able to find a solution without dependency on .bash-preexec.sh.

Error converting a bash function to a csh alias

I am writing a csh alias so that I can use the following bash function in my csh :
function up( )
{
LIMIT=$1
P=$PWD
for ((i=1; i <= LIMIT; i++))
do
P=$P/..
done
cd $P
export MPWD=$P
}
(I stole the above bash function from here)
I have written this:
alias up 'set LIMIT=$1; set P=$PWD; set counter = LIMIT; while[counter!=0] set counter = counter-1; P=$P/.. ; end cd $P; setenv MPWD=$P'
However, I am getting the following error:
while[counter!=0]: No match.
P=/net/devstorage/home/rghosh/..: Command not found.
end: Too many arguments.
and my script is not working as intended. I have been reading up on csh from here.
I am not an expert in csh and what I have written above is my first csh script. Please let me know what I am doing wrong.
You can also do this
alias up 'cd `yes ".." | head -n\!* | tr "\n" "\/"`'
yes ".." will repeat the string .. indefinitely; head will truncate it to the number passed as argument while calling the alias ( !* expands to the arguments passed; similar to $# ) and tr will convert the newlines to /.
radical7's answer seems to be more neat; but will only work for tcsh ( exactly wat you wanted ). This should work irrespective of the shell
You can use the csh's repeat function
alias up 'cd `pwd``repeat \!^ echo -n /..`'
No loops needed (which is handy, because while constructs in tcsh seem very finicky)
For multiple lines of code, aliases must be within single quotes, and each end of line must precede a backslash. The end of the last line must precede a single quote to delimit the end of the alias:
alias up 'set counter = $1\
set p = $cwd\
while $counter != 0\
# counter = $counter - 1\
set p = $p/..\
end\
cd $p\
setenv mpwd $p'
By the way, variables set with set are better with the equal sign separated from the variable name and content; setenv doesn't require an equal sign; math functionality is provided by #; control structures make use of parentheses (though aren't required for simple tests); use $cwd to print the current working directory.

Resources