How to properly write for loop in Makefile - shell

I went through some posts, but still didn't get how it work.
My request is:
for i in *.json
do
file = `echo $i |cut -d _ -f2`
echo ${file}
# do the rest tasks
done
How to convert above script to target of Makefile?
Here is what I tried
foo:
for i in *.json; do \
$(eval FILE = $(shell echo $$i |cut -d _ -f2)); \
echo $(FILE) ;\
done
But it doesn't work

Using $(eval) or $(shell) is ... not even wrong.
foo:
for i in *.json; do \
file=$$(echo "$$i" |cut -d _ -f2); \
echo "$$file"; \
done
Notice the quoting of the filename variables, and the absence of spaces around the = assignment operator, and the doubling of any dollar sign in order to pass it through from make to the shell.
However, the shell provides a much better mechanism for this;
foo:
for i in *.json; do \
j=$${i#*_}; \
echo "$${j%%_*}"; \
done
or perhaps
foo:
printf '%s\n' *.json \
| sed 's/[^_]*_\([^_]*\)_.*/\1/'
If you only expect a single underscore, both of these can be further simplified.
Or maybe you are just looking for
makefile_variable := $(foreach x,$(wildcard *.json),$(word 2,$(subst _, ,$x)))

Related

Makefile cache creating false positive outcome

I have a make target, which i usually need to run twice to get accurate outcome. I.e the 1st run if accurate thenn on the 2nd run, if the variable is changed, it still displays the previous output, which is wrong, is there a way to get rid of cache or clear it in between.
.PHONY:check-tf-lint
check-tf-lint: configure ## TF Linting
$(eval list_of_dir := $(shell cd ${deployment} && ls -ld */ | awk '{print $$NF}'| grep -v 'test_cases'| sed 's|/||g'))
$(shell touch ${quality-metrics}/formatting.txt)
#for i in aws_bot; do \
make set-tf-version -e infra_module_path=$$i; \
terraform fmt -check -list=false ${deployment}/$$i ; \
if [ "$$?" != "0" ]; then \
echo "Need Formatting in $$i" >> ${quality-metrics}/formatting.txt; \
terraform fmt -check ${deployment}/$$i >> ${quality-metrics}/formatting.txt; \
echo "" >> ${quality-metrics}/formatting.txt; \
fi \
done
$(eval TMP := $(shell (cat ${quality-metrics}/formatting.txt | wc -l)))
echo "${TMP}"
#if [ "$(TMP)" = "0" ]; then \
echo "All Good! No Formatting Needed."; \
else \
echo "Kindly Format Below Mentioned code and check in Again"; \
cat ${quality-metrics}/formatting.txt; \
fi
$(shell rm -rf ${quality-metrics}/formatting.txt)
#if [ "$(TMP)" != "0" ]; then \
exit 1; \
fi
Rule of thumb: you should never use eval or shell functions in a make recipe. If you are doing that it's a pretty sure sign that something has gone wrong somewhere.
In your case the reason you see this behavior is that make will expand ALL variables and functions for all lines in a recipe before the first line in the recipe is invoked. So as far as make is concerned your recipe is handled like this:
.PHONY:check-tf-lint
check-tf-lint: configure ## TF Linting
$(eval list_of_dir := $(shell cd ${deployment} && ls -ld */ | awk '{print $$NF}'| grep -v 'test_cases'| sed 's|/||g'))
$(shell touch ${quality-metrics}/formatting.txt)
$(eval TMP := $(shell (cat ${quality-metrics}/formatting.txt | wc -l)))
$(shell rm -rf ${quality-metrics}/formatting.txt)
#for i in aws_bot; do \
make set-tf-version -e infra_module_path=$$i; \
terraform fmt -check -list=false ${deployment}/$$i ; \
if [ "$$?" != "0" ]; then \
echo "Need Formatting in $$i" >> ${quality-metrics}/formatting.txt; \
terraform fmt -check ${deployment}/$$i >> ${quality-metrics}/formatting.txt; \
echo "" >> ${quality-metrics}/formatting.txt; \
fi \
done
echo "${TMP}"
#if [ "$(TMP)" = "0" ]; then \
echo "All Good! No Formatting Needed."; \
else \
echo "Kindly Format Below Mentioned code and check in Again"; \
cat ${quality-metrics}/formatting.txt; \
fi
#if [ "$(TMP)" != "0" ]; then \
exit 1; \
fi
You should always write your recipes using shell facilities and not make facilities. Set shell variables, don't use eval to set make variables, and run shell commands directly (you're in a recipe after all!) rather than using make's shell function.
You may need to put all the lines in a single script (with semicolon / backslash) to allow this to work. Or consider .ONESHELL but that's a much bigger set of changes.
This worked for me !
.PHONY:check-tf-lint
check-tf-lint: ## TF Linting
$(eval list_of_dir := $(shell cd ${deployment} && ls -ld */ | awk '{print $$NF}'| grep -v 'test_cases'| sed 's|/||g'))
#for i in $(list_of_dir); do \
make set-tf-version -e infra_module_path=$$i; \
terraform fmt -check -list=false ${deployment}/$$i ; \
if [ "$$?" != "0" ]; then \
echo "Need Formatting in $$i" >> ${quality-metrics}/formatting.txt; \
terraform fmt -check ${deployment}/$$i >> ${quality-metrics}/formatting.txt; \
echo "" >> ${quality-metrics}/formatting.txt; \
fi \
done
#if [ -e "${quality-metrics}/formatting.txt" ]; then \
export MNC=`cat ${quality-metrics}/formatting.txt | wc -l`; \
if [ "$${MNC}" = "0" ]; then \
echo "All Good! No Formatting Needed."; \
else \
echo ""; \
echo "Kindly Format Below Mentioned code and check in Again"; \
cat ${quality-metrics}/formatting.txt; \
fi; \
rm -rf ${quality-metrics}/formatting.txt; \
if [ "$${MNC}" != "0" ]; then \
exit 1; \
fi \
fi

