preserve whitespace in preprocessor macro value taken from environment variable - objective-c

In an Objective-C project I am attempting to take a file path from an environment variable and set it as a location to write generated files. This is used to run test code using xcodebuild in an automated testing environment where the file path is not determined until xcodebuild is called.
In attempt to do this I am entering a preprocessor macro in the Build Settings that references the variable:
BUILDSERVER_WORKSPACE=\#\"$(WORKSPACE)\"
and then setting the value of a string using that macro
NSString *workspaceLocation = BUILDSERVER_WORKSPACE;
in cases where the (string value of) the path for $WORKSPACE does not contain spaces it works fine but in cases where the path has spaces, the macro preprocessor sees the whitespaces as a macro separator and attempts to process them as separate macro definitions.
for example:
$WORKSPACE=/foo/bar/thudblat
will set the value of workspacelocation as #"/foo/bar/thudblat"
but
$WORKSPACE="/foo/bar/thud blat"
ends up creating multiple preprocessor definitions:
#define BUILDSERVER_WORKSPACE #"/foo/bar/thud
#define blat"
I have attempted to stringify the path, but since the presence or absence of whitespace only happens when i call xcodebuild to build and then run and so I cannot get that to work.
In the end, what I want is to simply take the path at $WORKSPACE and set its value to the NSString *workspaceLocation
so that workspaceLocation could potentially be "\foo\bar\thud blat"

I thought I had tried every scheme of quoting and escaping but, the one thing I had not tried was quoting the entire thing as suggested by #nielsbot
BUILDSERVER_WORKSPACE="\#\"$(WORKSPACE)\""
with an unescaped quote at the beginning and end of the entire value statement. Thad did the trick and gave me the string: #"/foo/bar/thud blat" when calling xcodebuild.

You can achieve that with double stringize trick:
#define STRINGIZE_NX(A) #A
#define STRINGIZE(A) STRINGIZE_NX(A)
static NSString *kWorkspace = #( STRINGIZE(BUILDSERVER_WORKSPACE) );
The way it works is very well explained in here: https://stackoverflow.com/a/2751891/351305

If you wish to really read the environment variable at runtime then you can simply obtain it from NSProcessInfo:
NSString *workspaceLocation = NSProcessInfo.processInfo.environment[#"WORKSPACE"];
That will give you the current value of the environment variables, spaces and all.
HTH

Related

How to launch make while appending a list of values to a variable?

In my Makefile I have defined a variable
VAR = -Ddef1 -Ddef2 -Ddef3
I'd like to launch make like this
make VAR+=-Ddef4 -Ddef5
But the space messes things up. How can I set VAR to a list of values, like it's possible to do inside the Makefile? Thanks for tips.
In your Makefile:
VAR = -Ddef1 -Ddef2 -Ddef3 $(EXTRAVAR)
On the command line:
make EXTRAVAR="-Ddef4 -Ddef5"
As frequently with the shell, tokens with spaces must be quoted. On the command line make FOO+= does the same as make FOO=. It does not append, it overwrites (in your case). Thus the trick with a second variable EXTRAVAR.

Can I create Variable Names from Constants in Objective-C/Swift?

This question is related to Swift and Objective-C.
I want to create variables from Constant Strings. So, in future, when I change name of a variable though out app, I just need to change it at one place, it must be changed, wherever it is used.
Example:
I have user_id in 14 files, if I want to change user_id into userID I have to change in all 14 files, but I want to change at once place only.
One way to do this would be to use the Xcode build process and add a script (language can be of your choice, but the default is a BASH script)
Create string constant text file where you define all your variables you want to change in some format that expresses the change you want to make, for example:
"variable_one_name" = "new_variable_one_name"
Depending on how 'smart' you wanted your script to be you could also list all your variables and include some way of indicating when a variable is not to be replaced.
"variable_one_name" = "new_variable_one_name"
"variable_two_name" = "DO_NOT_CHANGE"
Run a pre build script on you project that reads in the string constant text file and then iterates through your source files and executes the desired replacement. Be careful to limit the directories you search to you OWN source files!
build project...
This would allow you to manage your constants from one place. However it clearly is only going to help you after you have created a project and written some code :)
BASH string replacement
Adding a run script to the Xcode build process

Compilation C, C++, Objective-C et Objective-C++ on Mac

