How to assign stdin arguments to variables in BASH from Pipe - bash

What I'm trying:
I'm trying to assign args to a variable if it is from Pipe
Expected:
What should I do inside my script to assign the arguments to variables so that they look like this?
if [ -p /dev/stdin ]; then
option1="one"
option2="two"
option3="three"
echo "$option1" "$option2" "$option3"
else
echo "no input"
fi
Input: echo one two three | ./myscript
Output: one two three
Question Update:
I need all the arguments presented before the |(pipe) as just string input to my script.
It should not check for existence or execute the binary(here the binary is echo) present before the |(pipe).
Input: echo one two three | ./myscript
Output: echo one two

The words one, two, three in echo one two three | ./myscript are arguments of echo; but to ./myscript they are only input, not arguments.
Reading "arguments" from stdin
To read each word into its own variable use
if [ -p /dev/stdin ]; then
read -r option1 option2 option3
echo "$option1" "$option2" "$option3"
else
echo "no input"
fi
If you want to allow an arbitrary number of words, use an array.
read -ra myArray reads all words from a single line into an array.
mapfile -t myArray reads all lines into an array.
read -rd '' -a myArray reads all words from all lines into an array.
To access the words in the array, use ${myArray[0]}, ${myArray[1]}, ..., ${myArray[${#myArray[#]}-1]}.
Using actual arguments
Instead of parsing stdin it might be better to use actual arguments. To do so, execute your script like ./myscript one two three. Then access the arguments using positional parameters:
if [ $# = 0 ]; then
echo "no arguments"
else
echo "The arguments are $1 $2 $3 ..."
fi
For an arbitrary number of arguments check out shift and $#.

You should probably simply use xargs instead. But what you ask isn't hard to do per se.
if [ -p /dev/stdin ]; then
read -r option1 option2 option3
echo "$option1" "$option2" "$option3"
else
echo "no input"
fi
With xargs, this would look like
option1=$1
option2=$2
option3=$3
and then you'd just run it with
echo first second third |
xargs ./yourscript

Related

Validate the number of arguments passed in bash from read

I have a question about validating user input regarding number of arguments passed by the user in a bash script.
For example, if I use:
if [[ $# -eq 2 ]]
then...
that will check if 2 arguments passed from the command line like this:
./somescript.sh arg1 arg2
but how to validate if user passed 2 arguments when asked?
For example:
echo "Type 2 names:"
read...
if [[ user passed more || less than 2 arguments]]
echo "incorrect number of names"
Now if I try to use $# -eq 2 it doesn't work.
What's the proper way to do it?
Use an array:
read -r -a array
if [[ "${#array[#]}" -eq 2 ]]; then ...
See output of:
declare -p array
Alternatively if your shell has no array like ksh or POSIX shell, you can populate the arguments from the read variable like this:
read -r reply
set -f # Disable globbing
set -- $reply # without quotes
if [ $# -eq 2 ]; then

Passing an array to a function as one argument and other arguments after it

I want to pass an array to a function and the loop thru it.
is_node ${nodes[#]}
if I try to loop
function is_node(){
for role in "${1[#]}"
do
I get the following error:
bad substitution
If I first try to check the number of arguments, I notice there are more than one.
function is_node(){
if [[ $# -ne 1 ]] then
echo "Error - The number of arguments is not correct. 1 argument(a role name) needed"
I want to pass the array, just as one argument, and pass other arguments after
is_node array status limit
then inside the function loop thru it.
The question is perfectly valid and don't think its a duplicate of Passing arrays as parameters in bash.
The problem with passing the array as argument to the function as "${nodes[#]}" or ${nodes[#]} in this case would be at the receiving side, the array contents are not kept intact, because the contents of the array is expanded before the function is called. So when the arguments are unpacked at the receiver, they are split at $1, $2 till the size of the array. You could see it from this simple example,
set -x
newf() { echo "$1"; echo "$2"; echo "$3"; }
arr=(1 2 3)
newf "${arr[#]}"
+ newf 1 2 3
+ echo 1
1
+ echo 2
2
+ echo 3
3
as you can see the array arr is expanded to the list of positional arguments while the intention was to use an array.
So given this problem and with your claim that you have additional argument flags after the array, you need to identify in the receiver side, how to start processing arguments after the array. The best way would be to pass the array expansion using *, so that the elements quoted as a whole.
So assuming your function expects 3 arguments to it, you can define it as below. The read command on the receiver will split the whole string of array content to individual elements and store it in the array arrayArgs and you can parse it as you wish.
is_node(){
(( $# < 3 )) && { printf 'insufficient args provided' >&2; return 1; }
read -ra arrayArgs <<<"$1"
printf 'Printing the array content \n'
for element in "${arrayArgs[#]}"; do
printf '%s\n' "$element"
done
printf '2nd arg=%s 3rd arg=%s\n' "$2" "$3"
}
and pass the array as
list=(1 2 3)
is_node "${list[*]}" 4 5
I assume that you want to write function with both arguments - array and traditional "single" ones. If I am mistaken please let me know.
My solution:
#!/bin/bash
function_with_array_and_single_argument () {
declare -a _array1=("${!1}")
echo "${_array1[#]}"
echo $2
}
array="a
b
c"
function_with_array_and_single_argument "array[#]" "Szczerba"
Output:
$ ./script.sh
a
b
c
Szczerba
You can pass in a list of arguments any way you like. The arguments to the function are simply "$#".
is_node(){
for role in "$#"; do
: something with "$role"
done
}
is_node "${nodes[#]}"
Notice also the proper use of quoting, and the omission of the (gratuitous, here) Bash-only keyword function.
More tangentially, the shell assumes in "$#" if you don't pass an explicit list of tokens, so this can (slightly obscurely) be simplified to for role; do
If you have a fixed number of other arguments, just put them before the variable-length list of arguments.
Nice Szczerba!
Your solution works perfectly, the contents of the array can change without changing the relative position of other variables, and that sets your answer appart. Here is an example backup script that can handle different sub directory's based on your solution.
#!/bin/bash
#Logging
logpath="/tmp/ram1"
eDate=$( date '+%Y%m%d_%H%M%S' )
ext="log"
#Backup Source/Destination drives and folder
src1="/mymedia"
subs1=(video audio)
lbl1="M_ResQ1"
dest1="/mnt/media/SG_ResQ1/Archive"
src2="/mymedia"
subs2=(TVSeries _In Test pic Theater)
lbl2="M_ResQ2"
dest2="/mnt/media/SG_ResQ2/Archive"
opt="-va --partial --del"
#-q quite
#-n dry run
#-P is = --partial --progress
Arc (){ # $1 subs $2 from $3 lbl $4 dest
declare -a subs=("${!1}")
from="$2"
lbl=$3
dest=$4
if [ -d "$dest" ]; then
for i in "${subs[#]}"; do
#logto=${logpath}/${eDate}_${lbl}_${i}.${ext}
logto=${logpath}/${eDate}_${lbl}.${ext}
echo $logto $lbl $dest
echo -e "\n\nStarting:\n\t${i}\tinto\t${lbl}\n\t${eDate}\n\t${opt}\n\n" | tee -a ${logto}
rsync ${opt} ${from}/${i} ${dest}/ | tee -a ${logto}
done
echo $( date '+Done %Y%m%d_%H%M%S' ) | tee -a ${logto}
cp ${logto} ${dest}/
else
echo -e "Not mounted or wrong drive"
fi
}
Arc "subs1[#]" $src1 $lbl1 $dest1
Arc "subs2[#]" $src2 $lbl2 $dest2

How to make multiple search and replace in files via bash

i have script. In this script i made search and replace of words. Word by word until word 'end'. It is ok and it works. You can see body of my script:
#!/bin/bash
end=end
until [ "$first" = "$end" ];do
echo "please write first word";
read first
if grep -q "$first" *txt; then
echo "word is exists"
grep "$first" *txt
echo "please write second word";
read second
sed -i 's/'"$first"'/'"$second"'/g' *txt
else
echo "second word does not exists"
exit 1
fi
done
It works for me. I have in the result console, where I can endlessly loop words, but if i want to do something like this: How can i write multiple words in line.
For example: "dog" "cat" "fish"
And search and replace all of these words. How can do it? For example, if i need to replace on these words ("elephat" "mouse" "bird"). How can you do it?
I mean search and replace words, like arguments.
You just need a loop to process the arguments.
Assuming you run the script passing pairs of original replacement words (myscript.sh original_word1 replacement1 original_word2 replacement2 ...) it would be something like the following:
while [[ $# -gt 1 ]]
do
original="$1"
replacement="$2"
# your code for actually replacing $original with $replacement
shift # discard already processed original arg
shift # discard already processed replacement arg
done
Note that if the user passes a last original word without replacement the script will just ignore it
Your English is rough, but I think you want to be able to prompt for multiple words, and replace them with a new set?
The below code will let you run a program like replace_words one two three and then be prompted for a list of words to replace, e.g. 1 2 3. After that, it exits.
declare -a replace_list=( "$#" ) # get the replace list as passed arguments
echo -n "Enter words to replace with: "; read -ra sub_list
for ((i=0; i < "${#replace_list[#]}"; ++i)); do
if grep -q "${replace_list[$i]}" *txt; then
echo "first word is exists"
sed -i "s/${replace_list[$i]}/${sub_list[$i]}/g" *txt
else
echo "${replace_list[$i]} does not exists"
exit 1
fi
done

Store array output to comma separated list in bash scripting

I have taken input from user into array. But I need to use them as comma separated list. How can I do that? Input in my case is path like (/usr/tmp/). I appreciate your help and time. Thank you !
Example:
read "Number of subdirectories : " count
for i in $(seq 1 $count)
do
read -e -p " Subdir : $i: " arr[$i]
done
Expected Result:
$var = {arr[1],arr[2],arr[3],......}
If you have an array like this:
$ declare -p arr
declare -a arr='([1]="abc" [2]="def")'
You can display it in comma-separated format:
$ (IFS=,; echo "{${arr[*]}}")
{abc,def}
That output can be saved in a shell variable using command substitution:
$ var=$(IFS=,; echo "{${arr[*]}}")
$ echo "$var"
{abc,def}

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