how to read tab separated line into variables - zsh

I wrote this function in zsh
function test() {
test="a\tb\tc"
while IFS=$'\t' read -r user host key; do
echo "user: $user"
echo "host: $host"
echo "key: $key"
done <<< "$test"
}
The output was:
user: a b c
host:
key:
if instead of
... IFS=$'\t' read -r ...
I change it to
... IFS='\t' read -r ...
the output is
user: a
host:
key: b c
Just what is going on?
I would like to read the tab separated line and set my variables accordingly.

Changing the double-quotes to $'...' (single quotes preceded by a $) could rescue for the variable $test:
test=$'a\tb\tc'
Here is the zsh manual for QUOTING (double-quoting and $'...'):
QUOTING
...
A string enclosed between $' and ' is processed the same way as the string arguments of the print builtin
...
Inside double quotes (""), parameter and command substitution occur, and \ quotes the characters \, `, ", $, and the first character of $histchars (default !).
--- zshmisc(1), QUOTING
For example:
"\$" -> $, "\!" -> ! etc.
"\t" -> \t (zsh does not recognize as tab this case), "\a" -> \a etc.
It does not treat the escape sequence \t as tab when it is used inside double quotes, so "a\tb\tc" does not mean "atabbtabc". (But things are a little more complicated: builtin echo recognizes the escape sequence \t.)
(1) ... IFS=$'\t' read -r ... (the original form)
Because expanding "$test" dose not have any tab characters, so read assigns the whole line to $user:
user: a b c
host:
key:
(But echo recognizes the escape sequence \t as the tab.)
(2) ... IFS='\t' read -r ...
Again, expanding "$test" does not have any tab characters, so read separate the field by \ and t according $IFS.
a\t\b\tc splits into a (to $user), \(separator), `` (empty to $host), t(separator), and the rest of the line (b\tc to $key):
user: a
host:
key: b c
(But again, echo recognizes the escape sequence \t as the tab.)
Here is the code changed from test="..." to test=$'...':
function test() {
test=$'a\tb\tc'
while IFS=$'\t' read -r user host key; do
echo "user: $user"
echo "host: $host"
echo "key: $key"
done <<< "$test"
}
test
The output is:
user: a
host: b
key: c
PS: it is worth reading POSIX's Quoting specification, which is simpler than zsh's (https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02)

Related

Loop over environment variables in POSIX sh

I need to loop over environment variables and get their names and values in POSIX sh (not bash). This is what I have so far.
#!/usr/bin/env sh
# Loop over each line from the env command
while read -r line; do
# Get the string before = (the var name)
name="${line%=*}"
eval value="\$$name"
echo "name: ${name}, value: ${value}"
done <<EOF
$(env)
EOF
It works most of the time, except when an environment variable contains a newline. I need it to work in that case.
I am aware of the -0 flag for env that separates variables with nul instead of newlines, but if I use that flag, how do I loop over each variable? Edit: #chepner pointed out that POSIX env doesn't support -0, so that's out.
Any solution that uses portable linux utilities is good as long as it works in POSIX sh.
There is no way to parse the output of env with complete confidence; consider this output:
bar=3
baz=9
I can produce that with two different environments:
$ env -i "bar=3" "baz=9"
bar=3
baz=9
$ env -i "bar=3
> baz=9"
bar=3
baz=9
Is that two environment variables, bar and baz, with simple numeric values, or is it one variable bar with the value $'3\nbaz=9' (to use bash's ANSI quoting style)?
You can safely access the environment with POSIX awk, however, using the ENVIRON array. For example:
awk 'END { for (name in ENVIRON) {
print "Name is "name;
print "Value is "ENVIRON[name];
}
}' < /dev/null
With this command, you can distinguish between the two environments mentioned above.
$ env -i "bar=3" "baz=9" awk 'END { for (name in ENVIRON) { print "Name is "name; print "Value is "ENVIRON[name]; }}' < /dev/null
Name is baz
Value is 9
Name is bar
Value is 3
$ env -i "bar=3
> baz=9" awk 'END { for (name in ENVIRON) { print "Name is "name; print "Value is "ENVIRON[name]; }}' < /dev/null
Name is bar
Value is 3
baz=9
Maybe this would work?
#!/usr/bin/env sh
env | while IFS= read -r line
do
name="${line%%=*}"
indirect_presence="$(eval echo "\${$name+x}")"
[ -z "$name" ] || [ -z "$indirect_presence" ] || echo "name:$name, value:$(eval echo "\$$name")"
done
It is not bullet-proof, as if the value of a variable with a newline happens to have a line beginning that looks like an assignment, it could be somewhat confused.
The expansion uses %% to remove the longest match, so if a line contains several = signs, they should all be removed to leave only the variable name from the beginning of the line.
Here an example based on the awk approach:
#!/bin/sh
for NAME in $(awk "END { for (name in ENVIRON) { print name; }}" < /dev/null)
do
VAL="$(awk "END { printf ENVIRON[\"$NAME\"]; }" < /dev/null)"
echo "$NAME=$VAL"
done

Parameter value truncated from second word onwards in ksh

I am calling a function in commonfuncs to send email as below:
#!/usr/bin/ksh
. commonfuncs
emailsend 'test mail' 'body of the mail' 'abc.efg#domain.com'
the function is as below:
function emailsend
{
esubject=$1
etext=$2
etolist=$3
efromid="from.id#domain.com"
echo $etext >email.txt
cat email.txt | mailx -r $efromid -s $esubject $etolist
}
The email is sent fine. But the subject is send only as test instead of test mail. Tried with double quotes too but no use.
Try to wrap with double quotes parameters of the malix command:
cat email.txt | mailx -r "$efromid" -s "$esubject" "$etolist"
because the space character is a delimiter of the command line arguemnts.

Using ARGV to get user input in awk script

I know ARGV[i] can be used for storing user input. However, I want to use it in awk script and get the ARGV[i] to compare the field from another text file. If the ARGV[i] matches the field or the field contains ARGV[i] which is the user input, then I want to return the other fields of that line.
Let say I have a file, testing.txt
123123123:Walter White:1:2:3:4
123123124:Jesse Pinkman:1:3:4:1
This is my awk script, awkScript.awk
#!/usr/bin/awk -f
BEGIN{FS = ":"; print "ENTER A NAME: "}
{
for (i = 1; i < ARGC; i++)
{
if ($2 ~ /'ARGV[i]'/)
{
print "Member ID: " $1
}
}
}
It just prints ENTER A NAME: when I execute the script file. It doesn't even get the input from the user.
From awk manual
ARGV is an array of command line arguments
That is the list of arguments passed while calling the awk sript.
you may want something like
$echo 'ENTER A NAME'
$read Name
Jesse Pinkman
$awk -v name="$Name" -F: '$2=name{print $1}' filename
123123123
123123124
Here -v option creates a variable named name in awk script and assigns the value of $Name variable from shell to name
$2=name{print $1} the $2=name selects all lines where $2 is name and prints the first column
Not sure what you're thinking about wrt using ARGV[] but here's one way to do what you want in awk:
$ awk 'BEGIN{FS=":"; msg="ENTER A NAME:"; print msg} NR==FNR{a[$2]=$0; next} $0 in a{print a[$0]} {print msg}' file -
ENTER A NAME:
Jesse Pinkman
123123124:Jesse Pinkman:1:3:4:1
ENTER A NAME:
Walter White
123123123:Walter White:1:2:3:4
ENTER A NAME:

substring before and substring after in shell script

I have a string:
//host:/dir1/dir2/dir3/file_name
I want to fetch value of host & directories in different variables in unix script.
Example :
host_name = host
dir_path = /dir1/dir2/dir3
Note - String length & no of directories is not fixed.
Could you please help me to fetch these values from string in unix shell script.
Using bash string operations:
str='//host:/dir1/dir2/dir3/file_name'
host_name=${str%%:*}
host_name=${host_name##*/}
dir_path=${str#*:}
dir_path=${dir_path%/*}
I would do it using regular expressions:
if [[ $path =~ ^//(.*):(.*)/(.*)$ ]]; then
host="${BASH_REMATCH[1]}"
dir_path="${BASH_REMATCH[2]}"
filename="${BASH_REMATCH[3]}"
else
echo "Invalid format" >&2
exit 1
fi
If you are sure that the format will match, you can do simply
[[ $path =~ ^//(.*):(.*)/(.*)$ ]]
host="${BASH_REMATCH[1]}"
dir_path="${BASH_REMATCH[2]}"
filename="${BASH_REMATCH[3]}"
Edit: Since you seem to be using ksh rather than bash (though bash was indicated in the question), the syntax is a bit different:
match=(${path/~(E)^\/\/(.*):(.*)\/(.*)$/\1 \2 \3})
host="${match[0]}"
dir_path="${match[1]}"
filename="${match[2]}"
This will break if there are spaces in the file name, though. In that case, you can use the more cumbersome
host="${path/~(E)^\/\/(.*):(.*)\/(.*)$/\1}"
dir_path="${path/~(E)^\/\/(.*):(.*)\/(.*)$/\2}"
filename="${path/~(E)^\/\/(.*):(.*)\/(.*)$/\3}"
Perhaps there are more elegant ways of doing it in ksh, but I'm not familiar with it.
The shortest way I can think of is to assign two variables in one statement:
$ read host_name dir_path <<< $(echo $string | sed -e 's,^//,,;s,:, ,')
Complete script:
string="//host:/dir1/dir2/dir3/file_name"
read host_name dir_path <<< $(echo $string | sed -e 's,^//,,;s,:, ,')
echo "host_name = " $host_name
echo "dir_path = " $dir_path
Output:
host_name: host
dir_path: /dir1/dir2/dir3/file_name

How can I set a default value when incorrect/invalid input is entered in Unix?

i want to set the value of inputLineNumber to 20. I tried checking if no value is given by user by [[-z "$inputLineNumber"]] and then setting the value by inputLineNumber=20. The code gives this message ./t.sh: [-z: not found as message on the console. How to resolve this? Here's my full script as well.
#!/bin/sh
cat /dev/null>copy.txt
echo "Please enter the sentence you want to search:"
read "inputVar"
echo "Please enter the name of the file in which you want to search:"
read "inputFileName"
echo "Please enter the number of lines you want to copy:"
read "inputLineNumber"
[[-z "$inputLineNumber"]] || inputLineNumber=20
for N in `grep -n $inputVar $inputFileName | cut -d ":" -f1`
do
LIMIT=`expr $N + $inputLineNumber`
sed -n $N,${LIMIT}p $inputFileName >> copy.txt
echo "-----------------------" >> copy.txt
done
cat copy.txt
Changed the script after suggestion from #Kevin. Now the error message ./t.sh: syntax error at line 11: `$' unexpected
#!/bin/sh
truncate copy.txt
echo "Please enter the sentence you want to search:"
read inputVar
echo "Please enter the name of the file in which you want to search:"
read inputFileName
echo Please enter the number of lines you want to copy:
read inputLineNumber
[ -z "$inputLineNumber" ] || inputLineNumber=20
for N in $(grep -n $inputVar $inputFileName | cut -d ":" -f1)
do
LIMIT=$((N+inputLineNumber))
sed -n $N,${LIMIT}p $inputFileName >> copy.txt
echo "-----------------------" >> copy.txt
done
cat copy.txt
Try changing this line from:
[[-z "$inputLineNumber"]] || inputLineNumber=20
To this:
if [[ -z "$inputLineNumber" ]]; then
inputLineNumber=20
fi
Hope this helps.
Where to start...
You are running as /bin/sh but trying to use [[. [[ is a bash command that sh does not recognize. Either change the shebang to /bin/bash (preferred) or use [ instead.
You do not have a space between [[-z. That causes bash to read it as a command named [[-z, which clearly doesn't exist. You need [[ -z $inputLineNumber ]] (note the space at the end too). Quoting within [[ doesn't matter, but if you change to [ (see above), you will need to keep the quotes.
Your code says [[-z but your error says [-z. Pick one.
Use $(...) instead of `...`. The backticks are deprecated, and $() handles quoting appropriately.
You don't need to cat /dev/null >copy.txt, certainly not twice without writing to it in-between. Use truncate copy.txt or just plain >copy.txt.
You seem to have inconsistent quoting. Quote or escape (\x) anything with special characters (~, `, !, #, $, &, *, ^, (), [], \, <, >, ?, ', ", ;) or whitespace and any variable that could have whitespace. You don't need to quote string literals with no special characters (e.g. ":").
Instead of LIMIT=`expr...`, use limit=$((N+inputLineNumber)).

Resources