I use CPLUS_INCLUDE_PATH environment variable to compile .cpp files and it works fine to find .h files.
But it's not ok to compile ObjC or ObjC++.
Do you know the good one variable.
Thanks.
From the manual page:
CPATH
C_INCLUDE_PATH
CPLUS_INCLUDE_PATH
OBJC_INCLUDE_PATH
Each variable’s value is a list of directories separated by a
special character, much like PATH, in which to look for header
files. The special character, "PATH_SEPARATOR", is target‐
dependent and determined at GCC build time. For Microsoft Windows‐
based targets it is a semicolon, and for almost all other targets
it is a colon.
CPATH specifies a list of directories to be searched as if
specified with −I, but after any paths given with −I options on the
command line. This environment variable is used regardless of
which language is being preprocessed.
The remaining environment variables apply only when preprocessing
the particular language indicated. Each specifies a list of
directories to be searched as if specified with −isystem, but after
any paths given with −isystem options on the command line.
In all these variables, an empty element instructs the compiler to
search its current working directory. Empty elements can appear at
the beginning or end of a path. For instance, if the value of
CPATH is ":/special/include", that has the same effect as
−I. −I/special/include.

Best practice using NSLocalizedString

I'm (like all others) using NSLocalizedStringto localize my app.
Unfortunately, there are several "drawbacks" (not necessarily the fault of NSLocalizedString itself), including
No autocompletition for strings in Xcode. This makes working not only error-prone but also tiresome.
You might end up redefining a string simply because you didn't know an equivalent string already existed (i.e. "Please enter password" vs. "Enter password first")
Similarily to the autocompletion-issue, you need to "remember"/copypaste the comment strings, or else genstring will end up with multiple comments for one string
If you want to use genstring after you've already localized some strings, you have to be careful to not lose your old localizations.
Same strings are scattered througout your whole project. For example, you used NSLocalizedString(#"Abort", #"Cancel action") everywhere, and then Code Review asks you to rename the string to NSLocalizedString(#"Cancel", #"Cancel action") to make the code more consistent.
What I do (and after some searches on SO I figured many people do this) is to have a seperate strings.h file where I #define all the localize-code. For example
// In strings.h
#define NSLS_COMMON_CANCEL NSLocalizedString(#"Cancel", nil)
// Somewhere else
NSLog(#"%#", NSLS_COMMON_CANCEL);
This essentially provides code-completion, a single place to change variable names (so no need for genstring anymore), and an unique keyword to auto-refactor. However, this comes at the cost of ending up with a whole bunch of #define statements that are not inherently structured (i.e. like LocString.Common.Cancel or something like that).
So, while this works somewhat fine, I was wondering how you guys do it in your projects. Are there other approaches to simplify the use of NSLocalizedString? Is there maybe even a framework that encapsulates it?
NSLocalizedString has a few limitations, but it is so central to Cocoa that it's unreasonable to write custom code to handle localization, meaning you will have to use it. That said, a little tooling can help, here is how I proceed:
Updating the strings file
genstrings overwrites your string files, discarding all your previous translations.
I wrote update_strings.py to parse the old strings file, run genstrings and fill in the blanks so that you don't have to manually restore your existing translations.
The script tries to match the existing string files as closely as possible to avoid having too big a diff when updating them.
Naming your strings
If you use NSLocalizedString as advertised:
NSLocalizedString(#"Cancel or continue?", #"Cancel notice message when a download takes too long to proceed");
You may end up defining the same string in another part of your code, which may conflict as the same english term may have different meaning in different contexts (OK and Cancel come to mind).
That is why I always use a meaningless all-caps string with a module-specific prefix, and a very precise description:
NSLocalizedString(#"DOWNLOAD_CANCEL_OR_CONTINUE", #"Cancel notice window title when a download takes too long to proceed");
Using the same string in different places
If you use the same string multiple times, you can either use a macro as you did, or cache it as an instance variable in your view controller or your data source.
This way you won't have to repeat the description which may get stale and get inconsistent among instances of the same localization, which is always confusing.
As instance variables are symbols, you will be able to use auto-completion on these most common translations, and use "manual" strings for the specific ones, which would only occur once anyway.
I hope you'll be more productive with Cocoa localization with these tips!
As for autocompletition for strings in Xcode, you could try https://github.com/questbeat/Lin.
Agree with ndfred, but I would like to add this:
Second parameter can be use as ... default value!!
(NSLocalizedStringWithDefaultValue does not work properly with genstring, that's why I proposed this solution)
Here is my Custom implementation that use NSLocalizedString that use comment as default value:
1 . In your pre compiled header (.pch file) , redefine the 'NSLocalizedString' macro:
// cutom NSLocalizedString that use macro comment as default value
#import "LocalizationHandlerUtil.h"
#undef NSLocalizedString
#define NSLocalizedString(key,_comment) [[LocalizationHandlerUtil singleton] localizedString:key comment:_comment]
2. create a class to implement the localization handler
#import "LocalizationHandlerUtil.h"
#implementation LocalizationHandlerUtil
static LocalizationHandlerUtil * singleton = nil;
+ (LocalizationHandlerUtil *)singleton
{
return singleton;
}
__attribute__((constructor))
static void staticInit_singleton()
{
singleton = [[LocalizationHandlerUtil alloc] init];
}
- (NSString *)localizedString:(NSString *)key comment:(NSString *)comment
{
// default localized string loading
NSString * localizedString = [[NSBundle mainBundle] localizedStringForKey:key value:key table:nil];
// if (value == key) and comment is not nil -> returns comment
if([localizedString isEqualToString:key] && comment !=nil)
return comment;
return localizedString;
}
#end
3. Use it!
Make sure you add a Run script in your App Build Phases so you Localizable.strings file will be updated at each build, i.e., new localized string will be added in your Localized.strings file:
My build phase Script is a shell script:
Shell: /bin/sh
Shell script content: find . -name \*.m | xargs genstrings -o MyClassesFolder
So when you add this new line in your code:
self.title = NSLocalizedString(#"view_settings_title", #"Settings");
Then perform a build, your ./Localizable.scripts file will contain this new line:
/* Settings */
"view_settings_title" = "view_settings_title";
And since key == value for 'view_settings_title', the custom LocalizedStringHandler will returns the comment, i.e. 'Settings"
Voilà :-)
In Swift I'm using the following, e.g. for button "Yes" in this case:
NSLocalizedString("btn_yes", value: "Yes", comment: "Yes button")
Note usage of the value: for the default text value. The first parameter serves as the translation ID. The advantage of using the value: parameter is that the default text can be changed later but the translation ID remains the same. The Localizable.strings file will contain "btn_yes" = "Yes";
If the value: parameter was not used then the first parameter would be used for both: for the translation ID and also for the default text value. The Localizable.strings file would contain "Yes" = "Yes";. This kind of managing localization files seems to be strange. Especially if the translated text is long then the ID is long as well. Whenever any character of the default text value is changed, then the translation ID gets changed as well. This leads to issues when external translation systems are used. Changing of the translation ID is understood as adding new translation text, which may not be always desired.
I wrote a script to help maintaining Localizable.strings in multiple languages. While it doesn't help in autocompletion it helps to merge .strings files using command:
merge_strings.rb ja.lproj/Localizable.strings en.lproj/Localizable.strings
For more info see:
https://github.com/hiroshi/merge_strings
Some of you find it useful I hope.
If anyone looking for a Swift solution. You may want to check out my solution I put together here: SwiftyLocalization
With few steps to setup, you will have a very flexible localization in Google Spreadsheet (comment, custom color, highlight, font, multiple sheets, and more).
In short, steps are: Google Spreadsheet --> CSV files --> Localizable.strings
Moreover, it also generates Localizables.swift, a struct that acts like interfaces to a key retrieval & decoding for you (You have to manually specify a way to decode String from key though).
Why is this great?
You no longer need have a key as a plain string all over the places.
Wrong keys are detected at compile time.
Xcode can do autocomplete.
While there're tools that can autocomplete your localizable key. Reference to a real variable will ensure that it's always a valid key, else it won't compile.
// It's defined as computed static var, so it's up-to-date every time you call.
// You can also have your custom retrieval method there.
button.setTitle(Localizables.login.button_title_login, forState: .Normal)
The project uses Google App Script to convert Sheets --> CSV , and Python script to convert CSV files --> Localizable.strings You can have a quick look at this example sheet to know what's possible.
with iOS 7 & Xcode 5, you should avoid using the 'Localization.strings' method, and use the new 'base localisation' method. There are some tutorials around if you google for 'base localization'
Apple doc : Base localization
#define PBLocalizedString(key, val) \
[[NSBundle mainBundle] localizedStringForKey:(key) value:(val) table:nil]
Myself, I'm often carried away with coding, forgetting to put the entries into .strings files. Thus I have helper scripts to find what do I owe to put back into .strings files and translate.
As I use my own macro over NSLocalizedString, please review and update the script before using as I assumed for simplicity that nil is used as a second param to NSLocalizedString. The part you'd want to change is
NSLocalizedString\(#(".*?")\s*,\s*nil\)
Just replace it with something that matches your macro and NSLocalizedString usage.
Here comes the script, you only need Part 3 indeed. The rest is to see easier where it all comes from:
// Part 1. Get keys from one of the Localizable.strings
perl -ne 'print "$1\n" if /^\s*(".+")\s*=/' myapp/fr.lproj/Localizable.strings
// Part 2. Get keys from the source code
grep -n -h -Eo -r 'NSLocalizedString\(#(".*?")\s*,\s*nil\)' ./ | perl -ne 'print "$1\n" if /NSLocalizedString\(#(".+")\s*,\s*nil\)/'
// Part 3. Get Part 1 and 2 together.
comm -2 -3 <(grep -n -h -Eo -r 'NSLocalizedString\(#(".*?")\s*,\s*nil\)' ./ | perl -ne 'print "$1\n" if /NSLocalizedString\(#(".+")\s*,\s*nil\)/' | sort | uniq) <(perl -ne 'print "$1\n" if /^\s*(".+")\s*=/' myapp/fr.lproj/Localizable.strings | sort) | uniq >> fr-localization-delta.txt
The output file contains keys that were found in the code, but not in the Localizable.strings file. Here is a sample:
"MPH"
"Map Direction"
"Max duration of a detailed recording, hours"
"Moving ..."
"My Track"
"New Trip"
Certainly can be polished more, but thought I'd share.

How to create a C #define for a certain target using CMake?

I feel a little stupid right now. After recently converting a few smaller projects to use CMake, I decided to also get rid of a few "Platform_Config.h" files. These files contain a few preprocessing directives like #define USE_NEW_CACHE and control compilation.
How would I 'convert' these defines to be controlled with CMake? Ideally by using these "cache" variables the user can easily edit.
There are two options. You can use the add_definitions method to pass defines as compiler flags: E.g. somewhere in your projects cmakelists.txt:
add_definitions( -DUSE_NEW_CACHE )
CMake will make sure the -D prefix is converted to the right flag for your compiler (/D for msvc and -D for gcc).
Alternatively, check out configure_file. It is more complex, but may be better suited to your original approach with a Platform_Config file.
You can create an input-file, similar to your original Platform_Config.h and add "#cmakedefine" lines to it.
Let's call in Platform_Config.h.in:
// In Platform_Config.h.in
#cmakedefine USE_NEW_CACHE
// end of Platform_Config.h.in
When then running
configure_file( ${CMAKE_SOURCE_DIR}/Platform_Config.h.in ${CMAKE_BINARY_DIR}/common/Platform_Config.h )
it will generate a new Platform_Config file in your build-dir. Those variables in cmake which are also a cmakedefine will be present in the generated file, the other ones will be commented out or undefed.
Of course, you should make sure the actual, generated file is then correctly found when including it in your source files.
option command might provide what you are looking for.
use it with the COMPILE DEFINITIONS property on the target and i think you are done.
To set the property on the target, use the command set target properties
option(DEBUGPRINTS "Prints a lot of debug prints")
target(myProgram ...)
if(DEBUGPRINTS)
set_target_properties(myProgram PROPERTIES COMPILE_DEFINITIONS "DEBUGPRINTS=1")
endif()
edit:
The option i wrote in the example shows up as a checkbox in the CMake GUI.
In case you want to set defines per target: Since 2.8.11 you can use target_compile_definitions.
In earlier versions you probably don't want to use set_target_properties as is, since it overwrites any defines you set previously. Call get_target_property first instead, then merge with previous values. See add_target_definitions here.
Use target_compile_options. Do not quote your define or it not be detected as a define. CMake parses off the /define and adds the actual define to the DefineConstants section of the csproj, if there are quotes it will put the entire quoted string in the AdditionalOptions section of the csproj.
An example from one of my projects that uses generator expressions:
target_compile_options( ${LIBRARY_NAME} PRIVATE
$<${IS_ART_ITERATION_BUILD}:/define:ART_ITERATION_BUILD>
)
An example without generator expressions:
target_compile_options( ${LIBRARY_NAME} PRIVATE
/define:GRAPHICS_VULKAN
)