How to handle "--" in the shell script arguments? - bash

This question has 3 parts, and each alone is easy, but combined together is not trivial (at least for me) :)
Need write a script what should take as its arguments:
one name of another command
several arguments for the command
list of files
Examples:
./my_script head -100 a.txt b.txt ./xxx/*.txt
./my_script sed -n 's/xxx/aaa/' *.txt
and so on.
Inside my script for some reason I need distinguish
what is the command
what are the arguments for the command
what are the files
so probably the most standard way write the above examples is:
./my_script head -100 -- a.txt b.txt ./xxx/*.txt
./my_script sed -n 's/xxx/aaa/' -- *.txt
Question1: Is here any better solution?
Processing in ./my_script (first attempt):
command="$1";shift
args=`echo $* | sed 's/--.*//'`
filenames=`echo $* | sed 's/.*--//'`
#... some additional processing ...
"$command" "$args" $filenames #execute the command with args and files
This solution will fail when the filenames will contain spaces and/or '--', e.g.
/some--path/to/more/idiotic file name.txt
Question2: How properly get $command its $args and $filenames for the later execution?
Question3: - how to achieve the following style of execution?
echo $filenames | $command $args #but want one filename = one line (like ls -1)
Is here nice shell solution, or need to use for example perl?

First of all, it sounds like you're trying to write a script that takes a command and a list of filenames and runs the command on each filename in turn. This can be done in one line in bash:
$ for file in a.txt b.txt ./xxx/*.txt;do head -100 "$file";done
$ for file in *.txt; do sed -n 's/xxx/aaa/' "$file";done
However, maybe I've misinterpreted your intent so let me answer your questions individually.
Instead of using "--" (which already has a different meaning), the following syntax feels more natural to me:
./my_script -c "head -100" a.txt b.txt ./xxx/*.txt
./my_script -c "sed -n 's/xxx/aaa/'" *.txt
To extract the arguments in bash, use getopts:
SCRIPT=$0
while getopts "c:" opt; do
case $opt in
c)
command=$OPTARG
;;
esac
done
shift $((OPTIND-1))
if [ -z "$command" ] || [ -z "$*" ]; then
echo "Usage: $SCRIPT -c <command> file [file..]"
exit
fi
If you want to run a command for each of the remaining arguments, it would look like this:
for target in "$#";do
eval $command \"$target\"
done
If you want to read the filenames from STDIN, it would look more like this:
while read target; do
eval $command \"$target\"
done

The $# variable, when quoted will be able to group parameters as they should be:
for parameter in "$#"
do
echo "The parameter is '$parameter'"
done
If given:
head -100 test this "File name" out
Will print
the parameter is 'head'
the parameter is '-100'
the parameter is 'test'
the parameter is 'this'
the parameter is 'File name'
the parameter is 'out'
Now, all you have to do is parse the loop out. You can use some very simple rules:
The first parameter is always the file name
The parameters that follow that start with a dash are parameters
After the "--" or once one doesn't start with a "-", the rest are all file names.
You can check to see if the first character in the parameter is a dash by using this:
if [[ "x${parameter}" == "x${parameter#-}" ]]
If you haven't seen this syntax before, it's a left filter. The # divides the two parts of the variable name. The first part is the name of the variable, and the second is the glob filter (not regular expression) to cut off. In this case, it's a single dash. As long as this statement isn't true, you know you have a parameter. BTW, the x may or may not be needed in this case. When you run a test, and you have a string with a dash in it, the test might mistake it for a parameter of the test and not the value.
Put it together would be something like this:
parameterFlag=""
for parameter in "$#" #Quotes are important!
do
if [[ "x${parameter}" == "x${parameter#-}" ]]
then
parameterFlag="Tripped!"
fi
if [[ "x${parameter}" == "x--" ]]
then
print "Parameter \"$parameter\" ends the parameter list"
parameterFlag="TRIPPED!"
fi
if [ -n $parameterFlag ]
then
print "\"$parameter\" is a file"
else
echo "The parameter \"$parameter\" is a parameter"
fi
done

Question 1
I don't think so, at least not if you need to do this for arbitrary commands.
Question 3
command=$1
shift
while [ $1 != '--' ]; do
args="$args $1"
shift
done
shift
while [ -n "$1" ]; do
echo "$1"
shift
done | $command $args
Question 2
How does that differ from question 3?

Related

Need help matching a mattern using grep/egrep in bash scripting

I am trying to match all characters of given string but those characters should match in the order as given to the bash script.
while [[ $# -gt 0 ]]; do
case $1 in
-i)
arg=$2
egrep "*[$arg]*" words.txt
shift ;;
esac
shift
done
$ sh match_the_pattern.sh -i aei words.txt
Should return words like
abstentious
adventitious
sacrilegiousness
If you notice, first a is matched then e and then i, all of them are in order. Plus, the whole word is matched and filtered.
You may use getopts with some bash parameter substitution to construct the query string.
#!/bin/bash
while getopts 'i:' choice
do
case "${choice}" in
i)
length=${#OPTARG}
for((count=0;count<length;count++))
do
if [ $count -eq 0 ]
then
pattern="${pattern}.*${OPTARG:count:1}.*"
else
pattern="${pattern}${OPTARG:count:1}.*"
fi
done
;;
esac
done
# The remaining parameter should be our filename
shift $(($OPTIND - 1))
filename="$1"
# Some error checking based on the parsed values
# Ideally user input should not be trusted, so a security check should
# also be done,omitting that for brevity.
if [ -z "$pattern" ] || [ -z "$filename" ]
then
echo "-i is must. Also, filename cannot be empty"
echo "Run the script like ./scriptname -i 'value' -- filename"
else
grep -i "${pattern}" "$filename"
fi
Refer this to know more on parameter substitution and this for getopts.
Change this:
arg=$2
egrep "*[$arg]*" words.txt
to this:
arg=$(sed 's/./.*[&]/g' <<< "$2")
grep "$arg" words.txt
If that's not all you need then edit your question to clarify your requirements and provide more truly representative sample input/output.
Your regex is matching 'a' or 'e' or 'i' because they are in a character set ([]).
I think the regular expression you are looking for is
a+.*e+.*i+.*
which matches 'a' one or more times, then anything, then 'e' one or more times, then anything, then 'i' one or more times.

How can I make bash evaluate IF[[ ]] from string?

I am trying to create a "Lambda" style WHERE script.
I want lambdaWHERE to take piped input and pass it through if condition after given as arguments is met. Like xargs I use {} to represent what comes down the pipe.
I call command like:
ls -d EqAAL* | lambdaWHERE.sh -f {}/INFO_ACTIVETICK
I want the folder names passed through if they contain a file called INFO_ACTIVETICK
Here is the script:
#!/bin/sh
#set -x
ARGS=$*
while read i
do
CMD=`echo $ARGS | sed 's/{}/'$i'/g'`
if [[ $CMD ]]
then
echo $i
fi
done
But when I run it a mysterious "-n" appears...
$ ls -d EqAAL* | /q/lambdaWHERE.sh -f {}/INFO_ACTIVETICK
+ ARGS='-f {}/INFO_ACTIVETICK'
+ read i
++ echo -f '{}/INFO_ACTIVETICK'
++ sed 's/{}/EqAAL-1m/g'
+ CMD='-f EqAAL-1m/INFO_ACTIVETICK'
+ [[ -n -f EqAAL-1m/INFO_ACTIVETICK ]]
+ echo EqAAL-1m
EqAAL-1m
+ read i
How can I get the bit in the [[ ]] correct?
You were quite close. you only need to switch to the standard POSIX [ $CMD ] and it will work.
The main difference between using [[ $CMD ]] and [ $CMD ] is that the first has fewer surprises and you need not quote variables. That also means that a variable is though of as one token and cannot have a whole expression in it like you are trying. [ $CMD ] however works the same way as the original shell where [ was just a command an thus need explicit quotations in order to interpret something with spaces as one argument.
There is a relevant question about the differences between [[ ...]] and [ ..]

Bash script trouble interpretting input

I wrote a bash script that uploads a file on my home server. It gets activated from a folder action script using applescript. The setup is the folder on my desktop is called place_on_server. Its supposed to have an internal file structure exactly like the folder I want to write to: /var/www/media/
usage goes something like this:
if directory etc added to place_on_server: ./upload DIR etc
if directory of directory: etc/movies ./upload DIR etc movies //and so on
if file to place_on_server: ./upload F file.txt
if file in file in place_on_server ./upload F etc file.txt //and so on
for creating a directory its supposed to execute a command like:
ssh root#192.168.1.1<<EOF
cd /var/www/media/wherever
mkdir newdirectory
EOF
and for file placement:
rsync -rsh='ssh -p22' file root#192.168.1.1:/var/www/media/wherever
script:
#!/bin/bash
addr=$(ifconfig -a | ./test)
if ($# -le "1")
then
exit
elif ($1 -eq "DIR")
then
f1="ssh -b root#$addr<<EOF"
list = "cd /var/www/media\n"
if($# -eq "2")
then
list=list+"mkdir $2\nEOF\n"
else
num=2
i=$(($num))
while($num < $#)
do
i=$(($num))
list=list+"mkdir $i\n"
list=list+"cd $i\n"
$num=$num+1
done
fi
echo $list
elif ($1 -eq "F")
then
#list = "cd /var/www/media\n"
f2="rsync -rsh=\'ssh -p22\' "
f3 = "root#$addr:/var/www/media"
if($# -eq "2")
then
f2=f2+$2+" "+f3
else
num=3
i=$(($num))
while($num < $#)
do
i=$(($num))
f2=f2+"/"+$i
$num=$num+1
done
i=$(($num))
f2=f2+$i+" "+$f3
fi
echo $f2
fi
exit
output:
(prompt)$ ./upload2 F SO test.txt
./upload2: line 3: 3: command not found
./upload2: line 6: F: command not found
./upload2: line 25: F: command not found
So as you can see I'm having issues handling input. Its been awhile since I've done bash. And it was never extensive to begin with. Looking for a solution to my problem but also suggestions. Thanks in advance.
For comparisons, use [[ .. ]]. ( .. ) is for running commands in subshells
Don't use -eq for string comparisons, use =.
Don't use < for numerical comparisons, use -lt
To append values, f2="$f2$i $f3"
To add line feeds, use $'\n' outside of double quotes, or a literal linefeed inside of them.
You always need "$" on variables in strings to reference them, otherwise you get the literal string.
You can't use spaces around the = in assignments
You can't use $ before the variable name in assignments
To do arithmetics, use $((..)): result=$((var1+var2))
For indirect reference, such as getting $4 for n=4, use ${!n}
To prevent word splitting removing your line feeds, double quote variables such as in echo "$line"
Consider writing smaller programs and checking that they work before building out.
Here is how I would have written your script (slightly lacking in parameter checking):
#!/bin/bash
addr=$(ifconfig -a | ./test)
if [[ $1 = "DIR" ]]
then
shift
( IFS=/; echo ssh "root#$addr" mkdir -p "/var/www/media/$*"; )
elif [[ $1 = "F" ]]
then
shift
last=$#
file=${!last}
( IFS=/; echo rsync "$file" "root#$addr:/var/www/media/$*" )
else
echo "Unknown command '$1'"
fi
$* gives you all parameters separated by the first character in $IFS, and I used that to build the paths. Here's the output:
$ ./scriptname DIR a b c d
ssh root#somehost mkdir -p /var/www/media/a/b/c/d
$ ./scriptname F a b c d somefile.txt
rsync somefile.txt root#somehost:/var/www/media/a/b/c/d/somefile.txt
Remove the echos to actually execute.
The main problem with your script are the conditional statements, such as
if ($# -le "1")
Despite what this would do in other languages, in Bash this is essentially saying, execute the command line $# -le "1" in a subshell, and use its exit status as condition.
in your case, that expands to 3 -le "1", but the command 3 does not exist, which causes the error message
./upload2: line 3: 3: command not found
The closest valid syntax would be
if [ $# -le 1 ]
That is the main problem, there are other problems detailed and addressed in that other guy's post.
One last thing, when you're assigning value to a variable, e.g.
f3 = "root#$addr:/var/www/media"
don't leave space around the =. The statement above would be interpreted as "run command f3 with = and "root#$addr:/var/www/media" as arguments".

bash picking arguments

I want to write a function for when I have something like the following
echo 1 2 3|pick
Pick will then take the arguments and I will do something with them.
How do I do this?
Are you looking for xargs?
pick() {
read -r arg1 arg2 remainder
echo first arg is $arg1
echo The remaining args are $remainder
}
--EDIT (response to question in comment)
One way to loop through the arguments:
pick() {
read args;
set $args;
while test $# -ne 0; do
echo $1
shift
done
}
On each iteration of the loop, $1 refers to an argument.
If I'm not mistaken, the OP wants the same thing I do: you feed it a string, and if the string containes multiple {words,lines}, it presents you a menu, and you pick one, and it returns the one you pick on stdout.
If there's only one item, it just returns it.
This is useful for--to use my particular use-case--a log file viewer script: you give it a substring of a filename, and it greps through find /var/log -name \*$arg\* -print to see what it can find. If it gets a unique hit, it hands it back to your script, which runs less against it. If it gets more than one hit, it shows you a menu, and lets you pick one.
ISTR that KSH has a builtin for this, but that I wasn't all that impressed with it; I don't recall if bash has one.
I am here because I was searching to see if someone had already written it before writing it myself. :-)
UPDATE: Nope; I wrote it myself:
Here's some example code:
/usr/local/bin/msg:
PATH=$PATH:/usr/local/bin
[ $UID = 0 ] || exec sudo su root -c "$0 $*"
FILE=/var/log/messages
[ $# -eq 1 ] &&
FILE=`find /var/log/ -name \*$1\* -print |
egrep -v '2011|.[0-9]$' |
pick`
echo "$FILE"
less +F $FILE
Since I'm piping the name to less +F I want to grep out archived log files; this is for interactive log viewing.
/usr/local/bin/pick:
# Present the user a bash Select menu, and let them pick
# Try to be smart about multi-line responses
# must take input on stdin if it might be multiline
# get multiline input from stdin
while read LINE </dev/stdin
do
CHOICES+=( $LINE )
done
# add on anything specified as arguments
while [ $# -gt 0 ]
do
CHOICES+=( $1 )
shift
done
# if only one thing to pick, just pick it
if [ ${#CHOICES[*]} -eq 1 ]
then
echo $CHOICES
exit
fi
# eval set $CHOICES
select CHOSEN in ${CHOICES[#]}
do
echo $CHOSEN
exit
done </dev/tty

How to detect a filename within a case statement - in unix shell?

I coded the below code and if no -l or -L option is passes to the script I need to assume (detect) whether a filename was passed as a param. The below third condition only matches if filename is one lowercase character. How can I make it flexible to match upper and lower case letters or variable length of the string?
while [ $# -gt 0 ]
do
case $1 in
-l) echo ls;;
-L) echo ls -l;;
[a-z]) echo filename;;
*) echo usage
exit 1;;
esac
shift
done
Also, how can I include a condition in that case statement that would react to empty $1?
If for example the script is called without any options or filenames.
You can match an empty string with a '') or "") case.
A file name can contain any character--even weird ones likes symbols, spaces, newlines, and control characters--so trying to figure out if you have a file name by looking for letters and numbers isn't the right way to do it. Instead you can use the [ -e filename ] test to check if a string is a valid file name.
You should, by the way, put "$1" in double quotes so your script will work if the file name does contain spaces.
case "$1" in
'') echo empty;;
-l) echo ls;;
-L) echo ls -l;;
*) if [ -e "$1" ]; then
echo filename
else
echo usage >&2 # echo to stderr
exit 1
fi;;
esac
Use getopts to parse options, then treat remaining non-option arguments however you like (such as by testing if they're a file).
you find the information in the bash man page if you search for "Pattern Matching" without the quotes.this does the trick: [a-zA-Z0-9]*)
you should probably read on about pattern matching, regular expressions and so on.
furthermore you should honour john kugelmans hint about the double quotes.
in the following code snippet you can see how to check if no parameter got passed.
#!/bin/sh
case "$1" in
[a-zA-Z0-9]*)
echo "filename"
;;
"")
echo "no data"
;;
esac
#OP, generally if you are using bash, to match case insensitivity you can use shopt and set nocasematch
$ shopt -s nocasematch
to check null in your case statement
case "$var" in
"") echo "empty value";;
esac
You are better off falling into the default case *) and there you can at least check if the file exists with [ -e "$1" ] ... if it doesn't then echo usage and exit 1.

Resources