Smarter and more condensed method to match input variable in shell script? - linux

I have a file with a list of numerous unique items, for this example I am using user ID's. The starting section of my script should display the list to the user running the script and allow them to chose one of the ID's. The script should then cross check the choice made by the user against the original file and if it matches it should provide a message advising of the match and continue with the script. If it does not match, the script should advise the user and exit.
My current script does this OK, but I was wondering if there is any way to make it a bit smarter/more condensed, perhaps using arrays? Current script:
This is my first post on this site so I apologies in advanced for any mistakes which have been made in the process of posting.
FILE=testfile
IDLIST="$(awk '{print $1}' $FILE)"
echo "$IDLIST"
echo "\nSelect one of the options"
read input
OUTPUT="$(for i in $IDLIST
do
if [[ $i = $input ]]
then
echo "Matched."
fi
done)"
if [[ -z $OUTPUT ]]
then
echo "Invalid choice."
exit 0
else
ID=$input
fi
echo "It is a match, continuing with script"

As you can imagine, there are many ways of doing this. One is using select instead:
PS3="Select an ID: "
select id in $(cut -d ' ' -f 1 testfile)
do
[[ -z $id ]] && echo "Pick a number" || break
done
echo "You selected $id"

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.

Am I setting this script up correctly to run specific commands based on user input?

