Distinguish different type of input (BASH) - bash

I have a script that must be able to accept both by files and stdin on the first argument. Then if more or less than 1 arguments, reject them
The goal that I'm trying to accomplish is able to accpet using this format
./myscript myfile
AND
./myscript < myfile
What I have so far is
if [ "$#" -eq 1 ]; then #check argument
if [ -t 0 ]; then #check whether input from keyboard (read from github)
VAR=${1:-/dev/stdin} #get value to VAR
#then do stuff here!!
else #if not input from keyboard
VAR=$1
if [ ! -f "$VAR" ]; then #check whether file readable
echo "ERROR!"
else
#do stuff heree!!!
fi
fi
fi
The PROBLEM is when I tried to say
./myscript < myfile
it prints
ERROR!
I dont know whether this is the correct way to do this, I really appreciate for suggestion or the correct code for my problem. Thank you

#!/bin/bash
# if nothing passed in command line pass "/dev/stdin" to myself
# so all below code can be made branch-free
[[ ${#} -gt 0 ]] || set -- /dev/stdin
# loop through the command line arguments, treating them as file names
for f in "$#"; do
echo $f
[[ -r $f ]] && while read line; do echo 'echo:' $line; done < $f
done
Examples:
$ args.sh < input.txt
$ args.sh input.txt
$ cat input.txt | args.sh

Related

Bash scripting: syntax error near unexpected token `done'

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.

Bash Script, stdin

I have a small problem here.
I've written a script, which works fine. But there is a small problem.
The script takes 1 or 2 arguments. The 2nd arguments is a .txt file.
If you write something like my_script arg1 test.txt, the script will work. But when you write my_script arg1 < test.txt it doesn't.
Here is a demo of my code:
#!/bin/bash
if [[ $# = 0 || $# > 2 ]]
then
exit 1
elif [[ $# = 1 || $# = 2 ]]
then
#do stuff
if [ ! -z $2 ]
then
IN=$2
else
exit 3
fi
fi
cat $IN
How can I make it work with my_script arg1 < test.txt?
If you just want to change how my_script is called, then just let cat read from myscript's standard input by giving it no argument:
#!/bin/bash
if [[ $# != 0 ]]
then
exit 1
fi
cat
If you want your script to work with either myscript arg1 < test.txt or myscript arg1 test.txt, just check the number of arguments and act accordingly.
#!/bin/bash
case $# in
0) exit 1 ;;
1) cat ;;
2) cat $2 ;;
esac
If you look at how the guys at bashnative implemented their cat you should be able to use 'read' to get the piped content..
eg. do something like this:
while read line; do
echo -n "$line"
done <"${1}"
HTH,
bovako

How to break infinite loop in this script

I am doing something interesting with bash
I wrote script below:
#!/bin/bash
while :
do
if [ -s /tmp/file.txt ]; then
for line in $(cat /tmp/file.txt)
do
echo $line
#May be some commands here
done
fi
done
and the content of my file.txt is:
1 True
2 Flase
How can I say the script if command cat /tmp/file.txt is finished (I mean all lines are read) and also echo $line and other commands are finished then break the infinitive while : loop?
Thank you
Use break.
#!/bin/bash
while :
do
if [ -s /tmp/file.txt ]; then
for line in $(cat /tmp/file.txt)
do
echo $line
#May be some commands here
done
break
fi
done
Although it would be simpler and more proper with:
#!/bin/bash
for (( ;; )); do
if [[ -s /tmp/file.txt ]]; then
# Never use `for X in $()` when reading output/input. Using word splitting
# method for it could be a bad idea in many ways. One is it's dependent with
# IFS. Second is that glob patterns like '*' could be expanded and you'd
# produce filenames instead.
while read line; do
# Place variables between quotes or else it would be subject to Word
# Splitting and unexpected output format could be made.
echo "$line"
done < /tmp/file.txt
break
fi
done
On another note, do you really need the outer loop? This time you don't need to use break.
#!/bin/bash
if [[ -s /tmp/file.txt ]]; then
while read line; do
echo "$line"
done < /tmp/file.txt
fi

Make Bash script exit and print error message if users invoke the script incorrectly

Script needed was
#!/bin/bash
# Check if there are two arguments
if [ $# -eq 2 ]; then
# Check if the input file actually exists.
if ! [[ -f "$1" ]]; then
echo "The input file $1 does not exist."
exit 1
fi
else
echo "Usage: $0 [inputfile] [outputfile]"
exit 1
fi
# Run the command on the input file
grep -P "^[\s]*[0-9A-Za-z-]+.?[\s]*$" "$1" > "$2"
Edit, the script has changed to
grep -P "^[\s]*[0-9A-Za-z-]+.?[\s]*$" $*
if [ ! -f "$1" ]; then
echo 'Usage: '
echo
echo './Scriptname inputfile > outputfile'
exit 0
fi
invoking the script with no parameters gives no erros and sits blank
Usage:
./Scriptname inputfile > outputfile
I have bit of code
grep -P "^[\s]*[0-9A-Za-z-]+.?[\s]*$" $*
This code pulls lines that have a single word on them and pumps the output to a new file, so for example
This is a multi word line
this
the above line is not
now
once again wrong
The output would be
This
now
The code works, users invoke the code using ./scriptname file > newfile
However, I am trying to expand the code to give users an error message if they invoke the script incorrectly.
For the error messange, I'm thinking of echoing something back like scriptname file_to_process > output_file.
I did try
if [incorrectly invoted unsure what to type]
echo $usage
exit 1
Usage="usage [inputfile] [>] [outputfile]
However I have had little luck. The code runs but does nothing if I invoke with just the script name. Also, if I invoke the script with just the scriptname and the input file, it will output the results instead of exiting with the error message.
Other ones I have tried are
if [ ! -n $1 ]; then
echo 'Usage: '
echo
echo './Scriptname inputfile > outputfile'
exit 0
fi
Given replies I have received so far, my code now is
#!/bin/bash
grep -P "^[\s]*[0-9A-Za-z-]+.?[\s]*$" $*
if [ ! -f "$1" ]; then
echo 'Usage: '
echo
echo './Scriptname inputfile > outputfile'
exit 0
fi
When invoking the script without an input file the script does nothing and has to be aborted with ctrl+c, still trying to get the echo of the invoke message.
When you are invoking the script like ./scriptname file > newfile, the shell interprets file as the only argument to ./scriptname. This is because > is the standard output redirection operator.
I would like to propose 2 possible alternatives:
Alternative 1:
Maybe you're can try passing it as 1 argument like this?
./scriptname 'file > newfile'
In that case one way to check the format would be
#!/bin/bash
# Check if the format is correct
if [[ $1 =~ (.+)' > '(.+) ]]; then
# Check if the input file actually exists.
if ! [[ -f "${BASH_REMATCH[1]}" ]]; then
echo "The input file ${BASH_REMATCH[1]} does not exist!"
exit 1
fi
else
echo "Usage: $0 \"[inputfile] [>] [outputfile]\""
exit 1
fi
# Redirect standard output to the output file
exec > "${BASH_REMATCH[2]}"
# Run the command on the input file
grep -P "^[\s]*[0-9A-Za-z-]+.?[\s]*$" "${BASH_REMATCH[1]}"
Note: If you are checking whether the arguments are valid or not, it's generally better to run commands only after the checking is done.
Alternative 2:
Passing 2 arguments like
./scriptname file newfile
The script looks like this
#!/bin/bash
# Check if there are two arguments
if [ $# -eq 2 ]; then
# Check if the input file actually exists.
if ! [[ -f "$1" ]]; then
echo "The input file $1 does not exist."
exit 1
fi
else
echo "Usage: $0 [inputfile] [outputfile]"
exit 1
fi
# Run the command on the input file
grep -P "^[\s]*[0-9A-Za-z-]+.?[\s]*$" "$1" > "$2"
I'd use parameter expansion for this:
inputfile=${1:?Usage: $(basename $0) inputfile > outputfile}
If the script is called without arguments (i.e. $1 is unset) the ${var:?error message} expansion causes the shell to display an error with the given message and exit. Otherwise the first argument is assigned to $inputfile.
Try to add double quotes around $1 and use -f to check for exists and is normal file:
if [ ! -f "$1" ]; then
echo 'Usage: '
echo
echo './Scriptname inputfile > outputfile'
exit 0
fi
Also you can check for the param count with $# and cat an usage message:
if [ ! $# -eq 1 ]; then
cat << EOF
Usage:
$0 'input_file' > output_file
EOF
exit 1
fi

Read line by line from different locations please help to avoid duplicated code

I've the following script.
for args
do
while read line; do
# do something
done <"$args"
done
If the script is started with a list of filenames, it should read out each file line by line.
Now I'm looking for a way the read from stdin when script is started without a list of filenames, but I doesn't want to duplicate the while loop.
Any ideas?
Quick answer:
[[ -z $1 ]] && defaultout=/dev/stdin
for f in "$#" $defaultout; do
while read line; do
# do something
done < "$f"
done
Drawback: parameters are not parsed
Second attempt:
[[ -z $1 ]] && defaultout=/dev/stdin
for f in $# $defaultout; do
if [[ -f $f ]]; then
while read line; do
# do something
done < "$f"
fi
done
Drawback: Filenames with spaces will be parsed into two words.
You could try:
args="$*"
if [ "$args" = "" ]; then
args=/dev/stdin;
fi
for arg in $args; do
while read -r line; do
# do something
done < "$arg";
done
The following should do what you want:
cat "$#" | while read line; do
# something
done

Resources