How to make multiple search and replace in files via bash - 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

Related

Why is my variable not holding its value in bash?

I am horribly perplexed.
I've written a bash script to sort lines into categories based on substrings within that line.
Here's my example "lines.txt"
i am line1
i am line2
If a line contains "line1", then it should be sorted into group "l1". If it contains "line2", then it should be sorted into group "l2"
The problem is that the variable which holds the category isn't retaining its value, and I have no clue why. Here's the script.
#!/bin/bash
categories="l1 l2"
l1="
line1
"
l2="
line2
"
# match line1
cat lines.txt | while read fline
do
cate="no match"
for c in $categories
do
echo "${!c}" | while read location
do
if [ ! -z "$location" ] && [[ "$fline" =~ "$location" ]]
then
echo "we are selecting category $c"
cate="$c"
break
fi
done
if [ "$cate" != "no match" ]
then
echo "we found a match"
break
fi
done
echo "$cate:$fline"
done
exit 0
And when I run it, I see the output
we are selecting category l1
no match:i am line1
we are selecting category l2
no match:i am line2
This means that we are selecting the correct group, but we don't remember it when we exit the nested "while" loop.
Why is my variable not retaining its value, and how could I fix that?
The while loop is executed in a subshell because of the pipe. That means that the name 'cate' really refers to two different variables. One outside the while loop and the other inside the loop inside the subshell. When the subshell exits that value is lost.
A way to get around this is to use a redirect like this
while read line; do
...
done < $myfile
If the expression is more complicated and you need something executed in a subshell, then you can use process substitution (Thanks to David Rankin for reminding me about this one).
while read -r line; do
...
done < <(find . -iname "*sh")

Making flashcards with "select" command

So I have a file called "nouns" that looks like this:
English word:matching Spanish word
Englsih word:matching Spanish word
..etc etc
I need to make a program that list all the English words with an option to quit. The program displays the English words and ask the user for the word he wants translated and he can also type "quit" to exit.
This is what I have so far that shows me the list in English
select english in $(cut -d: -f1 nouns)
do
if [ "$english" = 'quit' ]
then
exit 0
fi
done
I know that I need to run a command that pulls up the second column (-f2) by searching for the corresponding English word like this
result=$(grep -w $english nouns|cut -d: -f2)
My end result should just out put the corresponding Spanish word. I am just not sure how to get all the parts to fit together. I know its based in a type of "if" format (I think) but do I start a separate if statement for the grep line?
Thanks
You need a loop in which you ask for input from user. The rest is putting things together with the correct control flow. See my code below:
while :
do
read -p "Enter word (or quit): " input
if [ "$input" = "quit" ]; then
echo "exiting ..."
break
else
echo "searching..."
result=$(grep $input nouns | cut -d ':' -f 2)
if [[ $result ]]; then
echo "$result"
else
echo "not found"
fi
fi
done
dfile=./dict
declare -A dict
while IFS=: read -r en es; do
dict[$en]=$es
done < "$dfile"
PS3="Select word>"
select ans in "${!dict[#]}" "quit program"; do
case "$REPLY" in
[0-9]*) w=$ans;;
*) w=$REPLY;;
esac
case "$w" in
quit*) exit 0;;
*) echo "${dict[$w]}" ;;
esac
done
You want to run this in a constant while loop, only breaking the loop if the user enters "quit." Get the input from the user using read to put it in a variable. As for the searching, this can be done pretty easily with awk (which is designed to work with delimited files like this) or grep.
#!/bin/sh
while true; do
read -p "Enter english word: " word
if [ "$word" = "quit" ]; then
break
fi
# Take your pick, either of these will work:
# awk -F: -v "w=$word" '{if($1==w){print $2; exit}}' nouns
grep -Pom1 "(?<=^$word:).*" nouns
done

sed with loop to replace certain fields in a file with delimiter :

How should I use the sed command to replace certain fields with delimiter : and run a check to make sure that the user's input can be found within the file & if it can't be found it will loop again.
main_menu #function main_menu
echo "1) choice 1"
echo "2) choice 2"
read choice #read user choice on which choice he wants
if [ $choice -eq 1 ]
then
edit_item #function
read $choice_e #read input
grep -iqs "$choice_e: " Item.txt && echo "item found" #search file to find match
while [[ ! ${choice_e} =~ ^([Item.txt])$ ]]; do #loop to find if input matches search
echo "New Title: " #input new
read choice_n
sed -i 's/^/"$choice_n"\t/' Item.txt #edit the item
done
edit_item
else
echo "error" #return user to input again
fi
The invocation of sed is flawed because of the single quotes mixed with double quotes:
sed -i 's/^/"$choice_n"\t/'
The single quotes mean that the $ (and double quotes) are not interpreted by the shell. What you're probably after is:
sed -i "s/^/$choice_n\t/"
Without knowing exactly which shell and version of sed you're using, it isn't clear whether the \t sequence will be translated to a tab or not. In Bash, you could use the ANSI C Quoting mechanism:
sed -i "s/^/$choice_n"$'\t'/
I don't see where your 'delimiter :' is coming into play at all.

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