I'm creating a shell script that takes in user input and text's people using the mail function. I am looking to make it more advanced. Right now it just text's one person at a time, I want it to have the ability to text multiple people or even everyone with a user input of 'All'.
#!/bin/sh
# Prefix the numbers with something
number_Joe=8881235555
number_Bob=8881235556
echo "Who do you want to text?:(i.e. Joe, Bob, etc)"
read name
echo "What do you want to say?:"
read quote
# Remove any dangerous characters that the user enters
sanitized=$(printf "%s" "$name" | tr -cd 'a-zA-Z')
#Look up by evaluating e.g. "number=$number_Joe"
eval "number=\$number_$sanitized"
if [ "$number" ]
then
echo "texting $name ($number) with $quote"
printf "%s\n" "$quote" | mailx -s "Text Message via email" "$number#txt.att.net"
else
echo "Unknown user"
exit 1
fi
Also, is there a cleaner method of bringing in a external txt file that houses the numbers instead of the script?
(note: we still have bash <4, thus why I'm not using a associative array)
Here's a rewrite.
Should work fine in bash3.
#!/bin/bash
# Prefix the numbers with something
names=()
names+=(Joe); numberJoe=8881235555
names+=(Bob); numberBob=8881235556
domain=txt.att.example.com
usage () {
echo "usage: $(basename $0) names message ..."
echo "where: names is a comma-separated list of names (no spaces)"
echo
echo "example: $(basename $0) Jim,Fred hello lads, this is my message"
}
while getopts ":hl" opt; do
case $opt in
h) usage; exit ;;
l) IFS=,; echo "known names: ${names[#]}"; exit ;;
esac
done
shift $((OPTIND - 1))
if (( $# < 2 )); then
usage
exit
fi
IFS=, read -ra usernamelist <<<"$1"
shift
message="$*"
# validate names
namelist=()
for name in "${usernamelist[#]}"; do
if [[ " ${names[#]} " == *" $name "* ]]; then
namelist+=("$name")
else
echo "unknown name: $name" >&2
fi
done
if (( ${#namelist[#]} == 0 )); then
echo "no valid names given" >&2
exit 1
fi
# generate the recipient list
echo "texting '$message' to:"
recipients=()
for name in "${namelist[#]}"; do
numvar="number$name"
echo " $name -> ${!numvar}"
recipients+=( "${!numvar}#$domain" )
done
# send it
printf "%s\n" "$message" | mailx -s "Text Message via email" "$(IFS=,; echo "${recipients[*]}")"
Related
The array variable ARRAY_ITEMS adding input from the user.
Here my script.
#!/bin/bash
var1=$(echo -e "Adding $INPUT to array.")
while true; do
printf "\n
A B C\n"
echo -e "This is the array: $ARRAY_ITEMS: "
read -p "Input: " INPUT
case "$INPUT" in
A) echo $var1 ; ARRAY_ITEMS+=$INPUT ;;
B) echo $var1 ; ARRAY_ITEMS+=$INPUT ;;
C) echo $var1 ; ARRAY_ITEMS+=$INPUT ;;
*) echo -e "Whoopsy! Invalid input." ;;
esac
done
The Output looks like this:
A B C This is the array: ABCAAABCAAA:
Input: D Whoopsy! Invalid input.
How does my script have to look like so that the following output appears for an input that has already been entered?
A B C
This is the array: ABC:
Input: A
ERROR. A is already selected.
Use pattern-matching to see if the input is already in the string.
items=
while true; do
printf "\n
A B C\n"
echo "This is the array: $items"
read -p "Input: " input
case "$INPUT" in
A|B|C) if [[ $items = *$input* ]]; then
echo "Error, $input is already in $items"
else
items+=$input
fi ;;
*) echo "Whoopsy! Invalid input." ;;
esac
done
If the input could be something more than a single letter, you'll need to use an array and resign yourself to walking the array, one element at a time, to compare them to the input. Something like
items=()
while true; do
...
found=0
case $input in
A|B|C) for x in "${items[#]}"; do
if [[ $x == $input ]]; then
echo "Error, already used $input"
found=1
break
fi
done
;;
*) echo "Error, invalid input" ;;
esac
(( found )) || items+=("$input")
done
My background is in SQL but I've been learning Bash to create tools to help non-Linux users find what they need from my Linux system - I am pretty green with Bash, I apologize if this looks a bit dumb.
The goal of the script is to essentially display all directories within the current directory to the user, and allow them to input 1-9 to navigate to lower directories.
My sticking point is that I'm trying to use arrays to define potential filepaths, since in practice new directories will be added over time and it is not practical to edit the script each time a filepath is added.
Here's my prototype so far, currently it navigates into Test1, Test2, or Test3 then echos pwd to prove it is there.
#Global Variables
DIR_MAIN='/home/admin/Testhome'
#Potential Filepaths
#/home/admin/Testhome/Test1/Test1-1/
#/home/admin/Testhome/Test1/Test1-2/
#/home/admin/Testhome/Test2/Test2-1/
#/home/admin/Testhome/Test2/Test2-2/
#/home/admin/Testhome/Test3/Test3-1/
#/home/admin/Testhome/Test3/Test3-2/
#Defining Array for first user input
arr=($(ls $DIR_MAIN))
#System to count total number of directories in filepath, then present to user for numbered selection
cnt=0
for i in ${arr[#]}
do
cnt=$(($cnt+1))
echo "$cnt) $i"
done
read -p "Select a folder from the list: " answer
case $answer in
1)
cd $DIR_MAIN/${arr[0]}
echo "Welcome to $(pwd)"
;;
2)
cd $DIR_MAIN/${arr[1]}
echo "Welcome to $(pwd)"
;;
3)
cd $DIR_MAIN/${arr[2]}
echo "Welcome to $(pwd)"
;;
esac
I've tried the following, but it doesn't like the syntax (to someone experienced I'm sure these case statements look like a grenade went off in vim).
I'm beginning to wonder if the SELECT CASE road I'm going down is appropriate, or if there is an entirely better way.
#User Input Start
echo "What is the secret number?"
while :
do
read STRING1
case $STRING1 in
1)
echo "Enter the number matching the directory you want and I will go there"
echo "1 - ${arr[0]}"
echo "2 - ${arr[1]}"
echo "3 - ${arr[2]}"
read STRING2
case $STRING2 in
1)
cd $DIR_MAIN/${arr[0]}
echo "Welcome to" $(pwd)
2)
cd $DIR_MAIN/${arr[1]}
echo "Welcome to" $(pwd)
3)
cd $DIR_MAIN/${arr[2]}
echo "Welcome to" $(pwd)
*)
echo "Thats not an option and you know it"
*)
echo "1 is the secret number, enter 1 or nothing will happen"
;;
esac
#break needs to be down here somewhere
done
Ultimately I know I'll need to variabilize a local array once I'm in Test2 for example (since in practice, this could descend as far as /Test2/Test2-9 and there would be tons of redundant code to account for this manually).
For now, I'm just looking for the best way to present the /Test2-1 and /Test2-2 filepaths to the user and allow them to make that selection after navigating to /Test2/
This might do what you wanted.
#!/usr/bin/env bash
shopt -s nullglob
n=1
for i in /home/admin/Testhome/Test[0-9]*/*; do
printf '%d) %s\n' "$n" "$i"
array[n]="$i"
((n++))
done
(( ${#array[*]} )) || {
printf 'It looks like there is/are no directory listed!\n' >&2
printf 'Please check if the directories in question exists!\n' >&2
return 1
}
dir_pattern_indices=$(IFS='|'; printf '%s' "#(${!array[*]})")
printf '\n'
read -rp "Select a folder from the list: " answer
if [[ -z $answer ]]; then
printf 'Please select a number and try again!' >&2
exit 1
elif [[ $answer != $dir_pattern_indices ]]; then
printf 'Invalid option %s\n' "$answer" >&2
exit 1
fi
for j in "${!array[#]}"; do
if [[ $answer == "$j" ]]; then
cd "${array[j]}" || exit
printf 'Welcome to %s\n' "$(pwd)"
break
fi
done
The script needs to be sourced e.g.
source ./myscript
because of the cd command. See Why can't I change directory using a script.
Using a function instead of a script.
Let's just name the function list_dir
list_dir() {
shopt -s nullglob
declare -a array
local answer dir_pattern_indices i j n
n=1
for i in /home/admin/Testhome/Test[0-9]*/*; do
printf '%d) %s\n' "$n" "$i"
array[n]="$i"
((n++))
done
(( ${#array[*]} )) || {
printf 'It looks like there is/are no directory listed!\n' >&2
printf 'Please check if the directories in question exists!\n' >&2
return 1
}
dir_pattern_indices=$(IFS='|'; printf '%s' "#(${!array[*]})")
printf '\n'
read -rp "Select a folder from the list: " answer
if [[ -z $answer ]]; then
printf 'Please select a number and try again!' >&2
return 1
elif [[ $answer != $dir_pattern_indices ]]; then
printf 'Invalid option %s\n' "$answer" >&2
return 1
fi
for j in "${!array[#]}"; do
if [[ $answer == "$j" ]]; then
cd "${array[j]}" || return
printf 'Welcome to %s\n' "$(pwd)"
break
fi
done
}
All of the array names and variables are declared local to the function in order not to pollute the interactive/enviromental shell variables.
Put that somewhere in your shellrc file, like say in ~/.bashrc then source it again after you have edited that shellrc file.
source ~/.bashrc
Then just call the function name.
list_dir
I took what #Jetchisel wrote and ran with it - I see they updated their code as well.
Between that code and what I hacked together piggybacking off what he wrote, I'm hoping future viewers will have what they need to solve this problem!
My code includes a generic logging function (can write to a log file if you define it and uncomment those logging lines, for a script this size I just use it to output debugging messages), everything below is the sequence used.
As he mentioned the "0" element needs to be removed from the array for this to behave as expected, as a quick hack I ended up assigning array element 0 as null and adding logic to ignore null.
This will also pull pretty much anything in the filepath, not just directories, so more tweaking may be required for future uses but this serves the role I need it for!
Thank you again #Jetchisel !
#hopt -s nullglob
DIR_MAIN='/home/admin/Testhome'
Dir_Cur="$DIR_MAIN"
LOG_LEVEL=1
array=(NULL $(ls $DIR_MAIN))
########FUNCTION LIST#########
####Generic Logging Function
Log_Message()
{
local logLevel=$1
local logMessage=$2
local logDebug=$3
local dt=$(date "+%Y-%m-%d %T")
##Check log level
if [ "$logLevel" == 5 ]
then local logLabel='INFO'
elif [ "$logLevel" == 1 ]
then local logLabel='DEBUG'
elif [ "$logLevel" == 2 ]
then local logLabel='INFO'
elif [ "$logLevel" == 3 ]
then local logLabel='WARN'
elif [ "$logLevel" == 4 ]
then local logLabel='ERROR'
fi
##Check conf log level
if [ "$LOG_LEVEL" == 1 ]
then #echo "$dt [$logLabel] $logMessage" >> $LOG_FILE ##Log Message
echo "$dt [$logLabel] $logMessage" ##Echo Message to Terminal
##Check if Debug Empty
if [ "$logDebug" != "" ]
then #echo "$dt [DEBUG] $logDebug" >> $LOG_FILE ##Extra Debug Info
echo "$dt [DEBUG] $logDebug" ##Extra Debug Info
fi
elif [ "$logLevel" -ge "$LOG_LEVEL" ]
then #echo "$dt [$logLabel] $logMessage" >> "$LOG_FILE" ##Log Message
echo "$dt [$logLabel] $logMessage"
fi
}
####
####Function_One
##Removes 0 position in array by marking it null, generates [1-X] list with further filepaths
Function_One()
{
Log_Message "1" "entered Function_One"
local local_array=("$#")
Log_Message "1" "${local_array[*]}"
n=1
for i in "${local_array[#]}"; do
if [ "$i" != "NULL" ]
then
printf '%d) %s\n' "$n" "$i"
array[n]="$i"
((n++))
fi
done
printf '\n'
read -rp "Select a folder from the list: " answer
for j in "${!local_array[#]}"; do
if [[ $answer == "$j" ]]; then
cd "$Dir_Cur/${local_array[j]}" || exit
printf 'Welcome to %s\n' "$(pwd)"
break
fi
done
}
####
########FUNCTION LIST END#########
########MAIN SEQUENCE########
echo "Script start"
Function_One "${array[#]}"
Dir_Cur="$(pwd)"
array2=(NULL $(ls $Dir_Cur))
Function_One "${array2[#]}"
Dir_Cur="$(pwd)"
$Dir_Cur/test_success.sh
echo "Script end"
########
I was fiddling around with bash last month and am trying to create a script.
I want the script to search through the folders for files with some kind of extension defined by the argument -e. The folders are defined without -option. The output is 2 columns where in the first it prints the found files, and in the second the respective folders.
Is this the most efficient and/or flexible way to go?
I also can't manage to let the -l command work. Any idea what's wrong? When I enter -name \${CHAR}*, it simply doesn't work. Also, how can I make it recognize a range being used? With an if-function looking for the "-" character or something?
I think I managed to mount a block device, but how can I add the path as a parameter so it can be used as a folder? Setting a number as a var doesn't work, it tells me it doesn't recognize the command.
For some reason the 'no recursion' tag works, but the 'no numbers' doesn't. I have no idea why this would be different.
When using the 'no recursion' (nn) and 'no numbers' (nr) tags I use a long tag --tag for the arguments. Is it possible to use only 1 -tag? This is possible with get opts, but then I can't manage to use the other tags after the get opts has been used. Someone a solution?
Finally, is it possible, when finding 2 files with the same file name, instead of printing the file twice, can it just show the file once. But for every file with the same name keep a white space, so it can still show all the folders in the second column?
#!/bin/bash
#FUNCTIONS
#Error
#Also written to stderr
err() {
echo 1>&2;
echo "Error, not enough arguments" 1>&2;
echo "Usage: $0 [-e <file extension>] [<folder>]";
echo "Please enter the argument -e and at least 1 folder.";
echo "More: Please chek Help by using -h or --help.";
echo 1>&2;
exit
}
#Help
help() {
echo
echo "--- Help ---"
echo
echo "This script will look for file extentions in 1 or more directories. The output shows the found files with the according folder where it's located."
echo
echo "Argument -e <ext> is required."
echo "Other arguments the to-look-trough folders."
echo
echo "These are also usable options:"
echo "-h or --help shows this."
echo "-l <character> looks for files starting with the character."
echo "-l <character1>-<character2> does the same, but looks trough a range of characters."
echo "-b <block-device> mounts a partition to /mnt and let it search through."
echo "--nn (no numbers) makes sure there are no numbers in the file name."
echo "--nr (no recursion) doesn't look trough subdirectories."
echo "-r of –-err <file> writes the errors (f.e. corrupted directory) to <file>."
echo "-s <word> searches the word through the files and only shows the files having that word."
echo
exit
}
#VARS
#execute getopt
OPTS=$(getopt -o e:hl:b:r:s: -l "help,nn,nr,err" -n "FileExtensionScript" -- "$#");
#Bad arguments
if [ $? -ne 0 ];
then
err;
exit
fi
#Rearrange arguments
eval set -- "$OPTS";
#echo "AFTER SET -- \$OPTS: $#";
while true; do
case "$1" in
-e)
shift;
if [ -n "$1" ]; then
EXT=$1;
shift;
fi
;;
-h|--help)
shift;
help;
;;
-l)
shift;
if [ -n "$1" ]; then
CHAR=$1;
shift;
fi
;;
-b)
shift;
if [ -n "$1" ]; then
sudo mkdir /mnt/$1;
sudo echo -e "/dev/$1 /mnt/$1 vfat defaults 0 0 " >> /etc/fstab;
sudo mount -a;
999=/mnt/$1;
shift;
fi
;;
--nn)
shift;
NONUM=" ! -name '*[0-9]*'";
;;
--nr)
shift;
NOREC="-maxdepth 1";
;;
-f|--err)
shift;
if [ -n "$1" ]; then
ERROR="| 2>filename | tee -a $1";
shift;
fi
;;
-s)
shift;
if [ -n "$1" ]; then
SEARCH="-name '*$1*'";
shift;
fi
;;
--)
shift;
break;
;;
esac
done
#No folder or arguments given
if [ $# -lt 1 ];
then
err;
exit
fi
#Debug
echo "Folder argumenten: $#" >2;
echo \# $# >2;
#Create arrays with found files and according folders
FILES=( $(find $# $NOREC $SEARCH $NONUM -name \*.${EXT} $ERROR | rev | cut -d/ -f 1 | rev) )
FOLDERS=( $(find $# $NOREC $SEARCH $NONUM -name \*.${EXT} $ERROR | rev | cut -d/ -f 1 --complement | rev) )
#Show arrays in 2 columns
for ((i = 0; i <= ${#FILES[#]}; i++));
do
printf '%s %s\n' "${FILES[i]}" "${FOLDERS[i]}"
done | column -t | sort -k1 #Make columns cleaner + sort on filename
I am not native English speaker and am hoping to get some tips to finish my script :) Thanks in advance!
#!/bin/bash
menu=0
dir=""
size=""
name=""
modif=""
while [ $menu -ne 6 ]
do
echo "1. Name: $name"
echo "2. Directory $dir"
echo "3. Last modified: $modif"
echo "4. Minimum size: $size"
echo "5. Search"
echo "6. End"
read menu
case "$menu" in
"1") read name ;;
"2") read dir ;;
"3") read modif ;;
"4") read size;;
"5") if [ -z $name ]
then
option1=""
else
option1="-name $name"
fi
find "$option1";;
"6") ;;
*) echo "Wrong number!"
esac
done
I need to make a script which will be working like find command, but I've encountered a problem. When user doesnt input for example name the find command should have name option disabled.
I came up with something like this above, but it doestn work when the variable name doesnt contain anything (user didnt input anything). I keep getting error:
find: paths must precede expression: BASH ....
I honestly have no idea how to make it work instead of having 2^4 IF's and executing find with only those specified options that user has choosen.
Is there any way to make it easier?
edit: Now i modified it to:
#!/bin/bash
menu=0
dir=""
modif=""
while [ $menu -ne 6 ]
do
echo "1. Name: $name"
echo "2. Directory $dir"
echo "3. Last modified: $modif"
echo "4. Minimum size: $size"
echo "5. Search"
echo "6. End"
read menu
case "$menu" in
"1")
read name
;;
"2") read dir ;;
"3") read modif ;;
"4")
read size
;;
"5")
if [ -z $name ]
then
unset tablica[0]
else
tablica[0]="-name $name"
fi
find "${tablica[#]}"
;;
"6") ;;
*) echo "Wrong number!"
esac
done
But another error:
find: unknown predicate `-name example.txt'
You can build an array of parameters:
myarray=()
if [[ $size ]]
then
myarray+=(-size +"$size")
fi
if [[ $name ]]
then
myarray+=(-name "$name")
fi
...
find "$dir" "${myarray[#]}"
The benefit of this approach is that it also handles spaces correctly, and doesn't allow code injection.
I have a bash script that will take one argument: a product ID. The product ID can be in one of two formats: all numbers, or a mix of letters, numbers, and underscores. Depending on which type of ID is entered, the script will handle it in a slightly different way.
Right now, I'm using getopts with one flag for each subtype to distinguish between which type of product ID I'm going to be using in the script. For example:
./myscript -n 1034596
or
./myscript -v AB_ABCD_12345
With a simplified version of the script looking like this:
#!/bin/bash
while getopts ":n:v:" opt; do
case $opt in
n)
echo "This is a numbers only ID." >&2
;;
v)
echo "This is a letters, numbers, underscore ID" >&2
;;
esac
done
Since the formats are static, that is, the first type of ID will never be anything but numbers, is there any way to automatically distinguish between the two types of IDs and handle them appropriately without the need for the -n or -v flag? So, I could just enter ./myscript 1034596 and the script will know that since the argument contains nothing but numbers it should process it a specific way.
#!/bin/bash
shopt -s extglob
case "$1" in
+([0-9]) ) echo "This is a numbers only ID." >&2
;;
+([a-zA-Z0-9_]) ) echo "This is a letters, numbers, underscore ID" >&2
;;
*) echo "Unrecognized Product ID" >&2
esac
Pure Bash 3.X
#!/bin/bash
if [[ "$1" =~ ^[0-9]+$ ]]; then
echo "This is a numbers only ID." >&2
else
echo "This is a letters, numbers, underscore ID" >&2
fi
Output
$ ./argtype.sh 1034596
This is a numbers only ID.
$ ./argtype.sh AB_ABCD_12345
This is a letters, numbers, underscore ID
Try this code:
#!/bin/bash
if [ $1 -eq $1 2> /dev/null ]; then
echo number
else
echo not number
fi
The output on your given input is:
brent#battlecruiser:~$ ./test2 1034596
number
brent#battlecruiser:~$ ./test2 AB_ABCD_12345
not number
There is a perhaps slightly more readable way with bash, but this can be done perfectly reasonably in pure portable sh.
unset LC_COLLATE
case $1 in
*[!0-9A-Z_a-z]*) echo 1>&2 "Invalid product ID"; exit 2;;
*[!0-9]*) echo "alphanumeric product ID";;
*) echo "numeric product ID";;
esac
Take a look at accepted the answer to this question. Around line 8 is a regex technique that you can adapt.
#!/bin/sh
arg="$1"
output=`echo "$1" | grep -o "[0-9]\+"`
if [ "$output" == "$arg" ]; then
echo "Numbers only"
else
echo "Mixed"
fi
In Bash 3.2 or greater:
pattern1='^[0-9]+$'
pattern2='^[0-9a-zA-Z_]+$'
if [[ $1 =~ $pattern1 ]]
then
echo "argument consists only of digits and is validated"
else
echo "argument contains other characters"
if [[ $1 =~ $pattern2 ]]
then
echo "argument is validated"
else
echo "argument contains invalid characters"
fi
fi
You could use grep to validate if the string passed in is a number:
#!/bin/bash
echo $1 | grep -q "^[0-9]*$"
if [ $? ]; then
echo "Number"
else
echo "Not a number"
fi
With comments updates:
#!/bin/bash
if grep -q "^[0-9]*$" <<< "$1"; then
echo "Number"
else
echo "Not a number"
fi