Why $1 output is nothing? - bash

I wrote a simple bash script and in the end of that I tried to test positional arguments like $0, $1, ...
echo please enter your name
read name
if [ -z "$name" ]
then
echo please enter your name
fi
if [ -n "$name" ]
then
echo Thank you so much
fi
echo $0
echo $1
echo $2
echo $3
After I run that, the output was:
please enter your name
j
Thank you so much
/bin/reza.sh
Why just $0 had output and other had nothing?

Run it like below
./bin/reza.sh first second third
please enter your name
monk
Thank you so much
/bin/reza.sh
first
second
third
Also, $0 is file name of the script itself.

The arguments you enter to a script are take in the order $1,$2,$3 and so on.
In this testscript:
#!/bin/bash
echo $0 #Gives you the command/script name itself
echo $1 #Gives you the first argument
echo $2 #Gives you the second argument
echo $3 #Gives you the third argument
echo $# #Gives you all arguments
echo $# #Gives you the total number of arguments excluding the script name
So the result of
$./testscript a b c
is
./testscript
a
b
c
a b c
3
If the argument is not assigned, its value is null or nothing will be printed.
$ printf "%sThere is nothing before this.\n" $1
gives you :
There is nothing before this.
Note: Don't use echo to test arguments, echo will append a newline at the end automatically as in bash.

Related

Simple bash shell program

I want to write the bash script which would accept 4 parameters: name of file, name of directory, and two strings.
If there is a mistake (if first parameter is not a file or second is not a directory) then string which is a third parameter should be printed else file should be copied to directory and string which is a fourth parameter should be printed. I don't why the compiler reports mistake in line 3 with then.
#!/bin/bash
if [-f $1]; then
if[-d $2] ; then
cp $1 / $2
echo $4
fi
done
else
echo $3
exit 1
fi
If you are having problems, paste your code in at https://www.shellcheck.net/
Fix each issue, then get the report again.
The result:
#!/bin/bash
if [ -f "$1" ]; then
if [ -d "$2" ] ; then
cp "$1" / "$2"
echo "$4"
fi
else
echo "$3"
exit 1
fi
I still think you are likely to have an issue at line 4 though, when it tries to copy the root directory into arg 2 without -r. I think what you meant was just
cp "$1" "$2"
Also, you have action for the case that someone passes a valid file as $1 but a non-directory as $2. The program will just exit silently and do nothing.

How do i compare a given string with multiple lines of text in bash?

