Don't know where to ask for help so trying here.
I'm creating a bash menu script with some operations and reading lots of bash tutorials but think my brain is starting to melt with all different syntax and ways to do it, can't fully wrap my head around bash/sh. End script will run on OSX for an art team.
What this is
A script to upload/download files with rsync.
The script that will grab latest 'config/menu' from remote server. That file menu.txt will create an menu that lists project and when selected you get the option to download / upload.
Issue
Where I'm stuck is how to handle arrays to menus. Tried 2d arrays with no luck so now it's split into 3 arrays to hold the values I need. However when trying to display the menus I can't get it to work correctly. Look at the bottom for how to test, what is shows and what it should show.
more info: create_menus function
This function parse the menu.txt to build an array that is used when showing a menu,
Project Title5, source1dir, destination1dir
Project Title6, source2dir, destination2dir
Project Title7, source3dir, destination3dir
Instead of gettin selection 1 'Project Title5' a menu will display
1) Project
2) Title5Project
3) Title6Project
4) Title7Quit
Script:
function create_menus() {
#operations for project
MENU_OPERATIONS=(
"Get latest from remote"
"Show changes from remote"
"Send latest from me to remote"
"Show changes from me to remote"
"Return to main menu"
)
#projects to choose from, load from textfile
declare -a t; declare -a s; declare -a d;
while IFS= read -r line; do
IFS=',' read -ra obj <<< "$line"
#TODO 2d array nicer than 3 arrays!
eval "t+=\"${obj[0]}\""
eval "s+=\"${obj[1]}\""
eval "d+=\"${obj[2]}\""
done <$FILE_MENU
t+="Quit" #add quit
MENU_MAIN=($t)
PROJECT_SOURCE=($s)
PROJECT_TARGET=($d)
}
then to show the main menu main_menu "${MENU_MAIN[#]}"
function main_menu
{
#clear
#header
PS3="Select project: "
select option; do # in "$#" is the default
if [ "$REPLY" -eq "$#" ];
then
echo "Exiting..."
break;
elif [ 1 -le "$REPLY" ] && [ "$REPLY" -le $(($#-1)) ];
then
# $REPLY = index
# $option = text
echo "You selected $option which is option $REPLY"
SELETED_PROJECT_TITLE=${MENU_MAIN[$REPLY]}
SELETED_PROJECT_SOURCE=${PROJECT_SOURCE[$REPLY]}
SELETED_PROJECT_TARGET=${PROJECT_TARGET[$REPLY]}
echo "Sel title $SELETED_PROJECT_TITLE"
echo "Sel source $SELETED_PROJECT_SOURCE"
echo "Sel target $SELETED_PROJECT_TARGET"
project_menu "${MENU_OPERATIONS[#]}" "$SELETED_PROJECT_TITLE" "$SELETED_PROJECT_SOURCE" "$SELETED_PROJECT_TARGET"
break;
else
echo "Incorrect Input: Select a number 1-$#"
fi
done
}
Here's full code
https://github.com/fbacker/BigFileProjectsSync/blob/master/app.sh
ADDED MORE DESCRIPTION
To test:
git clone https://github.com/fbacker/BigFileProjectsSync.git
cd BigFileProjectsSync/
./app.sh
What happens:
Shows a menu with options:
1) Project
2) Title5Project
3) Title6Project
4) Title7Quit
Should happen:
Shows a menu with options:
1) Project Title5
2) Project Title6
3) Project Title7
4) Quit
app.sh > function create_menus() > This should create a menu based on the menu.txt file.
menu.txt one line is a project: first value is project name, second value is source directory and third is target directory.
Here's a fixed version of your create_menus() function, which should do the trick:
function create_menus() {
#operations for project
# MENU_OPERATIONS=( # ... OMITTED FOR BREVITY
#projects to choose from
local -a titles sources destinations
local title source destination
while IFS='|' read -r title source destination; do
titles+=( "$title" )
sources+=( "$source" )
destinations+=( "$destination" )
done < <(sed 's/, /|/g' "$FILE_MENU")
# Copy to global arrays
MENU_MAIN+=( "${titles[#]}" )
PROJECT_SOURCE+=( "${sources[#]}" )
PROJECT_TARGET+=( "${destinations[#]}" )
MENU_MAIN+=( "Quit" ) #add quit
}
There were 2 crucial problems with your approach (I'll assume an array variable $arr below):
In order to append a new element to an array, the new element must itself be specified as an array too; i.e., it must be enclosed in (...):
arr+=( "$newElement" ) - OK: value is appended as new element
arr+=$newElement - BROKEN: String-appends the value of $newElement to $arr's first element(!), without adding a new one.
arr=( 1 2 ); arr+=3; declare -p arr -> declare -a arr='([0]="13" [1]="2")'
You can't copy a whole array with arrCopy=( $arr ) - all that does is to create a single-item array containing only $arr's first element. To refer to an array as a whole, you must use "${arr[#]}" (enclosing in "..." ensures that no word-splitting is applied):
arrCopy=( "${arr[#]}" ) - OK
arrCopy=( $arr ) - BROKEN - only copies 1st element
Also note that it's better not to use all-uppercase shell-variable names in order to avoid conflicts with environment variables and special shell variables.
Related
I'm working on a file that contains info such as:
service.txt
service1 - info1
service2 - info2
service3 - info3
...
I added each line of the file to an array. Subsequently with the select command I want to query the elements of the array displaying only the "serviceN" and display the "info" once the element has been selected.
At the moment I can't cut the line to display only the "service"
`
#File example
#service.txt
#service1 - info1
#service2 - info2
#...
#serviceN - infoN
#!/bin/bash
file='service.txt'
line=()
while read -r line; do
line+=($line)
done < $file
echo "Select the service..."
select line in ${line[#]}; do # how can i use "cut" here?
echo $line
break
done
exit 0
You don't need the cut for this particular problem. In pure bash:
#!/bin/bash
readarray -t lines < service.txt
echo "Select the service..." >&2
select service in "${lines[#]%%[[:blank:]]*}"; do
echo "$service"
break
done
For the "${lines[#]%%[[:blank:]]*}", see Shell Parameter Expansion, paragraph starting with ${parameter%word}.
I have a grep output and I'm trying to make an associative array from the output that I get.
Here is my grep output:
"HardwareSerialNumber": "123456789101",
"DeviceId": "devid1234",
"HardwareSerialNumber": "111213141516",
"DeviceId": "devid5678",
I want to use that output to define an associative array, like this:
array[123456789101]=devid1234
array[11213141516]=devid5678
Is that possible? I'm new at making arrays. I hope someone could help me in my problem.
Either pipe your grep output to a helper script with a while loop containing a simple "0/1" toggle to read two lines taking the last field of each to fill your array, e.g.
#!/bin/bash
declare -A array
declare -i n=0
arridx=
while read -r label value; do # read 2 fields
if [ "$n" -eq 0 ]
then
arridx="${value:1}" # strip 1st and lst 2 chars
arridx="${arridx:0:(-2)}" # save in arridx (array index)
((n++)) # increment toggle
else
arrval="${value:1}" # strip 1st and lst 2 chars
arrval="${arrval:0:(-2)}" # save in arrval (array value)
array[$arridx]="$arrval" # assign to associative array
n=0 # zero toggle
fi
done
for i in ${!array[#]}; do # output array
echo "array[$i] ${array[$i]}"
done
Or you can use process substitution containing the grep command within the script to do the same thing, e.g.
done < <( your grep command )
You can also add a check under the else clause that if [[ $label =~ DeviceId ]] to validate you are on the right line and catch any variation in the grep output content.
Example Input
$ cat dat/grepout.txt
"HardwareSerialNumber": "123456789101",
"DeviceId": "devid1234",
"HardwareSerialNumber": "111213141516",
"DeviceId": "devid5678",
Example Use/Output
$ cat dat/grepout.txt | bash parsegrep2array.sh
array[123456789101] devid1234
array[111213141516] devid5678
Parsing out the values is easy, and once you have them you can certainly use those values to build up an array. The trickiest part comes from the fact that you need to combine input from separate lines. Here is one approach; note that this script is verbose on purpose, to show what's going on; once you see what's happening, you can eliminate most of the output:
so.input
"HardwareSerialNumber": "123456789101",
"DeviceId": "devid1234",
"HardwareSerialNumber": "111213141516",
"DeviceId": "devid5678",
so.sh
#!/bin/bash
declare -a hardwareInfo
while [[ 1 ]]; do
# read in two lines of input
# if either line is the last one, we don't have enough input to proceed
read lineA < "${1:-/dev/stdin}"
# if EOF or empty line, exit
if [[ "$lineA" == "" ]]; then break; fi
read lineB < "${1:-/dev/stdin}"
# if EOF or empty line, exit
if [[ "$lineB" == "" ]]; then break; fi
echo "$lineA"
echo "$lineB"
hwsn=$lineA
hwsn=${hwsn//HardwareSerialNumber/}
hwsn=${hwsn//\"/}
hwsn=${hwsn//:/}
hwsn=${hwsn//,/}
echo $hwsn
# some checking could be done here to test that the value is numeric
devid=$lineB
devid=${devid//DeviceId/}
devid=${devid//\"/}
devid=${devid//:/}
devid=${devid//,/}
echo $devid
# some checking could be done here to make sure the value is valid
# populate the array
hardwareInfo[$hwsn]=$devid
done
# spacer, for readability of the output
echo
# display the array; in your script, you would do something different and useful
for key in "${!hardwareInfo[#]}"; do echo $key --- ${hardwareInfo[$key]}; done
cat so.input | ./so.sh
"HardwareSerialNumber": "123456789101",
"DeviceId": "devid1234",
123456789101
devid1234
"HardwareSerialNumber": "111213141516",
"DeviceId": "devid5678",
111213141516
devid5678
111213141516 --- devid5678
123456789101 --- devid1234
I created the input file so.input just for convenience. You would probably pipe your grep output into the bash script, like so:
grep-command | ./so.sh
EDIT #1: There are lots of choices for parsing out the key and value from the strings fed in by grep; the answer from #David C. Rankin shows another way. The best way depends on what you can rely on about the content and structure of the grep output.
There are also several choices for reading two separate lines that are related to each other; David's "toggle" approach is also good, and commonly used; I considered it myself, before going with "read two lines and stop if either is blank".
EDIT #2: I see declare -A in David's answer and in examples on the web; I used declare -a because that's what my version of bash wants (I'm using a Mac). So, just be aware that there can be differences.
What I'm trying to accomplish:
Making an array of active opened windows
uses a combination of wmctrl & xdotool
if the window focus changes then I add that window to the top of the
array
then delete the duplicate window in position [>=1] in the
array.
My problem is that I cannot seem to correctly deal with checking between the wmctrl window id that is open and the xdotool focus window (one is an integer and one is a hexdecimal). My if statement needs work, but I am having a problem finding out how to correctly check my xdotool window data against my array containing wmcrtl window id data.
fid = focus window id
appArray contains wmctrl window id's in an array.
echo -e "-------Current array of active windows-----"
for i in ${appArray[#]}; do echo $i; done
while :
do
#UPDATES CURRENT WINDOW FOCUS
fid=$(xdotool getactivewindow)
#CHECK IF WINDOW OF FOCUS IS AT TOP OF THE ARRAY
if [ $appArray == $(printf 0x0%x $fid) ] ;
#IF IT IS THEN DO NOTHING
then
echo -e "----current window is at top of array, and active------"
else
#IF IT ISNT THEN UPDATE ARRAY
echo -e "\n------Adding new focus window to top of array------"
appArray=($(printf 0x0%x $fid) "${appArray[#]}");
#find location of duplicate if any
newArray=$(echo "${appArray[#]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')
appArray=("${newArray[#]}")
#prints the array of applications
for i in ${appArray[#]}; do echo $i; done
echo "----------------------------------------------------"
fi
done
One way to do this is to shift each window in the array down one position until you find the target.
target="$(printf '0x0%x' "$fid")"
prev="$target"
# Loop over array indices
for i in "${!appArray[#]}"; do
curr="${appArray[$i]}"
appArray[$i]="$prev"
prev="$curr"
if [ "$curr" = "$target" ]; then
break
fi
done
Another way is to loop over the indices to find the index of the target, and use that to update the array.
target="$(printf '0x0%x' "$fid")"
for i in "${!appArray[#]}"; do
if [ "${appArray[$i]}" = "$target" ]; then
unset appArray[$i]
break
fi
done
appArray=("$target" "${appArray[#]}")
this is another question related to importing values from a text file (similar to one of my previous ones), but with added complexity (the more I learn about bash scripting the more challenging it becomes)
The goal: to create an array of Day_.... on each outer loop iteration. I'm trying to do this assuming no knowledge of how many Day_... lists exist in the *.txt file.
The issue: At the moment my inner loop only iterates once (should iterate as the number of elements on Monday. And, also, I'm using my_sub_dom=$( sed 's/=.*//' weekly.txt ) to get the number of lists/arrays in weekly.txt and then filter the ones that contain Day.
Bash script:
#!/bin/bash
source weekly.txt
declare -a my_sub_dom
day=( ${Monday[*]} )
my_sub_dom=$( sed 's/=.*//' weekly.txt ) # to construct a list of the number of of lists in the text file
#echo "${my_sub_dom}"
counter=0
main_counter=0
for i in "${day[#]}"
do
let main_counter=main_counter+1
for j in "${my_sub_dom[#]}"
do
# echo "$j"
if grep -q "Day" "${my_sub_dom}"
then
echo "$j"
sub_array_name="${my_sub_dom[counter]}" # storing the list name
sub_array_content=( ${sub_array_name[*]} )
echo "${sub_array_content}"
else
echo "no"
fi
let counter=counter+1
done
echo "$counter"
counter=0
done
echo "$main_counter"
Text file format:
Day_Mon=( "google" "yahoo" "amazon" )
Day_Tu=( "cnn" "msnbc" "google" )
Day_Wed=( "nytimes" "fidelity" "stackoverflow" )
Monday= ( "one" "two" "three" )
....
Script output:
grep: Day_Mon
Day_Tu
Day_Wed
Monday: No such file or directory
no
1
grep: Day_Mon
Day_Tu
Day_Wed
Monday: No such file or directory
no
1
grep: Day_Mon
Day_Tu
Day_Wed
Monday: No such file or directory
no
1
3
Please let me know if you'd like any other information.... And I really appreciate any input in this matter, I've been trying this for a couple of days now.
Thank you
Given a file weekly.txt containing
Day_Mon=( "google" "yahoo" "amazon" )
Day_Tu=( "cnn" "msnbc" "google" )
Day_Wed=( "nytimes" "fidelity" "stackoverflow" )
Monday=( "one" "two" "three" )
You can loop through each array named Day_-something with
#!/bin/bash
# Set all arrays defined in the file
source weekly.txt
# Get all variables prefixed with "Day_" (bash 4)
for name in "${!Day_#}"
do
echo "The contents of array $name is: "
# Use indirection to expand the array
arrayexpansion="$name[#]"
for value in "${!arrayexpansion}"
do
echo "-- $value"
done
echo
done
This results in:
The contents of array Day_Mon is:
-- google
-- yahoo
-- amazon
The contents of array Day_Tu is:
-- cnn
-- msnbc
-- google
The contents of array Day_Wed is:
-- nytimes
-- fidelity
-- stackoverflow
I have a customized .profile that I use in ksh and below is a function that I created to skip back and forth from directories with overly complicated or long names.
As you can see, the pathnames are stored in an array (BOOKMARKS[]) to keep track of them and reference them at a later time. I want to be able to delete certain values from the array, using a case statement (or OPTARG if necessary) so that I can just type bmk -d # to remove the path at the associated index.
I have fiddled around with array +A and -A, but it just wound up screwing up my array (what is left in the commented out code may not be pretty...I didn't proofread it).
Any suggestions/tips on how to create that functionality? Thanks!
# To bookmark the current directory you are in for easy navigation back and forth from multiple non-aliased directories
# Use like 'bmk' (sets the current directory to a bookmark number) to go back to this directory, i.e. type 'bmk 3' (for the 3rd)
# To find out what directories are linked to which numbers, type 'bmk -l' (lowercase L)
# For every new directory bookmarked, the number will increase so the first time you run 'bmk' it will be 1 then 2,3,4...etc. for every consecutive run therea
fter
# TODO: finish -d (delete bookmark entry) function
make_bookmark()
{
if [[ $# -eq 0 ]]; then
BOOKMARKS[${COUNTER}]=${PWD}
(( COUNTER=COUNTER+1 ))
else
case $1 in
-l) NUM_OF_ELEMENTS=${#BOOKMARKS[*]}
while [[ ${COUNTER} -lt ${NUM_OF_ELEMENTS} ]]
do
(( ACTUAL_NUM=i+1 ))
echo ${ACTUAL_NUM}":"${BOOKMARKS[${i}]}
(( COUNTER=COUNTER+1 ))
done
break ;;
#-d) ACTUAL_NUM=$2
#(( REMOVE=${ACTUAL_NUM}-1 ))
#echo "Removing path ${BOOKMARKS[${REMOVE}]} from 'bmk'..."
#NUM_OF_ELEMENTS=${#BOOKMARKS[*]}
#while [[ ${NUM_OF_ELEMENTS} -gt 0 ]]
#do
#if [[ ${NUM_OF_ELEMENTS} -ne ${ACTUAL_NUM} ]]; then
# TEMP_ARR=$(echo "${BOOKMARKS[*]}")
# (( NUM_OF_ELEMENTS=${NUM_OF_ELEMENTS}-1 ))
#fi
#echo $TEMP_ARR
#done
#break
#for VALUE in ${TEMP_ARR}
#do
# set +A BOOKMARK ${TEMP_ARR}
#done
#echo ${BOOKMARK[*]}
#break ;;
*) (( INDEX=$1-1 ))
cd ${BOOKMARKS[${INDEX}]}
break ;;
esac
fi
}
Arrays in the Korn shell (and Bash and others) are sparse, so if you use unset to delete members of the array, you won't be able to use the size of the array as an index to the last member and other limitations.
Here are some useful snippets (the second for loop is something you might be able to put to use right away):
array=(1 2 3)
unset array[2]
echo ${array[2]} # null
indices=(${!array[#]}) # create an array of the indices of "array"
size=${#indices[#]} # the size of "array" is the number of indices into it
size=${#array[#]} # same
echo ${array[#]: -1} # you can use slices to get array elements, -1 is the last one, etc.
for element in ${array[#]}; do # iterate over the array without an index
for index in ${indices[#]} # iterate over the array WITH an index
do
echo "Index: ${index}, Element: ${array[index]}"
done
for index in ${!array[#]} # iterate over the array WITH an index, directly
That last one can eliminate the need for a counter.
Here are a couple more handy techniques:
array+=("new element") # append a new element without referring to an index
((counter++)) # shorter than ((counter=counter+1)) or ((counter+=1))
if [[ $var == 3 ]] # you can use the more "natural" comparison operators inside double square brackets
while [[ $var < 11 ]] # another example
echo ${array[${index}-1] # math inside an array subscript
This all assumes ksh93, some things may not work in earlier versions.
you can use unset. eg to delete array element 1
unset array[0]
to delete entire array
unset array
A few caveats regarding the previous answer:
First: I see this error all the time. When you provide an array element to "unset", you have to quote it. Consider:
$ echo foo > ./a2
$ ls a[2]
a2
$ a2="Do not delete this"
$ a=(this is not an array)
$ unset -v a[2]
$ echo "a2=${a2-UNSET}, a[]=${a[#]}"
a2=UNSET a[]=this is not an array
What happened? Globbing. You obviously wanted to delete element 2 of a[], but shell syntax being what it is, the shell first checked the current directory for a file that matched the glob pattern "a[2]". If it finds a match, it replaces the glob pattern with that filename, and you wind up making a decision about which variable to delete based on what files exist in your current directory.
This is profoundly stupid. But it's not something anyone has bothered to fix, apparently, and the error turns up in all kinds of documentation and example code from the last 3 decades.
Next is a related problem: it's easy to insert elements in your associative array with any key you like. But it's harder to remove these elements:
typeset -A assoc
key="foo] bar"
assoc[$key]=3 #No problem!
unset -v "assoc[$key]" #Problem!
In bash you can do this:
unset -v "assoc[\$key]"
In Korn Shell, you have to do this:
unset -v "assoc[foo\]\ bar]"
So it gets a bit more complicated in the case where your keys contain syntax characters.