My assignment asks that:
Create a directory ~/UnixCourse/scriptAsst. Turn the two-line version,
above, of the substitution commands into a shell script, subst1 taking
three parameters: the string to be replaced the string with which to
replace it the name of the file in which to make the substitution.
For example,
`~/UnixCourse/scriptAsst/subst1 foo bar myFile.txt`
should replace all occurrences of foo in the file myFile.txt by bar, leaving the
original file as myFile.txt.bak.
Similarly,
`~/UnixCourse/scriptAsst/subst1 abc "" aardvark.dat`
should remove (replace by the empty string) all occurrences of abc in the file aardvark.dat with nothing, leaving the original file as aardvark.dat.bak.
My code that I came up with is:
#!/bin/bash
set p1 = "$1"
shift
set p2 = "$1"
shift
set p3 = "$*"
echo $p1
echo $p2
echo $p3
if grep "$p1" "$p3" > /dev/null; then
mv "$p3" "$p3.bak"
sed "s/$p1/$p2/g" "$p3.bak" > "$p3"
fi
When I try to run:
./subst1 foo bar myFile.txt
I keep getting:
grep: : No such file or directory
Please help!! What am I doing wrong??
This is how you set variables:
p1="$1"
shift
p2="$1"
shift
p3="$1"
or in this case simply:
p1="$1"; p2="$2"; p3="$3"
Note:
try to use meaningful variable names, or simply use $1 directly.
there is grep -q so you don't have to redirect standard output.
You don't actually need to do anything.
sed -i.bak "s/$1/$2/g" "$3"
or use the parameters directly...
#!/bin/bash
echo $1
echo $2
echo $3
if grep "$1" "$3" > /dev/null; then
mv "$3" "$3.bak"
sed "s/$1/$2/g" "$3.bak" > "$3"
fi
Related
I have a textfile called log.txt, and it logs the file name and the path it was gotten from. so something like this
2.txt
/home/test/etc/2.txt
basically the file name and its previous location. I want to use grep to grab the file directory save it as a variable and move the file back to its original location.
for var in "$#"
do
if grep "$var" log.txt
then
# code if found
else
# code if not found
fi
this just prints out to the console the 2.txt and its directory since the directory has 2.txt in it.
thanks.
Maybe flip the logic to make it more efficient?
f=''
while read prev
do case "$prev" in
*/*) f="${prev##*/}"; continue;; # remember the name
*) [[ -e "$f" ]] && mv "$f" "$prev";;
done < log.txt
That walks through all the files in the log and if they exist locally, move them back. Should be functionally the same without a grep per file.
If the name is always the same then why save it in the log at all?
If it is, then
while read prev
do f="${prev##*/}" # strip the path info
[[ -e "$f" ]] && mv "$f" "$prev"
done < <( grep / log.txt )
Having the file names on the same line would significantly simplify your script. But maybe try something like
# Convert from command-line arguments to lines
printf '%s\n' "$#" |
# Pair up with entries in file
awk 'NR==FNR { f[$0]; next }
FNR%2 { if ($0 in f) p=$0; else p=""; next }
p { print "mv \"" p "\" \"" $0 "\"" }' - log.txt |
sh
Test it by replacing sh with cat and see what you get. If it looks correct, switch back.
Briefly, something similar could perhaps be pulled off with printf '%s\n' "$#" | grep -A 1 -Fxf - log.txt but you end up having to parse the output to pair up the output lines anyway.
Another solution:
for f in `grep -v "/" log.txt`; do
grep "/$f" log.txt | xargs -I{} cp $f {}
done
grep -q (for "quiet") stops the output
I want to input multiple strings.
For example:
abc
xyz
pqr
and I want output like this (including quotes) in a file:
"abc","xyz","pqr"
I tried the following code, but it doesn't give the expected output.
NextEmail=","
until [ "a$NextEmail" = "a" ];do
echo "Enter next E-mail: "
read NextEmail
Emails="\"$Emails\",\"$NextEmail\""
done
echo -e $Emails
This seems to work:
#!/bin/bash
# via https://stackoverflow.com/questions/1527049/join-elements-of-an-array
function join_by { local IFS="$1"; shift; echo "$*"; }
emails=()
while read line
do
if [[ -z $line ]]; then break; fi
emails+=("$line")
done
join_by ',' "${emails[#]}"
$ bash vvuv.sh
my-email
another-email
third-email
my-email,another-email,third-email
$
With sed and paste:
sed 's/.*/"&"/' infile | paste -sd,
The sed command puts "" around each line; paste does serial pasting (-s) and uses , as the delimiter (-d,).
If input is from standard input (and not a file), you can just remove the input filename (infile) from the command; to store in a file, add a redirection at the end (> outfile).
If you can withstand a trailing comma, then printf can convert an array, with no loop required...
$ readarray -t a < <(printf 'abc\nxyx\npqr\n' )
$ declare -p a
declare -a a=([0]="abc" [1]="xyx" [2]="pqr")
$ printf '"%s",' "${a[#]}"; echo
"abc","xyx","pqr",
(To be fair, there's a loop running inside bash, to step through the array, but it's written in C, not bash. :) )
If you wanted, you could replace the final line with:
$ printf -v s '"%s",' "${a[#]}"
$ s="${s%,}"
$ echo "$s"
"abc","xyx","pqr"
This uses printf -v to store the imploded text into a variable, $s, which you can then strip the trailing comma off using Parameter Expansion.
I have a variable that contains a complete path name. I am trying to conditionally replace the last occurrence of a word in the path. Example script to show what I am trying
#!/bin/sh
testvar="/home/downloads/user/downloads"
if [ "$1" = "alternate" ]; then
newtestvar=$(echo $testvar | sed -e 's/\(.*\)downloads$/\1alternate_downloads/g')
else
newtestvar=$(echo $testvar | sed -e 's/\(.*\)downloads$/\1new_downloads/g')
fi
echo "testvar:" $testvar
echo "newtestvar:" $newtestvar
Run #1
$ ./foofile
testvar: /home/downloads/user/downloads
newtestvar: /home/downloads/user/new_downloads
Run #2
$ ./foofile alternate
testvar: /home/downloads/user/downloads
newtestvar: /home/downloads/user/alternate_downloads
I do get the intended result, but I am looking for a way to avoid the if/else and rather achieve the result by checking the $1 in context of sed.
Edit-1
I replaced the if/else block with following shorthand. but it looks really clumsy and difficult to read.
newtestvar=$([[ $1 = "alternate" ]] && echo $testvar | sed -e 's/\(.*\)downloads$/\1alternate_downloads/g' || echo $testvar | sed -e 's/\(.*\)downloads$/\1new_downloads/g')
You can avoid sed and handle this in bash itself:
#!/bin/bash
testvar="/home/downloads/user/downloads"
# default s to "new"
s="${1:-new}"
# replace only last value of downloads
newtestvar="${testvar/%downloads/${s}_downloads}"
# examine both variables
declare -p testvar newtestvar
Now call it as:
./foofile
declare -- testvar="/home/downloads/user/downloads"
declare -- newtestvar="/home/downloads/user/new_downloads"
./foofile alternate
declare -- testvar="/home/downloads/user/downloads"
declare -- newtestvar="/home/downloads/user/alternate_downloads"
This can probably not be done with sed, because sed has no way to test the value of a variable and then conditionally branch the execution.
However, it can be done with AWK:
#!/bin/sh
testvar="/home/downloads/user/downloads"
newtestvar=$(awk -v arg="$1" '{
replacement = arg == "alternate" ? "alternate_downloads" : "new_downloads";
sub("downloads$", replacement);
print $0;
}
' <<<"$testvar")
echo "testvar:" $testvar
echo "newtestvar:" $newtestvar
I have some variables in a bash script that may contain a file name or be unset. Their content should be passed as an additional argument to a program. But this leaves an empty argument when the variable is unset.
$ afile=/dev/null
$ anotherfile=/dev/null
$ unset empty
$ cat "$afile" "$empty" "$anotherfile"
cat: : No such file or directory
Without quotes, it works just fine as the additional argument is simply omitted. But as the variables may contain spaces, they have to be quoted here.
I understand that I could simply wrap the whole line in a test on emptiness.
if [ -z "$empty" ]; then
cat "$afile" "$anotherfile"
else
cat "$afile" "$empty" "$anotherfile"
fi
But one test for each variable would lead to a huge and convoluted decision tree.
Is there a more compact solution to this? Can bash made to omit a quoted empty variable?
You can use an alternate value parameter expansion (${var+altvalue}) to include the quoted variable IF it's set:
cat ${afile+"$afile"} ${empty+"$empty"} ${anotherfile+"$anotherfile"}
Since the double-quotes are in the alternate value string (not around the entire parameter expression), they only take effect if the variable is set. Note that you can use either + (which uses the alternate value if the variable is set) or :+ (which uses the alternate value if the variable is set AND not empty).
A pure bash solution is possible using arrays. While "$empty" will evaluate to an empty argument, "${empty[#]}" will expand to all the array fields, quoted, which are, in this case, none.
$ afile=(/dev/null)
$ unset empty
$ alsoempty=()
$ cat "${afile[#]}" "${empty[#]}" "${alsoempty[#]}"
In situations where arrays are not an option, refer to pasaba por aqui's more versatile answer.
Try with:
printf "%s\n%s\n%s\n" "$afile" "$empty" "$anotherfile" | egrep -v '^$' | tr '\n' '\0' | xargs -0 cat
In the case of a command like cat where you could replace an empty argument with an empty file, you can use the standard shell default replacement syntax:
cat "${file1:-/dev/null}" "${file2:-/dev/null}" "${file3:-/dev/null}"
Alternatively, you could create a concatenated output stream from the arguments which exist, either by piping (as shown below) or through process substitution:
{ [[ -n "$file1" ]] && cat "$file1";
[[ -n "$file2" ]] && cat "$file2";
[[ -n "$file3" ]] && cat "$file3"; } | awk ...
This could be simplified with a utility function:
cat_if_named() { [[ -n "$1" ]] && cat "$1"; }
In the particular case of cat to build up a new file, you could just do a series of appends:
# Start by emptying or creating the output file.
. > output_file
cat_if_named "$file1" >> output_file
cat_if_named "$file2" >> output_file
cat_if_named "$file3" >> output_file
If you need to retain the individual arguments -- for example, if you want to pass the list to grep, which will print the filename along with the matches -- you could build up an array of arguments, choosing only the arguments which exist:
args=()
[[ -n "$file1" ]] && args+=("$file1")
[[ -n "$file2" ]] && args+=("$file2")
[[ -n "$file3" ]] && args+=("$file3")
With bash 4.3 or better, you can use a nameref to make a utility function to do the above, which is almost certainly the most compact and general solution to the problem:
non_empty() {
declare -n _args="$1"
_args=()
shift
for arg; do [[ -n "$arg" ]] && _args+=("$arg"); done
}
eg:
non_empty my_args "$file1" "$file2" "$file3"
grep "$pattern" "${my_args[#]}"
I have the following script which does a "which -a" on a command then a "ls -l" to let me know if it's a link or not .. ie "grep" since I have gnu commands installed (Mac with iTerm).
#!/usr/bin/env bash
which -a $1 | xargs -I{} ls -l "{}" \
| awk '{for (i = 1; i < 9; i++) $i = ""; sub(/^ */, ""); print}'
When I run this from the script "test grep" I receive no output, but when I run it via "bash -x test grep" I receive the following:
bash -x test grep
+ which -a grep
+ xargs '-I{}' ls -l '{}'
+ awk '{for (i = 1; i < 9; i++) $i = ""; sub(/^ */, ""); print}'
/usr/local/bin/grep -> ../Cellar/grep/3.1/bin/grep
/usr/bin/grep
The last 2 lines is what I'm looking to display. Thought this would be easier to do ;-) .. I also tried appending the pipe thinking printf would fix the issue:
| while read path
do
printf "%s\n" "$path"
done
Thanks and .. Is there a better way to get what I need?
The problem is that you named your script test.
If you want to run a command that's not in your PATH, you need to specify the directory it's in, e.g. ./test.
You're not getting an error for trying to run test because there is a built-in bash command called test that is used instead. For extra confusion, the standard test produces no output.
In conclusion:
Use ./ to run scripts in the current directory.
Never call your test programs test.
Thanks for the never naming a script "test" .. old habits are hard to break (I came from a non-unix background.
I ended with the following
for i in $(which -a $1)
do
stat $i | awk NR==1{'$1 = ""; sub(/^ */, ""); print}'
done
or simpler
for i in $(which -a $1)
do
stat -c %N "$i"
done
Consider the following shell function:
cmdsrc() {
local cmd_file cmd_file_realpath
case $(type -t -- "$1") in
file) cmd_file=$(type -P -- "$1")
if [[ -L "$cmd_file" ]]; then
echo "$cmd_file is a symlink" >&2
elif [[ -f "$cmd_file" ]]; then
echo "$cmd_file is a regular file" >&2
else
echo "$cmd_file is not a symlink or a regular file" >&2
fi
cmd_file_realpath=$(readlink -- "$cmd_file") || return
if [[ $cmd_file_realpath != "$cmd_file" ]]; then
echo "...the real location of the executable is $cmd_file_realpath" >&2
fi
;;
*) echo "$1 is not a file at all: $(type -- "$1")" >&2 ;;
esac
}
...used as such:
$ cmdsrc apt
/usr/bin/apt is a symlink
...the real location of the executable is /System/Library/Frameworks/JavaVM.framework/Versions/A/Commands/apt
$ cmdsrc ls
/bin/ls is a regular file
$ cmdsrc alias
alias is not a file at all: alias is a shell builtin
Took some suggestions and came up with the following:
The prt-underline is just a fancy printf function. I decided not to go with readline since the ultimate command resolution may be unfamiliar to me and I only deal with regular files .. so does't handle every situation but in the end gives me the output I was looking for. Thanks for all the help.
llt ()
{
case $(type -t -- "$1") in
function)
prt-underline "Function";
declare -f "$1"
;;
alias)
prt-underline "Alias";
alias "$1" | awk '{sub(/^alias /, ""); print}'
;;
keyword)
prt-underline "Reserved Keyword"
;;
builtin)
prt-underline "Builtin Command"
;;
*)
;;
esac;
which "$1" &> /dev/null;
if [[ $? = 0 ]]; then
prt-underline "File";
for i in $(which -a $1);
do
stat "$i" | awk 'NR==1{sub(/^ File: /, ""); print}';
done;
fi
}