Bash run command according to environment variable - bash

See I wanna copy a file to a destination: cp filename /home/example/temp.txt.
The question the filename will be changed by some programes, and the new name of it will be written in file /home/example/.env.
What I want is alias something like alias cpf=cp ${filename} /home/nope/temp.txt to .bashrc, then what I need is only run cpf if I want to copy the latest finename to /home/example/temp.txt.
What I have tried:
eval $(grep -v "^#" "/home/example/.env") cp ${filename} /home/nope/temp.txt
and faild to get ${filename}.
Is there some changes to make what I tried work?

Example .env:
key1='do not put me in the environment'
key2=1231
filename=thisvaluechanges
key4="I hate being evaluated"
You only want to evaluate the line with filename. First test how you can select that line, something like
grep "^filename=" /home/example/.env
# or
sed -n 's/^\s*filename\s*=\s*/filename=/p' /home/example/.env
Next you can source the selected line.
source <(grep "^filename=" /home/example/.env)
When the filename is a fixed string (without $() that needs to be evaluated), you can do without source:
cp $(grep "^filename=" /home/example/.env) /home/nope/temp.txt
Before putting this in an alias, remember that a function can do everything an alias can, and can do more. You "should" stop using alias.
When you have three or four files like filename1, 2, 3, 4, you can use a function with an argument:
cpf() {
if (( $# = 0 )); then
echo "Usage: cpf filenumber"
else
cp $(grep "^filename${1}=" /home/example/.env) /home/nope/temp.txt
fi
}
You can call the function with cpf 2 for filename2.
When you want to put the filename in the environment, you can change the function
source <(grep "^filename${1}=" /home/example/.env)

My guess is that assuming /home/example/.env contains:
#!/bin/bash
# bash sourcable file
filename=$(echo 123)
then you want:
#!/bin/bash
cpf() {
(
. /home/example/.env
cp "$filename" /home/nope/temp.txt
)
}
Notes:
eval is evil. Your use of eval $(grep...) is very dangerous.
Always remember to qoute your expansions.

Related

How can I get the first extension of a filename in a shell script?

Is there an inline command which can be used to get the first extension of a file ?
I use the following command to get the latest:
FILE="filename.tar.bz2"
EXT="${FILE##*.}"
echo "EXT = ${EXT}"
which returns
EXT = bz2
Is there a similar command to isolate "tar" only ?
var="config/filename.tar.bz2"
ext=$(basename "$var") # extract filename only
ext=${ext#*.} # remove everything in front the first dot
ext=${ext%%.*} # remove everything after a dot
echo "$ext"
Note: uppercase variables by convention are used for exported variables like COLUMNS, LINES UID PWD TERM etc. Prefer using lowercase variables in your scripts.
Expanding on good answer from #KamilCuk, using only POSIX shell grammar and no external command sub-shell call:
#!/usr/bin/env sh
filepath="/path/to/config/filename.tar.bz2.bak"
echo 'filepath:' "$filepath"
# remove everything upto and including last /
filename="${filepath##*/}"
echo 'filename:' "$filename"
# remove everything until and including first dot
all_extensions="${filename#*.}"
echo 'all_extensions:' "$all_extensions"
# remove everything from and including first dot
first_extension="${all_extensions%%.*}"
echo 'First extension:' "$first_extension"
last_extension="${all_extensions##*.}"
echo 'Last extension:' "$last_extension"
# Fill argument array with extensions
IFS='.'; set -- $all_extensions
if [ $# -gt 0 ]
then
ext_num=1
printf '\nIterating all %d extensions:\n' $#
printf '%s\t%s\n' 'ext#' 'extension'
for extension
do
printf '%4d\t%s\n' "$ext_num" "$extension"
ext_num="$((ext_num+1))"
done
fi
Output:
filepath: /path/to/config/filename.tar.bz2.bak
filename: filename.tar.bz2.bak
all_extensions: tar.bz2.bak
First extension: tar
Last extension: bak
Iterating all 3 extensions:
ext# extension
1 tar
2 bz2
3 bak
$ filename="filename.tar.bz2"
$ echo ${filename##*.}
ext
$ file_ext=${filename##*.} #put to variable
$ echo ${file_ext}
ext
echo "filename.tar.gz" | awk -F "." '{print $2}'
This outputs the first instance of a string between periods. In this case, 'tar'.

Parse ${} placeholder into absolute path in shell script

I have a app.properties file something like below
Base.dir="/user/test/application"
Result.dir="${base.dir}/result"
and i've create bash script to parse above properties
function readConfigFile()
{
(grep -E "^${2}=" -m 1 "${1}" 2>/dev/null || echo "VAR=__UNDEFINED__") | head -n 1 | cut -d '=' -f 2-;
}
function setConfigFile()
{
sourceFile=${1}
}
function configGet()
{
if [ ! -z $sourceFile ]; then
val="$(readConfigFile $sourceFile "${1}")";
if [ "${val}" = "__UNDEFINED__" ]; then
echo "${1} value not exist"
# return empty string
printf -- "%s" "";
fi
printf -- "%s" "${val}";
else
echo "config file not exist"
# return empty string
printf -- "%s" "";
fi
}
and the way i call above parser is something like below
$Result_dir=$(configGet Result.dir)
however, i cant really translate placeholder ${} into base_dir
and i got following error
ls $Result_dir
ls: cannot access ${Base_dir}/result: No such file or directory
Is there any way that i can translate ${Base.dir} into /user/test/application?
I guess you're not going to be able to substitute ${base.dir} (btw shouldn't it be ${Base.dir}?) the way you were hoping mainly because, as far as I know, dots are not allowed in variable names in bash.
What you could do is manually substitute the ${base.dir} part with the corresponding path using bash's substitution syntax. For example:
setConfigFile 'app.properties'
Result_dir_raw=$(configGet Result.dir)
Result_dir=${Result_dir_raw/'${base.dir}'/$(configGet Base.dir)}
echo ${Result_dir}
I say "manually" because you still specify in your source code that the pattern you want to replace is ${base.dir} which I'm guessing isn't what you wanted.
Now if you run this you'll see that the ${Result_dir} variable evaluates to ""/user/test/application"/result" which obviously isn't a path, and this is because you're surrounding the paths in app.properties with double quotes, so you either need to get rid of them in your readConfigFile function or lose them altogether in your config file, which to me makes more sense.
Why have you a . in your variable name, which is not allowed in bash:
$ Base.dir="/user/test/application"
-bash: Base.dir=/user/test/application: No such file or directory
$ Base_dir="/user/test/application"
$
So, why do you get No such file or directory? Here is an explanation:
Create a file called Base.dir=gash.sh, yes, that's a legal filename
$ echo 'echo Hello World' > Base.dir=gash.sh
Make the file executable:
$ PATH=$PATH:.
$ chmod u+x Base.dir=gash.sh
Now type the command:
$ Base.dir="gash.sh"
Hello World
Use an underscore, not a dot. By the Way, ksh Korn shell not only allows the dot, it has a special meaning, it is a compound variable.

Making a script that transforms sentences to title case?

I have a command that I use to transform sentences to title case. It is inefficient to have to copy this command out of a text file, and then paste it into the terminal before then also pasting in the sentence I want converted. The command is:
echo "my text" | sed 's/.*/\L&/; s/[a-z]*/\u&/g'
How can I convert this to a script so I can just call something like the following from the terminal:
TitleCaseConverter "my text"
Is it possible to create such a script? Is it possible to make it work from any folder location?
Since bash's parameter expansion includes case modification, there's no need for sed. Just a short function:
tc() { set ${*,,} ; echo ${*^} ; }
Test (don't use quotes, since a title is typically no longer than a sentence, it shouldn't matter):
tc FOO bar
Output:
Foo Bar
Fancy version that avoids capitalizing some conjunctions, articles and such:
ftc() { set ${*,,} ; set ${*^} ; echo -n "$1 " ; shift 1 ; \
for f in ${*} ; do \
case $f in A|The|Is|Of|And|Or|But|About|To|In|By) \
echo -n "${f,,} " ;; \
*) echo -n "$f " ;; \
esac ; \
done ; echo ; }
Test:
ftc the last of the mohicans
Output:
The Last of the Mohicans
How about just wrapping it into a function in .bashrc or .bash_profile and source it from the current shell
TitleCaseConverter() {
sed 's/.*/\L&/; s/[a-z]*/\u&/g' <<<"$1"
}
or) if you want it pitch-perfect to avoid any sorts of trailing new lines from the input arguments do
printf "%s" "$1" | sed 's/.*/\L&/; s/[a-z]*/\u&/g'
Now you can source the file once from the command line to make the function available, do
source ~/.bash_profile
Now you can use it in the command line directly as
str="my text"
newstr="$(TitleCaseConverter "$str")"
printf "%s\n" "$newstr"
My Text
Also to your question,
How can I convert this to a script so I can just call something like the following from the terminal
Adding the function to one of the start-up files takes care of that, recommend adding it to .bash_profile more though.
TitleCaseConverter "this is stackoverflow"
This Is Stackoverflow
Update:
OP was trying to create a directory with the name returned from the function call, something like below
mkdir "$(TitleCaseConverter "this is stackoverflow")"
The key again here is to double-quote the command-substitution to avoid undergoing word-splitting by shell.
I don't have comment privs, but slight improvement to ManUnitedBloke's answer, this will handle contractions like "don't" and "who's".
echo "my text" | sed 's/.*/\L&/; s/[a-z']*/\u&/g'

ls $FOLDER_PATH with space in $FOLDER_PATH: No such file or directory

I am trying to get the filename in a folder with only one file in it.
FYI: The $FOLDER_TMP contains a space in it, that is why I use printf
function nameofkeyfile(){
FOLDER_TMP="${PWD%/*/*}/folder/"
FOLDER=$(printf %q "${FOLDER_TMP}")
FILENAME=ls "$FOLDER" # Error: No such file or directory
# or this: FILENAME=$(ls "$FOLDER") # Error: No such file or directory
FNAME=`basename $FILENAME`
}
The problem is the line:
FILENAME=ls "$FOLDER" # Error: No such file or directory
Do you know why - and yes the folder is there?
And if I echo the $FOLDER it gives me the right folder.
I am trying to get the filename in a folder with only one file in it.
You definitely have the wrong approach.
Instead, consider using globbing like so:
The assignment
fname=( "${PWD%/*/*}"/folder/* )
will populate the array fname will the expansion of the given glob: that is, all files in the directory "${PWD%/*/*}"/folder/, if any. If there are no files at all, your array will contain the glob, verbatim.
Hence, a more robust approach is the following:
nameofkeyfile() {
fname=( "${PWD%/*/*}"/folder/* )
# Now check that there's at most one element in the array
if (( ${#fname[#]} > 1 )); then
echo "Oh no, there are too many files in your folder"
return 1
fi
# Now check that there is a file
if [[ ! -f ${fname[0]} ]]; then
echo "Oh no, there are no files in your folder"
return 1
fi
# Here, all is good!
echo "Your file is: $fname"
}
This uses Bash (named) arrays. If you want the function to be POSIX-compliant, it's rather straightforward since POSIX shells have an unnamed array (the positional parameters):
# POSIX-compliant version
nameofkeyfile() {
set -- "${PWD%/*/*}"/folder/*
# Now check that there's at most one element in the array
if [ "$#" -gt 1 ]; then
echo "Oh no, there are too many files in your folder"
return 1
fi
# Now check that there is a file
if [ ! -f "$1" ]; then
echo "Oh no, there are no files in your folder"
return 1
fi
# Here, all is good!
echo "Your file is: $1, I'll store it in variable fname for you"
fname=$1
}
I didn't strip the full path from the filename, but that's really easy (don't use basename for that!):1
fname=${fname##*/}
More precisely: in the Bash version, you'd use:
fname=${fname[0]##*/}
and in the POSIX version you'd use:
fname=${1##*/}
1there's a catch when using parameter expansions to get the basename, it's the case of /. But it seems you won't be in this case, so it's all safe!
To store the output ls "$FOLDER" in a variable, put it in a sub-shell:
FILENAME=$(ls "$FOLDER")
Another problem is the printf.
It adds escaping backslashes in the string,
and when you try to list the directory in the next step,
those backslashes are used literally by the shell.
So drop the printf:
function nameofkeyfile() {
FOLDER="${PWD%/*/*}/folder/"
FILENAME=$(ls "$FOLDER")
FNAME=$(basename $FILENAME)
}
Lastly, it's better to use $(...) than `...`:

Quoting parameters with spaces for later execution

I have this (test) script:
#!/bin/bash
my_cmd_bad_ ( ) {
cmd="$#"
$cmd
}
my_cmd_good_ ( ) {
"$#"
}
my_cmd_bad_ ls -l "file with space"
my_cmd_good_ ls -l "file with space"
The output is (the file does not exist, which is not the point of this question):
ยป ~/test.sh
ls: cannot access file: No such file or directory
ls: cannot access with: No such file or directory
ls: cannot access space: No such file or directory
ls: cannot access file with space: No such file or directory
I am surprised that the first version does not work as expected: the parameter is not quoted, and instead of processing one file, it processes three. Why?
How can I save the command that I want to execute, properly quoted? I need to execute it later, where I do not have "$#" anymore.
A simple rework of this test script would be appreciated.
See similar question: How to pass command line parameters with quotes stored in single variable?
Use those utility functions ho save a command to a string for later execution:
bash_escape() {
# backtick indirection strictly necessary here: we use it to strip the
# trailing newline from sed's output, which Solaris/BSD sed *always* output
# (unlike GNU sed, which outputs "test": printf %s test | sed -e s/dummy//)
out=`echo "$1" | sed -e s/\\'/\\''\\\\'\\'\\'/g`
printf \'%s\' "$out"
}
append_bash_escape() {
printf "%s " "$1"
bash_escape "$2"
}
your_cmd_fixed_ ( ) {
cmd="$#"
while [ $# -gt 0 ] ; do
cmd=`append_bash_escape "$cmd" "$1"` ; shift
done
$cmd
}
You can quote any single parameter and evaluate it later:
my_cmd_bad_ ( ) {
j=0
for i in "$#"; do
cmd["$j"]=\"$"$i"\"
j=$(( $j + 1 ))
done;
eval ${cmd[*]}
}
You are combining three space-delimited strings "ls", "-l", and "file with space" into a single space-delimited string cmd. There's no way to know which spaces were originally quoted (in "file with space") and which spaces were introduced during the assignment to cmd.
Typically, it is not a good idea to try to build up command lines into a single string. Use functions, or isolate the actual command and leave the arguments in $#.
Rewrite the command like this:
my_cmd_bad_ () {
cmd=$1; shift
$cmd "$#"
}
See http://mywiki.wooledge.org/BashFAQ/050
Note that your second version is greatly preferred most of the time. The only exceptions are if you need to do something special. For example, you can't bundle an assignment or redirect or compound command into a parameter list.
The correct way to handle the quoting issue requires non-standard features. Semi-realistic example involving a template:
function myWrapper {
typeset x IFS=$' \t\n'
{ eval "$(</dev/fd/0)"; } <<-EOF
for x in $(printf '%q ' "$#"); do
echo "\$x"
done
EOF
}
myWrapper 'foo bar' $'baz\nbork'
Make sure you understand exactly what's going on here and that you really have a good reason for doing this. It requires ensuring side-effects can't affect the arguments. This specific example doesn't demonstrate a very good use case because everything is hard-coded so you're able to correctly escape things in advance and expand the arguments quoted if you wanted.

Resources