Arrays in Shell Script, not Bash - linux

I am probably just having a brain fart, but I can not for the life of me figure out how to loop through an array in shell script, not bash. Im sure the answer is on stackoverflow somewhere already, but I can not find a method of doing so without using bash. For my embedded target system bash is not currently an option. Here is an example of what I am attempting to do and the error that is returned.
#!/bin/sh
enable0=1
enable1=1
port=0
while [ ${port} -lt 2 ]; do
if [ ${enable${port}} -eq 1 ]
then
# do some stuff
fi
port=$((port + 1))
done
Whenever I run this script the error "Bad substitution" is returned for line with the if statement. If you guys have any ideas I would greatly appreciate it. Thanks!

a="abc 123 def"
set -- $a
while [ -n "$1" ]; do
echo $1
shift
done
Output via busybox 1.27.2 ash:
abc
123
def

BusyBox provides ash which does not directly provide array support. You could use eval and something like,
#!/bin/busybox sh
enable0=0
enable1=1
for index in 0 1 ; do
eval assign="\$enable$index"
if [ $assign == 1 ]; then
echo "enable$index is enabled"
else
echo "enable$index is disabled"
fi
done

One could use positional parameters for that...
http://pubs.opengroup.org/onlinepubs/009696799/utilities/set.html
#!/bin/sh
enable0=0
enable1=1
set -- $enable0 $enable1
for index in 0 1; do
[ "$1" -eq 1 ] && echo "$1 is enabled." || echo "$1 is disabled."
shift
done
Running on busybox:
~ $ ./test.sh
0 is disabled.
1 is enabled.

It's best not to use eval unless there is no other alternative. (The recent spate of bash exploits is due to the shell internally evaling the contents of environment variables without verifying their contents first). In this case, you seem to be in complete control for the variables involved, but you can iterate over the variable values without using eval.
#!/bin/sh
enable0=1
enable1=1
for port_enabled in "$enable0" "$enable1"; do
if [ "$port_enabled" -eq 1 ]; then
# do some stuff
fi
done

Related

bash until not meeting condition [duplicate]

