I have to write a bash script which will count all the commands in a text file. Arguments to a script are -p, -n num, and a file. This means that commands like:
script.sh -n 3 -p file.txt
script -p -n 3 file.txt
and similar are all legit.
However, I have to echo an error for any commands that are not similar to this: script.sh -n -k file.txt for example.
Here is a link to my code.
I managed to make it work, but it is way too long and redundant. Is there a way I can do this in a short way?
You may want to have a look at one of the following standard commands:
getopts is a Bash builtin. It is newer and simple to use, but does not support long options (--option).
getopt is an external program which may involve a little more glue code. There are different implementations. getopt usually supports long options.
This is a small getopts example (modified one of the examples from this external site):
#!/bin/bash
flag=off
dir=
# iterate over each option with getopts:
while getopts fd: opt
do
case "$opt" in
f) flag=on;;
d) dir="$OPTARG";;
*) echo >&2 "usage: $0 [-f] [-d directory] [file ...]"
exit 1;;
esac
done
# remove all positional pararmeters we already
# handled from the command line:
shift $(( expr $OPTIND - 1 ))
# main part of your program, remaining arguments are now in
# $# resp. $0, $1, ...
I'd like to suggest another snippet that is a lot simpler to read than yours, because it exactly depicts the only two valid cases you specified in your comment:
If I want to "call" my script it has to look like this: script.sh -n +number -p file.txt. file.txt must be the last argument, however, -n and -p can be switched.
So the cases are ($0 to $4):
script.sh -n +number -p file.txt
script.sh -p -n +number file.txt
It uses only if and Bash's logical operators:
#!/bin/bash
if ! { [[ "$1" = "-n" ]] && [[ "$2" =~ ^-[0-9]+$ ]] && [[ "$3" = "-p" ]] && [[ "$4" =~ ".txt"$ ]] ; } &&
! { [[ "$2" = "-n" ]] && [[ "$3" =~ ^-[0-9]+$ ]] && [[ "$1" = "-p" ]] && [[ "$4" =~ ".txt"$ ]] ; }
then
echo "Error" && exit 1
fi
Notes:
The group ({, }) syntax expects a ; at the end of its list.
You have to use a regex to check for *.txt
The number regex you gave will require the number to start with a -, while in your specification you say +.
Related
I am new to bash scripting and I have to create this script that takes 3 directories as arguments and copies in the third one all the files in the first one that are NOT in the second one.
I did it like this:
#!/bin/bash
if [ -d $1 && -d $2 && -d $3 ]; then
for FILE in [ ls $1 ]; do
if ! [ find $2 -name $FILE ]; then
cp $FILE $3
done
else echo "Error: one or more directories are not present"
fi
The error I get when I try to execute it is: "line 7: syntax error near unexpected token `done' "
I don't really know how to make it work!
Also even if I'm using #!/bin/bash I still have to explicitly call bash when trying to execute, otherwise it says that executing is not permitted, anybody knows why?
Thanks in advance :)
Couple of suggestions :
No harm double quoting variables
cp "$FILE" "$3" # prevents wordsplitting, helps you filenames with spaces
for statement fails for the fundamental reason -bad syntax- it should've been:
for FILE in ls "$1";
But then, never parse ls output. Check [ this ].
for FILE in ls "$1"; #drastic
Instead of the for-loop in step2 use a find-while-read combination:
find "$1" -type f -print0 | while read -rd'' filename #-type f for files
do
#something with $filename
done
Use lowercase variable names for your script as uppercase variables are reserved for the system. Check [this].
Use tools like [ shellcheck ] to improve script quality.
Edit
Since you have mentioned the input directories contain only files, my alternative approach would be
[[ -d "$1" && -d "$2" && -d "$3" ]] && for filename in "$1"/*
do
[ ! -e "$2/${filename##*/}" ] && cp "$filename" "$3"
done
If you are baffled by ${filename##*/} check [ shell parameter expansion ].
Sidenote: In linux, although discouraged it not uncommon to have non-standard filenames like file name.
Courtesy: #chepner & #mklement0 for their comments that greatly improved this answer :)
Your script:
if ...; then
for ...; do
if ...; then
...
done
else
...
fi
Fixed structure:
if ...; then
for ...; do
if ...; then
...
fi # <-- missing
done
else
...
fi
If you want the script executable, then make it so:
$ chmod +x script.sh
Notice that you also have other problems in you script. It is better written as
dir1="$1"
dir2="$2"
dir3="$3"
for f in "$dir1"/*; do
if [ ! -f "$dir2/$(basename "$f")" ]; then
cp "$f" "$dir3"
fi
done
this is not totally correct:
for FILE in $(ls $1); do
< whatever you do here >
done
There is a big problem with that loop if in that folder there is a filename like this: 'I am a filename with spaces.txt'.
Instead of that loop try this:
for FILE in "$1"/*; do
echo "$FILE"
done
Also you have to close every if statement with fi.
Another thing, if you are using BASH ( #!/usr/bin/env bash ), it is highly recommended to use double brackets in your test conditions:
if [[ test ]]; then
...
fi
For example:
$ a='foo bar'
$ if [[ $a == 'foo bar' ]]; then
> echo "it's ok"
> fi
it's ok
However, this:
$ if [ $a == 'foo bar' ]; then
> echo "it's ok";
> fi
bash: [: too many arguments
You've forgot fi after the innermost if.
Additionally, neither square brackets nor find do work this way. This one does what your script (as it is now) is intended to on my PC:
#!/bin/bash
if [[ -d "$1" && -d "$2" && -d "$3" ]] ; then
ls -1 "$1" | while read FILE ; do
ls "$2/$FILE" >/dev/null 2>&1 || cp "$1/$FILE" "$3"
done
else echo "Error: one or more directories are not present"
fi
Note that after a single run, when $2 and $3 refer to different directories, those files are still not present in $2, so next time you run the script they will be copied once more despite they already are present in $3.
I was running a small bash script, but I couldn't figure out why it was entering a if block even when condition should be false.
$ cat script.sh
#!/bin/bash
if [[ "$#"="-h" || "$#"="--help" ]]
then
echo 'Show help'
exit
fi
echo 'Do stuff'
$ ./script.sh
Show help
$ bash -x script.sh
+ [[ -n =-h ]]
+ echo 'Show help'
Show help
+ exit
$ bash -x script.sh -option
+ [[ -n -option=-h ]]
+ echo 'Show help'
Show help
+ exit
So why is $# equal to -n when I didn't pass any arguments? Also even if it is, how does -n =-h evaluate to true? When I do pass an argument -option, why is it evaluated to true, too?
Whitespace is significant. Spaces between the arguments to [[ are mandatory.
if [[ "$#" = "-h" || "$#" = "--help" ]]
Also, "$#" means "all of the command-line arguments". It would be better to just check a single argument.
if [[ "$1" = "-h" || "$1" = "--help" ]]
And for what it's worth, variable expansions in [[ don't have to be quoted. It doesn't hurt, and quoting your variables actually a good habit to develop, but if you want you can remove the quotes.
if [[ $1 = -h || $1 = --help ]]
[[ string ]] return true if string is not empty, i.e. it's a shorcut for
[[ -n string ]]
In your case, the string was =-h, that's why you see
[[ -n =-h ]]
To test for string equiality, you have to use the = (or ==) operator, that must be preceded and followed by whitespace.
[[ "$#" = "-h" ]]
Note that "$#" means all the arguments:
set -- a b c
set -x
[[ "$#" == 'a b c' ]] && echo true
gives
+ [[ a b c == \a\ \b\ \c ]]
+ echo true
true
The other answers have already explained the problems with your code. This one shows that
bashisms such as [[ ... ]] are not needed,
you can gain flexibility by using a for loop to check whether at least one of the command-line argument matches -h or --help.
Script
#!/bin/sh
show_help=0
for arg in "$#"; do
shift
case "$arg" in
"--help")
show_help=1
;;
"-h")
show_help=1
;;
*)
;;
esac
done
if [ $show_help -eq 1 ]; then
printf "Show help\n"
exit
fi
Tests
After making the script (called "foo") executable, by running
chmod u+x foo
I get the following results
$ ./foo
$ ./foo -h
Show help
$ ./foo --help
Show help
$ ./foo bar
$ ./foo bar --help
Show help
$ ./foo bar --help baz -h
Show help
I've recently started working with the getopts command in bash. I am confused as to why my script runs the dafult action "cat ~bin/Temp/log.txt | ~bin/Scripts/report.pl" when arguments have been provided. I only want that to run if no arguments were passed to the shell script. I've used getopts:Std in perl where I was able to code somthing like:
unless ($opts{d}) {
do something...}
How would I code something like that in a shell script? Also, how would I code logic such as this:
if ($opts{c}) {
cat ~bin/Temp/mag.txt | ~bin/Scripts/report.pl -c
}
elsif ($opts{d} {
cat ~bin/Temp/mag.txt | ~bin/Scripts/report.pl -d
My code:
#!/bin/sh
while getopts cd name
do
case $name in
c)copt=1;;
d)dopt=1;;
*)echo "Invalid arg";;
esac
done
if [[ ! -z $copt ]] #Specifies what happens if the -c argument was provided
then
echo "CSV file created!"
cat "~/bin/Temp/log.txt" | ~/bin/Scripts/vpnreport/report.pl -c
fi
if [[ ! -z $dopt ]] #Specifies what happens if the -d argument was provided
then
echo "Debug report and files created"
cat ~bin/Temp/mag.txt | ~bin/Scripts/report.pl -d
fi
if [[ ! -z $name ]] #Specifies what happens if no argument was provided
then
echo "Running standard VPN report"
cat ~bin/Temp/log.txt | ~bin/Scripts/report.pl
fi
shift $(($OPTIND -1))
My Output:
[~/bin/Scripts/report]$ sh getoptstest.sh
Running standard report
[~/bin/Scripts/report]$ sh getoptstest.sh -d
Debug report and files created
Running standard report
[~/bin/Scripts/report]$
The two getopts commands are vasty different from bash to perl and I just can't seem to get the hang of the bash varient even after reading several tutorials. Any help would be greatly appreciated!
On the final run of getopts, your variable (name) will be set to "?".
#!/bin/bash
while getopts abc foo; do :; done
echo "<$foo>"
Output of the above:
$ ./mytest.sh
<?>
$ ./mytest.sh -a
<?>
Insead, use elif, which is like Perl's elsif:
if [[ ! -z $copt ]]
then
# ...
elif [[ ! -z $dopt ]]
then
# ...
else
# ...
fi
Or test if [[ -z $copt && -z $dopt ]], or so forth. Other notes:
See the official if and case documentation in the Bash manual under "Conditional Constructs".
[[ ! -z $name ]] means the same as the more-direct [[ -n $name ]].
Use #!/bin/bash instead of #!/bin/sh, or switch off of [[ in favor of [. The double square bracket (and your use thereof) is specific to bash, and rarely works with sh.
I took Jeff's answer and rewrote my script so it works now:
#!/bin/bash
while getopts cd name
do
case $name in
c)carg=1;;
d)darg=1;;
*)echo "Invalid arg";;
esac
done
#Specifies what happens if the -c argument was provided:
if [[ ! -z $carg ]]
then
if [[ -z $darg ]]
then
echo "CSV created"
cat ~bin/Temp/log.txt | ~bin/Scripts/report.pl -c
else
echo "Debug CSV created"
cat ~bin/Temp/log.txt | ~bin/Scripts/report.pl -cd
fi
fi
#Specifies what happens if the -d argurment was provided:
if [[ ! -z $darg ]]
then
echo "Debug report created"
cat ~bin/Temp/log.txt | ~bin/Scripts/report.pl -d
#Specifies what happens if no argument was provided:
else
echo "Standard report created"
cat ~bin/Temp/logs.txt | ~bin/Scripts/report.pl
fi
Thank you again for your assistance!
I am trying to create a script using getopts that would work as wc. the problem is that I get stuck when I use two switches together. The script:
while getopts l:w:c: choice
do
case $choice in
l) wc -l $OPTARG;;
w) wc -w $OPTARG;;
c) wc -c $OPTARG;;
?) echo wrong option.
esac
done
When I run this script with ./script.sh -l file it works, but when I use ./script -wl file it just goes into an infinite loop. Can anyone please explain what's going on and how to fix it?
You're using it incorrectly. As per the getopts manual:
If a letter is followed by a colon, the option is expected to have an
argument.
And in your example you're not passing argument for -w and -l options;
Correct usage is:
./script -w file1 -l file2
Which will process both options correctly.
Otherwise to support an option without argument just use it without colon like this:
while getopts "hl:w:c:" choice
Here option h will not need an argument but l, w, c will support one.
You need to build the options in the case statement and then execute wc:
# Set WC_OPTS to empty string
WC_OPTS=();
while getopts lwc choice
do
case $choice in
l) WC_OPTS+='-l';;
w) WC_OPTS+='-w';;
c) WC_OPTS+='-c';;
?) echo wrong option.
esac
done
# Call wc with the options
shift $((OPTIND-1))
wc "${WC_OPTS[#]}" "$#"
To add to the other comments . . . the version of wc that I have handy seems to handle its options like this:
#!/bin/bash
options=()
files=()
while (( $# > 0 )) ; do
if [[ "$1" = --help || "$1" = --version ]] ; then
wc "$1" # print help-message or version-message
exit
elif [[ "${1:0:1}" = - ]] ; then
while getopts cmlLw opt ; do
if [[ "$opt" = '?' ]] ; then
wc "$1" # print error-message
exit
fi
options+="$opt"
done
shift $((OPTIND-1))
OPTIND=1
else
files+="$1"
shift
fi
done
wc "${options[#]}" "${files[#]}"
(The above could be refined further, by using a separate variable for each of the five possible options, to highlight the fact that wc doesn't care about the order its options appear in, and doesn't care if a given option appears multiple times.)
Got a workaround.
#!/bin/bash
if [ $# -lt 2 ]
then
echo not a proper usage
exit
fi
file=$2
while getopts wlc choice
do
case $choice in
l) wc -l $file
;;
w) wc -w $file
;;
c) wc -c $file
;;
?) echo thats not a correct choice
esac
done
I think I got obsessed with OPTARG, thanks everyone for your kind help
Suppose I have this simple script
#! /bin/sh
if [ $# -ne 2 ]
then
echo "Usage: $0 arg1 arg2"
exit 1
fi
head $1 $2
## But this is supposed to be:
## if -f flag is set,
## call [tail $1 $2]
## else if the flag is not set
## call [head $1 $2]
So what's the simplest way to add a 'flag' checking to my script?
Thanks
fflag=no
for arg in "$#"
do
test "$arg" = -f && fflag=yes
done
if test "$fflag" = yes
then
tail "$1" "$2"
else
head "$1" "$2"
fi
This simpler approach might also be viable:
prog=head
for i in "$#"
do
test "$i" = -f && prog=tail
done
$prog "$1" "$2"
I usually go for the "case" statement when parsing options:
case "$1" in
-f) call=tail ; shift ;;
*) call=head ;;
esac
$call "$1" "$2"
Remember to quote the positional parameters. They might contain file names or directory names with spaces.
If you can use e.g. bash instead of Bourne shell, you can use e.g. the getopts built-in command. For more information see the bash man page.