Trying to implement a loop in Makefile target

This is my target, where i want to run a loop for each element in the list variable.
The problem is the loop runs but the test variable value is passed as empty
list = mlflow emr
common=$(someDir)/common
.PHONY:build
build:
for var in $(list); do \
cd ${common}; \
test=$(git diff --name-only --diff-filter=AM master | grep ^$(var)/); \
if [ "$(test)" != "" ]; then \
echo "condition met"; \
else \
echo "It is Not Setup"; \
fi \
done
Error:
bash-5.0# sudo make build n=1
for var in mlflow emr; do \
cd /mak/epa-toolchain/common; \
test=; \
if [ "" != "" ]; then \
echo "condition met"; \
else \
echo "It is Not Setup"; \
fi \
done
It is Not Setup
It is Not Setup
The $ is a special character to make: it introduces a make variable reference. So this:
$(git diff --name-only --diff-filter=AM master | grep ^$(var)/)
is not a shell $(...) command, it's a make variable with a very strange name. Wherever you want the shell to see $ you have to escape it as $$:
$$(git diff --name-only --diff-filter=AM master | grep ^$$var/)
(note you have to change $(var) to $$var because the former is a reference to a make variable var, but you are looping in the shell which sets the shell variable var).
Ditto this:
if [ "$(test)" != "" ]; then \
has to be:
if [ "$$test" != "" ]; then \
because test is a shell variable you just assigned, not a make variable.

how to use if statement from a makefile function?

