How to redirect output to STDERR? - bash

Suppose that I passed in 2 parameters to my function. How can I redirect a message to stderr?
#!/bin/bash
if [ $# = 0 ]; then
dir="."
elif [ $# = 1 ]; then
dir=$1
elif [ $# -ge 2 ]; then
echo "Too many operands." 2>> err.txt //???
exit 1
fi

Just add >&2 to the statement producing the output:
echo "Too many operands." >&2
and if you want it to be appended to a file named err.txt too:
echo "Too many operands." | tee -a 'err.txt' >&2
FWIW I'd write your code using a case statement instead of nested ifs (and tidy up a couple of things):
#!/bin/env bash
case $# in
0 ) dir='.' ;;
1 ) dir="$1" ;;
* ) printf 'Too many operands: %d\n' "$#" | tee -a 'err.txt' >&2
exit 1 ;;
esac

Related

bash- reading file from stdin and arguments

So I have googled this and thought I found the answers, but it still doesnt work for me.
The program computes the average and median of rows and columns in a file of numbers...
Using the file name works:
./stats -columns test_file
Using cat does not work
cat test_file | ./stats -columns
I am not sure why it doesnt work
#file name was given
if [[ $# -eq 2 ]]
then
fileName=$2
#file name was not given
elif [[ $# -eq 1 ]]
then
#file name comes from the user
fileName=/dev/stdin
#incorrect number of arguments
else
echo "Usage: stats {-rows|-cols} [file]" 1>&2
exit 1
fi
A very simple program that accepts piped input:
#!/bin/sh
stdin(){
while IFS= read -r i
do printf "%s" "$i"
done
}
stdin
Test is as follows:
echo "This is piped output" | stdin
To put that into a script / utility similar to the one in the question you might do this:
#!/bin/sh
stdin(){
while IFS= read -r i
do printf "%s" "$i"
done
}
rowbool=0
colbool=0
for i in $#
do case "$i" in
-rows) echo "rows set"
rowbool=1
shift
;;
-cols) echo "cols set"
colbool=1
shift
;;
esac
done
if [[ $# -gt 0 ]]
then
fileName=$1
fi
if [[ $# -eq 0 ]]
then fileName=$(stdin)
fi
echo "$fileName"

Bash script errors: i in for loop gives 'no such file or directory'

Long story short, I need to write a shell script. The script will take a single command line argument which will be a directory path.
The script will then read each of the files in that directory and output it to standard output; the output will be in HTML and will be a table.
The files will be in this format:
owner sysadmin group
admin ajr
loc S-309
ser 18r97
comment noisy fan
What I have so far:
PATH=/bin:/usr/bin
cd "$#"
if [ test $? -ne 0]
then
exit 1
fi
filenames=$(ls "$#")
for i in $filenames
do
while read item value
do
if [ $item="owner" ] || [ $item="admin" ] || [ $item="loc" ] || [ $item="ser"]
then
a[$item]=$value
fi
done < i
done
echo '<html>'
echo '<body>'
echo '<table border=1>'
echo '<tr><th>hostname</th><th>location</th><th>Admin</th><th>Serial Number</th><th>owner</th><tr>'
for i in filename
do
echo '<tr><td>'$i'</td><td>'${i[loc]}'</td><td>${i[admin]}'</td><td>'${i[ser]}'</td><td>'${i[owner]}'</td><tr>'
done
echo
echo '</table>'
echo '</body>'
echo '</html>'
The HTML isn't my main concern since I am just following a format given, with each of the values going in between. However, I am getting an error that I have no idea why:
invrep: line 10: i: no such file or directory
but I am using it in a loop. Why is it giving me this error?
Also to confirm, the directory that I used exists; I'm not sure if that has to do with anything though.
Caveat Lector: the code in the question has been edited. The code I commented on may not be the code you can see.
Not directly the problem (chepner diagnosed that in his comment), but:
cd "$#"
if [ test $? -ne 0]
then
exit 1
fi
has a variety of problems. You don't verify that there's only one argument, and you pass all the arguments that are given to cd, which may just quietly ignore the surplus. The test line should use either [ or test but not both. If you use [, the last argument must be ] so you're missing a space:
if test $? -ne 0
if [ $? -ne 0 ]
However, you could short circuit that paragraph by:
cd "$#" || exit 1
(or you could drop the 1 even, though I'd leave it there).
You might want to consider:
case $# in
1) cd "$1" || exit 1;;
*) echo "Usage: $0 directory" >&2; exit 1;;
esac
This verifies that a single argument was passed and that it names a directory you can cd to.
Your looping code also has problems. The while loop should be redirected from "$i" once you've fixed things up:
filenames=$(ls "$#")
for i in $filenames
do
while read item value
do
if [ $item="owner" ] || [ $item="admin" ] || [ $item="loc" ] || [ $item="ser"]
then
a[$item]=$value
fi
done < $i
# Print HTML here!! Not after this loop
done
Your HTML loop has a lot of problems too — notably using $i as an array instead of $a.
PATH=/bin:/usr/bin
case $# in
1) cd "$1" || exit 1;;
*) echo "Usage: $0 directory" >&2; exit 1;;
esac
echo '<html>'
echo '<body>'
echo '<table border=1>'
echo '<tr><th>hostname</th><th>location</th><th>Admin</th><th>Serial Number</th><th>owner</th><tr>'
filenames=$(ls "$#")
for i in $filenames
do
while read item value
do
if [ $item="owner" ] || [ $item="admin" ] || [ $item="loc" ] || [ $item="ser"]
then
a[$item]=$value
fi
done < $i
echo "<tr><td>$i</td><td>${a[loc]}</td><td>${a[admin]}</td><td>${a[ser]}</td><td>${a[owner]}</td><tr>"
done
echo '</table>'
echo '</body>'
echo '</html>'
And that still doesn't fix the problem with using ls to generate a list of file names. For that, given the rest of the script, lose filenames altogether and use for file in * instead. You then need to quote $i in the I/O redirection, too.
PATH=/bin:/usr/bin
case $# in
1) cd "$1" || exit 1;;
*) echo "Usage: $0 directory" >&2; exit 1;;
esac
echo '<html>'
echo '<body>'
echo '<table border=1>'
echo '<tr><th>hostname</th><th>location</th><th>Admin</th><th>Serial Number</th><th>owner</th><tr>'
for i in *
do
while read item value
do
if [ $item = "owner" ] || [ $item = "admin" ] ||
[ $item = "loc" ] || [ $item="ser"]
then
a[$item]=$value
fi
done < "$i"
echo "<tr><td>$i</td><td>${a[loc]}</td><td>${a[admin]}</td><td>${a[ser]}</td><td>${a[owner]}</td><tr>"
done
echo '</table>'
echo '</body>'
echo '</html>'
(Also fixed spacing in the if statement in the loops. The code is still not very elegant, but it is somewhat related to the original code.)
filenames=$(ls "$#")
is wrong, and should never be used by anyone. See the first entry in http://mywiki.wooledge.org/BashPitfalls, or the entire page http://mywiki.wooledge.org/ParsingLs.
If your argument list is a set of directories, the inner loop would look more like this:
declare -A a
for dir in "$#"; do
for i in "$dir/"*; do
while read -r item value; do
case $item in
owner|admin|loc|ser)
a[$item]=$value
;;
esac
done <"$i"
done
done

Best way to parse command line args in Bash?

After several days of research, I still can't figure out the best method for parsing cmdline args in a .sh script. According to my references the getopts cmd is the way to go since it "extracts and checks switches without disturbing the positional parameter variables.Unexpected switches, or switches that are missing arguments, are recognized and reportedas errors."
Positional params(Ex. 2 - $#, $#, etc) apparently don't work well when spaces are involved but can recognize regular and long parameters(-p and --longparam). I noticed that both methods fail when passing parameters with nested quotes ("this is an Ex. of ""quotes""."). Which one of these three code samples best illustrates the way to deal with cmdline args? The getopt function is not recommended by gurus, so I'm trying to avoid it!
Example 1:
#!/bin/bash
for i in "$#"
do
case $i in
-p=*|--prefix=*)
PREFIX=`echo $i | sed 's/[-a-zA-Z0-9]*=//'`
;;
-s=*|--searchpath=*)
SEARCHPATH=`echo $i | sed 's/[-a-zA-Z0-9]*=//'`
;;
-l=*|--lib=*)
DIR=`echo $i | sed 's/[-a-zA-Z0-9]*=//'`
;;
--default)
DEFAULT=YES
;;
*)
# unknown option
;;
esac
done
exit 0
Example 2:
#!/bin/bash
echo ‘number of arguments’
echo "\$#: $#"
echo ”
echo ‘using $num’
echo "\$0: $0"
if [ $# -ge 1 ];then echo "\$1: $1"; fi
if [ $# -ge 2 ];then echo "\$2: $2"; fi
if [ $# -ge 3 ];then echo "\$3: $3"; fi
if [ $# -ge 4 ];then echo "\$4: $4"; fi
if [ $# -ge 5 ];then echo "\$5: $5"; fi
echo ”
echo ‘using $#’
let i=1
for x in $#; do
echo "$i: $x"
let i=$i+1
done
echo ”
echo ‘using $*’
let i=1
for x in $*; do
echo "$i: $x"
let i=$i+1
done
echo ”
let i=1
echo ‘using shift’
while [ $# -gt 0 ]
do
echo "$i: $1"
let i=$i+1
shift
done
[/bash]
output:
bash> commandLineArguments.bash
number of arguments
$#: 0
using $num
$0: ./commandLineArguments.bash
using $#
using $*
using shift
#bash> commandLineArguments.bash "abc def" g h i j*
Example 3:
#!/bin/bash
while getopts ":a:" opt; do
case $opt in
a)
echo "-a was triggered, Parameter: $OPTARG" >&2
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
exit 1
;;
esac
done
exit 0
I find the use of getopt to be the easiest. It provides correct handling of arguments which is tricky otherwise. For example, getopt will know how to handle arguments to a long option specified on the command line as --arg=option or --arg option.
What is useful in parsing any input passed to a shell script is the use of the "$#" variables. See the bash man page for how this differs from $#. It ensures that you can process arguments that include spaces.
Here's an example of how I might write s script to parse some simple command line arguments:
#!/bin/bash
args=$(getopt -l "searchpath:" -o "s:h" -- "$#")
eval set -- "$args"
while [ $# -ge 1 ]; do
case "$1" in
--)
# No more options left.
shift
break
;;
-s|--searchpath)
searchpath="$2"
shift
;;
-h)
echo "Display some help"
exit 0
;;
esac
shift
done
echo "searchpath: $searchpath"
echo "remaining args: $*"
And used like this to show that spaces and quotes are preserved:
user#machine:~/bin$ ./getopt_test --searchpath "File with spaces and \"quotes\"."
searchpath: File with spaces and "quotes".
remaining args: other args
Some basic information about the use of getopt can be found here
If you want to avoid using getopt you can use this nice quick approach:
Defining help with all options as ## comments (customise as you wish).
Define for each option a function with same name.
Copy the last five lines of this script to your script (the magic).
Example script: log.sh
#!/bin/sh
## $PROG 1.0 - Print logs [2017-10-01]
## Compatible with bash and dash/POSIX
##
## Usage: $PROG [OPTION...] [COMMAND]...
## Options:
## -i, --log-info Set log level to info (default)
## -q, --log-quiet Set log level to quiet
## -l, --log MESSAGE Log a message
## Commands:
## -h, --help Displays this help and exists
## -v, --version Displays output version and exists
## Examples:
## $PROG -i myscrip-simple.sh > myscript-full.sh
## $PROG -r myscrip-full.sh > myscript-simple.sh
PROG=${0##*/}
LOG=info
die() { echo $# >&2; exit 2; }
log_info() {
LOG=info
}
log_quiet() {
LOG=quiet
}
log() {
[ $LOG = info ] && echo "$1"; return 1 ## number of args used
}
help() {
grep "^##" "$0" | sed -e "s/^...//" -e "s/\$PROG/$PROG/g"; exit 0
}
version() {
help | head -1
}
[ $# = 0 ] && help
while [ $# -gt 0 ]; do
CMD=$(grep -m 1 -Po "^## *$1, --\K[^= ]*|^##.* --\K${1#--}(?:[= ])" log.sh | sed -e "s/-/_/g")
if [ -z "$CMD" ]; then echo "ERROR: Command '$1' not supported"; exit 1; fi
shift; eval "$CMD" $# || shift $? 2> /dev/null
done
Testing
Running this command:
./log.sh --log yep --log-quiet -l nop -i -l yes
Produces this output:
yep
yes
By the way: It's compatible with posix!

How to count and check passed arguments?

How can I translate the following Ruby code to Bash?
if ARGV.length == 0
abort "\nError: The project name is required. Aborting...\n\n"
elsif ARGV.length > 2
abort "\nError: The program takes two arguments maximum. Aborting...\n\n"
end
#!/bin/bash
USAGE="$0: <project name> [subproject attribute]"
if [ $# -lt 1 ]; then echo -e "Error: The project name is required.\n$USAGE" >&2; exit 1; fi
if [ $# -gt 2 ]; then echo -e "Error: Two arguments maximum.\n$USAGE" >&2; exit 1; fi
The following should be what you need:
#!/bin/bash
if [ $# -eq 0 ]; then
echo -e "\nError: The project name is required. Aborting...\n\n"
exit 1
elif [ $# -gt 2 ]; then
echo -e "\nError: The program takes two arguments maximum. Aborting...\n\n"
exit 1
fi
The TLDP bash guide is very good if you are looking to learn bash, see TDLP Bash guide.
Maybe:
#!/bin/bash
function functionName {
if [ $# = 0 ]
then echo "\nError: The project name is required. Aborting...\n\n"; exit 1
fi
if [ $# \> 2 ]
then echo "\nError: The program takes two arguments maximum. Aborting...\n\n"; exit 1
fi
}
functionName a

Checking for the correct number of arguments

How do i check for the correct number of arguments (one argument). If somebody tries to invoke the script without passing in the correct number of arguments, and checking to make sure the command line argument actually exists and is a directory.
#!/bin/sh
if [ "$#" -ne 1 ] || ! [ -d "$1" ]; then
echo "Usage: $0 DIRECTORY" >&2
exit 1
fi
Translation: If number of arguments is not (numerically) equal to 1 or the first argument is not a directory, output usage to stderr and exit with a failure status code.
More friendly error reporting:
#!/bin/sh
if [ "$#" -ne 1 ]; then
echo "Usage: $0 DIRECTORY" >&2
exit 1
fi
if ! [ -e "$1" ]; then
echo "$1 not found" >&2
exit 1
fi
if ! [ -d "$1" ]; then
echo "$1 not a directory" >&2
exit 1
fi
cat script.sh
var1=$1
var2=$2
if [ "$#" -eq 2 ]
then
if [ -d $var1 ]
then
echo directory ${var1} exist
else
echo Directory ${var1} Does not exists
fi
if [ -d $var2 ]
then
echo directory ${var2} exist
else
echo Directory ${var2} Does not exists
fi
else
echo "Arguments are not equals to 2"
exit 1
fi
execute it like below -
./script.sh directory1 directory2
Output will be like -
directory1 exit
directory2 Does not exists
You can check the total number of arguments which are passed in command line with "$#"
Say for Example my shell script name is hello.sh
sh hello.sh hello-world
# I am passing hello-world as argument in command line which will b considered as 1 argument
if [ $# -eq 1 ]
then
echo $1
else
echo "invalid argument please pass only one argument "
fi
Output will be hello-world

Resources