Why will it not echo "$d" anwser - bash

I need the script to output the result, but echo "$d" does not output anything. I made the ciphertext.gz earlier in the script and the $fil is ciphertext.gz. Bash script:
echo "Fil: ciphertext.gz"
a="ABCDEFGHIJKLMNOPQRSTUVXYZ"
[[ "${*/-d/}" != "" ]] &&
echo "Usage: $0 [-d]" && exit 1
m=${1:+-}
m=-
t=$fil
printf "Nøgle 'eks. ABCDE': "
read -r k
k=$(echo "$k" | tr [a-vx-z] [A-VX-Z] )
printf "\n"
for ((i=0;i<${#t};i++)); do
p1=${a%%${t:$i:1}*}
p2=${a%%${k:$((i%${#k})):1}*}
d="${d}${a:$(((${#p1}${m:-+}${#p2})%${#a})):1}"
done
echo "$d"

Extended comments, (not really an answer, since it's unclear what the code should do):
This is wrong:
m=${1:+-}
m=-
...since it has the same effect as:
m=-
This reads one line from standard input:
read -r k
...which, unless ciphertext is only one line long, probably
defeats the purpose of the next eight lines of code. Even if
standard input was unzip < ciphertext.gz |, it would only decode
the first line of ciphertext.
Wrap the for in an appropriate while read k loop.

Related

Asking for user input in a loop until match found in array of command line arguments

Sometimes I need to find a specific serial in a box with many items, so I wrote a simple Bash script that allows me to use a barcode scanner to scan hundreds of barcodes until a match is found, at which point the screen flashes (so I can see it from the corner of my eyes while looking at the box).
The script works great, but it only checks against one specific serial number provided by the user. Here's the code:
#!/bin/bash
INPUT=''
SCAN=''
SN=''
I='0'
clear
printf "Enter serial\n"
read INPUT
SN=`printf "${INPUT}" | tr '[:lower:]' '[:upper:]'`
# Keep comparing scans to needed serial until a match is found
while [[ "${SCAN}" != *"${SN}"* ]];
do
clear
printf "Looking for [ "${SN}" ]\n"
printf "Please scan barcode\n"
read INPUT
SCAN=`printf "${INPUT}" | tr '[:lower:]' '[:upper:]'`
done
# Flash screen when match is found
while [[ "${I}" -lt 3 ]];
do
printf '\e[?5h' && sleep 0.3
printf '\e[?5l' && sleep 0.3
I=$[${I}+1]
done
printf "FOUND\n"
Today I spent hours trying to implement a way to pass multiple possible serial numbers as command line arguments, but I can't seem to get it working. I would like to be able to pass a small, manageable number of possible serials, like this:
$ ./script.sh sn1 sn2 sn3 sn4 sn5
And for the script continue asking for input until I come across the item I am looking for.
I've studied the handling of shell arguments, but I can't seem to "massage" the above while loop to get it to check if the scanned serial exists in the array (created from the command line arguments passed):
#!/bin/bash
snList=( "$#" )
INPUT=''
SCAN=''
SN=''
I='0'
clear
#displaying "things" so I can see what each variable contains (debugging)
printf "$#\n"
printf "$0\n"
printf "$*\n"
printf "$0\n"
printf "$1\n"
printf "$2\n"
printf "$3\n"
printf "snList: $snList\n"
printf "snList[#]: ${snList[#]}\n"
printf "snList[*]: ${snList[*]}\n"
# Keep comparing scans to needed serial until a match is found
while [[ ! " ${snList[*]} " =~ "${SCAN}" ]];
do
clear
printf "Looking for [ "$*" ]\n"
printf "Please scan barcode\n"
read INPUT
SCAN=`printf "${INPUT}" | tr '[:lower:]' '[:upper:]'`
done
I've tried using ${snList[#]} in the loop as well, same result, it behaves like a match was found immediately, without even asking for a scan (indicating that the content of the while loop is not being executed).
Any help will be immensely appreciated, I think I am close, but I can't figure out what I am doing wrong.
Thanks in advance!
Something like this maybe?
#!/usr/bin/env bash
to_compare_input=("$#")
exglob_pattern_input=$(IFS='|'; printf '%s' "#(${to_compare_input[*]})")
until [[ $user_input == $exglob_pattern_input ]]; do
read -r user_input
done
Run the script with the the following arguments.
bash -x ./myscript foo bar baz more
Output
+ to_compare_input=("$#")
++ IFS='|'
++ printf %s '#(foo|bar|baz|more)'
+ exglob_pattern_input='#(foo|bar|baz|more)'
+ [[ '' == #(foo|bar|baz|more) ]]
+ read -r user_input
papa
+ [[ papa == #(foo|bar|baz|more) ]]
+ read -r user_input
mama
+ [[ mama == #(foo|bar|baz|more) ]]
+ read -r user_input
baz
+ [[ baz == #(foo|bar|baz|more) ]]
The first user input is empty since the builtinread has not been executed to ask for the user's input. As shown at the debug message.
+ [[ '' == #(foo|bar|baz|more) ]]
The second (assuming the user has entered papa) is papa
The third (assuming the user has entered mama) is mama
The last is baz which breaks out of off the until loop, because it belongs to the $extglob_pattern_input, which is an extglob feature.
A regex is also an alternative using the =~ operator.
#!/usr/bin/env bash
to_compare_input=("$#")
regex_pattern_input=$(IFS='|'; printf '%s' "^(${to_compare_input[*]})$")
until [[ $user_input =~ $regex_pattern_input ]]; do
read -r user_input
done
Run the script same as before.
Using two loops which was suggested in the comments section.
#!/usr/bin/env bash
to_compare_input=("$#")
inarray() {
local n=$1 h
shift
for h; do
[[ $n == "$h" ]] && return
done
return 1
}
until inarray "$user_input" "${to_compare_input[#]}"; do
read -r user_input
done
As for the tr if your version of bash supports the ^^ and ,, for uppercase and lowercase parameter expansion. use ${user_input^^}
until [[ ${user_input^^} == $exglob_pattern_input ]]; do
until [[ ${user_input^^} =~ $regex_pattern_input ]]; do
until inarray "${user_input^^}" "${to_compare_input[#]}"; do
Assuming no spaces in the bar code texts. You can do something like this
while read -r INPUT
do
#Append spaces to prevent substring matching
if [[ $(echo " $# " | grep -i " ${INPUT} " | wc -l) -eq 1 ]]
then
break
fi
done

Reformatting a csv file, script is confused by ' %." '

I'm using bash on cygwin.
I have to take a .csv file that is a subset of a much larger set of settings and shuffle the new csv settings (same keys, different values) into the 1000-plus-line original, making a new .json file.
I have put together a script to automate this. The first step in the process is to "clean up" the csv file by extracting lines that start with "mme " and "sms ". Everything else is to pass through cleanly to the "clean" .csv file.
This routine is as follows:
# clean up the settings, throwing out mme and sms entries
cat extract.csv | while read -r LINE; do
if [[ $LINE == "mme "* ]]
then
printf "$LINE\n" >> mme_settings.csv
elif [[ $LINE == "sms "* ]]
then
printf "$LINE\n" >> sms_settings.csv
else
printf "$LINE\n" >> extract_clean.csv
fi
done
My problem is that this thing stubs its toe on the following string at the end of one entry: 100%." When it's done with the line, it simply elides the %." and the new-line marker following it, and smears the two lines together:
... 100next.entry.keyname...
I would love to reach in and simply manually delimit the % sign, but it's not a realistic option for my use case. Clearly I'm missing something. My suspicion is that I am in some wise abusing cat or read in the first line.
If there is some place I should have looked to find the answer before bugging you all, by all means point me in that direction and I'll sod off.
Syntax for printf is :
printf format [argument]...
In [ printf ] format string, anything followed by % is a format specifier as described in the link above. What you would like to do is :
while read -r line; do # Replaced LINE with line, full uppercase variable are reserved for the syste,
if [[ "$line" = "mme "* ]] # Here* would glob for anything that comes next
then
printf "%s\n" $line >> mme_settings.csv
elif [[ "$line" = "sms "* ]]
then
printf "%s\n" $line >> sms_settings.csv
else
printf "%s\n" $line >> extract_clean.csv
fi
done<extract.csv # Avoided the useless use of cat
As pointed out, your problem is expanding a parameter containing a formatting instruction in the formatting argument of printf, which can be solved by using echo instead or moving the parameter to be expanded out of the formatting string, as demonstrated in other answers.
I recommend not looping over your whole file with Bash in the first place, as it's notoriously slow; you're extracting lines starting with certain patterns, which is a job at which grep excels:
grep '^mme ' extract.csv > mme_settings.csv
grep '^sms ' extract.csv > sms_settings.csv
grep -v '^mme \|^sms ' extract.csv > extract_clean.csv
The third command uses the -v option (extract lines that don't match) and alternation to exclude lines both starting with mme and sms.

Bash problems with string comparison

I have a problem with writing bash script. The problem is in comparison of strings. When I launch it, there's no errors. However in result, it is always changing the variable client.
So if for an example we have two lines in file
apple A
orange D
and if I give the who=A I expect to see in result apple, or if at D - orange
But no matter of what I choose A or D it is always giving me the result - orange
No matter of the strings, it always change the variable client, like ignoring the comparison. Please help.
while read line
do
IFS=" "
set -- $line
echo $2" "$who":"$1
if [[ "$2"="$who" ]]
then
echo "change"
client=$1
fi
done < $file
echo $client
So now I changed the code as in one of the comment below, but now the caparison always false therefore the variable client is always empty
while read -r line
do
#IFS=" "
#set -- $line
#echo $2" "$who":"$1
#if [[ "$2" = "$who" ]]
a="${line% *}"
l="${line#* }"
if [[ "$l" == "$who" ]]
then
echo "hi"
client="$a"
fi
done < $file
If you have data in a file with each line like apple D and you want to read the file and separate then items, the parameter expansion/substring extraction is the correct way to process the line. For example (note $who is taken from your problem statement):
while read -r line
do
fruit="${line% *}" # remove from end to space
letter="${line#* }" # remove from start to space
if [[ "$letter" == "$who" ]]
then
echo "change"
client="$fruit"
fi
done < $file
Short Example
Here is a quick example of splitting the words with parameter expansion/substring extraction:
#!/bin/bash
while read -r line
do
fruit="${line% *}"
letter="${line#* }"
echo "fruit: $fruit letter: $letter"
done
exit 0
input
$ cat dat/apple.txt
Apple A
Orange D
output
$ bash apple.sh <dat/apple.txt
fruit: Apple letter: A
fruit: Orange letter: D
Change if [[ "$2"="$who" ]] to
if [[ "$2" = "$who" ]]
spaces around =
Example (for clarification):
who=A
while read line
do
IFS=" "
set -- $line
echo $2" "$who":"$1
if [[ "$2" = "$who" ]]
then
echo "change"
client=$1
fi
done < file #this is the file I used for testing
echo $client
Output:
A A:apple
change
D A:orange
apple
For who=D:
A D:apple
D D:orange
change
orange
You do need spaces around that = operator.
However, I think you're facing yet another issue as you're trying to change the value of the client variable from inside the while loop (which executes in a subshell). I don't think that will work; see this quesion for details.

How to write a tail script without the tail command

How would you achieve this in bash. It's a question I got asked in an interview and I could think of answers in high level languages but not in shell.
As I understand it, the real implementation of tail seeks to the end of the file and then reads backwards.
The main idea is to keep a fixed-size buffer and to remember the last lines. Here's a quick way to do a tail using the shell:
#!/bin/bash
SIZE=5
idx=0
while read line
do
arr[$idx]=$line
idx=$(( ( idx + 1 ) % SIZE ))
done < text
for ((i=0; i<SIZE; i++))
do
echo ${arr[$idx]}
idx=$(( ( idx + 1 ) % SIZE ))
done
If all not-tail commands are allowed, why not be whimsical?
#!/bin/sh
[ -r "$1" ] && exec < "$1"
tac | head | tac
Use wc -l to count the number of lines in the file. Subtract the number of lines you want from this, and add 1, to get the starting line number. Then use this with sed or awk to start printing the file from that line number, e.g.
sed -n "$start,\$p"
There's this:
#!/bin/bash
readarray file
lines=$(( ${#file[#]} - 1 ))
for (( line=$(($lines-$1)), i=${1:-$lines}; (( line < $lines && i > 0 )); line++, i-- )); do
echo -ne "${file[$line]}"
done
Based on this answer: https://stackoverflow.com/a/8020488/851273
You pass in the number of lines at the end of the file you want to see then send the file via stdin, puts the entire file into an array, and only prints the last # lines of the array.
The only way I can think of in “pure” shell is to do a while read linewise on the whole file into an array variable with indexing modulo n, where n is the number of tail lines (default 10) — i.e. a circular buffer, then iterate over the circular buffer from where you left off when the while read ends. It's not efficient or elegant, in any sense, but it'll work and avoids reading the whole file into memory. For example:
#!/bin/bash
incmod() {
let i=$1+1
n=$2
if [ $i -ge $2 ]; then
echo 0
else
echo $i
fi
}
n=10
i=0
buffer=
while read line; do
buffer[$i]=$line
i=$(incmod $i $n)
done < $1
j=$i
echo ${buffer[$i]}
i=$(incmod $i $n)
while [ $i -ne $j ]; do
echo ${buffer[$i]}
i=$(incmod $i $n)
done
This script somehow imitates tail:
#!/bin/bash
shopt -s extglob
LENGTH=10
while [[ $# -gt 0 ]]; do
case "$1" in
--)
FILES+=("${#:2}")
break
;;
-+([0-9]))
LENGTH=${1#-}
;;
-n)
if [[ $2 != +([0-9]) ]]; then
echo "Invalid argument to '-n': $1"
exit 1
fi
LENGTH=$2
shift
;;
-*)
echo "Unknown option: $1"
exit 1
;;
*)
FILES+=("$1")
;;
esac
shift
done
PRINTHEADER=false
case "${#FILES[#]}" in
0)
FILES=("/dev/stdin")
;;
1)
;;
*)
PRINTHEADER=true
;;
esac
IFS=
for I in "${!FILES[#]}"; do
F=${FILES[I]}
if [[ $PRINTHEADER == true ]]; then
[[ I -gt 0 ]] && echo
echo "==> $F <=="
fi
if [[ LENGTH -gt 0 ]]; then
LINES=()
COUNT=0
while read -r LINE; do
LINES[COUNT++ % LENGTH]=$LINE
done < "$F"
for (( I = COUNT >= LENGTH ? LENGTH : COUNT; I; --I )); do
echo "${LINES[--COUNT % LENGTH]}"
done
fi
done
Example run:
> bash script.sh -n 12 <(yes | sed 20q) <(yes | sed 5q)
==> /dev/fd/63 <==
y
y
y
y
y
y
y
y
y
y
y
y
==> /dev/fd/62 <==
y
y
y
y
y
> bash script.sh -4 <(yes | sed 200q)
y
y
y
y
Here's the answer I would give if I were actually asked this question in an interview:
What environment is this where I have bash but not tail? Early boot scripts, maybe? Can we get busybox in there so we can use the full complement of shell utilities? Or maybe we should see if we can squeeze a stripped-down Perl interpreter in, even without most of the modules that would make life a whole lot easier. You know dash is much smaller than bash and perfectly good for scripting use, right? That might also help. If none of that is an option, we should check how much space a statically linked C mini-tail would need, I bet I can fit it in the same number of disk blocks as the shell script you want.
If that doesn't convince the interviewer that it's a silly question, then I go on to observe that I don't believe in using bash extensions, because the only good reason to write anything complicated in shell script nowadays is if total portability is an overriding concern. By avoiding anything that isn't portable even in one-offs, I don't develop bad habits, and I don't get tempted to do something in shell when it would be better done in a real programming language.
Now the thing is, in truly portable shell, arrays may not be available. (I don't actually know whether the POSIX shell spec has arrays, but there certainly are legacy-Unix shells that don't have them.) So, if you have to emulate tail using only shell builtins and it's got to work everywhere, this is the best you can do, and yes, it's hideous, because you're writing in the wrong language:
#! /bin/sh
a=""
b=""
c=""
d=""
e=""
f=""
while read x; do
a="$b"
b="$c"
c="$d"
d="$e"
e="$f"
f="$x"
done
printf '%s\n' "$a"
printf '%s\n' "$b"
printf '%s\n' "$c"
printf '%s\n' "$d"
printf '%s\n' "$e"
printf '%s\n' "$f"
Adjust the number of variables to match the number of lines you want to print.
The battle-scarred will note that printf is not 100% available either. Unfortunately, if all you have is echo, you are up a creek: some versions of echo cannot print the literal string "-n", and others cannot print the literal string "\n", and even figuring out which one you have is a bit of a pain, particularly as, if you don't have printf (which is in POSIX), you probably don't have user-defined functions either.
(N.B. The code in this answer, sans rationale, was originally posted by user 'Nirk' but then deleted under downvote pressure from people whom I shall charitably assume were not aware that some shells do not have arrays.)

Counting newline characters in bash shell script

I cannot get this script to work at all. I am just trying to count the number of lines in a file WITHOUT using wc. here is what I have so far
FILE=file.txt
lines=0
while IFS= read -n1 char
do
if [ "$char" == "\n" ]
then
lines=$((lines+1))
fi
done < $FILE
this is just a small part of a bigger script that should count total words, characters and lines in a file. I cannot figure any of it out though. Please help
The problem is the if-statement conditional is never true.. Its as if the program cannot detect what a '\n' is.
declare -i lines=0 words=0 chars=0
while IFS= read -r line; do
((lines++))
array=($line) # don't quote the var to enable word splitting
((words += ${#array[#]}))
((chars += ${#line} + 1)) # add 1 for the newline
done < "$filename"
echo "$lines $words $chars $filename"
You have two problems there. They are fixed in the following:
#!/bin/bash
file=file.txt
lines=0
while IFS= read -rN1 char; do
if [[ "$char" == $'\n' ]]; then
((++lines))
fi
done < "$file"
One problem was the $'\n' in the test, the other one, more subtle, was that you need to use the -N switch, not the -n one in read (help read for more information). Oh, and you also want to use the -r option (check with and without, when you have backslashes in your file).
Minor things I changed: Use more robust [[...]], used lower case variable names (it's considered bad practice to have upper case variable names). Used arithmetic ((++lines)) instead of the silly lines=$((lines+1)).

Resources