Bash script: internal function alias for longer arguments - bash

Suppose I have defined an array, like this:
DIR=(A B Supercalifragilistic)
and I need to invoke the script as
./script A B Supercalifragilistic
where the arguments are processed by internal functions func1() and func2(). Is there a way to make an alias (or anything, however it's called) S for Supercalifragilistic so that when I invoke:
./script A B S
the internal functions will process/interpret S as Supercalifragilistic?
Thank you in advance.
[edit]
I should add that the script is invoked via terminal, not inside a script, and the arguments A B Supercalifragilistic, or (hopefully) S, are passed on to the script in the terminal. I'm sorry for the confusion.
[edit2]
The script is here: Bash script: if any argument is "N" then function has extra options , in the answer below. What it does is explained in the OP there, below the script. Finally, instead of DIR=(A B C D E F) it's DIR=(A B Clarification D E F) (it's just an example) and the folder Clarification is the only one in a different path than the rest. I hope it's more clear now, if not, please tell me.
[final edit, I hope]
I think I can shout "Evrika!". Your word "hardcoded" made me realize I have to modify the script anytime a new folder gets added/deleted, so I thought of making the array dynamic, as in
./script a b "d e" g results in array=(a b "d e" g)
but also that it should replace the long paths with some short ones (Clarification >> C), so I made this test script based on also the answers here:
#!/bin/bash
array=()
for i in "$#"
do
if [[ "$i" == C ]]
then
array+=("Clarification")
else
array+=("$i")
fi
done
echo ${array[*]}
echo
for i in $(seq 0 $(( $# - 1 )))
do
echo ${array["$i"]}
done
and this is what it shows at command prompt:
$ ./x.sh abc C "d f" e
abc Clarification d f e
abc
Clarification
d f
e
I think now I can finally make the script to do what I want. Thank you, all, for the answers.

I really have no idea what you exactly want to achieve! But I had a look at the script you linked in your last edit. Since you have a hard-coded array you might as well instead use an associative array:
declare -A dir_h
dir_h["A"]=A
dir_h["B"]=B
dir_h["C"]=../path/Clarification
dir_h["D"]=D
dir_h["E"]=E
to loop on the keys of dir_h, i.e., on A B C D E:
for k in "${!dir_h[#]}"; do
echo "$k => ${dir_h[$k]}"
done
Try it, this might help you with your "alias" problem (or not).
Here's your script from your other post, using this technique and in a more consistent and readable form (note: I haven't tried it, there might be some minor typos, let me know if it's the case):
#!/bin/bash
# ./test.sh = 1. searches for existing archives
# 1.a. if they exist, it backups them into BKP/.
# 1.b. if not, displays a message
# 2. archives all the directories in the array list
# ./test.sh N = 1. deletes all the folder's archives existent and
# specified in the array list
# 2. archives all the directories in the array list
# ./test.sh {A..F} = 1. searches for existing archives from arguments
# 1.a. if they exist, it backups them into BKP/.
# 1.b. if not, displays a message
# 2. archives all the directories passed as arguments
# ./test.sh {A..F} N = 1. deletes all the archives matching $argument.zip
# 2. archives all the directories passed as arguments
# The directories to be backed-up/archived, all in the current (script's) path
# except "C", on a different path
declare -A dir_h
dir_h["A"]=A
dir_h["B"]=B
dir_h["C"]=../path/Clarification
dir_h["D"]=D
dir_h["E"]=E
dir_h["F"]=F
declare -A nope_h
nope_h["A"]=bogus
nope_h["B"]=bogus
nope_h["C"]=nope
nope_h["D"]=bogus
nope_h["E"]=bogus
nope_h["F"]=bogus
die() {
(($#)) && printf >&2 "%s\n" "$#"
exit 1
}
bak() {
if [[ "$1" != N ]]; then
# Check that arg is in dir list:
[[ -n ${dir_h["$1"]} ]] || die "Error in bak: argument \`$1' not handled"
if [[ -f $1.zip ]]; then
mv -vi "$1.zip" "BKP/$1.zip_$(date +"%H-%M")" || die
else
echo "$(tput setaf 1) no $1.zip$(tput sgr0)"
fi
fi
}
# The archive function, if any argument is "N", processing it is omitted. Folder
# "C" has special treatment
archive() {
if [[ $1 != N ]]; then
7z a -mx=9 "$1.zip" "${dir_h["$1"]}" -r -x\!"$1/${nope_h["$1"]}" || die
fi
}
# Let's check once for all whether N is in the arg list
foundN=0
for a in "$#"; do [[ $a = N ]] && foundN=1 && break; done
if (($#==0)); then
# case #1: no arguments
for d in "${!dir_h[#]}"; do
echo "$(tput setaf 2) backup$(tput sgr0)"
bak "$d"
archive "$d"
done
elif (($#==1)) && ((foundN)); then
# case #2: one argument, "N"
for d in "${!dir_h[#]}"; do
echo "$(tput setaf 1) no backup needed, removing$(tput sgr0)"
rm -v "$d".zip || die
archive "$d"
done
elif (($#>1)) && ((foundN)); then
# case #3: folders as arguments with "N"
for f in "$#"; do
if [[ $f != N ]]; then
echo "$(tput setaf 1) no backup needed, removing$(tput sgr0)"
rm -v "$f.zip" || die
fi
archive "$f"
done
else
for f in "$#"; do
echo "$(tput setaf 2) backup$(tput sgr0)"
bak "$f"
archive "$f"
done
fi
From this you can do a lot, and have pretty much infinite "alias" handling possibilities.

No need to use an alias. You could try something like :
$ cat test.sh
#!/bin/bash
declare -a args
for arg in "$#"; do
[[ $arg = "S" ]] && arg="Supercalifragilistic"
args+=( "$arg" )
done
for arg in "${args[#]}"; do
echo "$arg"
done
$ ./test.sh a b S e
a
b
Supercalifragilistic
e

You don't need alias here. Just set variable S to your string:
S=Supercalifragilistic
and then use:
./script A B "$S"
OR else call your script directly using array:
./script ${DIR[#]}
PS: It is not a good habit to use all caps variable names in shell and you can accidentally overwrite PATH variable some day.

You can do this:
processed_directories=()
for dir in "${directories[#]}"
do
if [ "$dir" = 'S' ]
then
dir='Supercalifragilistic'
fi
processed_directories+=("$dir")
done
It'll replace the value "S" with "Supercalifragilistic" anywhere in the array.

Related

Recursively list hidden files without ls, find or extendedglob

As an exercise I have set myself the task of recursively listing files using bash builtins. I particularly don't want to use ls or find and I would prefer not to use setopt extendedglob. The following appears to work but I cannot see how to extend it with /.* to list hidden files. Is there a simple workaround?
g() { for k in "$1"/*; do # loop through directory
[[ -f "$k" ]] && { echo "$k"; continue; }; # echo file path
[[ -d "$k" ]] && { [[ -L "$k" ]] && { echo "$k"; continue; }; # echo symlinks but don't follow
g "$k"; }; # start over with new directory
done; }; g "/Users/neville/Desktop" # original directory
Added later: sorry - I should have said: 'bash-3.2 on OS X'
Change
for k in "$1"/*; do
to
for k in "$1"/* "$1"/.[^.]* "$1"/..?*; do
The second glob matches all files whose names start with a dot followed by anything other than a dot, while the third matches all files whose names start with two dots followed by something. Between the two of them, they will match all hidden files other than the entries . and ...
Unfortunately, unless the shell option nullglob is set, those (like the first glob) could remain as-is if there are no files whose names match (extremely likely in the case of the third one) so it is necessary to verify that the name is actually a file.
An alternative would be to use the much simpler glob "$1"/.*, which will always match the . and .. directory entries, and will consequently always be substituted. In that case, it's necessary to remove the two entries from the list:
for k in "$1"/* "$1"/.*; do
if ! [[ $k =~ /\.\.?$ ]]; then
# ...
fi
done
(It is still possible for "$1"/* to remain in the list, though. So that doesn't help as much as it might.)
Set the GLOBIGNORE file to exclude . and .., which implicitly turns on "shopt -u dotglob". Then your original code works with no other changes.
user#host [/home/user/dir]
$ touch file
user#host [/home/user/dir]
$ touch .dotfile
user#host [/home/user/dir]
$ echo *
file
user#host [/home/user/dir]
$ GLOBIGNORE=".:.."
user#host [/home/user/dir]
$ echo *
.dotfile file
Note that this is bash-specific. In particular, it does not work in ksh.
You can specify multiple arguments to for:
for k in "$1"/* "$1"/.*; do
But if you do search for .* in directories , you should be aware that it also gives you the . and .. files. You may also be given a nonexistent file if the "$1"/* glob matches, so I would check that too.
With that in mind, this is how I would correct the loop:
g() {
local k subdir
for k in "$1"/* "$1"/.*; do # loop through directory
[[ -e "$k" ]] || continue # Skip missing files (unmatched globs)
subdir=${k##*/}
[[ "$subdir" = . ]] || [[ "$subdir" = .. ]] && continue # Skip the pseudo-directories "." and ".."
if [[ -f "$k" ]] || [[ -L "$k" ]]; then
printf %s\\n "$k" # Echo the paths of files and symlinks
elif [[ -d "$k" ]]; then
g "$k" # start over with new directory
fi
done
}
g ~neville/Desktop
Here the funky-looking ${k##*/} is just a fast way to take the basename of the file, while local was put in so that the variables don't modify any existing variables in the shell.
One more thing I've changed is echo "$k" to printf %s\\n "$k", because echo is irredeemably flawed in its argument handling and should be avoided for the purpose of echoing an unknown variable. (See Rich's sh tricks for an explanation of how; it boils down to -n and -e throwing a spanner in the works.)
By the way, this will NOT print sockets or fifos - is that intentional?

How to fetch last argument and stop before last arguments in shell script?

I want to merge all files into one. Here, the last argument is the destination file name.
I want to take last argument and then in loop stop before last arguments.
Here code is given that I want to implement:
echo "No. of Argument : $#"
for i in $* - 1
do
echo $i
cat $i >> last argument(file)
done
How to achieve that?
Using bash:
fname=${!#}
for a in "${#:1:$# - 1}"
do
echo "$a"
cat "$a" >>"$fname"
done
In bash, the last argument to a script is ${!#}. So, that is where we get the file name.
bash also allows selecting elements from an array. To start with a simple example, observe:
$ set -- a b c d e f
$ echo "${#}"
a b c d e f
$ echo "${#:2:4}"
b c d e
In our case, we want to select elements from the first to the second to last. The first is number 1. The last is number $#. We want to select all but the last. WE thus want $# - 1 elements of the array. Therefore, to select the arguments from the first to the second to last, we use:
${#:1:$# - 1}
A POSIX-compliant method:
eval last_arg=\$$#
while [ $# -ne 1 ]; do
echo "$1"
cat "$1" >> "$last_arg"
shift
done
Here, eval is safe, because you are only expanding a read-only parameter in the string that eval will execute. If you don't want to unset the positional parameters via shift, you can iterate over them, using a counter to break out of the loop early.
eval last_arg=\$$#
i=1
for arg in "$#"; do
echo "$arg"
cat "$arg" >> "$last_arg"
i=$((i+1))
if [ "$i" = "$#" ]; then
break
fi
done

Bash script trouble interpretting input

I wrote a bash script that uploads a file on my home server. It gets activated from a folder action script using applescript. The setup is the folder on my desktop is called place_on_server. Its supposed to have an internal file structure exactly like the folder I want to write to: /var/www/media/
usage goes something like this:
if directory etc added to place_on_server: ./upload DIR etc
if directory of directory: etc/movies ./upload DIR etc movies //and so on
if file to place_on_server: ./upload F file.txt
if file in file in place_on_server ./upload F etc file.txt //and so on
for creating a directory its supposed to execute a command like:
ssh root#192.168.1.1<<EOF
cd /var/www/media/wherever
mkdir newdirectory
EOF
and for file placement:
rsync -rsh='ssh -p22' file root#192.168.1.1:/var/www/media/wherever
script:
#!/bin/bash
addr=$(ifconfig -a | ./test)
if ($# -le "1")
then
exit
elif ($1 -eq "DIR")
then
f1="ssh -b root#$addr<<EOF"
list = "cd /var/www/media\n"
if($# -eq "2")
then
list=list+"mkdir $2\nEOF\n"
else
num=2
i=$(($num))
while($num < $#)
do
i=$(($num))
list=list+"mkdir $i\n"
list=list+"cd $i\n"
$num=$num+1
done
fi
echo $list
elif ($1 -eq "F")
then
#list = "cd /var/www/media\n"
f2="rsync -rsh=\'ssh -p22\' "
f3 = "root#$addr:/var/www/media"
if($# -eq "2")
then
f2=f2+$2+" "+f3
else
num=3
i=$(($num))
while($num < $#)
do
i=$(($num))
f2=f2+"/"+$i
$num=$num+1
done
i=$(($num))
f2=f2+$i+" "+$f3
fi
echo $f2
fi
exit
output:
(prompt)$ ./upload2 F SO test.txt
./upload2: line 3: 3: command not found
./upload2: line 6: F: command not found
./upload2: line 25: F: command not found
So as you can see I'm having issues handling input. Its been awhile since I've done bash. And it was never extensive to begin with. Looking for a solution to my problem but also suggestions. Thanks in advance.
For comparisons, use [[ .. ]]. ( .. ) is for running commands in subshells
Don't use -eq for string comparisons, use =.
Don't use < for numerical comparisons, use -lt
To append values, f2="$f2$i $f3"
To add line feeds, use $'\n' outside of double quotes, or a literal linefeed inside of them.
You always need "$" on variables in strings to reference them, otherwise you get the literal string.
You can't use spaces around the = in assignments
You can't use $ before the variable name in assignments
To do arithmetics, use $((..)): result=$((var1+var2))
For indirect reference, such as getting $4 for n=4, use ${!n}
To prevent word splitting removing your line feeds, double quote variables such as in echo "$line"
Consider writing smaller programs and checking that they work before building out.
Here is how I would have written your script (slightly lacking in parameter checking):
#!/bin/bash
addr=$(ifconfig -a | ./test)
if [[ $1 = "DIR" ]]
then
shift
( IFS=/; echo ssh "root#$addr" mkdir -p "/var/www/media/$*"; )
elif [[ $1 = "F" ]]
then
shift
last=$#
file=${!last}
( IFS=/; echo rsync "$file" "root#$addr:/var/www/media/$*" )
else
echo "Unknown command '$1'"
fi
$* gives you all parameters separated by the first character in $IFS, and I used that to build the paths. Here's the output:
$ ./scriptname DIR a b c d
ssh root#somehost mkdir -p /var/www/media/a/b/c/d
$ ./scriptname F a b c d somefile.txt
rsync somefile.txt root#somehost:/var/www/media/a/b/c/d/somefile.txt
Remove the echos to actually execute.
The main problem with your script are the conditional statements, such as
if ($# -le "1")
Despite what this would do in other languages, in Bash this is essentially saying, execute the command line $# -le "1" in a subshell, and use its exit status as condition.
in your case, that expands to 3 -le "1", but the command 3 does not exist, which causes the error message
./upload2: line 3: 3: command not found
The closest valid syntax would be
if [ $# -le 1 ]
That is the main problem, there are other problems detailed and addressed in that other guy's post.
One last thing, when you're assigning value to a variable, e.g.
f3 = "root#$addr:/var/www/media"
don't leave space around the =. The statement above would be interpreted as "run command f3 with = and "root#$addr:/var/www/media" as arguments".

Bash - being space-safe when saving $# in a variable

I have a problem when looping over such a variable. I have prepared 2 examples to show the problem.
ex1:
#!/bin/bash
DIRS="$#"
for DIR in $DIRS; do
echo "$DIR"
done
ex2:
#!/bin/bash
for DIR in "$#"; do
echo "$DIR"
done
The second example works as expected (and required). A quick test follows:
$ ex1 "a b" "c"
a
b
c
$ ex2 "a b" "c"
a b
c
The reason, why I want to use the first method is because I want to be able to pass multiple directories to the program or none to use the current dir. Like so:
[ $# -eq 0 ] && DIRS=`pwd` || DIRS="$#"
So, how do I get example 1 to be space-safe?
Use an array instead of a simple variable.
declare -a DIRS
DIRS=("$#")
for d in "${DIRS[#]}"
do echo "$d"
done
This produces the result:
$ bash xx.sh a "b c" "d e f g" h z
a
b c
d e f g
h
z
$
Why not use the default expansion feature?
for DIR in "${#:-$(pwd)}"; do
echo $DIR
done
One approach is to replace the spaces within arguments with something else, and then substitute spaces back again when you use the argument:
dirs="${#/ /###}"
for dir in $dirs; do
echo "${dir/###/ }"
done
This relies on your being able to come up with some sequence of characters that you can be confident will never appear in a real file name.
For the specific situation you have, where you want to be able to choose between supplying an explicit list of directories or defaulting to the current directory, a better solution is probably to use a function:
do_something() {
for dir in "$#"; do
echo "$dir"
done
}
if [ $# -eq 0 ]; then
do_something .
else
do_something "$#"
fi
Or possibly:
do_something() {
echo "$1"
}
if [ $# -eq 0 ]; then
do_something .
else
for dir in "$#"; do
do_something "$dir"
done
fi

Extract parameters before last parameter in "$#"

I'm trying to create a Bash script that will extract the last parameter given from the command line into a variable to be used elsewhere. Here's the script I'm working on:
#!/bin/bash
# compact - archive and compact file/folder(s)
eval LAST=\$$#
FILES="$#"
NAME=$LAST
# Usage - display usage if no parameters are given
if [[ -z $NAME ]]; then
echo "compact <file> <folder>... <compressed-name>.tar.gz"
exit
fi
# Check if an archive name has been given
if [[ -f $NAME ]]; then
echo "File exists or you forgot to enter a filename. Exiting."
exit
fi
tar -czvpf "$NAME".tar.gz $FILES
Since the first parameters could be of any number, I have to find a way to extract the last parameter, (e.g. compact file.a file.b file.d files-a-b-d.tar.gz). As it is now the archive name will be included in the files to compact. Is there a way to do this?
To remove the last item from the array you could use something like this:
#!/bin/bash
length=$(($#-1))
array=${#:1:$length}
echo $array
Even shorter way:
array=${#:1:$#-1}
But arays are a Bashism, try avoid using them :(.
Portable and compact solutions
This is how I do in my scripts
last=${#:$#} # last parameter
other=${*%${!#}} # all parameters except the last
EDIT
According to some comments (see below), this solution is more portable than others.
Please read Michael Dimmitt's commentary for an explanation of how it works.
last_arg="${!#}"
Several solutions have already been posted; however I would advise restructuring your script so that the archive name is the first parameter rather than the last. Then it's really simple, since you can use the shift builtin to remove the first parameter:
ARCHIVENAME="$1"
shift
# Now "$#" contains all of the arguments except for the first
Thanks guys, got it done, heres the final bash script:
#!/bin/bash
# compact - archive and compress file/folder(s)
# Extract archive filename for variable
ARCHIVENAME="${!#}"
# Remove archive filename for file/folder list to backup
length=$(($#-1))
FILES=${#:1:$length}
# Usage - display usage if no parameters are given
if [[ -z $# ]]; then
echo "compact <file> <folder>... <compressed-name>.tar.gz"
exit
fi
# Tar the files, name archive after last file/folder if no name given
if [[ ! -f $ARCHIVENAME ]]; then
tar -czvpf "$ARCHIVENAME".tar.gz $FILES; else
tar -czvpf "$ARCHIVENAME".tar.gz "$#"
fi
Just dropping the length variable used in Krzysztof Klimonda's solution:
(
set -- 1 2 3 4 5
echo "${#:1:($#-1)}" # 1 2 3 4
echo "${#:(-$#):($#-1)}" # 1 2 3 4
)
I would add this as a comment, but don't have enough reputation and the answer got a bit longer anyway. Hope it doesn't mind.
As #func stated:
last_arg="${!#}"
How it works:
${!PARAM} indicates level of indirection. You are not referencing PARAM itself, but the value stored in PARAM ( think of PARAM as pointer to value ).
${#} expands to the number of parameters (Note: $0 - the script name - is not counted here).
Consider following execution:
$./myscript.sh p1 p2 p3
And in the myscript.sh
#!/bin/bash
echo "Number of params: ${#}" # 3
echo "Last parameter using '\${!#}': ${!#}" # p3
echo "Last parameter by evaluating positional value: $(eval LASTP='$'${#} ; echo $LASTP)" # p3
Hence you can think of ${!#} as a shortcut for the above eval usage, which does exactly the approach described above - evaluates the value stored in the given parameter, here the parameter is 3 and holds the positional argument $3
Now if you want all the params except the last one, you can use substring removal ${PARAM%PATTERN} where % sign means 'remove the shortest matching pattern from the end of the string'.
Hence in our script:
echo "Every parameter except the last one: ${*%${!#}}"
You can read something in here: Parameter expansion
Are you sure this fancy script is any better than a simple alias to tar?
alias compact="tar -czvpf"
Usage is:
compact ARCHIVENAME FILES...
Where FILES can be file1 file2 or globs like *.html
Try:
if [ "$#" -gt '0' ]; then
/bin/echo "${!#}" "${#:1:$(($# - 1))}
fi
Array without last parameter:
array=${#:1:$#-1}
But it's a bashism :(. Proper solutions would involve shift and adding into variable as others use.
#!/bin/bash
lastidx=$#
lastidx=`expr $lastidx - 1`
eval last='$'{$lastidx}
echo $last
Alternative way to pull the last parameter out of the argument list:
eval last="\$$#"
eval set -- `awk 'BEGIN{for(i=1;i<'$#';i++) printf " \"$%d\"",i;}'`
#!/bin/sh
eval last='$'$#
while test $# -gt 1; do
list="$list $1"
shift
done
echo $list $last
I can't find a way to use array-subscript notation on $#, so this is the best I can do:
#!/bin/bash
args=("$#")
echo "${args[$(($#-1))]}"
This script may work for you - it returns a subrange of the arguments, and can be called from another script.
Examples of it running:
$ args_get_range 2 -2 y a b "c 1" d e f g
'b' 'c 1' 'd' 'e'
$ args_get_range 1 2 n arg1 arg2
arg1 arg2
$ args_get_range 2 -2 y arg1 arg2 arg3 "arg 4" arg5
'arg2' 'arg3'
$ args_get_range 2 -1 y arg1 arg2 arg3 "arg 4" arg5
'arg2' 'arg3' 'arg 4'
# You could use this in another script of course
# by calling it like so, which puts all
# args except the last one into a new variable
# called NEW_ARGS
NEW_ARGS=$(args_get_range 1 -1 y "$#")
args_get_range.sh
#!/usr/bin/env bash
function show_help()
{
IT="
Extracts a range of arguments from passed in args
and returns them quoted or not quoted.
usage: START END QUOTED ARG1 {ARG2} ...
e.g.
# extract args 2-3
$ args_get_range.sh 2 3 n arg1 arg2 arg3
arg2 arg3
# extract all args from 2 to one before the last argument
$ args_get_range.sh 2 -1 n arg1 arg2 arg3 arg4 arg5
arg2 arg3 arg4
# extract all args from 2 to 3, quoting them in the response
$ args_get_range.sh 2 3 y arg1 arg2 arg3 arg4 arg5
'arg2' 'arg3'
# You could use this in another script of course
# by calling it like so, which puts all
# args except the last one into a new variable
# called NEW_ARGS
NEW_ARGS=\$(args_get_range.sh 1 -1 \"\$#\")
"
echo "$IT"
exit
}
if [ "$1" == "help" ]
then
show_help
fi
if [ $# -lt 3 ]
then
show_help
fi
START=$1
END=$2
QUOTED=$3
shift;
shift;
shift;
if [ $# -eq 0 ]
then
echo "Please supply a folder name"
exit;
fi
# If end is a negative, it means relative
# to the last argument.
if [ $END -lt 0 ]
then
END=$(($#+$END))
fi
ARGS=""
COUNT=$(($START-1))
for i in "${#:$START}"
do
COUNT=$((COUNT+1))
if [ "$QUOTED" == "y" ]
then
ARGS="$ARGS '$i'"
else
ARGS="$ARGS $i"
fi
if [ $COUNT -eq $END ]
then
echo $ARGS
exit;
fi
done
echo $ARGS
This works for me, with sh and bash:
last=${*##* }
others=${*%${*##* }}

Resources