Bash: tr command with variables representing chars - bash

I want to use the tr command to map chars to new chars, for example:
echo "hello" | tr '[a-z]' '[b-za-b]' Will output: ifmmp
(where each letter in the lower-case alphabet is shifted over one to the right)
See below the mapping to new chars for '[b-za-b]':
[a b c d e f g h i j k l m n o p q r s t u v w x y z] will map to:
[b c d e f g h i j k l m n o p q r s t u v w x y z a]
However, when I want it to rotate multiple times, how can I use a variable to control the rotate-value for the tr command?
Eg: for a shift of 1:
echo "hello" | tr '[a-z]' '[b-za-b]' without variables and:
echo "hello" | tr '[a-z]' '[(a+$var)-za-(a+$var)]' where $var=1
here I have: (a+$var)-z representing the same as b-z and
....................a-(a+$var) representing the same as a-b
I have tried converting the ascii value to a char to use within the tr command but I don't think that is allowed.
My problem is that bash is not interpreting:
(a+$var) as the char b when $var=1
(a+$var) as the char c when $var=2
... etc.
How can I tell bash to interpret these equations as chars for the tr command
EDIT
I tried doing it with an array but it's not working:
chars=( {a..z} )
var=2
echo "hello" | tr '[a-z]' '[(${chars[var]}-z)(a-${chars[var]})]'
I used: (${chars[var]}-z) to represent b-z where var=1
Because ${chars[1]} is b but this is not working. Am I missing something?