I am reading configurations from .config file and I want to do some operation if a configuration is enabled. I have written following function but it is throwing error message "/bin/sh: 1: Syntax error: ")" unexpected (expecting "then")"
define parse_configs
while read -r file; do \
config=$$(echo $$file | grep -Po '(?<=(CONFIG_)).*(?==)'); \
val=$$(echo $$file | grep -Po '(?<=(=)).*'); \
$$(if $(findstring y, $$val), echo "do Ops", echo "No ops"); \
done < .config;
endef
The problem is with if statement, other part of function is fine. Please let me know the mistake in the code. Thanks.
What is wrong with the statement:
$$(if $(findstring y, $$val), echo "do Ops", echo "No ops");
is that is actually a GNU Make if-function,
calling the GNU Make findstring-function,
which you have written in the middle of a shell statement, and required ($$) that it be expanded by the shell, but it makes no sense to the shell.
It might as well be Javascript. Replace it with an appropriate shell if-statement, e.g.
while read -r file; do \
config=$$(echo $$file | grep -Po '(?<=(CONFIG_)).*(?==)'); \
val=$$(echo $$file | grep -Po '(?<=(=)).*'); \
if [ -z $${val##*"y"*} ]; then echo "do Ops"; else echo "No ops"; fi; \
done < .config;

Makefile: make text file and append strings in it

I'm having difficulties with makefiles.
So in a recipe, I'm making a file (with a name and a .ujc extension) in a for loop and would like to have a text file at the end which contains all the created files. Purpose is to feed it to an application.
For example, in a semi high-level example,
List= [Class1,Class2,Class3]
foreach(Class C in List) {
#do operations on C > outputs a ClassX.ujc file
# add name of file to a text file named "list_of_files"
}
At the end I should have a text file, list_of_files.txt, which contains the following string:
Class1.ujc Class2.ujc Class3.ujc
As a reference, the code I have at the moment (and which does a bit of the stuff above but does not work is) is:
pc: $(APP)
$(foreach C, $(shell echo $(CLASS) | tr ',' ' '), \
make -C BUILDENV CLASS=$(C) BUILD=just_filelist OUTPUT=filelist.txt SKIPSELF=yes && \
../classCvt/classCvt <./Applications/$(C).class> ./Applications/$(C).ujc && \
cat app_file_list.txt | xargs echo ./Applications/$(C).ujc >app_file_list.txt && \
) true
time -p ./$(APP) `cat app_file_list.txt` `cat filelist.txt`
The internal make does make a filelist which is fed to the app, but I'd also like to feed the app_file_list but its construction goes totally wrong.
Probably simple, but I'm not getting there.
Edit:
The code below does what I want:
pc: $(APP)
rm -f cat app_file_list.txt
$(foreach C, $(shell echo $(CLASS) | tr ',' ' '), \
make -C BUILDENV CLASS=$(C) BUILD=just_filelist OUTPUT=filelist.txt SKIPSELF=yes && \
../classCvt/classCvt <./Applications/$(C).class> ./Applications/$(C).ujc && \
cat app_file_list.txt | echo ./Applications/$(C).ujc >>app_file_list.txt && \
) true
time -p ./$(APP) `cat app_file_list.txt` `cat filelist.txt`
Notable mistake I made was the xargs.
(Also in the post)
The solution turned out to be not-so-difficult. I needed to remove the xargs command and do the correct operation (i.e., >> instead of >) in the 'cat app_file_list.txt | etc...' line.
The code below does what I want:
pc: $(APP)
rm -f cat app_file_list.txt
$(foreach C, $(shell echo $(CLASS) | tr ',' ' '), \
make -C BUILDENV CLASS=$(C) BUILD=just_filelist OUTPUT=filelist.txt SKIPSELF=yes && \
../classCvt/classCvt <./Applications/$(C).class> ./Applications/$(C).ujc && \
cat app_file_list.txt | echo ./Applications/$(C).ujc >>app_file_list.txt && \
) true
time -p ./$(APP) `cat app_file_list.txt` `cat filelist.txt`
Notable mistake I made was the xargs which caused strings to repeat into the .txt file.

Add "-I" flag to string of paths in bash

Having a string of space separated paths, relative or absolute, example:
/aaaa/bbbb/ccc /ddas/sdsa/dasd ./dasd/dsd dasd/dsda/dsd dsd/dsad/erer/rerer ../dasd/dsad ../../sdasd/sdsd
How can I process this in bash in order to prepend every one of these paths with -I? Example output should be:
-I/aaaa/bbbb/ccc -I/ddas/sdsa/dasd -I./dasd/dsd dasd/dsda/dsd dsd/dsad/erer/rerer -I../dasd/dsad -I../../sdasd/sdsd
Thanks
Edit for context:
As some of you may already have guessed, the purpose of this is to prepend folder paths with the -I flag for gcc commands.
I'm using this in a makefile. The following (slightly modified from anubhava's suggestion) works perfectly:
#to include subdirectories in source
TEMP := $(shell find $(SOURCE_PATH)* -type d)
TEMP := $(shell echo $(TEMP) | awk 1 ORS=' ')
TEMP := $(shell printf -- "-I%s " ${TEMP} )
ifdef TEMP
INC_PATHS += $(TEMP)
endif
You can use printf:
s='/aaaa/bbbb/ccc /ddas/sdsa/dasd ./dasd/dsd dasd/dsda/dsd dsd/dsad/erer/rerer ../dasd/dsad ../../sdasd/sdsd'
printf -v output -- "-I%s " $s
echo "$output"
-I/aaaa/bbbb/ccc -I/ddas/sdsa/dasd -I./dasd/dsd -Idasd/dsda/dsd -Idsd/dsad/erer/rerer -I../dasd/dsad -I../../sdasd/sdsd
Or if using an array:
arr=(/aaaa/bbbb/ccc /ddas/sdsa/dasd ./dasd/dsd dasd/dsda/dsd dsd/dsad/erer/rerer ../dasd/dsad ../../sdasd/sdsd)
printf -v output -- "-I%s " "${arr[#]}"
If you have the paths in an array:
paths=(/aaaa/bbbb/ccc /ddas/sdsa/dasd ./dasd/dsd dasd/dsda/dsd dsd/dsad/erer/rerer ../dasd/dsad ../../sdasd/sdsd)
then you can use bash's find-and-replace syntax:
includes=("${paths[#]/#/-I}")
You can provide an array as a series of arguments to a command (or function):
compile $the_file "${includes[#]}"
You can do a similar transform on $# (in quotes) in a bash function
with_includes() {
# If you need to do something with the first few arguments,
# collect them here and then call shift:
# the_file=$1; shift
# But you need to check $# to make sure the arguments exist :)
local includes=("{#/#/-I}")
compile $the_file "${includes[#]}"
}
#!/usr/local/bin/bash
echo "/aaaa/bbbb/ccc /ddas/sdsa/dasd ./dasd/dsd dasd/dsda/dsd dsd/dsad/erer/rerer ../dasd/dsad ../../sdasd/sdsd" | \
sed s'/ / -I/'g
I agree with the others that this is bad form, however. Your paths should ideally be on a seperate line.
#!/usr/local/bin/bash
echo "/aaaa/bbbb/ccc /ddas/sdsa/dasd ./dasd/dsd dasd/dsda/dsd \
dsd/dsad/erer/rerer ../dasd/dsad ../../sdasd/sdsd" | \
tr ' ' '\n' > tempfile
let "lines=$(wc -l tempfile | cut -d' ' -f1)"
let "lines=lines+1"
let "counter=1"
function loop() {
[ "${counter}" -lt "${lines}" ] && \
echo "Current loop iteration: ${counter}." && \
sed -n "${counter}p" tempfile | sed s'#^# -I#'g >> tempfile2 && \
let "counter=counter+1" && \
loop
}
loop
[ -f ./tempfile ] && \
echo "File exists: ./tempfile." && \
rm ./tempfile
[ ! -f ./tempfile ] && \
echo "File deleted: ./tempfile."
Then you can just edit the beginning of each line in tempfile2 to do whatever, and run it as a shell script.
Inb4 "Oh God no, he's using test! That could accidentally create an infinite loop! WE'RE ALL GOING TO DIE!"
Infinite loops aren't the end of the world. The worst it will cause is a segfault. Occasionally they can even be useful. OP, you will need to make sure you include "&& \" (without quotes) at the end of every command within a test block though; except for the last one of course.
I learned to do it this way because I've spent time in Sparta OpenBSD, where rm and most other utilities don't have -v flags, and we also aren't afraid of infinite loops. ;)

Resources