I have a small script that I am working on. This is only the second script that I have made using bash script.
Basically what I am wanting this script to do is take the users input and fire a command based on that choice.
As you can see the user first enters the host address of the instance they are going to ssh into and ultimately tail logs on. There are a couple things that I am not understanding.
If / Then / Else / Elif - The concept seems simple enough but perhaps how these should be used eludes me.
When I run my script through a bash parser, the parser comes back with the following message:
Line 2:
if [ "$mainmenuinput" = "1" ]; then
^-- SC2154: mainmenuinput is referenced but not assigned.
mainmenu() {
if [ "$mainmenuinput" = "1" ]; then
ssh "$customerurl" tail -f /data/jirastudio/jira/j2ee_*/log/main/current
elif [ "$mainmenuinput" = "2" ]; then
ssh "$customerurl" tail -f /data/jirastudio/confluence/j2ee_*/log/main/current
elif [ "$mainmenuinput" = "3" ]; then
ssh "$customerurl" tail -f /data/jirastudio/horde/service/log/main/current
elif [ "$mainmenuinput" = "4" ]; then
ssh "$customerurl" tail -f /data/jirastudio/apache/logs/access_log
fi
}
printf "\nEnter the customers host URL:\n"
read -r customerurl
printf "Press 1 for JIRA\n"
printf "Press 2 for Confluence\n"
printf "Press 3 for Horde\n"
printf "Press 4 for Apache Access\n"
printf "Press 5 for Apache Error\n"
read -p -r "Make your choice:" "$mainmenuinput"
Looking up the SC2154 entry I found that it means this:
ShellCheck has noticed that you reference a variable that is not assigned. Double check that the variable is indeed assigned, and that the name is not misspelled.
I am a little confused on what that means. If someone can explain that, I would greatly appreciate it.
As it stands, when I run the script, it pauses to wait for the user to enter the host address. The user hits ENTER and the script then presents them with the menu to have them choose which log they want to tail. The menu looks a little odd:
Press 1 for JIRA
Press 2 for Confluence
Press 3 for Horde
Press 4 for Apache Access
Press 5 for Apache Error
-r
Im not sure why the -r is showing up at the end of the menu. When a selection is made, the script ends and outputs this:
./tail_logs.sh: line 23: read:Make your choice:': not a valid identifier`
Any help with this would be appreciated or if anything a push in the right direction. I love figuring this stuff out but sometimes, its helpful to get shoved at least in the general direction of the error/resolution.
Thanks
EDIT 1
Ok, I updated my script with your suggestions. It seemed to still balk at a few things. For example:
(mainmenu "$customerurl" "$mainmenuinput")
Using ShellCheck I got back this:
Line 1:
(mainmenu "$customerurl" "$mainmenuinput") {
^-- SC2154: customerurl is referenced but not assigned.
^-- SC2154: mainmenuinput is referenced but not assigned.
^-- SC1070: Parsing stopped here. Mismatched keywords or invalid parentheses?
If I write this out like:
mainmenu() { then it does not complain. Also, if I run the script with it typed out as per the suggested way, I get an error about `syntax error near unexpected token '{'
The current code looks like this:
#!/bin/sh
mainmenu() {
echo "$1"
echo "$2"
if [ "$2" = "1" ]; then
ssh "$1" tail -f "/data/jirastudio/jira/j2ee_*/log/main/current"
elif [ "$2" = "2" ]; then
ssh "$1" tail -f "/data/jirastudio/confluence/j2ee_*/log/main/current"
elif [ "$2" = "3" ]; then
ssh "$1" tail -f "/data/jirastudio/horde/service/log/main/current"
elif [ "$2" = "4" ]; then
ssh "$1" tail -f "/data/jirastudio/apache/logs/access_log"
elif [ "$2" > 4 || < 1 ]; then
echo "Uh uh uh, you didnt say the magic word! The number you picked isnt in the list. Pick again."
fi
}
echo
echo "Enter the customers host address:"
read -r customerurl
echo "Press 1 for JIRA"
echo "Press 2 for Confluence"
echo "Press 3 for Horde"
echo "Press 4 for Apache Access"
echo "Press 5 for Apache Error"
read -r -p "Pick a number: " mainmenuinput
I get no errors when running this. But when I make a selection, the script ends and does not output the tail command at all. Also, Im not sure if I am validating user input outside of 1-4 correctly with the last elif statement although if I change this to else I get an error when I run the script.
I think my issue is in the first part of the function?
mainmenu() {
echo "$1"
echo "$2"
Without having $hostAddress and mainMenuInput does the script not know what should be assigned to $1 and $2 or does it automatically assign the first thing typed in to these variables?
The main problems are with the read command at the end. First, whatever immediately follows the -p option is used as a prompt string; in this case, the next argument is "-r", so it prints that as a prompt. You clearly want "Make your choice:" to be the prompt, so that must go immediately after -p (i.e. use either read -r -p "Make your choice:" ... or read -p "Make your choice:" -r ...). Second, when you use $mainmenuinput, it replaces that with the current value of mainmenuinput. In the shell, you use $variable to get the value of a variable, not to set it. With both of these problems corrected, the last command becomes:
read -p "Make your choice:" -r mainmenuinput
There's also another important thing: after reading the users' input, you need to actually call the mainmenu function. So just add mainmenu as the last line.
As for the if ... then ... elif ... structure, yours looks fine; I'm not sure what the question is. Although personally I'd add an else clause that printed an error that the option was not valid.
I do have some stylistic/best practice recommendations, though:
It's best to pass information to functions in the form of arguments, rather than global variables. That is, rather than using customerurl and mainmenuinput directly in the function, pass them as arguments (mainmenu "$customerurl" "$mainmenuinput"), then reference those arguments ("$1" and "$2") inside the functions. This doesn't matter much in a small script like this, but having clear distinctions between the variables used by different parts of a program makes things much easier to keep straight in larger programs.
In shell scripts, printf is the best way to do complex things like printing lines without a linefeed at the end, or translating escape characters... but if you're just doing a standard print-a-line-with-a-linefeed-at-the-end, echo is simpler. Thus, I'd replace the various printf "something\n" commands with echo "something", and printf "\nEnter the customers host URL:\n" with:
echo
echo "Enter the customers host URL:"
In the command
ssh "$customerurl" tail -f /data/jirastudio/jira/j2ee_*/log/main/current
(or ssh "$1" ... if you follow my recommendation about arguments instead of global variables), the wildcard (*) will be expanded on the local computer before being handed to ssh and passed to the remote computer to be executed. It'd be best to quote that argument to prevent that:
ssh "$customerurl" tail -f "/data/jirastudio/jira/j2ee_*/log/main/current"
Note that the quotes will be removed before it's passed to ssh and then to the remote computer, so they will not prevent the wildcard from being expanded on the remote computer.
The thing you're calling a URL isn't actually a URL; URLs are things like "https://stackoverflow.com/questions". They start with a protocol (or "scheme") like "http" or "ftp", then "://", then a server name, then "/", etc. ssh just takes a raw server name (optionally with a username, in the form user#server).
Update, based on EDIT 1: I wasn't clear on how to call the function; your definition (using mainmenu() { ...) is correct, but having defined the function you then need to actually run the function. Do to this, change the end of the script to something like this:
...
echo "Press 5 for Apache Error"
read -r -p "Pick a number: " mainmenuinput
mainmenu "$customerurl" "$mainmenuinput"
This will run the function, with the first argument ($1) set to "$customerurl", and second argument ($2) set to "$mainmenuinput".
There's also a problem with the elif clause you added in the function. The shell's syntax for test expressions is really really weird (mostly for historical reasons). Also, there are three common variants, the original [ ... ] (which is actually a command) which has the weirdest syntax, bash's [[ ... ]] variant (much cleaner syntax, but not available available in generic POSIX shells), and (( ... )) (cleaner syntax, math- rather than string-oriented, not portable). See BashFAQ #31 for details.
For what you're trying to do, any of these would work:
elif [ "$2" -gt 4 -o "$2" -lt 1 ]; then
# [ ... ] doesn't use || or &&, and uses -lt etc for numeric comparisons.
# < and > do string comparisons, which are ... different. And you'd
# need to quote them to keep them from being mistaken for redirects.
# Also, you need to specify the "$2" explicitly for each comparison.
elif [[ "$2" -gt 4 || "$2" -lt 1 ]]; then
# [[ ... ]] uses || and &&, but still uses -lt etc for numeric comparisons.
# < and > still do string comparisons, but don't need to be quoted
elif (( $2 > 4 || $2 < 1 )); then
# All numeric here, so < and > work
But there's still a problem, since the user might have entered something that isn't a number at all (just pressed return, typed "wibble", etc.), and in all of these cases numeric comparison will fail. Solution: skip the test, and use else instead of elif:
...
elif [ "$2" = "4" ]; then
ssh "$1" tail -f "/data/jirastudio/apache/logs/access_log"
else
echo "Uh uh uh, you didnt say the magic word! The number you picked isnt in the list. Pick again."
fi
}
... that way, if any of the previous conditions aren't met for any reason at all, it'll print the error message.

How to create a shell script that can scan a file for a specific word?

one of the questions that I have been given to do for my Computer Science GCSE was:
Write a shell script that takes a string input from a user, asks for a file name and reports whether that string is present in the file.
However way I try to do it, I cannot create a shell script.
I don't need you to tell me the whole number, however, I have no idea where to start. I input the variable and the file name, however, I have no idea how to search for the chosen word in the chosen file. Any ideas?
Using grep can get this working, for example
viewEntry()
{
echo "Entering view entry"
echo -n "Enter Name: "
read input
if grep -q "$input" datafile
then
echo ""
echo -n "Information -> "
grep -w "$input" datafile
echo ""
else
echo "/!\Name Not Found/!\\"
fi
echo "Exiting view entry"
echo ""
}
dataFile is the file you would be reading from. Then making use of -q and -w arguments of grep, you should be able to navigate your chosen file.
This site does a great job explaining grep and your exact problem: http://www.cyberciti.biz/faq/howto-use-grep-command-in-linux-unix/
The following shell-script is a very quick approach to do what you suggested:
#!/bin/sh # Tell your shell with what program this script should be exectued
echo "Please enter the filename: "
read filename # read user input into variable filename
count=`grep -c $1 $filename` # store result of grep into variable count
if [ $count -gt 0 ] # check if count is greater than 0
then
echo "String is present:" $1
else
echo "String not found:" $1
fi
You should look at some tutorials to get the basics of shell-scripting. Your task isn't very complex, so after some reading you should be able understand what the script does and modify it according your needs.

Does not work to execute command in double brackets in bash

In an attempt to stay consistent i have tried to use double brackets [[ ]] in all my if statements. I did however get into a problem when i was going to check the return value for a command i wanted to run. After testing several ways of creating an if statement i found that only without brackets could i execute a command.
The following does not work:
if [[ $command ]] ; then
echo "something"
fi
if [[ $(command) ]] ; then
echo "something"
fi
if [[ ${command} ]] ; then
echo "something"
fi
and the code above makes the if loop true even when the command was not run.
since the code above doesnt work with braces it doesnt work to use this either:
[[ $command ]] || echo "failed"
and it doesnt work in a subshell either.
The following works:
if $command ; then
echo "something"
fi
if $(command) ; then
echo "something"
fi
Why doesnt it work to place a command in an if loop with brackets, and why does the if loops above report true when it didnt even run the command ? I'm using bash version 4.1.9. Ive tried this many times and the if loops are just as simple as the ones i typed above, it just checks if a command was run successfully and exits if it wasnt.
The short answer is:
[ and [[ expect an expression.
if expects a command.
Saying:
[[ $(command) ]]
would essentially execute:
[[ -n <command_output> ]]
which may or may not be what you want. On the other hand, saying:
$command && echo something || echo other
would echo something or other based on the return code of the command (0 and non-zero respectively).
Double braces are a shortcut for test. In your examples, what's happening is that you're testing the shell variable $command for existence.
if [[ $PWD ]]; then
echo PWD is set to a value
fi
if [[ $NOT_A_REAL_VAR ]]; then
echo Nope, its not set
fi
In your second example, you're using command substitution to check that command output something on standard output.
if [[ $(echo hi) ]]; then
echo "echo said hi'
fi
if [[ $(true) ]]; then #true is a program that just quits with successful exit status
echo "This shouldn't execute"
fi
Your third example is the same as your first, pretty much. You use the curly braces if you want to group your variables. for example if you want to put an 's' after something.
WORD=Bike
echo "$WORDS" #won't work because "WORDS" isn't a variable
echo "${WORD}S" # will output "BikeS"
Then in your fifth example, you are running the program that is sitting inside command.
So, if you want to test some strings, use [[ ]] or [ ]. If you just want to test the exit status of a program, then don't use those, just use a bare if.
Check man test for details on the braces.
If you're just checking the return value of the command, drop the double brackets.
if $command
then
echo "Command succeeded"
else
echo "Command failed: $!"
fi
The double brackets are a test command. (Well, not really, but their a takeoff of the single square brackets that were an alias to the test command.) In early Bourne shell, you would see things like:
if test -z "$string"
then
echo "This is an empty string"
fi
The square brackets were syntactic sugar:
if [ -z "$string" ]
then
echo "This is an empty string"
fi
So, if you're not doing an actual test, you can eliminate the double or single square brackets.
If you're using square brackets, you should use the double ones and not the single ones because the double ones are a bit more forgiving and can do a bit more:
if [ -z $string ] # No quotes: This will actually fail if string is zero bytes!
if [[ -z $string ]] # This will work despite the lack of quotes

Issue controlling script flow

I'm new to shell scripting, my script appears to be okay, but its the flow that I'm having an issue controlling. Could someone point out what silly mistake I've made please.
#! /bin/sh
echo "Are you sure youx want to delete $1? Answer y or n"
read ans
echo $ans
if $ans = "y"|"Y"
then
mv $1 /home/parallels/dustbin
echo "File $1 has been deleted"
else echo "File $1 has not been deleted"
fi
Make your if condition like this:
if [ "$ans" = "y" -o "$ans" = "Y" ]
There are a few things wrong with your script. Some are serious, some are less so.
First, the serious problems.
As guru suggested, you need to use square brackets to surround your if condition. This is because if only tests for the output of a condition, it doesn't perform actual string comparisons. Traditionally, a program called /bin/test, which was also called /bin/[ took care of that. These days, that functionality is built in to the shell, but /bin/sh still behaves as if it's a separate program.
In fact, you can do interesting things with if when you don't use square brackets for your condition. For example, if grep -q 'RE' /path/to/file; then is quite common. The grep -q command issues no output, but simply returns a "success" or "fail" that is detected by if.
Second serious problem is that you are echoing a status that may or may not be true. I call this a serious problem because ... well, log messages simply shouldn't make false claims. If the permissions are wrong for the file in $1, or the filename contains a space, then your mv command will fail, but the message will claim that it did not. More on this later.
Next, the less serious problems.
These are mostly style and optimization things.
First off, read on most platforms includes a -p option that lets you specify a prompt. Use this, and you don't need to include an echo command.
Second, your indenting makes it hard to see what the if construct is wrapping. This isn't a huge problem in a program this small, but as you grow, you REALLY want to follow consistent standards.
Third, you can probably get more flexibility in multiple-choice questions like this if you use case statements instead of if.
After all that, here's how I'd write this script:
#!/bin/sh
if [ "$1" = "-y" ]; then
ans=y
shift
elif [ -t 0 ]; then
read -p "Are you sure you want to delete '$1' (y/N) ? " ans
fi
case "$ans" in
Y*|y*)
retval=0
if [ -z "$1" ]; then
retval=64
echo "ERROR: you didn't specify a filename." >&2
if [ ! -f "$1" ]; then
retval=66
echo "ERROR: file '$1' not found!" >&2
elif mv "$1" /home/parallels/dustbin/; then
echo "File '$1' has been deleted" >&2
else
retval=$?
echo "ERROR: file '$1' could not be deleted!" >&2
fi
;;
*)
echo "ABORT: file '$1' has not been deleted" >&2
retval=4
;;
esac
exit $retval
Aside from what's mentioned above, here are some things in this code snippet:
[ "$1" = "-y" ] - if the user specifies a -y option, then we behave as if the question was answered with a "yes".
[ -t 0 ] - this tests whether we are on an interactive terminal. If we are, then it makes sense to ask questions with read.
Y*|y*) - in a case statement, this matches any string that begins with an upper or lower case "y". Valid affirmative responses would therefore be "Y", "yes", "yellow", etc.
[ ! -f "$1" ] - this tests whether the file exists. You can man test or man sh to see the various tests available in shell. (-f may not be the most appropriate for you.)
>&2 - at the end of a line, sends its output to "standard error" instead of "standard out". This changes how output will be handled by pipes, cron, etc. Errors and log data are often sent to stderr, so that stdout can be dedicated to a program's actual output.
mv "$1" ... - The filename is in quotes. This protects you in case the filename has special characters like spaces in it.
$retval - the values for this came from a best guess of the closest item in man sysexits.
retval=$? - this is the exit status of the most recently executed command. In this case, that means we're assigning mv's exit status to the variable $retval, so that if mv failed, the whole script reports the reason for the fail, as far as mv is concerned.
You can also convert the user response to either case and just check it for respective case like
read ans
ans=${ans,,} # make 'ans' lowercase, or use ${ans^^} for making it uppercase
if [ "$ans" = "y" ]
then
....
fi
Below is the perfect code with error handling included
#!/bin/sh
echo "Are you sure you want to delete $1? Answer y or n"
read ans
echo $ans
if [ $ans == "y" ] || [ $ans == "Y" ]
then
if [ -f $1 ]
then
mv $1 /home/parallels/dustbin
echo "File $1 has been deleted"
else
echo " File $1 is not found"
fi
else
echo "File $1 has not been deleted"
fi

Resources