What you are trying to do cannot be done using tr which does not handle your requirement. Moreover when you meant to modify and use variables to add to glob patterns in bash which is something you cannot possibly do.
There is a neat little trick you can do with bash arrays!. The tr command can take expanded array sequence over the plain glob patterns also. First define a source array as
source=()
Now add its contents as a list of character ranges from a-z using brace expansion as
source=({a..z})
and now for the transliterating array, from the source array, construct it as follows by using the indices to print the array elements
trans=()
Using a trick to get the array elements from the last with syntax ${array[#]: (-num)} will get you the total length - num of the elements. So building the array first as
var=2
trans+=( "${source[#]:(-(26-$var))}" )
and now to build the second part of the array, use another trick ${array[#]:0:num} to get the first num number of elemtents.
trans+=( "${source[#]:0:$(( $var + 1 ))}" )
So what we have done now is for a given value of var=2, we built the trans array as
echo "${trans[#]}"
c d e f g h i j k l m n o p q r s t u v w x y z a b c
Now you can just use it easily in the tr command
echo "hello" | tr "${source[*]}" "${trans[*]}"
jgnnq
You can just put it all in function and print its value as
transChar() {
local source
local trans
local result
source=({a..z})
trans=()
var="$2"
input="$1"
trans+=( "${source[#]:(-(26-$var))}" )
trans+=( "${source[#]:0:$(( $var + 1 ))}" )
result=$( echo "$input" | tr "${source[*]}" "${trans[*]}" )
echo "$result"
}
Some of the tests
transChar "hello" 1
ifmmp
transChar "hello" 2
jgnnq
transChar "hello" 3
khoor

rot-random:
# generate alphabet as arr:
arr=( {1..26} )
i=$(($RANDOM%24+1))
# left and right
l=$(echo ${arr[$i]})
r=$(echo ${arr[$i+1]})
# reusing arr for testing:
echo ${arr[#]} | tr "a-z" "$r-za-$l"
echo "secret:" | tr "a-z" "$r-za-$l" ; echo $l $r $i
amkzmb:
h i 7

You could use octal \XXX character codes for characters to do what you intend. Using the octal codes you could do any arithmetic manipulations to numbers and then convert them to character codes
# rotr x
#
# if 0 <= x <= 25 rotr x outputs a set specification
# that could be used as an argument to tr command
# otherwise it outputs 'a-z'
function rotr(){
i = $(( 97 + $1 ))
if [ $i -lt 97 ] ; then
translation='a-z'
elif [ $i -eq 97 ] ; then
translation='\141-\172' # 141 is the octal code for "a"
# 172 is the octal code for "z"
elif [ $i -eq 98 ] ; then
translation='\142-\172\141'
elif [ $i -lt 122 ] ; then # $i is between 99 and 121 ("c" and "y")
ii=$(echo "obase=8 ; $i" | bc)
jj=$(echo "obase=8 ; $(( $i - 1 ))" | bc)
translation=\\$ii'-\172\141-'\\$jj
elif [ $i -eq 122 ] ; then
translation='\172\141-\171'
else # $i > 122
tranlation='a-z'
fi
echo $translation
}
Now you could use this as follows
echo hello | tr 'a-z' $(rotr 7)
prints
olssv

Related

Bash script failed to Compare SHA-512 salted hashes. Gets wrong output

I was able to fix the error not being able to use * as part of the LIST. However i was not able to compare and get the desired output of the password.
#!/bin/bash
LIST=(W 2 v '*' %)
encr="Cd9AjUI4nGglIcP3MByrZUnu.hHBJc7.eR0o/v0A1gu0/6ztFfBxeJgKTzpgoCLptJS2NnliZLZjO40LUseED/"
salt="8899Uidd"
for i in "${LIST[#]}"
do
for j in "${LIST[#]}"
do
for k in "${LIST[#]}"
do
for l in "${LIST[#]}"
do
for a in "${LIST[#]}"
do
echo -n "$i$j$k$l$a "
test="mkpasswd -m SHA-512 $i$j$k$l$a -s $salt | cut -d"$" -f4"
if [ "$test" == "$encr" ] ; then
echo " Password is: $i$j$k$l$a"
exit
fi
done
done
done
done
done
#error comparing
The Output should be Password is: W2v*%, but it came out as %%%%%
With a little bit of tweaking here and there. Here we have a fixed one :D
Hope its working for you guys as well.
#!/bin/bash
LIST1=(A B C D E F G H I J K L M N O P Q R S T U V W X Y Z)
LIST2=(0 1 2 3 4 5 6 7 8 9)
LIST3=(a b c d e f g h i j k l m n o p q r s t u v w x y z)
LIST4=("~" "#" "#" "$" "%" "^" "*" "_" "+" "-" "=" "{" "}" "[" "]" "?" ";" ":")
LIST5=("~" "#" "#" "$" "%" "^" "*" "_" "+" "-" "=" "{" "}" "[" "]" "?" ";" ":")
encr="Cd9AjUI4nGglIcP3MByrZUnu.hHBJc7.eR0o/v0A1gu0/6ztFfBxeJgKTzpgoCLptJS2NnliZLZjO40LUseED/"
salt="8899Uidd"
for i in "${LIST1[#]}"
do
for j in "${LIST2[#]}"
do
for k in "${LIST3[#]}"
do
for l in "${LIST4[#]}"
do
for a in "${LIST5[#]}"
do
echo -n "$i$j$k$l$a"
test="$(mkpasswd -m SHA-512 "$i$j$k$l$a" -s $salt | cut -d"$" -f4)"
if [ "$test" == "$encr" ] ; then
echo " Password is: $i$j$k$l$a"
exit
fi
done
done
done
done
done
P.S: I overdid with the list. You can combine the list into one.

Concatenating digits from a string in sh

Assuming that I have a string like this one:
string="1 0 . # 1 1 ? 2 2 4"
Is it possible to concatenate digits that are next to each other?
So that string be like: 10 . # 11 ? 224 ?
I found only basic things how to distinguish integers from other characters and how to "connect" them. But I have no idea how to iterate properly.
num=""
for char in $string; do
if [ $char -eq $char 2>/dev/null ] ; then
num=$num$char
Here's an almost pure-shell implementation -- transforming the string into a character per line and using a BashFAQ #1 while read loop.
string="1 0 . # 1 1 ? 2 2 4"
output=''
# replace spaces with newlines for easier handling
string=$(printf '%s\n' "$string" | tr ' ' '\n')
last_was_number=0
printf '%s\n' "$string" | {
while read -r char; do
if [ "$char" -eq "$char" ] 2>/dev/null; then # it's a number
if [ "$last_was_number" -eq "1" ]; then
output="$output$char"
last_was_number=1
continue
fi
last_was_number=1
else
last_was_number=0
fi
output="$output $char"
done
printf '%s\n' "$output"
}
To complement Charles Duffy's helpful, POSIX-compliant sh solution with a more concise perl alternative:
Note: perl is not part of POSIX, but it is preinstalled on most modern Unix-like platforms.
$ printf '%s\n' "1 0 . # 1 1 ? 2 2 4" | perl -pe 's/\d( \d)+/$& =~ s| ||gr/eg'
10 . # 11 ? 224
The outer substitution, s/\d( \d)+/.../eg, globally (g) finds runs of at least 2 adjacent digits (\d( \d)+), and replaces each run with the result of the expression (e) specified as the replacement string (represented as ... here).
The expression in the inner substitution, $& =~ s| ||gr, whose result is used as the replacement string, removes all spaces from each run of adjacent digits:
$& represents what the outer regex matched - the run of adjacent digits.
=~ applies the s call on the RHS to the LHS, i.e., $& (without this, the s call would implicitly apply to the entire input string, $_).
s| ||gr replaces all (g) instances of <space> from the value of the value of $& and returns (r) the result, effectively removing all spaces.
Note that | is used arbitrarily as the delimiter character for the s call, so as to avoid a clash with the customary / delimiter used by the outer s call.
POSIX compliant one-liner with sed:
string="1 0 . # 1 1 ? 2 2 4"
printf '%s\n' "$string" | sed -e ':b' -e ' s/\([0-9]\) \([0-9]\)/\1\2/g; tb'
It just iteratively removes the any space between two digits until there aren't any more, resulting in:
10 . # 11 ? 224
Here is my solution:
string="1 0 . # 1 1 ? 2 2 4"
array=(${string/// })
arraylength=${#array[#]}
pattern="[0-9]"
i=0
while true; do
str=""
start=$i
if [ $i -eq $arraylength ]; then
break;
fi
for (( j=$start; j<${arraylength}; j++ )) do
curr=${array[$j]}
i=$((i + 1))
if [[ $curr =~ $pattern ]]; then
str="$str$curr"
else
break
fi
done
echo $str
done

Find specific cell from space-separated CSV file in bash

I have a question related to bash operating on comma-separated value files (.csv) saved with spaces as the selected separator.
As an example I'll put small .csv file here:
A B C D
1 a b c d
2 e f g h
3 i j k l
4 m n o p
And here is my question: Is that possible in bash to read specific value for example from cell C4?
Tried to find any topic with similar problem but cannot it.
Thanks in advance!
Can do this very easily in awk :
example.sh
#!/bin/bash
awk '
{
if(NR==5){ print $4; }
}
' < "$1"
output
$ ./example.sh input.txt
o
details
NR filters the line number
$4 refers to the fourth field ( under C column )
The tricky part is converting "C4" into column 3, row 4. Here's one way with bash:
#!/bin/bash
cell=$1
file=$2
colnum() {
local -u col=$1
local val=0 i
for ((i=0; i<${#col}; i++)); do
# ascii value of a char, ref: http://stackoverflow.com/q/890262/7552
printf -v ascii "%d" "'${col:i:1}"
val=$(( val*26 + ascii - 64 ))
done
echo "$val"
}
if ! [[ $cell =~ ^([A-Za-z]+)([0-9]+)$ ]]; then
echo "error: invalid cell '$cell'"
exit 1
fi
col=$(colnum "${BASH_REMATCH[1]}")
row=${BASH_REMATCH[2]}
lineno=0
while read -ra fields; do
if (( ++lineno == row )); then
echo "${fields[col-1]}"
fi
done < "$file"

Assigning a value from csv file to variable in bash [duplicate]

I have a question related to bash operating on comma-separated value files (.csv) saved with spaces as the selected separator.
As an example I'll put small .csv file here:
A B C D
1 a b c d
2 e f g h
3 i j k l
4 m n o p
And here is my question: Is that possible in bash to read specific value for example from cell C4?
Tried to find any topic with similar problem but cannot it.
Thanks in advance!
Can do this very easily in awk :
example.sh
#!/bin/bash
awk '
{
if(NR==5){ print $4; }
}
' < "$1"
output
$ ./example.sh input.txt
o
details
NR filters the line number
$4 refers to the fourth field ( under C column )
The tricky part is converting "C4" into column 3, row 4. Here's one way with bash:
#!/bin/bash
cell=$1
file=$2
colnum() {
local -u col=$1
local val=0 i
for ((i=0; i<${#col}; i++)); do
# ascii value of a char, ref: http://stackoverflow.com/q/890262/7552
printf -v ascii "%d" "'${col:i:1}"
val=$(( val*26 + ascii - 64 ))
done
echo "$val"
}
if ! [[ $cell =~ ^([A-Za-z]+)([0-9]+)$ ]]; then
echo "error: invalid cell '$cell'"
exit 1
fi
col=$(colnum "${BASH_REMATCH[1]}")
row=${BASH_REMATCH[2]}
lineno=0
while read -ra fields; do
if (( ++lineno == row )); then
echo "${fields[col-1]}"
fi
done < "$file"

Escaped asterisk (*) in list of characters outputs file in folder

At the very beginning of my Bash script I'm passing a set of characters like this:
#!/bin/bash
set0="0 1 2 3 4 5 6 7 8 9"
set1="° § + \" ç % & / ( ) = ? ' ^ ! £ $ - _ ; , : . *"
The script combines every single character with the others in order to get all possible combination with length of n.
As you can see in set1 above I have to escape the quote " character for example. My problem now is, that I have to escape the asterisk * as well. But I couldn't figure out how.
This didn't work so far:
#!/bin/bash
set1="° § + \" ç % & / ( ) = ? ' ^ ! £ $ - _ ; , : . \*"
It echoes \*. If I don't escape the asterisk * with a backslash \ the script echoes all the files in current directory all time.
Do you know how I can escape the asterisk * successfully?
This is the whole script:
8 chars0="a b c d e f g h i j k l m n o p q r s t u v w x y z"
9 chars1="A B C D E F G H I J K L M N O P Q R S T U V W X Y Z"
10 chars2="0 1 2 3 4 5 6 7 8 9"
11 chars3="° § + \" ç % & / ( ) = ? ' ^ ! £ $ - _ ; , : . *"
12
13 function increment {
14 INPUT=$1
15 [[ ${#INPUT} -ge $minlen ]] && echo $1
16 if [[ ${#INPUT} -lt $maxlen ]]; then
17 for c in $chars; do
18 increment $1$c
19 done
20 fi
21 }
22
23 function showUsage {
24 echo "$0 MAXLEN [MINLEN] [a [A [n [p]]]]"
25 echo
26 echo " MAXLEN integer Maximum lenght, or specific lengt if MINLEN is omitted"
27 echo " MINLEN integer Minimum lenght, equals MAXLEN if omitted"
28 echo ""
29 echo " characters:"
30 echo " a lower case a-z"
31 echo " A Uppercase A-Z"
32 echo " n numeric 0-9"
33 echo " p punctuation . ?"
34 }
35
36
37
38
39 ## argument handler
40 [[ "$1" = "" ]] && showUsage && exit
41 maxlen=$1
42 [[ $2 -eq 0 ]] && minlen=$maxlen || minlen=$2
43
44
45 for arg in "$#"; do
46 case $arg in
47 a) chars="$chars $chars0" ;;
48 A) chars="$chars $chars1" ;;
49 n) chars="$chars $chars2" ;;
50 p) chars="$chars $chars3" ;;
51 esac;
52 done
53
54 #fallback
55 [[ "$chars" = "" ]] && chars="$chars0"
56
57
58
59 ## kickof
60
61 for i in $chars; do
62 increment $i
63 done
Thank you for all your suggestions. The one that helped me fixing this was
set -f
It's a quick fix of course. But it works well for me.
The best way to do this is maybe to use:
set -o noglob
set="* + ?"
for i in $set; do echo $i; done
*
+
?
The problem when you use the variable. noglob will prevent all of the file glob characters from expanding.
You can't safely loop on a list of words in a string like this, without having nasty globbing effects. In your case, it boils down to this:
a=*
for i in $a; do
echo "$a"
done
You'll see that the files in your current directory are printed, because the (unquoted! horror!) $a in the for statement expands to * which will glob to the list of files in current dir.
You must fix this by using another design. You're lucky, Bash supports arrays very well, so redesign your code to use arrays. For example:
chars=( ° § + \" ç % \& / '(' ')' = \? \' ^ ! £ $ - _ \; , : . \* )
(this defines the array chars) and to loop through that:
for i in "${chars[#]}"; do
echo "$i"
done
(observe the quotes).
Below is a rewriting of your code to show you how to use arrays (and I've fixed/improved some other minor stuff too):
#!/bin/bash
chars_alpha=( {a..z} )
chars_ALPHA=( {A..Z} )
chars_digit=( {0..9} )
chars_punct=( '°' '§' '+' '"' 'ç' '%' '&' '/' '(' ')' '=' '?' "'" '^' '!' '£' '$' '-' '_' ';' ',' ':' '.' '*' )
increment() {
local input=$1
(( ${#input} >= minlen )) && printf '%s\n' "$input"
if (( ${#input} < maxlen )); then
for c in "${chars[#]}"; do
increment "$input$c"
done
fi
}
showUsage() {
cat <<EOF
$0 MAXLEN [MINLEN] [a] [A] [n] [p]
MAXLEN integer Maximum lenght, or specific lengt if MINLEN is omitted
MINLEN integer Minimum lenght, equals MAXLEN if omitted
characters:
a lower case a-z
A Uppercase A-Z
n numeric 0-9
p punctuation . ? etc.
EOF
}
## argument handler
(($#)) || { showUsage; exit 1; }
[[ $1 = +([[:digit:]]) ]] || { showUsage; exit 1; }
((maxlen=10#$1))
shift
# Check that maxlen is >0 or exit gracefully
((maxlen>0)) || exit 0
if [[ $1 = +([[:digit:]]) ]]; then
((minlen=10#$1))
shift
else
((minlen=maxlen))
fi
# Check that minlen<=maxlen otherwise swap them
if ((minlen>maxlen)); then
((temp=minlen,minlen=maxlen,maxlen=temp))
fi
chars=()
while (($#)); do
case $1 in
(a) chars+=( "${chars_alpha[#]}" ); chars_alpha=() ;;
(A) chars+=( "${chars_ALPHA[#]}" ); chars_ALPHA=() ;;
(n) chars+=( "${chars_digit[#]}" ); chars_digit=() ;;
(p) chars+=( "${chars_punct[#]}" ); chars_punct=() ;;
(*) printf >&2 'Ignored arguments %s\n' "$1" ;;
esac
shift
done
#fallback
(( ${#chars[#]} )) || chars=( "${chars_alpha[#]}" )
#kick off!
increment
One of the fixes includes using printf instead of echo. With echo, your code is broken, since the strings:
-e
-n
-E
would never be printed! see help echo to understand why. Try it yourself too:
echo -e
echo -n
echo -E
don't output anything!
Use single quotes to wrap your string instead of double quotes. You won't have to have to escape any characters beside the single quote.

Resources