What i wanna do is assign the 3rd field (each field is separated by :) from each line in Nurses.txt to a variable and compare it with another string which is manually given by the user when he runs the script.
Nurses.txt has this content in it:
12345:Ana Correia:CSLisboa:0:1
98765:Joao Vieira:CSPorto:0:1
54321:Joana Pereira:CSSantarem:0:1
65432:Jorge Vaz:CSSetubal:0:1
76543:Diana Almeida:CSLeiria:0:1
87654:Diogo Cruz:CSBraga:0:1
32198:Bernardo Pato:CSBraganca:0:1
21654:Maria Mendes:CSBeja:0:1
88888:Alice Silva:CSEvora:0:1
96966:Gustavo Carvalho:CSFaro:0:1
And this is the script I have so far, add_nurses.sh:
#!/bin/bash
CS=$(awk -F "[:]" '{print $3}' nurses.txt)
if [["$CS" == "$3"]] ;
then
echo "Error. There is already a nurse registered in that zone";
else
echo "There are no nurses registered in that zone";
fi
When I try to run the script and give it some arguments as shown here:
./add_nurses "Ana Correia" 12345 "CSLisboa" 0
It´s supposed to return "Error. There is already a nurse registered in that zone" but instead it just tells me i have an Output error in Line #6...
A simpler and shorter way to do this job is
if grep -q "^[^:]*:[^:]*:$3:" nurses.txt; then
echo "Error. There is already a nurse registered in that zone"
else
echo "There are no nurses registered in that zone"
fi
The grep call can be simplified as grep -Fq ":$3:" if there is no risk of collision with other fields.
Alternatively, in pure bash without using any external command line utilities:
#!/bin/bash
while IFS=: read -r id name region rest && [[ $region != "$3" ]]; do
:
done < nurses.txt
if [[ $region = "$3" ]]; then
echo "Error. There is already a nurse registered in that zone"
else
echo "There are no nurses registered in that zone"
fi
An alternative way to read the colon separated file would not need awk at all, just bash built-in commands:
read to read from a file into variables
with the -r option to prevent backslash interpretation
IFS as Internal Field Separator to specify the colon : as field separator
#!/bin/bash
# parse parameters to variables
set add_nurse=$1
set add_id=$2
set add_zone=$3
# read colon separated file
set IFS=":"
while read -r nurse id zone d1 d2; do
echo "Nurse: $nurse (ID $id)" "Registered Zone: $zone" "$d1" "$d2"
if [ "$nurse" == "$add_nurse" ] ; then
echo "Found specified nurse '$add_nurse' already registered for zone '$zone'.'"
exit 1
fi
if [ "$zone" == "$add_zone" ] ; then
echo "Found another nurse '$nurse' already registered for specified zone '$add_zone'.'"
exit 1
fi
done < nurses.txt
# reset IFS to default: space, tab, newline
unset IFS
# no records found matching nurse or zone
echo "No nurse is registered for specified zone."
See also:
bash - Read cells in csv file - Unix & Linux Stack Exchange
Judging by the user input (by field from the nurses.txt) to determine if there is indeed a nurse in a given zone according to the op's description, I came up with this solution.
#!/usr/bin/env bash
user_input=("$#")
mapfile -t text_input < <(awk -F':' '{print $2, $1, $3, $4}' nurses.txt)
pattern_from_text_input=$(IFS='|'; printf '%s' "#(${text_input[*]})")
if [[ ${user_input[*]} == $pattern_from_text_input ]]; then
printf 'Error. There is already a nurse "%s" registered in that zone!' "$1" >&2
else
printf 'There are no nurse "%s" registered in that zone.' "$1"
fi
run the script with a debug flag -x e.g.
bash -x ./add_nurses ....
to see what the script is actually doing.
The script will work with the (given order) sample of arguments otherwise an option parser might be required.
It requires bash4+ version because of mapfile aka readarray. For completeness a while read loop and an array assignment is an alternative to mapfile.
while read -r lines; do
text_input+=("$lines")
done < <(awk -F':' '{print $2, $1, $3, $4}' nurses.txt)
First, the content of $CS is a list of items and not only one item so to compare the input against all the items you need to iterate over the fields. Otherwise, you will never get true for the condition.
Second [[ is not the correct command to use here, it will consider the content as bash commands and not as strings.
I updated your script, to make it work for the case you described above
#!/bin/bash
CS=$(awk -F "[:]" '{print $3}' nurses.txt)
for item in `echo $CS`
do
[ "$item" == "$3" ] && echo "Error. There is already a nurse registered in that zone" && exit 1
done
echo "There are no nurses registered in that zone";
Output
➜ $ ./add_nurses.sh "Ana Correia" 12345 "CSLisboa" 0
Error. There is already a nurse registered in that zone
➜ $ ./add_nurses.sh "Ana Correia" 12345 "CSLisboadd" 0
There are no nurses registered in that zone
As already stated in comments and answer:
use single brackets with space inside to test variables: [ "$CS" == "$3" ]
if using awk to get 3rd field of CSV file, it actually returns a column with multiple values as array: verify output by echo "$CS"
So you must use a loop to test each element of the array.
If you iterate over each value of the 3rd nurse's column you can apply almost the same if-test. Only difference are the consequences:
in the case when a value does not match you will continue with the next value
if a value matches you could leave the loop, also the bash-script
#!/bin/bash
# array declaration follows pattern: array=(elements)
CS_array=($(awk -F "[:]" '{print $3}' nurses.txt))
# view how the awk output looks: like an array ?!
echo "$CS_array"
# use a for-each loop to check each string-element of the array
for CS in "${CS_array[#]}" ;
do
# your existing id with corrected test brackets
if [ "$CS" == "$3" ] ;
then
echo "Error. There is already a nurse registered in that zone"
# exit to break the loop if a nurse was found
exit 1
# no else needed, only a 'not found' after all have looped without match
fi
done
echo "There are no nurses registered in that zone"
Notice how complicated the array was passed to the loop:
the "" (double quotes) around are used to get each element as string, even if containing spaces inside (like a nurse's name might)
the ${} (dollar curly-braces) enclosing an expression with more than just a variable name
the expression CS_array[#] will get each element ([#]) from the array (CS_array)
You could also experiment with the array (different attributes):
echo "${#CS_array[*]}" # size of array with prepended hash
echo "${CS_array[*]}" # word splitting based on $IFS
echo "${CS_array[0]}" # first element of the array, 0 based
Detailed tutorial on arrays in bash: A Complete Guide on How To Use Bash Arrays
See also:
Loop through an array of strings in Bash?

Sending the output of a while loop to a bash function

I've created a .bashrc file where I have two functions. One loops through the lines of a file in a while loop. I'm attempting to save the content of the lines if they match certain conditions and then pass all three matches to a second function that will then echo them. However, I've tried exporting variables and I've also tried piping to the second function, but neither works. Piping also acts very strangely as I'll try to illustrate in my code example.
readAndPipe() {
while read -r line || [[ -n "$line" ]]; do
(
if [[ $line == FIRSTNAME=BOB ]]; then
echo $line;
fi;
if [[ $line == LASTNAME=SMITH ]]; then
echo $line;
fi;
if [[ $line == BIRTHMONTH=AUGUST ]]; then
echo $line;
fi;
); done < "file.txt" | printArguments $1 #pass the original command line argument;
}
printArguments() {
#This is where the weirdness happens
echo $# #Prints: only the original command line argument
echo $# #Prints: 1
echo $2 $3 $4 #Prints nothing
varName=$(cat $2 $3 $4)
echo $varName #Prints: FIRSTNAME=BOB
# LASTNAME=SMITH
# BIRTHMONTH=AUGUST
cat $2 $3 $4 #Prints nothing
echo $(cat $2 $3 $4) #Prints nothing
cat $2 $3 $4 | tr "\n" '' #Prints tr: empty string2
}
Obviously I'm not a bash expert so I'm sure there's a lot of mistakes here, but what I'm wondering is
What are these seemingly magical $2 $3 $4 arguments that are not
printed by echo but can be used by cat exactly once.
What is the
correct way to save content during a while loop and pass it to
another function so that I can echo it?
$#, $*, $1, $2, etc are the arguments that are passed to the function. For example, in myfunc foo bar baz, we have $1 == foo, $2 == bar and $3 == baz.
When you pipe data to your function, you have to retrieve it from from stdin:
myfunc() {
data=$(cat)
echo "I received: >$data<"
}
for n in {1..5}; do echo "x=$n"; done | myfunc
produces
I received: >x=1
x=2
x=3
x=4
x=5<
varName=$(cat $2 $3 $4) works because $2 $3 and $4 are empty, so the shell sees this:
varName=$(cat )
The reason cat "only works once" is because you are consuming a stream. Once you consume it, it's gone. "you can't eat your cake and have it too."
The printArguments function can use the readarray command to grab the incoming lines into an array, instead of using cat to grab all the incoming text into a variable:
printArguments() {
readarray -t lines
echo "I have ${#lines[#]} lines"
echo "they are:"
printf ">>%s\n" "${lines[#]}"
}
{ echo foo; echo bar; echo baz; } | printArguments
outputs
I have 3 lines
they are:
>>foo
>>bar
>>baz
Learn more by typing help readarray at an interactive bash prompt.
Imagine a script:
func() {
echo $# # will print 2, func were executed with 2 arguments
echo "$#" # will print `arg1 arg2`, ie. the function arguments
in=$(cat) # will pass stdin to `cat` function and save cat's stdout into a variable
echo "$in" # will print `1 2 3`
}
echo 1 2 3 | func arg1 arg2
# ^^^^^^ function `func` arguments
# ^ passed one command stdout to other command stdin
# ^^^ outputs `1 2 3` on process stdout
cat invoked without any arguments, reads stdin and outputs it on stdout.
Invoking a command in command substitution passes stdin along (ie. in=$(cat) will read stdin as normal cat just saves the output (ie. the cat's stdout) into a variable)
To your script:
readAndPipe() {
# the while read line does not matter, but it outputs something on stdout
while read -r line || [[ -n "$line" ]]; do
echo print something
# the content of `file.txt` is passed as while read input
done < "file.txt" | printArguments $1 # the `print something` (the while loop stdout output) is passed as stdin to the printArguments function
}
printArguments() {
# here $# is equal to 1
# $1 is equal to passed $1 (unless expanded, will get to that)
# $2 $3 $4 expand to nothing
varName=$(cat $2 $3 $4) # this executes varName=$(cat) as $2 $3 $4 expand to nothing
# now after this point stdin has been read (it can be read once, it's a stream or pipe
# is you execute `cat` again it will block (waiting for more input) or fail (will receive EOF - end of file)
echo $varName #Prints: `print something` as it was passed on stdin to this function
}
If the file file.txt contains only just:
FIRSTNAME=BOB
LASTNAME=SMITH
BIRTHMONTH=AUGUST
you can just load the file . file.txt or source file.txt. This will "load" the file, ie. make it part of your script, syntax is bash. So you can:
. file.txt
echo "$FIRSTNAME"
echo "$LASTNAME"
echo "$BIRTHMONTH"
This is a common way of creating configuration files in /etc/ and then they are loaded by scripts. That's why in many /etc/ files comments start with #.
Notes:
Always enclose your variables. echo "$1" printArguments "$1" echo "$#" echo "$#" cat "$2" "$3" "$4" [ "$line" == ... ], a good read is here
Remove newlines with tr -d '\n'
A ( ) creates a subshell, which creates a new shell which has new variables and does not share variable with the parent, see here.

Unix How to check if a specific word is entered in as an argument

I'm writing a script in Unix but I need a way to check an argument that is entered in the command line is a specific word.
So if when using the script the user types:
$ ./script hello
my script can tell that "hello" was entered as an argument and can display a message appropriately.
And if the user types something other than "hello" as an argument then my script can display another message.
Thanks.
This should work:
#!/bin/bash
if [[ $1 == hello ]];then
echo "hello was entered"
else
echo "hello wasn't entered"
fi
There are a number of ways to check positional arguments against a list. When there are a number of items in the list, you can use a case statement instead of a string of if ... elif ... elif ... fi comparisons. The syntax is as follows:
#!/bin/bash
case "$1" in
"hello" )
printf "you entered hello\n"
;;
"goodbye" )
printf "well goodbye to you too\n"
;;
* )
printf "you entered something I don't understand.\n"
;;
esac
exit 0
Output/Use
$ ./caseex.sh hello
you entered hello
$ ./caseex.sh goodbye
well goodbye to you too
$ ./caseex.sh morning
you entered something I don't understand.
In Bash arguments passed to shell scripts are stored in variables named as follows:
$0 = name of the script.
$1~$n = arguments.
$# = number of arguments.
$* = single string of all arguments: "arg1,arg2,..."
you can simply use if [ $1 == "some string" ]; then ...
You can retrieve the command line arguments with $(number)
for example the first argument would exist at $1 the second at $2 etc.
You can use conditionals in BASH (I assume you are using bash) just like any other language; however the syntax is a bit wonky :). here is a link for you
http://tldp.org/LDP/Bash-Beginners-Guide/html/chap_07.html
If you are sure about the position of the argument you can :
#!/bin/bash
if [[ $1 == SearchWord]];then
echo "SearchWord was entered"
else
echo "SearchWord wasn't entered"
fi
Incase you are not sure:
You can use $*
[ `echo $* | grep $SearchWord| wc -l` -eq 1 ] && echo "Present"|| echo "Not present"

Extract parameters before last parameter in "$#"

I'm trying to create a Bash script that will extract the last parameter given from the command line into a variable to be used elsewhere. Here's the script I'm working on:
#!/bin/bash
# compact - archive and compact file/folder(s)
eval LAST=\$$#
FILES="$#"
NAME=$LAST
# Usage - display usage if no parameters are given
if [[ -z $NAME ]]; then
echo "compact <file> <folder>... <compressed-name>.tar.gz"
exit
fi
# Check if an archive name has been given
if [[ -f $NAME ]]; then
echo "File exists or you forgot to enter a filename. Exiting."
exit
fi
tar -czvpf "$NAME".tar.gz $FILES
Since the first parameters could be of any number, I have to find a way to extract the last parameter, (e.g. compact file.a file.b file.d files-a-b-d.tar.gz). As it is now the archive name will be included in the files to compact. Is there a way to do this?
To remove the last item from the array you could use something like this:
#!/bin/bash
length=$(($#-1))
array=${#:1:$length}
echo $array
Even shorter way:
array=${#:1:$#-1}
But arays are a Bashism, try avoid using them :(.
Portable and compact solutions
This is how I do in my scripts
last=${#:$#} # last parameter
other=${*%${!#}} # all parameters except the last
EDIT
According to some comments (see below), this solution is more portable than others.
Please read Michael Dimmitt's commentary for an explanation of how it works.
last_arg="${!#}"
Several solutions have already been posted; however I would advise restructuring your script so that the archive name is the first parameter rather than the last. Then it's really simple, since you can use the shift builtin to remove the first parameter:
ARCHIVENAME="$1"
shift
# Now "$#" contains all of the arguments except for the first
Thanks guys, got it done, heres the final bash script:
#!/bin/bash
# compact - archive and compress file/folder(s)
# Extract archive filename for variable
ARCHIVENAME="${!#}"
# Remove archive filename for file/folder list to backup
length=$(($#-1))
FILES=${#:1:$length}
# Usage - display usage if no parameters are given
if [[ -z $# ]]; then
echo "compact <file> <folder>... <compressed-name>.tar.gz"
exit
fi
# Tar the files, name archive after last file/folder if no name given
if [[ ! -f $ARCHIVENAME ]]; then
tar -czvpf "$ARCHIVENAME".tar.gz $FILES; else
tar -czvpf "$ARCHIVENAME".tar.gz "$#"
fi
Just dropping the length variable used in Krzysztof Klimonda's solution:
(
set -- 1 2 3 4 5
echo "${#:1:($#-1)}" # 1 2 3 4
echo "${#:(-$#):($#-1)}" # 1 2 3 4
)
I would add this as a comment, but don't have enough reputation and the answer got a bit longer anyway. Hope it doesn't mind.
As #func stated:
last_arg="${!#}"
How it works:
${!PARAM} indicates level of indirection. You are not referencing PARAM itself, but the value stored in PARAM ( think of PARAM as pointer to value ).
${#} expands to the number of parameters (Note: $0 - the script name - is not counted here).
Consider following execution:
$./myscript.sh p1 p2 p3
And in the myscript.sh
#!/bin/bash
echo "Number of params: ${#}" # 3
echo "Last parameter using '\${!#}': ${!#}" # p3
echo "Last parameter by evaluating positional value: $(eval LASTP='$'${#} ; echo $LASTP)" # p3
Hence you can think of ${!#} as a shortcut for the above eval usage, which does exactly the approach described above - evaluates the value stored in the given parameter, here the parameter is 3 and holds the positional argument $3
Now if you want all the params except the last one, you can use substring removal ${PARAM%PATTERN} where % sign means 'remove the shortest matching pattern from the end of the string'.
Hence in our script:
echo "Every parameter except the last one: ${*%${!#}}"
You can read something in here: Parameter expansion
Are you sure this fancy script is any better than a simple alias to tar?
alias compact="tar -czvpf"
Usage is:
compact ARCHIVENAME FILES...
Where FILES can be file1 file2 or globs like *.html
Try:
if [ "$#" -gt '0' ]; then
/bin/echo "${!#}" "${#:1:$(($# - 1))}
fi
Array without last parameter:
array=${#:1:$#-1}
But it's a bashism :(. Proper solutions would involve shift and adding into variable as others use.
#!/bin/bash
lastidx=$#
lastidx=`expr $lastidx - 1`
eval last='$'{$lastidx}
echo $last
Alternative way to pull the last parameter out of the argument list:
eval last="\$$#"
eval set -- `awk 'BEGIN{for(i=1;i<'$#';i++) printf " \"$%d\"",i;}'`
#!/bin/sh
eval last='$'$#
while test $# -gt 1; do
list="$list $1"
shift
done
echo $list $last
I can't find a way to use array-subscript notation on $#, so this is the best I can do:
#!/bin/bash
args=("$#")
echo "${args[$(($#-1))]}"
This script may work for you - it returns a subrange of the arguments, and can be called from another script.
Examples of it running:
$ args_get_range 2 -2 y a b "c 1" d e f g
'b' 'c 1' 'd' 'e'
$ args_get_range 1 2 n arg1 arg2
arg1 arg2
$ args_get_range 2 -2 y arg1 arg2 arg3 "arg 4" arg5
'arg2' 'arg3'
$ args_get_range 2 -1 y arg1 arg2 arg3 "arg 4" arg5
'arg2' 'arg3' 'arg 4'
# You could use this in another script of course
# by calling it like so, which puts all
# args except the last one into a new variable
# called NEW_ARGS
NEW_ARGS=$(args_get_range 1 -1 y "$#")
args_get_range.sh
#!/usr/bin/env bash
function show_help()
{
IT="
Extracts a range of arguments from passed in args
and returns them quoted or not quoted.
usage: START END QUOTED ARG1 {ARG2} ...
e.g.
# extract args 2-3
$ args_get_range.sh 2 3 n arg1 arg2 arg3
arg2 arg3
# extract all args from 2 to one before the last argument
$ args_get_range.sh 2 -1 n arg1 arg2 arg3 arg4 arg5
arg2 arg3 arg4
# extract all args from 2 to 3, quoting them in the response
$ args_get_range.sh 2 3 y arg1 arg2 arg3 arg4 arg5
'arg2' 'arg3'
# You could use this in another script of course
# by calling it like so, which puts all
# args except the last one into a new variable
# called NEW_ARGS
NEW_ARGS=\$(args_get_range.sh 1 -1 \"\$#\")
"
echo "$IT"
exit
}
if [ "$1" == "help" ]
then
show_help
fi
if [ $# -lt 3 ]
then
show_help
fi
START=$1
END=$2
QUOTED=$3
shift;
shift;
shift;
if [ $# -eq 0 ]
then
echo "Please supply a folder name"
exit;
fi
# If end is a negative, it means relative
# to the last argument.
if [ $END -lt 0 ]
then
END=$(($#+$END))
fi
ARGS=""
COUNT=$(($START-1))
for i in "${#:$START}"
do
COUNT=$((COUNT+1))
if [ "$QUOTED" == "y" ]
then
ARGS="$ARGS '$i'"
else
ARGS="$ARGS $i"
fi
if [ $COUNT -eq $END ]
then
echo $ARGS
exit;
fi
done
echo $ARGS
This works for me, with sh and bash:
last=${*##* }
others=${*%${*##* }}

Resources