bash picking arguments - bash

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

Related

How to iterate on different lists according to a parameter

I have a script that can get a list of folders or get them automatically with find. I'd like to write a single loop that handles both cases: if there are parameters, use them, otherwise, create the list automatically.
So far, I have something like:
if [ "$#" -gt 0 ]
then
for value in "$#"
do
v1=$(process_data "$value")
<do things with $v1>
done
else
for value in "$(find ...)"
do
v1=$(process_data "$value")
<do things with $v1>
done
fi
I'd like to do something like:
if [ "$#" -gt 0 ]
then
data="$#"
else
data="$(find ...)"
fi
for value in "${data[#]}"
do
v1=$(process_data "$value")
<do things with $v1>
done
but this data assignment can be potentially big. In a language with pointers this should not be a problem, but in bash it doesn't look good.
Is there a better way to achieve my needs?
These don't look right:
data="$#"
data="$(find ...)"
If you want data to be an array, you need
data=("$#")
data=($(find ...))
What you can do is assign to the positional parameters:
#!/bin/bash
if [ "$#" = 0 ]
then
# no arguments; default to all matching files
set -- $(find ...)
fi
for value in "$#"
do
# things
done
This does mean that the loop can't start until the find command finishes. To solve that, an alternative might be to turn it around and make using "$#" more like the output of find, i.e. stream it to a pipe, like this:
#!/bin/bash
args_or_find() {
if [ "$#" -gt 0 ]
then
printf '%s\n' "$#"
else
find ...
fi
}
while read -r value
do
# things
done < <(args_or_find)
The natural extension to improve robustness using \0 instead of \n as separator is left as an exercise.
You may use a script like this to have processing loop only once:
if (($#)); then # arguments are passed
arr=("$#")
else # build array from find command's result
arr=()
while IFS= read -rd '' f; do
arr+=("$f")
done < <(find . -name '*.txt' -print0)
fi
# main processing loop
for line in "${arr[#]}"; do
echo "processing... <$line>"
done

find -iname not working in script

Following find command.
find Work/Linux4/test/test/test_goal/spyglass_reports/clock-reset/Ac_coherency06/ -iname "Ac_coherency*.csv"
is working fine when run on shell.
But in perl script it return nothing.
#!/bin/bash
REPORT_DIR=$1
FIND_CMD=$2
echo "##";
echo $REPORT_DIR ;
echo $FIND_CMD ;
LIST_OF_CSV=$(find $REPORT_DIR $FIND_CMD)
echo $LIST_OF_CSV
if [ "X" == "X${LIST_OF_CSV}" ]; then
echo "No files Found for : '$FIND_CMD' in directory ";
echo " '$REPORT_DIR' " | sed -e 's;Work/.*/test_reports;Work/PLATFORM/test_reports;g';
echo;
exit 0;
fi
Output of script:
##
Work/$PLATFORM_SPECIES/test_reports/clock-reset/Ac_coherency06 -iname "Ac_coherency06*.csv"
No files Found for : '-iname "Ac_coherency06*.csv"' in directory 'Work/PLATFORM/test_reports/clock-reset/Ac_coherency06'
If you're allowing a list of find predicates to be passed, keep them in list form, one argument to find per argument to your script. As an example implemented in this manner:
#!/bin/bash
# read report_dir off the command line, and shift it from arguments
report_dir=$1; shift
# generate a version of report_dir for human consumption
re='Work/.*/test_reports'
replacement='Work/PLATFORM/test_reports'
if [[ $report_dir =~ $re ]]; then
report_dir_name=${report_dir//${BASH_REMATCH[0]}/$replacement}
else
report_dir_name=$report_dir
fi
# read results from find -- stored NUL-delimited -- into an array
# using NUL-delimited inputs ensure that even unusual filenames work correctly
declare -a list_of_csv
while IFS= read -r -d '' filename; do
list_of_csv+=( "$filename" )
done < <(find "$report_dir" '(' "$#" ')' -print0)
# Use the length of that array to determine whether we found contents
echo "Found ${#list_of_csv[#]} files" >&2
if (( ${#list_of_csv[#]} == 0 )); then
echo "No files found in $report_dir_name" >&2
fi
Here, shift consumes the first argument from your list, and "$#" refers to all the others that remain after that point. This means that the items you want to have passed as separate, individual arguments to find can (and must) be passed as separate, individual arguments to your script.
Thus, with usage yourscript "/path/to/report/dir" -name '*.txt', initially, $1 will be /path/to/report/dir, $2 will be -name, and $3 will be *.txt. However, after shift is run, $1 will be -name, and $2 will be *.txt; and "$#" will refer to both of those, each passed as a separate word.
For details on the use of a while read loop to read items off of a stream, see BashFAQ #001.
For details on the syntax used for bash-native string replacement, see BashFAQ #100 or http://wiki.bash-hackers.org/syntax/pe
For details on shell arrays, including ${#arrayname[#]} to check their length or "${arrayname[#]}" to expand to their contents, see BashFAQ #005.
If you have a command that is running well on the shell but not on your script, the first thing I would try would be to specify Bash on the command being called, see if this works:
bash -c 'find Work/Linux4/test/test/test_goal/spyglass_reports/clock-reset/Ac_coherency06/ -iname "Ac_coherency*.csv"'
Or even better:
/bin/bash -c 'find Work/Linux4/test/test/test_goal/spyglass_reports/clock-reset/Ac_coherency06/ -iname "Ac_coherency*.csv"'
You could also store the result on a variable or other data structure as needed, and pass it later to the script, for example:
ResultCommand="$(bash -c 'find Work/Linux4/test/test/test_goal/spyglass_reports/clock-reset/Ac_coherency06/ -iname "Ac_coherency*.csv"')"
Edit: this answer was edited more than once to fix possible issues.

Creating a which command in bash script

For an assignment, I'm supposed to create a script called my_which.sh that will "do the same thing as the Unix command, but do it using a for loop over an if." I am also not allowed to call which in my script.
I'm brand new to this, and have been reading tutorials, but I'm pretty confused on how to start. Doesn't which just list the path name of a command?
If so, how would I go about displaying the correct path name without calling which, and while using a for loop and an if statement?
For example, if I run my script, it will echo % and wait for input. But then how do I translate that to finding the directory? So it would look like this?
#!/bin/bash
path=(`echo $PATH`)
echo -n "% "
read ans
for i in $path
do
if [ -d $i ]; then
echo $i
fi
done
I would appreciate any help, or even any starting tutorials that can help me get started on this. I'm honestly very confused on how I should implement this.
Split your PATH variable safely. This is a general method to split a string at delimiters, that is 100% safe regarding any possible characters (including newlines):
IFS=: read -r -d '' -a paths < <(printf '%s:\0' "$PATH")
We artificially added : because if PATH ends with a trailing :, then it is understood that current directory should be in PATH. While this is dangerous and not recommended, we must also take it into account if we want to mimic which. Without this trailing colon, a PATH like /bin:/usr/bin: would be split into
declare -a paths='( [0]="/bin" [1]="/usr/bin" )'
whereas with this trailing colon the resulting array is:
declare -a paths='( [0]="/bin" [1]="/usr/bin" [2]="" )'
This is one detail that other answers miss. Of course, we'll do this only if PATH is set and non-empty.
With this split PATH, we'll use a for-loop to check whether the argument can be found in the given directory. Note that this should be done only if argument doesn't contain a / character! this is also something other answers missed.
My version of which handles a unique option -a that print all matching pathnames of each argument. Otherwise, only the first match is printed. We'll have to take this into account too.
My version of which handles the following exit status:
0 if all specified commands are found and executable
1 if one or more specified commands is nonexistent or not executable
2 if an invalid option is specified
We'll handle that too.
I guess the following mimics rather faithfully the behavior of my which (and it's pure Bash):
#!/bin/bash
show_usage() {
printf 'Usage: %s [-a] args\n' "$0"
}
illegal_option() {
printf >&2 'Illegal option -%s\n' "$1"
show_usage
exit 2
}
check_arg() {
if [[ -f $1 && -x $1 ]]; then
printf '%s\n' "$1"
return 0
else
return 1
fi
}
# manage options
show_only_one=true
while (($#)); do
[[ $1 = -- ]] && { shift; break; }
[[ $1 = -?* ]] || break
opt=${1#-}
while [[ $opt ]]; do
case $opt in
(a*) show_only_one=false; opt=${opt#?} ;;
(*) illegal_option "${opt:0:1}" ;;
esac
done
shift
done
# If no arguments left or empty PATH, exit with return code 1
(($#)) || exit 1
[[ $PATH ]] || exit 1
# split path
IFS=: read -r -d '' -a paths < <(printf '%s:\0' "$PATH")
ret=0
# loop on arguments
for arg; do
# Check whether arg contains a slash
if [[ $arg = */* ]]; then
check_arg "$arg" || ret=1
else
this_ret=1
for p in "${paths[#]}"; do
if check_arg "${p:-.}/$arg"; then
this_ret=0
"$show_only_one" && break
fi
done
((this_ret==1)) && ret=1
fi
done
exit "$ret"
To test whether an argument is executable or not, I'm checking whether it's a regular file1 which is executable with:
[[ -f $arg && -x $arg ]]
I guess that's close to my which's behavior.
1 As #mklement0 points out (thanks!) the -f test, when applied against a symbolic link, tests the type of the symlink's target.
#!/bin/bash
#Get the user's first argument to this script
exe_name=$1
#Set the field separator to ":" (this is what the PATH variable
# uses as its delimiter), then read the contents of the PATH
# into the array variable "paths" -- at the same time splitting
# the PATH by ":"
IFS=':' read -a paths <<< $PATH
#Iterate over each of the paths in the "paths" array
for e in ${paths[*]}
do
#Check for the $exe_name in this path
find $e -name $exe_name -maxdepth 1
done
This is similar to the accepted answer with the difference that it does not set the IFS and checks if the execute bits are set.
#!/bin/bash
for i in $(echo "$PATH" | tr ":" "\n")
do
find "$i" -name "$1" -perm +111 -maxdepth 1
done
Save this as my_which.sh (or some other name) and run it as ./my_which java etc.
However if there is an "if" required:
#!/bin/bash
for i in $(echo "$PATH" | tr ":" "\n")
do
# this is a one liner that works. However the user requires an if statment
# find "$i" -name "$1" -perm +111 -maxdepth 1
cmd=$i/$1
if [[ ( -f "$cmd" || -L "$cmd" ) && -x "$cmd" ]]
then
echo "$cmd"
break
fi
done
You might want to take a look at this link to figure out the tests in the "if".
For a complete, rock-solid implementation, see gniourf_gniourf's answer.
Here's a more concise alternative that makes do with a single invocation of find [per name to investigate].
The OP later clarified that an if statement should be used in a loop, but the question is general enough to warrant considering other approaches.
A naïve implementation would even work as a one-liner, IF you're willing to make a few assumptions (the example uses 'ls' as the executable to locate):
find -L ${PATH//:/ } -maxdepth 1 -type f -perm -u=x -name 'ls' 2>/dev/null
The assumptions - which will hold in many, but not all situations - are:
$PATH must not contain entries that when used unquoted result in shell expansions (e.g., no embedded spaces that would result in word splitting, no characters such as * that would result in pathname expansion)
$PATH must not contain an empty entry (which must be interpreted as the current dir).
Explanation:
-L tells find to investigate the targets of symlinks rather than the symlinks themselves - this ensures that symlinks to executable files are also recognized by -type f
${PATH//:/ } replaces all : chars. in $PATH with a space each, causing the result - due to being unquoted - to be passed as individual arguments split by spaces.
-maxdepth 1 instructs find to only look directly in each specified directory, not also in subdirectories
-type f matches only files, not directories.
-perm -u=x matches only files and directories that the current user (u) can execute (x).
2>/dev/null suppresses error messages that may stem from non-existent directories in the $PATH or failed attempts to access files due to lack of permission.
Here's a more robust script version:
Note:
For brevity, only handles a single argument (and no options).
Does NOT handle the case where entries or result paths may contain embedded \n chars - however, this is extremely rare in practice and likely leads to bigger problems overall.
#!//bin/bash
# Assign argument to variable; error out, if none given.
name=${1:?Please specify an executable filename.}
# Robustly read individual $PATH entries into a bash array, splitting by ':'
# - The additional trailing ':' ensures that a trailing ':' in $PATH is
# properly recognized as an empty entry - see gniourf_gniourf's answer.
IFS=: read -r -a paths <<<"${PATH}:"
# Replace empty entries with '.' for use with `find`.
# (Empty entries imply '.' - this is legacy behavior mandated by POSIX).
for (( i = 0; i < "${#paths[#]}"; i++ )); do
[[ "${paths[i]}" == '' ]] && paths[i]='.'
done
# Invoke `find` with *all* directories and capture the 1st match, if any, in a variable.
# Simply remove `| head -n 1` to print *all* matches.
match=$(find -L "${paths[#]}" -maxdepth 1 -type f -perm -u=x -name "$name" 2>/dev/null |
head -n 1)
# Print result, if found, and exit with appropriate exit code.
if [[ -n $match ]]; then
printf '%s\n' "$match"
exit 0
else
exit 1
fi

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".

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

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?

Resources