Wondering if it's possible to finagle this logic (checking a variable for changes over time and running a loop while true) into a bash if statement or while loop condition. I was hoping for something like:
var=$(du -h *flat*.vmdk)
var2=$(sleep 1 ; du -h *flat*.vmdk)
if [[ $var != $var2 ]]; then
while true
do
echo -ne $(du -h *flat*.vmdk)\\r
sleep 1
done
else
echo "Transfer complete"
fi
I've also played with a while loop, rather than an if then with no luck.
while [ $var != $var2 ] ; do echo -ne $(du -h *flat*.vmdk)\\r ; sleep 1 ; done
But I'm seeing that's not possible? Or I'm having issues where things are incorrectly getting expanded. I'm open to any solution, although I am limited by a very basic shell (ESXi Shell) where many common unix/shell tools may not be present.
You are doing while [ $var != $var2 ] but never updating any of these variables ..
I would do something like:
function get_size() {
echo $(du -h *flat*.vmdk)
}
var="$(get_size)"
sleep 1
var2="$(get_size)"
while [ $var != $var2 ]; do
var=$var2
var2="$(get_size)"
echo -ne "$(get_size)\\r"
sleep 1
done
echo "Transfer complete"
What it does:
Use a function, because when you have to write two times or more a same line, it should trigger a "I should make it a function" in your brain.
Updating $var and $var2 within the while loop, so you don't check the same exact values each time, but check diff between last value and current one.
Add newlines to your code, because code is done to be read by humans, not machines, humans does not likes one-liners :)
I've not tested it
Not a generic solution, but if what you need is to wait while file keeps on changing, you can simply monitor it's modification timestamp with find (taking that this command is available), like that:
while find . -name *flat*.vmdk -newermt $(date --date "-1 second" +#%s)|read
do
sleep 1
done
echo "Transfer Completed !"
w/o using any variables at all.
I like #zeppelin's approach and I think I would have used it, but the date command in my environment was limited and I wasn't looking to invest any more time trying to figure that out. I did go with Arount's solution with a few modifications as seen below:
get_size() {
echo $(du -h *flat*.vmdk)
}
update() {
var="$(get_size)"
sleep 2
var2="$(get_size)"
}
update
while [ "$var" != "$var2" ]; do
update
echo -ne "$(get_size)\\r"
sleep 1
done
echo "Transfer complete"
The changes I needed:
ESXi Shell uses sh/Dash so I wasn't able to use the proposed function get_size() {
For whatever reason, the variables always matched until I created the update function to run in and outside the while loop.
Works well/as expected now. Thank you everyone for your help...hope it helps someone else.

How to recover when stuck by executing wrong shell commands?

I'm using Git Bash 2.9.0-64-bit in win7 64bit.
Here is my shell code
function sum1_x(){
a=$1
while [ $a -ge 1 ]; do
sum=$[$sum + $a]
a=$[$a - 1]
done
echo $sum
}
sum $1
In Git Bash,my type history is as follows:
wen#PC-WEN MINGW64 /d/git/ (dev)
$sh sum1_x.sh
exit
:q
quit
quit()
exit()
After I typed sh sum1_x.sh,I can no longer execute any shell command.I tried many commands as shows.I know there are bugs in my script,but how can I get back to execute shell script facing this kind of problems? Nothing can I do now.
You have several problems with your script:
you have function sum1_x but you are calling sum $1
You didn't check that input value exists, because of it you stuck in infinite loop
Here is the corrected script with check that input argument is exists:
function sum1_x(){
a=$1
while [ $a -ge 1 ]; do
sum=$[$sum + $a]
a=$[$a - 1]
done
echo $sum
}
if [ -z "$1" ]; then # check that input parameter is exists
echo "No input"
else
sum1_x $1
fi
How to stop program:
Ctrl + 'c'
Open new mingw and find pid of the progrman via ps -aux | grep "sum1_x.sh"
and then use kill pid to kill the program
As shows in the question,the shell script contains many bugs.The command sh sum1_x.sh was wrong too.It should be replaced by command sh sum1_x.sh 9 or things like it.After I fixed these bugs,it runs without problems and print the right sum.
But I still didn't know how to shop the shell script.Fortunately,By accident,I clicked Ctrl & c,and I can input shell commands again.

verifing some arguments in bash

I'm doing a verification in BASH
if [ !( ( -e $f ) || ($# -lt 2) || ( -d $f ) ) ]; then
exit 0
fi
I'm trying to see if the file existes or if it's a directory or enough args are passed via terminal. Can I do this or must it be done in another way?
I gather from your question you want to reject anything that is not an ordinary file. In that case, the test for a directory is redundant if you use -f instead of -e.
if [[ ! -f "$f" || $# -lt 2 ]]; then
exit 0
fi
By the way, if this is an error exit, you should exit with something other than 0 - 0 indicates success in most cases; exit 1 would be better.
It looks like you have at least a few problems here.
The syntax for your test will require the more modern double [[ command instead of the legacy single [ command.
You're missing a fi at the end. (I edited this one in for you, with thanks to whoever modded it up.)
You may have the sense of your test reversed, but I'm not sure. It depends on what the rest of your script looks like.

Bash - Update terminal title by running a second command

On my terminal in Ubuntu, I often run programs which keep running for a long time. And since there are a lot of these programs, I keep forgetting which terminal is for which program, unless I tab through all of those. So I wanted to find a way to update my terminal title to the program name, whenever I run a command. I don't want to do it manually.
I use gnome-terminal, but answer shouldn't really depend on that. Basically, If I'm able to run a second command, then I can simply use gconftool command to update the title. So I was hoping to find a way to capture the command in bash and update the title after every command. How do I do that?
I have some answers for you :) You're right that it shouldn't matter that you're using gnome-terminal, but it does matter what command shell you're using. This is a lot easier in zsh, but in what follows I'm going to assume you're using bash, and that it's a fairly recent version (> 3.1).
First of all:
Which environment variable would
contain the current 'command'?
There is an environment variable which has more-or-less what you want - $BASH_COMMAND. There's only one small hitch, which is that it will only show you the last command in a pipe. I'm not 100% sure what it will do with combinations of subshells, either :)
So I was hoping to find a way to
capture the command in bash and update
the title after every command.
I've been thinking about this, and now that I understand what you want to do, I realized the real problem is that you need to update the title before every command. This means that the $PROMPT_COMMAND and $PS1 environment variables are out as possible solutions, since they're only executed after the command returns.
In bash, the only way I can think of to achieve what you want is to (ab)use the DEBUG SIGNAL. So here's a solution -- stick this at the end of your .bashrc:
trap 'printf "\033]0;%s\007" "${BASH_COMMAND//[^[:print:]]/}"' DEBUG
To get around the problem with pipes, I've been messing around with this:
function settitle () {
export PREV_COMMAND=${PREV_COMMAND}${#}
printf "\033]0;%s\007" "${BASH_COMMAND//[^[:print:]]/}"
export PREV_COMMAND=${PREV_COMMAND}' | '
}
export PROMPT_COMMAND=${PROMPT_COMMAND}';export PREV_COMMAND=""'
trap 'settitle "$BASH_COMMAND"' DEBUG
but I don't promise it's perfect!
Try this:
trap 'echo -ne "\033]2;$(history 1 | sed "s/^[ ]*[0-9]*[ ]*//g")\007"' DEBUG
Thanks to the history 1 it works even with complicated expressions like:
true && (false); echo $? | cat
For which approaches relying on $BASH_COMMAND or $# fail. For example simon's displays:
true | echo $? | cat
Thanks to Gilles and simon for providing inspiration.
I see what stoutie is trying to do, except it's a lot more work than needed. And doesn't cause all sorts of other potentially bad things that can occur as a result of redefining 'cd' and putting in all of that testing just to change directories. Bash has built in support for most of this.
You can put this in your .bashrc anywhere after you set your current PS1 prompt (this way it just prepends it)
# If this is an xterm set the titlebar to user#host:dir
case "$TERM" in
xterm*|rxvt*)
PS1="\[\e]0;\u#\h: \w\a\]$PS1"
;;
*)
;;
esac
The OP asked for bash, but others might be interested to learn that (as mentioned above) this is indeed a lot easier using the zsh shell. Example:
# Set window title to command just before running it.
preexec() { printf "\x1b]0;%s\x07" "$1"; }
# Set window title to current working directory after returning from a command.
precmd() { printf "\x1b]0;%s\x07" "$PWD" }
In preexec, $1 contains the command as typed (requires shell history to be enabled, which seems to be a fair assumption), $2 the expanded command (shell aliases etc.) and $3 the "very expanded" command (shell function bodies). (more)
I'm doing something like this, to show my pwd in the title, which could be modified to do whatever you want to do with the title:
function title { echo -en "\033]2;$1\007"; }
function cd { dir=$1; if [ -z "$dir" ]; then dir=~; fi; builtin cd "$dir" && title `pwd`; }
I just threw this in my ~/.bash_aliases.
Update
I ran into strange bugs with my original answer. I ended up picking apart the default Ubuntu PS1 and breaking it into parts only to realize one of the parts was the title:
# simple prompt
COLOR_YELLOW_BOLD="\[\033[1;33m\]"
COLOR_DEFAULT="\[\033[0m\]"
TITLE="\[\e]0;\u#\h:\w\a\]"
PROMPT="\w\n$ "
HUH="${debian_chroot:+($debian_chroot)}"
PS1="${COLOR_YELLOW_BOLD}${TITLE}${HUH}${PROMPT}${COLOR_DEFAULT}"
Without breaking into variables, it would look like this:
PS1="\[\033[1;33m\]\[\e]0;\u#\h:\w\a\]${debian_chroot:+($debian_chroot)}\w\n$ \[\033[0m\]"
I have tested three method, all is OK, use any one for your pleasure.
export PROMPT_COMMAND='echo -ne "\033]2;$(history 1 | sed "s/^[ ]*[0-9]*[ ]*//g")\007"'
trap 'echo -ne "\033]2;$(history 1 | sed "s/^[ ]*[0-9]*[ ]*//g")\007"' DEBUG
trap 'echo -ne "\e]0;"; echo -n $BASH_COMMAND; echo -ne "\a"' DEBUG
please note if use $BASH_COMMAND, it don't recognize bash alias, and use PROMPT_COMMAND show finished command, but use trap show running command.
Based on the the need to auto position putty windows I have modified my /etc/bash.bashrc file on a Debian/Ubuntu system. I have posted the full contents for completeness but the relevant bit to starts on the # Display command ... comment line.
# System-wide .bashrc file for interactive bash(1) shells.
# To enable the settings / commands in this file for login shells as well,
# this file has to be sourced in /etc/profile.
# If not running interactively, don't do anything
[ -z "$PS1" ] && return
# check the window size after each command and, if necessary,
# update the values of LINES and COLUMNS.
shopt -s checkwinsize
# set variable identifying the chroot you work in (used in the prompt below)
if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
debian_chroot=$(cat /etc/debian_chroot)
fi
# set a fancy prompt (non-color, overwrite the one in /etc/profile)
PS1='${debian_chroot:+($debian_chroot)}\u#\h:\w\$ '
# Display command run in title which allows us to distinguish Kitty/Putty
# windows and re-position easily using AutoSizer window utility. Based on a
# post here: http://mg.pov.lt/blog/bash-prompt.html
case "$TERM" in
xterm*|rxvt*)
# Show the currently running command in the terminal title:
# http://www.davidpashley.com/articles/xterm-titles-with-bash.html
show_command_in_title_bar()
{
case "$BASH_COMMAND" in
*\033]0*)
# The command is trying to set the title bar as well;
# this is most likely the execution of $PROMPT_COMMAND.
# In any case nested escapes confuse the terminal, so don't
# output them.
;;
*)
echo -ne "\033]0;${USER}#${HOSTNAME}: ${BASH_COMMAND}\007"
;;
esac
}
trap show_command_in_title_bar DEBUG
;;
*)
;;
esac
# Commented out, don't overwrite xterm -T "title" -n "icontitle" by default.
# If this is an xterm set the title to user#host:dir
#case "$TERM" in
#xterm*|rxvt*)
# PROMPT_COMMAND='echo -ne "\033]0;${USER}#${HOSTNAME}: ${PWD}\007"'
# ;;
#*)
# ;;
#esac
# enable bash completion in interactive shells
if ! shopt -oq posix; then
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
elif [ -f /etc/bash_completion ]; then
. /etc/bash_completion
fi
fi
# if the command-not-found package is installed, use it
if [ -x /usr/lib/command-not-found -o -x /usr/share/command-not-found/command-not-found ]; then
function command_not_found_handle {
# check because c-n-f could've been removed in the meantime
if [ -x /usr/lib/command-not-found ]; then
/usr/bin/python /usr/lib/command-not-found -- "$1"
return $?
elif [ -x /usr/share/command-not-found/command-not-found ]; then
/usr/bin/python /usr/share/command-not-found/command-not-found -- "$1"
return $?
else
printf "%s: command not found\n" "$1" >&2
return 127
fi
}
fi
You can set up bash such that it sends a certain escape sequence to the terminal every time it starts an external program. If you use the escape sequence that terminals use to update their titles, your problem should be solved.
I have used that before, so I know it is possible. but I cannot remember it off the top of my head and do not have time to research the details right now, though.
Some of the old methods were removed from gnome-terminal 3.14 due to these two bugs (724110 and 740188).
In Ubuntu 20.04
PS1=$PS1"\[\e]0;New_Terminal_Name\a\]"
\[ begin a sequence of non-printing characters
\e]0; is the char sequence for setting the terminal title. Bash identifies this sequence and set the tile with the following characters. Number 0 turns out to be the value to reference the title property.
New_Terminal_Name is the tile we gave
\a is the ASCII bell character, also in this case, it marks the end of the tile to read from Bash.
\] end a sequence of non-printing characters
We can create a function for future use
function set_title(){
if [ -z "$PS1_BACK" ]; # set backup if it is empty
then
PS1_BACK="$PS1"
fi
TITLE="\[\e]0;$*\a\]"
PS1="${PS1_BACK}${TITLE}"
}
Open the ~/.bashrc file in your home directory with a text editor and append the above function at the end of it. Save and close.
To use it immediately source it to the current terminal.
source ~/.bashrc
We can use it then like this
set_title <New terminal tab title>
My terminal window titler script
This dynamic backgrounded script show all running command with pid number and elapsed time in seconds, like if I run du -h | less, this will build title looking like:
204640 6 du -h | 204641 6 less
Then when no command (other than himself) are running, don't change the terminal title, so standard behaviours works normaly.
First run start backgroud task. Second run in same terminal ask for kill previous backgrounded task.
Save this into a file, set execute flag then run it without argument:
cat <<"EOF" >titleWin.sh
#!/bin/bash
## Ask for kill process if already started
mapfile -t pids < <(ps -C ${0##*/} ho pid)
for pid in ${pids[#]} ;do
if [[ $pid != $$ ]] && [ -d /proc/$pid ]; then
echo -n "STARTED: [$pid]: ${0##*/}. Kill them (Y/n)? "
read -rsn 1 act
case $act in
n|N ) echo No;;
* ) echo Yes;kill $pid ;;
esac
exit
fi
done
## Title win for xterm or screen (or tmux).
case $TERM in
xterm*|rxvt* ) titleFmt='\e];%s\a';;
screen* ) titleFmt='\ek%s\e\\';;
* ) echo "Unable to title window.";exit 1;;
esac
tty=$(tty)
## Date to epochseconds converter
exec {dateout}<> <(:)
exec {datein}> >(exec stdbuf -o0 date -f - +%s >&$dateout)
DPID=$!
trap "echo TRAP;kill $DPID" 1 2 3 6 9 15
# Main loop
while :;do
string=""
while read -r pid wday mon day time year cmd; do
if [[ $pid != $$ ]] && [[ $pid != $PPID ]] && [[ $pid != $BASHPID ]] &&
[[ $pid != $DPID ]] && [ "${cmd#*pid,lstart,cmd}" ] &&
[ -d /proc/$pid ] ;then
echo >&${datein} $wday $mon $day $time $year
read -ru $dateout date
string+="$pid $((EPOCHSECONDS-date)) $cmd | "
fi
done < <(exec ps --tty ${tty#*/dev/} ho pid,lstart,cmd)
[[ "$string" ]] && printf "$titleFmt" "${string% | }"
sleep .333
done &
EOF
chmod +x titleWin.sh
./titleWin.sh

busybox sh wrapper to add extra functionality

I need a simple busybox sh wrapper which will do:
IF "-Q" PARAMETER IS PROVIDED THEN
acommand ALL PARAMETERS BUT "-Q" 2>&1 1>/dev/null
ELSE
acommand ALL PARAMETERS
FI
Parameters may include spaces.
BTW I want to run the script with busybox sh and it doesn't support arrays.
It's possible to do it all in busybox's ash shell:
#!/bin/sh
for i in "${#}"
do
if [ "$i" = "-Q" ]
then
flagQ=1
else
args="$args \"$i\""
fi
done
if [ "$flagQ" = "1" ]
then
eval acommand "$args" 2>&1 1>/dev/null
else
eval acommand "$args"
fi
This uses bash arrays - but I see from the comments to another answer that the code isn't supposed to run under bash (despite the bash tag originally applied to the question); it is meant to run under the busybox shell.
I'm almost certain it doesn't answer the question because the question is substantially unanswerable given the limitations of busybox. In times past, I have used a custom program I called 'escape' to build up an argument string that can be eval'd to get the original arguments - spaces and all. But that requires support from outside the shell.
This solution only uses 'bash'. I'm not sure it is fully idiomatic bash code, but it works.
#!/bin/bash
i=0
Qflag=0
for arg in "$#"
do
if [ "X$arg" = "X-Q" ]
then Qflag=1
else args[$((i++))]=$arg
fi
done
if [ $Qflag = 1 ]
then exec acommand "${args[#]}" 2>&1 >/dev/null
else exec acommand "${args[#]}"
fi
The first loops builds up an array, args, with the arguments to the script, except it doesn't add '-Q' to the list and records its presence in variable Qflag.
The if statement at the end notes whether Qflag was set to 1, and if so, sends the errors from 'acommand' to standard output and sends regular standard output to /dev/null (which is different from the effect if the I/O redirections are reversed - that would send standard output to /dev/null and send standard error to the same place, forcing silence on 'acommand').
The use of 'exec' is a trivial optimization that simplifies exit status handling in this case.
Tested with 'acommand' that prints its arguments on separate lines:
#!/bin/sh
for arg in "$#"
do echo "$arg"
done
and with command lines such as:
bash wrapper.sh -c -d 'arg with spaces'
which produces the output:
-c
-d
arg with spaces
Obviously, with the I/O redirection in place, there is no output from:
bash wrapper.sh -c -Q -d 'arg with spaces'
However, if you omit the I/O redirection, you get to see the same output.
It's a pity that you need to handle spaces in the arguments otherwise this might work:
#!/bin/sh
Q=0
ARGS=
while [ $# -ge 1 ]; do
case $1 in
-Q)
Q=1
;;
*)
ARGS="$ARGS $1"
;;
esac
shift
done
if [ $Q -eq 1 ] ; then
acommand $ARGS 2>&1 1>/dev/null
else
acommand $ARGS
fi
EDIT:
So this version handles spaces, at the expense of interpreting back-ticks.
#!/bin/busybox ash
Q=0
ARGS=
while [ $# -ge 1 ]; do
case $1 in
-Q)
Q=1
;;
*)
ARGS="$ARGS \"$1\""
;;
esac
shift
done
if [ "$Q" -eq 1 ] ; then
eval acommand $ARGS 2>&1 1>/dev/null
else
eval acommand $ARGS
fi
I think to have a complete solution you are going to have to code it in C, which will be a bit ugly.

Resources