bash complete from two sources, folders and keys from array - bash

I am trying to define a bash function, mycd. This function uses an associative array mycdar. If the key exists in the array the function will change directory to the corresponding value of the key. If the key doesn't exist it will change to the dir provided in the command line.
What I would like to do is to have completion for this function, from both they keys of the associated array or from the folders existing in the current directory.
Thank you.

Building my own cd function with completion
Using an associative array for storing some paths.
First the command:
mycd() { [ -v mycdar["$1"] ] && cd "${mycdar[$1]}" || cd "$1"; }
Second the completion command
_mycd() {
local cur;
_cd ;
_get_comp_words_by_ref cur;
COMPREPLY=($(
printf "%s\n" "${!mycdar[#]}" |
grep ^$cur)
${COMPREPLY[#]});
}
One array:
declare -A mycdar='(
["docs"]="/usr/share/doc"
["home"]="$HOME"
["logs"]="/var/log"
["proc"]="/proc"
["root"]="/"
["tmp"]="/tmp"
)'
Than finaly the bind:
complete -F _mycd -o nospace mycd
Or to permit standard path building behaviour:
complete -F _mycd -o nospace -o plusdirs mycd

It turns out that there is an option that to the complete function that does exactly what is asked:
complete -o plusdirs -o nospace -F _mycd mycd
In this case _mycd just returns matching elements from the keys of the associative array.

Related

Pass array to from one function to another and perform parameter expansion

The below utility script does all kinds of tarring for client scripts. It does absolute-to-relative path conversions many times so I extracted that into a separate function (_convertToRelative).
#!/bin/bash
_convertToRelative() {
local -n arr1=$1
local arr2=( "${arr1[#]/#\//}" ) # ( /foo/bar /foo/baz ) -> ( foo/bar foo/baz )
echo $arr2
}
tarPaths() {
local -n paths_absolute=$1
local paths_relative=${_convertToRelative paths_absolute} # fails here
# ...run tar
}
A client script would do this
paths=( '/foo/bar/baz/a' '/foo/bar/baz/b' '/foo/bar/baz/c' )
tarPaths paths
But it fails with:
paths_relative=${_convertToRelative paths_absolute}: bad substitution
You could pass variable paths_relative as reference too. Then you don't need to echo the array as string and convert that back to an array which could lead to problems if a path contains spaces, tabs or newlines:
#!/bin/bash
_convertToRelative() {
local -n arr1=$1 arr2=$2
arr2=( "${arr1[#]#/}" ) # <- remove shortest prefix `/` from each array element
}
tarPaths() {
local -n paths_absolute=$1
local -a paths_relative
_convertToRelative paths_absolute paths_relative
# ...run tar
}
There seems to be a typo: Instead of ${...}, you need to type $(...) (brackets instead of accolades).

Recursively create directories for all letters

I would like to create a folder structure based on a brace expansion such as {a-z}. Each string generated by the brace-expansion should be a new folder. Furthermore, each of these folders should contain the same set of subfolders similarly generated. And this up to a given level.
An example for the range a-z and depth 16
a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/
a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/b/
a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/c/
...
d/a/h/r/y/d/s/b/e/y/k/f/o/o/q/c/
...
z/z/z/z/z/z/z/z/z/z/z/z/z/z/z/y/
z/z/z/z/z/z/z/z/z/z/z/z/z/z/z/z/
The following code allows me to go upto depth 2:
for x in {a..z} ; do mkdir -p $x/{a..z} ; done
But how do I go further?
A recursive solution. Job is called with 2 params: max_depth and base_path
#!/bin/bash
function job()
{
local depth=$(($1-1))
local path=$2
local x
for x in a b c # reduced for test
do
mkdir -p "$path/$x"
((depth>0)) && job $depth "$path/$x"
done
}
job 3 ./test
Proof it with:
find test -type d
The simplest form would be to use any of the following lines:
mkdir -p {a..c}/{a..c} # depth 2
mkdir -p {a..c}/{a..c}/{a..c} # depth 3
mkdir -p {a..c}/{a..c}/{a..c}/{a..c} # depth 4
...
The brace-expansion will make all combinations and mkdir -p will take care of the rest.
Of course, you do not want to type this over and over for various sets. So you could generate the full brace-expansion-string with bash and use exec to process the brace-expansion-string before passing it to mkdir -p:
depth=3
set={a..c}
dirs=$(printf "/${set}%.0s" $(seq $depth))
mkdir -p $(eval echo .${dirs})
Be aware however that if your set has length m, and you want a depth of n, you are creating m^n directories. This number could conflict with the number of arguments you can pass on to a program.
Related information:
What is the maximum allowed depth of sub-folders?
https://www.in-ulm.de/~mascheck/various/argmax/
A recursive funcion may solve your problem. Take care with inodes generation when using high directory levels...
#!/bin/bash
function createDir {
mkdir -p $1 && cd $1;
for x in {a..z} ; do
local i=$(($2-1))
[ $i -lt 0 ] && continue;
createDir $x $i
done
cd ..
}
createDir $1 $2
Save into a file, like mkdir.sh, and call it: ./mkdir.sh <main_folder> <level>.

Shell script: Copy file and folder N times

I've two documents:
an .json
an folder with random content
where <transaction> is id+sequancial (id1, id2... idn)
I'd like to populate this structure (.json + folder) to n. I mean:
I'd like to have id1.json and id1 folder, an id2.json and id2 folder... idn.json and idn folder.
Is there anyway (shell script) to populate this content?
It would be something like:
for (i=0,i<n,i++) {
copy "id" file to "id+i" file
copy "id" folder to "id+i" folder
}
Any ideas?
Your shell syntax is off but after that, this should be trivial.
#!/bin/bash
for((i=0;i<$1;i++)); do
cp "id".json "id$i".json
cp -r "id" "id$i"
done
This expects the value of n as the sole argument to the script (which is visible inside the script in $1).
The C-style for((...)) loop is Bash only, and will not work with sh.
A proper production script would also check that it received the expected parameter in the expected format (a single positive number) but you will probably want to tackle such complications when you learn more.
Additionaly, here is a version working with sh:
#!/bin/sh
test -e id.json || { (>&2 echo "id.json not found") ; exit 1 ; }
{
seq 1 "$1" 2> /dev/null ||
(>&2 echo "usage: $0 transaction-count") && exit 1
} |
while read i
do
cp "id".json "id$i".json
cp -r "id" "id$i"
done

Dynamic variable created in function not available in future calls

I have a script that is (supposed to be) assigning a dynamic variable name (s1, s2, s3, ...) to a directory path:
savedir() {
declare -i n=1
sn=s$n
while test "${!sn}" != ""; do
n=$n+1
sn=s$n
done
declare $sn=$PWD
echo "SAVED ($sn): ${!sn}"
}
The idea is that the user is in a directory they'd like to recall later on and can save it to a shell variable by typing 'savedir'. It -does- in fact write out the echo statement successfully: if I'm in the directory /home/mrjones and type 'savedir', the script returns:
SAVED (s1): /home/mrjones
...and I can further type:
echo $sn
and the script returns:
s1
...but typing either...
> echo $s1
...or
echo ${!sn}
...both return nothing (empty strings). What I want, in case it's not obvious, is this:
echo $s1
/home/mrjones
Any help is greatly appreciated! [apologies for the formatting...]
To set a variable using a name stored in another variable I use printf -v, in this example:
printf -v "$sn" '%s' "$PWD"
declare here is creating a variable local to the function, which doesn't seem to be what you want. Quoting from help declare:
When used in a function, declare makes NAMEs local, as with the local
command. The -g option suppresses this behavior.
so you can either try the -g or with the printf
Use an array instead.
savedir() {
s+=("$PWD")
echo "SAVED (s[$((${#s[#]}-1))]): ${s[${#s[#]}-1]}"
}

accessing newly created directory in shell script

I'm attempting to make a new folder, a duplicate of the input, and then tar the contents of that folder. I can't figure out why - but it seems like instead of searching the contents of my newly created directory - it is searching my entire computer... returning lines such as
/Applications/GarageBand.app/Contents/Frameworks/MAAlchemy.framework/Resources/Libraries/WaveOsc/Sine/Sine - Vocal 1.raw is a file
/Applications/GarageBand.app/Contents/Frameworks/MAAlchemy.framework/Resources/Libraries/WaveOsc/Sine/Sine - Vocal 2.raw is a file
/Applications/GarageBand.app/Contents/Frameworks/MAAlchemy.framework/Resources/Libraries/WaveOsc/Sine/Triangle - Arp.raw is a file
/Applications/GarageBand.app/Contents/Frameworks/MAAlchemy.framework/Resources/Libraries/WaveOsc/Sine/Triangle - Asym 4.raw is a file
/Applications/GarageBand.app/Contents/Frameworks/MAAlchemy.framework/Resources/Libraries/WaveOsc/Sine/Triangle - Eml.raw is a file
/Applications/GarageBand.app/Contents/Frameworks/MAAlchemy.framework/Resources/Libraries/WaveOsc/Square is a folder
/Applications/GarageBand.app/Contents/Frameworks/MAAlchemy.framework/Resources/Libraries/WaveOsc/Square/Square - Arp.raw is a file
/Applications/GarageBand.app/Contents/Frameworks/MAAlchemy.framework/Resources/Libraries/WaveOsc/Square/Square - Bl Saw.raw is a file
can you guys spot a simple error?
BTW, I know that the script to tar isn't present yet, but that will be easy once i can navigate the new folder.
#!/bin/bash
##--- deal with help args ------------------
##
print_help_message() {
printf "Usage: \n"
printf "\t./`basename $0` <input_dir> <output_dir>\n"
printf "where\n"
printf "\tinput_dir : (required) the input directory.\n"
printf "\toutput_dir : (required) the output directory.\n"
}
if [ "$1" == "help" ]; then
print_help_message
exit 1
fi
## ------ get cli args ----------------------
##
if [ $# == 2 ]; then
INPUT_DIR="$1"
OUTPUT_DIR="$2"
fi
## ------ tree traversal function -----------
##
mkdir "$2"
cp -r "$1"/* "$2"/
## ------ return output dir name ------------
##
return_output_dir() {
echo $OUTPUT_DIR/$(basename $(basename $(dirname $1)))
}
bt() {
output_dir="$1"
for filename in $output_dir/*; do
if [ -d "${filename}" ]; then
echo "$filename is a folder"
bt $filename
else
echo "$filename is a file"
fi
done
}
## ------ main ------------------------------
##
main() {
bt $return_output_dir
exit 0
}
main
}
Well, I can tell you why it's doing that, but I'm not clear on what it's supposed to be doing, so I'm not sure how to fix it. The immediate problem is that return_output_dir is a function, not a variable, so in the command bt $return_output_dir the $return_output_dir part expands to ... nothing, and bt gets run with no argument. That means that inside bt, output_dir gets set to the empty string, so for filename in $output_dir/* becomes for filename in /*, which iterates over the top-level items on your boot volume.
There are a number of other things that're confusing/weird about this code:
The function main() doesn't seem to serve any purpose -- some of the main-line code is outside it (notably, the argument parsing stuff), some inside, for no apparent reason. Having a main function is required in some languages, but in a shell script it generally makes more sense to just put the main code inline. (Also, functions shouldn't exit, they should return.)
You have variables named both OUTPUT_DIR and output_dir. Use distinct names. Also, it's generally best to stick to lowercase (or mixed-case) variable names, to avoid conflicts with the variables that're used by the shell and other programs.
You copy $1 and $2 into INPUT_DIR and OUTPUT_DIR, then continue to use $1 and $2 rather than the more-clearly-named variables you just copied them into.
output_dir is changed in the recursive function, but not declared as local; this means that inner invocations of bt will be changing the values that outer ones might try to use, leading to weirdness. Declare function-local variables as local to avoid trouble.
$(basename $(basename $(dirname $1))) doesn't make sense. Suppose $1 is "/foo/bar/baz/quux": then dirname $1 returns /foo/bar/baz, basename /foo/bar/baz returns "baz", and basename baz returns "baz" again. The second basename isn't doing anything! And in any case, I'm pretty sure the whole thing isn't doing what you expect it to.
What directory is bt supposed to be recursing through? Nothing in how you call it has any reference to either INPUT_DIR or OUTPUT_DIR.
As a rule, you should put variable references in double-quotes (e.g. for filename in "$output_dir"/* and bt "$filename"). You do this in some places, but not others.

Resources