Bash associative arrays error - bash

I seem to have this problem. This code breaks at line 119 in my script with bash associative arrays. I am sorry for the comments but I am kind to new to bash scripting. This is the code:
#!/bin/bash
# Aliases file
# Command usage: cpRecent/mvRecent -d {dirFrom},{dirTo} -n {numberofFiles} -e {editTheNames}
# Error codes
NO_ARGS="You need to pass in an argument"
INVALID_OPTION="Invaild option:"
NO_DIRECTORY="No directory found"
# Return values
fullpath=
directories=
numfiles=
interactive=
typeset -a files
typeset -A filelist
# Advise that you use relative paths
__returnFullPath(){
local npath
if [[ -d $1 ]]; then
cd "$(dirname $1)"
npath="$PWD/$(basename $1)"
npath="$npath/" #Add a slash
npath="${npath%.*}" #Delete .
fi
fullpath=${npath:=""}
}
__usage(){
wall <<End-Of-Message
________________________________________________
<cpRecent/mvRecent> -d "<d1>,<d2>" -n <num> [-i]
-d First flag: Takes two arguments
-n Second flag: Takes one argument
-i Takes no arguments. Interactive mode
d1 Directory we are reading from
d2 Directory we are writing to
num Number of files
________________________________________________
End-Of-Message
}
__processOptions(){
while getopts ":d:n:i" opt; do
case $opt in
d ) IFS=',' read -r -a directories <<< "$OPTARG";;
n ) numfiles=$OPTARG;;
i ) interactive=1;;
\? ) echo "$INVALID_OPTION -$OPTARG" >&2 ; return 1;;
: ) echo "$NO_ARGS"; __usage; return 1;;
* ) __usage; return 1;;
esac
done
}
__getRecentFiles(){
# Check some conditions
(( ${#directories[#]} != 2 )) && echo "$INVALID_OPTION Number of directories must be 2" && return 2
#echo ${directories[0]} ${directories[1]}
# Get the full paths of the directories to be read from/written to
__returnFullPath "${directories[0]}"
directories[0]="$fullpath"
__returnFullPath "${directories[1]}"
directories[1]="$fullpath"
if [[ -z ${directories[0]} || -z ${directories[1]} ]]; then
echo $NO_DIRECTORY
return 3
fi
[[ numfiles != *[!0-9]* ]] && echo "$INVALID_OPTION Number of files cannot be a string" && return 4
#numfiles=$(($numfiles + 0))
(( $numfiles == 0 )) && echo "$INVALID_OPTION Number of files cannot be zero" && return 4
local num="-"$numfiles""
# Get the requested files in directory(skips directories)
if [[ -n "$(ls -t ${directories[0]} | head $num)" ]]; then
# For some reason using local -a or declare -a does not seem to split the string into two
local tempfiles=($(ls -t ${directories[0]} | head $num))
#IFS=' ' read -r -a tempfiles <<< "$string"
#echo ${tempfiles[#]}
for index in "${!tempfiles[#]}"; do
echo $index ${tempfiles[index]}
[[ -f "${directories[0]}${tempfiles[index]}" ]] && files+=("${tempfiles[index]}")
done
fi
}
####################################
# The problem is this piece of code
__processLines(){
local name
local answer
local dirFrom
local dirTo
if [[ -n $interactive ]]; then
for (( i=0; i< ${#files[#]}; i++ )); do
name=${files[i]}
read -n 1 -p "Old name: $name. Do you wish to change the name(y/n)?" answer
[[ answer="y" ]] && read -p "Enter new name:" name
dirFrom="${directories[0]}${files[i]}"
dirTo="${directories[1]}$name"
fileslist["$dirFrom"]="$dirTo"
done
else
for line in $files; do
dirFrom="${directories[0]}$line"
echo $dirFrom # => /home/reclusiarch/Documents/test
dirTo="${directories[1]}$line"
echo $dirTo # => /home/reclusiarch/test
fileslist["$dirFrom"]="$dirTo" # This is the offending line
done
fi
}
###########################################################
cpRecent(){
__processOptions $*
__getRecentFiles
__processLines
for line in "${!filelist[#]}"; do
cp $line ${filelist[$line]}
done
echo "You have copied ${#fileList[#]} files"
unset files
unset filelist
return
}
mvRecent(){
__processOptions $*
__getRecentFiles
__processLines
for line in "${!filelist[#]}"; do
mv $line ${filelist[$line]}
done
echo "You have copied ${#fileList[#]} files"
unset files
unset filelist
return
}
cpRecent "$*"
I have tried a lot of things. To run the script,
$ bash -x ./testing.sh -d "Documents,." -n 2
But nothing seems to work:
The error is this(when using bash -x):
./testing.sh: line 119: /home/reclusiarch/Documents/test: syntax error: operand expected (error token is "/home/reclusiarch/Documents/test")
If I run that section on the command line, it works:
$ typeset -A filelist
$ filelist["/home/reclusiarch/Documents/test"]=/home/reclusiarch/test
$ echo ${filelist["/home/reclusiarch/Documents/test"]}
/home/reclusiarch/test
Thanks for your help!!
Edit: I intially pared down the script to the piece of offending code but that might make it not run. Again, if you want to test it, you could run the bash command given. (The script ideally would reside in the user's $HOME directory).
Edit: Solved (Charles Duffy solved it) It was a simple mistake of forgetting which name was which.

Your declaration is:
typeset -A filelist
However, your usage is:
fileslist["$dirFrom"]="$dirTo"
fileslist is not filelist.

Related

Need alternative to readarray/mapfile for script on older version of Bash

The script is:
#!/bin/bash
# Dynamic Menu Function
createmenu () {
select selected_option; do # in "$#" is the default
if [ 1 -le "$REPLY" ] && [ "$REPLY" -le $(($#)) ]; then
break;
else
echo "Please make a vaild selection (1-$#)."
fi
done
}
declare -a drives=();
# Load Menu by Line of Returned Command
mapfile -t drives < <(lsblk --nodeps -o name,serial,size | grep "sd");
# Display Menu and Prompt for Input
echo "Available Drives (Please select one):";
createmenu "${drives[#]}"
# Split Selected Option into Array and Display
drive=($(echo "${selected_option}"));
echo "Drive Id: ${drive[0]}";
echo "Serial Number: ${drive[1]}";
The older system doesn't have mapfile or readarray so I need to convert that line to some alternative that can read each line of the lsblk output into an array.
The line in question that creates the array is:
mapfile -t drives < <(lsblk --nodeps -o name,serial,size | grep "sd");
You can loop over your input and append to the array:
$ while IFS= read -r line; do arr+=("$line"); done < <(printf '%d\n' {0..5})
$ declare -p arr
declare -a arr='([0]="0" [1]="1" [2]="2" [3]="3" [4]="4" [5]="5")'
Or, for your specific case:
while IFS= read -r line; do
drives+=("$line")
done < <(lsblk --nodeps -o name,serial,size | grep "sd")
See the BashFAQ/001 for an excellent explanation why IFS= read -r is a good idea: it makes sure that whitespace is conserved and backslash sequences not interpreted.
Here's the solution I came up with a while back. This is better because it provides a substitute function for older versions of Bash that don't support mapfile/readarray.
if ! type -t readarray >/dev/null; then
# Very minimal readarray implementation using read. Does NOT work with lines that contain double-quotes due to eval()
readarray() {
local cmd opt t v=MAPFILE
while [ -n "$1" ]; do
case "$1" in
-h|--help) echo "minimal substitute readarray for older bash"; exit; ;;
-r) shift; opt="$opt -r"; ;;
-t) shift; t=1; ;;
-u)
shift;
if [ -n "$1" ]; then
opt="$opt -u $1";
shift
fi
;;
*)
if [[ "$1" =~ ^[A-Za-z_]+$ ]]; then
v="$1"
shift
else
echo -en "${C_BOLD}${C_RED}Error: ${C_RESET}Unknown option: '$1'\n" 1>&2
exit
fi
;;
esac
done
cmd="read $opt"
eval "$v=()"
while IFS= eval "$cmd line"; do
line=$(echo "$line" | sed -e "s#\([\"\`]\)#\\\\\1#g" )
eval "${v}+=(\"$line\")"
done
}
fi
You don't have to change your code one bit. It just works!
readarray -t services -u < <(lsblk --nodeps -o name,serial,size | grep "sd")
For those playing along at home, this one aims to provide a mapfile that's feature-compliant with Bash 5, but still runs as far back as Bash 3.x:
#!/usr/bin/env bash
if ! (enable | grep -q 'enable mapfile'); then
function mapfile() {
local DELIM="${DELIM-$'\n'}"; opt_d() { DELIM="$1"; }
local COUNT="${COUNT-"0"}"; opt_n() { COUNT="$1"; }
local ORIGIN="${ORIGIN-"0"}"; opt_O() { ORIGIN="$1"; }
local SKIP="${SKIP-"0"}"; opt_s() { SKIP="$1"; }
local STRIP="${STRIP-"0"}"; opt_t() { STRIP=1; }
local FROM_FD="${FROM_FD-"0"}"; opt_u() { FROM_FD="$1"; }
local CALLBACK="${CALLBACK-}"; opt_C() { CALLBACK="$1"; }
local QUANTUM="${QUANTUM-"5000"}"; opt_c() { QUANTUM="$1"; }
unset OPTIND; local extra_args=()
while getopts ":d:n:O:s:tu:C:c:" opt; do
case "$opt" in
:) echo "${FUNCNAME[0]}: option '-$OPTARG' requires an argument" >&2; exit 1 ;;
\?) echo "${FUNCNAME[0]}: ignoring unknown argument '-$OPTARG'" >&2 ;;
?) "opt_${opt}" "$OPTARG" ;;
esac
done
shift "$((OPTIND - 1))"; set -- ${extra_args[#]+"${extra_args[#]}"} "$#"
local var="${1:-MAPFILE}"
### Bash 3.x doesn't have `declare -g` for "global" scope...
eval "$(printf "%q" "$var")=()" 2>/dev/null || { echo "${FUNCNAME[0]}: '$var': not a valid identifier" >&2; exit 1; }
local __skip="${SKIP:-0}" __counter="${ORIGIN:-0}" __count="${COUNT:-0}" __read="0"
### `while read; do...` has trouble when there's no final newline,
### and we use `$REPLY` rather than providing a variable to preserve
### leading/trailing whitespace...
while true; do
if read -d "$DELIM" -r <&"$FROM_FD"
then [[ ${STRIP:-0} -ge 1 ]] || REPLY="$REPLY$DELIM"
elif [[ -z $REPLY ]]; then break
fi
(( __skip-- <= 0 )) || continue
(( COUNT <= 0 || __count-- > 0 )) || break
### Yes, eval'ing untrusted content is insecure, but `mapfile` allows it...
if [[ -n $CALLBACK ]] && (( QUANTUM > 0 && ++__read % QUANTUM == 0 ))
then eval "$CALLBACK $__counter $(printf "%q" "$REPLY")"; fi
### Bash 3.x doesn't allow `printf -v foo[0]`...
### and `read -r foo[0]` mucks with whitespace
eval "${var}[$((__counter++))]=$(printf "%q" "$REPLY")"
done
}
### Alias `readarray` as well...
readarray() { mapfile "$#"; }
fi
if [[ -z ${PS1+YES} ]]; then
echo "'mapfile' should only be called as a shell function; try \"source ${BASH_SOURCE[0]##*/}\" first..." >&2
exit 1
fi

Setting Shell Positional Parameters With "set ls -AF"

I am writing a ksh function (that is placed in the .profile file) that will present a menu of subdirectories and permit the user to choose one into which to cd. Here is the code:
# Menu driven subdirectory descent.
d(){
# Only one command line argument accepted
[ "$1" = "--" ] && shift $# # Trap for "ls --" feature
wd=`pwd`; arg="${1:-$wd}"
dirs="`/bin/ls -AF $arg 2>/dev/null | grep /$ | tr -d \"/\"`"
# Set the names of the subdirectories to positional parameters
if [ "$dirs" ] ;then
set $dirs
if [ $# -eq 1 -a "$arg" = "$wd" ] ;then cd $arg/$1; return; fi # trap: it's obvious; do it
else echo "No subdirectories found" >&2; return 1
fi
# Format and display the menu
if [ `basename "${arg}X"` = "${arg}X" ] ;then arg="$wd/$arg"; fi # Force absolute path if relitive
echo -e "\n\t\tSubdirectories relative to ${arg}: \n"
j=1; for i; do echo -e "$j\t$i"; j=`expr $j + 1`; done | pr -r -t -4 -e3
echo -e "\n\t\tEnter the number of your choice -- \c "
# Convert user-input to directory-name and cd to it
read choice; echo
dir=`eval "(echo $\{"$choice"\})"` # Magic here.
[ "$choice" -a "$choice" -ge 1 -a "$choice" -le "$#" ] && cd $arg/`eval echo "$dir"`
}
This function works reasonably well with the exception of directory names that contain space characters. If the directory name contains a space, the set command sets each space delimited element of the directory name (instead of the complete directory name) into a separate positional parameter; that is not useful here.
I have attempted to set the $IFS shell variable (which contains a space, tab, and newline by default) to a single newline character with:
IFS=`echo` # echo outputs a trailing newline character by default
Which appears to accomplish what is intended as verified with:
echo -e "$IFS\c" | hexdump -c
But despite my best efforts (over the course of several days work) I have failed to set the entire directory names that contain spaces as values for positional parameters.
What am I missing?
Suggestions are hereby solicited and most welcome.
ADVAthanksNCE
Bob
Short answer: You can't do that. Don't try. See the ParsingLs page for an understanding of why programmatic use of ls is inherently error-prone.
You can't get -F behavior without implementing it yourself in shell (which is indeed feasible), but the following is the correct way to put a list of subdirectories into the argument list:
set -- */
If you don't want to have a literal / on the end of each entry:
set -- */ # put list of subdirectories into "$#"
set -- "${#%/}" # strip trailing / off each
Even better, though: Use an array to avoid needing eval magic later.
dirs=( */ )
dirs=( "${dirs[#]%/}" )
printf '%s\n' "${dirs[$choice]}" # emit entry at position $choice
Let's tie this all together:
d() {
destdir=$(
FIGNORE= # ksh93 equivalent to bash shopt -s dotglob
while :; do
subdirs=( ~(N)*/ ) # ksh93 equivalent to subdirs=( */ ) with shopt -s nullglob
(( ${#subdirs[#]} > 2 )) || break # . and .. are two entries
for idx in "${!subdirs[#]}"; do
printf '%d) %q\n' "$idx" "${subdirs[$idx]%/}" >&2
done
printf '\nSelect a subdirectory: ' >&2
read -r choice
if [[ $choice ]]; then
cd -- "${subdirs[$choice]}" || break
else
break
fi
done
printf '%s\n' "$PWD"
)
[[ $destdir ]] && cd -- "$destdir"
}
Although still not working, this version does pass shellcheck albeit with one exception:
3 # Menu driven subdirectory descent.
4 function d{
5 # Only one command line argument accepted
6 [ "$1" = "--" ] && shift $# # Trap for "ls --" feature
7 wd="$PWD"; arg="${1:-$wd}"
8 set -- "${#%/}" # Set the names of the subdirectories to positional parameters
9 if [ $# -eq 1 -a "$arg" = "$wd" ] ;then cd "$arg/$1" || exit 1; return; # trap: it's obvious; do it
10 else echo "No subdirectories found" >&2; return 1
11 fi
12 # Format and display the menu
13 if [[ $(basename "${arg}X") = "${arg}X" ]] ;then arg="$wd/${arg}"; fi # Force absolute path if relitive
14 echo -e "\n\t\tSubdirectories relative to ${arg}: \n"
15 j=1; for i; do echo -e "$j\t$i"; j=(expr $j + 1); done | pr -r -t -4 -e3
16 echo -e "\n\t\tEnter the number of your choice -- \c "
17 # Convert user-input to directory-name and cd to it
18 read -r choice; echo
19 dir=(eval "(echo $\{\"$choice\"\})") # Magic here.
20 [ "$choice" -a "$choice" -ge 1 -a "$choice" -le "$#" ] && cd "${arg}"/"$(eval echo "${dir}")" || exit 1
^SC2128 Expanding an array without an index only gives the first element.
21 }
Once I have incorporated your suggestions into the code, and made it functional, I'll post it here, and mark my question answered. Thank you for your kind assistance.
I've used the code you kindly wrote as a basis for the d function below. It pretty much does what I'd like, with a few little issues:
All subdirectory names that contain a SPACE character are surrounded by characters, but those that do not are not.
All subdirectory names that contain a SINGLE QUOTE character have that character escaped with a BACKSLASH character.
Given that 1 and 2 above cause no issues, they are acceptable, but not ideal.
After user input does the cd, the menu of subdirectory names is again looped through. This could be considered a feature, I suppose. I tried substituting a return for the brake commands in the sections of code following the cd commands, but was unsuccessful in overcoming the subsequent looped menu.
The inclusion of "." and ".." at the head of the menu of subdirectories is not ideal, and actually serves no good purpose.
------------------- Code Begins ------------------------------
d() {
if [ "$BASH" ] && [ "$BASH" != "/bin/sh" ]; then
echo "$FUNCNAME: ksh only";return 1
fi
FIGNORE= # ksh93 equivalent to bash shopt -s dotglob
if [ ${#} -gt 0 ] ;then # Only one command line argument accepted
cd -- "$1" && return 0
fi
if [ `ls -AF1|grep /|wc -l` -eq 1 ] ;then # cd if only one subdirectory
cd -- `ls -AF1|grep /` && return 0
fi
destdir=$(
while :; do
subdirs=( ~(N)*/ ) # ksh93 equivalent to subdirs=( */ ) with shopt -s nullglob
(( ${#subdirs[#]} > 2 )) || break # . and .. are two entries
echo -e "\n\t\tSubdirectories below ${PWD}: \n" >&2
for idx in "${!subdirs[#]}"; do
printf '%d) %q\n' "$idx" "${subdirs[$idx]%/}" >&2
done
printf '\nSelect a subdirectory: ' >&2
read -r
if [[ $REPLY ]]; then
cd -- "${subdirs[$REPLY]}" || break # Continue to loop through subdirectories after cding
else
break
fi
done
printf '%s\n' "$PWD"
)
--------------------------- Code Ends ------------------------------------
So, overall I'm very pleased, and consider myself very fortunate to have received the knowledgeable assistance of such an accomplished Unix wizard. I can't thank you enough.

can't seem to get this function with bash to work

# define some variables for later
date=`date`
usr=`whoami`
# define usage function to echo syntax if no args given
usage(){
echo "error: filename not specified"
echo "Usage: $0 filename directory/ directory/ directory/"
exit 1
}
# define copyall function
copyall() {
# local variable to take the first argument as file
local file="$1" dir
# shift to the next argument(s)
shift
# loop through the next argument(s) and copy $file to them
for dir in "$#"; do
cp -R "$file" "$dir"
done
}
# function to check if filename exists
# $f -> store argument passed to the script
file_exists(){
local f="$1"
[[ -f "$f" ]] && return 0 || return 1
}
# call usage() function to print out syntax
[[ $# -eq 0 ]] && usage
here's what I can't figure out
# call file_exists() and copyall() to do the dirty work
if ( file_exists "$1" ) then
copyall
I would also love to figure out how to take this next echo section and condense it to one line. Instead of $1 then shift then move on. Maybe split it into an array?
echo "copyall: File $1 was copied to"
shift
echo "$# on $date by $usr"
else
echo "Filename not found"
fi
exit 0
It seems to me that the file_exists macro is superfluous:
if [ -f "$1" ]
then copy_all "$#"
else echo "$0: no such file as $1" >&2; exit 1
fi
or even just:
[ -f "$1" ] && copy_all "$#"
You probably just need to remove the parentheses around file_exists "$1", and add a semicolon:
if file_exists "$1"; then
copyall

BASH: comparing strings, one from a file, one in my program, if statement always is true

So I have this block of code. Basically, I'm taking file $i, checking if it's got content or not, checking if I can read it, if I can open it, grab the first line and see if it's a bash file. When I run this every time on a non-empty file, it was registers as true and echo's bash.
## File is empty or not
if [[ -s $i ]]
then
## Can we read the file
if [[ -r $i ]]
then
## File has content
if [[ $(head -n 1 $i) = "#! /bin/bash" ]]
then
echo -n " bash"
fi
fi
else
## file does not have content
echo -n " empty"
fi
This is what does the check of if it's bash:
if [[ $(head -n 1 $i) = "#! /bin/bash" ]]
Replace [[ with [ and enclose $(head -n 1 $i) in quotes.
[[ is itself an operator that tests its contents.

bash scripting: How to test list membership

(This is debian squeeze amd64)
I need to test if a file is a member of a list of files.
So long my (test) script is:
set -x
array=$( ls )
echo $array
FILE=log.out
# This line gives error!
if $FILE in $array
then echo "success!"
else echo "bad!"
fi
exit 0
¿Any ideas?
Thanks for all the responses. To clarify: The script given is only an example, the actual problem is more complex. In the final solution, it will be done within a loop, so I need the file(name) to be tested for to be in a variable.
Thanks again. No my test-script works, and reads:
in_list() {
local search="$1"
shift
local list=("$#")
for file in "${list[#]}" ; do
[[ "$file" == "$search" ]] && return 0
done
return 1
}
#
# set -x
array=( * ) # Array of files in current dir
# echo $array
FILE="log.out"
if in_list "$FILE" "${array[#]}"
then echo "success!"
else echo "bad!"
fi
exit 0
if ls | grep -q -x t1 ; then
echo Success
else
echo Failure
fi
grep -x matches full lines only, so ls | grep -x only returns something if the file exists.
If you just want to check if a file exists, then
[[ -f "$file" ]] && echo yes || echo no
If your array contains a list of files generated by some means other than ls, then you have to iterate over it as demonstrated by Sorpigal.
How about
in_list() {
local search="$1"
shift
local list=("$#")
for file in "${list[#]}" ; do
[[ $file == $search ]] && return 0
done
return 1
}
if in_list log.out * ; then
echo 'success!'
else
echo 'bad!'
fi
EDIT: made it a bit less idiotic.
EDIT #2:
Of course if all you're doing is looking in the current directory to see if a particular file is there, which is effectively what the above is doing, then you can just say
[ -e log.out ] && echo 'success!' || echo 'bad!'
If you're actually doing something more complicated involving lists of files then this might not be sufficient.

Resources